Deploy / deploy (push) Successful in 55s
- C / C++ 的函数定义排掉 main,下拉改叫「自定义函数」。原来「必须使用」永远通过、「不能使用」永远失败, 出题页挑战候选里 C 的「用自定义函数再写一遍」也因此一直挖不出来 - 运算符只认二元运算 / 复合赋值 / 自增自减里的 token,一元只认取反。scanf 的 &、int *p、vector<int>、 *args、import *、**kw、not in、is not、case 1 | 2 不再算「用了」 - Python 的 math.sqrt(4) 算调用了 sqrt()(只认 import 进来的模块) - Python 的「赋值语句」连 s += 1 一起算,和 C 一个口径 - 保存时拦下负数次数、至少比至多还多的次数规则 现有 19 道题的规则都不受影响:1265 条真实提交重判,结论和改之前一致。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
625 lines
28 KiB
TypeScript
625 lines
28 KiB
TypeScript
import { z } from 'zod'
|
||
|
||
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'])
|
||
|
||
/**
|
||
* 题目详情/列表里的难度。**可为 null** —— 比赛进行中、看的人又不是管理员时,
|
||
* 难度和提交数一样属于「先别告诉参赛者」的信息(旧 Django 的
|
||
* `ProblemSafeSerializer` 直接把 difficulty 放进 exclude,字段整个不下发)。
|
||
*
|
||
* 端上必须当「没有」处理,不要拿它去查 DIFFICULTY 映射表 —— 这正是
|
||
* problemDifficultySchema 写成严格枚举要防的那件事。
|
||
*/
|
||
export const maskedProblemDifficultySchema = problemDifficultySchema.nullable()
|
||
|
||
/**
|
||
* SQL 题配置与展示数据。两者都是 `problem.sql_config` / `problem.sql_display`
|
||
* 的 **JSONB 原文**,所以键名保持 snake_case —— 生产库 9 道 SQL 题存的就是这个形状
|
||
* (移植自旧后端 `judge/sql_runner.py:build_display`,逐条比对过,键集完全一致)。
|
||
*
|
||
* 写成精确 schema 而不是 `z.record(z.unknown())`:前端原来得自己手抄一份
|
||
* SQLDisplay 接口才能渲染表格,抄错了没人拦得住。
|
||
*/
|
||
export const sqlConfigSchema = z.object({
|
||
mode: z.enum(['query', 'modify']),
|
||
order_sensitive: z.boolean(),
|
||
})
|
||
|
||
/** 表格里的单元格。SQLite 只会给出这三种;BLOB 在落库前已转成十六进制字符串 */
|
||
const sqlCellSchema = z.union([z.string(), z.number(), z.null()])
|
||
|
||
const sqlDisplayColumnSchema = z.object({
|
||
name: z.string(),
|
||
/** 表达式/聚合列(COUNT(*)、别名)在数据表里无同名列,类型为空串,前端据此隐藏 */
|
||
type: z.string(),
|
||
})
|
||
|
||
const sqlResultSetSchema = z.object({
|
||
columns: z.array(sqlDisplayColumnSchema),
|
||
rows: z.array(z.array(sqlCellSchema)),
|
||
total_rows: z.number().int().nonnegative(),
|
||
truncated: z.boolean(),
|
||
})
|
||
|
||
export const sqlDisplayTableSchema = sqlResultSetSchema.extend({
|
||
name: z.string(),
|
||
/** 被标准答案 DROP 的表:条目用初始数据补齐、rows 清空,前端提示「表已删除」 */
|
||
dropped: z.boolean().optional(),
|
||
})
|
||
|
||
export const sqlDisplaySchema = z.object({
|
||
tables: z.array(sqlDisplayTableSchema),
|
||
// query 题给结果集,modify 题给改动后的表 —— 两种形态,前端按有没有
|
||
// changed_tables 分支
|
||
expected: z.union([sqlResultSetSchema, z.object({ changed_tables: z.array(sqlDisplayTableSchema) })]),
|
||
})
|
||
|
||
/**
|
||
* AST 代码要求。同一个形状原来在**三个地方**各写了一份,三份都不一样:
|
||
* apps/api/src/judge/ast.ts 的 AstRule(判题机真读的那份,九个字段)、
|
||
* apps/web/src/utils/types.ts 的 AstRules(少了 label / exact / outer / inner)、
|
||
* AstRulesEditor.vue 里的本地 AstRule(少了 outer / inner)。
|
||
* 编辑器写得出 label / exact,题目类型却描述不了它们。现在以这里为准。
|
||
*
|
||
* 除 engine 外全部可选:判题机每条规则只读自己那几个字段
|
||
* (见 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',
|
||
// 只有 Python 有关键字参数:「调用 print() 时必须用 sep 参数」
|
||
'must_use_keyword',
|
||
// 判题机实现了,但后台编辑器还没有对应的选项,目前只能手工造数据用上
|
||
'must_have_nesting',
|
||
])
|
||
|
||
export const astRuleSchema = z.object({
|
||
engine: astRuleEngineSchema,
|
||
/** 检查目标:节点类型 / 函数名 / 方法名 / 运算符,按 engine 而定 */
|
||
target: z.string().optional(),
|
||
/** must_have_nesting 专用:外层、内层节点 */
|
||
outer: z.string().optional(),
|
||
inner: z.string().optional(),
|
||
/** must_use_keyword 专用:参数名(target 是函数名) */
|
||
keyword: z.string().optional(),
|
||
/** 展示用的中文名,缺省回落到 target */
|
||
label: z.string().optional(),
|
||
/** 自定义提示。生产库里存的是空串而不是缺键,判题机按 `||` 回落到默认文案 */
|
||
message: z.string().optional(),
|
||
/** count_* 引擎的次数约束 */
|
||
exact: z.number().int().optional(),
|
||
min: z.number().int().optional(),
|
||
max: z.number().int().optional(),
|
||
})
|
||
|
||
/** 按语言分组:`{ Python: [...], C: [...] }`,键是 languages 里的语言名 */
|
||
export const astRulesSchema = z.record(z.string(), z.array(astRuleSchema))
|
||
|
||
/**
|
||
* 一个 target 一条,`label` 给人看(后台下拉、题目页的「要求」),`node` 给判题机
|
||
* 拿去比 tree-sitter 的节点类型。
|
||
*
|
||
* **这两半原来是分在两个包里的两张表** —— 这边 target → 中文名,
|
||
* `apps/api/src/judge/ast.ts` 的 mappings 是 target → tree-sitter 节点类型,靠一句
|
||
* 「两边必须同增同减」的注释维持。只加一边是静默错判:老师给 C 题选到只有 Python
|
||
* 有的 `list_comprehension`,判题机 `mapping[target] ?? target` 拿裸名去比节点类型,
|
||
* C 的语法树里永远不存在它,于是「必须使用列表推导式」永远失败、「不能使用
|
||
* f-string」永远通过,两头都不报错,只有学生受着。并成一张之后,加 target 而漏配
|
||
* 节点类型在结构上就不可能了。
|
||
*
|
||
* (更早之前 AstRulesEditor.vue 和 ProblemContent.vue 还各手抄过一份中文名,
|
||
* 且 C 和 Python 的节点混在一张 15 条的表里铺成后台下拉。)
|
||
*/
|
||
export interface AstNodeTarget {
|
||
/** 后台下拉和题目页展示的中文名 */
|
||
label: string
|
||
/** tree-sitter 里对应的节点类型,判题机按它 collectNodes */
|
||
node: string
|
||
}
|
||
|
||
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' },
|
||
// 不算 main(判题机 judge/ast.ts 的 nodeMatch 里排掉的),所以不叫「函数定义」
|
||
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>> = {
|
||
C: C_NODE_TARGETS,
|
||
/**
|
||
* tree-sitter-cpp 继承 tree-sitter-c 的语法,C 那 14 条在 C++ 树里逐个实测通用,
|
||
* 所以共用的条目一律**引用** C_NODE_TARGETS 而不是抄一遍 —— 原来 C 的 14 行在
|
||
* 契约和判题机两个文件里各抄了两遍(C 一份、C++ 一份),同一份数据四份拷贝。
|
||
* 这里逐条列出来是为了保住下拉框的显示顺序(C++ 独有的几条是插在中间的)。
|
||
*/
|
||
'C++': {
|
||
for_loop: C_NODE_TARGETS.for_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,
|
||
else_clause: C_NODE_TARGETS.else_clause,
|
||
switch_statement: C_NODE_TARGETS.switch_statement,
|
||
case_statement: C_NODE_TARGETS.case_statement,
|
||
break: C_NODE_TARGETS.break,
|
||
continue: C_NODE_TARGETS.continue,
|
||
return: C_NODE_TARGETS.return,
|
||
function_definition: C_NODE_TARGETS.function_definition,
|
||
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' },
|
||
},
|
||
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' },
|
||
/**
|
||
* f-string 认的是 `interpolation`(`f"{x}"` 里的 `{x}`),不是 `format_string` ——
|
||
* 这个版本的 tree-sitter-python 里**没有** format_string 这种节点,f-string 是
|
||
* 一个 `string`,靠 `string_start` 为 `f"` 和内部的 interpolation 子节点来认。
|
||
* 配成 format_string 的那阵子,「不能使用 f-string」从上线起就一直判成通过。
|
||
*
|
||
* 代价是 `f"abc"` 这种**没有占位符**的 f-string 认不出来(它确实不含
|
||
* interpolation)。没占位符的 f-string 本来也没有意义,而且比起「一个都认不出来」
|
||
* 这已经是严格的改善。实测 `%` 格式化、`.format()`、普通字符串、字符串拼接
|
||
* 都不会误伤。
|
||
*/
|
||
f_string: { label: 'f-string', node: 'interpolation' },
|
||
},
|
||
}
|
||
|
||
/**
|
||
* 运算符 target → 该语言里的实际写法。逻辑名 `and` / `or` / `not` 在 C 里写作
|
||
* `&&` / `||` / `!`,文案要翻(否则 C 题的学生看到「必须使用 and 运算符」,而 C 里
|
||
* 根本没有 `and` 这个词),判题机也正好拿同一个值去比节点类型 —— 所以这张表**一份
|
||
* 两用**,不像节点那样需要两个字段。原来判题机的 mappings 里还抄了一份非恒等的
|
||
* (`and`→`&&` 那三条),取值逐个相同,纯属重复。
|
||
*
|
||
* 恒等的条目(`+`、`==` …)写全是因为这张表同时是后台下拉的选项来源。
|
||
*/
|
||
const C_OPERATOR_TARGETS = {
|
||
'+': '+',
|
||
'-': '-',
|
||
'*': '*',
|
||
'/': '/',
|
||
'%': '%',
|
||
'+=': '+=',
|
||
'-=': '-=',
|
||
'*=': '*=',
|
||
'/=': '/=',
|
||
'%=': '%=',
|
||
'++': '++',
|
||
'--': '--',
|
||
'==': '==',
|
||
'!=': '!=',
|
||
'>': '>',
|
||
'>=': '>=',
|
||
'<': '<',
|
||
'<=': '<=',
|
||
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, '<<': '<<', '>>': '>>' },
|
||
Python: {
|
||
'+': '+',
|
||
'-': '-',
|
||
'*': '*',
|
||
'/': '/',
|
||
'//': '//',
|
||
'%': '%',
|
||
'**': '**',
|
||
'+=': '+=',
|
||
'-=': '-=',
|
||
'*=': '*=',
|
||
'/=': '/=',
|
||
'%=': '%=',
|
||
'==': '==',
|
||
'!=': '!=',
|
||
'>': '>',
|
||
'>=': '>=',
|
||
'<': '<',
|
||
'<=': '<=',
|
||
and: 'and',
|
||
or: 'or',
|
||
not: 'not',
|
||
'&': '&',
|
||
'|': '|',
|
||
},
|
||
}
|
||
|
||
/**
|
||
* 判题机真正能做 AST 检查的语言(C / C++ / Python 三种,就是上面这张表的键)。设计文档写的是
|
||
* "支持全部 6 种语言",其余语言 judge/ast.ts 的 loadLanguage 返回 null,规则一条都不会跑。
|
||
* 所以后台不给别的语言开 tab,题目页也不把它们的规则展示成「要求」。
|
||
*/
|
||
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])),
|
||
)
|
||
|
||
export function astNodeLabel(target: string, language?: string): string {
|
||
const table = language ? AST_NODE_TARGETS_BY_LANGUAGE[language] : undefined
|
||
return table?.[target]?.label ?? AST_NODE_TARGET_LABELS[target] ?? target
|
||
}
|
||
|
||
/**
|
||
* target → tree-sitter 节点类型。判题机唯一的解析入口 ——
|
||
* 节点走上面那张表的 `node`,运算符走运算符表(它的值本身就是要比的 token),
|
||
* 都对不上就回落到裸 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
|
||
}
|
||
|
||
export function astOperatorLabel(target: string, language?: string): string {
|
||
return (language ? AST_OPERATOR_TARGETS_BY_LANGUAGE[language]?.[target] : undefined) ?? target
|
||
}
|
||
|
||
/**
|
||
* count_* 引擎至少要有一个数字约束,否则这条规则恒真 —— rangePassed 三个字段全
|
||
* undefined 直接返回 true,描述也退化成光秃秃一个「for 循环」。编辑器切到
|
||
* 「出现次数」时会清掉 exact/min/max,老师不填数字就会存出这种规则。
|
||
*
|
||
* 读取路径(判题、题目页要求)用它把这种规则整条丢掉,而不是让 schema 校验失败:
|
||
* astRulesSchema 同时用于**读**后台题目详情,在那儿抛错会让整个详情打不开。
|
||
*/
|
||
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
|
||
}
|
||
|
||
/**
|
||
* 下发给**学生**的代码要求。只有渲染要用的两个字段 —— 文案由后端生成,
|
||
* engine / target 这些内部字段不出现在响应里。
|
||
*
|
||
* 旧后端的 ProblemSerializer 没排掉 ast_rules,学生拿到的是规则原文;阶段 3
|
||
* 泄露评审刻意收掉了它,同时写明「前端要读具体内容的话得补个专门的字段」——
|
||
* 就是这个。收紧保留,展示恢复。
|
||
*/
|
||
export const astRequirementSchema = z.object({
|
||
/** 已经渲染好的中文文案,例如「if 条件 出现 2 次」 */
|
||
description: z.string(),
|
||
/** 标签配色:必须做 / 不能做 / 次数约束 */
|
||
kind: z.enum(['require', 'forbid', 'count']),
|
||
})
|
||
|
||
/** 按语言分组,与 astRulesSchema 同一套键 */
|
||
export const astRequirementsSchema = z.record(z.string(), z.array(astRequirementSchema))
|
||
|
||
/**
|
||
* 挑战:做对之后,同一道题加一条写法上的限制再做一遍(「整首歌只用 1 次 print」)。
|
||
* 判定沿用 AST 规则,几条规则同时满足才算做到 —— 单写「用 while」的话学生留着原来的
|
||
* for、再补一个空 while 就过了,所以常配成「用 while + 不用 for」。
|
||
*
|
||
* 和 astRules 一样按语言分组。`id` 是谁做到了(challenge_success)的记账键:
|
||
* 规则一改就换新 id,旧的做到记录跟着旧规则作废,只改说法不换。
|
||
* `scannedAt` 是后端记的「已经拿历史上做对的代码补过一遍记录」,见 services/challenge.ts。
|
||
*/
|
||
export const challengeSchema = z.object({
|
||
id: z.string(),
|
||
/** 给学生看的说法,例如「整首歌只用 1 次 print」 */
|
||
wording: z.string(),
|
||
rules: z.array(astRuleSchema),
|
||
scannedAt: z.string().optional(),
|
||
})
|
||
|
||
export const problemChallengesSchema = z.record(z.string(), z.array(challengeSchema))
|
||
|
||
/** 挑战说法的长度上限:要在结果页签的一行里放得下 */
|
||
export const CHALLENGE_WORDING_MAX = 24
|
||
|
||
/** 一条挑战最多几条规则、一道题一种语言最多几条挑战 */
|
||
export const CHALLENGE_RULES_MAX = 4
|
||
export const CHALLENGES_PER_LANGUAGE_MAX = 4
|
||
|
||
/** 我做到了一条挑战:本班、全站各是第几个做到的 */
|
||
export const challengeMineSchema = z.object({
|
||
at: z.string(),
|
||
siteRank: z.number().int(),
|
||
/** 没有班级(老师、自己注册的号)时为 null */
|
||
classRank: z.number().int().nullable(),
|
||
})
|
||
|
||
/** 下发给学生的一条挑战:只有说法和人数,规则原文不出去(同 astRequirements) */
|
||
export const challengeItemSchema = z.object({
|
||
id: z.string(),
|
||
wording: z.string(),
|
||
/** 全站做到的人数 */
|
||
doneCount: z.number().int(),
|
||
mine: challengeMineSchema.nullable(),
|
||
})
|
||
|
||
/** GET /challenges:几道题、某种语言下的挑战 */
|
||
export const problemChallengeGroupSchema = z.object({
|
||
problemId: z.string(),
|
||
title: z.string(),
|
||
/** 这种语言做对过这道题的人数(「642 个做对的人里 62 人做到」的 642) */
|
||
acCount: z.number().int(),
|
||
/** 我用这种语言做对过没有:没做对的挑战不给接 */
|
||
solved: z.boolean(),
|
||
challenges: z.array(challengeItemSchema),
|
||
})
|
||
|
||
export const challengeListSchema = z.array(problemChallengeGroupSchema)
|
||
|
||
export const problemDetailSchema = z.object({
|
||
id: z.number().int(),
|
||
_id: z.string(),
|
||
title: z.string(),
|
||
description: z.string(),
|
||
inputDescription: z.string(),
|
||
outputDescription: z.string(),
|
||
samples: z.array(
|
||
z.object({
|
||
input: z.string(),
|
||
output: z.string(),
|
||
}),
|
||
),
|
||
hint: z.string().nullable(),
|
||
languages: z.array(problemLanguageSchema),
|
||
/**
|
||
* 语言 → 代码模板。**用 partialRecord 让键受语言联合约束** ——
|
||
* 原来这里是 `z.record(z.string(), z.string())`,等价于 `Record<string, string>`,
|
||
* 前端按语言查模板时拿不到任何键名保护(`template["Pytho3"]` 也是合法表达式)。
|
||
*
|
||
* partialRecord 而非 record:没配模板的语言不该出现该键(`template: {}` 是常态),
|
||
* 用 record 会要求每一个语言键都存在。
|
||
*/
|
||
template: z.partialRecord(problemLanguageSchema, z.string()),
|
||
createTime: z.string(),
|
||
lastUpdateTime: z.string().nullable(),
|
||
timeLimit: z.number().int(),
|
||
memoryLimit: z.number().int(),
|
||
difficulty: maskedProblemDifficultySchema,
|
||
source: z.string().nullable(),
|
||
prompt: z.string().nullable(),
|
||
submissionNumber: z.number().int(),
|
||
acceptedNumber: z.number().int(),
|
||
statisticInfo: z.record(z.string(), z.unknown()),
|
||
contestId: z.number().int().nullable(),
|
||
tags: z.array(z.string()),
|
||
createdBy: z.object({
|
||
id: z.number().int(),
|
||
username: z.string(),
|
||
realName: z.string().nullable(),
|
||
}),
|
||
myStatus: z.number().int().nullable(),
|
||
myFailedCount: z.number().int(),
|
||
allowFlowchart: z.boolean(),
|
||
showFlowchart: z.boolean(),
|
||
mermaidCode: z.string().nullable(),
|
||
flowchartData: z.record(z.string(), z.unknown()).nullable(),
|
||
flowchartHint: z.string().nullable(),
|
||
sqlConfig: sqlConfigSchema.nullable(),
|
||
sqlDisplay: sqlDisplaySchema.nullable(),
|
||
// 代码要求(AST 规则的展示投影)。规则原文不下发给学生,见 astRequirementSchema
|
||
astRequirements: astRequirementsSchema.nullable(),
|
||
})
|
||
|
||
export type ProblemDetail = z.infer<typeof problemDetailSchema>
|
||
|
||
export const problemListItemSchema = z.object({
|
||
id: z.number().int(),
|
||
_id: z.string(),
|
||
title: z.string(),
|
||
submissionNumber: z.number().int(),
|
||
acceptedNumber: z.number().int(),
|
||
difficulty: maskedProblemDifficultySchema,
|
||
createdBy: sampleUserSchema,
|
||
tags: z.array(z.string()),
|
||
contestId: z.number().int().nullable(),
|
||
allowFlowchart: z.boolean(),
|
||
showFlowchart: z.boolean(),
|
||
hasAstRules: z.boolean(),
|
||
/** SQL 题(可选语言里有 SQL):写 SQL 语句查数据库,和写 C / Python 是两回事,列表里单独标出来 */
|
||
isSql: z.boolean(),
|
||
/** 0 = 做对了。画流程图的题,流程图评到 A / S 也给 0(和专题、课堂条一个口径) */
|
||
myStatus: z.number().int().nullable(),
|
||
/**
|
||
* 做对的人 / 交过的人,和题目页「统计」页签同一个口径(代码提交、不算还在判的)。
|
||
* 代替原来的提交数和按提交算的通过率:1044 按提交算只有 69%,按人算 817 / 826
|
||
*/
|
||
solvedUsers: z.number().int(),
|
||
triedUsers: z.number().int(),
|
||
})
|
||
|
||
export const problemListSchema = paginatedSchema(problemListItemSchema)
|
||
|
||
/**
|
||
* 相似题推荐的一条。`sharedTags` 是它和原题共有的标签(知识点在前、主题在后),
|
||
* 学生不点开就知道练的是什么
|
||
*/
|
||
export const similarProblemSchema = problemListItemSchema.extend({
|
||
sharedTags: z.array(z.string()),
|
||
})
|
||
|
||
/** 题目列表「全部类型」下拉:SQL / 有参考图 / 画流程图 / 语法要求 */
|
||
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'])
|
||
|
||
/**
|
||
* 题目列表顶上那行和左栏的进度。**只说做到了什么**:做对几道、这周新做对几道、
|
||
* 离哪个成就最近;没做对的只给一个数(「有 n 道差一点」),不列出来。
|
||
*/
|
||
export const problemProgressSchema = z.object({
|
||
/** 做对的题数,和成就的「AC 题目数」同一个数(不含比赛),这样「再做对 n 道」加得上 */
|
||
solved: z.number().int(),
|
||
/** 本周(东八区周一起)新做对几道 */
|
||
weekSolved: z.number().int(),
|
||
/** 连续几周都有新做对(到本周或上周为止;断了就是 0) */
|
||
weekStreak: z.number().int(),
|
||
/** 交过、还没做对的公开题。只在点「有 n 道差一点」时弹出来看,不常驻在页面上 */
|
||
almost: z.array(z.object({ _id: z.string(), title: z.string() })),
|
||
/** 标签 id → 这个标签下做对了几道(只数公开可见的题) */
|
||
byTag: z.record(z.string(), z.number().int()),
|
||
/** 离得最近的下一个成就;全拿到了、或者没有能提示的为 null */
|
||
next: z
|
||
.object({
|
||
name: z.string(),
|
||
icon: z.string(),
|
||
rarity: achievementRaritySchema,
|
||
metric: z.string(),
|
||
threshold: z.number().int(),
|
||
progress: z.number().int(),
|
||
})
|
||
.nullable(),
|
||
})
|
||
|
||
/**
|
||
* 标签分两类:`knowledge` 是知识点(循环结构、字符串……),`theme` 是题目的
|
||
* 题材(LOL、周杰伦、哈利波特……)。两类原来混在一起,题目列表上四十多个标签
|
||
* 挤成一片,学生想按知识点找题得在一堆游戏名里挑。新标签默认算知识点。
|
||
*/
|
||
export const tagCategorySchema = z.enum(['knowledge', 'theme'])
|
||
|
||
export const tagSchema = z.object({
|
||
id: z.number().int(),
|
||
name: z.string(),
|
||
category: tagCategorySchema,
|
||
problemCount: z.number().int().nonnegative(),
|
||
})
|
||
|
||
export const problemAuthorSchema = z.object({
|
||
username: z.string(),
|
||
problemCount: z.number().int().nonnegative(),
|
||
})
|
||
|
||
const failureCountSchema = z.object({ result: judgeStatusSchema, count: z.number().int() })
|
||
|
||
/**
|
||
* 题目页「统计」页签。全部**按人**算,不按提交条数 —— 原来的「通过数」是 AC 条数
|
||
* (3092 是 219 条、202 个人),「通过率」是按提交算的,学生看了不知道这题难不难。
|
||
* 原来的「击败用户」(分母是近两年所有登录过的人)、饼图、历年 AC 率一起去掉了。
|
||
*/
|
||
export const problemStatsSchema = z.object({
|
||
/** 比赛还没结束(又不是比赛管理员):别人的情况一律不给,下面的数字都是 0 / 空 */
|
||
locked: z.boolean(),
|
||
/** 交过的人 */
|
||
tried: z.number().int(),
|
||
solved: z.number().int(),
|
||
/** 做对的人用了几次:1 次 / 2–3 次 / 4 次以上 */
|
||
tries: z.object({ one: z.number().int(), few: z.number().int(), many: z.number().int() }),
|
||
/** 没通过的提交按结果分,多的在前(不含判题中) */
|
||
failures: z.array(failureCountSchema),
|
||
/** 我自己:交了几次、做对没有。没登录为 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(),
|
||
/**
|
||
* 老师:各班做这道题的情况,和数据统计「按班级汇总」**同一个口径**(只算学生的提交;
|
||
* 做完 / 班级人数、交了没对、没交、正确率、最近一次),交过的人多的在前。
|
||
* 两边数字要一模一样 —— 老师从这里点过去,看到的数「变了」就不会再信任何一边。
|
||
* 学生、比赛里为 null
|
||
*/
|
||
classes: z
|
||
.array(
|
||
z.object({
|
||
/** null = 没有班级的号(数据统计那边写「没有班级」) */
|
||
className: z.string().nullable(),
|
||
/** 班里的学生数(学生角色、没禁用);没有班级的那一行就是交过的人数 */
|
||
classSize: z.number().int(),
|
||
solved: z.number().int(),
|
||
/** 交了但没做对的人 */
|
||
unsolved: z.number().int(),
|
||
/** 班里还没交过的人 */
|
||
untouched: z.number().int(),
|
||
/** 提交正确率,分母不算还在判的;一条都没判完为 null */
|
||
correctRate: z.number().nullable(),
|
||
lastTime: z.string(),
|
||
}),
|
||
)
|
||
.nullable(),
|
||
})
|
||
|
||
export type AstRuleEngine = z.infer<typeof astRuleEngineSchema>
|
||
export type AstRule = z.infer<typeof astRuleSchema>
|
||
export type AstRules = z.infer<typeof astRulesSchema>
|
||
export type AstRequirement = z.infer<typeof astRequirementSchema>
|
||
export type AstRequirements = z.infer<typeof astRequirementsSchema>
|
||
export type Challenge = z.infer<typeof challengeSchema>
|
||
export type ProblemChallenges = z.infer<typeof problemChallengesSchema>
|
||
export type ChallengeMine = z.infer<typeof challengeMineSchema>
|
||
export type ChallengeItem = z.infer<typeof challengeItemSchema>
|
||
export type ProblemChallengeGroup = z.infer<typeof problemChallengeGroupSchema>
|
||
export type ChallengeList = z.infer<typeof challengeListSchema>
|
||
export type ProblemDifficulty = z.infer<typeof problemDifficultySchema>
|
||
export type ProblemListItem = z.infer<typeof problemListItemSchema>
|
||
export type ProblemList = z.infer<typeof problemListSchema>
|
||
export type SimilarProblem = z.infer<typeof similarProblemSchema>
|
||
export type ProblemTypeFilter = z.infer<typeof problemTypeFilterSchema>
|
||
export type ProblemListSort = z.infer<typeof problemListSortSchema>
|
||
export type ProblemProgress = z.infer<typeof problemProgressSchema>
|
||
export type Tag = z.infer<typeof tagSchema>
|
||
export type TagCategory = z.infer<typeof tagCategorySchema>
|
||
export type ProblemAuthor = z.infer<typeof problemAuthorSchema>
|
||
export type SqlConfig = z.infer<typeof sqlConfigSchema>
|
||
export type SqlDisplay = z.infer<typeof sqlDisplaySchema>
|
||
export type SqlDisplayTable = z.infer<typeof sqlDisplayTableSchema>
|
||
export type SqlDisplayColumn = z.infer<typeof sqlDisplayColumnSchema>
|
||
export type ProblemStats = z.infer<typeof problemStatsSchema>
|