Files
OJ2/packages/contract/src/submission.ts
yuetsh 6ac458e6f3 refactor(WebSocket): 判题/流程图推送改用驼峰,去掉没人读的三个字段
submission_id / time_cost / memory_cost / err_info 是 OJ2 两端自己定的线上格式,
没有第三方消费(/ws/submissions 只有 apps/web 一个客户端),没理由留着 snake。
flowchart 那条更别扭:同一个对象里 submission_id 是 snake、criteriaDetails 是驼峰。

三个字段直接删掉而不是改名 —— 它们是从 statistic_info 原样抄出来的一份,前端
一处都没读过(useSubmissionMonitor 只用 submissionId / result / status)。耗时和
错误信息在提交详情里本来就有,判完了去拉一次就是,不必让推送顺带背一份 JSONB
的形状。score 保留,它不涉及大小写。

**没动的都是有外部约束的**,别顺手一起改:

- 发给判题沙箱的请求体(language_config / max_cpu_time / max_memory /
  test_case_id / io_mode)和它回的字段(cpu_time / memory / test_case)——
  那是沙箱的 API,不是我们的
- 测试点 info 文件的键,沙箱直接读那个文件
- submission.statistic_info 里的 time_cost / err_info / ast_results ——
  判题机按这套写,12 万条历史提交就是这形状
- acm_problems_status、progress_detail 这些存量 JSONB
- AST 规则键(for_loop)、成就指标(accepted_count)、reaction 语义键 ——
  那是词汇表标识符不是字段名,for_loop 还要映射到 tree-sitter 的 while_statement

验证:起 dev 栈(api + worker + 沙箱),学生账号真提一次代码,抓 /ws/submissions
的帧:

    {"type":"submission_update","submissionId":"bf13b7f0…","result":6,"status":"pending"}
    {"type":"submission_update","submissionId":"bf13b7f0…","result":7,"status":"judging"}
    {"type":"submission_update","submissionId":"bf13b7f0…","result":-2,"status":"finished","score":0}

subscribe 帧也换成 submissionId 并被接受(否则会回一个 error 帧,没有)。
流程图那条路径要 AI 评分才跑得起来,本机没配,只做了类型检查。

前后端同一个 docker 栈一起构建部署,没有版本错配窗口。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 00:48:54 -06:00

