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
007ae8619b
feat(阶段4): SQL 测试点脚本回显
...
GET admin/problems/:id/sql-scripts
只读磁盘上的 N.sql,不需要 SQL 引擎 —— 同组的 sql-preview / sql-ai-gen 要跑 SQLite
生成展示数据,新后端还没有那条链路,那两个仍指向旧后端。
实测:SQL 题回显两个脚本内容正确;测试点不是 SQL 类型返回 409 并说明;题目不存在 404。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 16:45:15 -06:00
cd5dd16f3b
feat(阶段4): 测试用例压缩包上传与下载
...
POST admin/test-cases
GET admin/problems/:id/test-cases (返回 zip 二进制)
落盘格式必须与判题沙箱镜像的约定一致(沙箱直接读挂进去的目录),已用真判题验证:
上传 zip → 建题 → 提交 Python 解法 → 沙箱读到用例并判出 Accepted。
安全与健壮性上比旧后端多做的几件事:
- **zip slip 从设计上进不来**:不遍历压缩包条目,只按精确文件名(`N.in`/`N.out`/`N.sql`)
取内容,条目名一律不参与路径拼接。实测带 `../../etc/passwd` 条目的包能正常处理,
且只取到 1.in/1.out。
- 单文件 32MB、解压后总量 128MB、测试点数 500 的上限,防 zip bomb 与写满磁盘 ——
旧后端一概没有,机房那台机器盘写满之后判题也会一起挂。
- 坏 zip 返回 400 而不是 500。
对齐旧后端的细节:CRLF→LF 归一;`stripped_output_md5` 按 Python `bytes.rstrip()`
的口径只剥尾部 ASCII 空白后再算(实测与 hashlib.md5 结果一致);编号从 1 起连续、
遇缺口即停;SQL 包至少 2 个测试点(题目页会展示测试点 1 的期望结果,只有一个时
学生可以对照着硬编码 AC);目录 0710、文件 0640。
## 顺带修掉一个只在判题时才暴露的路径 bug
config 里的相对路径(data/test_case、data/avatar、data/upload)原先按进程 cwd 解析,
而起服务的方式会把 cwd 切到 apps/api/,于是测试点落在 apps/api/data/ 下 ——
但 docker/compose.dev.yml 把**仓库根**的 data/test_case 挂进判题沙箱。两边不是同一个
目录,新传的测试点判题时会「找不到测试数据」,且只在真正判题时才暴露。
改成一律按仓库根解析,实测沙箱能看到新传的目录。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 16:42:54 -06:00
e396d78a07
feat(阶段4): 题目管理 / 比赛题目 / 比赛题⇄公开题互转
...
GET/POST admin/problems
GET/PUT/DELETE admin/problems/:id
POST admin/problems/:id/make-public
GET/POST admin/contests/:contestId/problems
POST admin/contests/:contestId/problems/from-public
合并了旧后端拆开的两套路由:旧 admin/problem 与 admin/contest/problem 的
GET 详情、PUT、DELETE 都只按题目 id 取、比赛是从题目推导出来的,分开没有意义,
还逼前端多传一个它未必知道的 contestId。现在共用 /admin/problems/:id。
归属判断按旧后端的口径分开:**比赛题看比赛的创建者,公开题看题目自己的创建者**
(旧 ensure_created_by(problem.contest, user) vs ensure_created_by(problem, user))。
一道比赛题的 created_by 可能是克隆时的操作人,跟谁有权改它没关系。
题号唯一性的作用域也跟着走:公开题在全部公开题里唯一,比赛题在本场比赛内唯一。
删题不删测试用例目录,与旧后端一致(它把 rmtree 注释掉了):删错了还能从磁盘捞回来,
而误删的测试数据没有别处备份;孤儿目录另有清理入口。有提交记录的题目拒绝删除。
## SQL 题目前只能编辑、不能新建
新后端**没有 SQL 判题链路** —— 旧 judge/sql_runner.py(378 行)+ sql_dispatcher.py
(113 行)无对应实现,languages.ts 里也只有 C/C++/Java/Golang/JavaScript/Python3 六种。
题目页给学生看的表结构与期望结果(sql_display)要跑 SQLite 现算,算不出来就没法建题。
因此:新建 SQL 题返回 501 并说明原因;**编辑已有 SQL 题时原样保留 sqlDisplay 不动** ——
不动它比生成一个错的安全,它直接决定学生看到的期望结果。SQL 的前置校验
(必须是唯一语言、要有 sql_config、要有 SQL 标准答案)照旧执行。
实测:学生 403;缺样例/缺输入描述/SQL 混语言三条校验文案与旧后端一致;
题号重复 409、跨比赛题号互不影响;公开题加入比赛后计数归零且标签带过来;
比赛题转公开后 visible=false、contestId=null、重复转 409;删除级联正常。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 16:37:20 -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
5e0229db12
feat(阶段4): 题单管理(题目 / 奖章 / 进度三层)
...
GET/POST admin/problem-sets
GET/PUT/DELETE admin/problem-sets/:id
PUT admin/problem-sets/:id/visibility (取反语义,与旧一致)
PUT admin/problem-sets/:id/status
GET/POST admin/problem-sets/:id/problems
PUT/DELETE admin/problem-sets/:id/problems/:itemId
GET/POST admin/problem-sets/:id/badges
PUT/DELETE admin/problem-sets/:id/badges/:badgeId
GET admin/problem-sets/:id/progress
DELETE admin/problem-sets/:id/progress/:userId
修掉旧后端三个问题:
1. **后台列表写死了 visible=True,可它同时又提供「切换可见性」的接口** ——
一旦把题单设成不可见,它就从后台列表消失,再也没法在界面上改回来。
新实现不按 visible 过滤,后台能看见自己管的全部题单。实测取反两次仍在列表里。
2. **加题/删题/改分值后不重算学生进度**。往题单里加一道题,学生那边的
totalProblemsCount 还是老数字,进度百分比因此偏高,甚至已「完成」的人分母变了
却还标着完成。新实现加了 resyncProgress,实测 2/2 加一道题后变成 2/3 = 66.67%。
只重算分母与百分比,不碰 completeTime —— 已完成过的事实不因加题而撤销。
3. 把人踢出题单时一并收回他基于这份题单拿到的奖章,否则奖章悬空。
奖章重算对齐旧 recalculate_user_badges(那边靠 post_save 信号,这里显式调):
只增删差集、保留已有记录的 earnedTime,否则每改一次条件所有人的获得时间都会
刷新成今天。实测 all_problems 建成时补发 1 人 → 加题后收回 → 改成
problem_count>=2 后重新发出 → 踢人后收回。
删题单要按序清五张子表(全是 NO ACTION 外键):user_badge 挂在 problemset_badge
上,得先于 badge 删。
实测:学生 403;重复加题 409、加不存在的题 404、野状态/野奖章条件 400;
级联删除后五张表全为 0。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 16:25:14 -06:00
553c084579
feat(阶段4): 比赛管理 + 克隆 + ACM 赛后核查
...
GET/POST admin/contests
GET/PUT admin/contests/:id
POST admin/contests/:id/clone
GET/PUT admin/contests/:id/acm-helper
克隆是深拷贝:新比赛从 10 分钟后开始、时长与原比赛相同、一律不可见(时间是拍脑袋定的,
直接开放会让学生看到一场没准备好的赛),比赛题目连同标签一起复制,
提交数/通过数/statistic_info 归零 —— 克隆的是题面不是历史战绩。
实测:源题 99/55 两个标签,克隆出来 0/0 两个标签俱在。
两处比旧后端更严:
- **ACM 核查的 rank 必须属于本场比赛**。旧后端只按 rank_id 取,不校验归属,
带上任意 rank_id 就能改别的比赛的核查标记。
- 比赛详情/编辑越权时报「不存在」而不是「无权限」,不泄露「有这么个东西但你看不到」。
其余对齐:非超管只看得到自己建的比赛;空串密码归一成 null(否则 contestType 会把
「密码是空字符串」当成密码保护赛);CIDR 按 ip_network(strict=False) 的口径校验,
允许主机位非零。
核查页的 realName 是**有意下发**的:这个页面就是老师对着名单确认谁抄了,
接口已由 requireTeacher + 归属校验双重把关。
实测:学生 403;结束早于开始 400、非法 CIDR 400 且文案带具体网段;创建空串密码
→ Public、改密码后 → Password Protected 且后台能读到密码原文;克隆时长一致且不可见;
不可见比赛的核查页 404;rank 不属于本场 404。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 16:02:28 -06:00
bc54fbff98
feat(阶段4): 站点配置 / 判题机 / 孤儿用例 / 概览 / 图片上传
...
GET/POST admin/website
GET admin/judge-servers
PUT admin/judge-servers/:id
DELETE admin/judge-servers/:hostname
GET/DELETE admin/orphan-test-cases
GET admin/dashboard
GET admin/random-usernames
POST admin/upload-image
顺带补上配置广播:旧后端改配置会经 WebSocket 推给所有开着页面的人,改完立刻生效。
新后端只服务 /ws/submissions,前端的 ConfigWebSocket 还连着旧 Django Channels。
现在加了 /ws/config 通道(同一个 Bun.serve 只能挂一个 handler,用 socket data 上的
kind 区分),前端 ConfigWebSocket 改走 /ws2/config。
几处判断:
- **判活不能比字符串**。库里 timestamptz 形如 `2026-08-07 13:42:50+00`(空格分隔),
toISOString() 是 `...T13:42:44.000Z`(T 分隔),字典序空格 < 'T',同一天的心跳永远
小于阈值 —— 所有判题机都会显示离线。实测确实复现(dashboard 说 1 台在线、列表却
两台全 abnormal),已改为 Date.parse 后比较。
- 删指定的孤儿用例时**先确认它确实是孤儿**。旧后端不校验,一个手抖的 id 就能删掉在用
题目的测试数据,而测试数据没有别处备份。
- 图片上传的文件名完全由服务端生成,不带用户提供的任何一段;另加 10MB 上限 ——
旧后端靠 nginx 兜,但机房那台机器盘写满之后判题也会一起挂。
- 停用判题机后不再 process_pending_task():任务在 BullMQ 里排着,worker 恢复自己接着
消费,不存在旧自研分发器那种「没有新提交就一直 waiting」的问题。
- dashboard 不再下发 env.FORCE_HTTPS / STATIC_CDN_HOST,前端从未读过。
实测:学生 403;配置读写回读 + oj 侧 /site 同步生效 + 还原;判题机列表带 token、
状态判定正确(一台 normal 一台 abnormal,与 dashboard 计数一致);删不存在 404;
删非孤儿用例 404;随机点名缺班级号 400。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 07:43:38 -06:00
7e1a782747
feat(阶段4): 用户管理 + 成就管理;补上 rescan 与 contest_joined
...
GET/POST/DELETE admin/users (DELETE 走 body 传 ids)
GET/PUT admin/users/:id
POST admin/users/:id/reset-password
GET admin/achievement-metrics
GET/POST admin/achievements
GET/PUT/DELETE admin/achievements/:id
顺带补了新后端缺失的两块(不补的话成就后台建出来的东西是坏的):
1. **rescanAchievement**:旧后端 `rescan_achievement` 的对应实现。判定平时只在判题
结算时发生,后台新建成就或调低阈值不会自动补发,必须显式扫一遍。补发标记
backfilled=true —— 前端据此只显示「已获得」不显示日期,否则一次补发会给几百人
盖同一个时间戳,把「最近获得」板块冲垮。
触发判据包含 metric 与 visible 的变化,不只看 operator/threshold:换维度、
以及从下架改上架(草稿期已达标的人)这两种都会漏。
2. **contest_joined 指标整个漏掉了**。旧 METRIC_REGISTRY 有 18 个指标,新后端只算
17 个,配在这个指标上的成就永远解锁不了。已补上计算,并把注册表抽成
services/achievement-metrics.ts 作为单一事实源 —— 后台下拉框和参数校验都读它,
避免「下拉框里选得到但没人算」这种组合。
用户管理的几处要点:
- className 解析位数不对**直接报错不猜**。猜错会把 class_name 存歪,而剥前缀显示
姓名、班级下拉、统计页都依赖它准确。
- problem_permission 按 admin_type 归一(超管恒 All、普通用户恒 None),否则把超管
降级成普通用户后他还留着 All。
- 改用户名要同步 submission.username 这个冗余列,否则历史提交查不到。
- openApi 已开着就不重置 appkey,否则每次保存用户都把对方的 key 换掉。
- **删除用户不复刻 Django 的应用层级联硬删**。用户是被引用最广的一张表,改成让
数据库外键拦下来:撞外键说明还有历史数据,应当禁用而不是删除,返回 409 并说明。
实测:学生 403;导入 2 人 / 重复 409 / 班级号位数报错文案正确;改名+降权后
permission 归一为 None、重名 409;重置密码 6 位无 0;删自己 400;
成就指标 18 项、新建后补发 unlockCount=2、野指标与野稀有度均 400。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 07:27:36 -06:00
af9cb796bd
feat(阶段4): 教程 / 练习 / AI 学情报告的后台接口
...
GET/POST admin/tutorials
GET/PUT/DELETE admin/tutorials/:id
PUT admin/tutorials/:id/visibility
GET admin/tutorials/:id/exercises
POST admin/exercises
PUT/DELETE admin/exercises/:id
GET admin/ai/reports (?pinnedOnly=true 不分页)
GET admin/ai/reports/:id
POST admin/ai/reports/:id/pin
几处判断:
- 练习改成挂在教程下的嵌套路径,旧后端是 ?tutorial_id= 查询参数。本来就是一对多的
从属关系,嵌套更贴事实,也省掉「忘了传 tutorial_id」这类错误。
- 删教程必须先删练习。Django 的 on_delete=CASCADE 是应用层实现的,库里外键实际是
NO ACTION(核对过 pg_constraint.confdeltype='a'),直接删会撞外键变 500。
已实测:带 2 个练习的教程能正常删掉且练习一并清除。**后台每个 DELETE 都要照此
核一遍子表**,这是本阶段的通用陷阱。
- 改可见性不动 updatedAt —— 上下架不是内容修改,改了会打乱按更新时间排序的直觉。
- AI 报告的 data / systemPrompt / userPrompt 一律不下发,里面是喂给模型的原始
学情数据与提示词。列表只给 120 字摘要,与旧 AIAnalysisListSerializer 一致。
实测:匿名 401 / 学生 403 / 超管 200;教程练习增删改查、非法 type 400、
挂到不存在的教程 404、置顶互斥(同一学生至多一份)、再钉一次取消,全部符合预期。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 07:10:34 -06:00
9a6c3ba225
feat(阶段4): 后台地基 + 公告管理
...
地基:
- auth/middleware.ts 加四个角色守卫 requireAdmin / requireTeacher /
requireSuperAdmin / requireProblemPermission,对应旧 account/decorators.py 的
四个装饰器。未登录 401 login-required、角色不够 403 permission-denied,
与前端 api2 拦截器按 code 分流的两支对上。
- routes/admin/ 目录 + 总入口挂在 /api/admin。角色守卫由各子路由自己挂,
不在总入口兜一层 —— 否则「这个接口要什么角色」从注册行看不出来,
正是阶段 3 Minor M2 踩过的坑。
- packages/contract/src/admin.ts 独立放后台契约。同一张表两侧下发的字段集不同
(后台要 visible,oj 侧连键都不该出现),混在一起迟早有人在 oj 侧复用后台那个。
- utils/legacy.ts:把 toLegacy / legacyResponse 从 oj/api.ts 抽出来共用。
admin 侧组件同样读 snake_case,走同一层适配,组件不动。
公告管理(旧 /api/admin/announcement 一个路径四个动词)拆成:
GET/POST admin/announcements
GET/PUT/DELETE admin/announcements/:id
一处有意不对齐旧后端:删除不存在的公告,旧后端 filter().delete() 静默成功,
这里返回 404 —— 后台是人手点删除,静默成功会让人以为删掉了,刷新后它还在。
实测:匿名 401 / 学生 403 / 超管 200;增删改查、列表不含 content、
空标题 400、不存在 404 全部符合预期。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 07:06:22 -06:00
cebaa87b13
fix(Minor M-1): 站内信内嵌提交改用独立 schema,并修复题号链接
...
两件事:
1. 内嵌的 submission 之前复用 submissionDetailSchema 并把 info / ip 写死成空值,
于是键仍留在响应里。旧 SubmissionSafeModelSerializer 是 exclude,这三个键
根本不出现。改成独立的 embeddedSubmissionSchema —— 形状对上了,将来有人
把空值改成真值也不会变成泄露,因为这里压根没有这三个字段。
2. 顺带修掉一个评审没覆盖的真回归:内嵌 submission 给的是 problemId(数字主键),
而旧 serializer 的 problem 是 SlugRelatedField(slug_field="_id"),即展示用题号。
oj/user/message.vue:20 拿它拼 /problem/<题号>,迁移后拼出的是 /problem/undefined,
题号那一栏也是空的。改为下发 problem,实测返回 "1004"。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 07:01:13 -06:00
20ecc05f63
fix(Minor M2): 比赛权限加中间件兜底
...
旧后端用 @check_contest_permission 装饰器,漏挂一眼看得出来;新后端手工在 handler
里调 canAccessContest,漏调一次就是静默放行,而且这类路由挂的是 optionalAuth
(本身不拦人),从路由注册那一行完全看不出它受保护。
新增 requireContestAccess(checkType, paramName),把「取比赛 → 404 → 鉴权 →
401/403」四步收进注册行。手工调用点 5 → 2:
- GET /contests/:id/access —— 报告权限而非强制,不能 403,留手工
- POST /submissions —— 比赛 id 来自请求体,中间件跑时 body 还没解析,留手工
两处都就地写了说明。canAccessContest 改成泛型,因为 Hono 的 Context 在
Variables 上逆变,写死 Context<AppEnv> 与 ContestEnv 不兼容。
实测拦截行为不变:匿名 401、登录 200、密码赛未过密码 403。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 07:00:58 -06:00
97c54d38b5
fix(Minor M1): 角色判断改回白名单
...
isAdminRole 之前写成 `adminType !== "Regular User"`。当前四种角色下与旧后端等价,
但将来新增任何角色(助教、家长……)都会默认拿到管理员权限,包括 canViewSubmission
里的「看所有人代码」——加角色的人多半想不到要回来改这里。
改成显式列举,对齐 account/models.py:65-73。实测四种已知角色行为不变,
虚构的新角色「助教」现在默认不是管理员。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 07:00:58 -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
44f6f27841
chore(阶段3): 删除阶段 1 的临时验证物
...
GET /api/dev/problems、dev-problems.vue、对应路由项,以及只服务于它的
problemSummarySchema。阶段 1 用来验证「能从真实库读出一道题」,已完成使命。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 06:48:20 -06:00
6c18c57e7a
fix(阶段3): api2 补上鉴权失败的统一处理
...
utils/http.ts 的拦截器一直有两条全站行为:login-required 清登录态并弹登录框、
permission-denied 弹提示。api2 从建包起就漏了这两条,于是此前已迁移到 api2 的
所有端点在鉴权失败时都是「点了没反应」—— 调用方各自 catch 才能提示,漏一个
就静默。
新加的两个统计端点是教师专属,非教师点「统计」必然 403,会把这个洞放大到
用户能天天撞见,所以一并补上。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 06:48:20 -06:00
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
f548aef9f0
fix(阶段3): 补齐 contestId 脱敏与仓库根 .env 加载
...
两处是首轮六条修复的收尾,均由验证过程暴露:
1. contestId 未脱敏 —— 旧后端 SubmissionSafeModelSerializer 排除的是
info/contest/ip 三个字段,首轮只处理了 info 与 ip。
2. 根 .env 读不到 —— Bun 只自动加载 cwd 下的 .env,而启动方式
(bun run --filter '@oj2/api' dev) 会把 cwd 切到 apps/api/,
于是 .env.example 教人写在仓库根的 JUDGE_SERVER_TOKEN 静默失效,
后端降级成随机 token,判题全部卡在 PENDING。config 改为显式补读
根 .env,且只填充未设置的键(真实环境变量与 cwd 下的 .env 优先)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 02:12:31 -06:00
b7adf2993d
fix(阶段3): 去掉判题机 token 的弱默认值
...
F5:token 校验实现本身是对的(用了 timingSafeEqual),问题是缺省值
"oj2-dev-token" 写死在仓库里 —— 写死在仓库里的 token 等于没有 token。
后端改为对齐旧后端 options/options.py:93 的 fail-safe:
env 缺失时生成随机值并在启动日志里告警,判题机连不上,但不会静默用弱默认值。
compose 改为 ${OJ2_JUDGE_TOKEN:?...},未设置直接报错退出。
本地开发怎么设写在 docker/compose.dev.yml 顶部与 .env.example 里:
两个变量名不同(判题机镜像认 TOKEN,后端认 JUDGE_SERVER_TOKEN)但值必须相同。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 01:59:27 -06:00
8237909d00
fix(阶段3): 提交可见性守卫补上匿名,详情脱敏,提交接口加限流
...
F3:`isRegularUser(user)` 对匿名用户(user 为 null)返回 false,
`submission_list_show_all` 关闭时守卫整体短路 —— 匿名能看到全部提交,
权限反而大于登录学生。实测开关关闭时匿名 total=23、登录学生 total=0。
而关闭这个开关的典型场景正是考试。改为 `!isAdminRole(user)`(非管理员即受限),
并删掉 isRegularUser(全仓只有这一处调用,留着就是下一个坑)。
F4:提交详情把 info(含每个测试点的 test_case 编号与 output_md5)与 ip
按「是不是自己的提交」下发。旧后端 submission/views/oj.py 把关的是角色:
is_admin_role() 决定用 SubmissionModelSerializer 还是
SubmissionSafeModelSerializer(exclude=("info", "contest", "ip"))。改为仅管理员可见。
F6:旧后端 SubmissionAPI.throttling 的 TokenBucket 没搬过来。
按旧参数重建(options/options.py:120 的 user 桶:capacity 20、fill_rate 0.03、
default_capacity 10,实际值仍以数据库 throttling 配置项为准),
落在 redis 上并改用 Lua 脚本做成原子操作 —— 旧实现自己注明「不是线程安全的」,
而限流要挡的正是并发突发。挂点位置与旧后端一致:比赛权限校验之后、取题目之前。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 01:59:19 -06:00
b4b61af6b0
fix(阶段3): 匿名不可读用户档案,真名改为默认不下发
...
F1:GET /profiles/:username 只挂了 optionalAuth、handler 内无登录判断,
匿名可读 email、adminType、className、lastLogin。用户名又能经 /rankings/users
公开枚举,等于可以无 cookie 批量收集全校学生的邮箱与最后登录时间。
handler 开头补上未登录即返回空,对齐旧后端 account/views/oj.py 的
UserProfileAPI.get 首行 `if not user.is_authenticated: return self.success()`。
F2:旧后端把「是否下发真名」做成 UsernameSerializer(need_real_name=False)
的默认关闭开关,全仓 11 处调用只有比赛榜单一处显式打开;新后端没搬这一层,
真名随用户对象无条件下发,13 个下发点里 8 个匿名可达。
这里补回同一层:helpers.ts 新增 sampleUser(),realName 默认不下发,
需要的地方显式传 { includeRealName: true }。12 个下发点改为走这个函数,
只有比赛榜单一处打开(对齐 contest/serializers.py:84 的 is_contest_admin)。
没有逐处删字段 —— 那样下次新增端点还会重犯。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 01:59:05 -06:00
8c00cdc947
feat(阶段3): oj 侧端点铺开(基线提交,未经评审)
...
由外部 agent (Codex) 在本会话额度中断期间完成。原样提交作为基线,
后续修复单独成 commit,便于区分与回退。
覆盖 oj 侧 65 个端点,新增 9 组路由(account/achievement/ai/classroom/
content/contest/flowchart/problemset/site)与对应 Zod 契约。
已核验:tsc --noEmit 退出码 0;API 可启动;/api/problems 返回真实数据;
judge 与 flowchart worker 均 ready。
未核验:权限边界与数据泄露,评审进行中。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-07 01:25:36 -06:00
ec274419c3
Build Phase 2 judge vertical slice
2026-08-06 22:42:39 -06:00
e6329ecabb
feat(阶段1): 端到端串通,前端显示本地库真实题目
2026-08-06 21:19:52 -06:00
ae1fb329b5
feat(阶段1): 搬入 ojnext 为 apps/web,未改业务代码
2026-08-06 21:18:16 -06:00
3c975e85ee
feat(阶段1): Hono 应用与题目列表接口,读通本地真实数据
2026-08-06 21:14:58 -06:00
0f0a5cb65a
fix(阶段1): 修正 problem 主键与展示编号映射
2026-08-06 21:11:44 -06:00
f635737453
feat(阶段1): drizzle schema 从本地库生成并剪掉 Django 框架表
...
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-06 20:45:51 -06:00