e66bdb79dc
完整描述复习功能的核心概念、流程设计、三个入口、 API 清单、组件结构、数据流图和文件索引。
414 lines
14 KiB
Markdown
414 lines
14 KiB
Markdown
# 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)的回忆卡片入口「前往回忆复习」按钮文字和位置可以优化
|