12 KiB
CLAUDE.md
OJ2 是判题狗(Online Judge)的后端重写:Django 6 → Bun + TypeScript,前后端同仓。
上一代在 ../OnlineJudge/(Django)和 ../ojnext/(Vue SPA)。
旧栈已不可逆地下线(
0002_drop_django_leftovers删掉了 Django 的框架表并已在生产库 执行完毕,漏网的一张空django_migrations由0014补删)。所以「停新栈起旧栈」已经 不是退路,唯一退路是从数据库备份恢复。旧仓库仍然零改动,没有例外 —— 包括修 bug、包括不影响外部接口的内部小修。 所有后续工作,包括在旧仓库里发现的 bug,都只落在 OJ2:先确认 OJ2 是否有对应逻辑、是否 重现了同样的问题,只在 OJ2 里修;旧仓库那边如实告知用户「未处理,按当前政策不动旧仓库」, 不要顺手改掉。冻结的理由现在只剩「留作参照、别分散精力」,不再是回滚保证。
细节文档(CLAUDE.md 只留日常要记住的,展开都在这几份里):
| 文档 | 什么时候读 |
|---|---|
docs/deploy.md |
部署、上线、备份恢复 |
docs/database.md |
写迁移、给新库打基线、drizzle-kit 抽风 |
docs/timezone.md |
动日历口径、动时间出参格式 |
docs/contract.md |
动 zod 契约、想给某个字段加校验 |
docs/ast-rules.md |
动 AST 代码规则、升级 tree-sitter |
docker/judge/README.md |
换判题沙箱镜像、升语言版本(gcc / Python …) |
docs/specs/ |
三份设计文档:后端重写、课堂求助与协作编辑、题目详情页重设计(动题目页之前必读) |
本机环境
Docker 可用,全套依赖都能在本机跑起来(PostgreSQL、Redis、判题沙箱), 镜像也能在本机构建并完整演练上线。这一点和上一代不同,别沿用「本机跑不起来后端」的旧假设。
bun install
bun run db:up # 起 postgres(5433) / redis(6380) / 判题沙箱(8081)
bun run db:migrate # 空库会从 0000 自举出全部结构
bun run dev # api(3000) + worker + web(5173) 一起起
首次要先建 .env(照 .env.example)。判题机 token 两边必须一致:
.env 的 JUDGE_SERVER_TOKEN 和 docker/.env 的 OJ2_JUDGE_TOKEN。
Bun 版本是锁死的:单二进制把运行时一起打进去,CI 装的是几、线上跑的就是几。
升级要改三处 —— 根 package.json 的 packageManager(CI 的 setup-bun 读它)和
@types/bun、docker/Dockerfile 的 oven/bun:x.y.z —— 然后 bun install。
bunfig.toml 设了 minimumReleaseAge = "3d",新发布不满 3 天的包装不上,@types/bun 除外。
常用检查:
bunx vp run verify # 下面这些一次跑完,带缓存(没改过的直接跳过)
bun run check # 格式检查 + lint,不含类型检查
bun run fmt # 格式化(bun fmt),全仓一把
bun run lint # lint(Oxlint),有一条 warning 就算失败
bun run --filter '@oj2/api' typecheck # 后端类型检查
bun run --filter '@oj2/api' check:routes # 路由遮蔽检查,加完路由跑一下
bun run --filter '@oj2/api' check:ast # AST 节点类型检查,升级 tree-sitter 后跑
cd apps/web && bun run type-check # 前端类型检查
cd apps/web && bun run build # 前端构建(不在 verify 里)
lint 和任务编排走 Vite+(vite-plus,vp 命令),格式化走 Bun 自带的 bun fmt,
配置都只在根目录的 vite.config.ts 一份:fmt 块(Oxfmt 的写法,bun fmt 直接读它;
semi: false,行宽用默认的 100)、lint 块(Oxlint,denyWarnings)、run.tasks
(verify 和它依赖的四个检查)。别用 vp fmt / vp check:Oxfmt 和 bun fmt 只在
长 CSS 值列表的折行上不一样,混着跑会把 apps/web/public/style.css 来回改。前端自己的构建
配置在 apps/web/vite.config.ts,和根目录那份无关。
- 格式化只管代码:
.md/.yml/.json/.toml在fmt.ignorePatterns里排掉了 (文档的中文表格、compose 的逐行注释是手排的),src/db/meta/快照和 unplugin 的两个.d.ts也排掉了。 run.tasks里一律经bun run <脚本>调,不直接写tsc:任务定义在根包,PATH 是根目录 的node_modules/.bin,直接写拿到的是根目录的 TS 5.9 而不是apps/api的 TS 7。- 任务名不能和根
package.json的脚本重名(Vite+ 直接报错),所以verify没有对应的bun run脚本,用bunx vp run verify。 - 缓存在
node_modules/.vite/task-cache,按读到的文件内容做指纹;怀疑缓存不对时删掉它。 - 改完代码顺手跑一下
bun run fmt。
⚠️ 前端类型检查只能走 bun run type-check 这个脚本。 两条看起来等价的路子都会静默
通过:vue-tsc --noEmit -p tsconfig.json 检查 0 个文件(那个 tsconfig 是 files: [] +
references 的壳,真正的配置在 tsconfig.app.json),而 vite build 根本不做类型检查。
改完 .vue / .ts 别拿构建当验证。
不要写测试 —— 沿用上一代的项目约定。验证靠实跑:起服务、打接口、看结果。 本机 Docker 全套都能起,实跑的成本比想象中低。
提交
直接在 main 上干活,不开特性分支。 注意 push 到 github 这个 remote 的 main
会触发 CI 部署;而 origin 会镜像到 github,平时 push origin 通常几秒后也就部署了
(偶尔不跟,要 git ls-remote github 核实)。见 docs/deploy.md。
提交信息用中文,格式 type(范围): 从用户视角描述现象,例如
fix(流程图): 「重新判题」从来没成功过,而且会把原来的评分清掉。
常用 type:fix / feat / refactor / perf / chore / style。
几件必须知道的事
后端专属的坑 —— 单二进制不能依赖 node_modules、路径走 runtime.ts、SQL 判题 spawn 自己、
路由遮蔽、出参用 satisfies 不用 parse、外键删除动作与迁移 —— 在 apps/api/CLAUDE.md,
动 apps/api 的文件时会自动加载。下面是全仓通用的。
判题状态码不能改
apps/api/src/judge/status.ts 和 apps/web/src/utils/constants.ts 必须一致。
这些整数是落库的值:12 万条历史提交的 submission.result 就是它们,判题沙箱回的也是
这套编码,所以只能新增、不能改已有的含义。题目表情 reaction 的语义 key 同理。
判题镜像是自己构建的
compose.*.yml 里的 oj2-judge-2 不在任何 registry 上:上游
QingdaoU/JudgeServer 停更在 2024-04(官方镜像的 latest 和 1.6.1 是同一份,
编译器停在 gcc-13),新工具链只能自己编。docker/judge/ 里是只改版本的 Dockerfile
分叉 + 构建脚本 + 冒烟测试,判题逻辑一行没动。
- 构建、分发、改名的步骤见
docker/judge/README.md。服务器和机房各有各的判题沙箱,两边都要装。 - 编译/运行命令在
apps/api/src/judge/languages.ts,不在镜像里。gcc-14 把隐式函数 声明等提成了 error(-w压不住),那边的cLooseErrors几个-Wno-error=就是 为此加的 —— 删掉它们等于让一批历史题解和 C 教程示例集体 CE。 - 判题沙箱只认 C / C++ / Python。 Java / JavaScript / Golang 连同镜像里的
JDK / Node / Go 在 2026-09 一起砍了(前端本来就没给入口,12 万条提交里它们共 62 条),
契约
judgeLanguageSchema里的键留着是为了渲染那 62 条历史提交。Python3/Python2这两个旧值已经没有了 —— 0019 迁移把 104530 条提交、937 道题、 1235 个用户的成就指标并成了一个Python,0020 顺手把那三种语言从题目的可选语言里摘掉 (不摘的话 84 道题的语言下拉还能选 Java,提交必 SYSTEM_ERROR)。查判题配置走judgeConfigFor(),它带旧值别名;回滚要连数据一起回,只滚代码会让 Python 提交全炸。 - 换完镜像跑
bun docker/judge/smoke.ts:三种语言、六种状态码、gcc 宽松度一起核。
AST 代码规则:一张表,外加一个机器检查
契约的 AST_NODE_TARGETS_BY_LANGUAGE 是唯一一张表(label 给界面、node 给判题机),
判题机侧没有第二张表,所以加 target 漏配节点类型在结构上不可能。但配错仍然可能,
而且完全静默 —— 节点类型对不上就是「必须使用 X」永远失败、「不能使用 X」永远通过。
bun run --filter '@oj2/api' check:ast # 升级 tree-sitter-* 之后一定要跑
判题机只认 C / C++ / Python(AST_SUPPORTED_LANGUAGES),别的语言配了规则一条都不会跑,
所以后台不给它们开 tab —— 看得见却不检查比没有更糟。C++ 的调用形态和 C 不一样、
规则的语义校验为什么不挂在 zod 上,见 docs/ast-rules.md。
比赛只有 ACM 模式
没有 OI。上一代残留的 OI 分支在阶段 0 已经砍掉,不要「顺手补回来」。
前端基线是 Chrome 105
机房部分电脑是 Chrome 105,其余更新 —— 按最低那档定基线。
@vitejs/plugin-legacy 和写死的 polyfill 清单别删(vite 8 默认 target 是 chrome111)。
升级前端依赖后要重新审计 polyfill 清单,步骤与 View Transitions 的降级见 apps/web/CLAUDE.md。
时间只有一个锚点:apps/api/src/time.ts
凡是要把一个时刻换算成「哪一天 / 几点 / 哪一年」,一律走那个模块。 不要写
new Date(x).getHours()、setHours(0,0,0,0)、getFullYear()、new Date(y, m, d) 这类跟
进程时区走的代码 —— 容器是 UTC、开发机是本机时区,两边答案不同而且不报错。
SQL 里要按日历切,用 localTime(列)(生成 列 at time zone 'Asia/Shanghai'),
别依赖数据库会话时区。
分层:存 UTC 时刻 → 后端判定按东八区 → 出参 ISO UTC → 前端按东八区渲染。
- 存:时间列全是
timestamptz,写侧一律new Date().toISOString()。 - 判定:日历语义走
time.ts,SQL 用localTime()。 - 出参:
db/index.ts给 OID 1184 挂了 parser,读出来的时刻统一成 ISO 8601 UTC, 微秒必须保留(截成毫秒会让翻页每页丢一条、班级 AC 排名少 1)。 - 渲染:前端
parseTime()/zonedParts()按同一个固定偏移取东八区部件 (见apps/web/CLAUDE.md)。
时区常量 TIME_ZONE / TIME_ZONE_OFFSET_MINUTES 在 packages/contract/src/time.ts,
前后端共用一份,按固定偏移算(大陆 1991 年起没有夏令时),与旧栈的东八区口径一致。
再动日历口径之前先读 docs/timezone.md,那里有实测数据和核实方法;
Dockerfile 的 TZ 和数据库连接的 TimeZone 是刻意不设的,别「顺手补上」。
部署
三套 compose 在 docker/:dev(本机)、debian(服务器)、school(机房)。
机房那套没有 postgres,连的是服务器的库。 两个站点共用一个数据库,但各有各的 Redis 和判题沙箱 —— 所以涉及两边的变更要一起做。
库是 PostgreSQL 18,本机 dev 用同一个大版本,别单独换。迁移在部署时自动跑,破坏性迁移
默认拦截,要 OJ2_ALLOW_DESTRUCTIVE=1 docker/deploy.sh 显式放行。
⚠️ DATA_DIR 默认值 ../data 是 OJ2/data,不是部署目录的 data/。 沿用旧数据却忘了
设它,会静默起一套空数据(空库、没测试点、图片 404),而且不报错 —— 这是整个部署里
唯一会静默走歪的地方,deploy.sh 为它专门设了一道自检。
上线两条路(push 触发 CI / 手工 docker/deploy.sh)、部署后的验证清单、NPM 反代那两个
不能关的开关、备份恢复的两个坑,都在 docs/deploy.md。