From a7de5e301ae4f604fe721cae1175cd2c16ffbabd Mon Sep 17 00:00:00 2001 From: root Date: Fri, 16 Oct 2026 19:03:26 +0800 Subject: [PATCH] fix: lazily seed demo data whenever demo user db first opens (self-heal if startup seeding failed) --- sync-server/src/index.mjs | 64 ++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/sync-server/src/index.mjs b/sync-server/src/index.mjs index f597cfa..1370bb8 100644 --- a/sync-server/src/index.mjs +++ b/sync-server/src/index.mjs @@ -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', () => {