Commit Graph
4 Commits
Author SHA1 Message Date
xuyueandClaude Opus 5 ed56a209ea chore(格式): Prettier 统一到全仓,后端和契约一次性格式化
Deploy / deploy (push) Has been cancelled
原来只有 `apps/web` 在 Prettier 下(配置在 `apps/web/.prettierrc.toml`、脚本在
web 的 package.json),后端和契约从来没格式化过 —— 手写在 100 列上下,`db/schema.ts`
还是 drizzle-kit pull 留下的 tab 缩进。两套口径分叉久了,跨端改一处就得记着「这边
什么风格」。

- 配置搬到根目录 `.prettierrc.toml`,内容不变(`semi=false`,其余全默认,
  printWidth 80 —— 和前端已有的格式一致,不另立一套宽度);
- 脚本统一成根目录 `bun run fmt`,覆盖 `apps/*/src`、`apps/web/tests` 和两个构建
  配置;web 自己那份 `fmt` 和重复的 prettier 依赖删掉;
- `.prettierignore` 挡掉两类不该碰的:drizzle-kit 生成的 `src/db/meta/` 结构快照
  (它是 db:generate 的比对输入,只该由 drizzle-kit 写)、unplugin 每次 dev 都会
  重写的 `auto-imports.d.ts` / `components.d.ts`;
- 全量跑了一遍。纯格式,无行为改动:api typecheck / check:routes / check:ast、
  前端 type-check 全过,起 api 打了接口确认正常。前端这 39 个文件的小改动是
  prettier 版本漂移(类型断言的换行口径变了),不是新配置带来的。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 08:27:34 -06:00
xuyueandClaude Opus 5 e2c6ee69da fix(阶段5): jieba 资源导入不能用在 dev;补上 /public/upload 的伺服
## jieba:两种形态得走两条路

上一条 commit 把 jieba 的 .node 改成 `with { type: "file" }` 内嵌,只验证了
编译产物,没跑 `bun run` —— 结果 dev 直接起不来:

    TypeError: To load Node-API modules, use require() or process.dlopen
               instead of import.

`.node` 的资源导入只有打包器认,运行时不认。而且必须是**动态** import:
静态 import 在模块加载时就求值,拿 isCompiled 判断也来不及。

所以分两条路:dev 用包自己的入口(那条路 bun run 下是好的),编译形态才走
内嵌资源。两边都实测过了。.wasm 没这个毛病,两种形态都正常。

withBuiltinDict 因此变成 async,连带 buildWordFrequencies 也是。
jieba 实例改成缓存 Promise 而不是结果,并发进来两个请求只会建一次词典。

## /public/upload 之前根本没人伺服

后台上传图片存盘、返回 `/public/upload/<name>`,但没有任何路由处理这个前缀 ——
题面里插的图片一律 404。补上,并和头像共用一个 serveUpload:只取路径最后一段
且要求它和原样一致,`..`、子目录、编码斜杠都在这里被拒。

生产环境这些请求也走后端(Caddy 反代整段 /public),不让 Caddy 直接读盘,
这样开发和生产是同一条代码路径。

dev 模式 8 项实测:上传图片/头像/默认头像回退 200,不存在、`..`、编码穿越、
子目录、编码斜杠全 404。编译产物在干净目录里 7 项照旧全过。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 23:20:12 -06:00
xuyueandClaude Opus 5 ce21a2bb8f feat(阶段5): 让 bun build --compile 的产物真正自足
编译产物拿到没有 node_modules 的目录里跑,原来是一路崩的 —— 而且**在仓库
目录里跑时全都正常**,因为它顺着 cwd 找到了 node_modules,假装没事。
这类问题只会在服务器上第一次启动时暴露。逐个堵掉:

- `import.meta.dir` 在二进制里恒为 `/$bunfs/root`,往上三级就是文件系统根:
  `data/test_case` 悄悄变成 `/data/test_case`,`.env` 去读 `/.env`。
  新增 runtime.ts 显式分叉:编译后按 cwd,开发时按仓库根(后者不能改,
  compose 挂给判题沙箱的是仓库根的 data/test_case)。

