文件
workbuddy-portal/workbuddy_portal/schema.sql
T
wangchuanli 3751dffef9 feat: 新增备份恢复与公网加固
- 新增备份管理页与 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 配置
2026-09-18 08:46:34 +08:00

145 行
7.0 KiB
SQL
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
-- 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);