文档定位:本文是「当前架构」的管理型总览——从公网 HTTPS 链路、双环境(内测/生产)部署,到代码分层、安全边界与工程工具链, 回答三个问题:系统现在长什么样、一次改动会碰到哪几层、该去哪篇文档查细节。
配套文档:环境说明书(非专业视角)ENVIRONMENTS.md · 部署操作 DEPLOY.md · 流程与红线 DEVELOPMENT_FLOW.md · 发布分发 PUBLISHING.md · 游戏空间 GAMES.md · 隐私与信息分离 DEVELOPMENT.md。
隐私铁律:本文面向 GitHub 公开,一律使用占位符 —— {DOMAIN} 生产域名、{BETA_HOST} 内测域名、 {HK_IP} 服务器 IP、{HOST_ROOT} 服务器上的部署父目录、{PROD_ROOT} / {BETA_ROOT} 两套部署根、 {BACKUP_DIR} 备份目录。({ORIGIN_IP} 随大陆源站退役已废弃,不应再出现。) 真实值只存在于 gitignored 的
local/ENV.md(对照表)与local/stage.env.ps1(部署参数),详见 docs/DEVELOPMENT.md §1。
一句话:一套代码、三种形态(网页 / 桌面 / 手机壳)、两层在线环境(L1 内测 / L2 生产)、一台香港服务器直出(Caddy 终结 TLS + 静态托管 + 反向代理);同机上两个相互隔离的实例共享同一个 PostgreSQL 集群。
┌──────────── 用户侧:三种形态共用同一份前端 ────────────┐
│ 浏览器网页 │ 手机壳 App(Capacitor)│ 桌面 Electron │
└──────┬──────────────────────────────┬──────────────┘
https://{DOMAIN} https://{BETA_HOST}
│ │
Cloudflare(DNS + 代理) DNS 仅解析(直连)
│ HTTPS │ HTTPS
▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ 单台服务器 {HK_IP} · Caddy(TLS 终结 / Let's Encrypt 自动证书 / gzip) │
│ {DOMAIN} 块:静态 → {PROD_ROOT}/app(7 天缓存)→ API :3000 │
│ {BETA_HOST} 块:静态 → {BETA_ROOT}/app(不缓存) → API :3100 │
└──────────────────────────────────────────┬──────────────────────────┘
│ 本机 127.0.0.1
▼
┌─────────────────────────────────────────────────────────────────────┐
│ {HK_IP} 同机运行 │
│ 静态直出:{PROD_ROOT}/app · {BETA_ROOT}/app(file_server + SPA 兜底)│
│ /api /uploads /avatars /dl → Node :3000(生产)/ :3100(内测) │
│ /games/werewolf/{api,werewolf-ws} → Node :3011(房间服务,内存态) │
└──────────────┬───────────────────────────────────┬──────────────────┘
qbao-api :3000 qbao-api-beta :3100
│ │
PostgreSQL(同一集群,127.0.0.1:5432)
│ │
库 qbao(生产账本) 库 qbao_beta(内测账本)
改动流动方向固定为从左到右:L0 本机 → L1 内测 → L2 生产(概念与入口见 docs/ENVIRONMENTS.md;顺序纪律见 docs/DEVELOPMENT_FLOW.md §7)。
2026-09-10 全量迁港:原「大陆源站 + 香港边缘网关」两层已合并为香港单机,源站已退役清理。 因此此前为规避大陆机房 ICP Host 拦截而设计的「回源 Host 改写 +
X-Qbao-Route路由头 + 出口 IP 白名单 map」 整套机制已废弃;环境区分回归最朴素的方式——Caddy 两个 site 块 + 两个本机端口。
X-Qbao-Route: beta +
源站 nginx map「路由头 + 网关出口 IP 白名单」分流内测。X-Qbao-Route 请求头与出口 IP 白名单(该机制已废弃,本文不再描述其配置);http://{ORIGIN_IP} IP 直连兜底随源站退役已不存在。| 层 | 载体 | 职责 | 关键点 |
|---|---|---|---|
| DNS / CDN | Cloudflare | 域名解析;{DOMAIN} 走代理(缓存加速、边缘 TLS);{BETA_HOST} 仅 DNS | 控制台操作需 VPN(见 local/ENV.md) |
| 入口 / TLS / 静态 / 反代 | {HK_IP} · Caddy | TLS 终结、Let’s Encrypt 自动证书、gzip、静态直出、SPA 兜底、API 反代 | 两个 site 块;配置 /etc/caddy/Caddyfile(见 2.4) |
| 业务 API | Node · :3000 / :3100 | REST API(Express) | systemd:qbao-api / qbao-api-beta |
| 房间服务 | Node · :3011 | 狼人杀实时(Koa + socket.io,房间在内存) | systemd:qbao-werewolf;重启即清零、不落库 |
| 数据库 | PostgreSQL(localhost) | 两本独立账本 qbao / qbao_beta | 见 §3.3 |
| 静态与分发 | 各环境 app/ 根 + downloads/ 储藏室 | SPA / 游戏门户 / 安装包与清单 | 缓存策略差异见 2.5 |
{DOMAIN} 与 {BETA_HOST} 各一张,ACME HTTP-01)。
{DOMAIN} 经 Cloudflare 代理时,用户侧先经 CF 边缘证书,CF ↔ 香港回源使用 Caddy 的正式证书。单文件 /etc/caddy/Caddyfile,两个 site 块各服务一个环境;块内用 order-preserving 的 route 块 + 显式 matcher 分流
(不用无 matcher 的兜底块,避免内部指令排序导致 404 抢先命中;占位符示意的等价形式):
{DOMAIN} {
encode gzip
route /download { redir * /api/v1/desktop/download 302 } # 桌面端安装包短链
route /games/werewolf/api/* { # 狼人杀 API:剥前缀
uri strip_prefix /games/werewolf/api
reverse_proxy 127.0.0.1:3011 { header_up Host {host} }
}
@static { not path /api /api/* /uploads/* /avatars/* /dl /dl/* /download \
/games/werewolf/werewolf-ws /games/werewolf/api/* }
route @static { # 静态直出 + SPA 兜底
root * {PROD_ROOT}/app
try_files {path} {path}/ /index.html
file_server
}
@ws path /games/werewolf/werewolf-ws # 狼人杀 WebSocket:不剥前缀
route @ws { reverse_proxy 127.0.0.1:3011 { header_up Host {host} } }
@dyn path /api/* /api /uploads/* /avatars/* /dl/* /dl # 其余动态 → 本机 API
route @dyn { reverse_proxy 127.0.0.1:3000 { header_up Host {host} } }
@assets { # 静态按扩展名;必须排除 /api/*
path *.js *.css *.png *.jpg *.jpeg *.gif *.ico *.svg *.woff *.woff2
not path /api/*
}
header @assets Cache-Control "public, max-age=604800" # 生产静态 7 天
}
{BETA_HOST} 块结构与上完全相同,仅三处不同:root * {BETA_ROOT}/app、reverse_proxy 127.0.0.1:3100、
Cache-Control "no-cache, no-store, must-revalidate"。
设计效果:一个 Caddy 进程服务两条公网域名、两个静态根与两个 API 实例; 环境由「访问哪个域名」唯一决定,不存在任何可伪造的请求头入口。 改配置后先
caddy validate --config /etc/caddy/Caddyfile,再systemctl reload caddy。
not path /api/*不能省,也不能用续行写(v3.37.5 踩坑):受保护媒体端点 (/api/v1/chat/files/*.png、/api/v1/issues/images/*)的扩展名与静态资源相同,纯扩展名匹配会把 下载响应改写成静态缓存策略 —— 生产上等于把 401/404 缓存 7 天(图片一旦裂开就再也好不了)。 而写成@assets path *.png … \换行not path /api/*不是「且」:适配器把"not"、"path"、"/api/*"当成三个扩展名模式塞进同一个path列表,条件恒假,且caddy validate照样通过。 必须用块式匹配器,或path *.png !/api/*;改完查http://127.0.0.1:2019/config/确认"not"是键而非数组元素。
| 环境 | 静态缓存 | 改版生效方式 |
|---|---|---|
| 生产 {DOMAIN} | public, max-age=604800(7 天);CF 边缘另有一层缓存 |
引用 js/css 必须带版本参数 ?v=(否则老浏览器拿旧文件);必要时 CF 控制台 Purge Cache |
| 内测 {BETA_HOST} | no-cache, no-store, must-revalidate |
部署刷新即见 |
| 动态接口 / 下载文件 | 不缓存(下载经 Node,支持 Range/206) | — |
| 受保护媒体(聊天附件 / 工单截图) | 服务端显式下发 private, max-age=31536000, immutable(v3.37.5) |
文件名随机且内容不可变,故长缓存安全;用 private 是因为属用户私有数据。反向代理的静态规则必须排除 /api/*,否则会覆盖此头(并把 401/404 也缓存下来) |
列表缩略图 /chat/files/x.webp?w=480 |
同上(同一处理器、同一长缓存) | 客户端上传时随主图一并送一张长边 480 的小图,服务端存成 <原名>.w480.<ext>;没有派生图就静默回退原图(老消息不改库表、不裂图)。票仍按原文件名校验,故 w 只是取图开关,不能越权 |
URL 即缓存键,票据必须稳定(v3.37.6):媒体地址带签名票
?t=<exp>.<hmac>, 若每次下发消息列表都按Date.now()+1h重签,同一张图每次都是新 URL,浏览器缓存命中率恒为 0。 现在签发时刻按 30 分钟时间桶对齐(同桶内逐字节相同),实际有效期 60~90 分钟; 客户端另有一层按文件名的 IndexedDB Blob 缓存(换桶、换会话、离线都不再回源)。
跨境链路的单流吞吐是图片体验的硬上限(v3.37.6 实测):服务器在香港、用户在境内, 内核拥塞控制从 cubic 换到 BBR 后,同一条 TCP 流(≈浏览器的一条 HTTP/2 连接) 从 10.6 KB/s 提升到 126.9 KB/s(12 倍),9.4MB 原图端到端由 471s 降到 22.4s。 服务器自身出网 43.9MB/s、负载 0.02、磁盘 r_await 1.24ms —— 瓶颈从来不在服务器的算力或磁盘。 跨境链路的单流吞吐是图片体验的硬上限(v3.37.6 实测):服务器在香港、用户在境内, 内核拥塞控制从 cubic 换到 BBR 后,同一条 TCP 流(≈浏览器的一条 HTTP/2 连接) 从 10.6 KB/s 提升到 126.9 KB/s(12 倍),9.4MB 原图端到端由 471s 降到 22.4s。 服务器自身出网 43.9MB/s、负载 0.02、磁盘 r_await 1.24ms —— 瓶颈从来不在服务器的算力或磁盘。 参数与持久化见 docs/DEPLOY.md §2。
聊天定位是学习资料级别的传递,不是网盘、不是长期存储、更不是无限流量出口。 此前上传只受「单文件 50MB」一条约束,四个口子全开着:不计数(可循环上传写满磁盘)、 孤儿不清理(只上传不发消息即长期占盘)、附件永久留存(当网盘用)、 下载只看次数不看流量(签名票在有效期内可无限重放)。
| 闸门 | 位置 | 口径 | 作用 |
|---|---|---|---|
| ① 配额 | services/chatMediaService.js 的 uploadGate(),排在 multer 之前 |
按账号:60 次/小时、200MB/天、留存 500MB;另有 Content-Length 预检(主文件+小图+64KB 余量) |
被拒的请求一个字节都不落盘,是防刷盘最省带宽的一道 |
| ② 磁盘水位 | 同上 assertDiskHeadroom() |
整机剩余 < 2GB 拒绝一切上传(fs.statfsSync,探测不可用则跳过) |
配额是「每人」,水位是「整机」,互补 |
| ③ 归属 | 发送消息时 resolveAttachments() |
地址必须是本站 /chat/files/;文件必须存在;台账归属必须是本人;一条消息最多 9 张图 |
不能引用他人文件,也不能把站外 URL 当附件分发 |
| ④ 回收 | 每小时 sweepOnce() |
孤儿(message_id IS NULL)24h;已引用媒体 180 天释放字节;撤回消息立即释放 |
磁盘只涨不减的问题被闭环 |
台账是唯一事实来源:
chat_media_assets(sql/020_v3.43_chat_media_guard.sql)记stored_name / user_id / file_size / message_id / purged_at。上传先落台账、发消息时再回填 message_id(两段式),因此「上传了但没发出去」天然可识别为孤儿;撤回/过期只删台账行或打purged_at,配额随之释放。派生小图<主名>.w480.<ext>不单独建账,但跟着主文件一起删。
降级优先于强一致:若部署顺序导致 019 迁移尚未执行,台账读写全部降级为「打一次告警 + 按 v3.37.6 的行为继续服务」,上传不会 500。
下载侧是「次数 + 流量」双闸门(
lib/mediaLimits.js):媒体路径从通用限流(120/min/IP) 里skip出来,改由独立限流器计数(300/min/IP,校园 NAT 共享出口不误伤);真正的兜底是 按 IP 的字节预算 1GB/10min,在sendMediaFile之前allows()、调用之后consume(), 超限 429。两个计数器都是进程内内存态:本服务单实例部署(docs/DEPLOY.md §3), 若将来水平扩容,这两处必须迁到 Redis/PG。
全部常驻进程运行在同一台香港服务器上,应用端口只绑回环:
| 服务 | 端口 | systemd 单元 | 工作目录(占位) | 说明 |
|---|---|---|---|---|
| Caddy | 80(ACME + 跳转)、443 tcp/udp | caddy | — | 配置:/etc/caddy/Caddyfile;TLS 终结 + 静态直出 + 反代 |
| qbao-api(生产) | 127.0.0.1:3000 | qbao-api.service | {PROD_ROOT}/server | 读本目录 .env → 库 qbao |
| qbao-api-beta(内测) | 127.0.0.1:3100 | qbao-api-beta.service | {BETA_ROOT}/server | .env 含 AUTO_ADMIN=1、AI Key 留空 → 库 qbao_beta |
| qbao-werewolf | 127.0.0.1:3011 | qbao-werewolf.service | {PROD_ROOT}/party/werewolf | 房间内存态;两环境共用同一房间服务 |
| PostgreSQL 14 | 127.0.0.1:5432 | postgresql | — | 不对外 |
运行时:Node v26.x(服务器自装路径,systemd 单元内以绝对路径 ExecStart 指定)、swap 1G(小内存主机)。
ufw 默认拒绝入站,仅放行 22/80/443 tcp 与 443 udp(QUIC),并显式 deny 3000/3100/3011。
每个在线环境都是一套独立的「仓库布局目录」(部署侧非 git 检出,由脚本同步 tar 产物):
{PROD_ROOT}/ # 生产(真实路径见 local/ENV.md)
app/ # Caddy 静态根:index.html + 资源 + games/(7 天缓存)
server/ # API 代码 + .env + sql 迁移(systemd 工作目录)
downloads/ # 分发储藏室:manifest.json(+.bak) stable/ beta/
uploads/ # 上传统一目录:pool/ chat/ issues/ avatars/
party/werewolf/ # 狼人杀房间服务(systemd 工作目录)
scripts/ docs/ … # 工具与文档(同步)
{BETA_ROOT}/ # 内测:同样布局(app 不缓存、server 端口/库/密钥独立)
目录更新用「整目录换名 + 滚动备份」:部署时
mv dir dir.bak_<时间戳> && mkdir && 解包,备份保留 5 份(scripts/stage.ps1 参数)。
qbao(生产,禁止测试写入)、qbao_beta(内测,可随时重建)。server/sql/NNN_*.sql 编号迁移(幂等),schema_migrations 表每库独立记账,node scripts/run_migration.js 按目录 .env 执行;顺序固定 L1 先、L2 后。qbao_beta 的初始化方式是对生产库做 schema-only 克隆(pg_dump –schema-only + 拷贝 schema_migrations),
而非执行仓库 init.sql——线上生产库经多年迁移后与 init.sql 基线存在漂移,克隆保证与生产 schema 逐字一致(重建步骤见 docs/DEPLOY.md §4B)。pg_dump 双库(生产 + 内测)gzip 至 {BACKUP_DIR},保留 30 天;恢复流程见 docs/DEPLOY.md §7。SKIP_AI_WORKER=1 开关)。local/backups/,不进 Git)。| 维度 | L1 内测 | L2 生产 |
|---|---|---|
| 入口 | https://{BETA_HOST}(CF 仅 DNS 直连) | https://{DOMAIN}(CF 代理) |
| Caddy site 块 | {BETA_HOST} 块 | {DOMAIN} 块 |
| API 进程 / 端口 | qbao-api-beta :3100 | qbao-api :3000 |
| 数据库 | qbao_beta(schema 与生产一致) | qbao(禁测试写入) |
| 静态目录 | {BETA_ROOT}/app(不缓存) | {PROD_ROOT}/app(静态 7 天) |
| 下载/上传 | 各自 downloads/ 与 uploads/(Caddy 按 site 块分流) | 同上(生产) |
| 账号体系 | 开放注册;AUTO_ADMIN=1 注册即管理员 | 管理员仅后台授予(bootstrap_admin / ADMIN_USERNAMES) |
| 测试动作 | 注册/对局/兑换/领奖/清库全部允许 | 仅金丝雀账号只读巡检 |
| 部署目标 | scripts/stage.ps1 -Env beta | scripts/stage.ps1 -Env prod |
| 共享 | 同一台主机、同一个房间服务(:3011) | 同左 |
requireAuth / requireAdmin 中间件;全局与登录限流。bootstrap_admin.js(或首次引导 ADMIN_USERNAMES 环境变量)——只能由服务器操作者执行;.env 开启 AUTO_ADMIN=1 → 注册即 admin(便于内测管理功能)。生产严禁开启 AUTO_ADMIN。| 操作 | 对其他 admin | 对自己 |
|---|---|---|
| 封禁(PATCH ban) | 403 | — |
| 改角色(PUT role) | 403(角色变更仅后台脚本) | 允许自降为 user |
| 重置密码(PUT password) | 403 | 允许 |
| 其他资料修改 | 允许(非敏感字段) | 允许 |
?qa=1&token=)仅 localhost 与 {BETA_HOST} 放行;
门禁纯函数 app/src/games/qa-gate.js(qaAllowedByHost),移植游戏时同一逻辑内联进其适配层,两端必须同步修改(见 docs/GAMES.md §六)。.env 权限 600;JWT_SECRET 强随机且 ≥32 字符(启动强校验);PG 仅 localhost。x-ai-api-key 透传,服务端不落库;桌面端凭据以 DPAPI(safeStorage)加密。GET /api/v1/chat/files/:filename)与工单截图(GET /api/v1/issues/images/:filename)
是正文里以 <img src> 引用的资源,而 <img> 无法携带 Authorization 头——只加 requireAuth 会让全部历史图片裂图。
采用的方案是签名短票据:服务端在响应出网时把 ?t=<exp>.<hmacBase64url> 追加到媒体 URL 上,
票据由 HMAC(JWT_SECRET, 'qbao:media-token:v1') 派生的键对「文件名 + 过期时间」签名(TTL 1 小时),
因此不可移用到另一个文件;端点接受「Bearer 头 或 有效票据」二者之一,其余一律 401。
DB 只存干净路径(写入前剥离 ?t=,读取时重新签发),既避免票据过期后历史行失效,也顺带修复了历史脏行。
quiz_data.images 走的是另一条链路(同源静态),不在本机制内。vite-plugin-singlefile 构建 → app/dist/index.html(内嵌全部 JS/CSS,约 550KB)。window.__QBAO_RUNTIME__ 提供 apiBase/updateChannel);
手机壳工程 mobile/(Capacitor)加载线上 URL(正式包 com.qbao.app / 内测包 com.qbao.beta,可共存安装)。core/env.js):网页形态同源 /api/v1;桌面形态用注入的 apiBase;壳应用打包时固定服务器 URL。app/public/games/ 游戏门户、下载落地页 /dl(服务端动态渲染);随构建拷入 dist 与各环境 app/。| 层 | 内容 |
|---|---|
| views/ + components/features/* | 视图与业务组件(答题/看板/聊天/反馈/用户中心/设置/下载中心/题库等) |
| stores/ | Pinia store(数据/答题/会话/同步/UI/积分/用户等) |
| services/ | 纯逻辑:subjectStats 统计引擎(单一权威源)、sync 同步引擎、chatVirtual、importExport、persistence、secureStore、API 封装族、desktopRelease |
| core/ | 启动引导 boot.js(跨标签守卫)、运行时环境 env.js |
| games/ | QA 门禁 qa-gate.js、游戏清单 gamesManifest.js |
数据底座:persistence.js(localStorage 骨架 + IndexedDB 大字段分流 + 配额自愈)、stateDb.js(IDB 行键按账号分区)、
sync.js(rev 乐观锁:空推跳过、409 实体级并集合并重推、keepalive 补推)。账号隔离:登录门禁 + 属主钉扎 + 跨标签守卫(E2E 验证零串账)。
users(账号/角色)、user_data(业务状态 JSONB)、backups、shared_banks、ai_request_log、answer_sessions、
user_files、issues/issue_messages、points_ledger(积分台账)、user_games_stats(游戏成绩)、
user_marble_profiles(弹珠钱包)、user_marble_rounds(单开一局防重放)、desktop_download_stats 等;
建表基线 server/init.sql + 版本化迁移 server/sql/NNN_*.sql(schema_migrations 追踪,见 §3.3 与 docs/DEVELOPMENT.md §7)。
| 端 | 正式(stable) | 内测/测试(beta) |
|---|---|---|
| 网页 | https://{DOMAIN} | https://{BETA_HOST}(独立实例) |
| 手机壳 | com.qbao.app · 1.0.x | com.qbao.beta(同签名共存安装,应用名带「内测」) |
| 桌面 | Qbao-Setup-X.Y.Z.exe · latest.yml | X.Y.Z-beta.N · beta 渠道 latest.yml |
downloads/manifest.json(每次发布前滚动 .bak);服务端只读,写经发布工具。/api/v1/desktop/{manifest,latest,download,update/<channel>/latest.yml,stats};手机:/api/v1/apps/{manifest,download,stats};
落地页 /dl 与网页「设置 → 下载中心」展示正式区 + 「内测版(可选)」区块(自选,非强制)。| 配置 | 位置 | 进 Git |
|---|---|---|
| 模板/默认值 | server/.env.example、scripts/stage.env.example.ps1、mobile/capacitor.config.ts 环境覆盖 | ✅ |
| 服务器实例 .env | 各环境 server/.env(每实例独立 PORT/PGDATABASE/JWT_SECRET/CORS/AUTO_ADMIN…) | ❌(仅服务器) |
| 真实环境对照表 | local/ENV.md | ❌ |
| 部署参数(SSH/密钥/远端目录/服务名) | local/stage.env.ps1 | ❌ |
| 服务器配置(Caddy/systemd/ufw) | 服务器 /etc/… 与 ufw 规则 | ❌ |
| 工具 | 作用 | |||||
|---|---|---|---|---|---|---|
| scripts/stage.ps1(-Env beta | prod,-Mode all | server | app | migrate | restart) | 双环境同步部署:本地打包 → scp → 远端换目录解包(滚动备份)→ 迁移 → 重启 |
| scripts/qa/smoke-stage.ps1(-Env beta | prod,-Marble,-DryRun) | L1 可写 E2E 冒烟(断言只写 qbao_beta);L2 只读金丝雀巡检 | ||||
| node scripts/run_migration.js | 按目录 .env 应用未执行迁移(幂等,每库独立记账) | |||||
| node scripts/bootstrap_admin.js | 生产管理员后台授予(唯一途径之一) | |||||
| 发布工具(§6) | manifest-first 入库与渠道纪律 | |||||
| CI(.github/workflows/ci.yml) | gitleaks 全历史扫描 · npm audit · 构建冒烟 · Vitest · ESLint |
/etc/caddy/Caddyfile({DOMAIN} 与 {BETA_HOST} 两个 site 块,兼静态直出与反代);
改后 caddy validate --config /etc/caddy/Caddyfile → systemctl reload caddy;改动前备份(Caddyfile.bak_<日期>)。/etc/cron.d/qbao-backup 每日 04:00 双库 dump → {BACKUP_DIR}(30 天)。@static 的 not path 列表必须与 @dyn 列表严格互补,漏项会导致该路径被静态兜底吞掉(返回 index.html 而非 API 响应)。| 改动类型 | 触碰的层 | 必读 | 必做(要点) |
|---|---|---|---|
| 前端页面/游戏静态 | app/ → 各环境静态根 | GAMES §六 | npm run build;stage -Mode app 双环境;js/css 引用带 ?v=;冒烟 |
| 后端 API / 服务 | server/ → 两实例 | DEVELOPMENT_FLOW §⑥ | 单测;stage -Mode server+restart 双环境;smoke-stage |
| 数据库 | server/sql + 两库 | DEVELOPMENT.md §7 | 新增编号幂等迁移;先 L1 后 L2(各自记账) |
| 新游戏接入 | public/games + 后端 + 门禁 | GAMES §六 | QA 门禁双端同步;先 L1 E2E;迁移先行 |
| 分发新包/改渠道 | downloads + 两 manifest 校验 | PUBLISHING | channel 纪律;双端校验同步;公网逐字节验证 |
| 网络/证书/域名 | CF 控制台 + Caddy(+ ufw) | DEPLOY §1/4、ENVIRONMENTS | Caddy 配置先备份再 validate/reload;@static 与 @dyn 互补性检查;CF 操作需 VPN;更新 local/ENV.md |
| 权限/安全规则 | users 路由 + 客户端 + 用例 | DEVELOPMENT_FLOW §5 | 服务端与客户端双端改(界面收敛);补守卫用例 |
| 新增真实值 | 文档/脚本 | DEVELOPMENT.md §1 | 只进 local/*(占位符纪律,push 前敏感扫描) |
硬性红线(与 DEVELOPMENT_FLOW §7 一致):push/tag/Release 只在用户验收后;生产库禁测试写入;迁移 L1 先 L2 后; QA 钩子仅内测域名与 localhost;真实地址/凭据永不进跟踪文件。
POST /api/v1/client-errors → journald 告警行,
见 §9「收口记录(v3.37 复核整改,2026-09)」R6);结构化日志与指标端点仍待做;17. users 身份列与 notices 表(新装库不再 42703),并新增 --verify 编号诊断
与 schema 漂移守卫单测;把 init.sql 收敛为「新装最小集」仍未做(见本 §9 R3);触发:2026-09 对 HEAD 全量只读复查(不依赖历史结论,逐条以代码/数据库/公网端点取证)。 编号 R1–R11 为本次整改项,与上方 T 体系、P 体系独立编号,避免与历史条目混淆。 全部改动仅本地提交,未经用户验收不 push/tag/Release(纪律见 DEVELOPMENT_FLOW §7)。
| # | 问题(复查发现) | 处置 |
|---|---|---|
| R1 | 聊天图片/工单截图下载端点仅 requireAuth,但前端用 <img src> 取图无法带 Authorization 头 |
端点改为「Bearer 或签名短票据」双通道:出网时服务端签发 ?t=<exp>.<hmac>(HMAC(JWT_SECRET) 派生键,TTL 1h),入库前剥离、读取时重签,兼容历史脏行。见 §4.3 媒体票据 |
| R2 | 全新建库(init.sql + 迁移)缺 users.role/is_banned/avatar_url/last_login_at/last_active_at,且代码引用的 notices 表全仓无 DDL |
迁移 018_v3.41_identity_notices.sql 补齐;新增 schema.drift.test.js 双向守卫(列存在 + 代码引用的表必须有 DDL) |
| R3 | run_migration.js 不报编号缺口,线上 schema_migrations 有幽灵版本时无告警 |
新增 --verify:列仓库编号缺口(允许)+ 告警「已应用但仓库无文件」的版本 |
| R4 | files.routes.v2.js 四个端点被注册两次(Express 只命中后者),前者为旧实现 |
删除旧副本,保留含章节关联与 in_pool 复位的完整实现 |
| R5 | AI 请求在参数校验前就写审计行,401 失败请求产生 2 条噪音日志 | 审计移到校验之后;补回归用例断言失败请求零写入 |
| R6 | 前后端均无全局错误兜底:Vue 渲染异常/未捕获 Promise/hydration 失败静默吞掉 | 前端 app.config.errorHandler + unhandledrejection + hydration/boot 失败 toast;新增 POST /api/v1/client-errors(免鉴权、限流 20/min、8KB 上限、仅落日志不落库) |
| R7 | fetchWithRetry 对 4xx 也重试;aiTasks/api 用固定 token 不校验有效性,401 报错文案误导 |
仅对 5xx/429 重试;统一 effectiveToken(),401 区分「登录过期」与「无权限」 |
| R8 | AI 上传通道无扩展名白名单、无总体积上限(可一次塞满磁盘) | 与文件池共用白名单常量;单次总量 >60MB → 413 并清理已落盘分片 |
| R9 | GET /api/v1/ai/providers 未鉴权,匿名可探测后端配置的供应商清单 |
加 requireAuth |
| R10 | 进程无 SIGTERM/SIGINT 处理:重启硬切在途 AI 任务与同步,ai_tasks 滞留 running |
新增 src/lib/gracefulShutdown.js:停定时器 → 停收新连接 → 等待在途(默认 15s,QBAO_SHUTDOWN_GRACE_MS 可调)→ 关连接池 → exit 0;二次信号立即退出 |
| R11 | PATCH /issues/:id/status 先读快照后开事务(TOCTOU),并发可写出互相矛盾的系统消息 |
改为 BEGIN + SELECT … FOR UPDATE 串行化,附图清理移到 COMMIT 之后(尽力而为,不牵连状态变更) |
| R12 | 全部 multer 错误一律映射 422 且直接回显英文枚举(File too large/LIMIT_FILE_COUNT):单文件超 20MB 被报成「参数错误」,用户看不懂也无法自查 |
按 err.code 分流:体积类 → 413(中文文案),其余 → 422;业务侧 ApiError 文案原样透出 |
| R13 | Provider 适配器把 fetch 与 JSON.parse 放在同一 try:HTTP 200 + 非 JSON 响应体(模型拒答文案/WAF 拦截页)直接抛出,两条生成链路写在 chatCompletions 之后的「纠正性重试(≤2 次)」永远不可达;且重试用尽后 0 题仍被标成 completed,客户端提示「已导入 0 题」 |
适配器解析失败改为返回合成 completion(保留原始文本),使重试分支可达;worker 出口新增「0 题 → failed」守卫。新增 aiGenerateFlow.e2e.test.js(4 例)锁定请求形态与三种终态 |
| R14 | 「AI 自动判定(自检)」提示词缺反向约束,模型对正确题目间歇性直接返回 [](实测 completion_tokens=2、约 1s,等于未执行审核;线上 6 次真实自检出现 4 次),自检调用被白白消耗且「模型没干活」与「审核后删光」共用同一句文案 |
提示词补「没有问题的题目必须原样保留」;runAiSelfCheck 识别「0 题且 tokens < 20」的空转结果并补显式指令重试(最多 3 次尝试),返回体新增 retried/unengaged;finalizer 对空转给出「自检未生效」专属文案与警告。新增 aiSelfCheck.retry.test.js(6 例)与 finalizer 空转用例 |
本次新增/加强的自动化守卫:schema.drift.test.js(双向 DDL 漂移 + 同一 method+path 不得重复注册)、
mediaToken.test.js(票据签名/过期/越权)、clientErrors.routes.test.js(限流与体量)、
gracefulShutdown.test.js(停机顺序/超时/二次信号)、errorHandler.unit.test.js(multer 错误分流,R12)、
ai.routes.validation.test.js(白名单零落盘、总量 413、审计零噪音、providers 鉴权)。
T5 积分并发(行锁+幂等快照+keyset 对账)、T6 成功才计费、T7 gemini SSE、T8 定时器 unref、T9 chapterMaterials 合并、 T10 beforeunload keepalive、T11 AI 任务自动续跑、T12 持久化配额治理、T13 无 rev PUT 保护、T14 v1 死代码删除、T16 上传目录收敛、T17 迁移工具化 等全部落地(全量 T1–T23 原始记录随 REVIEW-2026-08 归档 local/archive/,仅本地可见;公开口径以本文件 §9 为准)。v3.31–v3.37 增量:分层持久化 E2E、同步写收敛、safeStorage 加固、 组件/Store 拆分、API 封装统一、虚拟滚动、错题本与 AI 讲解、导入导出、登录门禁与账号隔离三层钉扎、竖屏专项、看板口径统一(恒等式单测锁定)。
git pull。Co-Authored-By 行,使公开贡献者列表只反映真人):
同样必须重新克隆;10 个既有 tag 已同步重指,文件树与重写前逐字节一致,仅提交 SHA 变化。X-Qbao-Route 路由头、出口 IP 白名单 map、
http://{ORIGIN_IP} 直连兜底等全部已废弃机制;新增 Caddy 实际路由配置(§2.4)、ufw 端口纪律(§3.1)、
单机风险与备份链路(§3.4/§7.3);占位符 {ORIGIN_IP} 废弃、新增 {HOST_ROOT}。
同日并补记第二次历史重写(移除 AI 工具署名尾注,公开贡献者列表去虚,见 §10)。