From 37d07907a1370b7eb9b0a9facd9b13a8d368b8a2 Mon Sep 17 00:00:00 2001 From: qubiaobiao <3294694717@qq.com> Date: Mon, 28 Sep 2026 11:58:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(big-question):=20=E5=BD=95=E5=85=A5?= =?UTF-8?q?=E4=BE=A7=E9=98=B2=E5=91=86=E2=80=94=E2=80=94=E9=A2=98=E5=B9=B2?= =?UTF-8?q?=E4=B8=8D=E5=BE=97=E5=90=AB=E3=80=8C=E5=8F=AF=E7=9B=B4=E6=8A=84?= =?UTF-8?q?=E7=9A=84=E7=AD=94=E6=A1=88=E3=80=8D=EF=BC=88R27=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 背景(2026-09-28 测试人员实测反馈): 「答题时大题部分有参考答案。」初判怀疑前端未区分 / 评分系统未隔离, 实测两条均被证伪: · 前端 Competition.vue 仅渲染 description / sub_prompts,无答案区块; · 后端 buildSnapshot() / getForCandidate() 返回字段无 answer; · 建表 DDL 023-024 的 big_questions 无答案列; · 真实联调 question 字段 = [big_question_uid, description, score_per_sub, snapshot_at, sub_prompts, title, total_score]。 ⇒ 泄露 100% 来自**题库录入内容**:中职组启用题 id=38 的题干内嵌 「指令输入示例」(>OFFMSG,COM1*4A 等三条),而第 3 小问正是问这些指令。 为什么守点是录入入口: 抽题快照 question_snapshot **抽题即固化**(防纠纷设计)⇒ 事后改题库 **撤不回**已抽考生看到的版本;下发链路本身无泄漏点 ⇒ 只能在录入时拦。 改动: · 新增 detectAnswerLeak():扫 title / description / sub_prompt_1..3 ① 「参考答案 / 标准答案 / 评分标准 / 评分细则 / 答案要点」字样; ② 带**字面参数**的指令示例(>CMD,值*两位十六进制校验码)。 · validateQuestionPayload() 内调用 ⇒ create 与 update **同源**拦截; 命中返回 code=SUSPECT_ANSWER_LEAK,message 直接教会管理员怎么改。 · 兼容 > 与裸 > 两种存储形态。 边界(刻意为之,勿收紧): 格式定义(参数用方括号占位,如 >OFFMSG[,Port[,Message]]*CS)**不拦** —— 校验码限定「两位十六进制」天然排除占位符 *CS(S 非 hex)。 向考生提供指令**手册**是允许的;等同于答案的**成品**才不允许。 —— 与同日数据修复口径一致(只删示例、保留格式定义)。 实测(后端已重启加载新码,5/5 PASS): ① 合法格式定义题 → 放行(code=0,新增 1 行) ② 含指令示例 → 拒绝 HTTP200 code=400,**未落库** ③ 含「参考答案」字样 → 拒绝,未落库 ④ PUT 更新路径 → 同样拒绝,且原内容未被改写 ⑤ 清理复原:行数与 MAX(id) 回到基线 --- app/service/bigQuestion.js | 63 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/app/service/bigQuestion.js b/app/service/bigQuestion.js index f3a6cd9..b05582a 100644 --- a/app/service/bigQuestion.js +++ b/app/service/bigQuestion.js @@ -641,6 +641,13 @@ class BigQuestionService extends Service { subPrompts.push(String(v == null ? '' : v)); } + // ── 防呆:题干不得混入「可被直接抄袭的答案性内容」(2026-09-28 R27 缺陷加固)── + // 背景:中职组某启用题的题干里给了三条指令**输入示例**(>OFFMSG,COM1*4A 等), + // 而该题第 3 小问恰是「列出配置指令」⇒ 答案被题干直接给出;又因抽题快照固化 + // (question_snapshot 抽题即冻结)无法事后撤回,只能从**录入入口**前置拦截。 + const leak = this.detectAnswerLeak({ title, description, subPrompts }); + if (leak) return { ok: false, code: 'SUSPECT_ANSWER_LEAK', message: leak }; + const perRaw = src.score_per_sub !== undefined ? src.score_per_sub : (src.scorePerSub !== undefined ? src.scorePerSub : (existing ? existing.score_per_sub : 10)); @@ -672,6 +679,62 @@ class BigQuestionService extends Service { }; } + /** + * 防呆扫描:题干 / 小问里是否混入了「考生可直接抄袭的答案性内容」。 + * + * 背景(2026-09-28 实测缺陷):中职组某启用题的 `description` 内嵌 + * 「答题可能用到的指令集」,除格式定义外还给了**可直接使用的输入示例**: + * >OFFMSG,COM1*4A / >OUTMSG,COM1,GGA,1*3B / >SAVECONFIG*0B + * 而该题第 3 小问正是「请列出对该接收机做串口输出配置需下发的指令」⇒ 答案白送。 + * 前端只渲染 description、后端按快照原样下发 ⇒ 唯一守点是**录入入口**。 + * + * 判定规则(命中即拒绝保存): + * 1) 出现「参考答案 / 标准答案 / 评分标准 / 评分细则 / 答案要点」等字样; + * 2) 出现**带具体字面参数**的指令示例(形如 >CMD,字面值*两位十六进制校验码)。 + * + * ⚠️ 刻意**不拦**「指令格式定义」(参数用方括号占位,如 >OFFMSG[,Port[,Message]]*CS): + * 向考生提供指令**手册**是允许的,构成泄露的是**能照抄的成品**。 + * —— 本规则与 2026-09-28 那次修复(只删示例、保留格式定义)同口径。 + * + * @param {Object} parts - { title, description, subPrompts } + * @returns {string|null} 命中说明(直接展示给管理员);未命中返回 null + */ + detectAnswerLeak(parts) { + const targets = [ + ['大题标题', parts.title], + ['题干', parts.description], + ...((parts.subPrompts) || []).map((p, i) => [`第 ${i + 1} 小问`, p]), + ]; + const KEYWORDS = ['参考答案', '标准答案', '评分标准', '评分细则', '答案要点']; + // 带字面参数的指令示例:>OFFMSG,COM1*4A + // 校验码限定「两位十六进制」⇒ 天然排除占位符 *CS(S 非十六进制字符) + // 兼容两种存储形态:富文本存 >(实测库中形态)或裸 > + const CMD_SAMPLE = /(?:>|>)\s*[A-Z][A-Z0-9]*(?:,[^<&\n]*)?\*[0-9A-Fa-f]{2}(?![0-9A-Fa-f])/g; + + for (const [where, raw] of targets) { + const text = String(raw == null ? '' : raw); + if (!text) continue; + + for (const kw of KEYWORDS) { + if (text.includes(kw)) { + return `${where}中出现「${kw}」字样,疑似把参考答案写进了题目,已拒绝保存。`; + } + } + + CMD_SAMPLE.lastIndex = 0; + let m; + while ((m = CMD_SAMPLE.exec(text)) !== null) { + const hit = m[0]; + // 方括号占位(如 [,Port])属「格式定义」,不算示例 + if (hit.includes('[')) continue; + return `${where}中出现可直接抄袭的指令示例「${hit}」,已拒绝保存。` + + '如确需向考生提供指令手册,请只写**格式定义**(参数用方括号占位,例如:' + + '>OUTMSG[,Port],Message,Period*CS);带具体参数值的示例等同于给出答案。'; + } + } + return null; + } + // ==================== 内部工具 ==================== /** 构造题干快照(抽题时刻固化,题目后续被改不影响存证) */