Commit Graph

18 Commits

Author SHA1 Message Date
e3faa689e7 feat(阶段2补课): SQL 判题链路 + 最后两个后台端点
新后端此前完全没有 SQL 判题(旧 judge/sql_runner.py 378 行 + sql_dispatcher.py
113 行无对应实现),阶段 2 纵切时漏了这条与沙箱完全不同的路径。

  judge/sql/engine.ts   判题核心,移植自 sql_runner.py,判定口径逐条对齐
  judge/sql/child.ts    子进程入口
  judge/sql/index.ts    父进程:spawn + 硬超时
  judge/run.ts          language === "SQL" 时分流,不经判题沙箱
  POST admin/sql-test-cases/preview    题目页展示数据预览
  POST admin/sql-test-cases/generate   AI 按标准答案倒推初始化脚本

题目保存时重新生成 sqlDisplay(对齐旧 generate_sql_display):取测试点 1 的初始化
脚本 + 标准答案跑一遍,失败一律拦下不让保存 —— 展示数据直接决定学生看到的表结构
与期望结果,宁可不保存也不能存错的。

## 防护换了实现,逐条实测

bun:sqlite 没有 authorizer / progress_handler / setlimit,且实测 Worker.terminate()
杀不掉跑飞的查询(原生代码占着线程)。改用「WASM 引擎 + 独立子进程」:

  ATTACH  → WASM 无宿主文件系统绑定,结构上够不到(比旧的 authorizer 更强)
  查询题只读 → PRAGMA query_only=1
  超时    → 子进程外部 SIGKILL
  单值内存 → 子进程 ulimit -d

八条提交实测:正确→Accepted;列少一个/漏过滤→Wrong Answer;语法错误→Compile Error;
查询题里 INSERT→运行错误并说明;递归 CTE 死循环→CPU 超时;hex(zeroblob(2e8))→内存超限;
attach '/etc/passwd'→打不开。

## 踩到的两个坑(已写进 docs/specs/phase3-coverage.md)

1. ulimit 必须用 -d 不能用 -v。-v 限虚拟地址空间而 JS 引擎预留巨量地址,实测 -v 之下
   Bun 退出时有概率 panic(SIGILL),结果早已写出但进程异常终止,父进程读到空串
   误判成超时 —— 6 次里坏 2 次。换 -d 后 12/12 稳定。
2. 子进程写完结果直接 SIGKILL 自己,不走 process.exit()——后者仍有清理会撞限额。

至此 admin/api.ts 已无任何指向旧后端的调用。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:03:41 -06:00
764e0d28cc feat(阶段4): 标签管理 / 批量打标签 / 题目可见性 / 卡点与 AC 趋势 / 流程图 AI
GET/PUT/DELETE    admin/problem-tags[/:id]
  POST              admin/problems/batch-tag
  PUT               admin/problems/:id/visibility
  GET               admin/problems/stuck
  GET               admin/problems/ac-trend
  POST              admin/problems/flowchart

要点:

- 后台标签列表用 leftJoin 且不加 having,能看到 problemCount=0 的标签 ——
  那正是要清理的那些。oj 侧的 /problem-tags 才过滤 >0。
- 标签改名撞上已有标签视为合并:只给「还没挂目标标签」的题目补关系,
  否则会撞 (problem_id, problemtag_id) 唯一约束。
- 批量打标签:add 时按需新建标签、remove 时只认已有标签 ——
  否则「移除」会顺手造出一堆空标签。名字去重且大小写不敏感。
- **旧 ProblemVisibleAPI 的 `self.error(...)` 少写了 return**,题目不存在时会继续
  往下跑并抛 AttributeError(500)。这里正常返回 404。

顺带记下一条阶段 5 的必做项(见 phase3-coverage.md 文末):本地库按显式 id 从生产
导入,序列没跟着走,第一次新建标签就撞 problem_tag_pkey。只要切换流程里有
「导出→导入到新库」这一步,就必须重置全部序列,否则读全正常、第一次写才炸。

