- 新增备份管理页与 API:在线快照、自动周期备份、按份数清理、下载、一键恢复(恢复前自动兜底) - 新增 /profile/export,普通用户可导出本人全部数据(不含 Cookie 明文) - 修复 X-Forwarded-For 可伪造导致三道 IP 防线失效,统一走 client_ip() 取客户端地址 - 取消 admin123 硬编码默认口令,留空则生成随机初始口令并仅打印一次 - .dockerignore 排除 backups/ 并加构建期断言,防止密钥随镜像分发 - 新增会话版本号,改密/停用/删除及恢复备份后其他会话立即失效 - 新增容器资源上限、采集跨度硬顶 31 天、重操作最小间隔与并发 409 - 新增访问日志、HSTS 条件下发、口令黑名单、验证码抗模板匹配、instance.json 0600 - 版本号升至 1.4.0,同步更新 README、SECURITY、.env.example 与 compose 配置
145 行
7.0 KiB
SQL
145 行
7.0 KiB
SQL
-- WorkBuddy Portal —— SQLite 表结构
|
||
-- 设计要点:
|
||
-- * 多用户:usage_records / collect_runs / audit_log 都带 user_id;
|
||
-- 每个账号只看得到自己的数据,管理员也不越过这条线(见 docs/ARCHITECTURE.md)
|
||
-- * settings 是 (user_id, key) 复合主键:user_id=0 为实例级,其余为个人级
|
||
-- * usage_records 主键是 (user_id, request_id):去重按「人 + 请求」,
|
||
-- 不同账号拿到相同 requestId 时互不覆盖
|
||
-- * ts 存「本地保留的最早开始时间」;cloud_ts 存云端最近一次返回的时间(观察长请求前移)
|
||
-- * day / hour 是冗余列,配合索引让区间扫描与 GROUP BY 都能走索引
|
||
-- * prompt 单独存一列且默认不参与任何列表接口(占传输量约 80%)
|
||
--
|
||
-- 升级:本文件是 DDL 的唯一来源。db._migrate() 用「改名旧表 -> 重跑本文件 ->
|
||
-- 回填数据 -> 删旧表 -> 再跑一次本文件补索引」的方式做在线迁移。
|
||
|
||
PRAGMA journal_mode = WAL;
|
||
PRAGMA synchronous = NORMAL;
|
||
|
||
-- ---------------- 采集正本 ----------------
|
||
CREATE TABLE IF NOT EXISTS usage_records (
|
||
user_id INTEGER NOT NULL DEFAULT 0, -- 归属账号(usage_records.user_id -> users.id)
|
||
request_id TEXT NOT NULL,
|
||
ts TEXT NOT NULL, -- 'YYYY-MM-DD HH:MM:SS'
|
||
day TEXT NOT NULL, -- 'YYYY-MM-DD'
|
||
hour INTEGER NOT NULL, -- 0..23
|
||
model TEXT NOT NULL DEFAULT '-',
|
||
client TEXT NOT NULL DEFAULT '-',
|
||
credits REAL NOT NULL DEFAULT 0,
|
||
prompt TEXT,
|
||
first_seen TEXT NOT NULL, -- 本地首次入库时间
|
||
last_seen TEXT NOT NULL, -- 本地最近一次见到的时间
|
||
cloud_ts TEXT, -- 云端最近一次返回的 requestTime
|
||
PRIMARY KEY (user_id, request_id)
|
||
);
|
||
|
||
-- 索引一律以 user_id 打头:所有查询都带「归属人」这个条件,
|
||
-- 少了它会退化成全表扫描(多用户下这是最容易踩的性能坑)。
|
||
CREATE INDEX IF NOT EXISTS idx_ur_day ON usage_records(user_id, day);
|
||
CREATE INDEX IF NOT EXISTS idx_ur_day_hour ON usage_records(user_id, day, hour);
|
||
CREATE INDEX IF NOT EXISTS idx_ur_model_day ON usage_records(user_id, model, day);
|
||
CREATE INDEX IF NOT EXISTS idx_ur_client_day ON usage_records(user_id, client, day);
|
||
CREATE INDEX IF NOT EXISTS idx_ur_credits ON usage_records(user_id, credits DESC);
|
||
CREATE INDEX IF NOT EXISTS idx_ur_ts ON usage_records(user_id, ts);
|
||
|
||
-- ---------------- 采集运行历史 ----------------
|
||
CREATE TABLE IF NOT EXISTS collect_runs (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL DEFAULT 0, -- 哪次「谁」的采集
|
||
trigger TEXT NOT NULL, -- manual | schedule | cli | startup
|
||
status TEXT NOT NULL, -- running | ok | warn | error
|
||
started_at TEXT NOT NULL,
|
||
finished_at TEXT,
|
||
duration_ms INTEGER,
|
||
win_from TEXT,
|
||
win_to TEXT,
|
||
fetched INTEGER DEFAULT 0, -- 云端返回条数
|
||
added INTEGER DEFAULT 0,
|
||
dup INTEGER DEFAULT 0,
|
||
total INTEGER DEFAULT 0, -- 入库后该账号总条数
|
||
conflicts INTEGER DEFAULT 0,
|
||
exit_code INTEGER,
|
||
message TEXT, -- 一句话结论
|
||
detail TEXT -- 逐行日志(含 [warn] / [error] 原文)
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_runs_started ON collect_runs(started_at DESC);
|
||
CREATE INDEX IF NOT EXISTS idx_runs_user ON collect_runs(user_id, id DESC);
|
||
|
||
-- ---------------- 键值配置 ----------------
|
||
-- user_id = 0 : 实例级(接口基址、注册开关、验证码策略)
|
||
-- user_id > 0 : 个人级(自己填的 Cookie / UA、采集参数、调度时刻、slot:* 簿记)
|
||
CREATE TABLE IF NOT EXISTS settings (
|
||
user_id INTEGER NOT NULL DEFAULT 0,
|
||
key TEXT NOT NULL,
|
||
value TEXT,
|
||
updated_at TEXT,
|
||
PRIMARY KEY (user_id, key)
|
||
);
|
||
|
||
-- ---------------- 账号 ----------------
|
||
CREATE TABLE IF NOT EXISTS users (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
username TEXT UNIQUE NOT NULL,
|
||
password_hash TEXT NOT NULL,
|
||
display_name TEXT,
|
||
email TEXT,
|
||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||
status TEXT NOT NULL DEFAULT 'active', -- active | disabled
|
||
session_ver INTEGER NOT NULL DEFAULT 0, -- 会话版本:改密/重置/停用 +1,旧会话立即失效
|
||
created_at TEXT,
|
||
register_ip TEXT, -- 自助注册来源,用于每日限额
|
||
last_login_at TEXT,
|
||
last_login_ip TEXT,
|
||
login_count INTEGER NOT NULL DEFAULT 0
|
||
);
|
||
|
||
-- ---------------- 操作审计 ----------------
|
||
-- 刻意**不记录任何凭证**:detail 里只写「改了哪些键」,不写键的值。
|
||
CREATE TABLE IF NOT EXISTS audit_log (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL DEFAULT 0, -- 归属账号(0 = 系统 / CLI / 未登录事件)
|
||
at TEXT NOT NULL,
|
||
actor TEXT,
|
||
action TEXT NOT NULL,
|
||
detail TEXT,
|
||
ip TEXT
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_audit_at ON audit_log(at DESC);
|
||
CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log(user_id, id DESC);
|
||
|
||
-- ---------------- 图形验证码 ----------------
|
||
-- 答案只在服务端存在。下发到浏览器的只是 id,且用一次即删。
|
||
CREATE TABLE IF NOT EXISTS captchas (
|
||
id TEXT PRIMARY KEY, -- 随机 token(下发给客户端)
|
||
answer TEXT NOT NULL, -- 正确答案(绝不下发)
|
||
purpose TEXT NOT NULL, -- login | register
|
||
created_at TEXT NOT NULL,
|
||
expires_at TEXT NOT NULL,
|
||
used_at TEXT
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_captcha_expires ON captchas(expires_at);
|
||
|
||
-- ---------------- 备份索引 ----------------
|
||
-- 表里只放「元数据」,备份文件本身在 config.BACKUP_DIR(**不在** data/ 卷里,
|
||
-- 免得 `docker compose down -v` 把正本和备份一起删掉)。
|
||
-- 刻意不记录备份内容、也不记录任何凭证 —— 这里只是一份可下载清单。
|
||
CREATE TABLE IF NOT EXISTS backups (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
filename TEXT NOT NULL UNIQUE, -- 备份目录内的文件名(不含路径)
|
||
bytes INTEGER NOT NULL DEFAULT 0, -- 归档大小
|
||
sha256 TEXT NOT NULL DEFAULT '', -- 归档整体校验值(前 64 位十六进制)
|
||
created_at TEXT NOT NULL,
|
||
trigger TEXT NOT NULL DEFAULT 'manual', -- manual | auto | cli | pre-restore
|
||
actor TEXT, -- 触发者用户名(自动备份为 system)
|
||
schema_ver INTEGER NOT NULL DEFAULT 0, -- 归档时的 user_version
|
||
records INTEGER NOT NULL DEFAULT 0, -- 归档时的记录条数(便于挑一份恢复)
|
||
credits REAL NOT NULL DEFAULT 0,
|
||
users INTEGER NOT NULL DEFAULT 0,
|
||
note TEXT, -- 备注 / 恢复来源
|
||
missing INTEGER NOT NULL DEFAULT 0 -- 1 = 文件已不在磁盘上(手工删过)
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_backups_at ON backups(created_at DESC);
|