①图鉴隐藏测试模式:连点标题'军'字10次开关,纯内存标志位全部军械可见可演示,不动SAVE.codex(persist/submitRun读它,动了会污染后端),刷新失效 ②24件主武器说明文案全部替换(用户提供梗文案);演示页介绍单行显示、去掉类别前缀、删'实战模拟·自动驾驶'提示行 ③新主武器'巡飞弹'(loiter):慢攻速追踪导弹(制导引擎新增maxSpd上限5保持中速),命中留火场(zones新kind:fire纯粒子火舌,无圆形区域)+分裂4枚小炸弹快爆;新精灵loms;CODEX_KEYS同步63 ④存档云同步:星币/机体/选中机体/永久强化/本地纪录上服务端(player_state表+POST /api/state+vState白名单清洗);login/me/register响应带state;客户端persist()钩子防抖600ms自动上传;服务端整体替换本地防串号,登出全清;老账号首登自动上传本地存档迁移 ⑤机体选择/购买从出击准备并入机库强化(buildShips);出击默认无尽模式 服务端已重启生效;/api/state全链路在独立测试库curl验证通过(合法推送/回读/非法拦截清洗/401)
71 lines
2.4 KiB
JavaScript
71 lines
2.4 KiB
JavaScript
"use strict";
|
||
/* ================= SQLite(node:sqlite 内置驱动,零依赖) ================= */
|
||
const { DatabaseSync } = require("node:sqlite");
|
||
const path = require("node:path");
|
||
const fs = require("node:fs");
|
||
const { expandHome } = require("./env");
|
||
|
||
/* 数据目录不放源码树:默认 ~/.starraid,可用 DATA_DIR 环境变量 / .env 覆盖 */
|
||
const DATA_DIR = expandHome(process.env.DATA_DIR || path.join(require("node:os").homedir(), ".starraid"));
|
||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||
|
||
const db = new DatabaseSync(path.join(DATA_DIR, "starraid.db"));
|
||
db.exec("PRAGMA journal_mode=WAL;");
|
||
db.exec("PRAGMA foreign_keys=ON;");
|
||
|
||
db.exec(`
|
||
CREATE TABLE IF NOT EXISTS users(
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||
email TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||
pass_hash TEXT NOT NULL,
|
||
created_at INTEGER NOT NULL
|
||
);
|
||
CREATE TABLE IF NOT EXISTS sessions(
|
||
token TEXT PRIMARY KEY,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
created_at INTEGER NOT NULL,
|
||
expires_at INTEGER NOT NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
|
||
CREATE TABLE IF NOT EXISTS user_stats(
|
||
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||
boss_kills INTEGER NOT NULL DEFAULT 0,
|
||
endless_high INTEGER NOT NULL DEFAULT 0,
|
||
runs INTEGER NOT NULL DEFAULT 0,
|
||
updated_at INTEGER NOT NULL DEFAULT 0
|
||
);
|
||
CREATE TABLE IF NOT EXISTS codex_unlocks(
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
entry_key TEXT NOT NULL,
|
||
unlocked_at INTEGER NOT NULL,
|
||
PRIMARY KEY(user_id, entry_key)
|
||
);
|
||
CREATE TABLE IF NOT EXISTS runs(
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
mode TEXT NOT NULL,
|
||
diff INTEGER NOT NULL,
|
||
score INTEGER NOT NULL,
|
||
wave INTEGER NOT NULL,
|
||
stage INTEGER NOT NULL,
|
||
time_sec INTEGER NOT NULL,
|
||
boss_kills INTEGER NOT NULL,
|
||
win INTEGER NOT NULL,
|
||
created_at INTEGER NOT NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_runs_user ON runs(user_id);
|
||
CREATE TABLE IF NOT EXISTS player_state(
|
||
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
||
state TEXT NOT NULL,
|
||
updated_at INTEGER NOT NULL
|
||
);
|
||
`);
|
||
|
||
/* 定期清理过期 session */
|
||
setInterval(() => {
|
||
try { db.prepare("DELETE FROM sessions WHERE expires_at<?").run(Date.now()); } catch (e) { /* 忽略 */ }
|
||
}, 3600000).unref();
|
||
|
||
module.exports = db;
|