gwy-exam/docs/checks/task-07.md

9.4 KiB
Raw Permalink Blame History

任务 07 检查记录:题库管理与导入导出

日期2026-09-01

交付内容

后端Handler + Schema + Routes

  • server/src/handlers/questions.ts
    • 新增 contentFingerprint():按「题干 + 模块 + 答案 + 选项文本(排序)」拼接(\u0000 分隔)生成内容指纹,用于导入去重。
    • list:按 module / difficulty / keyword 筛选 + page / pageSize 分页,返回 { items, total, page, pageSize }
    • importQuestions:读现有题库建立 seen 指纹集合 → 逐条校验(任何一条格式错误不影响其它有效题导入)→ 后台生成 UUIDnewId('q'))→ createdAt 取当前时间 → updateData 原子写入 → 返回 { total, success, skipped, failed, errors } 导入结果报告。
    • exportQuestions:导出 { version: '1.0', exportedAt, questions },剔除 id / createdAt,保证「导出的数据可再次导入」。
    • countReferences(id):共享的引用统计助手(错题/作答记录/刷题会话),供 refs 预检与 remove 校验共用。
    • refs(新增):GET /api/questions/:id/refs 返回该题引用数量,供前端删除弹层预检(题目不存在抛 404
    • remove:读 questions → 不存在抛 404 → countReferences 查引用 → 被引用抛 ApiError.conflict409→ 无引用则过滤移除。
  • server/src/errors.ts:新增 static conflict(message, details?)new ApiError(409, 'CONFLICT', ...)
  • server/src/server.ts修复删除接口 400 报错根因。默认 JSON 解析器遇到「Content-Type: application/json 但空 body」会抛 400「请求参数不合法」前端 openapi-fetch 给所有请求带该头,含无 body 的 DELETE。通过 addContentTypeParser('application/json', { parseAs: 'string' }, ...) 把空 body 视为 undefined,非空才 JSON.parse
  • server/src/routes.ts5 条路由 GET /api/questions/listPOST /api/questions/importGET /api/questions/exportDELETE /api/questions/:idGET /api/questions/:id/refstags: 题库管理)。

前端View + API

  • client/src/api/index.ts:新增类型 QuestionListItem / QuestionListPage / QuestionImportBody / QuestionExport / QuestionRefs(从 generated/schema 推导);questionsApi 新增 refs(id)
  • client/src/views/questions/QuestionsView.vue(重写,原为 ComingSoon 占位):「列表 / 手动录入」双模式,对照原型「桌面-题库录入.png」还原。
    • 手动录入模式(核心,对照原型):左表单 + 右实时题目预览。
      • 题型 tab单选题 / 多选题 / 判断题(需求 4.1 仅支持单选题,多选/判断点击保留单选并 toast 提示)。
      • 模块 / 考点下拉 + 输入、难度下拉、题干 textarea、A-D 选项行(点击序号标记正确答案)、答案解析。
      • 右侧「题目预览」实时联动:模块 / 难度徽章、题干、选项(正确项绿色 + ✓ 正确答案)、答案解析块。
      • 「保存题目并继续录入」复用 importQuestions(单条)保存 → toast + 表单自动清空。
      • 页头「返回列表」。
    • 列表模式:页头「导入真题」(新增题目)+「新增题目」;筛选栏(模块/难度/关键词防抖/重置 + 计数 + 导出);桌面表格、移动卡片、分页、删除确认弹层。
    • mode 开关控制列表 / 录入两视图。
    • 复用 AppPageHeader / AppCard / AppButton / AppIcon / AppBadge / AppModal / AppLoading / AppError / AppEmpty。

与原型图对照

原型「桌面-题库录入.png」仅一张主题是手动录入 + 实时预览,此前误做成纯列表页已修正。现页面在保留原有列表/筛选/导入/导出/删除基础上,新增手动录入模式,左表单 + 右预览与原型结构、字段(题型/模块考点/题干/选项/正确答案/解析)、「保存题目并继续录入」按钮一致。

接口实测curl演示数据 18 条)