实测:学生 403;大小写重复与空白名去重后 tagCount=1、重复 add 幂等、
remove 不存在的标签 404 且不新建;纯改名 merged=false、合并 merged=true
affectedCount=1 且只剩一个标签;可见性取反两次复原、不存在的题 404。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:30:48 -06:00
359b91ab4d docs(阶段3): 记录 7 条 Minor 的逐条结论
其中两条判为无需改动并说明理由:M-2(user-progress 的 realName)已被 F2 的
sampleUser 默认关闭开关覆盖,现在与旧后端一样返回 null;M-3(练习答案下发)
旧后端逐字相同,属教程练习「客户端比对」的既有设计,不是本次重写引入的回归。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 07:01:13 -06:00
ced8f3ee1b docs(阶段3): 记录出口标准达成;补跑 prettier
覆盖率对账表更新:admin 侧 3/45 已实现,缺口 42。达成判据是 apps/web 的
oj/ 与 shared/ 两个目录已无指向旧 Django 的运行时调用,残留的 utils/http
引用只剩 ApiResponse 这一个类型。

顺带补跑 npm fmt,几个此前未格式化的文件随之改动,均为纯格式。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 06:48:20 -06:00
cb6d42363b docs(阶段3): 修复波复评通过,安全 findings 收口
8 条(F1-F6 + 收尾 F4b/F5b)全部 ADDRESSED,修复 diff 内无新引入破坏。

复评补上了控制方没验充分的 F4b:真造了一条比赛提交,确认数据库存真值
而 API 返回给本人的是 info:{} / ip:null / contestId:null。

另核实 sampleUser() 是真正的默认关闭开关(覆盖全部 14 个下发点)、
isRegularUser 全仓确实只有一个调用点、.env 加载器优先级正确、
限流参数与旧后端 options/options.py 逐值吻合。

评审产物从 gitignore 的 .superpowers/sdd/ 归档进 docs/specs/ 以免丢失。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 06:26:20 -06:00
9c04b00e3f docs(阶段3): 权限与泄露双评审报告 + 合并修复清单
两份评审独立进行、互不知情,各自命中同两条问题(匿名可读用户档案、
realName 无条件下发),独立复现提高可信度。严重度取更严一方 ——
使用者是中职学生,姓名邮箱班级属个人信息。

3 Critical / 3 Important,F1-F3 已由控制方独立实跑复现。
无敏感字段泄露、无泄题,比赛权限重建得最好。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 01:43:55 -06:00
0f999aa5b1 docs: 阶段 0/1/2 出口标准核验 + 阶段 3 覆盖率对账
核验为逐条实跑,不采信文档声称。阶段 0/1/2 出口标准全部通过,
其中阶段 2 判题链路端到端实测(注册→登录→提交→JudgeServer→WS 推送)。

覆盖率对账:oj 侧 65 条已全部实现,admin 侧 45 条一条未做。
新旧路径不同名(API 已重新设计),对照关系为人工按语义逐条比对。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 01:25:36 -06:00
0a8fcef3f2 chore(阶段1): 题目样本导入脚本(不含用户数据) 2026-08-06 21:14:23 -06:00
63faf5c6c0 docs: 更正本机开发环境约束 —— Docker 可用
根 CLAUDE.md 长期写着本机跑不了 Docker/PostgreSQL/Redis/判题沙箱,实测不成立:
docker 29.7.2 与 docker-compose 5.4.0 早已安装,只是服务未启用、用户不在 docker 组。
另确认判题沙箱不需要特权模式(read_only + cap_drop,只减能力不加)。

影响:阶段 2 判题竖线可在本机完整验证并反复试错,不必每次推服务器。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 20:30:17 -06:00
9f5dd95289 docs: raw_password 保留决策及其对 7.1 结论的影响
user.raw_password 明文列是有意的运维需求(教师查学生密码),决定保留。
据此如实收窄 7.1 的结论:argon2id 升级不防"数据库泄露",只解决 pbkdf2
的 CPU 开销和脱离 Django 哈希格式,不得描述为"提升密码安全性"。
附一条未采纳的备选(应用层可逆加密)供日后备查。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 20:24:01 -06:00
1251b4e27d docs(阶段0): 端点清单人工裁决完成
6 条 REVIEW 全判 KEEP:5 条是提取盲点造成的假阴性(原生 fetch 4 条、
动态变量路径 1 条),judge_server_heartbeat 是判题机注册心跳、不经前端,
新架构沙箱镜像原样复用故必须保留。

最终:新后端需实现 110 个端点,砍掉 17 个(13%)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 20:22:34 -06:00
f68b04baf0 chore(阶段0): 取回生产库 schema,解阻塞阶段 1
34 张表(27 业务 + 7 Django 框架),schema-only,无 COPY/INSERT、
无密码哈希、无邮箱。阶段 1 的 drizzle-kit pull 据此离线生成 TS schema。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 20:20:54 -06:00
e3fe9e1ab9 docs(阶段0): 更正存量盘点数字,ground truth 改为可独立核验
设计文档 §4 原写「端点 122、DEPRECATED 16、前端调用 78、疑似无人调用约
35%」四项全错:前三项来自漏抓 5 个端点的提取器和只数字面量的前端统计,
35% 是从 (122−78)/122 推的。改为实测值 127 / 17 / 148 条 method+path
(104 条不同路径)/ 23 个无调用 = 18%。减法空间是 18% 不是三分之一,
后续阶段按此排期。