164 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { z } from "zod"
import { paginatedSchema } from "./common"
export const judgeStatusSchema = z.union([
z.literal(-2),
z.literal(-1),
z.literal(0),
z.literal(1),
z.literal(2),
z.literal(3),
z.literal(4),
z.literal(5),
z.literal(6),
z.literal(7),
z.literal(8),
z.literal(10),
])
export const createSubmissionRequestSchema = z.object({
problemId: z.number().int().positive(),
language: z.string().min(1).max(32),
code: z.string().min(1).max(1024 * 1024),
contestId: z.number().int().positive().optional(),
})
export const createSubmissionResponseSchema = z.object({
submissionId: z.string(),
})
export const submissionDetailSchema = z.object({
id: z.string(),
createTime: z.string(),
userId: z.number().int(),
username: z.string(),
code: z.string(),
result: judgeStatusSchema,
info: z.unknown(),
language: z.string(),
shared: z.boolean(),
statisticInfo: z.record(z.string(), z.unknown()),
ip: z.string().nullable(),
contestId: z.number().int().nullable(),
problemId: z.number().int(),
/**
* 题目的展示编号problem._id。**独立的 /submission/:id 页面要靠它** ——
* 那条路由只喂 submissionID组件拿不到 display id而「复制回到题目」要用它
* 拼路由。原来只给内部数字 id于是那个按钮在这条路由上一点就抛
* `Missing required param "problemID"`。
*/
problemDisplayId: z.string(),
showLink: z.boolean(),
canUnshare: z.boolean(),
})
/**
* 内嵌在别处(目前只有站内信)的提交对象。对齐旧后端的
* `SubmissionSafeModelSerializer(exclude=("info", "contest", "ip"))` ——
* 这三个键**根本不出现**,而不是出现但值为空。
*
* 独立成一个 schema 而不是复用 submissionDetailSchema 传空值:形状一致了,
* 将来有人「顺手」把空值改成真值就不会变成泄露,因为这里压根没有这三个字段。
*/
export const embeddedSubmissionSchema = submissionDetailSchema
.omit({ info: true, ip: true, contestId: true, problemId: true })
// 旧 SubmissionSafeModelSerializer 里 problem 是
// `SlugRelatedField(slug_field="_id")`,即**展示用题号**而非数字主键。
// 站内信页面拿它拼 `/problem/<题号>` 链接,给数字 id 会拼出打不开的地址。
.extend({ problem: z.string() })
/**
* 判题进度推送。**只带前端真正要用的东西**:靠 submissionId 认领、靠 result /
* status 决定是继续等还是去拉详情。
*
* 这里曾经还带着 time_cost / memory_cost / err_info —— 从 statistic_info 原样
* 抄一份出来,前端一处都没读过。耗时和错误信息在提交详情里本来就有,判完了去
* 拉一次就是了,不必让推送顺带背一份 JSONB 的形状。
*/
export const submissionUpdateSchema = z.object({
type: z.literal("submission_update"),
submissionId: z.string(),
result: judgeStatusSchema,
status: z.enum(["pending", "judging", "finished", "error"]),
score: z.number().optional(),
})
export const submissionListItemSchema = z.object({
id: z.string(),
problem: z.string(),
problemTitle: z.string(),
showLink: z.boolean(),
createTime: z.string(),
userId: z.number().int(),
username: z.string(),
result: judgeStatusSchema,
language: z.string(),
shared: z.boolean(),
statisticInfo: z.record(z.string(), z.unknown()),
})
export const submissionListSchema = paginatedSchema(submissionListItemSchema)
export const shareSubmissionRequestSchema = z.object({ shared: z.boolean() })
/**
* 未完成学生。`realName` 是从用户名里剥掉 `ks<班级号>` 前缀后剩下的那一段,
* 不是 user.real_name 列 —— 与 F2「真名默认不下发」不冲突这里只有教师能看到
* 且教师面板的用途正是点名谁没做。
*/
export const unacceptedStudentSchema = z.object({
username: z.string(),
realName: z.string(),
})
export const submissionStatisticsUserSchema = z.object({
username: z.string(),
className: z.string().nullable(),
submissionCount: z.number().int(),
acceptedCount: z.number().int(),
// 百分比数值,不带 %。旧后端返回 "85.5%" 字符串,展示格式化交给前端。
correctRate: z.number(),
submissionItems: z.array(
z.object({ id: z.string(), result: judgeStatusSchema }),
),
})
export const submissionStatisticsSchema = z.object({
submissionCount: z.number().int(),
acceptedCount: z.number().int(),
correctRate: z.number(),
personCount: z.number().int(),
personRate: z.number(),
data: z.array(submissionStatisticsUserSchema),
dataUnaccepted: z.array(unacceptedStudentSchema),
})
export const formatCodeRequestSchema = z.object({
code: z.string().max(1024 * 1024),
language: z.enum(["python", "c", "cpp", "sql"]),
})
export const formatCodeResponseSchema = z.object({ code: z.string() })
export type JudgeStatus = z.infer<typeof judgeStatusSchema>
export type CreateSubmissionRequest = z.infer<
typeof createSubmissionRequestSchema
>
export type SubmissionDetail = z.infer<typeof submissionDetailSchema>
export type SubmissionUpdate = z.infer<typeof submissionUpdateSchema>
export type SubmissionStatistics = z.infer<typeof submissionStatisticsSchema>
export type SubmissionStatisticsUser = z.infer<
typeof submissionStatisticsUserSchema
>
export type UnacceptedStudent = z.infer<typeof unacceptedStudentSchema>
export type SubmissionListItem = z.infer<typeof submissionListItemSchema>
export type SubmissionList = z.infer<typeof submissionListSchema>
export type EmbeddedSubmission = z.infer<typeof embeddedSubmissionSchema>
export type CreateSubmissionResponse = z.infer<typeof createSubmissionResponseSchema>
export type FormatCodeResponse = z.infer<typeof formatCodeResponseSchema>
export type ShareSubmissionRequest = z.infer<typeof shareSubmissionRequestSchema>
export type FormatCodeRequest = z.infer<typeof formatCodeRequestSchema>