Files
xuyueandClaude Opus 5.5 1b703d13ae
Deploy / deploy (push) Canceled after 0s
style(专题): 「题单」改名「专题」,顶栏、列表、详情、后台、成就说法统一
只改界面文字和注释:网址 /problemset、接口、表名、字段名都不动,老链接照常能打开。
设计稿页名「题单重设计」和已执行的迁移文件保留原样。
线上 3 个成就(题单收藏家 / 入门 / 通关)已同步改名。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 00:12:18 +08:00

84 lines
5.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
OJ2 的后端(`apps/api`)。这里只放后端专属的坑;全仓通用的约束(旧仓库冻结、判题状态码、
时间锚点、判题镜像、部署)在 `../../CLAUDE.md`。
## 单二进制是有代价的
`apps/api` 编译成 `bun build --compile` 的单二进制,所以**运行时不能依赖 node_modules**。
任何 `require.resolve` / `Bun.resolveSync` / `__dirname` 去找文件的写法,本地都正常、编译后
都会炸,而且**只在离开仓库目录后才炸**(在仓库里跑时它顺着 cwd 摸到了 node_modules,
假装没事)。
资源要用 `with { type: "file" }` 内嵌。`.node` 原生模块还要额外注意:这个写法只有打包器认、
`bun run` 不认,所以必须按形态分叉 —— 见 `src/vendor/jieba.ts` 的注释,那里把坑写全了。
**改完这类代码,dev 和编译两种形态都要跑一遍。** 我吃过亏:只验了编译产物,dev 直接起不来。
`build` 带 `--bytecode --format=esm`:每道 SQL 题都要起一次 `sql-child`,预编译字节码让它
从约 120ms 降到约 72ms(单核实测),`serve` 起服务从约 180ms 降到约 100ms,内存不变,
代价是二进制大 8MB。去掉 `--format=esm` 会退回 CJS 输出,顶层 await 编不过。
## 路径解析看 `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`。
## 出参不 `parse`,用 `satisfies`
**后端的响应一律 `satisfies XxxType`,不要写 `xxxSchema.parse({...})`。** 出参是后端自己刚
拼出来的字面量,TS 已经在编译期校验过;再 parse 一遍拿不到任何新信息,唯一可能失败的输入是
**库里的历史数据**,而失败的代价是 500 —— 这条规矩是被四次这样的线上故障换来的。
**闸设在写入侧**:入参 `safeParse`、`db/schema.ts` 的 `.$type<>()` 列收窄、
语义校验函数(`astRulesError()` / `exerciseDataError`)。JSONB 原文
(`submission.info` / `statistic_info` / `exercise.data`)一律放行,它们的形状真相在判题机
那边。query 的筛选值走 `routes/helpers.ts` 的 `asFilterValue()`,那是纯类型交接、不加校验。
四次故障的细节、`.$type<>()` 断言该怎么核,见 `../../docs/contract.md`;
前端为什么只在三处挂运行时闸门,见 `../web/CLAUDE.md`。
## 数据库
Drizzle schema 最初是 `drizzle-kit pull` 从生产库拉出来的,所以它长得像 Django 建的表
(表名、bigint/int4 混用),`schema.ts` 顶部记了哪些地方是手工修的。
**schema 现在归 OJ2 独占**,结构变更走 migration 正常演进。
**外键的删除动作从 0010 起是显式的**,不再是 Django 留下的一律 NO ACTION:
- **CASCADE**:父行消失后子行必然无意义、且不构成「学生做过什么」的证据 —— 中间表
(problem_tags)、专题/教程/成就的组成部分、一对一附属(user_profile)与可重算的缓存
(user_stat)。
- **NO ACTION(即拦住)**:需要人看见的删除 —— `submission.problem_id`、以及 `user` 的绝大
多数外键。删用户撞外键会被 handler 翻译成「请改为禁用账号」,这是有意的。
**加新子表时必须回来想一遍该走哪一档**,别默认新外键会自己连坐 —— drizzle 不写
`.onDelete()` 就是 NO ACTION,而 0010 只改了当时存在的那批。
### 改 schema 走 drizzle migration
`bun run db:generate`(造迁移文件)→ `bun run db:migrate`(按
`drizzle.__drizzle_migrations` 增量执行),就是 Django `makemigrations` / `migrate` 的
等价物。索引/结构变更走这条,不要再手写 SQL 往 `docs/` 里塞。
- **部署时自动执行**:`docker/deploy.sh` 在构建镜像之后、起栈之前跑,失败就中止部署。
- **破坏性迁移默认拦截**(`DROP TABLE` / `DROP COLUMN` / `ALTER COLUMN ... TYPE` /
`TRUNCATE`),退出 4,要显式放行:`OJ2_ALLOW_DESTRUCTIVE=1 docker/deploy.sh`。
`CREATE INDEX CONCURRENTLY` 怎么写、给已有库打基线的 SQL、`.op()` 会吞掉索引方向这类
drizzle-kit 的坑,全在 `../../docs/database.md`。