- sql.js / tree-sitter 的 wasm、jieba 的 .node 和 4.8MB 词典,
  原来都靠运行时 require.resolve / Bun.resolveSync / __dirname 去 node_modules 里找。
  全部改成 `with { type: "file" }` 内嵌成资源。jieba 尤其绕:它的 index.js
  运行时探测平台再 require 子包,dict.js 用 __dirname 读 dict.txt,两条都
  依赖磁盘布局,所以单独包了 vendor/jieba.ts 直接 require 内嵌的 .node。

- SQL 判题要 spawn 一个能被 SIGKILL 的子进程,原来 spawn 的是 child.ts 的路径,
  编译后那个文件不存在。改成二进制自己按 argv 分发:新增 main.ts 作为唯一入口,
  serve / worker / sql-child 三个子命令,镜像里只需要一份运行时。

## 顺带修掉一个能拖垮生产的坑

「spawn 自己」意味着只要 argv 分发这一环出问题(子命令改名没同步、compose 里
command 写错、拿别的入口编了二进制),「起自己」就变成「把整个程序再跑一遍」,
而那一遍又会 spawn 一个自己 —— 指数增长。

这不是假想:开发时用一个没有分发器的临时入口编了个二进制,一跑就递归 fork
105MB 的进程,几秒触发 global OOM,内核把开发机的终端杀了
(`selftest invoked oom-killer ... Killed process ... Alacritty`)。
同样的错误发生在服务器上就是判题机连同数据库一起拖死。

加了递归闸:父进程 spawn 时打 OJ2_SQL_CHILD 标记,带标记的进程一律拒绝再 spawn,
最多一层就停在一条明确的 SYSTEM_ERROR 上。

## 验证

产物拷进只有它自己一个文件的目录,2G 内存上限下跑,7 项全过:
jieba 分词(自定义词「循环结构」「死循环」命中)、tree-sitter Python3/C 各两条
(含一条**预期失败**的规则做对照,否则「语言没加载成功」和「规则通过」返回值
一模一样,分不出来)、SQL 判题 AC、SQL 只读防护仍拦住 PRAGMA。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 23:13:57 -06:00
xuyueandClaude Opus 5 836d97847e feat(阶段3): 补齐用户侧依赖的三个 admin 端点
重判、提交统计、流程图统计这三条挂在旧后端的 admin 路由下,权限也确实是
teacher/super admin,但入口在用户侧页面里(提交列表页的重判按钮、两个统计面板)。
不做完,阶段 3 的出口标准「用户侧全部功能跑在新后端上」就不成立 —— 按 URL
前缀切阶段会漏掉它们。

- POST submissions/:id/rejudge     ← GET admin/submission/rejudge
- GET  submissions/statistics      ← GET admin/submission/statistics
- GET  flowcharts/statistics       ← GET admin/flowchart/statistics

几处对齐旧后端的细节:
- AST_CHECK_FAILED(10) 与 ACCEPTED(0) 同算通过
- 完成度先用原始花名册人数算、再修正 person_count,顺序照搬,兜住「学生已删号
  但提交记录还在」
- 有提交但零通过的学生,两个名单里都不出现(旧后端同样口径)
- 词云用 @node-rs/jieba,STOPWORDS 与 38 个自定义词逐词照搬;jieba@2 没有
  insertWord,改用 loadDict 加载用户词典
- avgScore 分母是有分数的条数,对齐 Django Avg() 跳过 NULL

rejudge 的 jobId 带时间戳。队列保留最近 100 个已完成任务,沿用 submissionId
做 jobId 的话 BullMQ 会认为任务已存在,重判会静默变成空操作。

correctRate 改成数值不带 %,展示格式化交给前端。stripClassPrefix 用
startsWith+slice 而不是 replace,前缀对不上时不会从中间截出乱码;site.ts
原有的 replace 写法一并改掉。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 06:48:05 -06:00