feat(题目): 学生题目页恢复「要求」展示,走新的 astRequirements 字段
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
旧后端的 ProblemSerializer.Meta.exclude 没排掉 ast_rules,学生拿到的是规则原文,
题目页上那块「要求」(「if 条件 出现 2 次」之类)是显示的。阶段 3 泄露评审刻意
收掉了它,只留 hasAstRules 布尔值 —— 报告里当时就写了:
「新的更安全,但如果 ojnext 有地方读 ast_rules 的具体内容(比如提示"必须用
for 循环"),需要补个专门的字段。」
前端确实有(ProblemContent.vue 的 astRulesForDisplay + 整块渲染),但那个字段
一直没补。结果是这块在 OJ2 上**永远拿不到数据**,代码还在、没人报错。12 道题
受影响;学生只有提交失败后才能从 statistic_info.ast_results 里看到要求。
现在按评审自己的建议补上:oj 侧题目详情(含比赛题)多一个 astRequirements,
**只有渲染要用的两个字段**:
{ description: "if 条件 出现 2 次", kind: "count" }
文案由后端生成,engine / target 这些内部字段不出现在响应里 —— 收紧保留,
展示恢复。实测响应里搜不到 "engine" 也搜不到 "if_statement"。
## 顺带合掉一堆重复
描述文案原来有两份几乎一样的实现:判题机 ast.ts 里一份(写进 ast_results)、
ProblemContent.vue 里一份(题目页用)。现在统一成 ast.ts 的 describeAstRule,
两边共用。差异只在 min/max 同时给出时的措辞(生产库里没有这种规则,实际输出
逐字不变),另外判题机现在也能用上节点类型的中文名 —— 没写 label 的规则以前
判题结果里显示 `必须使用 function_definition`,现在是 `必须使用 函数定义`。
节点类型中文名那 15 条原来在 AstRulesEditor.vue(下拉 options)和
ProblemContent.vue(NODE_TARGET_LABELS)各手抄一份,收进契约的
AST_NODE_TARGET_LABELS,编辑器的下拉现在从它生成。
前端删掉 NODE_TARGET_LABELS / ruleDescription / ruleTagType 共 ~80 行,
`Problem` 类型里那个 oj 侧根本不下发的 astRules 幽灵字段也去掉了。顺带修掉
一处潜在重复渲染:原来 message 非空时 ruleDescription 返回 message、模板里
又单独渲染一次 message(生产库 message 全是空串,所以没露出来)。
## 一个坑:契约里差点搞出循环引用
astRequirements 一开始放在 admin.ts,problem.ts 去 import 它 —— 而 admin.ts
本来就 import problem.ts。**tsc 一声不吭地过了**,运行时才炸:
ReferenceError: Cannot access 'astRequirementsSchema' before initialization
所以整块 AST schema 从 admin.ts 挪到了 problem.ts(本来也是题目域的东西),
admin.ts 反过来从那边取。这类环 tsc 抓不到,加跨文件 schema 引用时得实跑一次。
## 验证
tsc(apps/api) 0 error、check:routes 168 条无遮蔽、vue-tsc 0 error、build 通过。
起服务实打:
- 把生产库那条规则种进本地库,匿名请求 /api/problems/1001:astRequirements 是
`{"Python3":[{"description":"if 条件 出现 2 次","kind":"count"}, ...]}`,
响应里没有 astRules、没有 engine、没有 if_statement。
- 浏览器打开题目页,「要求」那块活了:`要求 | if 条件 出现 2 次 | else 子句 出现 2 次`。
- 后台编辑页展开「代码规则检查」,两条规则正常渲染成
`出现次数 | if 条件 | 精确`,下拉选项(现在从契约生成)正确。
- 直接调 describeAstRule / checkAst / astRequirements:三种 kind 分类正确,
engine 认不出的规则被丢掉,null 和非对象都回落成 null。
- oj 侧 3 页 + 后台 4 页走查无重定向、console 无报错。
冒烟改动已还原(problem 2 的 ast_rules 复位成 null)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,12 @@ import { achievementRaritySchema } from "./achievement"
|
||||
import { rankProfileSchema } from "./account"
|
||||
import { paginatedSchema, sampleUserSchema } from "./common"
|
||||
import { reactionKeySchema } from "./content"
|
||||
import { problemDifficultySchema, sqlConfigSchema, sqlDisplaySchema } from "./problem"
|
||||
import {
|
||||
astRulesSchema,
|
||||
problemDifficultySchema,
|
||||
sqlConfigSchema,
|
||||
sqlDisplaySchema,
|
||||
} from "./problem"
|
||||
|
||||
/**
|
||||
* 后台侧的契约。与 oj 侧分开放:同一张表在两侧下发的字段集通常不同
|
||||
@@ -43,9 +48,6 @@ export const createAnnouncementRequestSchema = z.object({
|
||||
|
||||
export const updateAnnouncementRequestSchema = createAnnouncementRequestSchema
|
||||
|
||||
export type AstRuleEngine = z.infer<typeof astRuleEngineSchema>
|
||||
export type AstRule = z.infer<typeof astRuleSchema>
|
||||
export type AstRules = z.infer<typeof astRulesSchema>
|
||||
export type ProblemSample = z.infer<typeof problemSampleSchema>
|
||||
export type ProblemAnswer = z.infer<typeof problemAnswerSchema>
|
||||
export type ProblemTestCaseScore = z.infer<typeof problemTestCaseScoreSchema>
|
||||
@@ -569,49 +571,6 @@ export const problemTestCaseScoreSchema = z.object({
|
||||
score: z.coerce.number().int().min(0),
|
||||
})
|
||||
|
||||
/**
|
||||
* 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",
|
||||
// 判题机实现了,但后台编辑器还没有对应的选项,目前只能手工造数据用上
|
||||
"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(),
|
||||
/** 展示用的中文名,缺省回落到 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(),
|
||||
})
|
||||
|
||||
/** 按语言分组:`{ Python3: [...], C: [...] }`,键是 languages 里的语言名 */
|
||||
export const astRulesSchema = z.record(z.string(), z.array(astRuleSchema))
|
||||
|
||||
/** 后台题目详情:包含 oj 侧永不下发的 answers / testCase* / astRules */
|
||||
export const adminProblemSchema = z.object({
|
||||
|
||||
@@ -55,6 +55,94 @@ export const sqlDisplaySchema = z.object({
|
||||
]),
|
||||
})
|
||||
|
||||
/**
|
||||
* 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",
|
||||
// 判题机实现了,但后台编辑器还没有对应的选项,目前只能手工造数据用上
|
||||
"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(),
|
||||
/** 展示用的中文名,缺省回落到 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(),
|
||||
})
|
||||
|
||||
/** 按语言分组:`{ Python3: [...], C: [...] }`,键是 languages 里的语言名 */
|
||||
export const astRulesSchema = z.record(z.string(), z.array(astRuleSchema))
|
||||
|
||||
/**
|
||||
* 节点类型的中文名。后台编辑器的下拉选项、后端生成要求文案都要用它 ——
|
||||
* 原来在 AstRulesEditor.vue(下拉 options)和 ProblemContent.vue
|
||||
* (NODE_TARGET_LABELS)各存了一份同样的 15 条。
|
||||
*/
|
||||
export const AST_NODE_TARGET_LABELS: Record<string, string> = {
|
||||
for_loop: "for 循环",
|
||||
while_loop: "while 循环",
|
||||
if_statement: "if 条件",
|
||||
else_clause: "else 子句",
|
||||
function_definition: "函数定义",
|
||||
return: "return 语句",
|
||||
break: "break 语句",
|
||||
continue: "continue 语句",
|
||||
list_comprehension: "列表推导式",
|
||||
list_literal: "列表",
|
||||
dict_literal: "字典",
|
||||
set_literal: "集合",
|
||||
f_string: "f-string",
|
||||
try_except: "try-except",
|
||||
class_definition: "类定义",
|
||||
}
|
||||
|
||||
/**
|
||||
* 下发给**学生**的代码要求。只有渲染要用的两个字段 —— 文案由后端生成,
|
||||
* 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),
|
||||
)
|
||||
|
||||
export const problemDetailSchema = z.object({
|
||||
id: z.number().int(),
|
||||
_id: z.string(),
|
||||
@@ -98,6 +186,8 @@ export const problemDetailSchema = z.object({
|
||||
flowchartHint: z.string().nullable(),
|
||||
sqlConfig: sqlConfigSchema.nullable(),
|
||||
sqlDisplay: sqlDisplaySchema.nullable(),
|
||||
// 代码要求(AST 规则的展示投影)。规则原文不下发给学生,见 astRequirementSchema
|
||||
astRequirements: astRequirementsSchema.nullable(),
|
||||
})
|
||||
|
||||
export type ProblemDetail = z.infer<typeof problemDetailSchema>
|
||||
@@ -138,6 +228,11 @@ export const yearlyAcSchema = z.object({
|
||||
acRate: z.number(),
|
||||
})
|
||||
|
||||
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 ProblemDifficulty = z.infer<typeof problemDifficultySchema>
|
||||
export type ProblemListItem = z.infer<typeof problemListItemSchema>
|
||||
export type ProblemList = z.infer<typeof problemListSchema>
|
||||
|
||||
Reference in New Issue
Block a user