diff --git a/DESIGN_REVIEW.md b/DESIGN_REVIEW.md new file mode 100644 index 0000000..747fc3a --- /dev/null +++ b/DESIGN_REVIEW.md @@ -0,0 +1,413 @@ +# LPT 复习流程设计文档 + +> 本文档描述 LPT 系统中「回忆复习」(Recall Review)功能的完整设计,包括数据模型、API、前端交互流程和三种入口的处理方式。 + +--- + +## 1. 核心概念 + +### 1.1 标准思维导图(Standard Mind Map) + +- **任务级单一知识树**:每个学习任务有且仅有一棵标准思维导图,存储在 `review_standard_mind_maps` 表 +- **生成方式**: + - `BUILTIN`:内置规则引擎,按 session 分组、去重、合并 + - `AI`:调用 lpt-ai 服务生成缩进大纲后解析 + - `USER`:用户手动编辑 + - `USER_MERGE`:增量合并后(保留用户编辑 + 追加 AI 新节点) +- **数据结构**:`content` 字段存储 JSON 树(递归 `MindMapNode`),`outline` 字段存储缩进大纲文本 +- **节点模型**: + ``` + MindMapNode { + title: string // 节点标题 + notes?: string // 备注 / 对比标记(MATCHED| / MISSED|) + sourceType?: string // REPORT | FRAGMENT | APPLICATION + sourceId?: number // 对应源数据主键 + children?: MindMapNode[] + } + ``` +- **节点标记约定**(用于对比着色): + - `notes` 以 `MATCHED|` 开头 → 绿色(回忆命中) + - `notes` 以 `MISSED|` 开头 → 红色(回忆遗漏) + - 无标记 → 默认色(未参与对比) + +### 1.2 回忆对比记录(Recall Record) + +- 每次用户提交回忆 → 生成一条 `review_recall_records` 记录 +- 核心字段: + - `task_num`:关联任务 + - `standard_map_id`:对比时使用的标准导图版本 + - `focus_path`:复习起点节点路径(`"根 / 分支A / 子节点B"`),null 为全量 + - `recall_content`:用户回忆大纲文本 + - `compare_result`:对比结果 JSON(含 `matchedTree`、`extraNodes`、覆盖率等) + - `recall_ratio` / `matched_count` / `missed_count` / `extra_count`:统计摘要 + +--- + +## 2. 复习流程 + +### 2.1 核心逻辑 + +``` +1. 确定复习起点(节点路径) +2. 获取标准导图 +3. 按起点提取子树作为对比基准 +4. 用户在大纲编辑器中回忆填充 +5. 提交对比(AI 语义匹配 或 BUILTIN 字符串匹配) +6. 展示对比结果(着色 + 统计) +``` + +### 2.2 节点选择 + +- 用户在标准导图上点击节点 → `MindMapViewer` 发射 `node-select` 事件(含 `title`、`path`、`childCount`) +- 弹窗确认:「以「节点名」为起点,复习其下 N 个子知识点?」 +- 确认后: + - `focusPath` = 选中节点的路径(如 `"Java基础 / 集合 / HashMap"`) + - `recallTree` 根节点标题 = 选中节点标题 + - 回忆导图以该标题为根,用户补充子节点 + +### 2.3 对比范围 + +- **有 `focusPath`**:`MindMapTreeTool.extractSubtree(root, focusPath)` 提取子树,对比基准 = 子树 +- **无 `focusPath`**:对比基准 = 整棵标准导图 +- AI 对比:发给 lpt-ai 的 outline 只包含子树范围 +- BUILTIN 对比:`compareTrees()` 在子树范围内做标题匹配 + +### 2.4 对比算法 + +**AI 语义匹配(优先):** +``` +POST /ai/tasks → type: "compare-recall" +→ 异步轮询结果 +→ 返回 { matches: [{standardTitle, recallTitle}], missedTitles, extraNodes, evaluation } +→ Java 端 buildCompareResultFromAI() 标注标准树 +``` + +**BUILTIN 字符串匹配(降级):** +- 展平标准树和回忆树 +- 标题标准化(去标点、去空格、转小写) +- 精确匹配 → 模糊匹配(bigram Jaccard ≥ 0.6) +- 标记 MATCHED / MISSED / 额外 + +### 2.5 结果展示 + +- 标准导图面板直接显示着色对比结果(不再有独立对比面板) +- 绿色(`#c8e6c9`)= 命中,红色(`#ffcdd2`)= 遗漏 +- 统计卡片:覆盖率、命中/遗漏/额外计数 +- 遗漏项列表 + 额外项列表(可点击回看原文) + +--- + +## 3. 三个复习入口 + +### 入口 A:复习总览 → "回忆复习" + +**路径:** `Welcome.vue` → "复习回看" → `Review.vue`(任务列表)→ 每行"回忆复习"按钮 + +**前端代码:** `Review.vue:110` +```typescript +router.push(`/review/recall/${row.taskNum}`) +``` + +**流程:** +1. 跳转到 `ReviewRecall.vue`,URL 为 `/review/recall/:taskNum` +2. 页面加载 → `loadData()` 获取标准导图 +3. 标准导图 `selectable=true`,显示提示「点击导图节点选择复习起点」 +4. 用户点击节点 → 弹窗确认 → 进入回忆模式 +5. 回忆填充 → 提交对比 → 展示结果 + +**相关文件:** +- `lpt-fe/src/components/Review.vue` +- `lpt-fe/src/components/ReviewRecall.vue` + +### 入口 B:从某个残片/报告详情 → "回忆复习" + +**路径:** `Welcome.vue` 滚动回顾 → 点击卡片 → `ReviewDetail.vue` → "回忆复习"按钮 + +**前端代码:** `ReviewDetail.vue` 的 `goToRecall()` +```typescript +const res = await findNode(taskNum, content.substring(0, 500)); +if (res?.data?.path) { + router.push(`/review/recall/${taskNum}?focusPath=${encodeURIComponent(res.data.path)}`); +} +``` + +**流程:** +1. 用户在详情页查看某个残片/报告的内容 +2. 点击"回忆复习" → 前端调 `POST /review/standard-mind-map/{taskNum}/find-node` +3. 后端 `findClosestNode()` 用 bigram Jaccard 匹配最接近的节点 +4. 返回 `{ path, nodeTitle, score }` +5. 跳转到 `/review/recall/:taskNum?focusPath=...` +6. `ReviewRecall.vue` 检测到 URL 参数 → 跳过节点选择,直接进入回忆模式 + +**后端 API:** +```http +POST /review/standard-mind-map/{taskNum}/find-node +Content-Type: application/json + +{ "content": "用户查看的残片/报告文本" } + +→ 200 OK +{ "code": 200, "data": { "path": "根 / 分支A / 子节点B", "nodeTitle": "子节点B", "score": 0.85 } } +``` + +**匹配算法(`MindMapTreeTool.findClosestNode`):** +1. 展平标准导图所有节点 +2. 对每个节点,计算其 `title` 与输入文本的 bigram Jaccard 相似度 +3. `notes` 字段也参与匹配(权重 0.5) +4. 返回得分最高(>0.1)的节点 + +**相关文件:** +- `lpt-fe/src/components/ReviewDetail.vue` +- `learning-progress-tracker/.../utils/MindMapTreeTool.java`(`findClosestNode`、`similarityScore`) + +### 入口 C:首页欢迎页 → 回忆卡片 → "前往回忆复习" + +**路径:** `Welcome.vue` → 滚动回顾标签 → 点击弹出回忆对话框 → "前往回忆复习"按钮 + +**前端代码:** `Welcome.vue` 的 `goToRecallFromCard()` +```typescript +const tn = group.taskNum; +const res = await findNode(tn, content.substring(0, 500)); +if (res?.data?.path) { + router.push(`/review/recall/${tn}?focusPath=${encodeURIComponent(res.data.path)}`); +} +``` + +**流程:** +1. Welcome 页加载 `GET /review/feed` 获取复习 feed +2. 按 session 分组为标签卡片 +3. 用户点击某个标签 → 弹出回忆对话框(先回忆→展开对照) +4. 点击"前往回忆复习" → 同入口 B,先调 findNode 再跳转 + +**相关文件:** +- `lpt-fe/src/components/Welcome.vue` + +--- + +## 4. 关键 API 清单 + +### 4.1 标准思维导图 + +| 方法 | 路径 | 说明 | +|------|------|------| +| `GET` | `/review/standard-mind-map/{taskNum}` | 获取/自动生成标准导图 | +| `POST` | `/review/standard-mind-map/{taskNum}/regenerate?mode=full\|incremental` | 重新生成 | +| `PUT` | `/review/standard-mind-map/{taskNum}` | 用户编辑(大纲文本) | +| `POST` | `/review/standard-mind-map/{taskNum}/recall` | 提交回忆对比 | +| `POST` | `/review/standard-mind-map/{taskNum}/find-node` | 匹配最近节点 | +| `GET` | `/review/standard-mind-map/{taskNum}/recall-records` | 历史回忆记录 | + +### 4.2 回忆对比请求体 + +```json +{ + "recallOutline": "用户回忆的缩进大纲文本\n - 子节点1\n - 子节点2", + "focusPath": "根标题 / 分支A / 子节点B" +} +``` + +- `recallOutline`:必填,用户回忆的缩进大纲 +- `focusPath`:可选,复习起点节点路径。null = 全量,提供时对比范围为该子树 + +### 4.3 对比结果结构(CompareResult) + +```json +{ + "matchedTree": { + "title": "根节点", + "notes": "", + "children": [ + { + "title": "命中的知识点", + "notes": "MATCHED|完整原文内容", + "sourceType": "REPORT", + "sourceId": 1, + "children": [] + }, + { + "title": "遗漏的知识点", + "notes": "MISSED|完整原文内容", + "sourceType": "FRAGMENT", + "sourceId": 2, + "children": [] + } + ] + }, + "extraNodes": [ + { "title": "用户额外回忆的知识", "path": "/额外/..." } + ], + "recallRatio": 0.75, + "matchedCount": 3, + "missedCount": 1, + "extraCount": 1, + "evaluation": "AI 评价文本(仅 AI 对比时有值)" +} +``` + +--- + +## 5. 前端组件结构 + +``` +ReviewRecall.vue(主页面) +├── Page header(返回、标题、历史记录按钮) +├── 复习起点指示(focusPath 显示 + 重选按钮) +├── 节点选择确认弹窗(el-dialog) +├── 统计卡片(覆盖率/命中/遗漏/额外) +├── 回忆历史列表 +├── 回忆输入(editable MindMapViewer) +├── 标准导图 + 对比结果(color-by-compare MindMapViewer) +│ ├── 未选择起点时 → selectable 模式 +│ ├── 已选择起点/已对比 → 着色展示 +│ └── 编辑模式 → editable 模式 +└── 遗漏项 + 额外项详情列表 +``` + +### 5.1 MindMapViewer 组件 Props + +| 属性 | 类型 | 默认 | 说明 | +|------|------|------|------| +| `tree` | `MindMapTreeNode \| null` | — | 树数据 | +| `editable` | `boolean` | `false` | 是否可编辑 | +| `colorByCompare` | `boolean` | `false` | 按 MATCHED/MISSED 着色 | +| `height` | `string` | `'480px'` | 画布高度 | +| `selectable` | `boolean` | `false` | 节点单击可选择 | +| `selectedPath` | `string` | `''` | 当前选中节点路径 | + +### 5.2 MindMapViewer 事件 + +| 事件 | 载荷 | 触发时机 | +|------|------|----------| +| `change` | `outline: string` | 编辑模式下结构变化 | +| `node-click` | `{ sourceType, sourceId }` | 点击带溯源标签的节点 | +| `node-select` | `{ title, path, childCount }` | selectable 模式下点击节点 | + +### 5.3 MindMapViewer 暴露方法 + +```typescript +defineExpose({ toOutline }) +// toOutline() → 缩进大纲文本 +``` + +--- + +## 6. 后端核心工具方法 + +### `MindMapTreeTool.java` + +| 方法 | 说明 | +|------|------| +| `extractSubtree(root, path)` | 按 "/" 分隔路径提取子树,返回深拷贝 | +| `findClosestNode(root, content)` | bigram Jaccard 匹配最近节点 | +| `getPath(root, targetTitle)` | 获取节点到根的路径字符串 | +| `similarityScore(a, b)` | 两段文本的 bigram Jaccard 相似度 | +| `mergeTrees(oldRoot, newRoot)` | 按标准化标题合并新旧树 | +| `toJson(root, mapper)` | 序列化为 JSON | +| `fromJson(json, mapper)` | 从 JSON 反序列化 | +| `toOutline(root)` | 树 → 缩进大纲 | +| `toFullOutline(root)` | 树 → 完整大纲(含根标题) | +| `parseOutline(text)` | 缩进大纲 → 树 | +| `flatten(root)` | 前序遍历展平 | +| `countNodes(root)` | 节点总数 | +| `maxDepth(root)` | 最大深度 | + +### `StandardMindMapServiceImpl.java` + +| 方法 | 说明 | +|------|------| +| `getOrGenerate(taskNum)` | 获取/自动生成标准导图 | +| `regenerate(taskNum)` | 全量重新生成(防并发) | +| `incrementalGenerate(taskNum)` | 增量合并(保留用户编辑) | +| `updateByOutline(taskNum, outline)` | 用户编辑 | +| `recallCompare(taskNum, outline, focusPath)` | 回忆对比 | +| `findNode(taskNum, content)` | 查找最近节点 | +| `listRecallRecords(taskNum)` | 历史记录 | +| `getRecallRecord(id)` | 单条记录详情 | + +--- + +## 7. 关键文件索引 + +### 前端(`lpt-fe/src/`) + +| 文件 | 说明 | +|------|------| +| `components/ReviewRecall.vue` | 回忆复习主页面 | +| `components/Review.vue` | 复习总览(任务列表+入口) | +| `components/ReviewDetail.vue` | 残片/报告详情页 | +| `components/Welcome.vue` | 首页(内含回忆卡片入口) | +| `components/MindMapViewer.vue` | 思维导图渲染/编辑/选择组件 | +| `api/standardMindMap.ts` | 标准导图 API 封装 | +| `api/review.ts` | 复习 feed API | +| `api/studySessions.ts` | 学习会话 API | + +### 后端(`learning-progress-tracker/src/main/java/.../`) + +| 文件 | 说明 | +|------|------| +| `controller/ReviewController.java` | 复习相关端点 | +| `service/StandardMindMapService.java` | 标准导图服务接口 | +| `service/impl/StandardMindMapServiceImpl.java` | 服务实现 | +| `service/impl/BuiltinMindMapGenerator.java` | 内置规则生成器 | +| `service/impl/RemoteAiMindMapClient.java` | AI 导图客户端 | +| `service/impl/AiServiceClient.java` | lpt-ai 通用客户端 | +| `utils/MindMapNode.java` | 树节点 DTO | +| `utils/MindMapTreeTool.java` | 树操作工具类 | +| `utils/CompareResult.java` | 对比结果 DTO | +| `entity/ReviewStandardMindMapEntity.java` | 标准导图实体 | +| `entity/ReviewRecallRecordEntity.java` | 回忆记录实体 | +| `db/migration/V20260706_1__add_focus_path_to_recall_records.sql` | focus_path 迁移 | + +### AI 服务(`lpt-ai/src/`) + +| 文件 | 说明 | +|------|------| +| `routes/ai.ts` | AI 任务提交/轮询/抓取标题 | +| `task-queue.ts` | 异步任务队列 | +| `llm/prompts.ts` | prompt 模板(generate-mind-map / compare-recall / aggregate-report) | +| `llm/client.ts` | LLM API 客户端 | +| `admin/routes.ts` | 管理面板 | + +--- + +## 8. 数据流图 + +``` +学习碎片/报告 + ↓ (按 session 分组) +BuiltinMindMapGenerator / RemoteAiMindMapClient + ↓ +标准思维导图(review_standard_mind_maps) + ↓ (用户选择节点) +extractSubtree(root, focusPath) + ↓ +子树(对比基准) + ↓ AI / BUILTIN 对比 ← 用户回忆大纲 + ↓ +CompareResult + ↓ (序列化存储 + 前端渲染) +review_recall_records + 着色 MindMapViewer +``` + +--- + +## 9. 历史版本记录 + +| Commit | 说明 | +|--------|------| +| `a636e51` | 新增 sessionNum 追踪 + 维度选择(已回退) | +| `e5adb40` | 前端会话选择器(已回退) | +| `3c3f682` | 防并发生成 + `mergeTrees` + 增量合 | +| `5a5a294` | 前端防抖/生成提示/移除冗余面板 | +| `84fe452` | **节点选择交互 + focusPath + findNode(当前)** | +| `b18f8b4` | **后端 focusPath + extractSubtree + 回退 session(当前)** | +| `ebbbeb4` | fix: Array.map 多参数 bug | + +--- + +## 10. 待办/已知问题 + +- [ ] AI 生成的导图(`RemoteAiMindMapClient`)因为没有 `sourceId`/`sourceType`,`node-click` 溯源不可用 +- [ ] `selectable` 模式下,点击无子节点的叶子节点,`childCount` = 0 的确认弹窗体验略奇怪 +- [ ] `findClosestNode` 的 bigram 相似度对短文本效果有限,可能需要 `notes` 加权优化 +- [ ] 欢迎页(Welcome.vue)的回忆卡片入口「前往回忆复习」按钮文字和位置可以优化