Commit Graph
44 Commits
Author SHA1 Message Date
xuyueandClaude Opus 5.5 11d2155908 style(提交列表): 结果筛选里去掉「内存超限」「正在评分」「系统错误」,只留学生真会碰到的几种
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:11:23 +08:00
xuyueandClaude Opus 5.5 773d15b033 feat(课堂看板): 「这节课的题」旁边多一个语言下拉,布置 C 的作业学生打开题目编辑器就是 C
Deploy / deploy (push) Canceled after 0s
默认是这个班上一次布置选的语言。学生那边首页、题号下拉、题目页课堂条都写上「C语言」;
打开这节课的题时编辑器自动切过去,每道题只切一次,学生自己再换就按他的。提交列表的
「这节课」连语言一起筛。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:54:21 +08:00
xuyueandClaude Opus 5.5 e3be3cd62c feat(统计): 「今日统计」「数据统计」合成一个「统计」,打开是一行一节课,这节课在最上面
Deploy / deploy (push) Canceled after 0s
老师的提交列表上只剩一个「统计」按钮;什么都没筛时按课列(选「今天」就是原来今日统计看的),
点一行进那节课(班、题、时段都带好),左上「← 全部」回来;只填题号时仍按班级汇总,和题目页
统计页签对得上。数字行加结果条,统计里的「课堂看板」按钮去掉,改成有班正在上课时才引过去。
学生仍是「今日统计」。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:23:54 +08:00
xuyueandClaude Opus 5.5 cb2fb57fdb style(提交列表): 「重新判题」是右上角最显眼的按钮,上课却最用不上,收进「⋯」
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 08:47:57 +08:00
xuyueandClaude Opus 5.5 e3eb41288c feat(提交列表): 「数据统计」改回在当前页弹框里看,不再开新标签页;按钮和「今日统计」一个样子
弹框带着列表现在的班级、学生、题号打开,「在提交列表里看」直接改背后的列表。
今日统计里点班级名也开同一个弹框。统计主体抽成 StatisticsView,/statistics 页面
留给课堂看板「回头看」、题目页这些别处的入口。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 01:20:01 +08:00
xuyueandClaude Opus 5.5 2fddf4a011 feat(提交列表): 今日统计重做——老师看今天哪几个班上了课、几点到几点、错得最多的题;学生版从简
- 老师:各班上课时段画在一条 7–20 点的时间轴上(点班级名去数据统计),有班正在上课时引去课堂看板
- 错得最多的题带班级、没过的原因、做对 x/y 人;底部一根全站结果条,语言只有一种时缩成一句
- 学生:你今天交了几次做对几道 + 今天大家都在做(按人数,带去做 / 接着做)
- 标题栏写「截至几点」、判题中几条;打开时把按钮上的数一起更新(原来只在进页面时拉一次)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:47:51 +08:00
xuyueandClaude Opus 5.5 5f9743efe0 feat(提交列表): 右栏名字旁多一个「只看这个班」,不在班级列表里的老班也能筛出来
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:00:38 +08:00
xuyueandClaude Opus 5.5 e37ad982c0 revert(提交列表): 撤掉班级下拉里补「近半年有提交的班」那一版,下拉回到只用系统设定的班级列表
改成在右栏名字旁加「只看这个班」过滤按钮(下一条)。
This reverts commit 61ec7d7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:00:38 +08:00
xuyueandClaude Opus 5.5 61ec7d7f8e fix(提交列表): 不在班级列表里的班(毕业、换届摘掉的老班)在班级下拉里选不到,筛不出它的提交
Deploy / deploy (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:31:50 +08:00
xuyueandClaude Opus 5.5 6bc927402e fix(提交列表): 列表上的「1/1」被学生当成交了几次,换成显示编程语言
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:31:50 +08:00
xuyueandClaude Opus 5.5 365ce0a7b4 style(提交列表): 列表改回比代码区窄一点(46 : 54)
Deploy / deploy (push) Canceled after 0s
56% 的列表嫌宽了。46% 在机房 1280 的屏上题目列约 215px,快照里抽的几组各 50 行题目名
都放得下,特别长的看鼠标提示;大屏上列表最宽 900。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:23:25 +08:00
xuyueandClaude Opus 5.5 816e295dc9 style(提交列表): 翻上一条下一条只用 ↑ ↓,去掉 ← →
Deploy / deploy (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:13:03 +08:00
xuyueandClaude Opus 5.5 b46529c741 style(提交列表): 列表放宽、代码区收窄,题目名不再被截成三个字;只看一个人时筛选框换成名字标签
- 左右从「列表 560 固定 + 代码占满」改成 56% : 44%(大屏列表最宽 1000):学生的代码大多
  十来行,右边原来空着一大片,左边的题目名却只剩三个字;用户名一列 132 → 116
- 右栏顶上固定一行:「只看他 / 只看这题」只留漏斗图标,题目名太长时省略,翻条时代码区不再上下跳
- 按整名只看一个人时(只看他、协作条、看板点进来),108px 的输入框装不下八九个字的用户名,
  换成「只看 xxx ×」的标签;列表里题目名加上鼠标提示

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:12:14 +08:00
xuyueandClaude Opus 5.5 52bf2d6111 feat(提交列表): 题号填错一道时顶上提示「没有这道题」;← → 也能翻上一条下一条
Deploy / deploy (push) Canceled after 0s
- 列表接口回带查不到的题号(公开列表只认公开题,比赛列表只认本场的题),
  一次填好几道时打错的那道原来被悄悄忽略
- 方向键恢复成四个都能翻(9 月加的时候就是这样,老师已经用惯了)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:58:31 +08:00
xuyueandClaude Opus 5.5 0bcc981e6f fix(提交列表/统计): 按方向键会顺手切掉「代码/流程图」、重判后右栏还是旧内容、没人交时统计说「全都做完了」
提交列表:
- 焦点在单选框、下拉框上时方向键不再一边切控件一边换条;跨页没回来前不再连跳一整页
- 重判、重新评分之后右栏重新拉,不再命中旧缓存;同一条新旧请求不再互相覆盖
- 从「我的提交」点顶栏「提交」、浏览器后退时,地址里没有的筛选回到默认
- 找回点用户名开个人主页;「只看他 / 只看这题」学生也能用;重判不用等代码拉下来
- 手机上老师也能重判、能按语言筛,流程图行有手机排版;学生界面带着的班级筛选显示成能点掉的标签
- mermaid 画不出来时先说中文、原始报错折起来

统计页:
- 没人交时说「这段时间没有人交」,不再说「全都做完了」;没填题号时各处统一叫「做对过题」
- 班级按 class_name 单独传;查询失败清掉旧数据并说清原因;标签跟着查出来的那一份走
- 「语法未过」的格子单独标「语」;正确率不算还在判的;截断提示说清哪些数字受影响
- 看板的请假隐藏只藏还没交过、没在举手的人,「恢复」只恢复这个班的
- 手机上筛选折行、上下排,不再挤出屏幕

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:52:58 +08:00
xuyueandClaude Opus 5.5 ed55ee5af6 feat(统计): 数据统计从弹框改成独立页面,左边题目、右边学生,一道题看全班每人的每一次
- 课堂看板管这节课,统计管回头看:时间段默认「两节课」,还有这节课、今天、7 天、30 天、这学期、全部、自己选日子;不再自动刷新
- 选一道题看每人在这道题上的方块串,鼠标停上去看那次的代码;选「全部」每人一行、展开按题看
- 没选班级(从题目页进来)先按班级汇总;流程图统计有等级分布、四项雷达、各项平均分、词云和每人最好的一次
- 提交列表、题目页的「数据统计」改成新标签打开这个页面
- 「请假隐藏」挪到课堂看板:点名字旁的 × 藏两个小时,做完人数的分母一起减掉

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:28:27 +08:00
xuyueandClaude Opus 5.5 99c158498c feat(提交列表): 老师也能在「全部 / 我的」之间切换,不用再靠「清空」退出「我的提交」
Deploy / deploy (push) Canceled after 0s
选「我的」时这节课、班级、学生三个框收起来(只看自己时后端本来就不认它们);
机房 1280 宽的屏上「今日统计」只留图标和数字,给这个切换腾地方。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 13:30:17 +08:00
xuyueandClaude Opus 5.5 660f689a5c feat(提交列表): 重做成左边列表、右边代码,方向键一条条翻,不再开弹框
- 右栏常驻选中那条的代码:编译失败 / 运行时错误用中文说清第几行错了并标红,
  老师看得到逐个测试点,学生看别人的只显示结果
- 老师一键「这节课」:筛到正在上课的班、这节课的题、今天
- 「正在评分」的行自己变成结果;新交的不插队,顶上提示「有 N 条新提交」
- 按天分组、默认每页 30 条;状态筛选补全,语言补上 SQL;流程图的评分详情也放进右栏
- 课堂看板的格子和名字能直接点进这个学生今天的提交

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:08:49 +08:00
xuyueandClaude Opus 5.5 ea480d5e0c feat(提交列表): 老师也能重新判题;列表能按班级、一次按几道题筛,还带「通过几个测试点」
- 重新判题从只放超管改成老师及以上:前端一直给老师亮着按钮,点了只会「权限不足」
- /submissions、/flowcharts 加 className(user.class_name 精确匹配),题号可以一次填几道
- 列表每行带 caseSummary,口径和详情一致,在库里数完只回两个数
- 新增 GET /classroom/lesson:提交列表「这节课」按钮用,班级和题的取法同课堂看板

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:08:49 +08:00
xuyueandClaude Opus 5.5 e7b95d211d fix(题目): 修掉题目页重构审查查出的问题——提交途中换题会把结果算到新题上、运行例子把对的菱形判错、点评完「下一题」不跳等
题目页重构之后按「功能对照 / 状态 / 结果页 / 布局与后端」四路审查了一遍,查出的问题一起修:

提交与换题
- 提交、画流程图途中换题:A 题格式化好的代码会盖掉 B 的草稿,A 判对了把 B 标成已解决、
  弹 B 的点评、题单里跳回题单。现在每个请求回来先核对题目,轮询和推送回来的也核对提交编号
- 画着流程图换到不能画的题,语言退到题目的第一门、还被记成偏好;现在回到学生习惯的那门
- 强制点评评完点「继续 / 下一题」,学生之前在看「题目」页签的话会停在原题
- 离开题目页之后判题、流程图的 WebSocket 一直连着,现在闲 15 分钟断开

运行例子
- 输出开头的空格被削掉,打印菱形这类题对的代码也显示「没对上」
- C 的编译警告拼进了「你的输出」(后台生成测试点也会写进 .out)
- Python 的语法错误(中文标点)被说成「下标有没有越界、除数是不是 0」
- 比赛里仍然把空格画成「·」、把不同的地方标红

入口与协作
- 比赛里不给「课堂统计」、不绑协作:房间和统计都按题号找,比赛题号和公开题撞号
- 「看他交过什么」按整名匹配,不再混进学号相近的同学
- 协作中点上下文条、相似题、下一题开新标签,协作不断
- 题单入口给管理员的「⋯」里留「本题提交」

结果与题面
- 「去自己输入拿 51 这些数试试」取到了题面 HTML 里的颜色值;「题目原话」露出 &quot;
- 流程图 A 级的学生标题下还写着「还没交过」
- 「我的提交」上方补回「答案错误 × 3」这样的计数
- 流程图历次评分能点开看那一版的图;读分数失败不再说「还没交过流程图」;手机上能看流程图点评
- 「结果」页签有新结果没看时加粗上色,看过就恢复
- 空画布拖进第一个节点画布会跳一下;画布底下空着一条
- 比赛没开始写成「已结束」、倒计时超过一个月算错;分隔条拖到头工具栏被挤坏;题单里的题目页有备案页脚
- 流程图历次分数只读需要的几列(一个学生一道题 23 次提交:96 kB → 2 kB)
- 比赛密码、比赛没开始等几条学生看得到的英文报错换成中文

设计文档照实际做法更新了第 5.5、5.6、6、8、9、10、11 节。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:07:16 +08:00
xuyueandClaude Opus 5.5 6bf8c6e1d9 style(前台): 列表页去掉表头 emoji、已结束的比赛改成灰色;比赛列表置顶进行中的比赛,排名页先看榜单
- 前台 53 处表头改成纯文字,删掉只剩这一个用途的 utils/renders.ts
- 「已结束」从红色改灰色(比赛列表 / 详情 / 后台 / 首页一起)
- 比赛列表第一页、无筛选时顶部列出进行中和即将开始的比赛
- 内容少的页面页脚贴底,不再悬在半屏
- 排名页两张柱状图挪到最后,第一屏是 Top100、本周进步榜和我的排名

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:13:29 +08:00
xuyue d25b5dbe6d update
Deploy / deploy (push) Canceled after 0s
2026-09-27 19:36:28 +08:00
xuyueandClaude Opus 5.5 9412ba3cf6 refactor(契约): 展示题号统一叫 problemDisplayId,题单 id 统一叫 problemSetId
Deploy / deploy (push) Canceled after 0s
前后端同时改,部署前开着的旧页面要刷新一次(题号列会是空的、提交筛选会失效)。

- 挂在非题目对象上的展示题号,原来叫 problem(字符串)或 displayId,统一成
  problemDisplayId:提交列表、流程图列表、统计的 lastFailure / items /
  今日最热题、站内信里的提交、后台题单的题目列表。别处的 problem 都是对象
  或数字 id,这几处是字符串,最容易看错
- 列表筛选的 query 参数 ?problemId= 实际传的是展示题号,改成
  ?problemDisplayId=(提交列表、流程图列表、三个统计接口)
- 契约里 problemsetId / problemSetId、problemset / problemSet 两种拼法并存,
  统一成 problemSetId / problemSet,和 ProblemSet* 类型名、problem-sets
  路径、前端路由参数一致。drizzle 的列键跟表名走,没动
- aiDetail.user(用户名字符串,而契约里别的 user 都是对象)→ username。
  ai_analysis.data 里存的历史快照仍是 user,没有代码读回它
- 后台题单题目表的列是按字符串键 'displayId' 取的,类型检查抓不到,一并改

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:08:31 +08:00
xuyueandClaude Opus 5.5 5ae830a7f9 refactor: 一批名不副实的命名改掉(只动一端,不碰契约)
后端:
- getUserByToken → authenticateToken:它会删会话、续 TTL、记在线,不是纯读
- todayStart → dayStart(instant):本来就接受任意时刻,不只是「今天」
- objectValue → asRecord,删掉 4 份一模一样的私有拷贝和 `as toObject` 别名
- 报错文案 "does not exists" / "Already be a public problem" 改通顺

前端:
- utils/types 里 Contest / User / Problem 三个别名删掉:Contest 原来指后台
  比赛、契约里同名的却是学生侧。现在 Contest = 学生侧、AdminContest = 后台,
  后台用户表单叫 AdminUserForm,题目详情直接用 ProblemDetail
- SubmissionInfo → ContestSubmissionInfo,免得和 submission.info 撞名
- utoa / atou → compressToBase64 / decompressFromBase64(实际是 zlib + base64),
  encode / decode → base64EncodeUtf8 / base64DecodeUtf8
- filterResult → toProblemRow、toProblemListItem → toAdminProblemRow,
  行字段 submission / rate → submissionCount / acRate
- 组件:Modal → AddFromLibraryModal、Form → EditorToolbar、
  ContestEditor → BasicEditor(题单也在用它)、rank 的 Index → RankMedal、
  Name → UserNameCell、Password → PasswordCell、ContestType → ContestAccessTag

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:08:21 +08:00
xuyueandClaude Opus 5.5 7a390323fe refactor(前端): 接口错误改成 ApiError 类,catch 不再写 any
拦截器原来 reject 一个裸对象 `{ error, data }`,没有类型也没有类型守卫,于是 46 处
`catch (err: any)` 直接读 `err.data` / `err.error` —— 代码 bug 抛出的 TypeError 也按
接口错误读,弹出来的是 undefined 或英文。

- utils/api.ts:reject 改成 `ApiError extends Error`,字段名沿用 error(错误码)/
  data(文案),存量读法语义不变;加 isApiError / errorCode / errorMessage。
  errorMessage 对非接口错误一律用 fallback,不把 TypeError 的英文甩给学生
- 46 处 catch 全部换掉 any(45 处接口错误 + pythonSyntaxCheck 里 Skulpt 的错误对象)
- 断网等没有响应体的情况,文案从英文 "Request failed" 换成「网络异常,请检查网络后重试」

浏览器实跑核过:登录密码错误仍显示「用户名或密码不正确」(错误码分支)、
接口错误 reject 出来是 ApiError、网络不通时拿到中文文案、非接口错误走 fallback。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:12:40 +08:00
xuyueandClaude Opus 5 8b4d8899f9 feat(判题机): 自建镜像升级工具链,语言收敛到 C / C++ / Python
Deploy / deploy (push) Canceled after 0s
上游 QingdaoU/JudgeServer 停更在 2024-04(registry 上的 latest 和 1.6.1 是同一份
镜像,编译器停在 gcc-13),没有新版可拉,所以自己重编。docker/judge/ 是只改工具链
的 Dockerfile 分叉,server/ 和 Judger/ 从上游固定 commit b28aa56 拉,一行没动。

镜像 oj2-judge-2(不在任何 registry 上:本机 build.sh --save → scp → docker load)
- gcc/g++ 13 → 14.2,Python 3.12 → 3.13.5,都是 trixie 默认
- Go / JDK / Node 整套删掉:前端的题目语言复选框从来只给 C / C++ / Python / SQL,
  12 万条提交里 Java 44 条、Golang 15、JavaScript 3,全是很早以前的
- 体积 1.1GB → 433MB;默认走清华源,构建 12 分钟 → 40 秒(--no-mirror 换回官方)
- deploy.sh 加一道自检:镜像不在本机就中止,并打印该跑的三条命令

C 的编译参数加三个 -Wno-error(implicit-function-declaration / int-conversion /
incompatible-pointer-types):gcc-14 把它们从 warning 提成了 error,而 -w 压不住。

语言值统一成 Python(迁移 0019 / 0020)
- 0019:Python3(104527 条提交)与 Python2(3 条)并成 Python,一并改掉 937 道题的
  languages、75 个 template 键、15 个 ast_rules 键、257 条 answers、1235 个用户的
  成就指标 _languages(languages_used 重算,总和 1928 → 1925,少的 3 个是同时用过
  两种 Python 的人)
- 0020:把 Java / JavaScript / Golang 从 84 道题的可选语言里摘掉 —— 不摘的话那些题
  的语言下拉还能选 Java,提交必 SYSTEM_ERROR
- 契约新增 normalizeLanguage() 别名表,判题侧一律走 judgeConfigFor():旧客户端
  localStorage 里的 Python3、迁移前排进队列的任务都还能判;协作的语言归一也走它,
  否则上线那一刻学生页面里的 Python3 会静默落到 C
- 回滚要连数据一起回,只滚代码会让所有 Python 提交变 SYSTEM_ERROR

实跑
- 判题冒烟 docker/judge/smoke.ts 13 条全过:三种语言、六种状态码、gcc 宽松度
- 拿备份里的真实代码逐文件比对新旧镜像的编译结果,0 差异:C 提交 1951 份
  (1725 过 / 226 CE)、C++ 882 份、Python 2000 份、20 篇 C 教程的 93 个代码块。
  不加那三个 -Wno-error 的话,C 有 26 份会从能过变成 CE
- 迁移在灌了 12.4 万行真实数据的一次性库里跑过:0 残留、没有题目被清空;
  dev 库用真正的执行器跑通
- check:ast 56 个 target 全过,前后端 typecheck 均 0

顺带记下一个升级之前就有的坑(现在随 Go 一起消失,写在 README 里):GOCACHE 指向
容器的 tmpfs,判题机重启后第一次 Go 提交是冷构建,Go 1.22 要 5.6 秒 CPU、超过 3 秒
的编译预算,于是重启后第一个交 Go 的学生必吃一次 CE,后面的人缓存热了又都正常。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 06:24:05 -06:00
xuyueandClaude Opus 5 ad858ed864 feat(提交列表): 代码详情弹框支持方向键翻阅,到头自动翻页
Deploy / deploy (push) Has been cancelled
老师看一个班的提交原来得「点开 → 看 → 关掉 → 点下一行」,现在弹框开着就能
用 ↑↓(←→ 等价)切上一条/下一条,Esc 关闭。

- 只在 showLink 为真的行之间走,跳到看不了的行只会得到空弹框
- 走到本页头尾自动翻页续上:向下落到下一页第一条,向上落到上一页最后一条。
  翻页是异步的,先记 pendingJump,等 listSubmissions() 回来再开;
  整页都看不了代码时保持原来那条不动,给一句提示
- 弹框没开不接管方向键,焦点在输入框里也不抢
- SubmissionDetail 加 :key="submissionID" —— 它只在 onMounted 拉一次代码,
  不换 key 切过去还是上一条的代码

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 19:26:51 -06:00
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 e600fd24cf feat(提交列表): 今日提交数旁加「统计」按钮,弹框给今天的提交统计
筛到今天之后标签旁边出现「统计」,弹框里是全站今天的提交概况:总提交 / 正确 /
判题中 / 正确率 / 参与人数,外加按钟点的 24 格分布、按语言、按判题结果,以及
今天最热的 10 道题。

新接口 GET /submissions/today-statistics,公开、只出聚合数,口径和那颗标签一致
(东八区今天 + 非比赛提交):

- 钟点分桶走 time.ts 的 localTime()。`extract(hour from create_time)` 按会话时区
  算,容器是 UTC,整张分布图会左移 8 小时;
- 正确率的分母摘掉未判完的条数,正确数含 AST_CHECK_FAILED;
- 热门题只算 visible 的题目 —— 这个接口不需要登录,不能拿它探未发布题目的标题;
  「提交列表对学生全开」关掉时这张表整个不下发,跟提交列表同一个开关(数字照给,
  否则标签说 21、弹框说 0)。

前端组件异步加载,不进本路由的关键路径;小时分布是纯 CSS 柱状图,没有引 chart.js。
柱子和基线取 useThemeVars(),深浅色都跟着走,「现在」那一格是基线上一段主色刻度。
流程图那档不给这颗按钮 —— 流程图提交在另一张表、只有 AI 评级没有判题状态。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 08:23:45 -06:00
xuyueandClaude Opus 5 9132901cc7 feat(提交结果): 没通过时显示「通过 x/y 个测试点」;编译失败不等三次就能用 AI 分析
Deploy / deploy (push) Has been cancelled
学生拿不到 info(每个点带 output_md5,只给管理员),测试点表格从来只有管理员看得见,
学生这边只有一句「答案错误」—— 从 2/8 交到 6/8 的人,感受是连输五次。

- 提交详情新增 caseSummary,后端从 info 数出通过数下发,不放开原文。
  比赛提交、SQL 题(被杀的测试点会 break,total 偏小)、无逐点结果时为 null
- 结果面板标题缀上通过数并加进度条,提交详情页标题同步;一个都没过时不缀
- POST /ai/hint 对编译失败跳过失败次数门槛,throttleAi 照旧;前端显示条件同口径

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013TecwAowmYcoNZRheSyTgH
2026-09-13 07:25:00 -06:00
xuyue aab0404ed7 refactor(契约): 语言与判题产物的形状收进契约,学生端高频响应接上运行时校验
Deploy / deploy (push) Has been cancelled
契约在前端一直只当类型包用:45 处引用里几乎全是 import type,三个 .parse() 后面
还都紧跟一个 as 把校验结果断言回去,等于没校验。这一轮把形状的来源收拢。

## 语言:三份真相并成一份

前端 utils/types.ts 手写了一份 9 值的语言联合,后端 judge/languages.ts 有自己的一套,
生产库又有一套。手抄那份**漏了 SQL**,而生产库 961 道题里有 9 道 SQL 题、
124191 条提交里有 91 条 SQL 提交 —— 这些提交的 language 在前端类型上是 undefined。
现在唯一来源是契约的 problemLanguageSchema,constants.ts 的显示映射以它为键,
契约里加语言而那边没补映射会当场编译不过。

## 判题产物:按生产数据实测收紧

judgeInfoSchema / statisticInfoSchema 的形状来自 124191 条提交的实测,不是手抄:

- info.data 有 12048 条是 null(编译失败等没有逐测试点结果),前端手抄的 Info
  却把 data 写成非空数组 —— 这 12048 条在类型上根本不成立;
- info 还允许**空对象**:非管理员看提交详情时后端下发 info: {}(权限投影)。
  收紧时必须把它算进去,否则每条非管理员看的提交详情直接 500 —— 本地实测复现过;
- statistic_info 的五个键按出现次数定成全部可选;另有 8916 条 JSONB 原文因内嵌
  带转义的 shell 输出不是合法 JSON,被后端 objectValue() 兜成 { value: ... },
  所以不能用严格对象,否则这 8916 条会被误判成分歧。

## 运行时闸门

新增 utils/contract.ts:safeParse 失败时记一条分歧(去重、控制台可见、
window.__OJ2_CONTRACT_DRIFT__ 可查)后**放行原始数据**,不白屏 —— 面向学生的
生产站点,字段空着比整页崩掉可接受。接在 7 条高频链路上:/site、/site/online、
/problems、/problems/:id、/submissions、/submissions/:id、/me。

提交详情的 info 是联合类型,调用方不再直接取 .data,统一走
utils/functions.ts 的 submissionCaseResults()。

## 验证

- vue-tsc 与 tsc -p apps/api 均 exit 0;vite build 通过;
- 契约 schema 直跑真实接口:7/7 通过(自包含脚本,从登录到详情全链路);
- 用生产备份复核收紧后的约束:961 题的 languages 无越界值,template 只出现
  C/Python3 两个键 —— 不会因为这次收紧在生产上抛错。
2026-09-10 04:19:41 -06:00
xuyueandClaude Opus 5 e624c62502 feat(提交): 列表标出来自题单的提交
Deploy / deploy (push) Has been cancelled
submission 新增 problemset_id,学生从 /problemset/:id/problem/:pid 入口提交时
前端带上、后端落库,提交列表在题目后面挂一个「题单 xxx」的标签,点了进题单。

只是来源标记:题单进度和奖章仍由判完之后的 recordSolvedProblem 按「已加入且含
这道题的所有题单」记账,和从哪个入口进来无关,所以这个字段带错顶多标签不准,
不影响成绩。校验只确认这道题在那个题单里 —— 不查 visible / status、也不查有没有
加入,藏起来的题单里还困着已加入的学生;对不上就当没带,提交照收。

外键 ON DELETE SET NULL:删题单不该带走提交,清掉标记就行。索引建成部分索引
(WHERE problemset_id IS NOT NULL),绝大多数提交不来自题单,全列索引是给 12 万行
白建一遍;谓词能被 problemset_id = $1 蕴含,删题单时的外键检查也用得上它。

列表按页单独查一次题单标题,没有把 problemset join 进那条调过的深翻页查询。
比赛提交恒为 null:题单只收非比赛题。

历史回填从 problemset_submission 取「当年首次 AC 那条」,其余老提交无从判断入口,
一律留空。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y7AsqceaUfC81k7UCDcSk2
2026-09-02 22:06:29 -06:00
xuyueandClaude Opus 5 8861393529 fix(流程图): 评分明细的顺序是乱的,40 分的那项排在最后
Deploy / deploy (push) Has been cancelled
AI 是按分值从高到低返回的:`逻辑正确性(40) → 完整性(30) → 规范性(20) → 清晰度(10)`。
但 `ai_criteria_details` 存在 jsonb 列里,而 Postgres 的 jsonb **不保留键序** ——
它按「键长度 + 字节序」重排,读出来变成 `完整性 → 清晰度 → 规范性 → 逻辑正确性`,
权重最高的那项被排到了最后。

评分弹框和教师端的评分详情都是直接 `v-for` 遍历这个对象,所以两处都乱。
统计面板因为用的是写死的 `CRITERIA_ORDER`,一直是对的 —— 于是同一份数据在两个
地方的顺序还不一致。

把顺序抽到 `utils/constants` 共享,三处统一走 `sortFlowchartCriteria()`;
表里没有的键排到后面,AI 万一返回别的评分项也不会丢。

顺带把限流的提示改得能看懂:撞上 429 时原来只显示「流程图提交失败」,学生会以为
是自己的图有问题然后反复点,越点等得越久。现在按错误码分支,提示「提交太频繁了,
缓一会儿再交」。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 10:53:29 -06:00
xuyueandClaude Opus 5 74d3b97f05 feat(流程图列表): 补上状态列,重判按钮按状态置灰
教师端的流程图提交列表一直没有状态列。「排队中」「评分中」「评分失败」三种情况
在界面上长得一模一样 —— 都只是分数栏空着,老师分不出是还没评完还是评失败了。

三处一起改:

- 加状态列(排队中 / 评分中 / 已完成 / 评分失败)。
- 分数列只在已完成时渲染 Grade。原来无论什么状态都渲染,没评完会显示成 0 分,
  看着像「评了但得了 0 分」。
- 重判按钮按状态置灰。后端只接受已完成 / 已失败的重判(其余返回 409),
  原来是点了才知道不行。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 06:27:33 -06:00
xuyueandClaude Opus 5 0392445dd2 fix(流程图): 补上几处静默失败 —— 点了没反应、弹框永久转圈
`utils/api.ts` 的拦截器只对 login-required / account-disabled / permission-denied
弹提示,其余业务错误一律静默 reject。而流程图这一块的调用点基本都没 catch,
于是失败时用户什么都看不到:

- **重新判题**:后端会拒掉还在评分中的提交(409 retry-not-allowed),也可能撞上
  限流。老师点下去完全没反应,连报错都没有。实测修复前 0 条提示,修复后弹出
  「这条还在评分中,等出了结果再重新评分」。
  提示按**错误码**分支而不是 match 文案(`utils/api.ts` 里写明的约定)——
  后端文案是英文的,直接弹给老师看不合适。
- **课堂统计**:请求失败后图表停在旧数据上,没有任何迹象表明这次没拉到。

同一批里 `SubmitFlowchart` 的三处(弹框翻页、打开评分详情、加载到编辑器)随
上一个提交一起改了,问题是一样的:前两处失败时 `rendering` 卡在 true,弹框
永久转圈。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 06:15:11 -06:00
xuyueandClaude Opus 5 5f0fe713dc fix(流程图渲染): 出错后再也画不出来、每失败一次往 body 漏一个 div
## 渲染失败后永久空白

四个渲染点都是同一个写法:

    <n-alert v-if="renderError" ... />
    <div v-else ref="mermaidContainer"></div>

一旦渲染出错,容器被 `v-else` 卸载,`mermaidContainer.value` 变成 null。而
`renderFlowchart` 一进来就先清 `renderError`、再因为 `!container` 直接 return ——
于是**报错提示消失了,图也再画不出来**,只剩一块空白,只能刷新页面。翻历史提交
最容易踩到。改成容器常驻、用 `v-show` 隐藏,和 MermaidEditor 本来的写法一致。
`FlowchartScoreDetail` 那处 Teleport 上不能挂 v-show,把条件挪到内层容器。

实测:修复前切回合法代码后 0 个 svg(永久空白),修复后正常渲染。

## 每次渲染失败都往 body 漏一个 div

`m.render(id, code)` 没传容器,mermaid 会在 `document.body` 上建一个临时
`div#d{id}`。而 `suppressErrorRendering` 默认关着,看 mermaid 源码,解析出错时
是先 `errorRenderer.draw()` 再 `throw`,**清理临时容器的那行在 throw 之后**,
永远执行不到;每次 render 用的又是新的随机 id,`removeExistingElements` 也清不掉
旧的。于是渲染失败一次就留一个。

出题页是边敲边预览,且没有防抖,每个字符触发一次完整渲染,中间态几乎全是语法
错误 —— 实测逐字符敲 26 个字符,body 里留下 16 个残留 div。打开
`suppressErrorRendering`(该分支是先清理再抛)+ 预览防抖 300ms,实测降到 0,
预览功能不受影响。

## 顺带

`loadMermaid` 缓存的是实例,两个组件同屏挂载时会双双落进 `if (!mermaidInstance)`,
import 和 initialize 各跑两次。改成缓存 Promise,并在失败时清掉缓存,避免一次
网络抖动把后续所有渲染都钉死在这个失败结果上。

`SubmitFlowchart` 的 `updatePage` / `openDetailModal` / `loadToEditor` 一并补了
错误兜底(同文件,见下一个提交的说明):前两个失败时 `rendering` 会卡在 true,
弹框永久转圈;`loadToEditor` 是裸 `JSON.parse`,老提交数据坏掉就点了没反应。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 06:14:40 -06:00
xuyueandClaude Opus 5 8f08ed03a0 fix(流程图): 学生能翻出全班评分、AI 调用没有限流
## 列表漏了一道门

代码提交列表在 `routes/submission.ts` 里有 `submission_list_show_all` 兜底:
关掉时非管理员一律返回空。流程图列表**从来没有这道门**,而它的过滤是

    if (myself === "1" || (!username && 是普通用户)) 只看自己
    else if (username) 按用户名模糊匹配

—— 只要带上 `username`,第二支就把第一支的限制绕过去了。学生在提交记录页把
语言切成「流程图」、用户名框随便填一个字,就能翻出全班同学的 AI 评分,不需要
动接口。补上和代码提交同一套口径。

## 提交与重判没有限流

每一次流程图提交都会触发一次外部 AI 调用,是和判题沙箱同级的有限资源,而这两个
入口都没限流。`canView` 还允许**本人**重试自己的提交,等于学生可以对着自己的
提交反复点,无上限地刷 AI 调用。

限流桶不能直接用 `throttling:user:<id>` —— 那是代码提交在用的桶(capacity 20,
回填约 1.8 个/分钟),共用的话学生在机房连着交几次代码,流程图这边就会莫名其妙
交不上去。单独开 `throttling:user:flowchart:<id>`。

重判对教师放行:成批点几十行是他们的正常用法。

## 提交编号的权限判断在前端自己算了一遍

契约里 `flowchartListItem.showLink` 是后端逐行下发的(与 `GET /flowcharts/:id`
的放行条件同源),前端却没用,自己按「超管或本人」重算了一次 —— 教师因此看得到
「重新判题」却打不开评分详情。

更要命的是无权限那一支渲染的 `n-text` **照样挂着 @click**,权限判断只改了外观。
学生点别人的编号,后端以 404 挡下,`loadSubmission` 只 console.error,于是弹出
一个 600px 高的空白面板,什么提示都没有。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 06:14:05 -06:00
xuyueandClaude Opus 5 47a43b9880 fix(提交列表): 转圈圈延迟 300ms 出现,快请求不再闪一下
Deploy / deploy (push) Has been cancelled
翻页、切筛选大多一百毫秒内就回来了,转圈圈闪一下再切数据比直接切更晃眼。
给 loading 加一层延迟:只有真的慢到 300ms 以上才显示。

是**单向**延迟 —— 只推迟「显示」,请求一结束立刻收掉,不留尾巴(用
refDebounced 之类的双向去抖会在数据到了之后还多转 300ms)。

没有用 n-spin 的 delay:n-data-table 压根不走 n-spin,它渲染的是内部的
_internal/loading,直接由 loading 控制、没有 delay 参数。要用内置的就得把表格
包进 <n-spin :show :delay> 再去掉 :loading —— 那会换掉加载样式,还会丢掉
loading 时锁住排序/分页交互(DataTable 里那个 `disabled: this.loading`)。
这几行就是 n-spin 内部同样的写法,理由写在注释里了。

验证:headless chromium 走 CDP,只给 /api/submissions 加延迟(给全站加延迟的话
dev 模式几百个模块请求全被拖慢,页面根本起不来,第一次就是这么测出假结果的),
每 120ms 采一次 DOM:

    API 不加延迟   转圈圈全程没出现,表格直接 0 行 → 5 行
    API +800ms     表格 291ms 挂上 → 654ms 转圈圈出现(挂载后约 300ms)
                   → 1138ms 数据到位 → 1381ms 收掉

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 00:53:40 -06:00
xuyueandClaude Opus 5 2ee61756b8 perf(提交列表): count 去掉无谓 join、只取序列化用得到的列
用户反馈「HEADER 点提交后页面空白一段时间才有数据」。拿生产快照
(12.3 万条提交 / submission 表 169MB)在本机实测,问题分两头。

**后端**(本次改的):

- count 无条件 `innerJoin(problem)`,但 problem 只有按题号筛选时才出现在
  where 里。带 join 的 count 走 seq scan 78ms,去掉 join 走索引 7.5ms。
- 行查询 `select submission.* + problem.*` 把两张表所有列都拉回来,包括
  submission.code(学生源码)、info、ip,以及 problem 的 description /
  hint / samples / answers / flowchart_data / sql_display —— 这些字段 map
  的时候一个都没用上。改成只 select 需要的列。
  canViewSubmission 的参数类型随之从整行 $inferSelect 收窄成实际用到的
  字段,完整行结构上仍然满足,详情接口调用不受影响。

公开列表和比赛列表两处是同一份代码,一起改了。

**前端**:

- list.vue 静态 import 了四个只在默认关闭的 n-modal 里用的组件,其中两个
  统计面板还只有老师看得见。光 chart.js 就 197KB,进页面前必须先下完。
  改成 defineAsyncComponent 后本路由增量下载 675KB / 59 个文件 →
  375KB / 43 个文件。
- n-data-table 没传 :loading,等接口这段时间表格就是一片空白,连转圈都
  没有 —— 这是「页面空白」最直接的观感来源。用 try/finally 包,接口抛错
  不会把转圈卡死。
- isAuthed 变化时重复拉了一次今日提交数。它不看登录态,onMounted 那次
  就够了。列表本身仍然重拉(要更新提交编号列的可点击状态),那次不是
  浪费;本想用 userStore.isFinished 把首次请求延后,但 getProfile() 一旦
  reject,isFinished 会永远停在 false,匿名用户就再也看不到列表了。

还有一个更大头的原因是索引用不上,导致每次翻页全表扫 169MB,那部分
需要加索引,走下一个提交。

验证:起真实 API 打生产快照,匿名 / 已登录 / myself / 题号筛选 /
语言+状态 / today / offset=5000 / 比赛列表全部 200,响应体大小前后一致
(2990 bytes),字段没丢。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 07:55:47 -06:00
xuyueandClaude Opus 5 75dd9bfa42 fix(提交): 独立的 /submission/:id 页面上「复制回到题目」抛 problemID
## 复现与根因

在 `/submission/<提交id>` 这个独立页面点「复制回到题目」,必抛:

    Error: Missing required param "problemID"

`detail.vue` 的 `problemID` 是**可选** prop,但 `copyToProblem()` 无条件把它塞
进 `router.push({ params: { problemID } })`。三个组件调用方(提交列表弹框、
题目页提交弹框、后台 ACM 助手)都显式传了这个 prop,所以弹框里一直是好的;
而路由 `submission/:submissionID` 配的是 `props: true`,只喂得进
`submissionID` —— 那条路上这个 prop 恒为 undefined。

组件想自己兜底也兜不了:`submissionDetailSchema` 只有 `problemId`(内部数字
id),没有拼路由要用的 display id(`problem._id`)。

ojnext 里是**一模一样**的代码,不是这次重写引入的。

## 改法

后端补 `problemDisplayId`:`submissionDetail()` 本来就 join 了 problem 表,
不额外查库。前端 `props.problemID ?? submission.problemDisplayId`。

## ⚠️ 还剩一条边没关

非管理员看到的 `contestId` 是被抹掉的(`full ? contestId : null`,对齐旧后端
`SubmissionSafeModelSerializer` 的 exclude,把关的是角色不是归属)。所以**学生
自己的比赛提交**从这个独立 URL 打开时,组件判断不出它属于比赛,会跳到公开题
路由 `/problem/<显示编号>`,那里查的是 `contestId is null`,落到「题目不存在」。

改之前这条路是抛异常,改之后是跳错地方 —— 都不对,但主路径(非比赛提交)现在
是对的。要彻底关掉得让后端把 `contestId` 也发给**提交本人**(不只是管理员),
那是在动一条刻意对齐旧后端的决定,留给你定。另外这条路由**全站没有任何入口
链接**(查过了),只有直接输 URL 或外部链接才会到。

## 验证

真起服务、真点按钮:

- 修之前:点击不跳转,控制台 `Missing required param "problemID"`,
  Vue 警告里能看到 `<Detail submissionID="..." >` 确实没有 problemID。
- 修之后:无异常,跳到 `/problem/1004`(那条提交对应的题),
  编辑器里是这条提交的代码(`n = int(input())…`),语言也带过去了。
- 回归:走提交列表弹框(有传 prop 的那条路)照样无异常、照样跳 `/problem/1004`。
- tsc(apps/api) 0 error、check:routes 168 条无遮蔽、vue-tsc 0 error、build 通过。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 23:59:46 -06:00
xuyueandClaude Opus 5 0b7b08f2cc refactor(前端): 去掉 { error, data } 信封,api2 改名 api
Deploy / deploy (push) Has been cancelled
信封是 Django 时代的形状:拦截器手工造一个**恒为 null** 的 error 字段,再把
真正的载荷塞进 data。后端 http.ts 的 success 其实只返回 { data },那个 error
从头到尾没人用 —— 全站成功路径读 res.error 的只有 admin/api.ts 的
resetPassword 一处,而它自己就是个把信封拆开再重新包一遍的 shim。

代价是每个调用点都要 .data 一次:47 个组件、3 个 api 层文件、200 多处。
现在拦截器直接返回 response.data.data,ApiResponse<T> 退化成 T,文件末尾那句
`as unknown as Api2Client` 的类型谎言也少了一层。失败路径不动,仍然 reject
`{ error: 错误码, data: 文案 }` —— 和成功路径不对称是故意的,成功没有错误码
可言,接口注释里写清楚了。

顺带把 api2 改回 api:utils/ 下早就没有 api.ts 了,"2" 是迁移期用来和旧
client 区分的,现在只剩下让人多想一秒的作用。

## 怎么改的

**没有全局 sed。** 先把客户端的返回类型从 Promise<ApiResponse<T>> 改成
Promise<T>,让 vue-tsc 把每一处报出来(210 条),再按它给的 file:line:col
精确删 `.data`(192 处),剩下的手工处理:

- 6 处 `const { data } = await ...` 解构 → `const data = await ...`
- 3 个 api 层函数(getProfile / getProblem / getSubmission)自己手工造信封,
  改成直接返回值;getProfile 的返回类型跟着从 ApiResponse<Profile|null>
  变成 Profile|null

**类型检查抓不到的,人工把剩下的每一处 `.data` 过了一遍** —— 载荷本身带
data 字段、或者载荷是索引签名时,`res.data` 照样过类型。这一遍捞出三条真 bug:

- `getTutorialList` 的载荷是 `{ [key: string]: TutorialListItem[] }`(按
  python / c 分组)。索引签名让 `res.data` 编译通过、运行时是 undefined ——
  改完信封之后教程列表会**两个 tab 全空且不报错**。实跑确认过修好了。
- `createExercise` / `updateExercise` 返回 `res.data`,而 Exercise 自己有
  data 字段(练习内容)。两个调用方都不看返回值,所以类型和运行时都不响。
- `getSimilarProblems` 的 `.then(r => ({ ...r, data: r.data.map(...) }))`
  删掉 .data 之后变成往对象里摊一个数组,能跑但形状是错的。

另外两处是**对的**,加了注释免得下次被"顺手清理"掉:
StatisticsPanel 的 `res.data` 是契约 submissionStatisticsSchema 自己的 data
字段(每个学生一行);download.ts 是独立 axios 实例,`res.data` 是 axios 的
响应体(zip 二进制,不走信封)。

## 验证

tsc(apps/api) 0 error、check:routes 168 条无遮蔽、vue-tsc 0 error、vite build
通过。**因为这改动碰的是每一个请求,静态检查不够,起了全套服务用浏览器实跑:**

- oj 侧 12 个页面 + 后台 13 个页面逐个打开,断言没有重定向、console 无报错。
- 关键页面进一步断言渲染出了真数据(后台用户列表 3 行、题目列表 10 行、
  站点配置表单三个输入框有值、教程列表分组正确)。
- 三条写路径实打:重置密码(库里 student123 → 531554,表格当场刷新)、
  公告可见性开关(走 getAnnouncement + editAnnouncement,就是手改解构那处,
  库里 visible t → f)、提交代码(POST → 判题机真跑出 -2 → 提交列表和详情页
  都正确渲染状态、语言、代码)。
- /rank 有一条 `{error: "class-missing"}` 的未捕获 reject,stash 掉本次改动
  复现同样报错,**是既有问题**,不在本次范围内。

本地 dev 库为了打通后台测试改了三处,都只影响本机:devadmin 补了 email 和
user_profile 行(原来缺这两样,getProfile 报 profile-not-found,AUTHED 存不
进去,所有 /admin 路由被守卫弹回首页)、密码重置成 devpass123。冒烟用的教程/
公告/提交三条测试数据已删干净,题目和用户的提交计数也回滚了。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 22:42:20 -06:00
xuyue b35118f32e update
Deploy / deploy (push) Has been cancelled
2026-08-25 21:21:20 -06:00
xuyueandClaude Opus 5 c2a81209d7 refactor(前端): 拆掉 camelCase→snake_case 转换层,契约成为唯一真相
utils/legacy.ts 是迁移期的临时层:新后端一律 camelCase,而组件读的还是
旧 Django 的 snake_case,于是在 api 层做一次递归键名重写。它自己的注释就
写了「迁移完成后这一层应当整体拆掉」。现在拆了。

代价不只是那 96 处包装:每个响应都要递归遍历整个对象重写一遍键名,而且
utils/types.ts 和 packages/contract 是两份真相 —— 手抄的那份还抄歪了好几处。

做法是按域推进,每域都用 vue-tsc 相对基线做差,确认零新增错误后再往下走。
前端的类型现在一律以契约为准,只在必要处窄化(比如 languages/template 的键
窄化成 LANGUAGE),删掉的重复定义包括 WebsiteConfig、LoginSummary、
AchievementSummary、ProblemSet、Contest、User、Profile、AdminTag、
StuckProblem 等等,其中 ClassComparison 有两个组件各手抄了一份。

## 顺带修掉的真 bug

- 管理端公告列表的「可见」开关每次都 400:列表响应被契约 omit 掉了 content,
  而更新接口要求 content 必填,toggleVisible 把列表行原样回传。而且是乐观
  翻转、不 await 不 catch,管理员看到开关动了、实际没存也没有提示。
  改成先 GET 整条再 PUT,加失败提示。

- 删有提交的题时只显示笼统的「删除失败」:前端还在 match 旧 Django 的英文
  文案,而后端返回的是 problem-has-submissions + 中文。连同另外 8 处同类
  匹配一起改成判错误码 —— 文案是后端随时能改的,match 文案改一个字就静默失效。

- SubmissionStatus.time_limit_exceeded 写成 `1 | 2`,TS 按位或算成 3,和
  memory_limit_exceeded 撞了同一个值。后端 judge/status.ts 里这是分开的
  两个码,按后端拆成 cpu_/real_ 两项。当前没有代码读这两个成员,但
  CLAUDE.md 明确要求判题状态码三处同步。

- 流程图历史翻到没有提交的那一页会直接抛:契约里 submission 是 nullable,
  被 any 掩盖成看起来非空。补了 null 分支。

## 契约里被逼出来的三处不诚实

- grade 写成 z.string(),但 averageGrade() 在没有可用数据时返回空串,
  前端三张图表拿它查 Record<Grade,...> 会查出 undefined。按实际收紧成
  z.enum([...,""]),四个查表点都补了「无评级」分支。

- difficulty 写成 z.string()。核对过生产库 dump:956 道题只有
  Low/Mid/High 三个值(761/149/46)。收紧成枚举。

- topReaction 写成 z.string(),既对不上前端渲染的 {type,count},也对不上
  旧后端 get_top_reactions 下发的形状。改成正确形状并注明当前恒传 null。

## 明确保留 snake_case 的 54 处

判题沙箱原始输出(cpu_time/exit_code/output_md5/compile_output)、
statistic_info 内容(err_info/time_cost/ast_results)、submission_info
JSONB(is_ac/ac_time/error_number,回滚时旧后端还要读)、SQL 判题引擎的
total_rows/order_sensitive/changed_tables、WebSocket 的 submission_id、
以及数据库选项键 enable_maxkb。每一处都在类型定义旁写了为什么不能改。

language 没有跟着收紧契约 —— 它是配置项、随时可能加语言,收紧会让新语言
在后端 parse 时直接抛。改在 api 边界一处窄化。

## 另外

- utils/http.ts 整个模块已是死代码(四处引用全是 import type),删除。
- profile 的 blog/github/school/major/language 五个字段全链路空转,没有
  任何组件读,从契约到类型一并摘除(数据库列不动)。
- admin/account.ts 往 user_profile 塞的 totalScore 是 OI 模式遗留,表里
  没这一列。Drizzle 按表定义拼列名会把它静默丢弃,所以没出过错,是死代码。

验证:vue-tsc 143 → 54 条且无新增,apps/api tsc、check:routes、web build
全通过;各域响应形状逐条打接口核对过。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:44:24 -06:00
xuyue ae1fb329b5 feat(阶段1): 搬入 ojnext 为 apps/web,未改业务代码 2026-08-06 21:18:16 -06:00