// 会员系统服务器(零依赖纯 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); });