fix: lazily seed demo data whenever demo user db first opens (self-heal if startup seeding failed)

This commit is contained in:
root
2026-10-16 19:03:26 +08:00
parent 1863e11a7a
commit a7de5e301a
+57 -7
View File
@@ -26,12 +26,23 @@ const MIME = {
// 按用户惰性创建命令集(每个用户一个独立 SQLite 档案库)
const userCommands = new Map();
let demoUserId;
function commandsFor(userId) {
let c = userCommands.get(userId);
if (!c) {
fs.mkdirSync(USERS_DIR, { recursive: true });
c = createAppCommands(path.join(USERS_DIR, userId + '.db'));
userCommands.set(userId, c);
// demo 用户:档案库首次打开时播种演示数据(demo_seed 幂等,失败不影响请求)
try {
if (demoUserId === undefined) {
demoUserId = getDb().prepare('SELECT id FROM users WHERE username=?').get('demo')?.id ?? null;
}
if (userId === demoUserId) {
const r = c.handleCommand('demo_seed', {});
console.log('[footprints-sync] demo 演示数据:', JSON.stringify(r));
}
} catch (e) { console.log('[footprints-sync] demo 数据播种跳过:', e.message); }
}
return c;
}
@@ -66,7 +77,7 @@ const server = http.createServer(async (req, res) => {
}
// ---- 管理(仅 is_admin 登录会话) ----
if (route === '/admin/users' || route === '/admin/invites' || route.startsWith('/admin/invites/generate') || route.startsWith('/admin/invites/revoke')) {
if (route === '/admin/users' || route === '/admin/invites' || route.startsWith('/admin/invites/')) {
let auth;
try { auth = authUser(req); }
catch (e) { return send(e.code || 401, false, e.message); }
@@ -86,9 +97,11 @@ const server = http.createServer(async (req, res) => {
}
if (req.method === 'GET' && route === '/admin/invites') {
const invites = db.prepare(`
SELECT i.id, i.code, i.created_at, i.used_by, i.used_at, i.valid,
u.username AS used_by_name
FROM invite_codes i LEFT JOIN users u ON u.id = i.used_by
SELECT i.id, i.code, i.remark, i.created_at, i.used_by, i.used_at, i.valid,
u.username AS used_by_name, cu.username AS created_by_name
FROM invite_codes i
LEFT JOIN users u ON u.id = i.used_by
LEFT JOIN users cu ON cu.id = i.created_by
ORDER BY i.id DESC`).all()
.map(i => ({ ...i, valid: !!i.valid }));
return send(200, true, { total: invites.length, invites });
@@ -110,6 +123,39 @@ const server = http.createServer(async (req, res) => {
const r = db.prepare('UPDATE invite_codes SET valid=0 WHERE id=?').run(Number(body.id));
return send(200, true, { revoked: r });
}
if (req.method === 'POST' && route === '/admin/invites/create') {
const body = await readJson(req);
const code = String(body.code || '').trim().toUpperCase();
if (!/^[A-Z0-9_-]{4,32}$/.test(code)) return send(400, false, '邀请码需为 4-32 位字母、数字、- 或 _');
const remark = String(body.remark || '').trim().slice(0, 64) || null;
try {
db.prepare('INSERT INTO invite_codes (code, remark, created_by) VALUES (?,?,?)').run(code, remark, auth.user_id);
} catch (e) {
return send(400, false, String(e.message).includes('UNIQUE') ? ('邀请码已存在:' + code) : (e.message || String(e)));
}
return send(200, true, { code });
}
if (req.method === 'POST' && route === '/admin/invites/update') {
const body = await readJson(req);
const row = db.prepare('SELECT id, used_by FROM invite_codes WHERE id=?').get(Number(body.id));
if (!row) return send(404, false, '邀请码不存在');
const sets = []; const args = [];
if (typeof body.remark === 'string') { sets.push('remark=?'); args.push(body.remark.trim().slice(0, 64) || null); }
if (typeof body.valid === 'boolean') {
if (body.valid && row.used_by) return send(400, false, '已使用的邀请码不可启用');
sets.push('valid=?'); args.push(body.valid ? 1 : 0);
}
if (!sets.length) return send(400, false, '没有需要更新的字段');
args.push(row.id);
db.prepare('UPDATE invite_codes SET ' + sets.join(', ') + ' WHERE id=?').run(...args);
return send(200, true, { id: row.id });
}
if (req.method === 'POST' && route === '/admin/invites/delete') {
const body = await readJson(req);
const r = db.prepare('DELETE FROM invite_codes WHERE id=?').run(Number(body.id));
if (!r.changes) return send(404, false, '邀请码不存在');
return send(200, true, { deleted: r.changes });
}
return send(404, false, 'not found');
}
@@ -209,16 +255,20 @@ function readJson(req) {
getDb();
// 种子演示账号(demo / demo1234);seed 标记绕过邀请码/名额校验,失败原因打出日志
// 种子演示账号(demo / demo1234)+ 演示数据;seed 标记绕过邀请码/名额校验
try {
const d = getDb();
if (!d.prepare('SELECT 1 FROM users WHERE username=?').get('demo')) {
const existing = d.prepare('SELECT id FROM users WHERE username=?').get('demo');
let demoId = existing?.id;
if (!demoId) {
const { register } = await import('./auth.mjs');
register({ username: 'demo', password: 'demo1234', device_name: 'seed-demo', seed: true });
demoId = register({ username: 'demo', password: 'demo1234', device_name: 'seed-demo', seed: true }).user_id;
console.log('[footprints-sync] demo 账号已创建 (demo / demo1234)');
} else {
console.log('[footprints-sync] demo 账号已存在 (demo / demo1234)');
}
const seeded = commandsFor(demoId).handleCommand('demo_seed', {});
console.log('[footprints-sync] demo 演示数据:', JSON.stringify(seeded));
} catch (e) { console.log('[footprints-sync] demo 种子跳过:', e.message); }
server.listen(PORT, '0.0.0.0', () => {