基线:全量源码首提(D1 版本控制落地,含第一轮优化 B1-B4/A1/A4/缩放修复)
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
// 会员系统服务器(零依赖纯 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);
|
||||
});
|
||||
Reference in New Issue
Block a user