Files
ctbjrj/会员服务器/server.cjs
T

244 lines
11 KiB
JavaScript

// 会员系统服务器(零依赖纯 node http,照 cadbt server.cjs 模式)
// · 端口 12701,只绑 127.0.0.1(外部访问不到)
// · 数据存 huiyuan.json(本目录): { "会员": [ {手机号,密码,会员类型,有效期,注册时间,备注} ] }
// · API:
// POST /api/login {手机号,密码} → {ok,会员类型,有效期} | {ok:false,msg}
// POST /api/register {手机号,密码} → {ok} 注册为"试用会员",有效期=今天+30天
// GET /api/member?手机号=xxx → {ok,手机号,会员类型,有效期}
// POST /api/changepwd {手机号,旧密码,新密码} → {ok}
// GET /api/list → {ok,会员:[...]}(管理页用)
// POST /api/save {会员:[...]} → {ok}(管理页整表保存)
// · 项目云 API(2026-09-16,数据落 项目云\<手机号安全化>\<项目编号安全化>\项目.zip):
// POST /api/proj/upload body=JSON头 + \n\n + zip二进制 → {ok,编号,大小,时间}(覆盖式)
// GET /api/proj/list?手机号= → {ok,项目:[{编号,名称,时间,大小}]}
// GET /api/proj/download?手机号=&编号= → zip 二进制流
// POST /api/proj/delete {手机号,编号} → {ok}
// 上传前校验会员已注册(huiyuan.json);手机号/编号文件名安全化+防路径穿越;
// 元数据存各项目目录 meta.json(项目名/上传时间/大小)。
// · 管理页: GET / 或 /admin.html → www/admin.html
var http = require('http');
var fs = require('fs');
var path = require('path');
// · 监听地址/端口可用环境变量覆盖(Windows 本地默认 127.0.0.1:12701;
// Linux 服务器部署用: MEMBER_HOST=0.0.0.0 MEMBER_PORT=12701 node server.cjs)
var PORT = parseInt(process.env.MEMBER_PORT || '12701', 10);
var HOST = process.env.MEMBER_HOST || '127.0.0.1';
var ROOT = __dirname;
var DB_FILE = path.join(ROOT, 'huiyuan.json');
var WWW = path.join(ROOT, 'www');
var MIME = {
'.html': 'text/html; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.ico': 'image/x-icon'
};
// ---- 数据读写(原子写:临时文件+rename) ----
function 读库() {
try {
var d = JSON.parse(fs.readFileSync(DB_FILE, 'utf8'));
if (!Array.isArray(d['会员'])) d['会员'] = [];
return d;
} catch (e) { return { '会员': [] }; }
}
function 写库(data) {
var tmp = DB_FILE + '.tmp';
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf8');
fs.renameSync(tmp, DB_FILE);
}
function 今天加天数(n) {
var d = new Date(Date.now() + n * 86400000);
return d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2);
}
// ---- 业务 ----
function 登录(b) {
var db = 读库();
for (var i = 0; i < db['会员'].length; i++) {
var m = db['会员'][i];
if (m['手机号'] === b['手机号']) {
if (m['密码'] !== b['密码']) return { ok: false, msg: '密码错误' };
return { ok: true, 会员类型: m['会员类型'], 有效期: m['有效期'] };
}
}
return { ok: false, msg: '该手机号未注册' };
}
function 注册(b) {
if (!b['手机号'] || !b['密码']) return { ok: false, msg: '手机号和密码不能为空' };
var db = 读库();
for (var i = 0; i < db['会员'].length; i++)
if (db['会员'][i]['手机号'] === b['手机号']) return { ok: false, msg: '该手机号已注册' };
db['会员'].push({
'手机号': b['手机号'], '密码': b['密码'],
'会员类型': '试用会员', '有效期': 今天加天数(30),
'注册时间': 今天加天数(0), '备注': ''
});
写库(db);
return { ok: true, msg: '注册成功' };
}
function 查会员(b) {
var db = 读库();
for (var i = 0; i < db['会员'].length; i++) {
var m = db['会员'][i];
if (m['手机号'] === b['手机号'])
return { ok: true, 手机号: m['手机号'], 会员类型: m['会员类型'], 有效期: m['有效期'] };
}
return { ok: false, msg: '该手机号未注册' };
}
function 改密(b) {
var db = 读库();
for (var i = 0; i < db['会员'].length; i++) {
var m = db['会员'][i];
if (m['手机号'] === b['手机号']) {
if (m['密码'] !== b['旧密码']) return { ok: false, msg: '旧密码错误' };
if (!b['新密码']) return { ok: false, msg: '新密码不能为空' };
m['密码'] = b['新密码'];
写库(db);
return { ok: true };
}
}
return { ok: false, msg: '该手机号未注册' };
}
// ---- 项目云(2026-09-16):数据落 项目云\<手机号安全化>\<编号安全化>\{项目.zip,meta.json} ----
var PROJ_ROOT = path.join(ROOT, '项目云');
function 安全名(s) { // 文件名安全化:去掉路径分隔/非法字符,限长 80
var t = String(s || '').replace(/[\\\/:*?"<>|]/g, '_').replace(/\.\./g, '_').trim();
if (t.length > 80) t = t.slice(0, 80);
return t;
}
function 会员存在(手机号) {
var db = 读库();
for (var i = 0; i < db['会员'].length; i++)
if (db['会员'][i]['手机号'] === 手机号) return true;
return false;
}
function 项目目录(手机号, 编号) { // 防穿越:拼好后必须仍在 项目云 下
var d = path.join(PROJ_ROOT, 安全名(手机号), 安全名(编号));
return d.indexOf(PROJ_ROOT) === 0 ? d : null;
}
function 列项目(手机号) {
if (!手机号 || !会员存在(手机号)) return { ok: false, msg: '该手机号未注册' };
var dir = 项目目录(手机号, '');
var 出 = [];
try {
fs.readdirSync(dir).forEach(function (编号) {
var p = path.join(dir, 编号);
try {
var meta = JSON.parse(fs.readFileSync(path.join(p, 'meta.json'), 'utf8'));
出.push({ 编号: meta['编号'] || 编号, 名称: meta['名称'] || '', 时间: meta['时间'] || '', 大小: meta['大小'] || 0 });
} catch (e) { }
});
} catch (e) { } // 目录不存在=无项目
return { ok: true, '项目': 出 };
}
// ---- HTTP ----
function sendJson(res, obj) {
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify(obj));
}
function readBody(req, cb) {
var chunks = [];
req.on('data', function (c) { chunks.push(c); });
req.on('end', function () {
try { cb(null, JSON.parse(Buffer.concat(chunks).toString('utf8'))); }
catch (e) { cb(e, null); }
});
}
var server = http.createServer(function (req, res) {
var urlPath = decodeURIComponent(req.url.split('?')[0]);
var query = {};
(req.url.split('?')[1] || '').split('&').forEach(function (kv) {
var p = kv.split('=');
if (p[0]) query[decodeURIComponent(p[0])] = decodeURIComponent((p[1] || '').replace(/\+/g, ' '));
});
// API 路由
if (urlPath === '/api/login') return readBody(req, function (e, b) { sendJson(res, e ? { ok: false, msg: '参数错误' } : 登录(b || {})); });
if (urlPath === '/api/register') return readBody(req, function (e, b) { sendJson(res, e ? { ok: false, msg: '参数错误' } : 注册(b || {})); });
if (urlPath === '/api/member') return sendJson(res, 查会员(query));
if (urlPath === '/api/changepwd') return readBody(req, function (e, b) { sendJson(res, e ? { ok: false, msg: '参数错误' } : 改密(b || {})); });
if (urlPath === '/api/list') {
var db = 读库();
return sendJson(res, { ok: true, '会员': db['会员'] });
}
if (urlPath === '/api/save') return readBody(req, function (e, b) {
if (e || !b || !Array.isArray(b['会员'])) return sendJson(res, { ok: false, msg: '参数错误' });
写库(b);
return sendJson(res, { ok: true });
});
// ---- 项目云路由 ----
// 上传:body = JSON 文本(项目编号/名称/手机号) + "\n\n" + zip 二进制
if (urlPath === '/api/proj/upload') {
var chunks = [];
req.on('data', function (c) { chunks.push(c); });
req.on('end', function () {
var buf = Buffer.concat(chunks);
var sep = buf.indexOf('\n\n');
if (sep < 0) return sendJson(res, { ok: false, msg: '缺少 JSON 头' });
var 头 = {};
try { 头 = JSON.parse(buf.slice(0, sep).toString('utf8')); } catch (e) { return sendJson(res, { ok: false, msg: 'JSON 头解析失败' }); }
var 手机号 = 头['手机号'], 编号 = 头['编号'];
if (!手机号 || !编号) return sendJson(res, { ok: false, msg: '缺少手机号或项目编号' });
if (!会员存在(手机号)) return sendJson(res, { ok: false, msg: '该手机号未注册' });
var zip = buf.slice(sep + 2);
if (zip.length < 100) return sendJson(res, { ok: false, msg: 'zip 数据为空或过小' });
var d = 项目目录(手机号, 编号);
if (!d) return sendJson(res, { ok: false, msg: '路径非法' });
try {
fs.mkdirSync(d, { recursive: true });
var tmp = path.join(d, '项目.zip.tmp');
fs.writeFileSync(tmp, zip);
var 目标 = path.join(d, '项目.zip');
try { fs.unlinkSync(目标); } catch (e) { }
fs.renameSync(tmp, 目标);
var meta = { 编号: 编号, 名称: 头['名称'] || '', 时间: 头['时间'] || 今天加天数(0), 大小: zip.length };
var mt = path.join(d, 'meta.json.tmp');
fs.writeFileSync(mt, JSON.stringify(meta), 'utf8');
fs.renameSync(mt, path.join(d, 'meta.json'));
return sendJson(res, { ok: true, 编号: 编号, 大小: zip.length, 时间: meta['时间'] });
} catch (e) { return sendJson(res, { ok: false, msg: '写盘失败: ' + e.message }); }
});
return;
}
if (urlPath === '/api/proj/list') {
return sendJson(res, 列项目(query['手机号']));
}
if (urlPath === '/api/proj/download') {
var 手机号 = query['手机号'], 编号 = query['编号'];
var d2 = 手机号 && 编号 ? 项目目录(手机号, 编号) : null;
var zp = d2 ? path.join(d2, '项目.zip') : '';
if (!d2 || !fs.existsSync(zp)) return sendJson(res, { ok: false, msg: '云上没有该项目' });
res.writeHead(200, { 'Content-Type': 'application/zip', 'Content-Length': fs.statSync(zp).size });
fs.createReadStream(zp).pipe(res);
return;
}
if (urlPath === '/api/proj/delete') return readBody(req, function (e, b) {
b = b || {};
var d3 = b['手机号'] && b['编号'] ? 项目目录(b['手机号'], b['编号']) : null;
if (!d3) return sendJson(res, { ok: false, msg: '参数错误' });
try { fs.rmSync(d3, { recursive: true, force: true }); return sendJson(res, { ok: true }); }
catch (e2) { return sendJson(res, { ok: false, msg: '删除失败: ' + e2.message }); }
});
// 静态文件(管理页)
if (urlPath === '/' || urlPath === '/admin.html') urlPath = '/admin.html';
var file = path.join(WWW, path.normalize(urlPath).replace(/^([A-Za-z]:)?[\\\/]+/, ''));
if (file.indexOf(WWW) !== 0) { res.writeHead(403); res.end('Forbidden'); return; }
fs.readFile(file, function (err, data) {
if (err) { res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('Not Found: ' + urlPath); return; }
var ext = path.extname(file).toLowerCase();
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
res.end(data);
});
}).listen(PORT, HOST, function () {
console.log('会员系统服务 on http://' + HOST + ':' + PORT + ' root=' + ROOT);
});