Deploy / deploy (push) Canceled after 0s
以前往届那一侧把所有记录都算进去,25 级比 24 级显示 73% / 82%, 其实 24 级在这些题上近一半是学了半年以上才做的;同期比是 73% / 75%。 现在每个人以自己第一次交题为起点,两边都只算离起点不超过 N 天的人题对, N = 两届里学得短的那届一半人开始做题到现在的天数。按人不按日历,因为各届 开始做题的时间不一样(25 级第二年才开始)。比哪届改成后端算,接口带 grade / compare。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1248 lines
47 KiB
TypeScript
1248 lines
47 KiB
TypeScript
import { z } from "zod"
|
||
|
||
import { ADMIN_TYPES, PROBLEM_PERMISSIONS } from "./roles"
|
||
|
||
import { achievementRaritySchema } from "./achievement"
|
||
import { rankProfileSchema } from "./account"
|
||
import { paginatedSchema, sampleUserSchema } from "./common"
|
||
import { reactionKeySchema } from "./content"
|
||
import { badgeConditionTypeSchema } from "./problemset"
|
||
import { problemLanguageSchema, runnableLanguageSchema } from "./language"
|
||
import {
|
||
astRuleSchema,
|
||
astRulesSchema,
|
||
problemDifficultySchema,
|
||
sqlConfigSchema,
|
||
sqlDisplaySchema,
|
||
tagCategorySchema,
|
||
} from "./problem"
|
||
|
||
// 放这里而不是 roles.ts:roles.ts 要保持不 import zod,理由见那边的文件头
|
||
export const adminTypeSchema = z.enum(ADMIN_TYPES)
|
||
export const problemPermissionSchema = z.enum(PROBLEM_PERMISSIONS)
|
||
|
||
/**
|
||
* 后台侧的契约。与 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"])
|
||
|
||
/** 后台一课挂的一道「去做几道题」。按 tutorial_problem.order 排 */
|
||
export const adminTutorialProblemSchema = z.object({
|
||
id: z.number().int(),
|
||
displayId: z.string(),
|
||
title: z.string(),
|
||
mustDo: z.boolean(),
|
||
})
|
||
|
||
export const adminTutorialSchema = z.object({
|
||
id: z.number().int(),
|
||
title: z.string(),
|
||
content: z.string(),
|
||
// 「示例代码」字段界面上已经去掉(正文里的代码块能直接运行了),发布脚本还在传,照收照回
|
||
code: z.string().nullable(),
|
||
project: z.string(),
|
||
problems: z.array(adminTutorialProblemSchema),
|
||
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,
|
||
problems: true,
|
||
})
|
||
.extend({
|
||
exerciseCount: z.number().int(),
|
||
problemCount: z.number().int(),
|
||
/** 打开过这一课的学生数(全部时间),老师判断哪课没人看 */
|
||
readers: z.number().int(),
|
||
})
|
||
|
||
/** 后台教程列表按语言分组返回,对齐旧 TutorialAdminAPI.get 的 `{python, c}` 形状 */
|
||
export const adminTutorialGroupsSchema = z.object({
|
||
python: z.array(adminTutorialListItemSchema),
|
||
c: z.array(adminTutorialListItemSchema),
|
||
})
|
||
|
||
/**
|
||
* `project` 不传就从正文第一段的「**所属项目:…**」抽 —— 发布脚本(docs/*-tutorials/
|
||
* publish.js)不认识这个字段,更新时也不能因为它没传就把项目清空。
|
||
*/
|
||
export const createTutorialRequestSchema = z.object({
|
||
title: z.string().trim().min(1).max(128),
|
||
content: z.string(),
|
||
code: z.string().nullable().default(null),
|
||
project: z.string().trim().max(64).optional(),
|
||
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 TUTORIAL_PROBLEM_MAX = 6
|
||
export const setTutorialProblemsRequestSchema = z.object({
|
||
problemIds: z.array(z.number().int().positive()).max(TUTORIAL_PROBLEM_MAX),
|
||
})
|
||
|
||
/** 拖动排序:同一门课的教程 id 按新顺序排好整串交上来 */
|
||
export const reorderTutorialsRequestSchema = z.object({
|
||
ids: z.array(z.number().int().positive()).min(1).max(500),
|
||
})
|
||
|
||
/** 项目改名:这门课里所有挂在 `from` 下的教程一起改 */
|
||
export const renameTutorialProjectRequestSchema = z.object({
|
||
type: tutorialTypeSchema,
|
||
from: z.string().max(64),
|
||
to: z.string().trim().min(1).max(64),
|
||
})
|
||
|
||
/** 后台给一课挑题时的候选:必会题排在前面 */
|
||
export const tutorialProblemCandidateSchema = adminTutorialProblemSchema.extend({
|
||
tags: z.array(z.string()),
|
||
})
|
||
|
||
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(),
|
||
/** 几个人做过(有作答记录)。编辑时据此决定要不要问「清空作答」 */
|
||
attemptUsers: 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,
|
||
})
|
||
.extend({
|
||
/**
|
||
* 同时清空这道题所有人的作答。练习是原地改的,作答记录跟着题 id 走:
|
||
* 改了答案或选项,按旧答案做对的人仍然算做对、「最后错在」说的也是旧选项
|
||
*/
|
||
resetAttempts: z.boolean().default(false),
|
||
})
|
||
|
||
// ---------------------------------------------------------------- AI 报告
|
||
|
||
/**
|
||
* 列表只给摘要,正文要点开详情才拿 —— 与旧 AIAnalysisListSerializer 一致。
|
||
* `data`(原始 prompt 与结构化输入)和 `system_prompt` / `user_prompt` 两侧都不下发。
|
||
*/
|
||
export const adminAiReportListItemSchema = z.object({
|
||
id: z.number().int(),
|
||
username: z.string(),
|
||
/** 列表里名字旁边写是哪个班的 */
|
||
className: z.string().nullable(),
|
||
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(),
|
||
isDisabled: z.boolean(),
|
||
// 在线与否不在库里,是从 Redis 的活动时间戳算出来的(api 的 auth/presence.ts)
|
||
isOnline: z.boolean(),
|
||
// 明文密码。是有意保留的运营需求:老师要能查学生的密码。
|
||
// 只在超管专属的这一个接口下发,别往任何其它地方复制。
|
||
rawPassword: z.string().nullable(),
|
||
className: z.string().nullable(),
|
||
})
|
||
|
||
export const adminUserListSchema = paginatedSchema(adminUserSchema).extend({
|
||
/** 同样筛选条件下从没登录过的人数(列表顶上「48 人 · 1 人从没登录」) */
|
||
neverLoggedIn: z.number().int(),
|
||
})
|
||
|
||
/** 批量建号之前查重:这几个用户名哪些已经有了 */
|
||
export const checkUsernamesRequestSchema = z.object({
|
||
usernames: z.array(z.string().trim().min(1).max(32)).min(1).max(500),
|
||
})
|
||
export const checkUsernamesResponseSchema = z.object({
|
||
existing: z.array(z.object({ username: z.string(), createTime: z.string().nullable() })),
|
||
})
|
||
|
||
export const updateUserRequestSchema = z.object({
|
||
username: z.string().trim().min(1).max(32),
|
||
email: z.email().max(64),
|
||
adminType: adminTypeSchema,
|
||
problemPermission: problemPermissionSchema,
|
||
realName: z.string().max(32).nullable().default(null),
|
||
isDisabled: 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({
|
||
websiteName: z.string().trim().min(1).max(64),
|
||
allowRegister: z.boolean(),
|
||
submissionListShowAll: z.boolean(),
|
||
classList: z.array(z.string()),
|
||
})
|
||
|
||
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(),
|
||
})
|
||
|
||
/** 上传目录里、库里哪儿都没提到的图片:编辑器插的图(upload)和换下来的头像(avatar) */
|
||
export const orphanImageSchema = z.object({
|
||
folder: z.enum(["upload", "avatar"]),
|
||
name: z.string(),
|
||
url: z.string(),
|
||
/** 字节 */
|
||
size: z.number(),
|
||
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(),
|
||
/** 今天判出系统错误的提交数:判题机出毛病时它先涨,比学生喊「交了没反应」早 */
|
||
todaySystemErrorCount: z.number().int(),
|
||
/** 现在在线的人数(5 分钟内有动静,auth/presence.ts) */
|
||
onlineCount: z.number().int(),
|
||
/** 正在上课的班(最近两小时同班在交题,和课堂看板猜班同一个办法);没有为 null */
|
||
activeClass: z.string().nullable(),
|
||
/** 全站的数(看板那一排)。学生 = 普通用户 + 学生管理员,老师 = 教师管理员 + 超管 */
|
||
site: z.object({
|
||
students: z.number().int(),
|
||
teachers: z.number().int(),
|
||
problems: z.number().int(),
|
||
visibleProblems: z.number().int(),
|
||
submissions: z.number().int(),
|
||
termSubmissions: z.number().int(),
|
||
contests: z.number().int(),
|
||
problemSets: z.number().int(),
|
||
tutorials: z.number().int(),
|
||
/** 这周(周一起)交过题的人数 */
|
||
weekActive: z.number().int(),
|
||
}),
|
||
/** 最近 30 天每天交了几次(东八区日历日,含今天,没人交的那天是 0) */
|
||
daily: z.array(z.object({ day: z.string(), count: 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(),
|
||
createdBy: sampleUserSchema,
|
||
status: z.enum(["1", "0", "-1"]),
|
||
contestType: z.enum(["Public", "Password Protected"]),
|
||
})
|
||
|
||
/** 后台比赛列表的一行:比赛本身,加几道题、几人参加(交过题的人数) */
|
||
export const adminContestListItemSchema = adminContestSchema.extend({
|
||
problemCount: z.number().int(),
|
||
participantCount: z.number().int(),
|
||
})
|
||
|
||
export const adminContestListSchema = paginatedSchema(adminContestListItemSchema)
|
||
|
||
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(),
|
||
})
|
||
|
||
export const updateContestRequestSchema = createContestRequestSchema
|
||
|
||
export const updateAcmHelperRequestSchema = z.object({
|
||
rankId: z.number().int().positive(),
|
||
problemId: z.string().min(1),
|
||
checked: z.boolean(),
|
||
})
|
||
|
||
// ---------------------------------------------------------------- 题单管理
|
||
|
||
export const adminProblemSetSchema = z.object({
|
||
id: z.number().int(),
|
||
title: z.string(),
|
||
description: z.string(),
|
||
/** 布置到哪天(东八区当天结束);null = 没布置 */
|
||
assignedUntil: z.string().nullable(),
|
||
/** 这一轮布置开始的时刻 */
|
||
assignedAt: z.string().nullable(),
|
||
/** 现在在不在布置期内(服务端的钟) */
|
||
assigning: z.boolean(),
|
||
/** 公开:学生能在题单列表里看到、能进 */
|
||
visible: z.boolean(),
|
||
createdBy: sampleUserSchema,
|
||
createTime: z.string(),
|
||
lastUpdateTime: z.string(),
|
||
problemsCount: z.number().int(),
|
||
/** 加入这份题单的人数,后台用来判断改动会影响多少人 */
|
||
participantCount: z.number().int(),
|
||
/** 做完(必做题全对)的人数 */
|
||
completedCount: z.number().int(),
|
||
badgeCount: z.number().int(),
|
||
})
|
||
|
||
export const adminProblemSetListSchema = paginatedSchema(adminProblemSetSchema)
|
||
|
||
export const createProblemSetRequestSchema = z.object({
|
||
title: z.string().trim().min(1).max(200),
|
||
description: z.string().default(""),
|
||
/** 布置到哪天;null = 不布置。从「没在布置」变成「布置到将来」时,服务端把布置开始记成现在 */
|
||
assignedUntil: z.string().nullable().default(null),
|
||
visible: z.boolean().default(true),
|
||
})
|
||
|
||
export const updateProblemSetRequestSchema = createProblemSetRequestSchema
|
||
|
||
export const adminProblemSetProblemSchema = z.object({
|
||
id: z.number().int(),
|
||
problemSetId: z.number().int(),
|
||
problemId: z.number().int(),
|
||
problemDisplayId: z.string(),
|
||
title: z.string(),
|
||
difficulty: z.string(),
|
||
order: z.number().int(),
|
||
isRequired: z.boolean(),
|
||
})
|
||
|
||
export const addProblemsToSetRequestSchema = z.object({
|
||
/** 展示用题号(_id),不是自增主键 —— 老师手里只有题号。按给的顺序接在最后 */
|
||
problemIds: z.array(z.string().trim().min(1)).min(1).max(100),
|
||
})
|
||
|
||
export const addProblemsToSetResultSchema = z.object({
|
||
added: z.array(z.string()),
|
||
/** 题号不存在、不可见或是比赛题 */
|
||
missing: z.array(z.string()),
|
||
/** 本来就在题单里 */
|
||
duplicate: z.array(z.string()),
|
||
})
|
||
|
||
export const updateProblemInSetRequestSchema = z.object({
|
||
isRequired: z.boolean(),
|
||
})
|
||
|
||
/** 拖动排序:题单里题目的链接 id,按新顺序给全 */
|
||
export const reorderProblemSetProblemsRequestSchema = z.object({
|
||
ids: z.array(z.number().int()).min(1),
|
||
})
|
||
|
||
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().min(1),
|
||
conditionType: badgeConditionTypeSchema,
|
||
conditionValue: z.number().int().default(0),
|
||
})
|
||
|
||
export const updateProblemSetBadgeRequestSchema = createProblemSetBadgeRequestSchema
|
||
|
||
// ---------------------------------------------------------------- 标签与题目分析
|
||
|
||
export const adminTagSchema = z.object({
|
||
id: z.number().int(),
|
||
name: z.string(),
|
||
category: tagCategorySchema,
|
||
/** 分类有没有人确认过。出题时新建的标签是 false,默认先算知识点,等老师归类 */
|
||
categoryConfirmed: z.boolean(),
|
||
problemCount: z.number().int(),
|
||
})
|
||
|
||
export const setTagCategoryRequestSchema = z.object({
|
||
category: tagCategorySchema,
|
||
})
|
||
|
||
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 batchProblemVisibleRequestSchema = z.object({
|
||
problemIds: z.array(z.number().int().positive()).min(1),
|
||
visible: z.boolean(),
|
||
})
|
||
|
||
export const batchProblemTagResponseSchema = z.object({
|
||
problemCount: z.number().int(),
|
||
tagCount: z.number().int(),
|
||
})
|
||
|
||
/**
|
||
* 卡点分析的一道题(一行)。口径是「人 × 题」:只算**第一次交**落在所选时间段里的那些
|
||
* 人题对,指标按这对的全部历史算(做没做对、交了几次才对)。比率由前端按人数算,
|
||
* 这样知识点的汇总能按所选门槛过滤后再加权。
|
||
*/
|
||
export const stuckProblemSchema = z.object({
|
||
id: z.number().int(),
|
||
displayId: z.string(),
|
||
title: z.string(),
|
||
/** 知识点(category = knowledge 的标签) */
|
||
tags: z.array(z.string()),
|
||
/** 做过的人 */
|
||
users: z.number().int(),
|
||
/** 第一次交就对的人 */
|
||
firstTry: z.number().int(),
|
||
/** 做对了的人 */
|
||
solved: z.number().int(),
|
||
/** 做对的人「做对那次及之前」一共交了几次,加总 —— 除以 solved 就是平均 */
|
||
solvedTries: z.number().int(),
|
||
})
|
||
|
||
export const stuckStudentSchema = z.object({
|
||
username: z.string(),
|
||
className: z.string().nullable(),
|
||
firstTry: z.boolean(),
|
||
solved: z.boolean(),
|
||
/** 做对了:做对那次及之前交了几次;没做对:一共交了几次 */
|
||
tries: z.number().int(),
|
||
/** 第一次交到做对隔了几分钟,没做对是 null */
|
||
minutes: z.number().int().nullable(),
|
||
lastResult: z.number().int(),
|
||
lastSubmissionId: z.string(),
|
||
})
|
||
|
||
export const stuckProblemDetailSchema = z.object({
|
||
problem: z.object({
|
||
id: z.number().int(),
|
||
displayId: z.string(),
|
||
title: z.string(),
|
||
tags: z.array(z.string()),
|
||
}),
|
||
students: z.array(stuckStudentSchema),
|
||
/** 这些人错的那些次,按判题结果分 */
|
||
errors: z.array(z.object({ result: z.number().int(), count: z.number().int() })),
|
||
/** 全站历年(学年按 9 月起算,`year` 是起始年),不受班级和时间段影响 */
|
||
history: z.array(
|
||
z.object({ year: z.number().int(), users: z.number().int(), firstTry: z.number().int() }),
|
||
),
|
||
})
|
||
|
||
const gradeTrendSideSchema = z.object({ users: z.number().int(), firstTry: z.number().int() })
|
||
|
||
/**
|
||
* 年度趋势:选中的一届(grade)和往届或另一届(compare)在两边都做过的题上逐题比「一次就对」。
|
||
* `days` 是同期窗口:两边都只算「第一次交这道题时,离他自己第一次交题不超过这么多天」的人题对;
|
||
* 两边的数据都没超出窗口时为 null(= 全部)。
|
||
*/
|
||
export const gradeTrendSchema = z.object({
|
||
/** 有数据的届,新的在前 */
|
||
grades: z.array(z.string()),
|
||
grade: z.string(),
|
||
compare: z.string(),
|
||
days: z.number().int().nullable(),
|
||
problems: z.array(
|
||
z.object({
|
||
id: z.number().int(),
|
||
displayId: z.string(),
|
||
title: z.string(),
|
||
mine: gradeTrendSideSchema,
|
||
theirs: gradeTrendSideSchema,
|
||
}),
|
||
),
|
||
})
|
||
|
||
// ---------------------------------------------------------------- 自学情况
|
||
|
||
/**
|
||
* 一个学生在一道配套题上的状态。「做对」不论早晚 —— 读这课之前在课上就做对的也算,
|
||
* 口径同学生端课文里的勾:代码 AC(含 AST_CHECK_FAILED)或流程图评到 A / S
|
||
*/
|
||
const learnProblemStateSchema = z.object({
|
||
problemId: z.number().int(),
|
||
solved: z.boolean(),
|
||
/** 代码和流程图一共交了几次,比赛提交不算 */
|
||
submissions: z.number().int(),
|
||
lastSubmitAt: z.string(),
|
||
})
|
||
|
||
/**
|
||
* 「自学情况」的方块格:一个班的学生 × 这门课的每一课。
|
||
*
|
||
* **没打开过任何一课的学生也要有一行**(`opened` 为空)—— 页面上把他们收成一串名单,
|
||
* 这张表首要回答的就是「谁还没开始」。`done` 是学完的课(练一练全对,见 `isLessonDone`),
|
||
* 一定也在 `opened` 里或者是没打开却把练一练做完了的(老链接直达练习的情况)。
|
||
*/
|
||
export const learnBoardStudentSchema = z.object({
|
||
userId: z.number().int(),
|
||
username: z.string(),
|
||
realName: z.string().nullable(),
|
||
className: z.string().nullable(),
|
||
opened: z.array(z.number().int()),
|
||
done: z.array(z.number().int()),
|
||
exerciseSolved: z.number().int(),
|
||
/**
|
||
* 碰过练一练的课,一课一项(一道没做的课不在里面)。方块格靠它分出「做了一部分」,
|
||
* `wrongAttempts` 是这课所有练习错的次数加起来
|
||
*/
|
||
lessons: z.array(
|
||
z.object({
|
||
tutorialId: z.number().int(),
|
||
tried: z.number().int(),
|
||
solved: z.number().int(),
|
||
wrongAttempts: z.number().int(),
|
||
}),
|
||
),
|
||
/** 交过的配套题(一次没交的不在里面),哪一课的题由 `tutorials[].problems` 对 */
|
||
problems: z.array(learnProblemStateSchema),
|
||
totalSeconds: z.number().int(),
|
||
lastViewedAt: z.string().nullable(),
|
||
})
|
||
|
||
export const learnBoardSchema = z.object({
|
||
/** 实际看的是哪个班。老师没选时后端按排名页同一套规则猜,猜不出是 null */
|
||
className: z.string().nullable(),
|
||
tutorials: z.array(
|
||
z.object({
|
||
id: z.number().int(),
|
||
title: z.string(),
|
||
project: z.string(),
|
||
exerciseTotal: z.number().int(),
|
||
/** 这课的配套题,按老师排的顺序;藏起来的题不算 */
|
||
problems: z.array(
|
||
z.object({ id: z.number().int(), displayId: z.string(), title: z.string() }),
|
||
),
|
||
}),
|
||
),
|
||
students: z.array(learnBoardStudentSchema),
|
||
})
|
||
|
||
/**
|
||
* 「按练习」一行。回答的是「哪道练习卡住了全班」——
|
||
* `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 learnExerciseAttemptListSchema = z.object({
|
||
results: z.array(learnExerciseAttemptSchema),
|
||
/** 打开过这一课、这道练习却一次没交的人 */
|
||
untried: z.array(z.object({ userId: z.number().int(), username: z.string() })),
|
||
})
|
||
|
||
/**
|
||
* 一个学生的自学明细(点方块格里的名字):他碰过的每一课,每道练一练做到哪了。
|
||
* 一课没碰过的不下发;练习没做过的 `attempts` 为 0、其余字段为空。
|
||
*/
|
||
export const learnStudentDetailSchema = z.object({
|
||
userId: z.number().int(),
|
||
username: z.string(),
|
||
className: z.string().nullable(),
|
||
lessons: z.array(
|
||
z.object({
|
||
tutorialId: z.number().int(),
|
||
title: z.string(),
|
||
viewCount: z.number().int(),
|
||
totalSeconds: z.number().int(),
|
||
lastViewedAt: z.string().nullable(),
|
||
exercises: z.array(
|
||
z.object({
|
||
exerciseId: z.number().int(),
|
||
type: exerciseTypeSchema,
|
||
question: z.string(),
|
||
attempts: z.number().int(),
|
||
solved: z.boolean(),
|
||
attemptsToSolve: z.number().int().nullable(),
|
||
lastWrongAnswer: z.string().nullable(),
|
||
lastAttemptAt: z.string().nullable(),
|
||
}),
|
||
),
|
||
/** 配套题,没交过的 `submissions` 为 0、`lastSubmitAt` 为空 */
|
||
problems: z.array(
|
||
z.object({
|
||
id: z.number().int(),
|
||
displayId: z.string(),
|
||
title: z.string(),
|
||
solved: z.boolean(),
|
||
submissions: z.number().int(),
|
||
lastSubmitAt: z.string().nullable(),
|
||
}),
|
||
),
|
||
}),
|
||
),
|
||
})
|
||
|
||
export const generateFlowchartRequestSchema = z.object({
|
||
python: z
|
||
.string()
|
||
.min(1)
|
||
.max(64 * 1024),
|
||
})
|
||
/** AI 照 Python 答案画的参考流程图:结构树(FlowDoc),后台画布直接摆上去接着改 */
|
||
export const generateFlowchartResponseSchema = z.object({
|
||
doc: z.record(z.string(), z.unknown()),
|
||
})
|
||
|
||
// ---------------------------------------------------------------- 题目管理
|
||
|
||
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()),
|
||
/** SQL 题:列表里标题后面挂标签;编辑时进 SQL 出题页 */
|
||
isSql: z.boolean(),
|
||
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(),
|
||
/**
|
||
* 做对 / 做过的人数(代码提交,和前台题目列表同一口径)。比赛题列表恒为 0 ——
|
||
* 那个口径只数题库里交的。做过的人数 > 0 就删不掉(有提交),列表里「删除」据此灰掉
|
||
*/
|
||
solvedUsers: z.number().int(),
|
||
triedUsers: z.number().int(),
|
||
})
|
||
|
||
export const adminProblemListSchema = paginatedSchema(adminProblemListItemSchema).extend({
|
||
/**
|
||
* 「全部 / 学生看得到 / 藏着的」三个按钮上的数:除了可见性,筛选条件和列表一样。
|
||
* 只有公开题列表给,比赛题列表没有这组按钮
|
||
*/
|
||
counts: z
|
||
.object({ all: z.number().int(), visible: z.number().int(), hidden: z.number().int() })
|
||
.optional(),
|
||
})
|
||
|
||
/**
|
||
* 题面样例 / 标准答案 / 测试点分值。这三个旧后端都是逐字段校验的
|
||
* (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(),
|
||
/**
|
||
* `problem.last_update_time` 是全库唯一可空的那一列(961 道题里 470 道是 NULL:
|
||
* 从来没被编辑过的老题)。公共的 problemDetailSchema 早就是 nullable,这里漏了,
|
||
* 于是后台打开任何一道没编辑过的题都会 500 在 parse 上。
|
||
*/
|
||
lastUpdateTime: z.string().nullable(),
|
||
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()),
|
||
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(),
|
||
/** 参考流程图的结构树(FlowDoc);2026-10 之前出的题只有 mermaidCode,这里是空对象 */
|
||
flowchartData: z.record(z.string(), z.unknown()),
|
||
flowchartHint: z.string().nullable(),
|
||
astRules: astRulesSchema.nullable(),
|
||
answers: z.array(problemAnswerSchema),
|
||
prompt: z.string().nullable(),
|
||
sqlConfig: sqlConfigSchema.nullable(),
|
||
sqlDisplay: sqlDisplaySchema.nullable(),
|
||
})
|
||
|
||
/**
|
||
* 出题页新建标签时的名字上限,沿用旧 CreateProblemSerializer 的 32。
|
||
* 出题页的输入框拿它做 maxlength —— 不共享一个常量的话,超长标签只会在保存时
|
||
* 撞出 zod 的英文报错("Too big: expected string to have <=32 characters"),
|
||
* 老师看不懂也不知道该改哪里。
|
||
*
|
||
* 列本身是 text 没有长度限制,标签管理页(createTagRequestSchema /
|
||
* renameTagRequestSchema)放到 64 —— 两处口径不同是旧后端就有的,这里只是把
|
||
* 出题页这一侧的数字挪到一处,没有改任何校验的松紧。
|
||
*/
|
||
export const PROBLEM_TAG_MAX_LENGTH = 32
|
||
|
||
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),
|
||
// 收窄到语言联合而不是裸 string[]:`problem.languages` 列上挂着
|
||
// `$type<ProblemLanguage[]>()`,那个断言得有人兑现 —— 闸就设在这里(写入侧)。
|
||
languages: z.array(problemLanguageSchema).min(1),
|
||
template: z.record(z.string(), z.string()),
|
||
visible: z.boolean(),
|
||
difficulty: z.enum(["Low", "Mid", "High"]),
|
||
tags: z.array(z.string().max(PROBLEM_TAG_MAX_LENGTH)).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([]),
|
||
allowFlowchart: z.boolean().default(false),
|
||
showFlowchart: z.boolean().default(false),
|
||
mermaidCode: z.string().nullable().default(null),
|
||
/**
|
||
* 用画布画的参考流程图(FlowDoc)。给了就以它为准,mermaidCode 由后端照它重新生成;
|
||
* 不给(老题只有手写的 Mermaid)就原样存 mermaidCode
|
||
*/
|
||
flowchartData: z.record(z.string(), z.unknown()).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),
|
||
})
|
||
|
||
/** 比赛题排顺序:给出这场比赛**全部**题目的 id,按这个顺序把编号改成 1、2、3… */
|
||
export const reorderContestProblemsRequestSchema = z.object({
|
||
problemIds: z.array(z.number().int().positive()).min(1).max(100),
|
||
})
|
||
|
||
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"]),
|
||
/**
|
||
* 这组是不是题面上给学生看的那组(第 1 组)。增删改题只有这一组必须改到行 ——
|
||
* 其余组可以一行都不改,用来看学生会不会多改(比如 WHERE 写漏了)
|
||
*/
|
||
shown: z.boolean().default(true),
|
||
})
|
||
|
||
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() })
|
||
|
||
/**
|
||
* 一道题最多几组测试数据(编程题的 N.in / N.out、SQL 题的 N.sql 都算)。
|
||
* 提交详情要把每组的结果一行行列出来,几百组的题截图截不下;
|
||
* 2026-10 线上超过 20 组的题已经全部缩到 20 组。
|
||
*/
|
||
export const TEST_CASE_MAX_CASES = 20
|
||
|
||
/**
|
||
* 编程题出题页回显已有的测试点(N.in / N.out 原文)。太大的不展开 —— 页面上要一组一组
|
||
* 摆出来、改了还要整包重跑一遍标准答案,几百组或者几 MB 的数据摆不下也跑不动,
|
||
* 这种只给数量和大小,老师走下载 / 导入 zip。
|
||
*/
|
||
export const TEST_CASE_EDIT_MAX_CASES = 100
|
||
export const TEST_CASE_EDIT_MAX_FILE_BYTES = 32 * 1024
|
||
|
||
export const testCaseFilesSchema = z.discriminatedUnion("editable", [
|
||
z.object({
|
||
editable: z.literal(true),
|
||
cases: z.array(z.object({ input: z.string(), output: z.string() })),
|
||
}),
|
||
z.object({
|
||
editable: z.literal(false),
|
||
count: z.number().int(),
|
||
totalBytes: z.number().int(),
|
||
}),
|
||
])
|
||
|
||
/**
|
||
* 出题页的语法要求:每条规则学生看到的那句话,以及标准答案自己过不过(规则配错时
|
||
* 标准答案也过不了)。`code` 给空串就只要那句话,`passed` 为 null
|
||
*/
|
||
export const astCheckRequestSchema = z.object({
|
||
language: runnableLanguageSchema,
|
||
code: z.string().max(1024 * 1024),
|
||
rules: z.array(astRuleSchema).min(1).max(50),
|
||
})
|
||
|
||
export const astCheckResponseSchema = z.object({
|
||
rules: z.array(
|
||
z.object({
|
||
description: z.string(),
|
||
passed: z.boolean().nullable(),
|
||
actual: z.number().int().optional(),
|
||
}),
|
||
),
|
||
})
|
||
|
||
/** AI 按题面和标准答案想几组测试输入(输出由标准答案在判题机上跑,不让 AI 编) */
|
||
export const generateTestInputsRequestSchema = z.object({
|
||
description: z.string().max(64 * 1024),
|
||
inputDescription: z.string().max(16 * 1024),
|
||
language: runnableLanguageSchema,
|
||
answer: z
|
||
.string()
|
||
.min(1)
|
||
.max(64 * 1024),
|
||
existing: z.array(z.string().max(4096)).max(100),
|
||
})
|
||
|
||
export const generateTestInputsResponseSchema = z.object({ inputs: z.array(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.input<typeof createProblemRequestSchema>
|
||
|
||
export type AdminContest = z.infer<typeof adminContestSchema>
|
||
export type AdminContestList = z.infer<typeof adminContestListSchema>
|
||
export type AdminContestListItem = z.infer<typeof adminContestListItemSchema>
|
||
export type AdminUser = z.infer<typeof adminUserSchema>
|
||
export type AdminUserList = z.infer<typeof adminUserListSchema>
|
||
export type CheckUsernamesResponse = z.infer<typeof checkUsernamesResponseSchema>
|
||
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 OrphanImage = z.infer<typeof orphanImageSchema>
|
||
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 StuckStudent = z.infer<typeof stuckStudentSchema>
|
||
export type StuckProblemDetail = z.infer<typeof stuckProblemDetailSchema>
|
||
export type GradeTrend = z.infer<typeof gradeTrendSchema>
|
||
export type LearnBoard = z.infer<typeof learnBoardSchema>
|
||
export type LearnBoardStudent = z.infer<typeof learnBoardStudentSchema>
|
||
export type LearnExerciseProgress = z.infer<typeof learnExerciseProgressSchema>
|
||
export type LearnExerciseProgressList = z.infer<typeof learnExerciseProgressListSchema>
|
||
export type LearnExerciseAttempt = z.infer<typeof learnExerciseAttemptSchema>
|
||
export type LearnExerciseAttemptList = z.infer<typeof learnExerciseAttemptListSchema>
|
||
export type LearnStudentDetail = z.infer<typeof learnStudentDetailSchema>
|
||
|
||
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 TestCaseFiles = z.infer<typeof testCaseFilesSchema>
|
||
export type AstCheckRequest = z.input<typeof astCheckRequestSchema>
|
||
export type AstCheckResponse = z.infer<typeof astCheckResponseSchema>
|
||
export type GenerateTestInputsRequest = z.input<typeof generateTestInputsRequestSchema>
|
||
export type GenerateTestInputsResponse = z.infer<typeof generateTestInputsResponseSchema>
|
||
|
||
export type AdminAnnouncementList = z.infer<typeof adminAnnouncementListSchema>
|
||
export type CreateAnnouncementRequest = z.input<typeof createAnnouncementRequestSchema>
|
||
export type UpdateAnnouncementRequest = z.input<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 AdminTutorialProblem = z.infer<typeof adminTutorialProblemSchema>
|
||
export type TutorialProblemCandidate = z.infer<typeof tutorialProblemCandidateSchema>
|
||
export type SetTutorialProblemsRequest = z.input<typeof setTutorialProblemsRequestSchema>
|
||
export type ReorderTutorialsRequest = z.input<typeof reorderTutorialsRequestSchema>
|
||
export type RenameTutorialProjectRequest = z.input<typeof renameTutorialProjectRequestSchema>
|
||
export type CreateTutorialRequest = z.input<typeof createTutorialRequestSchema>
|
||
export type UpdateTutorialRequest = z.input<typeof updateTutorialRequestSchema>
|
||
export type SetTutorialVisibilityRequest = z.input<typeof setTutorialVisibilityRequestSchema>
|
||
export type ExerciseType = z.infer<typeof exerciseTypeSchema>
|
||
export type AdminExercise = z.infer<typeof adminExerciseSchema>
|
||
export type CreateExerciseRequest = z.input<typeof createExerciseRequestSchema>
|
||
export type UpdateExerciseRequest = z.input<typeof updateExerciseRequestSchema>
|
||
export type ToggleAiReportPinResponse = z.infer<typeof toggleAiReportPinResponseSchema>
|
||
export type AchievementOperator = z.infer<typeof achievementOperatorSchema>
|
||
export type CreateAchievementRequest = z.input<typeof createAchievementRequestSchema>
|
||
export type UpdateAchievementRequest = z.input<typeof updateAchievementRequestSchema>
|
||
export type UpdateUserRequest = z.input<typeof updateUserRequestSchema>
|
||
export type ImportUsersRequest = z.input<typeof importUsersRequestSchema>
|
||
export type DeleteUsersRequest = z.input<typeof deleteUsersRequestSchema>
|
||
export type ResetPasswordResponse = z.infer<typeof resetPasswordResponseSchema>
|
||
export type UpdateWebsiteConfigRequest = z.input<typeof updateWebsiteConfigRequestSchema>
|
||
export type UpdateJudgeServerRequest = z.input<typeof updateJudgeServerRequestSchema>
|
||
export type UploadImageResponse = z.infer<typeof uploadImageResponseSchema>
|
||
export type CreateContestRequest = z.input<typeof createContestRequestSchema>
|
||
export type UpdateContestRequest = z.input<typeof updateContestRequestSchema>
|
||
export type UpdateAcmHelperRequest = z.input<typeof updateAcmHelperRequestSchema>
|
||
export type AdminProblemSet = z.infer<typeof adminProblemSetSchema>
|
||
export type AdminProblemSetList = z.infer<typeof adminProblemSetListSchema>
|
||
export type CreateProblemSetRequest = z.input<typeof createProblemSetRequestSchema>
|
||
export type UpdateProblemSetRequest = z.input<typeof updateProblemSetRequestSchema>
|
||
export type AdminProblemSetProblem = z.infer<typeof adminProblemSetProblemSchema>
|
||
export type AddProblemsToSetRequest = z.input<typeof addProblemsToSetRequestSchema>
|
||
export type AddProblemsToSetResult = z.infer<typeof addProblemsToSetResultSchema>
|
||
export type UpdateProblemInSetRequest = z.input<typeof updateProblemInSetRequestSchema>
|
||
export type AdminProblemSetBadge = z.infer<typeof adminProblemSetBadgeSchema>
|
||
export type CreateProblemSetBadgeRequest = z.input<typeof createProblemSetBadgeRequestSchema>
|
||
export type UpdateProblemSetBadgeRequest = z.input<typeof updateProblemSetBadgeRequestSchema>
|
||
export type RenameTagRequest = z.input<typeof renameTagRequestSchema>
|
||
export type BatchProblemTagRequest = z.input<typeof batchProblemTagRequestSchema>
|
||
export type BatchProblemVisibleRequest = z.input<typeof batchProblemVisibleRequestSchema>
|
||
export type GenerateFlowchartRequest = z.input<typeof generateFlowchartRequestSchema>
|
||
export type GenerateFlowchartResponse = z.infer<typeof generateFlowchartResponseSchema>
|
||
export type UpdateProblemRequest = z.input<typeof updateProblemRequestSchema>
|
||
export type MakeProblemPublicRequest = z.input<typeof makeProblemPublicRequestSchema>
|
||
export type AddContestProblemRequest = z.input<typeof addContestProblemRequestSchema>
|
||
export type ReorderContestProblemsRequest = z.input<typeof reorderContestProblemsRequestSchema>
|
||
export type ReorderProblemSetProblemsRequest = z.input<
|
||
typeof reorderProblemSetProblemsRequestSchema
|
||
>
|
||
export type SetTagCategoryRequest = z.input<typeof setTagCategoryRequestSchema>
|
||
export type CheckUsernamesRequest = z.input<typeof checkUsernamesRequestSchema>
|
||
export type TestCaseEntry = z.infer<typeof testCaseEntrySchema>
|
||
export type UploadTestCaseResponse = z.infer<typeof uploadTestCaseResponseSchema>
|
||
export type SqlPreviewRequest = z.input<typeof sqlPreviewRequestSchema>
|
||
export type GenerateSqlTestCaseRequest = z.input<typeof generateSqlTestCaseRequestSchema>
|