style(格式化): 改成单引号、一行最长 160,全仓重新格式化
Deploy / deploy (push) Canceled after 0s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 13:52:24 +08:00
1 parent 530e1a3d5d
commit 195069450f
480 files changed
+15507 -24650

No files matched your search

+5 -5
View File
@@ -1,8 +1,8 @@
import { z } from "zod"
import { z } from 'zod'
import { sampleUserSchema } from "./common"
import { achievementRaritySchema } from "./achievement"
import { userProfileSchema } from "./auth"
import { sampleUserSchema } from './common'
import { achievementRaritySchema } from './achievement'
import { userProfileSchema } from './auth'
/** 个性签名最多几个字。以前是 256,排名上要露出来后改成 30(九成签名本来就不超过 26 字) */
export const MOOD_MAX = 30
@@ -49,7 +49,7 @@ export const weeklyRankItemSchema = z.object({
export const weeklyRankSchema = z.object({
/** 本周一 0:00(东八区)对应的 UTC 时刻,前端拿它显示统计区间 */
start: z.string(),
scope: z.enum(["global", "class"]),
scope: z.enum(['global', 'class']),
/** `scope = "class"` 时是我的班号,全服榜为 null */
className: z.string().nullable(),
/** 本周有新增 AC 的总人数。榜面只回前几名,这个数是全量 */
+4 -4
View File
@@ -1,6 +1,6 @@
import { z } from "zod"
import { z } from 'zod'
export const achievementRaritySchema = z.enum(["bronze", "silver", "gold", "platinum"])
export const achievementRaritySchema = z.enum(['bronze', 'silver', 'gold', 'platinum'])
export const achievementSchema = z.object({
id: z.number().int(),
@@ -10,7 +10,7 @@ export const achievementSchema = z.object({
rarity: achievementRaritySchema,
hidden: z.boolean(),
metric: z.string().nullable(),
operator: z.enum(["gte", "lte"]).nullable(),
operator: z.enum(['gte', 'lte']).nullable(),
threshold: z.number().int().nullable(),
unlocked: z.boolean(),
unlockTime: z.string().nullable(),
@@ -65,7 +65,7 @@ export const pendingAchievementSchema = z.object({
* 题单奖章和成就来自两张表、id 会重叠,前端靠它区分(见 events.ts 的 publishAchievementNotification)。
*/
export const achievementNotificationSchema = pendingAchievementSchema.extend({
kind: z.enum(["achievement", "badge"]),
kind: z.enum(['achievement', 'badge']),
})
export const markAchievementsReadSchema = z.object({
+29 -50
View File
@@ -1,13 +1,13 @@
import { z } from "zod"
import { z } from 'zod'
import { ADMIN_TYPES, PROBLEM_PERMISSIONS } from "./roles"
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 { 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,
@@ -19,7 +19,7 @@ import {
sqlConfigSchema,
sqlDisplaySchema,
tagCategorySchema,
} from "./problem"
} from './problem'
// 放这里而不是 roles.ts:roles.ts 要保持不 import zod,理由见那边的文件头
export const adminTypeSchema = z.enum(ADMIN_TYPES)
@@ -68,7 +68,7 @@ export type AdminAnnouncementListItem = z.infer<typeof adminAnnouncementListItem
// ---------------------------------------------------------------- 教程 / 练习
export const tutorialTypeSchema = z.enum(["python", "c"])
export const tutorialTypeSchema = z.enum(['python', 'c'])
/** 后台一课挂的一道「去做几道题」。按 tutorial_problem.order 排 */
export const adminTutorialProblemSchema = z.object({
@@ -156,15 +156,7 @@ export const tutorialProblemCandidateSchema = adminTutorialProblemSchema.extend(
tags: z.array(z.string()),
})
export const exerciseTypeSchema = z.enum([
"mcq",
"sort",
"fill",
"match",
"predict",
"debug",
"group",
])
export const exerciseTypeSchema = z.enum(['mcq', 'sort', 'fill', 'match', 'predict', 'debug', 'group'])
export const adminExerciseSchema = z.object({
id: z.number().int(),
@@ -227,7 +219,7 @@ export const toggleAiReportPinResponseSchema = z.object({
// ---------------------------------------------------------------- 成就
// rarity 复用 achievement.ts 里已有的定义,不重复声明 —— 两份枚举迟早会长歪
export const achievementOperatorSchema = z.enum(["gte", "lte"])
export const achievementOperatorSchema = z.enum(['gte', 'lte'])
export const achievementMetricSchema = z.object({
key: z.string(),
@@ -309,14 +301,12 @@ export const updateUserRequestSchema = z.object({
realName: z.string().max(32).nullable().default(null),
isDisabled: z.boolean(),
// 空串表示不改密码,与旧 EditUserSerializer 的 allow_blank 一致
password: z.string().max(128).default(""),
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),
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({
@@ -348,7 +338,7 @@ export const judgeServerSchema = z.object({
serviceUrl: z.string().nullable(),
isDisabled: z.boolean(),
/** 心跳在 6 秒内才算在线,与 dashboard 的判活口径一致 */
status: z.enum(["normal", "abnormal"]),
status: z.enum(['normal', 'abnormal']),
})
export const judgeServerListSchema = z.object({
@@ -367,7 +357,7 @@ export const orphanTestCaseSchema = z.object({
/** 上传目录里、库里哪儿都没提到的图片:编辑器插的图(upload)和换下来的头像(avatar) */
export const orphanImageSchema = z.object({
folder: z.enum(["upload", "avatar"]),
folder: z.enum(['upload', 'avatar']),
name: z.string(),
url: z.string(),
/** 字节 */
@@ -434,8 +424,8 @@ export const adminContestSchema = z.object({
password: z.string().nullable(),
visible: z.boolean(),
createdBy: sampleUserSchema,
status: z.enum(["1", "0", "-1"]),
contestType: z.enum(["Public", "Password Protected"]),
status: z.enum(['1', '0', '-1']),
contestType: z.enum(['Public', 'Password Protected']),
})
/** 后台比赛列表的一行:比赛本身,加几道题、几人参加(交过题的人数) */
@@ -494,7 +484,7 @@ export const adminProblemSetListSchema = paginatedSchema(adminProblemSetSchema)
export const createProblemSetRequestSchema = z.object({
title: z.string().trim().min(1).max(200),
description: z.string().default(""),
description: z.string().default(''),
/** 布置到哪天;null = 不布置。从「没在布置」变成「布置到将来」时,服务端把布置开始记成现在 */
assignedUntil: z.string().nullable().default(null),
visible: z.boolean().default(true),
@@ -587,7 +577,7 @@ export const renameTagResponseSchema = z.object({
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"]),
action: z.enum(['add', 'remove']),
})
/** 勾了几道题,一次让学生看得到 / 藏起来。只认公开题、只认自己能改的 */
@@ -646,9 +636,7 @@ export const stuckProblemDetailSchema = z.object({
/** 这些人错的那些次,按判题结果分 */
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() }),
),
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() })
@@ -732,9 +720,7 @@ export const learnBoardSchema = z.object({
project: z.string(),
exerciseTotal: z.number().int(),
/** 这课的配套题,按老师排的顺序;藏起来的题不算 */
problems: z.array(
z.object({ id: z.number().int(), displayId: z.string(), title: z.string() }),
),
problems: z.array(z.object({ id: z.number().int(), displayId: z.string(), title: z.string() })),
}),
),
students: z.array(learnBoardStudentSchema),
@@ -871,9 +857,7 @@ export const adminProblemListSchema = paginatedSchema(adminProblemListItemSchema
* 「全部 / 学生看得到 / 藏着的」三个按钮上的数:除了可见性,筛选条件和列表一样。
* 只有公开题列表给,比赛题列表没有这组按钮
*/
counts: z
.object({ all: z.number().int(), visible: z.number().int(), hidden: z.number().int() })
.optional(),
counts: z.object({ all: z.number().int(), visible: z.number().int(), hidden: z.number().int() }).optional(),
})
/**
@@ -978,10 +962,7 @@ export const challengeInputSchema = z.object({
rules: z.array(astRuleSchema).min(1).max(CHALLENGE_RULES_MAX),
})
export const challengeInputsSchema = z.record(
z.string(),
z.array(challengeInputSchema).max(CHALLENGES_PER_LANGUAGE_MAX),
)
export const challengeInputsSchema = z.record(z.string(), z.array(challengeInputSchema).max(CHALLENGES_PER_LANGUAGE_MAX))
export const createProblemRequestSchema = z.object({
_id: z.string().trim().min(1).max(32),
@@ -1006,7 +987,7 @@ export const createProblemRequestSchema = z.object({
languages: z.array(problemLanguageSchema).min(1),
template: z.record(z.string(), z.string()),
visible: z.boolean(),
difficulty: z.enum(["Low", "Mid", "High"]),
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),
@@ -1082,7 +1063,7 @@ export const sqlPreviewRequestSchema = z.object({
.string()
.min(1)
.max(1024 * 1024),
mode: z.enum(["query", "modify"]),
mode: z.enum(['query', 'modify']),
/**
* 这组是不是题面上给学生看的那组(第 1 组)。增删改题只有这一组必须改到行 ——
* 其余组可以一行都不改,用来看学生会不会多改(比如 WHERE 写漏了)
@@ -1095,7 +1076,7 @@ export const generateSqlTestCaseRequestSchema = z.object({
.string()
.min(1)
.max(64 * 1024),
mode: z.enum(["query", "modify"]),
mode: z.enum(['query', 'modify']),
})
export const generateSqlTestCaseResponseSchema = z.object({ sql: z.string() })
@@ -1115,7 +1096,7 @@ export const TEST_CASE_MAX_CASES = 20
export const TEST_CASE_EDIT_MAX_CASES = 100
export const TEST_CASE_EDIT_MAX_FILE_BYTES = 32 * 1024
export const testCaseFilesSchema = z.discriminatedUnion("editable", [
export const testCaseFilesSchema = z.discriminatedUnion('editable', [
z.object({
editable: z.literal(true),
cases: z.array(z.object({ input: z.string(), output: z.string() })),
@@ -1291,9 +1272,7 @@ 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 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>
+29 -31
View File
@@ -1,18 +1,18 @@
import { z } from "zod"
import { z } from 'zod'
import { classPkPeriodSchema } from "./ranking"
import { classPkPeriodSchema } from './ranking'
/**
* 智能分析看哪段时间。全按东八区日历切(同排名页):这周 = 本周一零点起,这个月 = 本月 1 日
* 零点起,这学期 = `termStart()`(9 月 1 日 / 2 月 1 日),全部 = 不设下限。
*/
export const learningPeriodSchema = z.enum(["week", "month", "term", "all"])
export const learningPeriodSchema = z.enum(['week', 'month', 'term', 'all'])
export const LEARNING_PERIOD_LABELS = {
week: "这周",
month: "这个月",
term: "这学期",
all: "全部",
week: '这周',
month: '这个月',
term: '这学期',
all: '全部',
} as const satisfies Record<z.infer<typeof learningPeriodSchema>, string>
/**
@@ -27,7 +27,7 @@ export const LEARNING_PERIOD_LABELS = {
export const learningItemSchema = z.object({
displayId: z.string(),
title: z.string(),
status: z.enum(["first", "tries", "fail", "none"]),
status: z.enum(['first', 'tries', 'fail', 'none']),
attempts: z.number().int(),
/** 班上第几个做对(只数这段时间里做对的同班同学);没做对、没有班级时为 null */
rank: z.number().int().nullable(),
@@ -80,9 +80,7 @@ export const learningReportSchema = z.object({
/** 没通过的代码提交按判题状态分组,多的在前;系统错误、还没判完的不算 */
errors: z.array(z.object({ result: z.number().int(), count: z.number().int() })),
/** 全部知识点(tag 的 knowledge 类),按题库里题数从多到少;没碰过的 solved = tried = 0 */
knowledge: z.array(
z.object({ name: z.string(), solved: z.number().int(), tried: z.number().int() }),
),
knowledge: z.array(z.object({ name: z.string(), solved: z.number().int(), tried: z.number().int() })),
/** 老师看学生时,同班按用户名排的前一个 / 后一个;学生看自己时为 null */
neighbors: z.object({ prev: z.string().nullable(), next: z.string().nullable() }).nullable(),
})
@@ -123,11 +121,11 @@ export const HINT_MIN_FAILURES = 1
* 老师按班级 / 作业设最高等级还没做,现在所有题都开到 L4。
*/
export const HINT_LEVELS = [
{ level: 0, name: "反问", summary: "只反问,不给结论" },
{ level: 1, name: "定位", summary: "只说问题在哪,不说为什么" },
{ level: 2, name: "概念", summary: "讲清涉及的概念,不给改法" },
{ level: 3, name: "思路", summary: "分步骤讲解题思路,不写代码" },
{ level: 4, name: "示例", summary: "用相似的例子演示,或给出关键的一行" },
{ level: 0, name: '反问', summary: '只反问,不给结论' },
{ level: 1, name: '定位', summary: '只说问题在哪,不说为什么' },
{ level: 2, name: '概念', summary: '讲清涉及的概念,不给改法' },
{ level: 3, name: '思路', summary: '分步骤讲解题思路,不写代码' },
{ level: 4, name: '示例', summary: '用相似的例子演示,或给出关键的一行' },
] as const
export const HINT_MAX_LEVEL = 4
@@ -145,7 +143,7 @@ export const HINT_LEVEL_COMPILE = -1
/** 给界面看的等级名。编译失败那一档不叫 L-1 */
export function hintLevelLabel(level: number) {
if (level === HINT_LEVEL_COMPILE) return "编译错误"
if (level === HINT_LEVEL_COMPILE) return '编译错误'
const item = HINT_LEVELS.find((entry) => entry.level === level)
// 给学生看的,从 1 数起、不写「L0」:学生不知道 L 是什么,也不会从 0 数
return item ? `第 ${item.level + 1} 档 · ${item.name}` : `第 ${level + 1} 档`
@@ -180,19 +178,19 @@ export const aiHintDoneSchema = z.object({
* 全角冒号、多一个空格、小数位数,是这批学生最常见、也最冤的一类 WA。
*/
export const HINT_ERROR_TAGS = {
syntax: "语法错误",
input_format: "输入读取方式不对(格式、分隔、个数)",
output_format: "输出格式不对(多余的输入提示语、全角/半角符号、多余空格或换行、小数位数)",
condition: "条件判断写错(比较符、漏了分支)",
loop_bound: "循环次数或边界不对(差一)",
integer_division: "整数除法或取余用错",
type_overflow: "数据类型不对或溢出(int 不够、浮点精度)",
uninitialized: "变量没初始化,或累加器没清零",
missing_case: "漏了特殊情况(0、负数、边界值)",
runtime_error: "运行时错误(下标越界、除以零)",
timeout: "超时(算法太慢或死循环)",
wrong_approach: "思路整体不对",
other: "其他,或者看不出来",
syntax: '语法错误',
input_format: '输入读取方式不对(格式、分隔、个数)',
output_format: '输出格式不对(多余的输入提示语、全角/半角符号、多余空格或换行、小数位数)',
condition: '条件判断写错(比较符、漏了分支)',
loop_bound: '循环次数或边界不对(差一)',
integer_division: '整数除法或取余用错',
type_overflow: '数据类型不对或溢出(int 不够、浮点精度)',
uninitialized: '变量没初始化,或累加器没清零',
missing_case: '漏了特殊情况(0、负数、边界值)',
runtime_error: '运行时错误(下标越界、除以零)',
timeout: '超时(算法太慢或死循环)',
wrong_approach: '思路整体不对',
other: '其他,或者看不出来',
} as const
export type HintErrorTag = keyof typeof HINT_ERROR_TAGS
@@ -206,7 +204,7 @@ export const hintDiagnosisSchema = z.object({
tag: z.enum(Object.keys(HINT_ERROR_TAGS) as [HintErrorTag, ...HintErrorTag[]]),
/** 问题所在的行号区间(从 1 起,含两端);说不准就是 null */
lines: z.tuple([z.number().int().min(1), z.number().int().min(1)]).nullable(),
confidence: z.enum(["high", "low"]),
confidence: z.enum(['high', 'low']),
})
export type HintDiagnosis = z.infer<typeof hintDiagnosisSchema>
+2 -2
View File
@@ -1,4 +1,4 @@
import { z } from "zod"
import { z } from 'zod'
export const loginRequestSchema = z.object({
username: z.string().trim().min(1).max(150),
@@ -15,7 +15,7 @@ export const refreshTokenRequestSchema = z.object({
* 前端传 `[WS_AUTH_PROTOCOL, token]` 两个子协议,服务端回第一个 —— 必须回一个前端
* 传过的,否则 Chrome 判握手失败。不放 URL 查询参数:URL 会原样进反代的访问日志。
*/
export const WS_AUTH_PROTOCOL = "oj2.bearer"
export const WS_AUTH_PROTOCOL = 'oj2.bearer'
/**
* 登录、换 token 的出参。
+11 -11
View File
@@ -1,8 +1,8 @@
import { z } from "zod"
import { z } from 'zod'
import { judgeStatusSchema } from "./judge-status"
import { lessonLanguageSchema as lessonLanguageValueSchema } from "./language"
import { statisticInfoSchema } from "./submission"
import { judgeStatusSchema } from './judge-status'
import { lessonLanguageSchema as lessonLanguageValueSchema } from './language'
import { statisticInfoSchema } from './submission'
/**
* 「班里在做」:同班同一天有好几个人做的题,基本就是老师在课上点名的那几道。
@@ -19,7 +19,7 @@ export const classActivityProblemSchema = z.object({
* 我自己在题库里(不含比赛)做这道题的状态,不限那一天。老师布置时选了语言的,
* 代码只认用那个语言交的(流程图照旧算)
*/
myStatus: z.enum(["accepted", "tried", "none"]),
myStatus: z.enum(['accepted', 'tried', 'none']),
})
/**
@@ -33,7 +33,7 @@ export const classActivitySchema = z.object({
className: z.string().nullable(),
day: z.string().nullable(),
/** teacher = 老师在课堂看板里布置的;inferred = 从同班提交记录推断的 */
source: z.enum(["teacher", "inferred"]).nullable(),
source: z.enum(['teacher', 'inferred']).nullable(),
language: lessonLanguageSchema,
problems: z.array(classActivityProblemSchema),
})
@@ -97,7 +97,7 @@ export const classBoardCellSchema = z.object({
* accepted 看的是题库里(不含比赛)有没有通过过,不限今天 —— 以前做过的也算做完。
* 布置时选了语言的,代码的次数和通过都只数那个语言(流程图照旧)
*/
status: z.enum(["accepted", "tried", "none"]),
status: z.enum(['accepted', 'tried', 'none']),
/** 今天在这道题上交了几次(代码 + 流程图) */
attempts: z.number().int(),
/**
@@ -134,7 +134,7 @@ export const classBoardCellSchema = z.object({
* 挑战:done = 做到了这道题的一条挑战(不限今天);doing = 做对之后今天又交了、还没做到 ——
* 做完了的学生是在挑战还是闲着,老师一眼看得出。没配挑战的题、没做对的格子为 null
*/
challenge: z.enum(["done", "doing"]).nullable(),
challenge: z.enum(['done', 'doing']).nullable(),
})
export const classBoardStudentSchema = z.object({
@@ -174,7 +174,7 @@ export const classBoardStudentSchema = z.object({
export const classBoardSchema = z.object({
className: z.string().nullable(),
day: z.string(),
source: z.enum(["teacher", "inferred"]).nullable(),
source: z.enum(['teacher', 'inferred']).nullable(),
/** 今天布置的语言(见 lessonLanguageSchema) */
language: lessonLanguageSchema,
/** 今天第一次布置的时刻,之后改题不变;推断出来的课为 null */
@@ -199,7 +199,7 @@ export const classBoardSchema = z.object({
*/
export const classLessonSchema = z.object({
className: z.string().nullable(),
source: z.enum(["teacher", "inferred"]).nullable(),
source: z.enum(['teacher', 'inferred']).nullable(),
/**
* 选了语言时,列表那边连语言一起筛。C / Python 的作业里夹了 SQL 题就是 null:
* 列表只能筛一种语言,按 C 筛会把 SQL 题交的全藏起来,宁可多列一些
@@ -216,7 +216,7 @@ export const classTodaySchema = z.object({
classes: z.array(
z.object({
className: z.string(),
source: z.enum(["teacher", "inferred"]).nullable(),
source: z.enum(['teacher', 'inferred']).nullable(),
/** 老师布置的时刻;推断出来的为 null */
assignedAt: z.string().nullable(),
/** 布置的老师;推断出来的为 null */
+1 -1
View File
@@ -1,4 +1,4 @@
import { z } from "zod"
import { z } from 'zod'
export const sampleUserSchema = z.object({
id: z.number().int(),
+6 -18
View File
@@ -1,6 +1,6 @@
import { z } from "zod"
import { z } from 'zod'
import { paginatedSchema, sampleUserSchema } from "./common"
import { paginatedSchema, sampleUserSchema } from './common'
export const announcementSchema = z.object({
id: z.number().int(),
@@ -25,15 +25,7 @@ export const announcementListItemSchema = announcementSchema.omit({
export const announcementListSchema = paginatedSchema(announcementListItemSchema)
export const reactionKeySchema = z.enum([
"too_easy",
"too_hard",
"confusing",
"buggy",
"learned",
"interesting",
"want_explain",
])
export const reactionKeySchema = z.enum(['too_easy', 'too_hard', 'confusing', 'buggy', 'learned', 'interesting', 'want_explain'])
export const reactionCountsSchema = z.record(reactionKeySchema, z.number().int())
export const reactionStateSchema = z.object({
@@ -70,7 +62,7 @@ export const tutorialSchema = tutorialSummarySchema.extend({
problems: z.array(tutorialProblemSchema),
isPublic: z.boolean(),
order: z.number().int(),
type: z.enum(["python", "c"]),
type: z.enum(['python', 'c']),
createdBy: sampleUserSchema,
createdAt: z.string(),
updatedAt: z.string(),
@@ -167,7 +159,7 @@ export const exerciseDataByType: Record<string, z.ZodType> = {
*/
export const exerciseSchema = z.object({
id: z.number().int(),
type: z.enum(["mcq", "sort", "fill", "match", "predict", "debug", "group"]),
type: z.enum(['mcq', 'sort', 'fill', 'match', 'predict', 'debug', 'group']),
data: z.record(z.string(), z.unknown()),
order: z.number().int(),
/** 当前学生做对过没有。未登录恒为 false;一课的练一练全做对就算学完这课 */
@@ -199,11 +191,7 @@ export type ExerciseAttemptRequest = z.input<typeof exerciseAttemptRequestSchema
*
* 前端目录、首页、后台「自学情况」都走这一个函数,别各写一份。
*/
export function isLessonDone(progress: {
viewCount: number
exerciseTotal: number
exerciseSolved: number
}) {
export function isLessonDone(progress: { viewCount: number; exerciseTotal: number; exerciseSolved: number }) {
if (progress.exerciseTotal > 0) return progress.exerciseSolved >= progress.exerciseTotal
return progress.viewCount > 0
}
+6 -6
View File
@@ -1,9 +1,9 @@
import { z } from "zod"
import { z } from 'zod'
import { paginatedSchema, sampleUserSchema } from "./common"
import { problemDetailSchema, problemListItemSchema } from "./problem"
import { paginatedSchema, sampleUserSchema } from './common'
import { problemDetailSchema, problemListItemSchema } from './problem'
export const contestStatusSchema = z.enum(["-1", "0", "1"])
export const contestStatusSchema = z.enum(['-1', '0', '1'])
export const contestSchema = z.object({
id: z.number().int(),
@@ -16,7 +16,7 @@ export const contestSchema = z.object({
lastUpdateTime: z.string(),
createdBy: sampleUserSchema,
status: contestStatusSchema,
contestType: z.enum(["Public", "Password Protected"]),
contestType: z.enum(['Public', 'Password Protected']),
now: z.string().optional(),
/** 列表才带:几道题(可见的)、几个学生交过题 */
problemCount: z.number().int().optional(),
@@ -68,7 +68,7 @@ export const contestSubmissionInfoSchema = z.object({
* 期中、期末:进行中学生看不到排名、每题做对人数、谁最先做对,考完一起公布(2026-10 用户定的)。
* 练习照常全看得到 —— 「我需要竞争」。标签只有 练习 / 期中 / 期末 三个值
*/
export const EXAM_TAGS = ["期中", "期末"] as const
export const EXAM_TAGS = ['期中', '期末'] as const
export function isExamTag(tag: string) {
return (EXAM_TAGS as readonly string[]).includes(tag)
}
+34 -47
View File
@@ -11,16 +11,16 @@
* 那些只能看 Mermaid 渲染出来的图,载不回画布,评分也只能退回老办法。
*/
export type BoxKind = "input" | "output" | "process"
export type StepKind = BoxKind | "if" | "while"
export type BoxKind = 'input' | 'output' | 'process'
export type StepKind = BoxKind | 'if' | 'while'
interface StepBase {
id: string
text: string
}
export type BoxStep = StepBase & { kind: BoxKind }
export type IfStep = StepBase & { kind: "if"; yes: FlowStep[]; no: FlowStep[] }
export type WhileStep = StepBase & { kind: "while"; body: FlowStep[] }
export type IfStep = StepBase & { kind: 'if'; yes: FlowStep[]; no: FlowStep[] }
export type WhileStep = StepBase & { kind: 'while'; body: FlowStep[] }
export type FlowStep = BoxStep | IfStep | WhileStep
export interface FlowDoc {
@@ -29,53 +29,46 @@ export interface FlowDoc {
}
/** 开始、结束在画布上的 id(它们不进数据) */
export const FLOW_START_ID = "__start"
export const FLOW_END_ID = "__end"
export const FLOW_START_ID = '__start'
export const FLOW_END_ID = '__end'
export function flowChildren(step: FlowStep): FlowStep[][] {
if (step.kind === "if") return [step.yes, step.no]
if (step.kind === "while") return [step.body]
if (step.kind === 'if') return [step.yes, step.no]
if (step.kind === 'while') return [step.body]
return []
}
/** 还没写字的框有几个 */
export function countBlankSteps(steps: FlowStep[]): number {
return steps.reduce(
(sum, step) =>
sum +
(step.text.trim() ? 0 : 1) +
flowChildren(step).reduce((n, c) => n + countBlankSteps(c), 0),
0,
)
return steps.reduce((sum, step) => sum + (step.text.trim() ? 0 : 1) + flowChildren(step).reduce((n, c) => n + countBlankSteps(c), 0), 0)
}
/** 一张图「长什么样」,不带 id:载回旧版、和交过的那一版比,只看结构和字 */
export function flowSignature(doc: FlowDoc): string {
const strip = (steps: FlowStep[]): unknown[] =>
steps.map((step) => [step.kind, step.text.trim(), ...flowChildren(step).map(strip)])
const strip = (steps: FlowStep[]): unknown[] => steps.map((step) => [step.kind, step.text.trim(), ...flowChildren(step).map(strip)])
return JSON.stringify(strip(doc.steps))
}
// ==================== 读回来的数据 ====================
const KINDS = new Set<string>(["input", "output", "process", "if", "while"])
const KINDS = new Set<string>(['input', 'output', 'process', 'if', 'while'])
function parseSteps(raw: unknown, prefix: string): FlowStep[] | null {
if (!Array.isArray(raw)) return null
const out: FlowStep[] = []
for (const [index, item] of raw.entries()) {
if (!item || typeof item !== "object") return null
if (!item || typeof item !== 'object') return null
const step = item as Record<string, unknown>
const kind = step.kind
if (typeof kind !== "string" || !KINDS.has(kind)) return null
const id = typeof step.id === "string" && step.id ? step.id : `${prefix}${index}`
const text = typeof step.text === "string" ? step.text : ""
if (kind === "if") {
if (typeof kind !== 'string' || !KINDS.has(kind)) return null
const id = typeof step.id === 'string' && step.id ? step.id : `${prefix}${index}`
const text = typeof step.text === 'string' ? step.text : ''
if (kind === 'if') {
const yes = parseSteps(step.yes ?? [], `${id}y`)
const no = parseSteps(step.no ?? [], `${id}n`)
if (!yes || !no) return null
out.push({ id, text, kind, yes, no })
} else if (kind === "while") {
} else if (kind === 'while') {
const body = parseSteps(step.body ?? [], `${id}b`)
if (!body) return null
out.push({ id, text, kind, body })
@@ -88,10 +81,10 @@ function parseSteps(raw: unknown, prefix: string): FlowStep[] | null {
/** 本地草稿、交过的某一版:是新格式就读出来,旧的 vue-flow 格式或者坏数据给 null */
export function parseFlowDoc(raw: unknown): FlowDoc | null {
if (!raw || typeof raw !== "object") return null
if (!raw || typeof raw !== 'object') return null
const data = raw as Record<string, unknown>
if (data.version !== 2) return null
const steps = parseSteps(data.steps, "p")
const steps = parseSteps(data.steps, 'p')
return steps ? { version: 2, steps } : null
}
@@ -119,7 +112,7 @@ export function mermaidOrder(doc: FlowDoc): string[] {
* 学生不知道扣分是因为一个引号。`#` 必须先转,否则本来就有的 `#quot;` 会被当成实体。
*/
function escapeLabel(raw: string) {
return raw.replace(/#/g, "#35;").replace(/"/g, "#quot;").replace(/\r?\n/g, "<br/>")
return raw.replace(/#/g, '#35;').replace(/"/g, '#quot;').replace(/\r?\n/g, '<br/>')
}
/**
@@ -129,12 +122,12 @@ function escapeLabel(raw: string) {
* class 行留着给前端 useMermaid 上色;classDef 不写 —— 颜色是渲染那边的事。
*/
export function flowToMermaid(doc: FlowDoc): string {
const lines: string[] = ["flowchart TD"]
const lines: string[] = ['flowchart TD']
const classes: string[] = []
let n = 0
const node = (shape: (label: string) => string, text: string, cls: string) => {
const id = `n${n++}`
lines.push(` ${id}${shape(escapeLabel(text.trim() || " "))}`)
lines.push(` ${id}${shape(escapeLabel(text.trim() || ' '))}`)
classes.push(` class ${id} ${cls}`)
return id
}
@@ -142,29 +135,23 @@ export function flowToMermaid(doc: FlowDoc): string {
type Exit = { from: string; label?: string }
const link = (exits: Exit[], to: string) => {
for (const exit of exits) {
lines.push(
exit.label ? ` ${exit.from} -->|"${exit.label}"| ${to}` : ` ${exit.from} --> ${to}`,
)
lines.push(exit.label ? ` ${exit.from} -->|"${exit.label}"| ${to}` : ` ${exit.from} --> ${to}`)
}
}
const walk = (steps: FlowStep[], entries: Exit[]): Exit[] => {
let exits = entries
for (const step of steps) {
if (step.kind === "if") {
const id = node((l) => `{"${l}"}`, step.text, "decision")
if (step.kind === 'if') {
const id = node((l) => `{"${l}"}`, step.text, 'decision')
link(exits, id)
exits = [
...walk(step.yes, [{ from: id, label: "是" }]),
...walk(step.no, [{ from: id, label: "否" }]),
]
} else if (step.kind === "while") {
const id = node((l) => `{"${l}"}`, step.text, "decision")
exits = [...walk(step.yes, [{ from: id, label: '是' }]), ...walk(step.no, [{ from: id, label: '否' }])]
} else if (step.kind === 'while') {
const id = node((l) => `{"${l}"}`, step.text, 'decision')
link(exits, id)
link(walk(step.body, [{ from: id, label: "是" }]), id)
exits = [{ from: id, label: "否" }]
link(walk(step.body, [{ from: id, label: '是' }]), id)
exits = [{ from: id, label: '否' }]
} else {
const shape =
step.kind === "process" ? (l: string) => `["${l}"]` : (l: string) => `[/"${l}"/]`
const shape = step.kind === 'process' ? (l: string) => `["${l}"]` : (l: string) => `[/"${l}"/]`
const id = node(shape, step.text, step.kind)
link(exits, id)
exits = [{ from: id }]
@@ -172,9 +159,9 @@ export function flowToMermaid(doc: FlowDoc): string {
}
return exits
}
const start = node((l) => `(["${l}"])`, "开始", "startNode")
const start = node((l) => `(["${l}"])`, '开始', 'startNode')
const exits = walk(doc.steps, [{ from: start }])
const end = node((l) => `(["${l}"])`, "结束", "endNode")
const end = node((l) => `(["${l}"])`, '结束', 'endNode')
link(exits, end)
return [...lines, "", ...classes].join("\n")
return [...lines, '', ...classes].join('\n')
}
+9 -24
View File
@@ -1,13 +1,8 @@
import { z } from "zod"
import { z } from 'zod'
import { paginatedSchema } from "./common"
import { paginatedSchema } from './common'
export const flowchartStatusSchema = z.union([
z.literal(0),
z.literal(1),
z.literal(2),
z.literal(3),
])
export const flowchartStatusSchema = z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)])
export const createFlowchartRequestSchema = z.object({
problemId: z.number().int().positive(),
@@ -16,10 +11,7 @@ export const createFlowchartRequestSchema = z.object({
.trim()
.min(1)
.max(50_000)
.refine(
(value) => value.split("\n").filter((line) => line.trim()).length <= 200,
"Flowchart is too complex",
),
.refine((value) => value.split('\n').filter((line) => line.trim()).length <= 200, 'Flowchart is too complex'),
flowchartData: z.record(z.string(), z.unknown()).default({}),
})
@@ -44,10 +36,7 @@ export const flowchartSubmissionSchema = z.object({
* 同一个人同一道题的上一次(评完了的)走对了几组,给「比上次多对了 n 组」用。
* 上一次不是走例子的评法(2026-10 之前的四项分)、或者没有上一次,给 null
*/
previousCases: z
.object({ passed: z.number().int(), total: z.number().int() })
.nullable()
.optional(),
previousCases: z.object({ passed: z.number().int(), total: z.number().int() }).nullable().optional(),
})
export const flowchartListItemSchema = z.object({
@@ -72,16 +61,16 @@ export const flowchartListSchema = paginatedSchema(flowchartListItemSchema).exte
})
export const createFlowchartResponseSchema = z.object({
submissionId: z.string(),
status: z.literal("pending"),
status: z.literal('pending'),
})
/**
* 流程图画到这两档算这道题做完:题单进度、课堂条、课堂看板、「你画的流程图」、结果页签的 ✓
* 都按它(设计文档 2026-09-28-problem-page-redesign 第 3 节决定 4、6)。前后端只有这一份
*/
export const FLOWCHART_PASS_GRADES = ["S", "A"] as const
export const FLOWCHART_PASS_GRADES = ['S', 'A'] as const
export function isFlowchartPass(grade: string | null | undefined) {
return (FLOWCHART_PASS_GRADES as readonly string[]).includes(grade ?? "")
return (FLOWCHART_PASS_GRADES as readonly string[]).includes(grade ?? '')
}
/** 自己在一道题上评完的历次分数,早的在前;hidden = 被题单闸门藏起来的次数 */
@@ -168,11 +157,7 @@ export interface FlowchartCaseReport {
}
export const flowchartUpdateSchema = z.object({
type: z.enum([
"flowchart_evaluation_completed",
"flowchart_evaluation_failed",
"flowchart_evaluation_update",
]),
type: z.enum(['flowchart_evaluation_completed', 'flowchart_evaluation_failed', 'flowchart_evaluation_update']),
submissionId: z.string(),
score: z.number().optional(),
grade: z.string().optional(),
+20 -20
View File
@@ -1,20 +1,20 @@
export * from "./admin"
export * from "./auth"
export * from "./account"
export * from "./achievement"
export * from "./ai"
export * from "./classroom"
export * from "./common"
export * from "./content"
export * from "./contest"
export * from "./flowchart"
export * from "./flowchart-doc"
export * from "./judge-status"
export * from "./language"
export * from "./problem"
export * from "./problemset"
export * from "./ranking"
export * from "./roles"
export * from "./site"
export * from "./submission"
export * from "./time"
export * from './admin'
export * from './auth'
export * from './account'
export * from './achievement'
export * from './ai'
export * from './classroom'
export * from './common'
export * from './content'
export * from './contest'
export * from './flowchart'
export * from './flowchart-doc'
export * from './judge-status'
export * from './language'
export * from './problem'
export * from './problemset'
export * from './ranking'
export * from './roles'
export * from './site'
export * from './submission'
export * from './time'
+2 -4
View File
@@ -1,4 +1,4 @@
import { z } from "zod"
import { z } from 'zod'
/**
* 判题状态码 —— **前后端唯一的一份**。
@@ -27,6 +27,4 @@ export type JudgeStatusValue = (typeof JudgeStatus)[keyof typeof JudgeStatus]
// 同名的类型:原来契约里就有 `type JudgeStatus`(各处按类型引用),值与类型同名合并
export type JudgeStatus = JudgeStatusValue
export const judgeStatusSchema = z.literal(
Object.values(JudgeStatus) as [JudgeStatusValue, ...JudgeStatusValue[]],
)
export const judgeStatusSchema = z.literal(Object.values(JudgeStatus) as [JudgeStatusValue, ...JudgeStatusValue[]])
+9 -9
View File
@@ -1,4 +1,4 @@
import { z } from "zod"
import { z } from 'zod'
/**
* 判题沙箱认得的语言。**这是全仓唯一的语言集合定义处。**
@@ -22,7 +22,7 @@ import { z } from "zod"
* 从别处冒出来(旧客户端的 localStorage、迁移之前排进队列的任务),走下面的
* `normalizeLanguage()`,别直接 parse。
*/
export const judgeLanguageSchema = z.enum(["C", "C++", "Python", "Java", "JavaScript", "Golang"])
export const judgeLanguageSchema = z.enum(['C', 'C++', 'Python', 'Java', 'JavaScript', 'Golang'])
/**
* 题目可以挂的语言 = 沙箱语言 + 两种非沙箱题型。
@@ -35,26 +35,26 @@ export const judgeLanguageSchema = z.enum(["C", "C++", "Python", "Java", "JavaSc
* 于是那 91 条提交的 `language` 在类型上是 `undefined` —— 这就是两份真相
* 各自演进的代价,现在并成一份。
*/
export const problemLanguageSchema = z.enum([...judgeLanguageSchema.options, "SQL", "Flowchart"])
export const problemLanguageSchema = z.enum([...judgeLanguageSchema.options, 'SQL', 'Flowchart'])
/**
* 判题沙箱**现在**跑得了的语言,就是 `apps/api/src/judge/languages.ts` 里那三块。
* 试运行(`POST /trial-runs`)只收这几个:历史语言交上来只会在判题机那边报系统错误。
*/
export const runnableLanguageSchema = judgeLanguageSchema.extract(["C", "C++", "Python"])
export const runnableLanguageSchema = judgeLanguageSchema.extract(['C', 'C++', 'Python'])
/**
* 老师布置作业能选的语言:判题机跑得了的三种,加 SQL(数据库课一整节都是 SQL 题)。
* SQL 题只收 SQL,所以 C / Python 的作业里夹 SQL 题也行 —— 那几道不受作业语言限制
*/
export const lessonLanguageSchema = problemLanguageSchema.extract(["Python", "C", "C++", "SQL"])
export const lessonLanguageSchema = problemLanguageSchema.extract(['Python', 'C', 'C++', 'SQL'])
/**
* SQL 题:可选语言里有 SQL(后端保存时强校验 SQL 只能单独出现)。题型只认这一条,
* 前后端一律走它,别再各自看 sqlConfig 或 languages
*/
export function isSqlProblem(problem: { languages: readonly string[] }) {
return problem.languages.includes("SQL")
return problem.languages.includes('SQL')
}
export type JudgeLanguage = z.infer<typeof judgeLanguageSchema>
@@ -72,13 +72,13 @@ export type ProblemLanguage = z.infer<typeof problemLanguageSchema>
* 别只滚代码。
*/
const LANGUAGE_ALIASES: Record<string, ProblemLanguage> = {
Python2: "Python",
Python3: "Python",
Python2: 'Python',
Python3: 'Python',
}
/** 把可能是旧值的语言名归一化;认不出来返回 null,由调用方决定怎么兜底。 */
export function normalizeLanguage(value: unknown): ProblemLanguage | null {
if (typeof value !== "string") return null
if (typeof value !== 'string') return null
const parsed = problemLanguageSchema.safeParse(LANGUAGE_ALIASES[value] ?? value)
return parsed.success ? parsed.data : null
}
+117 -140
View File
@@ -1,16 +1,16 @@
import { z } from "zod"
import { z } from 'zod'
import { achievementRaritySchema } from "./achievement"
import { paginatedSchema, sampleUserSchema } from "./common"
import { judgeStatusSchema } from "./judge-status"
import { problemLanguageSchema } from "./language"
import { achievementRaritySchema } from './achievement'
import { paginatedSchema, sampleUserSchema } from './common'
import { judgeStatusSchema } from './judge-status'
import { problemLanguageSchema } from './language'
/**
* 题目难度。生产库 956 道题只有这三个值(旧 Django 的 Problem.difficulty choices
* 也是这三个),前端的 DIFFICULTY 映射表按它建 —— 写成 z.string() 的话
* 多出来的值会静默渲染成 undefined。
*/
export const problemDifficultySchema = z.enum(["Low", "Mid", "High"])
export const problemDifficultySchema = z.enum(['Low', 'Mid', 'High'])
/**
* 题目详情/列表里的难度。**可为 null** —— 比赛进行中、看的人又不是管理员时,
@@ -31,7 +31,7 @@ export const maskedProblemDifficultySchema = problemDifficultySchema.nullable()
* SQLDisplay 接口才能渲染表格,抄错了没人拦得住。
*/
export const sqlConfigSchema = z.object({
mode: z.enum(["query", "modify"]),
mode: z.enum(['query', 'modify']),
order_sensitive: z.boolean(),
})
@@ -61,10 +61,7 @@ export const sqlDisplaySchema = z.object({
tables: z.array(sqlDisplayTableSchema),
// query 题给结果集,modify 题给改动后的表 —— 两种形态,前端按有没有
// changed_tables 分支
expected: z.union([
sqlResultSetSchema,
z.object({ changed_tables: z.array(sqlDisplayTableSchema) }),
]),
expected: z.union([sqlResultSetSchema, z.object({ changed_tables: z.array(sqlDisplayTableSchema) })]),
})
/**
@@ -78,17 +75,17 @@ export const sqlDisplaySchema = z.object({
* (见 ast.ts 的 evaluateRule),缺了就走默认文案。
*/
export const astRuleEngineSchema = z.enum([
"must_exist_node",
"must_not_exist_node",
"count_node",
"must_call_function",
"must_not_call_function",
"count_function_call",
"must_call_method",
"must_not_call_method",
"must_use_operator",
'must_exist_node',
'must_not_exist_node',
'count_node',
'must_call_function',
'must_not_call_function',
'count_function_call',
'must_call_method',
'must_not_call_method',
'must_use_operator',
// 判题机实现了,但后台编辑器还没有对应的选项,目前只能手工造数据用上
"must_have_nesting",
'must_have_nesting',
])
export const astRuleSchema = z.object({
@@ -134,20 +131,20 @@ export interface AstNodeTarget {
}
const C_NODE_TARGETS = {
for_loop: { label: "for 循环", node: "for_statement" },
while_loop: { label: "while 循环", node: "while_statement" },
do_while: { label: "do-while 循环", node: "do_statement" },
if_statement: { label: "if 条件", node: "if_statement" },
else_clause: { label: "else 子句", node: "else_clause" },
switch_statement: { label: "switch 语句", node: "switch_statement" },
case_statement: { label: "case 分支", node: "case_statement" },
break: { label: "break 语句", node: "break_statement" },
continue: { label: "continue 语句", node: "continue_statement" },
return: { label: "return 语句", node: "return_statement" },
function_definition: { label: "函数定义", node: "function_definition" },
assignment: { label: "赋值语句", node: "assignment_expression" },
struct: { label: "结构体", node: "struct_specifier" },
include: { label: "#include 指令", node: "preproc_include" },
for_loop: { label: 'for 循环', node: 'for_statement' },
while_loop: { label: 'while 循环', node: 'while_statement' },
do_while: { label: 'do-while 循环', node: 'do_statement' },
if_statement: { label: 'if 条件', node: 'if_statement' },
else_clause: { label: 'else 子句', node: 'else_clause' },
switch_statement: { label: 'switch 语句', node: 'switch_statement' },
case_statement: { label: 'case 分支', node: 'case_statement' },
break: { label: 'break 语句', node: 'break_statement' },
continue: { label: 'continue 语句', node: 'continue_statement' },
return: { label: 'return 语句', node: 'return_statement' },
function_definition: { label: '函数定义', node: 'function_definition' },
assignment: { label: '赋值语句', node: 'assignment_expression' },
struct: { label: '结构体', node: 'struct_specifier' },
include: { label: '#include 指令', node: 'preproc_include' },
} satisfies Record<string, AstNodeTarget>
export const AST_NODE_TARGETS_BY_LANGUAGE: Record<string, Record<string, AstNodeTarget>> = {
@@ -158,9 +155,9 @@ export const AST_NODE_TARGETS_BY_LANGUAGE: Record<string, Record<string, AstNode
* 契约和判题机两个文件里各抄了两遍(C 一份、C++ 一份),同一份数据四份拷贝。
* 这里逐条列出来是为了保住下拉框的显示顺序(C++ 独有的几条是插在中间的)。
*/
"C++": {
'C++': {
for_loop: C_NODE_TARGETS.for_loop,
range_for_loop: { label: "范围 for 循环", node: "for_range_loop" },
range_for_loop: { label: '范围 for 循环', node: 'for_range_loop' },
while_loop: C_NODE_TARGETS.while_loop,
do_while: C_NODE_TARGETS.do_while,
if_statement: C_NODE_TARGETS.if_statement,
@@ -171,37 +168,37 @@ export const AST_NODE_TARGETS_BY_LANGUAGE: Record<string, Record<string, AstNode
continue: C_NODE_TARGETS.continue,
return: C_NODE_TARGETS.return,
function_definition: C_NODE_TARGETS.function_definition,
class_definition: { label: "类定义", node: "class_specifier" },
class_definition: { label: '类定义', node: 'class_specifier' },
struct: C_NODE_TARGETS.struct,
assignment: C_NODE_TARGETS.assignment,
include: C_NODE_TARGETS.include,
try_except: { label: "try-catch", node: "try_statement" },
throw: { label: "throw 语句", node: "throw_statement" },
namespace: { label: "namespace 定义", node: "namespace_definition" },
template: { label: "模板定义", node: "template_declaration" },
lambda: { label: "lambda 表达式", node: "lambda_expression" },
using: { label: "using 声明", node: "using_declaration" },
try_except: { label: 'try-catch', node: 'try_statement' },
throw: { label: 'throw 语句', node: 'throw_statement' },
namespace: { label: 'namespace 定义', node: 'namespace_definition' },
template: { label: '模板定义', node: 'template_declaration' },
lambda: { label: 'lambda 表达式', node: 'lambda_expression' },
using: { label: 'using 声明', node: 'using_declaration' },
},
Python: {
for_loop: { label: "for 循环", node: "for_statement" },
while_loop: { label: "while 循环", node: "while_statement" },
if_statement: { label: "if 条件", node: "if_statement" },
elif_clause: { label: "elif 子句", node: "elif_clause" },
else_clause: { label: "else 子句", node: "else_clause" },
break: { label: "break 语句", node: "break_statement" },
continue: { label: "continue 语句", node: "continue_statement" },
return: { label: "return 语句", node: "return_statement" },
function_definition: { label: "函数定义", node: "function_definition" },
class_definition: { label: "类定义", node: "class_definition" },
assignment: { label: "赋值语句", node: "assignment" },
try_except: { label: "try-except", node: "try_statement" },
with_statement: { label: "with 语句", node: "with_statement" },
import: { label: "import 语句", node: "import_statement" },
import_from: { label: "from-import 语句", node: "import_from_statement" },
list_comprehension: { label: "列表推导式", node: "list_comprehension" },
list_literal: { label: "列表", node: "list" },
dict_literal: { label: "字典", node: "dictionary" },
set_literal: { label: "集合", node: "set" },
for_loop: { label: 'for 循环', node: 'for_statement' },
while_loop: { label: 'while 循环', node: 'while_statement' },
if_statement: { label: 'if 条件', node: 'if_statement' },
elif_clause: { label: 'elif 子句', node: 'elif_clause' },
else_clause: { label: 'else 子句', node: 'else_clause' },
break: { label: 'break 语句', node: 'break_statement' },
continue: { label: 'continue 语句', node: 'continue_statement' },
return: { label: 'return 语句', node: 'return_statement' },
function_definition: { label: '函数定义', node: 'function_definition' },
class_definition: { label: '类定义', node: 'class_definition' },
assignment: { label: '赋值语句', node: 'assignment' },
try_except: { label: 'try-except', node: 'try_statement' },
with_statement: { label: 'with 语句', node: 'with_statement' },
import: { label: 'import 语句', node: 'import_statement' },
import_from: { label: 'from-import 语句', node: 'import_from_statement' },
list_comprehension: { label: '列表推导式', node: 'list_comprehension' },
list_literal: { label: '列表', node: 'list' },
dict_literal: { label: '字典', node: 'dictionary' },
set_literal: { label: '集合', node: 'set' },
/**
* f-string 认的是 `interpolation`(`f"{x}"` 里的 `{x}`),不是 `format_string` ——
* 这个版本的 tree-sitter-python 里**没有** format_string 这种节点,f-string 是
@@ -213,7 +210,7 @@ export const AST_NODE_TARGETS_BY_LANGUAGE: Record<string, Record<string, AstNode
* 这已经是严格的改善。实测 `%` 格式化、`.format()`、普通字符串、字符串拼接
* 都不会误伤。
*/
f_string: { label: "f-string", node: "interpolation" },
f_string: { label: 'f-string', node: 'interpolation' },
},
}
@@ -227,59 +224,59 @@ export const AST_NODE_TARGETS_BY_LANGUAGE: Record<string, Record<string, AstNode
* 恒等的条目(`+`、`==` …)写全是因为这张表同时是后台下拉的选项来源。
*/
const C_OPERATOR_TARGETS = {
"+": "+",
"-": "-",
"*": "*",
"/": "/",
"%": "%",
"+=": "+=",
"-=": "-=",
"*=": "*=",
"/=": "/=",
"%=": "%=",
"++": "++",
"--": "--",
"==": "==",
"!=": "!=",
">": ">",
">=": ">=",
"<": "<",
"<=": "<=",
and: "&&",
or: "||",
not: "!",
"&": "&",
"|": "|",
'+': '+',
'-': '-',
'*': '*',
'/': '/',
'%': '%',
'+=': '+=',
'-=': '-=',
'*=': '*=',
'/=': '/=',
'%=': '%=',
'++': '++',
'--': '--',
'==': '==',
'!=': '!=',
'>': '>',
'>=': '>=',
'<': '<',
'<=': '<=',
and: '&&',
or: '||',
not: '!',
'&': '&',
'|': '|',
}
export const AST_OPERATOR_TARGETS_BY_LANGUAGE: Record<string, Record<string, string>> = {
C: C_OPERATOR_TARGETS,
// `<<` / `>>` 对 C++ 主要是 cout/cin 的流运算符(位移是同一个 token)
"C++": { ...C_OPERATOR_TARGETS, "<<": "<<", ">>": ">>" },
'C++': { ...C_OPERATOR_TARGETS, '<<': '<<', '>>': '>>' },
Python: {
"+": "+",
"-": "-",
"*": "*",
"/": "/",
"//": "//",
"%": "%",
"**": "**",
"+=": "+=",
"-=": "-=",
"*=": "*=",
"/=": "/=",
"%=": "%=",
"==": "==",
"!=": "!=",
">": ">",
">=": ">=",
"<": "<",
"<=": "<=",
and: "and",
or: "or",
not: "not",
"&": "&",
"|": "|",
'+': '+',
'-': '-',
'*': '*',
'/': '/',
'//': '//',
'%': '%',
'**': '**',
'+=': '+=',
'-=': '-=',
'*=': '*=',
'/=': '/=',
'%=': '%=',
'==': '==',
'!=': '!=',
'>': '>',
'>=': '>=',
'<': '<',
'<=': '<=',
and: 'and',
or: 'or',
not: 'not',
'&': '&',
'|': '|',
},
}
@@ -292,9 +289,7 @@ export const AST_SUPPORTED_LANGUAGES = Object.keys(AST_NODE_TARGETS_BY_LANGUAGE)
/** 全语言的节点中文名并集,只给拿不到语言的场合做回落。有语言一律走 astNodeLabel() */
export const AST_NODE_TARGET_LABELS: Record<string, string> = Object.fromEntries(
Object.values(AST_NODE_TARGETS_BY_LANGUAGE).flatMap((table) =>
Object.entries(table).map(([target, entry]) => [target, entry.label]),
),
Object.values(AST_NODE_TARGETS_BY_LANGUAGE).flatMap((table) => Object.entries(table).map(([target, entry]) => [target, entry.label])),
)
export function astNodeLabel(target: string, language?: string): string {
@@ -308,11 +303,7 @@ export function astNodeLabel(target: string, language?: string): string {
* 都对不上就回落到裸 target(恒等的运算符 `+` / `==` 走的就是这条)。
*/
export function astTargetNodeType(target: string, language: string): string {
return (
AST_NODE_TARGETS_BY_LANGUAGE[language]?.[target]?.node ??
AST_OPERATOR_TARGETS_BY_LANGUAGE[language]?.[target] ??
target
)
return AST_NODE_TARGETS_BY_LANGUAGE[language]?.[target]?.node ?? AST_OPERATOR_TARGETS_BY_LANGUAGE[language]?.[target] ?? target
}
export function astOperatorLabel(target: string, language?: string): string {
@@ -327,13 +318,8 @@ export function astOperatorLabel(target: string, language?: string): string {
* 读取路径(判题、题目页要求)用它把这种规则整条丢掉,而不是让 schema 校验失败:
* astRulesSchema 同时用于**读**后台题目详情,在那儿抛错会让整个详情打不开。
*/
export function astRuleIsMeaningful(rule: {
engine: string
exact?: number
min?: number
max?: number
}): boolean {
if (!rule.engine.startsWith("count")) return true
export function astRuleIsMeaningful(rule: { engine: string; exact?: number; min?: number; max?: number }): boolean {
if (!rule.engine.startsWith('count')) return true
return rule.exact !== undefined || rule.min !== undefined || rule.max !== undefined
}
@@ -349,7 +335,7 @@ export const astRequirementSchema = z.object({
/** 已经渲染好的中文文案,例如「if 条件 出现 2 次」 */
description: z.string(),
/** 标签配色:必须做 / 不能做 / 次数约束 */
kind: z.enum(["require", "forbid", "count"]),
kind: z.enum(['require', 'forbid', 'count']),
})
/** 按语言分组,与 astRulesSchema 同一套键 */
@@ -495,20 +481,13 @@ export const problemListItemSchema = z.object({
export const problemListSchema = paginatedSchema(problemListItemSchema)
/** 题目列表「全部类型」下拉:SQL / 有参考图 / 画流程图 / 语法要求 */
export const problemTypeFilterSchema = z.enum(["sql", "reference", "flowchart", "ast"])
export const problemTypeFilterSchema = z.enum(['sql', 'reference', 'flowchart', 'ast'])
/**
* 题目列表的排序。值沿用原来的(旧链接照样能用):空串 = 最新创建、create_time = 最早创建,
* ±submission_number / ±accepted_number 是做过 / 做对的**人数**,带 - 是从多到少
*/
export const problemListSortSchema = z.enum([
"",
"create_time",
"-submission_number",
"submission_number",
"-accepted_number",
"accepted_number",
])
export const problemListSortSchema = z.enum(['', 'create_time', '-submission_number', 'submission_number', '-accepted_number', 'accepted_number'])
/**
* 题目列表顶上那行和左栏的进度。**只说做到了什么**:做对几道、这周新做对几道、
@@ -543,7 +522,7 @@ export const problemProgressSchema = z.object({
* 题材(LOL、周杰伦、哈利波特……)。两类原来混在一起,题目列表上四十多个标签
* 挤成一片,学生想按知识点找题得在一堆游戏名里挑。新标签默认算知识点。
*/
export const tagCategorySchema = z.enum(["knowledge", "theme"])
export const tagCategorySchema = z.enum(['knowledge', 'theme'])
export const tagSchema = z.object({
id: z.number().int(),
@@ -577,9 +556,7 @@ export const problemStatsSchema = z.object({
/** 我自己:交了几次、做对没有。没登录为 null */
me: z.object({ attempts: z.number().int(), solved: z.boolean() }).nullable(),
/** 学生自己班这题 x / y 人做对(y 是班里交过的人)。没班级、比赛里为 null */
myClass: z
.object({ className: z.string(), tried: z.number().int(), solved: z.number().int() })
.nullable(),
myClass: z.object({ className: z.string(), tried: z.number().int(), solved: z.number().int() }).nullable(),
/**
* 老师:各班做这道题的情况,和数据统计「按班级汇总」**同一个口径**(只算学生的提交;
* 做完 / 班级人数、交了没对、没交、正确率、最近一次),交过的人多的在前。
+5 -7
View File
@@ -1,7 +1,7 @@
import { z } from "zod"
import { z } from 'zod'
import { paginatedSchema, sampleUserSchema } from "./common"
import { maskedProblemDifficultySchema } from "./problem"
import { paginatedSchema, sampleUserSchema } from './common'
import { maskedProblemDifficultySchema } from './problem'
/**
* 我在这份题单里的进度。
@@ -22,7 +22,7 @@ export const problemSetUserProgressSummarySchema = z.object({
})
/** 奖章只有两种条件:全部做完、做对 N 道(「总分达到 N」随分数一起拿掉了,0026 换算过) */
export const badgeConditionTypeSchema = z.enum(["all_problems", "problem_count"])
export const badgeConditionTypeSchema = z.enum(['all_problems', 'problem_count'])
export const problemSetBadgeSchema = z.object({
id: z.number().int(),
@@ -117,9 +117,7 @@ export const problemSetClassStudentSchema = z.object({
export const problemSetClassViewSchema = z.object({
/** 加入过这份题单的学生分属哪些班:三分之一以上的人加入了的班在前(按最近一次加入),零星几个人的在后 */
classes: z.array(
z.object({ className: z.string(), joined: z.number().int(), size: z.number().int() }),
),
classes: z.array(z.object({ className: z.string(), joined: z.number().int(), size: z.number().int() })),
/** 这次看的是哪个班;谁都没加入时为 null */
className: z.string().nullable(),
problems: z.array(
+6 -8
View File
@@ -1,6 +1,6 @@
import { z } from "zod"
import { z } from 'zod'
import { sampleUserSchema } from "./common"
import { sampleUserSchema } from './common'
/**
* 排名页(设计稿「排名重设计」G1–G3)。用户要的是**激发竞争、一眼看出排名**,所以:
@@ -10,8 +10,8 @@ import { sampleUserSchema } from "./common"
* 按先后排,名次差的是「这节课谁先做完」。
* - 「做对」= 这道题**第一次**做对落在这段时间里(比赛里的提交不算),老题重交不算成绩。
*/
export const rankScopeSchema = z.enum(["class", "grade", "all"])
export const rankPeriodSchema = z.enum(["week", "term", "all"])
export const rankScopeSchema = z.enum(['class', 'grade', 'all'])
export const rankPeriodSchema = z.enum(['week', 'term', 'all'])
export const rankRowSchema = z.object({
rank: z.number().int().positive(),
@@ -147,9 +147,7 @@ export const classDetailSchema = z.object({
/** 每个同学这学期做对几道,画分布点阵用 */
distribution: z.array(z.number().int().nonnegative()),
/** 这学期每周(周一起)人均新做对,和同年级在用的班平均比 */
weeks: z.array(
z.object({ weekStart: z.string(), perCapita: z.number(), gradeAvg: z.number().nullable() }),
),
weeks: z.array(z.object({ weekStart: z.string(), perCapita: z.number(), gradeAvg: z.number().nullable() })),
/** 要多关心的同学(这学期做对不到 3 道),只给老师;学生拿到的是 null */
care: z
.array(
@@ -175,7 +173,7 @@ export type ClassDetail = z.infer<typeof classDetailSchema>
* 不然 37/39 和 38/40 都显示 95%,皇冠却只给一边。口径和排名页一样:比赛里的提交不算,
* 不计入排名的人分子分母都不算。
*/
export const classPkPeriodSchema = z.enum(["week", "term"])
export const classPkPeriodSchema = z.enum(['week', 'term'])
export const classPkCellSchema = z.object({
/** 这段时间交过这道题的人 */
+14 -21
View File
@@ -12,15 +12,10 @@
* **这个文件不能 import zod。** 前端入口要用这里的 USER_TYPE / toAdminType,
* 一 import 就把整个 zod 运行时(~100KB)拖进首屏包。对应的 zod schema 在 admin.ts。
*/
export const ADMIN_TYPES = [
"Regular User",
"Student Admin",
"Teacher Admin",
"Super Admin",
] as const
export const ADMIN_TYPES = ['Regular User', 'Student Admin', 'Teacher Admin', 'Super Admin'] as const
export type AdminType = (typeof ADMIN_TYPES)[number]
export const PROBLEM_PERMISSIONS = ["None", "Own", "All"] as const
export const PROBLEM_PERMISSIONS = ['None', 'Own', 'All'] as const
export type ProblemPermission = (typeof PROBLEM_PERMISSIONS)[number]
/**
@@ -32,29 +27,27 @@ export type ProblemPermission = (typeof PROBLEM_PERMISSIONS)[number]
*/
/** 能进后台的三种 */
export const ADMIN_ROLES: readonly AdminType[] = ["Student Admin", "Teacher Admin", "Super Admin"]
export const ADMIN_ROLES: readonly AdminType[] = ['Student Admin', 'Teacher Admin', 'Super Admin']
/** 老师及以上 */
export const TEACHER_ROLES: readonly AdminType[] = ["Teacher Admin", "Super Admin"]
export const TEACHER_ROLES: readonly AdminType[] = ['Teacher Admin', 'Super Admin']
/**
* 学生口径:排行榜、班级榜、比赛榜、自学统计都只算这两种,教师和超管不入榜。
* 注意 Student Admin 算学生 —— 他要参赛、要上榜,只是多了个后台入口。
*/
export const STUDENT_ROLES: readonly AdminType[] = ["Regular User", "Student Admin"]
export const STUDENT_ROLES: readonly AdminType[] = ['Regular User', 'Student Admin']
/**
* 把库里读出来的裸字符串收成联合类型。**认不出来的一律降成最低权限**,
* 不抛错:脏数据不该让人登不上,但更不该让人凭一个拼错的角色名拿到权限。
*/
export function toAdminType(value: string): AdminType {
return (ADMIN_TYPES as readonly string[]).includes(value) ? (value as AdminType) : "Regular User"
return (ADMIN_TYPES as readonly string[]).includes(value) ? (value as AdminType) : 'Regular User'
}
export function toProblemPermission(value: string): ProblemPermission {
return (PROBLEM_PERMISSIONS as readonly string[]).includes(value)
? (value as ProblemPermission)
: "None"
return (PROBLEM_PERMISSIONS as readonly string[]).includes(value) ? (value as ProblemPermission) : 'None'
}
/**
@@ -65,14 +58,14 @@ export function toProblemPermission(value: string): ProblemPermission {
* 原先 `apps/web/src/utils/constants.ts` 里另抄了一份,那是这四个字符串的第三份副本。
*/
export const USER_TYPE = {
REGULAR_USER: "Regular User",
STUDENT_ADMIN: "Student Admin",
TEACHER_ADMIN: "Teacher Admin",
SUPER_ADMIN: "Super Admin",
REGULAR_USER: 'Regular User',
STUDENT_ADMIN: 'Student Admin',
TEACHER_ADMIN: 'Teacher Admin',
SUPER_ADMIN: 'Super Admin',
} as const satisfies Record<string, AdminType>
export const PROBLEM_PERMISSION = {
NONE: "None",
OWN: "Own",
ALL: "All",
NONE: 'None',
OWN: 'Own',
ALL: 'All',
} as const satisfies Record<string, ProblemPermission>
+1 -1
View File
@@ -1,4 +1,4 @@
import { z } from "zod"
import { z } from 'zod'
/**
* 站点配置。网址、简称、页脚三项 2026-10-13 删了(前后端从来没读过,改了也看不到变化),
+13 -13
View File
@@ -1,8 +1,8 @@
import { z } from "zod"
import { z } from 'zod'
import { paginatedSchema } from "./common"
import { judgeStatusSchema, type JudgeStatus } from "./judge-status"
import { problemLanguageSchema, runnableLanguageSchema } from "./language"
import { paginatedSchema } from './common'
import { judgeStatusSchema, type JudgeStatus } from './judge-status'
import { problemLanguageSchema, runnableLanguageSchema } from './language'
/**
* 判题机原始输出(`submission.info` 的 JSONB 原文)。**只是类型,不作运行时校验。**
@@ -285,10 +285,10 @@ export const submissionDetailSchema = z.object({
* 拉一次就是了,不必让推送顺带背一份 JSONB 的形状。
*/
export const submissionUpdateSchema = z.object({
type: z.literal("submission_update"),
type: z.literal('submission_update'),
submissionId: z.string(),
result: judgeStatusSchema,
status: z.enum(["pending", "judging", "finished", "error"]),
status: z.enum(['pending', 'judging', 'finished', 'error']),
score: z.number().optional(),
})
@@ -439,7 +439,7 @@ export const submissionStatisticsSchema = z.object({
* 这批号(夏令营、兴趣班、自己注册的)`user.class_name` 是 null,当成一个班来看。
* 真班名全是数字(`253`、`2511`),撞不上
*/
export const NO_CLASS = "none"
export const NO_CLASS = 'none'
/**
* 统计「一行一节课」的总览(GET /submissions/statistics/lessons,设计稿「统计合二为一」B)。
@@ -564,14 +564,14 @@ export const todaySubmissionStatisticsSchema = z.object({
userCount: z.number().int(),
acceptedUsers: z.number().int(),
/** 我自己今天在这道题上:做对了 / 交了没对 / 没交;没登录为 null */
mine: z.enum(["accepted", "tried", "none"]).nullable(),
mine: z.enum(['accepted', 'tried', 'none']).nullable(),
}),
),
})
export const formatCodeRequestSchema = z.object({
code: z.string().max(1024 * 1024),
language: z.enum(["python", "c", "cpp", "sql"]),
language: z.enum(['python', 'c', 'cpp', 'sql']),
/** C / C++ 格式化前要先编一遍,得套上这道题的模板才编得过 */
problemId: z.number().int().optional(),
})
@@ -627,15 +627,15 @@ export const trialRunRequestSchema = z.object({
*/
export type TrialRunResponse =
| {
status: "done"
status: 'done'
cases: {
result: JudgeStatus
output: string
runtimeError?: NonNullable<StatisticInfo["runtime_error"]>
runtimeError?: NonNullable<StatisticInfo['runtime_error']>
}[]
}
| { status: "compile-error"; message: string }
| { status: "too-much-output" }
| { status: 'compile-error'; message: string }
| { status: 'too-much-output' }
export type StatisticInfo = z.infer<typeof statisticInfoSchema>
export type AstResultInfo = z.infer<typeof astResultSchema>
+1 -1
View File
@@ -4,5 +4,5 @@
* 两边都按**固定偏移**算、不查 tzdata:中国大陆 1991 年起没有夏令时,东八区恒为 UTC+8,
* 这样进程 TZ、浏览器时区、镜像里有没有 tzdata 都不影响结果。换时区时两个常量一起改。
*/
export const TIME_ZONE = "Asia/Shanghai"
export const TIME_ZONE = 'Asia/Shanghai'
export const TIME_ZONE_OFFSET_MINUTES = 8 * 60