接口 结果
GET /api/questions/list?page=1&pageSize=3 total 18,返回 3 条演示题
GET /api/questions/list?module=数量关系 total 4module 全为数量关系
GET /api/questions/list?difficulty=困难 total 4difficulty 全为困难
GET /api/questions/list?keyword=相遇 total 1
POST /api/questions/import(新题 1 条) {total:1, success:1, skipped:0, failed:0}total 18→19
POST /api/questions/import(重复内容再导) {total:1, success:0, skipped:1, failed:0}total 保持 19指纹去重
GET /api/questions/export version 1.0、19 条,字段 type/module/subModule/difficulty/stem/options/answer/analysis/tags/source无 id/createdAt(可再次导入)
GET /api/questions/:id/refsdemo-q-001 {wrongQuestions:0, practiceRecords:4, practiceSessions:0, total:4},供删除弹层预检
GET /api/questions/:id/refs(未引用新题) {wrongQuestions:0, practiceRecords:0, practiceSessions:0, total:0}
GET /api/questions/:id/refs(不存在的 id 404 NOT_FOUND:「题目不存在」
DELETE /api/questions/:id(带 Content-Type: application/json 头 + 空 body 修复前 400「请求参数不合法」→ 修复后 200 {success:true}server.ts addContentTypeParser 把空 body 视作 undefined
DELETE /api/questions/:id(未引用新题) {success:true}total 19→18
DELETE /api/questions/demo-q-001(被作答记录引用 4 条) 409 CONFLICT:「题目已被作答记录 4 条引用无法删除」total 不变

浏览器检查agent-browser

场景 结果
桌面 1440 题库管理 页头按钮 + 筛选栏 + 表格 + 分页完整渲染,与原型结构对齐;共 18 道题目
移动 390 题库管理 卡片布局渲染正常;scrollWidth 380 ≤ 390 无横向溢出
JSON 导入弹层 打开 → 粘贴 JSON → 开始导入 → 展示导入结果报告(成功 1 / 跳过 0 / 失败 0 + 全部导入成功)
重复导入去重实测 再导相同 JSON 显示「成功 0 · 跳过 1 · 失败 0」列表 total 不变
导出 exportQuestions 返回无 id/createdAt 的合法 JSONBlob 下载逻辑已就绪,接口数据经 curl 核对)
删除确认弹层 点击删除 → 弹层先展示「正在检查引用…」spinner随后题干预览 + 引用提醒文案正确
删除(被引用题) 打开弹层即预检:显示黄色警示块「该题已被引用,将无法删除 / 作答记录 4 条」+ 引用明细,确认按钮 disabled=true
删除(未引用题) 弹层显示「该题暂无关联引用,删除后不可恢复」,确认按钮可点,点击后 total 回落到演示基线 18
关键词搜索 输入「相遇」→ 共 1 道题目,刷新防抖生效
手动录入 → 新增题目 左表单 + 右「题目预览」实时联动(模块/难度徽章、题干、选项、答案解析均随输入更新)
手动录入 → 标记正确答案 点击选项 A 序号 → 预览 A 项绿色高亮 + ✓ 正确答案
手动录入 → 保存 toast「题目已保存可继续录入下一题」→ 表单自动清空 → 题库 total 18→19保存题含 q- UUID / module=数量关系 / subModule=行程问题 / answer=A / 4 选项 / 完整解析
手动录入 → 返回列表 「返回列表」回到列表视图,共 19 道题目
移动 390 手动录入 表单纵向单列,预览在表单下方;scrollWidth 380 ≤ 390 无横向溢出
数据复位 删除手动测试题后题库恢复演示基线 total=18

截图:deliverables/checks/task-07-questions-{desktop,mobile}.pngtask-07-entry-mode.pngtask-07-entry-preview.pngtask-07-entry-mobile.pngtask-07-list-after-entry.pngtask-07-import-modal.pngtask-07-import-result.pngtask-07-delete-confirm.pngtask-07-delete-referenced.pngtask-07-delete-unreferenced.pngtask-07-search-filter.png

完成标准核对

  • 导入文件不含 ID 也能成功(后台 newId('q') 生成 UUID
  • 重复题目被跳过(内容指纹去重,实测成功 0 / 跳过 1
  • 错误题目不影响有效题目导入(逐条 try/catch返回 errors 报告)。
  • 导出的数据可再次导入(导出剔除 id/createdAt字段与 import body 对齐)。
  • 删除前引用校验(错题/作答记录/刷题会话),被引用抛 409未引用删除成功。
  • 删除弹层引用预检(GET /api/questions/:id/refs):被引用题展示警示并禁用确认按钮,未引用题可直接删除。
  • 空 body 的 JSON 请求不再误抛 400server.ts addContentTypeParser 修复)。
  • 页面(列表筛选/导入报告/导出/删除确认)双端渲染与原型结构对齐,无横向溢出。

当前结论

任务 07 完成题库管理与导入导出在前后端打通并持久化。列表筛选分页、JSON 批量导入(去重 + UUID + 逐条校验报告)、导出(可再导入格式)、删除(引用校验 + 确认弹层)全部实测通过;修复了「空 body 的 JSON 请求误抛 400」的删除接口报错根因并新增删除弹层引用预检GET /api/questions/:id/refs,被引用题警示 + 禁用确认。类型检查vue-tsc / tsc与生产构建119 模块)全部通过。测试数据已清理,题库恢复演示基线 18 条。