Files
OJ2/apps/api/src/main.ts
yuetsh fafeebd281
Some checks failed
Deploy / deploy (push) Has been cancelled
feat(运维): 加 oj2-api recount,把反范式计数列对回 submission 表
problem.submission_number / accepted_number / statistic_info 和 user_profile 的
submission_number / accepted_number / acm_problems_status 由 judge/run.ts 的
persistResult 在判题时手工加减,上线至今没人事后核对过。

已知的漂移来源是重判:routes/submission.ts 的 rejudge 把 result 打回 PENDING
就重新入队,不回退任何计数,persistResult 随后再加一次。实测重判一条提交,
problem 的 submission_number、statistic_info 和 user_profile 的 submission_number
全部虚增,而提交一条没多。

默认只读预演,--apply 才写,落库后用同一份 computePlan 复核,还剩差异就非零退出。
口径逐条照抄 persistResult:题目侧连比赛提交一起算、用户侧只算非比赛;
accepted_number 是去重到题的首次通过;acm_problems_status 通过过就恒为 ACCEPTED,
没通过过取最后一次结果。acm_problems_status 里 problems / contest_problems 之外
的顶层键原样保留 —— 来历不明的数据不该被重算顺手抹掉。

不管的:acm_contest_rank(罚时与每题尝试次数口径复杂,单独一件事)、
achievement.unlock_count(0010 之后随成就级联,漂不了)、题单进度与奖章
(走 backfill-problemsets)。

--apply 要挑没人做题的时候跑:差异在事务外算、写的是绝对值,算完到写完之间判完
的那一笔加法会被覆盖;复核会把它报成「仍有 N 处差异」并以 1 退出,不会静默。

验证:dev 库先备份计数列,测完原样还原(差异数 0)。验过收敛(--apply 后再跑报
一致)、真实重判造成的漂移精确报出 3 处且无误报、人为删掉某学生已 AC 的格子能按
「曾经 AC → ACCEPTED」恢复、注入的未知顶层键完好保留。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeJoYc2t2d7cThVqMBYrBF
2026-09-02 22:58:01 -06:00

75 lines
3.2 KiB
TypeScript
Raw 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.
/**
* 唯一入口。所有角色都从这里按子命令分叉。
*
* 为什么不保留三个独立入口文件:`bun build --compile` 一次只产出一个二进制,
* 而部署要跑 HTTP 服务、判题 workerSQL 判题还要 fork 一个能被 SIGKILL 的子进程
* (见 judge/sql/index.ts。三个入口就得编三个二进制、镜像里塞三份运行时。
* 一个二进制 + 子命令镜像里只有一份compose 里改 command 就能换角色。
*
* oj2-api # 等同 serve
* oj2-api serve # HTTP + WebSocket
* oj2-api worker # BullMQ 判题消费者
* oj2-api healthcheck # 探活,给 Dockerfile 的 HEALTHCHECK 用
* oj2-api sql-child # SQL 判题子进程,由服务自己 spawn不该手动调
* oj2-api migrate # 执行待办的数据库迁移,部署时由 docker/deploy.sh 调
* oj2-api backfill-problemsets # 把题单进度与奖章订正到与规则一致,默认只读预演
* oj2-api recount # 把题目/用户的计数列重算回与 submission 一致,默认只读预演
*
* 用动态 import 而非顶层 import这几个模块都有导入即执行的副作用
* Bun.serve、连 Redis 开消费者),静态导入会让 sql-child 也把整个服务拉起来。
*/
export {} // 只有动态 import 的话 TS 不认这是模块,顶层 await 会报错
const command = process.argv[2] ?? "serve"
switch (command) {
case "serve":
await import("./index")
break
case "worker":
await import("./worker")
break
case "migrate": {
const { runMigrations } = await import("./db/migrate")
await runMigrations()
break
}
// 一次性的数据订正,跟着二进制走而不是留成源码脚本 —— 生产镜像里没有 bun 也没有源码
case "backfill-problemsets": {
const { backfillProblemSets } = await import("./scripts/backfill-problemsets")
const args = process.argv.slice(3)
process.exit(await backfillProblemSets({
apply: args.includes("--apply"),
allowRevoke: args.includes("--allow-revoke"),
}))
}
// 同上,一次性的数据订正。反范式计数列被重判等操作带偏之后拿它对账。
case "recount": {
const { recount } = await import("./scripts/recount")
process.exit(await recount({ apply: process.argv.slice(3).includes("--apply") }))
}
case "sql-child": {
const { runSqlChild } = await import("./judge/sql/child")
await runSqlChild()
break
}
// 运行镜像是 debian-slim没有 curl/wget探活让二进制自己做。
// 只打 /health不碰库 —— 库挂了该由库自己的 healthcheck 报,
// 不该让 api 容器跟着被判成不健康、进而被重启。
case "healthcheck": {
const { config } = await import("./config")
try {
const response = await fetch(`http://127.0.0.1:${config.port}/health`, {
signal: AbortSignal.timeout(3000),
})
process.exit(response.ok ? 0 : 1)
} catch {
process.exit(1)
}
}
default:
console.error(`未知子命令:${command}\n可用serve | worker | migrate | backfill-problemsets | recount | healthcheck | sql-child`)
process.exit(2)
}