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 export type ProblemAnswer = z.infer export type ProblemTestCaseScore = z.infer export type AdminAnnouncement = z.infer export type AdminAnnouncementListItem = z.infer // ---------------------------------------------------------------- 教程 / 练习 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(), isDisabled: z.boolean(), // 在线与否不在库里,是从 Redis 的活动时间戳算出来的(api 的 auth/presence.ts) isOnline: 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: 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({ 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(), 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(), }) 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 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()), /** 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(), }) 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(), /** * `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(), 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()`,那个断言得有人兑现 —— 闸就设在这里(写入侧)。 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), 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 export type AchievementMetric = z.infer export type AdminProblem = z.infer export type AdminProblemListItem = z.infer export type AdminProblemList = z.infer export type CreateProblemRequest = z.infer export type AdminContest = z.infer export type AdminContestList = z.infer export type AdminUser = z.infer export type AdminUserList = z.infer export type JudgeServer = z.infer export type DashboardInfo = z.infer export type JudgeServerList = z.infer export type OrphanTestCase = z.infer export type AdminUserRank = z.infer export type AdminAiReportListItem = z.infer export type AdminAiReport = z.infer export type AdminAiReportList = z.infer export type StuckProblem = z.infer export type LearnStudentProgress = z.infer export type LearnStudentProgressList = z.infer export type LearnTutorialProgress = z.infer export type LearnTutorialProgressList = z.infer export type LearnExerciseProgress = z.infer export type LearnExerciseProgressList = z.infer export type LearnExerciseAttempt = z.infer export type AcTrend = z.infer export type AdminTag = z.infer export type RenameTagResponse = z.infer export type BatchProblemTagResponse = z.infer export type SqlTestCaseScript = z.infer export type GenerateSqlTestCaseResponse = z.infer export type TestCaseFiles = z.infer export type AstCheckRequest = z.infer export type AstCheckResponse = z.infer export type GenerateTestInputsRequest = z.infer export type GenerateTestInputsResponse = z.infer export type AdminAnnouncementList = z.infer export type CreateAnnouncementRequest = z.infer export type UpdateAnnouncementRequest = z.infer export type TutorialType = z.infer export type AdminTutorial = z.infer export type AdminTutorialListItem = z.infer export type AdminTutorialGroups = z.infer export type CreateTutorialRequest = z.infer export type UpdateTutorialRequest = z.infer export type SetTutorialVisibilityRequest = z.infer export type ExerciseType = z.infer export type AdminExercise = z.infer export type CreateExerciseRequest = z.infer export type UpdateExerciseRequest = z.infer export type ToggleAiReportPinResponse = z.infer export type AchievementOperator = z.infer export type CreateAchievementRequest = z.infer export type UpdateAchievementRequest = z.infer export type UpdateUserRequest = z.infer export type ImportUsersRequest = z.infer export type DeleteUsersRequest = z.infer export type ResetPasswordResponse = z.infer export type UpdateWebsiteConfigRequest = z.infer export type UpdateJudgeServerRequest = z.infer export type UploadImageResponse = z.infer export type CreateContestRequest = z.infer export type UpdateContestRequest = z.infer export type UpdateAcmHelperRequest = z.infer export type AdminProblemSet = z.infer export type AdminProblemSetList = z.infer export type CreateProblemSetRequest = z.infer export type UpdateProblemSetRequest = z.infer export type AdminProblemSetProblem = z.infer export type AddProblemsToSetRequest = z.infer export type AddProblemsToSetResult = z.infer export type ReorderProblemSetProblemsRequest = z.infer< typeof reorderProblemSetProblemsRequestSchema > export type UpdateProblemInSetRequest = z.infer export type AdminProblemSetBadge = z.infer export type CreateProblemSetBadgeRequest = z.infer export type UpdateProblemSetBadgeRequest = z.infer export type RenameTagRequest = z.infer export type BatchProblemTagRequest = z.infer export type AcTrendYear = z.infer export type GenerateFlowchartRequest = z.infer export type GenerateFlowchartResponse = z.infer export type UpdateProblemRequest = z.infer export type MakeProblemPublicRequest = z.infer export type AddContestProblemRequest = z.infer export type TestCaseEntry = z.infer export type UploadTestCaseResponse = z.infer export type SqlPreviewRequest = z.infer export type GenerateSqlTestCaseRequest = z.infer