三条迁移,一次部署(0008/0009 含 DROP COLUMN,需要 OJ2_ALLOW_DESTRUCTIVE=1): - 0008 删 IP 相关:比赛 IP 白名单(前端本来就没有输入框,detail.vue 无条件置空)、 submission.ip(前端从未显示过)、以及一次都没被调用过的 IP 限流桶。 judge_server.ip 是运维数据,保留。 - 0009 删九个只有 Django 时代写过、OJ2 一次都没读过的列:user 的 auth_token / open_api / open_api_appkey / session_keys,user_profile 的 blog / github / school / major / language。open_api 后台连开关都没有,那段「已经开着就不重置 appkey」的逻辑从上线起没进过 if。判据是「全仓零读取」而不是「看着没用」—— raw_password 同样刺眼却是在用的,别一起清掉。 - 0010 给 17 条外键补上删除动作,不再是 Django 留下的一律 NO ACTION。父行消失后 必然无意义、且不构成学生留痕的走 CASCADE(中间表、题单/教程/成就的组成部分、 user_profile 与 user_stat);需要人看见的继续拦着——submission.problem_id、 以及 user 的绝大多数外键,删用户撞外键会被 handler 翻译成「请改为禁用账号」, 这是有意的:全 CASCADE 会静默抹掉成就与进度,而 submission.user_id 压根没有 外键,结果是一半删一半留。六处手工级联随之删掉。 角色字符串收进 packages/contract/src/roles.ts:原先 ADMIN_ROLES / TEACHER_ROLES 在两个文件各抄一份、学生口径在四个文件各写一遍、前端 USER_TYPE 是第三份副本。 AuthUser.adminType 与 drizzle 的列都收窄成联合类型,二十多处 `=== "Super Admin"` 从此受编译器管着($type 是纯 TS 层的,generate 确认不产生任何 SQL 变更)。 顺带删掉 db/relations.ts —— drizzle-kit pull 的产物,全仓零引用。 一处行为变化:后台用户列表传非法的 ?type= 回 400,不再静默返回空列表;界面上的 下拉只有合法值,打不到这条。 验证:tsc / vue-tsc / vite build / check:routes 全过;三条迁移在 dev 库执行, 并逐条建 fixture 走 HTTP 接口验过删除连坐与拦截(题单五张子表连坐、user_badge 二级连坐、删有提交的题目仍 409、删有表情的用户仍 409)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeJoYc2t2d7cThVqMBYrBF
178 lines
6.7 KiB
TypeScript
178 lines
6.7 KiB
TypeScript
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(),
|
||
/**
|
||
* 来源题单。学生从 `/problemset/:id/problem/:pid` 那个入口提交时前端带上,
|
||
* 后端落进 `submission.problemset_id`,提交列表据此标出「这条是刷题单刷出来的」。
|
||
*
|
||
* 只是**来源标记**,不参与判题、也不参与题单进度记账 —— 进度由判完之后的
|
||
* `recordSolvedProblem` 记进所有已加入且含这道题的题单,和从哪个入口进来无关。
|
||
* 所以这里带错了顶多是标记不准,不会影响成绩。
|
||
*/
|
||
problemSetId: 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()),
|
||
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"))` ——
|
||
* 这些键**根本不出现**,而不是出现但值为空。(`ip` 已随 IP 功能整体删除。)
|
||
*
|
||
* 独立成一个 schema 而不是复用 submissionDetailSchema 传空值:形状一致了,
|
||
* 将来有人「顺手」把空值改成真值就不会变成泄露,因为这里压根没有这些字段。
|
||
*/
|
||
export const embeddedSubmissionSchema = submissionDetailSchema
|
||
.omit({ info: 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()),
|
||
/**
|
||
* 来源题单,非题单入口提交的为 null。比赛提交恒为 null(比赛题不会进题单)。
|
||
* 历史提交里只有「当年首次 AC 那一条」有值 —— 迁移 0007 从 problemset_submission
|
||
* 回填的就是这些,其余老提交无从判断入口,一律留空。
|
||
*/
|
||
problemSet: z.object({ id: z.number().int(), title: z.string() }).nullable(),
|
||
})
|
||
|
||
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>
|