Files
OJ2/packages/contract/src/admin.ts
yuetsh bd84599174
Some checks failed
Deploy / deploy (push) Has been cancelled
feat(自学): 教程和练一练都留痕,老师能看到谁学了多少
自学模块以前一个字节都不落库:读到第几课只存在浏览器的 localStorage 里,
练一练的对错是组件内的一个 ref,刷新即失忆。老师能看到的只有「谁交了题」。

现在两张新表:
* tutorial_progress —— 一个学生 × 一课,记打开次数和累计停留秒数
* exercise_attempt  —— 一个学生 × 一道练习,记试了几次、错了几次、
  第几次做对的、最后一次做错时填的什么

都存聚合不存流水。练习那张表尤其明显:流水会随着学生反复点提交无限长,
而多出来的行回答不了任何新问题 ——「他第 3 次和第 5 次都选了 B」对老师
没有意义,「他试了 7 次才对」有。

停留时长只在页面可见、且十分钟内有过操作时才计。机房的电脑经常开着页面
就走了,不设这道闸的话「停留时长」会变成「电脑开机时长」,老师看到的
数字全是假的。换课、切标签页、关窗口都会先把攒着的秒数冲给**离开的那一课**。

练一练的对错仍然是前端判的:答案本来就随题面一起下发到浏览器,后端再判
一遍也挡不住任何人,只是重复实现七套判题。所以这是教学观察数据,不是成绩。
`last_wrong_answer` 存的是前端拼好的一句人话(「选了 C」「顺序 3-1-2」),
不是原始作答结构 —— 七种题型形状各不相同,存结构就得在后台按题型各写一套
渲染,而老师要看的只是他错在哪。

顺带修掉预测输出题的一个老问题:它的 `submitted` 一旦为真就不再收回,而
`allCorrect` 是跟着输入实时算的,于是学生错一次之后把答案改对,界面直接
跳成「输出正确!」、提交按钮同时禁用,submit() 再也执行不到 —— 这道题
**永远不会被记成做对**。排序/连线/找错/分组四种题本来就在交互处把 submitted
置回 false,只有这里漏了,按同一套补上。

学生端:目录每课显示「✓ 已读 · 11 分钟」和「练一练 3/5」。教程保持免登录
可读,未登录只是不留痕,并明说一句。

老师端:后台新开「自学情况」(教师及以上可进),三个 tab ——
按学生(默认把读得最少的排在最前,这张表要回答的是谁还没开始)、
按练习(每道题的正确率、一次做对几人、做对的人平均试几次;展开看逐人明细
和他们最后错在哪)、按课程。班级框填 3-4 位是具体班级,1-2 位当年级前缀。

外键用了库级 CASCADE,和 Django 建的那批 NO ACTION 不同:删教程、删用户
不必再记得回来手工清子表。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GW5ef6C2kRW8Ru27ghCaUu
2026-09-01 08:54:27 -06:00

