refactor(契约): 语言与判题产物的形状收进契约,学生端高频响应接上运行时校验
Some checks failed
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 两个键 —— 不会因为这次收紧在生产上抛错。
This commit is contained in:
2026-09-10 04:19:41 -06:00
parent a475cac128
commit aab0404ed7
11 changed files with 414 additions and 115 deletions

View File

@@ -0,0 +1,45 @@
import { z } from "zod"
/**
* 判题沙箱认得的语言。**这是全仓唯一的语言集合定义处。**
*
* 键名必须与 `apps/api/src/judge/languages.ts` 的 `languageConfigs` 一致 ——
* 沙箱的编译/运行命令按这些键查表,查不到就是 `Unsupported judge language`。
* 后端那边是 `Record<string, …>`(判题机要按名字取配置,不能窄化成联合),
* 所以这个联合是**手写**的,改 languages.ts 时两边一起改。
*
* 这 6 种是**现在还能提交**的语言。含历史值 `Python2`:判题机已经没有它的
* 编译配置了,但生产库里有 3 条当年用 Python2 提交的记录,提交列表要能渲染出来。
*/
export const judgeLanguageSchema = z.enum([
"Python2",
"Python3",
"C",
"C++",
"Java",
"JavaScript",
"Golang",
])
/**
* 题目可以挂的语言 = 沙箱语言 + 两种非沙箱题型。
*
* - `SQL` 走 `judge/sql/` 那条独立链路(子进程 + sql.js不经过沙箱
* - `Flowchart` 走 AI 评分flowchart/run.ts也不经过沙箱。
*
* **两者都是真实可选、真实有历史数据的**,不是预留值:生产库 961 道题里有
* 9 道 SQL 题、124191 条提交里有 91 条 SQL。前端原来手抄的语言联合漏了 SQL
* 于是那 91 条提交的 `language` 在类型上是 `undefined` —— 这就是两份真相
* 各自演进的代价,现在并成一份。
*/
export const problemLanguageSchema = z.enum([
...judgeLanguageSchema.options,
"SQL",
"Flowchart",
])
/** 沙箱语言组成的数组(顺序即提权顺序,前端用它排语言 tab */
export const JUDGE_LANGUAGES = judgeLanguageSchema.options
export type JudgeLanguage = z.infer<typeof judgeLanguageSchema>
export type ProblemLanguage = z.infer<typeof problemLanguageSchema>