# 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. 协作约定 - 完成一项后:勾选对应条目并追加一行「完成于 <仓库> 」;若条目语义已被改动,直接重写条目,不要追加开发日志。 - 涉及概念/设计语义的改动(如会话生命周期、有效时间定义):必须同步更新 `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 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-16)Feed 使用 `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-19)Feed 返回全文 - [ ] 未完成 - **涉及仓库**:`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-22)Actuator 端点鉴权边界未固定 - [ ] 未完成 - **涉及仓库**:`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`。