Files
OJ2/packages/contract/src/submission.ts
yuetsh cebaa87b13 fix(Minor M-1): 站内信内嵌提交改用独立 schema,并修复题号链接
两件事:

1. 内嵌的 submission 之前复用 submissionDetailSchema 并把 info / ip 写死成空值,
   于是键仍留在响应里。旧 SubmissionSafeModelSerializer 是 exclude,这三个键
   根本不出现。改成独立的 embeddedSubmissionSchema —— 形状对上了,将来有人
   把空值改成真值也不会变成泄露,因为这里压根没有这三个字段。

2. 顺带修掉一个评审没覆盖的真回归:内嵌 submission 给的是 problemId(数字主键),
   而旧 serializer 的 problem 是 SlugRelatedField(slug_field="_id"),即展示用题号。
   oj/user/message.vue:20 拿它拼 /problem/<题号>,迁移后拼出的是 /problem/undefined,
   题号那一栏也是空的。改为下发 problem,实测返回 "1004"。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 07:01:13 -06:00

143 lines
4.6 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(),
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() })
export const submissionUpdateSchema = z.object({
type: z.literal("submission_update"),
submission_id: z.string(),
result: judgeStatusSchema,
status: z.enum(["pending", "judging", "finished", "error"]),
time_cost: z.number().optional(),
memory_cost: z.number().optional(),
score: z.number().optional(),
err_info: z.string().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>