契约原来有 184 个 schema 但只导出了 104 个类型,缺的那 80 个前端只能照着
手抄一遍 —— 这是「表格列静默空白」那一类 bug 的根因(d3348f9、2edb8cf,
以及上一个 commit 修的 Top100 两列)。现在 186 个 schema 对 186 个类型,
一一对应,下次要用直接 import。
补导出是机械的(fooSchema → Foo),零命名冲突。真正有价值的是换的过程中
契约逼出来的 5 处分歧 —— 手抄那份在说谎,而 vue-tsc 拦不住,因为类型说它是对的:
- Tutorial.createdBy 手抄成了可选的 `User`(即 AdminUser,带 email、
rawPassword),后端下发的是必有的 SampleUser。读 createdBy.email 会拿到
undefined。列表页因此被迫写 `row.createdBy?.username` 和 `row.createdAt!`,
换成契约类型后两处断言都不需要了。
- TutorialListItem 手抄成 `Omit<Tutorial, "content">`,但后端列表接口连 code
一起省了 —— 类型声称 code 在。
- Testcase 手抄成 `{input_name, output_name, score}`:响应实际有 5 个字段,
且**没有 score**。score 是上传完成后前端按测试点数量平分补上去的,手抄那份
把本地字段说成了响应字段。现在写成 `TestCaseEntry & { score: string }`。
- Tag 手抄成 `{id, name}`,契约是 `{id, name, problemCount}` —— shared/api.ts
只好用 `Tag & { problemCount: number }` 把丢掉的补回来。
- CreateMessage 是旧后端按名字投递的形状(sender/recipient/submission),
契约要的是 recipientId/submissionId。全仓零引用,删掉。
同时删掉另外两个零引用的手写类型:LANGUAGE_SHOW_LABEL、UserAdminType;
本地重复的 SampleUser 换成契约的;oj/problem/list.vue 里本地第三份 Tag 改成
`ContractTag & { checked: boolean }`。
需要收窄的一律**从契约派生再收窄**,字段名跟着契约走,只有真正本地的那一两个
键是自己的:
export type Exercise = Omit<AdminExercise, "data"> & { data: 七种题型的联合 }
export type SubmitCodePayload =
Omit<CreateSubmissionRequest, "language"> & { language: LANGUAGE }
保留不动的窄化:StatisticInfo / SubmissionInfo(判题 JSONB 原文,snake_case)、
SQLDisplay*、ProblemFiltered(视图模型)、Exercise*Data(契约里 data 就是
Record<string, unknown>,七种题型结构不同,后端本来也不校验)。
types.ts 的手写 interface 从 31 个降到 22 个。
顺带修的代码:admin/tutorial/detail.vue 新建教程的表单对象缺三个后端产出的
字段,加了 TutorialEdit(对齐 BlankProblem / BlankContest 的写法);三处测试点
上传原来是拿响应对象原地塞 score,改成 map 出新对象,分数算法一字未改。
验证:apps/api tsc(7.0.2) 0 error、check:routes 168 条无遮蔽、
apps/web vue-tsc 0 error、vite build 通过。改动绝大部分在类型层,运行时只有
测试点上传那三处(等价替换)—— **那条路径要传 zip 才能实跑,没有实打**,
只做了代码等价性核对。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
114 lines
3.7 KiB
TypeScript
114 lines
3.7 KiB
TypeScript
import { z } from "zod"
|
|
|
|
import { paginatedSchema, sampleUserSchema } from "./common"
|
|
import { problemListItemSchema } from "./problem"
|
|
|
|
export const problemSetUserProgressSummarySchema = z.object({
|
|
isJoined: z.boolean(),
|
|
progressPercentage: z.number(),
|
|
completedCount: z.number().int(),
|
|
totalCount: z.number().int(),
|
|
isCompleted: z.boolean(),
|
|
})
|
|
|
|
export const problemSetBadgeSchema = z.object({
|
|
id: z.number().int(),
|
|
problemsetId: z.number().int(),
|
|
name: z.string(),
|
|
description: z.string(),
|
|
icon: z.string(),
|
|
conditionType: z.enum(["all_problems", "problem_count", "score"]),
|
|
conditionValue: z.number().int(),
|
|
isEarned: z.boolean().optional(),
|
|
})
|
|
|
|
export const problemSetSchema = z.object({
|
|
id: z.number().int(),
|
|
title: z.string(),
|
|
description: z.string(),
|
|
createdBy: sampleUserSchema,
|
|
createTime: z.string(),
|
|
lastUpdateTime: z.string().optional(),
|
|
difficulty: z.enum(["Easy", "Medium", "Hard"]),
|
|
status: z.enum(["active", "archived", "draft"]),
|
|
endTime: z.string().nullable(),
|
|
visible: z.boolean(),
|
|
problemsCount: z.number().int(),
|
|
completedCount: z.number().int().optional(),
|
|
userProgress: problemSetUserProgressSummarySchema,
|
|
badges: z.array(problemSetBadgeSchema).optional(),
|
|
})
|
|
|
|
export const problemSetListSchema = paginatedSchema(problemSetSchema)
|
|
|
|
export const problemSetProblemSchema = z.object({
|
|
id: z.number().int(),
|
|
problemsetId: z.number().int(),
|
|
problem: problemListItemSchema,
|
|
order: z.number().int(),
|
|
isRequired: z.boolean(),
|
|
score: z.number().int(),
|
|
hint: z.string().nullable(),
|
|
isCompleted: z.boolean(),
|
|
})
|
|
|
|
export const joinProblemSetRequestSchema = z.object({
|
|
problemSetId: z.number().int().positive(),
|
|
})
|
|
|
|
export const updateProblemSetProgressRequestSchema = z.object({
|
|
problemSetId: z.number().int().positive(),
|
|
problemId: z.number().int().positive(),
|
|
submissionId: z.string().min(1),
|
|
})
|
|
|
|
export const completedProblemSchema = z.object({
|
|
id: z.number().int(),
|
|
_id: z.string(),
|
|
title: z.string(),
|
|
})
|
|
|
|
export const problemSetProgressSchema = z.object({
|
|
id: z.number().int(),
|
|
problemsetId: z.number().int(),
|
|
user: sampleUserSchema,
|
|
joinTime: z.string(),
|
|
completeTime: z.string().nullable(),
|
|
isCompleted: z.boolean(),
|
|
progressPercentage: z.number(),
|
|
completedProblemsCount: z.number().int(),
|
|
totalProblemsCount: z.number().int(),
|
|
totalScore: z.number().int(),
|
|
completedProblems: z.array(completedProblemSchema),
|
|
})
|
|
|
|
export const problemSetProgressListSchema = paginatedSchema(problemSetProgressSchema).extend({
|
|
statistics: z.object({
|
|
total: z.number().int(),
|
|
completed: z.number().int(),
|
|
avgProgress: z.number(),
|
|
}),
|
|
problems: z.array(completedProblemSchema),
|
|
})
|
|
|
|
export const userBadgeSchema = z.object({
|
|
id: z.number().int(),
|
|
userId: z.number().int(),
|
|
badge: problemSetBadgeSchema,
|
|
earnedTime: z.string(),
|
|
problemset: z.object({ id: z.number().int(), title: z.string() }),
|
|
})
|
|
|
|
export type ProblemSet = z.infer<typeof problemSetSchema>
|
|
export type ProblemSetList = z.infer<typeof problemSetListSchema>
|
|
export type ProblemSetBadge = z.infer<typeof problemSetBadgeSchema>
|
|
export type ProblemSetProblem = z.infer<typeof problemSetProblemSchema>
|
|
export type ProblemSetProgress = z.infer<typeof problemSetProgressSchema>
|
|
export type ProblemSetProgressList = z.infer<typeof problemSetProgressListSchema>
|
|
export type UserBadge = z.infer<typeof userBadgeSchema>
|
|
export type CompletedProblem = z.infer<typeof completedProblemSchema>
|
|
|
|
export type ProblemSetUserProgressSummary = z.infer<typeof problemSetUserProgressSummarySchema>
|
|
export type JoinProblemSetRequest = z.infer<typeof joinProblemSetRequestSchema>
|
|
export type UpdateProblemSetProgressRequest = z.infer<typeof updateProblemSetProgressRequestSchema>
|