Commit Graph
28 Commits
Author SHA1 Message Date
xuyueandClaude Opus 5.5 b7ec65eee8 feat(提交详情): 点了班级筛选之后,要挪到上面班级下拉去点 × 才能取消——右栏那个班级标签原地变成「取消」
已经在筛这个班时,标签不再消失,填色带 ×,悬停变红,再点一下回到全部班级。手机上顺手关掉详情抽屉。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:40:08 +08:00
xuyueandClaude Opus 5.5 2fafb25959 style(提交详情): 状态那一行挤得快放不下——去掉「测试点 1、2、3 答案错误」和耗时、内存
Deploy / deploy (push) Canceled after 0s
测试点小方块本身带颜色、悬停有结果名,后面再写一遍是重复;耗时和内存在课堂上没人看。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:28:59 +08:00
xuyueandClaude Opus 5.5 c22521fb61 fix(答案错误): 说明写「在例子 1 上就错了」,旁边测试点 1 是绿的,看着像写反了——改成「题目里的例子 1 就错了」
例子是题面样例,测试点是隐藏数据,两边都从 1 编号,只写「例子 1」分不开。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:18:06 +08:00
xuyueandClaude Opus 5.5 fa092d334c feat(编译错误): C / C++ 编译没过时也给中文说明、标出出错位置,中文标点能一键换成英文
Deploy / deploy (push) Canceled after 0s
原来 C / C++ 的编译错误学生看到的是 gcc 的英文原文。现在和 Python 一样一张翻译表
(cError.ts),照全库 2851 条 C / C++ 编译错误用现在的判题机(gcc-14)重新编译出来的
报错定的,只看第一条 error,98% 能给出说明。

三件事先于句式判,因为 gcc 对同一个错报法五花八门:
- 交成了 Python 代码(330 条):gcc 报的是 request for member 之类,直接说换语言;
- C 里写了 C++(iostream、cout);
- 中文标点:gcc-14 不再报 stray,而是把「;」吞进变量名、错报到下一行,所以直接在
  代码里找引号和注释之外的中文标点,行号按代码算。中文标点查找认 C 的 // 和 /* */。

句式上:缺分号报在下一句开头的往回挪一行,把分号写成冒号、关键字拼错(coutinue、
RETURN、return0)、文字没加引号、#include 拼错、main 外面写语句等各有说法。
gcc 的 did you mean 会从库函数里挑(num → enum),只信学生代码里有的名字。
列范围按 gcc 的显示宽度换算(中文占两格),含中文的行也能标到具体位置。

卡片改名 CompileErrorExplain,提交结果、运行例子、提交列表弹框三处都接上了。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:22:31 +08:00
xuyueandClaude Opus 5.5 96ce9725b2 fix(语法错误): 有模板的题报错行号多 1;else 后面写了条件被说成「少了冒号」,a+b=c 被说成「判断相等要用 ==」
全库 9182 条 Python 编译错误逐条过了一遍翻译表,1014 条的说明变了:

- 行号:判题跑的是「模板 + 代码」,运行时错误扣了模板行数、编译错误没扣,
  「第 N 行」和标红都落在下一行(691 条)。提交和运行例子两条路都扣上,
  gcc 的 main.c:N: 和代码摘录左边的行号一起扣。报错前先打了一条警告时
  File 那行是缩进的,行号原来解析不出来。
- 同一句报错要看出错那一行才分得开:expected ':' 九成是 else a<60:(行尾有冒号),
  「Maybe you meant '=='」只有 if / while 行才是真想判相等,a+b=c、a=96,b=92、
  print(1+1=2) 各有各的说法。
- 选了 Python 却交了 C 代码(294 条),直接说换语言。只凭 printf 不算:
  Python 里 print 手误成 printf 的不少。
- 单独说清楚的:elif: 没写条件、esle / else if、=< 写反、条件之间用逗号、
  漏了 +、√ ² ≤ ≥、顿号、全角 #、emoji(正则少了 u 标志)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:44:09 +08:00
xuyueandClaude Opus 5.5 f413518a85 fix(运行时错误): 列表练习里 append() 参数给错,说明却是「把文字当成数字、input() 读进来的是文字」,代码里根本没有 input()
全库 9228 条 Python 运行时错误在判题沙箱里重跑了一遍(9028 条复现),按真实回溯重定归类表:

- TypeError 原来只认 12 种句式,633 条掉进兜底,一律被说成「把文字当成数字」。
  实际多半是函数用法错:参数个数不对(点名 append / insert 并给正确写法)、
  end= 拼成 enp=、list.index(x) 该写变量名.index(x)、忘写 ()(int(input)、
  input[0],约 200 条)、拿 print() / append() 的返回值去算(约 100 条)等。
  兜底从 633 条降到 64 条,ValueError 从 336 降到 5(split("")、%.2f 写错、
  值不在列表里)。
- input[0] 原来被说成「对一个数字用了下标」:值的类型名比句式更说明问题,
  builtin_function_or_method / NoneType 先于句式判。类型名来自代码不来自输入。
- AttributeError 带上 Python 的「Did you mean」(spilt → split 一种就 188 条),
  s.len() 说成函数不是方法,数字没有 split() 说顺序反了。
- 「input() 读进来的是文字」只在学生代码里真有 input( 时才说。
- C:信号 31(system("pause") 被沙箱拦 / 字符数组溢出)和 25(死循环打印)
  原来只有「崩溃了」,占 C 运行时错误一半多;void main 不再说成「要写 return 0」。

已经存进库的诊断没有新的 kind,旧提交看到的是新的中性兜底话。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:43:44 +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 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 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
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 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 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 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 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