94 lines
9.4 KiB
Markdown
94 lines
9.4 KiB
Markdown
# 任务 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` 指纹集合 → 逐条校验(任何一条格式错误不影响其它有效题导入)→ 后台生成 UUID(`newId('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.conflict`(409)→ 无引用则过滤移除。
|
||
- `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.ts`:5 条路由 `GET /api/questions/list`、`POST /api/questions/import`、`GET /api/questions/export`、`DELETE /api/questions/:id`、`GET /api/questions/:id/refs`(tags: 题库管理)。
|
||
|
||
### 前端(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 4`,module 全为数量关系 |
|
||
| `GET /api/questions/list?difficulty=困难` | `total 4`,difficulty 全为困难 |
|
||
| `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/refs`(demo-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 的合法 JSON(Blob 下载逻辑已就绪,接口数据经 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}.png`、`task-07-entry-mode.png`、`task-07-entry-preview.png`、`task-07-entry-mobile.png`、`task-07-list-after-entry.png`、`task-07-import-modal.png`、`task-07-import-result.png`、`task-07-delete-confirm.png`、`task-07-delete-referenced.png`、`task-07-delete-unreferenced.png`、`task-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 请求不再误抛 400(server.ts `addContentTypeParser` 修复)。
|
||
- ✅ 页面(列表筛选/导入报告/导出/删除确认)双端渲染与原型结构对齐,无横向溢出。
|
||
|
||
## 当前结论
|
||
|
||
任务 07 完成:题库管理与导入导出在前后端打通并持久化。列表筛选分页、JSON 批量导入(去重 + UUID + 逐条校验报告)、导出(可再导入格式)、删除(引用校验 + 确认弹层)全部实测通过;修复了「空 body 的 JSON 请求误抛 400」的删除接口报错根因,并新增删除弹层引用预检(GET `/api/questions/:id/refs`,被引用题警示 + 禁用确认)。类型检查(vue-tsc / tsc)与生产构建(119 模块)全部通过。测试数据已清理,题库恢复演示基线 18 条。
|