实施计划里的 ground truth 原本就是那个有 bug 的脚本自己产出的,自检退化成
「脚本必须复现自己的 bug」。改为 127 / 17 / 104 并写明独立核验方式。

另:§7 引导语两处改三处;7.3 把「启动时一次性 loadDict」提成衍生约束小节,
附实测数据(200 词逐条 39.4ms vs 一次性 0.98ms,且 loadDict 语义确认为累加)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 20:08:53 -06:00
ae5eea7250 fix(阶段0): 端点提取器漏抓 5 个端点,补反向对账
提取器按文件名白名单扫 <app>/urls/{oj,admin}.py,漏掉两类真实挂载:
tutorial/urls/tutorial.py(文件名不在白名单)和 utils/urls.py(没有
urls/ 目录,被 statSync 的 catch 吞掉),共 5 个端点,其中 4 个前端在用。

改为以 OnlineJudge/oj/urls.py 为唯一入口解析 26 条 include,side 与路径
前缀直接取挂载前缀,app 取 Python 模块名首段。127(oj 77 / admin 50),
DEPRECATED 17,与 cat */urls/*.py utils/urls.py | grep -c "path(" 一致。

reconcile.ts 补上反向对账:前端调用了但后端查无此端点的路径会告警并写进
产出的 markdown,让同类系统性盲区不再依赖人眼评审。修完后 orphan 为 0。

另:生成日期改用本地时区(toISOString 是 UTC,本地 UTC+8 晚间会写成前一天);
补记盲点 3(src/utils/download.ts 的独立 axios 实例,提取器抓不到)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 20:08:53 -06:00
ec563ef64b chore(阶段0): 验证 jieba 替代方案
@node-rs/jieba@2.0.1 在 Bun 1.3.11 下通过验证:NAPI 绑定正常加载,
cut 结果符合预期,1000 次切词 2ms。API 与预期有出入——没有
insertWord/addWord,改用 loadDict 加载自定义词典缓冲区达到同等效果。
结论写入设计文档 7.3 节。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 19:48:27 -06:00
b4cbfd85b0 chore(阶段0): 端点对账脚本与机器初判清单
KEEP 100 / CUT 16 / REVIEW 6,合计 122。

修复 brief key() 里的归一化 bug:/^\/?api\/(admin\/)?/ 会把后端
admin 端点的 admin/ 前缀也剥掉,但前端 http 客户端共用一个 axios
实例(baseURL: /api),admin 接口路径是前端代码里手写的字面
admin/xxx,不该被剥。未修复时 REVIEW 卡在 40(几乎全是被误判的
admin 端点),修复后降到 6。

REVIEW 里 5 条经核实是提取脚本的已知/新发现盲点(动态变量传路径、
原生 fetch() 调用)导致的假阴性,已在生成的 markdown 里写明提示;
唯一真正需要人工裁决的是 judge_server_heartbeat(判题机而非前端
调用的接口)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 19:40:25 -06:00
3da7482741 docs: 阶段 0 实施计划 + 端点提取器
计划覆盖端点盘点、人工裁决、jieba 验证、取回生产库 schema 四件事。
相对设计文档有两处偏离,已在计划内说明:阶段 0 不删旧代码只产出清单
(与"旧仓库全程冻结"一致);不写测试(遵循项目既定策略)。

盘点中发现两处事实,已修正设计文档:
- /api/sessions 前端从不调用,User.session_keys 只写不读,选 opaque
  token 而非 JWT 的原始理由不成立,理由已换成账号封禁需即时生效
- SessionRecordMiddleware 每个已登录请求都写库,新后端不要复刻

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 19:24:41 -06:00
ec4f649a2e docs: OJ2 后端重写设计文档
将 Django 后端重写为 Bun + TypeScript 的设计方案,含前后端 monorepo
结构、技术选型与分阶段路线。

两处高风险技术假设已实测验证,spike 代码见 docs/spikes/:
- Django pbkdf2 密码哈希可在 Bun 侧验证,存量密码无需重置
- tree-sitter 可用 WASM 在服务端运行,且比现状更易部署

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 19:16:57 -06:00