## 切换前核对:前端调的接口后端有没有漏
写脚本把前端 224 个调用点和后端注册的路由逐条比对。**零缺口** —— 唯一报出来的
一条是我的正则被嵌套括号截断了(`users/${encodeURIComponent(...)}/badges`),
后端那条路由是有的。「后端有、前端没调」那 25 条也逐个看过,全是脚本的假阳性
(把 `.get("user")` 这类非路由调用当成了路由)和假阴性(前端用三元表达式拼路径,
正则看不见,比如 `GET /me` 其实在 shared/api.ts 里被调)。
结论是没发现缺口,但这个脚本不够可靠、不足以证明"一定没有",所以没留进仓库。
## 路由遮蔽检查(留成常驻脚本)
比"有没有漏"更值得防的是遮蔽:**Hono 按注册顺序匹配,不是静态优先**。
`/problems/:id` 注册在 `/problems/random` 前面的话,后者永远进不去 ——
不报错、不警告,只是静默走进前一条的 handler。阶段 4 真实发生过一次,
两个教师用的分析端点被吃掉,一直到评审才发现。
全仓 167 条路由按真实注册顺序扫:**零遮蔽**。
这个结论敢下,是因为检测器本身也验了:
- 自检用例里放了阶段 4 那个历史真实案例,能抓到;边界(两边都是参数、
段数不同、不同前缀)不误报
- 核对了 24 个 router 全在扫描范围内,没有漏扫
- 反向验证:往 problem.ts 末尾加一条注册在 `:displayId` 之后的字面量路由,
脚本立刻报出来并 exit 1
未经验证的检测器报"没问题"是没有意义的 —— 这个教训今天已经吃过两次
(tree-sitter 那次、SQL 内存那次)。
脚本落在 apps/api/src/scripts/check-route-shadowing.ts,
`bun run --filter '@oj2/api' check:routes`,加完路由跑一下。
CLAUDE.md 里也写了。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5.0 KiB
CLAUDE.md
OJ2 是判题狗(Online Judge)的后端重写:Django 6 → Bun + TypeScript,前后端同仓。
上一代在 ../OnlineJudge/(Django)和 ../ojnext/(Vue SPA),两者都已冻结,
是回滚路径,任何情况下都不要改。
设计文档:docs/specs/2026-08-06-bun-backend-rewrite-design.md
切换手册:docs/specs/phase5-cutover-runbook.md ← 上线当天照这份走
仓库结构
| 目录 | 作用 |
|---|---|
apps/api/ |
后端。Hono + Drizzle + BullMQ,编译成单二进制 |
apps/web/ |
前端。从 ojnext 原样搬来的 Vue 3 SPA |
packages/contract/ |
前后端共用的 Zod 契约 |
docker/ |
Dockerfile + 三套 compose(dev / debian / school) |
docs/specs/ |
设计、端点清单、各阶段评审报告与演练报告 |
本机环境
Docker 可用,全套依赖都能在本机跑起来(PostgreSQL、Redis、判题沙箱), 镜像也能在本机构建并完整演练上线。这一点和上一代不同,别沿用"本机跑不起来后端" 的旧假设。
bun install
bun run db:up # 起 postgres(5433) / redis(6380) / 判题沙箱(8081)
bun run dev # api(3000) + worker + web(5173) 一起起
首次要先建 .env(照 .env.example)。判题机 token 两边必须一致:
.env 的 JUDGE_SERVER_TOKEN 和 docker/.env 的 OJ2_JUDGE_TOKEN。
常用检查:
bunx tsc --noEmit -p apps/api # 后端类型检查
bun run --filter '@oj2/api' check:routes # 路由遮蔽检查,加完路由跑一下
cd apps/web && bun run build # 前端构建(vite 不做类型检查,构建即验证)
不要写测试 —— 沿用上一代的项目约定。验证靠实跑:起服务、打接口、看结果。
几件必须知道的事
单二进制是有代价的
apps/api 编译成 bun build --compile 的单二进制,所以运行时不能依赖
node_modules。任何 require.resolve / Bun.resolveSync / __dirname 去找文件的
写法,本地都正常、编译后都会炸,而且只在离开仓库目录后才炸(在仓库里跑时它顺着
cwd 摸到了 node_modules,假装没事)。
资源要用 with { type: "file" } 内嵌。.node 原生模块还要额外注意:这个写法
只有打包器认、bun run 不认,所以必须按形态分叉 —— 见 apps/api/src/vendor/jieba.ts
的注释,那里把坑写全了。
改完这类代码,dev 和编译两种形态都要跑一遍。 我吃过亏:只验了编译产物, dev 直接起不来。
路径解析看 runtime.ts
编译后 import.meta.dir 恒为 /$bunfs/root,往上三级就是文件系统根。
相对路径一律走 runtime.ts 的 pathBase,别自己拼。
SQL 判题会 spawn「自己」
judge/sql/index.ts 起的子进程是二进制自身 + sql-child 子命令(因为编译后磁盘上
没有 child.ts 可以 spawn)。所以入口必须有 argv 分发,否则「起自己」变成
「把整个程序再跑一遍」→ 指数级 fork。这不是假想,开发时炸过一次开发机。
OJ2_SQL_CHILD 那道递归闸不要删。
加路由要防遮蔽
Hono 按注册顺序匹配,不是静态优先(实测确认过,别凭直觉)。/problems/:id
注册在 /problems/random 前面的话,后者永远进不去 —— 而且不报错、不警告,
只是静默走进前一条的 handler。阶段 4 真实发生过一次,两个教师用的分析端点被吃掉,
一直到评审才发现。
加完路由跑 bun run --filter '@oj2/api' check:routes。
判题状态码要三处同步
apps/api/src/judge/status.ts、apps/web/src/utils/constants.ts、
以及上一代的 ../OnlineJudge/submission/models.py(回滚时要对得上)。
题目表情 reaction 的语义 key 同理。
比赛只有 ACM 模式
没有 OI。上一代残留的 OI 分支在阶段 0 已经砍掉,不要"顺手补回来"。
前端要兼容老 Chrome
机房电脑 Chrome < 94。mermaid-legacy 等 fallback 依赖和 vite 的构建 target
不能动,vite.config.ts 里有注释说明。
数据库
Drizzle schema 是从生产库 drizzle-kit pull 出来的,不写迁移。
新旧后端跑在同一套表结构上(阶段 5 演练逐列比对过,零差异),这是回滚能成立的前提 ——
所以改 schema 前先想清楚回滚怎么办。
生产库的几个约定:
raw_password明文列要保留,老师用它找学生密码。不要"顺手清理"。judge_server_heartbeat保留。problem.prompt是给未来 AI 预留的,当前没接线,不要删。
部署
三套 compose 在 docker/:dev(本机)、debian(服务器)、school(机房)。
机房那套没有 postgres,连的是服务器的库。 两个站点共用一个数据库, 但各有各的 Redis 和判题沙箱 —— 所以上线那天两边必须一起切。
细节和演练结果都在 docs/specs/phase5-cutover-runbook.md。