信封是 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>
重判、提交统计、流程图统计这三条挂在旧后端的 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>