# 结构化题目编辑器 + 应用题答案判分模式 - 设计文档 日期:2026-08-20 状态:已批准 ## 背景 `/parent/exams/[paperId]` 编辑页目前用**裸 JSON 文本域**录入题目,家长需手写 quiz JSON 数组(曾明确拒绝结构化表单,现转向)。同时用户对应用题判分提出新模式:孩子拍照上传解题过程 + 填写最终答案来判定对错(替代/补充现有 AI 逐评分点判分)。 现状事实(grilling 确认): - 现有 86 道种子题 = choice 48 + fill 30 + solve 8,**judge/order/match 零数据**。 - solve 题现状:孩子拍照 → 百炼 Qwen-VL 按 rubric 评分点逐点判分(ai-grade.ts,无 key 演示回退),照片与逐点判定进错题本。 - 孩子端 QuizBlock 已支持全部 6 题型答题 + 对错反馈;fill 题的参考答案本身是**数组**(answers)。 - 现有 svaeEqxamPaper 的 validateQuestionsJson 已做形状校验。 ## 需求(已与用户确认) 1. **结构化编辑器**:试卷题目改为逐题表单(题型联动字段),不再手写 JSON。题目卡片流:新增/编辑/删除/上移/下移。 2. **首发四种题型**:判断题/选择题/填空题/应用题(judge/order/match 暂不做,有数据时后置)。 3. **应用题新增判定模式** `judgeMode: "answer"`:家长只填**最终答案数组**(复用 fill 的 answers 语义)+ 分值;孩子拍照解题过程 + 填最终答案,**归一化匹配秒级判对错**,不调 AI。 4. **共存**:现有 rubric 模式(AI 逐点评分)保留,存量 8 道题零改动(`judgeMode` 缺省 = rubric)。 5. 孩子端此模式下:拍照必填(进错题本),最终答案判对错;对 = 满分,错 = 0 分(不做部分给分,可解释)。 ## 方案选择 - **方案 A(采用):结构化表单产出仍是 QuizQ JSON,存储/孩子端渲染零schema改动** - 编辑 UI 从 JSON textarea 换成题型联动表单;服务端校验扩展 judgeMode 分支;判分在现有 child 提交链路内按 judgeMode 分流。 - 方案 B(弃):引入独立"题目表"(questions 拆表)→ 迁移 86 题、错题本/组卷/判分全部重写,收益为零。 - 方案 C(弃):AI 判最终答案(取代归一化)→ 有延迟、依赖 key、判定无法对孩子解释;归一化数组对家庭场景足够。 ## 设计 ### 1. QuizQ 扩展(src/lib/quiz-parse.ts) ```ts // solve 新增可选字段(缺省即 rubric 老模式,向后兼容): judgeMode?: "rubric" | "answer"; // judgeMode==="answer" 时复用: answers?: string[]; // 最终答案数组(与 fill 同字段语义,等价答法都填) // points 两种模式下都必须(分值);reference 为展示用参考解答 ``` ### 2. 归一化判定 lib(新建 src/lib/answer-match.ts) ```ts // xuchao: 答案等价判定——数值类归一化(分数/小数/百分数互转),文本类去空格/全角/大小写 export function normalizeAnswer(s: string): string; export function answersMatch(expected: string[], given: string): boolean; // 任一期望答案等价即中 ``` - 纯数字语义(`1/2`→`0.5`、`50%`→`0.5`、`0.50`→`0.5`,去尾零)互转比较。 - 含字母/单位词(如 `10个`、`across`)按文本归一(去空白、全角→半角、小写)严格比较;等价形式靠家长在答案数组里补填。 - **fill 现有判分不动**(grammar.ts `gradeOne`:trim+小写精确匹配)——归一化仅用于 judgeMode==="answer" 的应用题,避免改变既有周测/记录判分行为。 ### 3. 孩子端判分流(exam/actions.ts takeExam + QuizBlock) - `takeExam` 中 solve 题分流: - `judgeMode==="answer"`:`answersMatch(q.answers, 孩子填的答案)` → score=points(对)/0(错);判定与照片写 `solveSnap`(`hits:[{ point:"最终答案", score, hit, reason }]`),照片 OSS 照常上传。 - 缺省/`rubric`:走现有 ai-grade 逐点评分,零改动。 - QuizBlock solve 分支:answer 模式下拍照必填 + 显示最终答案 Input;rubric 模式现状不变。 - 错题本:**零改动**(solveSnap 结构兼容;answerText 已对 solve 显示拍照与得分)。 ### 4. 家长端结构化编辑器(/parent/exams/[paperId] 重构) - PaperForm 拆为:试卷 MetaForm(现有字段,不动)+ `QuestionList`(题目卡片区)。 - QuestionList: - 每题卡片:题型徽标 + 题干摘要 + 操作(编辑/上移/下移/删除)。 - 「+ 新增题目」按钮;题型下拉(judge/choice/fill/solve 四项)。 - QuestionForm 按 type 联动: - **judge**:题干 + 解析 + 对/错单选 - **choice**:题干 + 选项列表(增/删/编辑,单选正确答案)+ 解析 - **fill**:题干 + 参考答案数组(增/删/编辑)+ 解析 - **solve**:题干 + 分值 + judgeMode 开关(rubric → 评分点列表(point/score)+ 参考解答;/ answer → 最终答案数组 + 参考解答可选)+ 解析 - 非法时客户端给出对应提示(与服务端同文案);保存仍走现有 `saveExamPaper`(JSON 序列化 QuizQ 数组)。 - 服务端 validateQuestionsJson 扩展: - solve:rubric 模式沿用现行(reference + points 必填);answer 模式要求 `answers` 非空数组 + points 必填,reference 可选;answer 模式不得带 rubric。 - 其余题型校验不变。 ### 5. 不改动 - DB schema(quiz JSON 存储不变)、ai-grade.ts、错题本 WrongRow、积分/达标规则、周测组卷。 - 孩子端之外:seed、overview、记录管理全不动。 ## 测试清单(手工,项目无自动化测试框架) 1. 编辑页:JSON 文本域消失,题目卡片流可新增/编辑/删除/上下移/保存。 2. 四种题型表单字段联动正确;应用题的 rubric/answer 开关切换字段正确。 3. 新建 answer 应用题(答案 0.5 与 1/2 都填):孩子端拍照 + 填 `1/2` → 判对满分;填 `2/3` → 判错 0 分,错题本含照片与判定。 4. 存量 rubric 题:孩子照旧拍照 AI 逐点评分,进错题本逐点判定。 5. 服务端校验:answer 模式缺 answers / rubric 模式缺 reference → 对应报错不保存。 6. 部署 jump.hn12301.net 后线上复验。