872 lines
32 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 { achievementRaritySchema } from "./achievement"
import { rankProfileSchema } from "./account"
import { paginatedSchema, sampleUserSchema } from "./common"
import { reactionKeySchema } from "./content"
import {
astRulesSchema,
problemDifficultySchema,
sqlConfigSchema,
sqlDisplaySchema,
} from "./problem"
/**
* 后台侧的契约。与 oj 侧分开放:同一张表在两侧下发的字段集通常不同
* (后台要 `visible` 这类管理字段oj 侧连键都不该出现),
* 混在一个 schema 里迟早会有人为了省事在 oj 侧复用后台那个。
*/
export const adminAnnouncementSchema = z.object({
id: z.number().int(),
title: z.string(),
tag: z.string(),
content: z.string(),
visible: z.boolean(),
top: z.boolean(),
createdBy: sampleUserSchema,
createTime: z.string(),
lastUpdateTime: z.string(),
})
// 列表不带 content公告正文是 8MB 上限的富文本,列表页只显示标题
export const adminAnnouncementListItemSchema = adminAnnouncementSchema.omit({
content: true,
})
export const adminAnnouncementListSchema = paginatedSchema(
adminAnnouncementListItemSchema,
)
export const createAnnouncementRequestSchema = z.object({
title: z.string().trim().min(1).max(64),
tag: z.string().max(64),
content: z.string().max(1024 * 1024 * 8),
visible: z.boolean(),
top: z.boolean(),
})
export const updateAnnouncementRequestSchema = createAnnouncementRequestSchema
export type ProblemSample = z.infer<typeof problemSampleSchema>
export type ProblemAnswer = z.infer<typeof problemAnswerSchema>
export type ProblemTestCaseScore = z.infer<typeof problemTestCaseScoreSchema>
export type AdminAnnouncement = z.infer<typeof adminAnnouncementSchema>
export type AdminAnnouncementListItem = z.infer<
typeof adminAnnouncementListItemSchema
>
// ---------------------------------------------------------------- 教程 / 练习
export const tutorialTypeSchema = z.enum(["python", "c"])
export const adminTutorialSchema = z.object({
id: z.number().int(),
title: z.string(),
content: z.string(),
code: z.string().nullable(),
isPublic: z.boolean(),
order: z.number().int(),
type: tutorialTypeSchema,
createdBy: sampleUserSchema,
createdAt: z.string(),
updatedAt: z.string(),
})
export const adminTutorialListItemSchema = adminTutorialSchema.omit({
// 教程正文是整篇 markdown列表页只排序和切换可见性不需要它
content: true,
code: true,
})
/** 后台教程列表按语言分组返回,对齐旧 TutorialAdminAPI.get 的 `{python, c}` 形状 */
export const adminTutorialGroupsSchema = z.object({
python: z.array(adminTutorialListItemSchema),
c: z.array(adminTutorialListItemSchema),
})
export const createTutorialRequestSchema = z.object({
title: z.string().trim().min(1).max(128),
content: z.string(),
code: z.string().nullable().default(null),
isPublic: z.boolean().default(false),
order: z.number().int().default(0),
type: tutorialTypeSchema,
})
export const updateTutorialRequestSchema = createTutorialRequestSchema
export const setTutorialVisibilityRequestSchema = z.object({ isPublic: z.boolean() })
export const exerciseTypeSchema = z.enum([
"mcq",
"sort",
"fill",
"match",
"predict",
"debug",
"group",
])
export const adminExerciseSchema = z.object({
id: z.number().int(),
type: exerciseTypeSchema,
data: z.record(z.string(), z.unknown()),
order: z.number().int(),
})
export const createExerciseRequestSchema = z.object({
tutorialId: z.number().int().positive(),
type: exerciseTypeSchema,
data: z.record(z.string(), z.unknown()),
order: z.number().int().default(0),
})
export const updateExerciseRequestSchema = createExerciseRequestSchema.omit({
// 练习不支持改挂到别的教程下 —— 旧 EditExerciseSerializer 也没有 tutorial_id
tutorialId: true,
})
// ---------------------------------------------------------------- AI 报告
/**
* 列表只给摘要,正文要点开详情才拿 —— 与旧 AIAnalysisListSerializer 一致。
* `data`(原始 prompt 与结构化输入)和 `system_prompt` / `user_prompt` 两侧都不下发。
*/
export const adminAiReportListItemSchema = z.object({
id: z.number().int(),
username: z.string(),
createTime: z.string(),
analysisExcerpt: z.string(),
isPinned: z.boolean(),
})
export const adminAiReportSchema = z.object({
id: z.number().int(),
username: z.string(),
className: z.string().nullable(),
createTime: z.string(),
analysis: z.string(),
})
export const adminAiReportListSchema = paginatedSchema(adminAiReportListItemSchema)
export const toggleAiReportPinResponseSchema = z.object({ isPinned: z.boolean() })
// ---------------------------------------------------------------- 成就
// rarity 复用 achievement.ts 里已有的定义,不重复声明 —— 两份枚举迟早会长歪
export const achievementOperatorSchema = z.enum(["gte", "lte"])
export const achievementMetricSchema = z.object({
key: z.string(),
name: z.string(),
helpText: z.string(),
})
export const adminAchievementSchema = z.object({
id: z.number().int(),
name: z.string(),
description: z.string(),
icon: z.string(),
rarity: achievementRaritySchema,
hidden: z.boolean(),
metric: z.string(),
metricName: z.string(),
operator: achievementOperatorSchema,
threshold: z.number().int(),
visible: z.boolean(),
// 后台列表必须显示:阈值配错时学生永远拿不到也永远不会来问,这个计数器是唯一的仪表盘
unlockCount: z.number().int(),
order: z.number().int(),
createTime: z.string().nullable(),
})
export const createAchievementRequestSchema = z.object({
name: z.string().trim().min(1).max(64),
description: z.string().trim().min(1),
icon: z.string().trim().min(1),
rarity: achievementRaritySchema,
hidden: z.boolean().default(false),
metric: z.string().min(1),
operator: achievementOperatorSchema,
threshold: z.number().int(),
visible: z.boolean().default(true),
order: z.number().int().default(0),
})
export const updateAchievementRequestSchema = createAchievementRequestSchema
// ---------------------------------------------------------------- 用户管理
export const adminUserSchema = z.object({
id: z.number().int(),
username: z.string(),
email: z.string().nullable(),
adminType: z.string(),
problemPermission: z.string(),
realName: z.string().nullable(),
createTime: z.string().nullable(),
lastLogin: z.string().nullable(),
openApi: z.boolean(),
isDisabled: z.boolean(),
// 明文密码。是有意保留的运营需求:老师要能查学生的密码。
// 只在超管专属的这一个接口下发,别往任何其它地方复制。
rawPassword: z.string().nullable(),
className: z.string().nullable(),
})
export const adminUserListSchema = paginatedSchema(adminUserSchema)
export const updateUserRequestSchema = z.object({
username: z.string().trim().min(1).max(32),
email: z.email().max(64),
adminType: z.enum(["Regular User", "Student Admin", "Teacher Admin", "Super Admin"]),
problemPermission: z.enum(["None", "Own", "All"]),
realName: z.string().max(32).nullable().default(null),
isDisabled: z.boolean(),
openApi: z.boolean(),
// 空串表示不改密码,与旧 EditUserSerializer 的 allow_blank 一致
password: z.string().max(128).default(""),
})
/** 导入用户:每行 [用户名, 密码, 邮箱, 真名],与前端粘贴的 Excel 列序一致 */
export const importUsersRequestSchema = z.object({
users: z.array(z.tuple([
z.string().trim().min(1).max(32),
z.string().min(1),
z.string(),
z.string(),
])).min(1),
})
export const deleteUsersRequestSchema = z.object({
ids: z.array(z.number().int().positive()).min(1),
})
export const resetPasswordResponseSchema = z.object({ password: z.string() })
// ---------------------------------------------------------------- 站点配置 / 运维
export const updateWebsiteConfigRequestSchema = z.object({
websiteBaseUrl: z.string().max(256),
websiteName: z.string().trim().min(1).max(64),
websiteNameShortcut: z.string().trim().min(1).max(32),
websiteFooter: z.string().max(1024 * 64),
allowRegister: z.boolean(),
submissionListShowAll: z.boolean(),
classList: z.array(z.string()),
enableMaxkb: z.boolean(),
})
export const judgeServerSchema = z.object({
id: z.number().int(),
hostname: z.string(),
ip: z.string().nullable(),
judgerVersion: z.string(),
cpuCore: z.number().int(),
memoryUsage: z.number(),
cpuUsage: z.number(),
lastHeartbeat: z.string(),
createTime: z.string(),
taskNumber: z.number().int(),
serviceUrl: z.string().nullable(),
isDisabled: z.boolean(),
/** 心跳在 6 秒内才算在线,与 dashboard 的判活口径一致 */
status: z.enum(["normal", "abnormal"]),
})
export const judgeServerListSchema = z.object({
token: z.string(),
servers: z.array(judgeServerSchema),
})
export const updateJudgeServerRequestSchema = z.object({ isDisabled: z.boolean() })
export const orphanTestCaseSchema = z.object({
id: z.string(),
createTime: z.number(),
})
/**
* 后台的用户排名列表。行的形状与 oj 侧榜单相同,但**没有 100 名的上限**
* 这里不是榜单,是老师按班级前缀翻全班学生的管理视图,要能一直翻到底。
*
* 独立成一条端点而不是给公开榜单加个「不限量」开关:那个开关一旦存在,
* 匿名请求就能拉走全校学生名单和个性签名。
*/
export const adminUserRankSchema = paginatedSchema(rankProfileSchema)
export const dashboardInfoSchema = z.object({
userCount: z.number().int(),
recentContestCount: z.number().int(),
todaySubmissionCount: z.number().int(),
judgeServerCount: z.number().int(),
})
export const uploadImageResponseSchema = z.object({
success: z.boolean(),
msg: z.string(),
filePath: z.string(),
})
// ---------------------------------------------------------------- 比赛管理
export const adminContestSchema = z.object({
id: z.number().int(),
title: z.string(),
description: z.string(),
tag: z.string(),
startTime: z.string(),
endTime: z.string(),
createTime: z.string(),
lastUpdateTime: z.string(),
// 后台要能看到自己设的密码用来告诉学生oj 侧的 contestSchema 则永远不含它
password: z.string().nullable(),
visible: z.boolean(),
allowedIpRanges: z.array(z.string()),
createdBy: sampleUserSchema,
status: z.enum(["1", "0", "-1"]),
contestType: z.enum(["Public", "Password Protected"]),
})
export const adminContestListSchema = paginatedSchema(adminContestSchema)
export const createContestRequestSchema = z.object({
title: z.string().trim().min(1).max(128),
description: z.string(),
tag: z.string().max(64),
startTime: z.string().min(1),
endTime: z.string().min(1),
// 空串等同于「不设密码」,与旧 CreateConetestSeriaizer 的 allow_blank 一致
password: z.string().max(32).nullable().default(null),
visible: z.boolean(),
allowedIpRanges: z.array(z.string().max(32)).default([]),
})
export const updateContestRequestSchema = createContestRequestSchema
export const acmHelperItemSchema = z.object({
id: z.number().int(),
username: z.string(),
realName: z.string().nullable(),
problemId: z.string(),
problemDisplayId: z.string(),
acInfo: z.record(z.string(), z.unknown()),
checked: z.boolean(),
})
export const updateAcmHelperRequestSchema = z.object({
rankId: z.number().int().positive(),
problemId: z.string().min(1),
checked: z.boolean(),
})
// ---------------------------------------------------------------- 题单管理
export const problemSetDifficultySchema = z.enum(["Easy", "Medium", "Hard"])
export const problemSetStatusSchema = z.enum(["draft", "active", "archived"])
export const badgeConditionTypeSchema = z.enum(["all_problems", "problem_count", "score"])
export const adminProblemSetSchema = z.object({
id: z.number().int(),
title: z.string(),
description: z.string(),
difficulty: problemSetDifficultySchema,
status: problemSetStatusSchema,
endTime: z.string().nullable(),
visible: z.boolean(),
createdBy: sampleUserSchema,
createTime: z.string(),
lastUpdateTime: z.string(),
problemsCount: z.number().int(),
/** 加入这份题单的人数,后台用来判断改动会影响多少人 */
participantCount: z.number().int(),
})
export const adminProblemSetListSchema = paginatedSchema(adminProblemSetSchema)
export const createProblemSetRequestSchema = z.object({
title: z.string().trim().min(1).max(200),
description: z.string(),
difficulty: problemSetDifficultySchema.default("Easy"),
status: problemSetStatusSchema.default("active"),
endTime: z.string().nullable().default(null),
visible: z.boolean().default(true),
})
export const updateProblemSetRequestSchema = createProblemSetRequestSchema
export const updateProblemSetStatusRequestSchema = z.object({ status: problemSetStatusSchema })
export const adminProblemSetProblemSchema = z.object({
id: z.number().int(),
problemsetId: z.number().int(),
problemId: z.number().int(),
displayId: z.string(),
title: z.string(),
difficulty: z.string(),
order: z.number().int(),
isRequired: z.boolean(),
score: z.number().int(),
hint: z.string().nullable(),
})
export const addProblemToSetRequestSchema = z.object({
/** 展示用题号_id不是自增主键 —— 老师手里只有题号 */
problemId: z.string().trim().min(1),
order: z.number().int().default(0),
isRequired: z.boolean().default(true),
score: z.number().int().default(0),
hint: z.string().default(""),
})
export const updateProblemInSetRequestSchema = z.object({
order: z.number().int().optional(),
isRequired: z.boolean().optional(),
score: z.number().int().optional(),
hint: z.string().optional(),
})
export const adminProblemSetBadgeSchema = z.object({
id: z.number().int(),
problemsetId: z.number().int(),
name: z.string(),
description: z.string(),
icon: z.string(),
conditionType: badgeConditionTypeSchema,
conditionValue: z.number().int(),
/** 已获得该奖章的人数,后台改条件前要能看到影响面 */
earnedCount: z.number().int(),
})
export const createProblemSetBadgeRequestSchema = z.object({
name: z.string().trim().min(1).max(100),
description: z.string(),
icon: z.string(),
conditionType: badgeConditionTypeSchema,
conditionValue: z.number().int().default(0),
})
export const updateProblemSetBadgeRequestSchema = createProblemSetBadgeRequestSchema
export const adminProblemSetProgressSchema = z.object({
id: z.number().int(),
userId: z.number().int(),
username: z.string(),
realName: z.string().nullable(),
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(),
})
// ---------------------------------------------------------------- 标签与题目分析
export const adminTagSchema = z.object({
id: z.number().int(),
name: z.string(),
problemCount: z.number().int(),
})
export const renameTagRequestSchema = z.object({ name: z.string().trim().min(1).max(64) })
export const renameTagResponseSchema = z.object({
/** 改名撞上已有标签时视为合并:题目关系转移过去、原标签删除 */
merged: z.boolean(),
id: z.number().int(),
name: z.string(),
affectedCount: z.number().int(),
})
export const batchProblemTagRequestSchema = z.object({
problemIds: z.array(z.number().int().positive()).min(1),
tagNames: z.array(z.string()).min(1),
action: z.enum(["add", "remove"]),
})
export const batchProblemTagResponseSchema = z.object({
problemCount: z.number().int(),
tagCount: z.number().int(),
})
export const stuckProblemSchema = z.object({
problemId: z.string(),
problemTitle: z.string(),
total: z.number().int(),
failed: z.number().int(),
failedUsers: z.number().int(),
acRate: z.number(),
})
export const acTrendYearSchema = z.object({
year: z.number().int(),
total: z.number().int(),
accepted: z.number().int(),
acRate: z.number(),
})
export const acTrendSchema = z.object({
problemId: z.string(),
problemTitle: z.string(),
yearly: z.array(acTrendYearSchema),
})
// ---------------------------------------------------------------- 自学情况
/**
* 「自学情况」按学生的一行。**没读过任何一课的学生也要有一行**readCount = 0——
* 这张表首要回答的问题是「谁还没开始」,只列有记录的人等于把该看的人筛掉了。
*/
export const learnStudentProgressSchema = z.object({
userId: z.number().int(),
username: z.string(),
realName: z.string().nullable(),
className: z.string().nullable(),
readCount: z.number().int(),
totalSeconds: z.number().int(),
lastViewedAt: z.string().nullable(),
// 练一练:做过几道、做对几道、一共点了几次提交
exerciseTried: z.number().int(),
exerciseSolved: z.number().int(),
exerciseAttempts: z.number().int(),
})
export const learnStudentProgressListSchema = z.object({
// 分母:该语言下已公开的教程篇数,前端拿它显示 3/17
tutorialCount: z.number().int(),
// 分母:这些教程里一共有多少道练一练
exerciseCount: z.number().int(),
results: z.array(learnStudentProgressSchema),
})
/**
* 「按练习」一行。回答的是「哪道练习卡住了全班」——
* `solvedUsers / triedUsers` 是正确率,`avgAttemptsToSolve` 是做对的人平均试了几次,
* `firstTryUsers` 是一次就做对的人数。三个一起看才分得清「题目难」和「题目有歧义」:
* 前者是试了几次终于做对,后者是很多人第一次就掉进同一个坑。
*/
export const learnExerciseProgressSchema = z.object({
exerciseId: z.number().int(),
tutorialId: z.number().int(),
tutorialTitle: z.string(),
tutorialOrder: z.number().int(),
type: exerciseTypeSchema,
order: z.number().int(),
question: z.string(),
triedUsers: z.number().int(),
solvedUsers: z.number().int(),
firstTryUsers: z.number().int(),
attempts: z.number().int(),
avgAttemptsToSolve: z.number(),
})
export const learnExerciseProgressListSchema = z.object({
studentCount: z.number().int(),
results: z.array(learnExerciseProgressSchema),
})
/** 单道练习的逐人明细,后台表格展开时才拉 */
export const learnExerciseAttemptSchema = z.object({
userId: z.number().int(),
username: z.string(),
realName: z.string().nullable(),
className: z.string().nullable(),
attempts: z.number().int(),
wrongAttempts: z.number().int(),
solved: z.boolean(),
attemptsToSolve: z.number().int().nullable(),
lastWrongAnswer: z.string().nullable(),
lastAttemptAt: z.string(),
})
/** 「自学情况」按课的一行 */
export const learnTutorialProgressSchema = z.object({
tutorialId: z.number().int(),
title: z.string(),
order: z.number().int(),
readers: z.number().int(),
totalSeconds: z.number().int(),
avgSeconds: z.number().int(),
})
export const learnTutorialProgressListSchema = z.object({
// 分母:统计范围内的学生数(受班级筛选影响)
studentCount: z.number().int(),
results: z.array(learnTutorialProgressSchema),
})
export const generateFlowchartRequestSchema = z.object({ python: z.string().min(1).max(64 * 1024) })
export const generateFlowchartResponseSchema = z.object({ flowchart: z.string() })
// ---------------------------------------------------------------- 题目管理
export const adminProblemListItemSchema = z.object({
id: z.number().int(),
_id: z.string(),
title: z.string(),
createdBy: sampleUserSchema,
visible: z.boolean(),
createTime: z.string(),
difficulty: problemDifficultySchema,
tags: z.array(z.string()),
hasAstRules: z.boolean(),
allowFlowchart: z.boolean(),
showFlowchart: z.boolean(),
// 最高票评价 {type, count}。**只有公开题列表下发**,比赛题列表恒传 null ——
// 与旧后端 reaction/services.py:get_top_reactions 的口径一致。
topReaction: z
.object({ type: reactionKeySchema, count: z.number().int() })
.nullable(),
})
export const adminProblemListSchema = paginatedSchema(adminProblemListItemSchema)
/**
* 题面样例 / 标准答案 / 测试点分值。这三个旧后端都是逐字段校验的
* CreateSampleSerializer / CreateAnswerSerializer / CreateTestCaseScoreSerializer
* 新后端一路写成 `z.record(z.unknown())` **反而比旧的松**。
*
* 生产库 956 道题逐条比对过samples 947 条是 {input,output}(另外 9 条是空数组,
* SQL 题没样例、answers 268 条是 {language,code}633 条 null、55 条空数组)。
*/
export const problemSampleSchema = z.object({
input: z.string(),
output: z.string(),
})
export const problemAnswerSchema = z.object({
language: z.string(),
code: z.string(),
})
/**
* 测试点分值。**只有这三个键** —— 上传接口的响应比这多三个
* stripped_output_md5 / input_size / output_size旧后端的 DRF serializer
* 收下时把多的丢掉,生产库 956 行全是三键。zod 的 object 默认也剥未知键,
* 行为一致。
*
* score 前端算出来是字符串(`(100 / n).toFixed(0)`),旧后端 IntegerField 转成
* 整数落库7990 条全是 int —— 这里用 coerce 保持同一个落库形状。
*/
export const problemTestCaseScoreSchema = z.object({
input_name: z.string().max(32),
output_name: z.string().max(32),
score: z.coerce.number().int().min(0),
})
/** 后台题目详情:包含 oj 侧永不下发的 answers / testCase* / astRules */
export const adminProblemSchema = z.object({
id: z.number().int(),
_id: z.string(),
title: z.string(),
description: z.string(),
inputDescription: z.string(),
outputDescription: z.string(),
samples: z.array(problemSampleSchema),
testCaseId: z.string(),
testCaseScore: z.array(problemTestCaseScoreSchema),
hint: z.string().nullable(),
languages: z.array(z.string()),
template: z.record(z.string(), z.string()),
createTime: z.string(),
lastUpdateTime: z.string(),
timeLimit: z.number().int(),
memoryLimit: z.number().int(),
visible: z.boolean(),
difficulty: problemDifficultySchema,
source: z.string().nullable(),
submissionNumber: z.number().int(),
acceptedNumber: z.number().int(),
statisticInfo: z.record(z.string(), z.unknown()),
shareSubmission: z.boolean(),
contestId: z.number().int().nullable(),
createdBy: sampleUserSchema,
isPublic: z.boolean(),
tags: z.array(z.string()),
allowFlowchart: z.boolean(),
showFlowchart: z.boolean(),
mermaidCode: z.string().nullable(),
flowchartHint: z.string().nullable(),
astRules: astRulesSchema.nullable(),
answers: z.array(problemAnswerSchema),
prompt: z.string().nullable(),
sqlConfig: sqlConfigSchema.nullable(),
sqlDisplay: sqlDisplaySchema.nullable(),
})
export const createProblemRequestSchema = z.object({
_id: z.string().trim().min(1).max(32),
title: z.string().trim().min(1).max(1024),
description: z.string(),
inputDescription: z.string(),
outputDescription: z.string(),
samples: z.array(problemSampleSchema),
testCaseId: z.string().regex(/^[a-zA-Z0-9]+$/).max(32),
testCaseScore: z.array(problemTestCaseScoreSchema),
timeLimit: z.number().int().min(1).max(1000 * 60),
memoryLimit: z.number().int().min(1).max(1024),
languages: z.array(z.string()).min(1),
template: z.record(z.string(), z.string()),
visible: z.boolean(),
difficulty: z.enum(["Low", "Mid", "High"]),
tags: z.array(z.string().max(32)).min(1),
hint: z.string().nullable().default(null),
source: z.string().max(256).nullable().default(null),
prompt: z.string().nullable().default(null),
answers: z.array(problemAnswerSchema).default([]),
shareSubmission: z.boolean(),
allowFlowchart: z.boolean().default(false),
showFlowchart: z.boolean().default(false),
mermaidCode: z.string().nullable().default(null),
flowchartHint: z.string().nullable().default(null),
astRules: astRulesSchema.nullable().default(null),
// order_sensitive 缺省补 false —— 与旧后端 SQLConfigSerializer 的
// `BooleanField(default=False)` 一致
sqlConfig: sqlConfigSchema
.extend({ order_sensitive: z.boolean().default(false) })
.nullable()
.default(null),
})
export const updateProblemRequestSchema = createProblemRequestSchema
export const makeProblemPublicRequestSchema = z.object({
displayId: z.string().trim().min(1).max(32),
})
export const addContestProblemRequestSchema = z.object({
problemId: z.number().int().positive(),
displayId: z.string().trim().min(1).max(32),
})
export const testCaseEntrySchema = z.object({
stripped_output_md5: z.string(),
input_size: z.number().int(),
output_size: z.number().int(),
input_name: z.string(),
output_name: z.string(),
})
/**
* 测试点上传的返回。字段名保持 snake_case —— 它会被原样存进 problem.test_case_score
* 和落盘的 info 文件,而判题沙箱读的就是这套键名,改成 camelCase 会判不了题。
*/
export const uploadTestCaseResponseSchema = z.object({
id: z.string(),
info: z.array(testCaseEntrySchema),
})
export const sqlTestCaseScriptSchema = z.object({
name: z.string(),
content: z.string(),
})
export const sqlPreviewRequestSchema = z.object({
initSql: z.string().min(1).max(1024 * 1024),
refSql: z.string().min(1).max(1024 * 1024),
mode: z.enum(["query", "modify"]),
})
export const generateSqlTestCaseRequestSchema = z.object({
refSql: z.string().min(1).max(64 * 1024),
mode: z.enum(["query", "modify"]),
})
export const generateSqlTestCaseResponseSchema = z.object({ sql: z.string() })
export type AdminAchievement = z.infer<typeof adminAchievementSchema>
export type AchievementMetric = z.infer<typeof achievementMetricSchema>
export type AdminProblem = z.infer<typeof adminProblemSchema>
export type AdminProblemListItem = z.infer<typeof adminProblemListItemSchema>
export type AdminProblemList = z.infer<typeof adminProblemListSchema>
export type CreateProblemRequest = z.infer<typeof createProblemRequestSchema>
export type AdminContest = z.infer<typeof adminContestSchema>
export type AdminContestList = z.infer<typeof adminContestListSchema>
export type AcmHelperItem = z.infer<typeof acmHelperItemSchema>
export type AdminUser = z.infer<typeof adminUserSchema>
export type AdminUserList = z.infer<typeof adminUserListSchema>
export type JudgeServer = z.infer<typeof judgeServerSchema>
export type DashboardInfo = z.infer<typeof dashboardInfoSchema>
export type JudgeServerList = z.infer<typeof judgeServerListSchema>
export type OrphanTestCase = z.infer<typeof orphanTestCaseSchema>
export type AdminUserRank = z.infer<typeof adminUserRankSchema>
export type AdminAiReportListItem = z.infer<typeof adminAiReportListItemSchema>
export type AdminAiReport = z.infer<typeof adminAiReportSchema>
export type AdminAiReportList = z.infer<typeof adminAiReportListSchema>
export type StuckProblem = z.infer<typeof stuckProblemSchema>
export type LearnStudentProgress = z.infer<typeof learnStudentProgressSchema>
export type LearnStudentProgressList = z.infer<typeof learnStudentProgressListSchema>
export type LearnTutorialProgress = z.infer<typeof learnTutorialProgressSchema>
export type LearnTutorialProgressList = z.infer<typeof learnTutorialProgressListSchema>
export type LearnExerciseProgress = z.infer<typeof learnExerciseProgressSchema>
export type LearnExerciseProgressList = z.infer<typeof learnExerciseProgressListSchema>
export type LearnExerciseAttempt = z.infer<typeof learnExerciseAttemptSchema>
export type AcTrend = z.infer<typeof acTrendSchema>
export type AdminTag = z.infer<typeof adminTagSchema>
export type RenameTagResponse = z.infer<typeof renameTagResponseSchema>
export type BatchProblemTagResponse = z.infer<typeof batchProblemTagResponseSchema>
export type SqlTestCaseScript = z.infer<typeof sqlTestCaseScriptSchema>
export type GenerateSqlTestCaseResponse = z.infer<typeof generateSqlTestCaseResponseSchema>
export type AdminProblemSetProgress = z.infer<typeof adminProblemSetProgressSchema>
export type AdminAnnouncementList = z.infer<typeof adminAnnouncementListSchema>
export type CreateAnnouncementRequest = z.infer<typeof createAnnouncementRequestSchema>
export type UpdateAnnouncementRequest = z.infer<typeof updateAnnouncementRequestSchema>
export type TutorialType = z.infer<typeof tutorialTypeSchema>
export type AdminTutorial = z.infer<typeof adminTutorialSchema>
export type AdminTutorialListItem = z.infer<typeof adminTutorialListItemSchema>
export type AdminTutorialGroups = z.infer<typeof adminTutorialGroupsSchema>
export type CreateTutorialRequest = z.infer<typeof createTutorialRequestSchema>
export type UpdateTutorialRequest = z.infer<typeof updateTutorialRequestSchema>
export type SetTutorialVisibilityRequest = z.infer<typeof setTutorialVisibilityRequestSchema>
export type ExerciseType = z.infer<typeof exerciseTypeSchema>
export type AdminExercise = z.infer<typeof adminExerciseSchema>
export type CreateExerciseRequest = z.infer<typeof createExerciseRequestSchema>
export type UpdateExerciseRequest = z.infer<typeof updateExerciseRequestSchema>
export type ToggleAiReportPinResponse = z.infer<typeof toggleAiReportPinResponseSchema>
export type AchievementOperator = z.infer<typeof achievementOperatorSchema>
export type CreateAchievementRequest = z.infer<typeof createAchievementRequestSchema>
export type UpdateAchievementRequest = z.infer<typeof updateAchievementRequestSchema>
export type UpdateUserRequest = z.infer<typeof updateUserRequestSchema>
export type ImportUsersRequest = z.infer<typeof importUsersRequestSchema>
export type DeleteUsersRequest = z.infer<typeof deleteUsersRequestSchema>
export type ResetPasswordResponse = z.infer<typeof resetPasswordResponseSchema>
export type UpdateWebsiteConfigRequest = z.infer<typeof updateWebsiteConfigRequestSchema>
export type UpdateJudgeServerRequest = z.infer<typeof updateJudgeServerRequestSchema>
export type UploadImageResponse = z.infer<typeof uploadImageResponseSchema>
export type CreateContestRequest = z.infer<typeof createContestRequestSchema>
export type UpdateContestRequest = z.infer<typeof updateContestRequestSchema>
export type UpdateAcmHelperRequest = z.infer<typeof updateAcmHelperRequestSchema>
export type ProblemSetDifficulty = z.infer<typeof problemSetDifficultySchema>
export type ProblemSetStatus = z.infer<typeof problemSetStatusSchema>
export type BadgeConditionType = z.infer<typeof badgeConditionTypeSchema>
export type AdminProblemSet = z.infer<typeof adminProblemSetSchema>
export type AdminProblemSetList = z.infer<typeof adminProblemSetListSchema>
export type CreateProblemSetRequest = z.infer<typeof createProblemSetRequestSchema>
export type UpdateProblemSetRequest = z.infer<typeof updateProblemSetRequestSchema>
export type UpdateProblemSetStatusRequest = z.infer<typeof updateProblemSetStatusRequestSchema>
export type AdminProblemSetProblem = z.infer<typeof adminProblemSetProblemSchema>
export type AddProblemToSetRequest = z.infer<typeof addProblemToSetRequestSchema>
export type UpdateProblemInSetRequest = z.infer<typeof updateProblemInSetRequestSchema>
export type AdminProblemSetBadge = z.infer<typeof adminProblemSetBadgeSchema>
export type CreateProblemSetBadgeRequest = z.infer<typeof createProblemSetBadgeRequestSchema>
export type UpdateProblemSetBadgeRequest = z.infer<typeof updateProblemSetBadgeRequestSchema>
export type RenameTagRequest = z.infer<typeof renameTagRequestSchema>
export type BatchProblemTagRequest = z.infer<typeof batchProblemTagRequestSchema>
export type AcTrendYear = z.infer<typeof acTrendYearSchema>
export type GenerateFlowchartRequest = z.infer<typeof generateFlowchartRequestSchema>
export type GenerateFlowchartResponse = z.infer<typeof generateFlowchartResponseSchema>
export type UpdateProblemRequest = z.infer<typeof updateProblemRequestSchema>
export type MakeProblemPublicRequest = z.infer<typeof makeProblemPublicRequestSchema>
export type AddContestProblemRequest = z.infer<typeof addContestProblemRequestSchema>
export type TestCaseEntry = z.infer<typeof testCaseEntrySchema>
export type UploadTestCaseResponse = z.infer<typeof uploadTestCaseResponseSchema>
export type SqlPreviewRequest = z.infer<typeof sqlPreviewRequestSchema>
export type GenerateSqlTestCaseRequest = z.infer<typeof generateSqlTestCaseRequestSchema>