后台题单接口下发的是扁平结构(displayId / title / isRequired),前端却按前台 题单页的嵌套结构取值,naive-ui 的 key 也不解析点号路径,于是「题目ID」「题目 标题」两列永远是空的。同一处还连累了另外几个操作: - 编辑题目弹窗读 problem.problem.title,点「编辑」直接抛错; - 添加题目发 problem_id / is_required,api 层取 problemId,出站是 undefined, 被后端按参数错误 400 掉; - 编辑题目发 is_required,「必做」怎么改都不生效; - 新建/编辑奖章发 condition_type,toBadgeBody 读 conditionType,同样 400。 根因是 admin/api.ts 把这批后台接口的返回值标成了 oj 侧的 ProblemSet / ProblemSetBadge / ProblemSetProblem,加上 detail.vue 四个 handler 用 data: any 接弹窗数据,字段拼错编译器一句都不吭。现在返回值全部换成 Admin* 契约类型, handler 换成 AddProblemToSetRequest / UpdateProblemInSetRequest 和一个本地 BadgeFormData(条件类型是「完成所有题目」时不带 conditionValue,后端补 0), 两个奖章弹窗里的 const data: any 也一并拆掉。 验证:dev 栈实跑,列表两列有值、编辑弹窗正常打开、加题 / 改必做 / 建奖章 / 改奖章四条链路走完界面都成功落库;bun run type-check 通过。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011WmMi9tPrnSAwwAy2i4WAa
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>