6.9 KiB
6.9 KiB
结构化题目编辑器 + 应用题答案判分模式 - 设计文档
日期: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 已做形状校验。
需求(已与用户确认)
- 结构化编辑器:试卷题目改为逐题表单(题型联动字段),不再手写 JSON。题目卡片流:新增/编辑/删除/上移/下移。
- 首发四种题型:判断题/选择题/填空题/应用题(judge/order/match 暂不做,有数据时后置)。
- 应用题新增判定模式
judgeMode: "answer":家长只填最终答案数组(复用 fill 的 answers 语义)+ 分值;孩子拍照解题过程 + 填最终答案,归一化匹配秒级判对错,不调 AI。 - 共存:现有 rubric 模式(AI 逐点评分)保留,存量 8 道题零改动(
judgeMode缺省 = rubric)。 - 孩子端此模式下:拍照必填(进错题本),最终答案判对错;对 = 满分,错 = 0 分(不做部分给分,可解释)。
方案选择
- 方案 A(采用):结构化表单产出仍是 QuizQ JSON,存储/孩子端渲染零schema改动
- 编辑 UI 从 JSON textarea 换成题型联动表单;服务端校验扩展 judgeMode 分支;判分在现有 child 提交链路内按 judgeMode 分流。
- 方案 B(弃):引入独立"题目表"(questions 拆表)→ 迁移 86 题、错题本/组卷/判分全部重写,收益为零。
- 方案 C(弃):AI 判最终答案(取代归一化)→ 有延迟、依赖 key、判定无法对孩子解释;归一化数组对家庭场景足够。
设计
1. QuizQ 扩展(src/lib/quiz-parse.ts)
// solve 新增可选字段(缺省即 rubric 老模式,向后兼容):
judgeMode?: "rubric" | "answer";
// judgeMode==="answer" 时复用:
answers?: string[]; // 最终答案数组(与 fill 同字段语义,等价答法都填)
// points 两种模式下都必须(分值);reference 为展示用参考解答
2. 归一化判定 lib(新建 src/lib/answer-match.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:题干 + 分值 + 新建题固定「仅最终答案(秒判)」模式(最终答案数组 + 参考解答可选);存量 rubric 题只读徽标「AI 逐评分点判分(存量题,保留原评分点)」+ 参考解答(必填语义),不提供评分点编辑(2026-08-20 用户裁定:新题仅 answer,无判分模式开关)
- 非法时客户端给出对应提示(与服务端同文案);保存仍走现有
saveExamPaper(JSON 序列化 QuizQ 数组)。
- 服务端 validateQuestionsJson 扩展:
- solve:rubric 模式沿用现行(reference + points 必填);answer 模式要求
answers非空数组 + points 必填,reference 可选;answer 模式不得带 rubric。 - 其余题型校验不变。
- solve:rubric 模式沿用现行(reference + points 必填);answer 模式要求
5. 不改动
- DB schema(quiz JSON 存储不变)、ai-grade.ts、错题本 WrongRow、积分/达标规则、周测组卷。
- 孩子端之外:seed、overview、记录管理全不动。
6. 自动化测试框架(用户追加要求:没有就搭建)
- 框架:Vitest(TS 原生、零配置成本),+ jsdom + @testing-library/react 用于组件测试(QuestionForm/QuestionList 题型联动)。
- 关键可测逻辑沉淀为纯函数进 lib,不做 DB/网络依赖:
- 新增
src/lib/answer-match.ts(归一化/等价,已设计于 §2) - 现
validateQuestionsJson(parent/actions.ts 私有)→ 迁到新src/lib/question-validation.ts导出,saveExamPaper引用之(行为不变)
- 新增
- 测试覆盖:answer-match 全分支(数值/文本/单位/边界);question-validation 全题型 + judgeMode 新分支(accept/reject 用例);QuestionForm 题型联动渲染与非法输入提示。
- 脚本:
"test": "vitest run";vitest.config.ts 配@alias。 - CI:Gitea Actions 新增
.gitea/workflows/test.yaml,push 即跑npm ci && npm run test(runner 为 node:24-slim,已可用)。
测试清单(手工 + 自动化)
- 自动化:上述 Vitest 用例全绿;
npm run test通过。 - 手工:编辑页 JSON 文本域消失,题目卡片流可新增/编辑/删除/上下移/保存。
- 手工:四种题型表单字段联动正确;应用题的 rubric/answer 开关切换字段正确。
- 手工:新建 answer 应用题(答案 0.5 与 1/2 都填):孩子端拍照 + 填
1/2→ 判对满分;填2/3→ 判错 0 分,错题本含照片与判定。 - 手工:存量 rubric 题:孩子照旧拍照 AI 逐点评分,进错题本逐点判定。
- 手工:服务端校验:answer 模式缺 answers / rubric 模式缺 reference → 对应报错不保存。
- 部署 jump.hn12301.net 后线上复验。