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; + } + // ==================== 内部工具 ==================== /** 构造题干快照(抽题时刻固化,题目后续被改不影响存证) */