Files
lpt-meta/OPTIMIZATION_BACKLOG.md
T

408 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# LPT 实现优化清单
> 面向接手实现的代理。每条都是**功能性问题**(数据正确性、能力缺失、性能、安全),不含外观类改进。
> §3 列出已确认的**设计决策**:它们是有意为之的行为,不要当作缺陷修改。
## 0. 基线信息
| 项目 | 核对基线 |
|------|---------|
| 生成时间 | 2026-09-14 |
| 代码基线 | `lpt-be` c961157 / `lpt-fe` e66bdb7 / `lpt-ai` 200f088 / `lpt-infra` 0a23075 / `lpt-docs` 9cb9c75 |
| 核对方式 | 通读上述仓库源码 + 对线上 prod/dev 做只读探测(见附录 A |
若某仓库 HEAD 已明显晚于上表,先确认问题是否已被修复,再动手。
## 1. 协作约定
- 完成一项后:勾选对应条目并追加一行「完成于 <仓库> <commit>」;若条目语义已被改动,直接重写条目,不要追加开发日志。
- 涉及概念/设计语义的改动(如会话生命周期、有效时间定义):必须同步更新 `lpt-docs` 的概念或设计文档;`lpt-docs` 只写概念与设计,不写实现过程。
- 本文档不记载开发过程,只保留「待办 + 依据 + 验收标准」。禁止写入任何密钥、密码、Token 明文。
- 条目粒度按「一个代理能独立完成并自测」切分,跨仓库条目已标注涉及仓库。
- §3「设计决策」不是待办:改动前必须先与维护者确认,禁止当成缺陷直接「修复」。若某项决策需要调整,先在 §3 改写该条,再开对应实现条目。
- 编号规则:条目编号以中文领域前缀开头,形如 `复习-02`,前缀必须让人一眼看出改动落在哪块(会话、复习、报告、归属、导出、AI、数据、安全、运维、清理);设计决策用 `会话-决策01` 这种形式。编号不复用,旧编号作为别名保留在条目首次出现处(如 `复习-02(原 LPT-09`)。
## 2. 改动计划总览(按模块)
按模块给设计和决策。`状态` 含义:**可直接实施**(方案已定,不需维护者再拍板)/**待决策**(结论落地前不要开工)/**依赖其他条目**。
| 模块 | 条目 | 优先级 | 状态 | 需要先定什么 |
|------|------|--------|------|--------------|
| 学习会话生命周期 | 会话-01 遗忘检测路径不统一 | P0 | 可直接实施 | — |
| | 会话-02 `isOverTime()` 误报超时 | P0 | 可直接实施 | — |
| | 会话-03 结束会话不幂等 | P0 | 可直接实施 | — |
| | 会话-04 悬挂会话无收敛 | P0 | 待决策 | 收敛方式(惰性 / 定时扫描 / 并用)+ 2 小时硬兜底阈值 |
| | 会话-05 活跃会话拦截无出路 | P1 | 待决策 | 出口形式(结束并开始新任务 / 放弃会话),建议与会话-04 同批 |
| 内容归属与报告 | 归属-01 内容归属与待归类 | 待实施 | 设计已确认 | 实现级取舍:碎片表如何承载归属、报告字段拆分、归类交互落点 |
| | 报告-01 报告与碎片不可修订 | P1 | 待设计 | 与归属-01 同批改报告结构;修订与删除的边界(软删除、历史报告标记) |
| 标记与元信息 | 标记-01 统一标记存储 | P1 | 待决策 | 可挂对象;标记名受控方式;标记值类型;按标记检索;标记名治理与可见性 |
| 复习 | 复习-01 复习回路补全 | 待确认 | 待决策(6 项) | Feed 是否含报告;反馈动作形态;覆盖率口径;待归类呈现;遗漏项语义;排序可配置性 |
| | 复习-02 Feed 只读无反馈动作 | P1 | 依赖复习-01 | 反馈动作形态(复习-01 第 2 项) |
| | 复习-03 Feed 随机取样 | P3 | 待决策 | id 区间随机 / 候选池 / 时间窗口随机 |
| | 复习-04 覆盖率查询拉全量 | P3 | 依赖复习-01 | 覆盖率口径(复习-01 第 3 项) |
| | 复习-05 Feed 返回全文 | P3 | 依赖复习-01 | 条目单位与摘要口径(复习-01 第 1 项) |
| | 复习-06 学习引导 | P1 | 已确认待实施 | 依赖 AI-02 / AI-03,以及标记-01 的存储设计 |
| | 复习-07 复习耗时统计 | P2 | 可直接实施(口径已定) | — |
| AI 链路 | AI-01 事务内同步等 AI | P2 | 待决策 | 事务外纯读对比(小改)/ 任务表 + 异步回写 |
| | AI-02 全链路同步阻塞 | P2 | 可实施(方案已定) | 若 AI-01 选任务表方案,则与 AI-01 同批 |
| | AI-03 单 worker 串行无重试 | P2 | 可直接实施 | — |
| | AI-04 防并发锁是 JVM 内存锁 | P2 | 可直接实施 | 需确认「生成中」的对外语义(返回任务状态而非实体) |
| 数据与检索 | 数据-01 业务主表缺索引 | P3 | 可直接实施 | — |
| | 数据-02 统计聚合在内存 | P3 | 待决策 | SQL 聚合下推 / 物化统计表 |
| | 数据-03 编号撞号风险 | P3 | 待决策 | UUID/ULID / 加随机位 |
| 导出 | 导出-01 无数据导出 | P1 | 待决策(小) | 格式(Markdown / JSON)与范围(按任务 / 时间),默认方案可直接采用 |
| 安全 | 安全-01 数据库凭证明文 | P4 | 待决策 + 需你执行 | 是否轮换凭证;历史值清理还是视为已泄露 |
| | 安全-02 Actuator 鉴权边界 | P4 | 待决策 | 白名单 / 独立端口 + 网络策略(涉 `lpt-infra` |
| | 安全-03 AI 入站无防护 | P4 | 可直接实施 | 内部鉴权方式(共享 token / 仅网络策略) |
| | 安全-04 生产打印全部 SQL | P4 | 可直接实施 | — |
| 运维与清理 | 运维-01 备份只在同一 PVC | P4 | 待决策 | 异地 / 对象存储目标选型,需你提供目标 |
| | 清理-01 死代码与死表 | P4 | 待决策 | 删除 / 保留并在 `lpt-docs` 说明定位 |
已确认的设计决策(不是待办):会话-决策01 少于 10 分钟不计入总时长;会话-决策02 遗忘判定阈值 50 分钟。
---
## 3. 已确认的设计决策(不是待办,不要当缺陷改)
### 会话-决策01(原 D-01)少于 10 分钟的有效学习时间不计入总学习时间
- **依据**`StudySessionsEntity.endedStudySession()``effectiveTime < 10 * 60 → setEffectiveTime(0); setEffectivenessRatio(0)`;常量 `MIN_EFFECTIVE_TIME_SECONDS`
- **决策**:25 分钟的番茄钟已经足够碎片化,更短的碎片视为噪声,刻意不记录。
- **注意**:这与 `lpt-docs/design/principles.md` 中「量化,但不评判」存在张力,但已由维护者确认为有意取舍。除非维护者主动改变口径,否则不要改成「保留真实值 + 打标记」。
- **来源**:原 LPT-02 条目,2026-09-14 由维护者澄清后重新归类。
### 会话-决策02(原 D-02)遗忘判定阈值为 50 分钟(2 × `WORK_DURATION`
- **依据**`StudySessionsEntity.isOverTime()``minutes >= WORK_DURATION * 2`;触发时按 `lastStartTime + 25 分钟` 截断有效时间。
- **决策**:25~50 分钟区间默认用户仍在屏幕前(番茄钟已提醒但可能正在收尾),不做截断;超过 50 分钟才判定为「忘了点暂停/结束」。
- **注意**:不要把这个阈值改成等于番茄钟长度(25 分钟);阈值存在的意义是区分「在学」与「忘了关」。真正要修的是执行路径不统一,见 会话-01。
---
## 4. P0 数据正确性
### 会话-01(原 LPT-01)遗忘检测只在「重新进入学习页面」这一条路径上执行
- [ ] 未完成
- **涉及仓库**`lpt-be`(可选 `lpt-fe` 心跳)
- **前提**:系统已具备遗忘检测(见 会话-决策02),问题不在于缺少机制,而在于它只挂在一条路径上。
- **依据**`pausedStudySession(lastStartTime.plusMinutes(WORK_DURATION))` 只在 `StudySessionsServiceImpl.startOrContinueStudySession()` 中出现(全仓唯一)。而 `pauseStudySession()``StudySessionsEntity.endedStudySession()`(内部 `pausedStudySession(now)`)都是直接 `effective += now - lastStartTime`,没有遗忘检测。
- **影响**:页面不刷新、停留在前台时,pause / ended 两条路径依然可达。典型场景:番茄钟响起后用户离开,3 小时后回到同一个页面点「休息」(`StartTask.vue``restTimer()``stopTimer()` → pause 接口),这 3 小时被全额计入有效学习时间。是否被截断取决于用户有没有刷新过页面。
- **理由**:这恰好落在 会话-决策02 想覆盖的「忘了点暂停/结束」场景里,属于检测点遗漏,会让同一行为产生两种统计结果,直接损害时间数据的可信度。
- **建议**:抽出统一的结算时间函数(如 `resolveSettlementTime(session)``now - lastStartTime > 50min` 时返回 `lastStartTime + 25min`,否则返回 `now`),在 pause / ended / 定时收敛三条路径共用;番茄钟长度与两类阈值统一配置化。
- **验收**:构造「ONGOING 且 `last_start_time` 为 3 小时前」的会话,分别调用 pause 与 ended 接口,断言 `effective_time` 被截断到 25 分钟;`StudySessionsEntityTest` 同步补用例。
### 会话-02(原 LPT-03`isOverTime()` 误报「超时 25 分钟」
- [ ] 未完成
- **涉及仓库**`lpt-be`
- **依据**`StudySessionsEntity.isOverTime()` 只比较 `last_start_time` 与当前时间,不看 `end_time``session_state``startOrContinueStudySession()` 用同一判定既驱动截断又驱动 `systemMessage`
- **影响**:已暂停很久的会话(时间早已在暂停时正确入账),每次回到页面都会弹「上段学习任务已经超时 25 分钟,将仅计算为 25 分钟的有效学习时间」。
- **建议**:拆分「是否需要截断」与「是否需要提示」两个判定,并纳入 `end_time` / `state`
- **验收**:对 `state=PAUSED``end_time` 已存在的会话,接口不返回 `systemMessage`
### 会话-03(原 LPT-04)结束会话不幂等,会产生重复学习报告
- [ ] 未完成
- **涉及仓库**`lpt-be`
- **依据**`StudySessionsEntity.endedStudySession()``state = ENDED` 时只 `log.warn` 并原样返回;`StudySessionsServiceImpl.endedStudySession()` 随后无条件 `studyReportsMapper.insert(...)`
- **影响**:重复点击、网络重试即可写入多条报告 → 复习 Feed 重复项、标准导图重复节点、统计重复计数。
- **建议**`state != ENDED` 才允许写报告,否则幂等返回;接口层可加幂等键。
- **验收**:对同一 `sessionNum` 连续调用两次结束接口,第二次不新增报告(断言报告条数)。
### 会话-04(原 LPT-05)缺少服务端兜底:悬挂会话与超长会话无人收敛
- [ ] 未完成
- **涉及仓库**`lpt-be`
- **依据**:全仓无 `@Scheduled` / `EnableScheduling``getActiveSession()` 直接返回最新的 ONGOING/PAUSED 会话;遗忘检测只在用户主动访问该任务页面时执行(见 会话-01)。
- **影响**
- 会话可长期停留「进行中/暂停」,只能等用户回到该任务页才被被动修正;用户不访问时「今日有效学习时间」等统计是滞后或错误的。
- 用户始终不回到该任务时,ONGOING 会话永久悬挂,并被 会话-05 的活跃会话守卫用来阻塞新任务。
- **建议**
1. 增加超长会话硬兜底:ONGOING 且 `now - last_start_time` 超过第二阈值(暂定 2 小时)时强制暂停、按遗忘规则(会话-决策02)截断,并打异常标记,供前端提示「本次会话疑似忘记结束」。
2. 收敛方式:惰性收敛(读取时收敛)+ 低频定时扫描兜底,二选一或并用。
3. 三类阈值(25 分钟番茄钟、50 分钟遗忘判定、2 小时强制暂停)统一配置化,不要散落在实体常量里。
- **验收**:制造一个 3 小时前的 ONGOING 会话,在不访问该任务页面的前提下,惰性/定时收敛后状态为 PAUSED 且有效时间已按 会话-决策02 截断;正常进行中的会话不受影响。
---
## 5. P1 会挡路的功能缺口与待补能力
### 会话-05(原 LPT-07)活跃会话拦截没有出路,会永久堵住新任务
- [ ] 未完成
- **涉及仓库**`lpt-fe`+ `lpt-be` 需提供结束/放弃能力)
- **依据**`Study.vue``startTask()` 在发现其他活跃会话后只提供「前往继续 / 取消」,取消分支为空函数,不跳转。
- **影响**:一个搁置数月的暂停会话会阻止开始任何新任务(所有任务入口都撞同一个守卫),用户必须先找回那个任务手动结束。
- **建议**:弹窗补充「结束它并开始新任务」或「放弃该会话」出口;复用已实现的会话关闭能力(`POST /{sessionNum}/study-sessions/ended``.../abort`,后者仅限未产生学习残片的会话)。
- **验收**:存在陈旧活跃会话时,用户可在一次交互内开始新任务。
### 报告-01(原 LPT-08)学习报告与知识碎片不可修订
- [ ] 未完成
- **涉及仓库**`lpt-be`+ `lpt-fe`
- **依据**:端点清单——`StudyReportFragments` 只有 `POST` / `PUT /{id}` / `GET``StudyReports` 只有创建与查询,**没有 PUT、没有 DELETE**;碎片也没有 DELETE。
- **影响**:写错不能改,也不能删除,只能改数据库。
- **理由**:知识碎片是学习报告、复习 Feed、标准导图的共同原料,记录成本上升会直接降低记录率,连锁削弱整个复习闭环。
- **建议**:报告支持编辑与删除;碎片支持删除;删除需考虑对标准导图、回忆记录、统计的影响(软删除 + 标记重建导图)。
- **验收**:报告/碎片可编辑与删除;删除后 Feed 与统计不再包含该条。
### 复习-02(原 LPT-09)复习 Feed 只读,没有反馈动作
- [ ] 未完成
- **涉及仓库**`lpt-be`+ `lpt-fe`
- **依据**:Feed 仅提供列表/详情/回忆对比;`ReviewServiceImpl.reviewNeedWeight()` 的智能权重只有两个信号——时间衰减(`createdTime`)与任务最近一次回忆覆盖率。
- **影响**:无法表达「这条其实很熟」与「这条完全没印象」;权重长期只有两个偏置,也没有「已复习」状态,同一批内容会反复出现。
- **建议**:加轻量动作(稍后再看 / 已掌握),把「上次复习时间」变成第三个权重信号;保持既有「无压力、偶遇式」设计,不引入强制清单。
- **依赖**:动作形态需先确认 复习-01(§11)中的反馈动作取舍。
- **验收**:用户反馈动作可落库并在下一次 Feed 排序中生效。
### 导出-01(原 LPT-10)无数据导出
- [ ] 未完成
- **涉及仓库**`lpt-be`+ `lpt-fe`
- **依据**:端点清单中没有任何导出能力。
- **理由**`lpt-docs` 明确系统定位是「辅助学习的工具」,工具型系统应保证数据可携带;同时可缓解备份粒度过粗的问题(见 运维-01)。
- **建议**:导出学习报告、知识碎片、任务统计为 Markdown / JSON,按任务或时间范围筛选。
- **验收**:导出内容可独立阅读,包含报告/碎片原文与时间信息。
### 复习-06 学习引导
- [ ] 未完成
- **涉及仓库**`lpt-be` + `lpt-fe` + `lpt-ai`(概念文档已写入 `lpt-docs`
- **依据**:复习只有接触(Feed)与结构提取(回忆对比),概念边界上的误解(例如把“参数量”当成能决定磁盘占用)不会被覆盖率捕捉。
- **已定**:开放式问题引导(例:“1B 参数量的 LLM 模型,会占用多少磁盘空间?”),无标准答案、不判分;AI 在对话中纠正回答里隐含的边界问题;对话不设轮数上限;产出是知识碎片,由用户确认后记录;复用碎片全链路(归属、待归类、导图取材、Feed 回收);碎片默认归属“被引导概念所属的学习任务”,并带「来自学习引导」标记(仅标记,不做特殊处理);记录上下文为复习行为;耗时计入复习-07;不进入学习报告;入口为 Feed 卡片与导图节点;不提供降级形态,AI 不可用时该功能整体不可用。
- **前置条件**:依赖 AI-02 / AI-03(多轮对话不能建在当前同步阻塞的链路上);「来自学习引导」标记的存储依赖标记-01。
- **验收**:一次学习引导结束后,用户确认可生成一条归属正确、带来源标记的碎片,并随导图增量更新出现;不确认则不产生任何数据;AI 不可用时入口不可用,且不影响复习其他部分。
### 复习-07 复习耗时统计
- [ ] 未完成
- **涉及仓库**`lpt-be` + `lpt-fe`
- **依据**:复习总览目前只展示来自会话的“累计有效学习时长”(`ReviewTaskStats.effectiveTime`),复习行为本身没有任何计时,投入不可见。
- **已定口径**:只对回忆对比与学习引导计时,滚动浏览不计时;超过 5 分钟无交互则截断到上一次交互时间(复习没有暂停动作);命名“复习耗时”,不叫有效复习时间、不套用 10 分钟最小阈值;复习总览按任务与学习时长并列展示;不做跨任务汇总大数字、不设目标、不参与复习排序。
- **建议**:把复习行为作为时间记录的主体(回忆对比已有记录实体,学习引导新增),耗时写在该行为上;前端补空闲检测并上报交互时间。
- **验收**:完成一次回忆对比后,复习总览该任务行出现复习耗时且与学习时长分列;页面挂机超过 5 分钟不再继续累计。
---
## 6. P2 AI 链路(收益成本比最高)
### AI-01(原 LPT-11`recallCompare` 在事务中同步等待 AI,长事务占用数据库连接
- [ ] 未完成
- **涉及仓库**`lpt-be`
- **依据**`StandardMindMapServiceImpl.recallCompare()` 标注 `@Transactional`,内部同步调用 `AiServiceClient.compareRecall()`(轮询上限 `lpt.ai-service.timeout-seconds: 600`)。Hikari 连接池未显式配置,为默认 10。
- **影响**:并发几个回忆对比即可耗尽连接池,导致全站请求阻塞。**这是建议最先修的一条:把 AI 调用移出事务即可,改动小、风险低。**
- **建议**:先做纯读的对比计算(事务外),再开短事务写入回忆记录;或引入任务表 + 异步回写(与 AI-02 一起做)。
- **验收**:AI 调用期间不持有数据库连接(可通过连接池监控或长事务日志证明)。
### AI-02(原 LPT-12)AI 全链路同步阻塞,用户需干等最长 10 分钟
- [ ] 未完成
- **涉及仓库**`lpt-be` + `lpt-fe` + `lpt-ai`
- **依据**`lpt-fe/src/utils/request.ts``timeout: 600000``lpt-be``AiServiceClient.submitAndWait()` 内部轮询最长 600s`GET /study-sessions/{sessionNum}/report-draft` 这类 GET 也会触发 10 分钟阻塞并计费;`ReviewRecall.vue` 存在「AI 对比分析中,已等待 N 秒…」的分档文案。
- **影响**:页面只能干等;刷新或断开后请求丢失,AI 已消耗的 token 无法回收,结果也不落库。
- **建议**`lpt-be` 建 AI 任务表(提交后立即返回 taskId),前端轮询 `lpt-be`;结果回写后前端再取。生成期间允许离开页面再回来。
- **验收**:提交 AI 任务后接口在秒级返回任务标识;任务结果可重复获取,无需重试即可拿到。
### AI-03(原 LPT-13`lpt-ai` 单 worker 串行且无重试
- [ ] 未完成
- **涉及仓库**`lpt-ai`+ `lpt-be` 超时策略)
- **依据**`task-queue.ts` 只有一个 `processing` 标志顺序处理;失败直接置 `failed`,无退避重试;任务记录 `TTL_MS = 1 小时`
- **影响**:N 个并发请求时第 N 个需等待 N × LLM 耗时,超过 `lpt-be` 的 600s 超时后表现为「AI 实际成功、客户端已超时」,结果既不落库也不告知,用户重复点击导致重复计费。LLM 的 429/5xx 抖动被当成永久失败。
- **建议**worker pool(并发度对齐服务商限流)+ 429/5xx 退避重试 + 幂等键/结果缓存(同一导图、同一报告草稿的相同输入直接复用)。
- **验收**:并发 N 个任务时耗时不再线性增长;可重试错误不产生失败结论。
### AI-04(原 LPT-14)防并发锁是 JVM 内存锁
- [ ] 未完成
- **涉及仓库**`lpt-be`
- **依据**`StandardMindMapServiceImpl``ConcurrentHashMap<String, AtomicBoolean> generatingTasks`
- **影响**:多副本部署时锁失效;「生成中」的重复请求会返回 `queryByTaskNum()` 的结果(可能为 `null`)而被前端当成成功。
- **建议**:改为数据库/分布式锁,并明确「生成中」的对外语义(返回任务状态而不是实体)。
- **验收**:多副本场景下同一任务不会并发生成;生成中重复请求返回明确的进行中状态。
---
## 7. P3 检索与数据增长
### 数据-01(原 LPT-15)业务主表缺索引,而每条查询都被追加租户条件
- [ ] 未完成
- **涉及仓库**`lpt-be`
- **依据**`V20250629_1__LPT_dataBase_init.sql``study_sessions` / `study_reports` / `study_report_fragments` / `tasks` 均只有主键;`MybatisPlusConfig` 的租户拦截器给每条 SELECT/UPDATE/DELETE 追加 `created_by = ?`;全仓仅少数 review 表有索引。
- **影响**:所有高频查询(Feed、统计、按会话取碎片/报告)都是全表扫描 + 过滤,随使用量线性劣化。
- **建议**:按实际查询补复合索引,例如 `(created_by, task_num, session_state)``(created_by, session_num, created_time)``(created_by, created_time)``(created_by, task_num, calculated_priority)`
- **验收**`EXPLAIN` 显示关键查询走索引而非全表扫描。
### 复习-03(原 LPT-16Feed 使用 `ORDER BY RAND()`
- [ ] 未完成
- **涉及仓库**`lpt-be`
- **依据**`ReviewServiceImpl.getReviewFeed()``getSmartFeed()` 对报告表和碎片表分别使用 `ORDER BY RAND() LIMIT ...`smart 模式抽样量为 `limit × 5`(上限 500)。
- **影响**:MySQL 上属全表随机排序,单次请求两次全表随机。
- **建议**:改为 id 区间随机、预生成候选池或「按时间窗口随机 + 按需补充」。
- **验收**:Feed 接口不产生全表随机排序(`EXPLAIN` 可验证)。
### 复习-04(原 LPT-17)取「每任务最近一次回忆覆盖率」时拉取全部回忆记录
- [ ] 未完成
- **涉及仓库**`lpt-be`
- **依据**`ReviewServiceImpl.latestRecallRatioByTask()``taskNum in (...)` 查询全部记录(含体积较大的 `compare_result` JSON)后只在内存里取每条任务的第一条。
- **影响**:数据传输量与内存占用随复习次数线性增长。
- **建议**:改为分组取最新(子查询/`MAX(created_time)`),或把最近覆盖率冗余到任务表/导图表。
- **验收**:该查询返回行数等于任务数而非回忆记录数。
### 数据-02(原 LPT-18)统计聚合在内存中完成且无时间范围
- [ ] 未完成
- **涉及仓库**`lpt-be`
- **依据**`ReviewServiceImpl.fillReviewTaskStats()` 加载**所有任务的全部会话**到内存再聚合,无时间窗、无分页。
- **影响**:使用越久越慢,且会随会话数增长而内存膨胀。
- **建议**:下推为 SQL 聚合(含今日/本周条件),必要时物化统计表。
- **验收**:统计接口加载的会话行数与任务数同阶,不随历史会话数线性增长。
### 复习-05(原 LPT-19Feed 返回全文
- [ ] 未完成
- **涉及仓库**`lpt-be` + `lpt-fe`
- **依据**`ReviewFeedItem.content` 直接返回报告/碎片全文,`getReviewFeed(100, "smart")` 一次最多 100 条;前端只展示前几十个字(详情页才需要全文)。
- **建议**:Feed 返回摘要字段(含长度标记),详情接口按需拉全文。
- **验收**:Feed 响应体积显著下降且展示行为不变。
### 数据-03(原 LPT-20)编号生成有撞号风险且无唯一约束
- [ ] 未完成
- **涉及仓库**`lpt-be`
- **依据**`GenerateNumTool.getNextSequence()` 使用 `System.currentTimeMillis() % 1_000_000`,格式为 `PREFIX-yyyyMMdd-<6位>``session_num` / `task_num` 上没有唯一索引(仅两张导图表有 `unique key`)。
- **影响**:同毫秒创建可能撞号;撞号后 `getOneOpt` 会因多行结果抛异常。
- **建议**:改用 UUID/ULID 或加随机位,并对业务编号加唯一约束。
- **验收**:并发创建 N 个任务/会话无重复编号,数据库层有唯一约束保护。
---
## 8. P4 安全与运维
### 安全-01(原 LPT-21)数据库凭证明文入库
- [ ] 未完成
- **涉及仓库**`lpt-be` + `lpt-infra`
- **依据**`application-prod.yml``application-dev.yml``application-local.yml` 均包含明文数据库账号密码(三个环境同一份)。`lpt-infra` 已通过 `lpt-secrets` 注入 `SPRING_DATASOURCE_PASSWORD` 覆盖,但配置文件仍在 Git 历史中。
- **建议**:轮换凭证 → 配置文件只保留占位符/环境变量引用 → 清理历史(或至少视为已泄露处理)。
- **验收**:仓库中不再出现明文凭证;本地开发通过环境变量或本地未跟踪配置提供。
- **注意**:**不要把任何密码值复制进本文档。**
### 安全-02(原 LPT-22Actuator 端点鉴权边界未固定
- [ ] 未完成
- **涉及仓库**`lpt-be`
- **依据**:线上只读探测确认 `/api/actuator/health``/api/actuator/info` 未登录返回 200,而 `/api/actuator/env` 与业务接口返回 401(已注册的 actuator 端点不受 `SaInterceptor` 约束)。
- **影响**:当前探针正常,但一旦放开 metrics/prometheus 等端点,会连带公开。
- **建议**:显式固定暴露白名单,或把管理端点放到独立端口并加网络策略。
- **验收**:未鉴权可访问的端点清单与预期完全一致。
### 安全-03(原 LPT-23`lpt-ai` 缺少入站防护(SSRF 与滥用)
- [ ] 未完成
- **涉及仓库**`lpt-ai`
- **依据**`POST /fetch-title` 由服务端抓取用户可控 URL(可探测内网/云元数据地址);`/ai/tasks``/admin` 无鉴权、无速率限制、无请求体上限;`admin/store.ts` 把完整 prompt 与学习内容明文写入 `data/logs.json`
- **建议**:抓取改为白名单/内网地址黑名单 + 协议与重定向校验;入站加鉴权与限流;`/admin` 加访问控制;日志中学习内容按需脱敏或缩短保留期。
- **验收**`/fetch-title` 无法访问内网与元数据地址;未授权请求无法提交 AI 任务。
### 安全-04(原 LPT-24)生产环境打印全部 SQL
- [ ] 未完成
- **涉及仓库**`lpt-be`
- **依据**`application.yml``mybatis-plus.configuration.log-impl: org.apache.ibatis.logging.stdout.StdOutImpl`(注释里保留了 `Slf4jImpl` 备选)。
- **影响**:生产日志包含全部 SQL 与参数(含学习内容),噪音大且不利于排查。
- **建议**:改为 `Slf4jImpl` 并按环境分级,生产默认不打印参数。
- **验收**:生产环境不再输出逐条 SQL。
### 运维-01(原 LPT-25)备份只在集群内同一 PVC
- [ ] 未完成
- **涉及仓库**`lpt-infra`
- **依据**`mysql-backup-cronjob.yaml` 每日 `mysqldump --all-databases` 写入同一集群的 `mysql-backup` PVC,保留 14 天,且 dev 与 prod 混在同一 dump。
- **影响**:集群级故障即全丢;无法按环境恢复。
- **建议**:增加异地/对象存储副本,按库分文件,并定期做恢复演练。
- **验收**:可在集群外取得可恢复的备份,并完成一次演练记录。
### 清理-01(原 LPT-26)死代码与死表清理
- [ ] 未完成
- **涉及仓库**`lpt-be` + `lpt-fe`
- **依据**`TestController``/test/{id}`)与 `test_table` 仍在生产代码中;`review_mind_maps` 表自 `V20260620_1` 创建后无任何代码引用也未 drop`lpt-fe/src/api/review.ts` 仍导出 `/review/mind-map/*`(含 xmind 上传解析)相关调用,而后端已无对应端点。
- **建议**:确认无外部调用后删除端点/表/前端死代码;若保留,需在 `lpt-docs` 说明定位。
- **验收**:前后端不再存在无实现的接口调用;数据库无孤儿表。
---
## 9. 建议推进顺序(按模块)
同一模块内的条目触碰同一批表结构、口径或配置,建议一个模块一次性改完,避免出现双口径并存的中间态。
1. **学习会话生命周期**:会话-03(结束幂等)+ 会话-01(结算口径统一)一批,修正时间数据口径并补用例;再做会话-02;最后做会话-04 + 会话-05(需先给收敛方式与出口形式的决策)。
2. **内容归属与报告**:归属-01 与报告-01 合并一次改完碎片表与报告结构(碎片归属、报告四段结构、导图待更新标记、检索口径);标记-01(统一标记存储)随同定下,学习引导的来源标记依赖它。
3. **复习**:先落复习-01 的六项结论(口径与内容边界),再做复习-07(复习耗时,可独立先做)与复习-02 / 复习-03 / 复习-04 / 复习-05;复习-06(学习引导)依赖 AI-02 / AI-03 与标记-01,排在 AI 链路之后。
4. **AI 链路**:AI-01(先把 AI 移出事务)→ AI-02 + AI-03(异步任务与队列可靠性)→ AI-04(并发锁与生成中语义)。
5. **数据与检索 + 导出**:数据-01(补索引)→ 数据-02 / 数据-03,导出-01 可并行。
6. **安全与运维清理**:安全-01 ~ 安全-04、运维-01、清理-01,彼此独立,可并行推进。
---
## 10. 已确认的设计变更(待实施)
### 归属-01(原 DC-01)内容归属与待归类
- **文档**`lpt-docs/docs/concepts/knowledge-ownership.md`(设计已确认,见文末结论)、`lpt-docs/docs/concepts/learning-report.md`(结构化报告形态)、`lpt-docs/docs/design/system-structure.md`(设计总纲)
- **背景**:学习 A 内容时顺手记下的旁支知识(例:学 AI 时记下“1B = one billion”)目前会被算作 A 的知识,进 A 的导图与回忆对比基准。根因是把「记录地点」当成了「内容归属」。
- **变更内容**:拆分记录上下文(不可变、不可为空;取值是学习会话或复习行为)与内容归属(可变、单一、可为空);时间按行为统计(学习会话→学习时间,复习行为→复习耗时)、内容按归属聚合;新增待归类视图;归类动作挂在复习 Feed 里;AI 只给归属建议;待归类内容可升格为学习任务;学习报告改为结构化(会话摘要、预期达成判断、按归属分组的碎片、自由总结,旁支碎片留在本次报告单独标注)。
- **影响范围**:碎片表结构、学习报告结构(字段、AI 草稿生成与降级拼接、前端展示、历史自由文本报告兼容)、标准导图取材、按任务的复习与检索(`StandardMindMapServiceImpl.doGenerate()``ReviewServiceImpl.getTaskReview()``getTaskFragments()` / `getTaskReports()`)。学习会话的时间与状态机不受影响。
- **前置条件**:已满足—— 2026-09-19 维护者确认:碎片不可脱离学习会话;报告改为结构化并保留自由总结;归属保持单一,跨领域关联由标签承担。结论原文见 `knowledge-ownership.md`「已确认的结论」。
- **实施要求**:一次替换干净,禁止出现“导图按归属、检索仍按会话”的双口径并存;归属变更后需标记导图待更新。
---
## 11. 待确认的设计缺口(确认前不要实施)
### 复习-01(原 DC-02)复习回路补全
- **文档**`lpt-docs/docs/design/review-loop.md`(立场、内容来源、两个层级、排序、归类、反馈、AI 边界)、`lpt-docs/docs/concepts/review-overview.md`(新增)、`recall-review.md`(回忆记录、覆盖率、复习起点、遗漏项)、`review-feed.md``lpt-docs/docs/design/principles.md``module-boundaries.md`
- **背景**:复习此前只有零散描述(Feed、导图、回忆对比各自成文),缺少系统级口径:复习内容的完整来源、任务级与条目级两层入口、覆盖率与掌握度的定义、遗漏项的语义、反馈动作、归类动作落点、AI 边界都未定;文档写的是“Feed 展示碎片和学习报告”,实现只返回碎片。
- **本次补全(已按以下取向写入文档)**:复习只做“低阻力反复出现 + 主动提取”,不做到期调度、打卡、评分;Feed 条目单位是碎片,报告从任务记录进入;覆盖率按任务聚合成掌握度,作为排序信号之一;遗漏项是每次对比的快照,不做跨次累计待办;归类动作落在复习里;AI 只做语义匹配与归属建议。
- **待确认**
1. Feed 是否包含学习报告(文档取向:不含)。
2. 反馈动作形态:只读 /“已掌握 + 稍后再看”/ 仅“稍后再看”(关联 复习-02)。
3. 覆盖率口径:最近一次 / 最近若干次加权。
4. 待归类内容的呈现:总览独立分类 / Feed 混排打标;归类是否需在卡片上一键完成。
5. 遗漏项语义:快照 + 回看原文 / 跨次累计集合。
6. 排序信号是否对用户可配置(掌握度与时间效应的强度)。
- **关联实现**:复习-02(反馈动作)、复习-03(Feed 随机取样)、复习-04(覆盖率查询)、复习-05(Feed 摘要)。
### 标记-01 统一标记存储
- **文档**`lpt-docs/docs/concepts/marker.md`(新增,含 5 个待决问题)
- **背景**:来源标记、用户标签等弱信息分散在各功能里考虑,缺少统一存放位置;每新增一种标记就要改一次功能表结构。
- **已定**:标记统一存放,至少包含对象类型与标识、标记名与值、种类(系统标记 / 用户标签)、建立者与时间;标记不是状态——会话状态、报告可否修订、导图是否待更新这类流程判断留在主体上;学习引导产出的碎片带「来自学习引导」系统标记。
- **待确认**
1. 标记可挂在哪些对象上(碎片 / 报告 / 任务 / 会话)。
2. 标记名的受控方式(系统标记固定词表、用户标签自由输入;是否允许在系统标记上叠加)。
3. 标记值的类型(布尔开关 / 枚举 / 自由文本)。
4. 是否需要按标记检索的入口,以及需要哪些索引。
5. 标记名的治理(重命名、废弃)与对用户的可见性。
- **关联实现**:复习-06(来源标记)、归属-01(用户标签)。
---
## 附录 A 核对方式(可复现)
- 代码核对:阅读 `lpt-be``service/impl/*``entity/*``controller/*``config/*``resources/application*.yml``resources/db/migration/*``lpt-fe/src``utils/request.ts``components/*``api/*``lpt-ai/src` 全部;`lpt-infra``dev/``prod/``traefik/`
- 线上只读探测(仅 GET,未做任何写操作):
- `GET https://lpt.cat-shark.xyz/api/actuator/health` → 200
- `GET https://lpt.cat-shark.xyz/api/actuator/info` → 200
- `GET https://lpt.cat-shark.xyz/api/actuator/env` → 401
- `GET https://lpt.cat-shark.xyz/api/review/feed`(未登录)→ 401
- 结构性核对命令示例:`rg -n "@Scheduled" lpt-be/src``rg -n "ORDER BY RAND" lpt-be/src``rg -n "unique" lpt-be/src/main/resources/db/migration`