Files
lpt-meta/OPTIMIZATION_BACKLOG.md
T

24 KiB
Raw Blame History

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 改写该条,再开对应实现条目。

2. 状态总览

优先级 条目
P0 数据正确性 LPT-01、LPT-03 ~ LPT-05(原 LPT-02 已改为设计决策 D-01,编号不再复用)
P1 会挡路的功能缺口 LPT-06 ~ LPT-10
P2 AI 链路 LPT-11 ~ LPT-14
P3 检索与数据增长 LPT-15 ~ LPT-20
P4 安全与运维 LPT-21 ~ LPT-26

3. 已确认的设计决策(不是待办,不要当缺陷改)

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 由维护者澄清后重新归类。

D-02 遗忘判定阈值为 50 分钟(2 × WORK_DURATION

  • 依据StudySessionsEntity.isOverTime()minutes >= WORK_DURATION * 2;触发时按 lastStartTime + 25 分钟 截断有效时间。
  • 决策:25~50 分钟区间默认用户仍在屏幕前(番茄钟已提醒但可能正在收尾),不做截断;超过 50 分钟才判定为「忘了点暂停/结束」。
  • 注意:不要把这个阈值改成等于番茄钟长度(25 分钟);阈值存在的意义是区分「在学」与「忘了关」。真正要修的是执行路径不统一,见 LPT-01。

4. P0 数据正确性

LPT-01 遗忘检测只在「重新进入学习页面」这一条路径上执行

  • 未完成
  • 涉及仓库lpt-be(可选 lpt-fe 心跳)
  • 前提:系统已具备遗忘检测(见 D-02),问题不在于缺少机制,而在于它只挂在一条路径上。
  • 依据pausedStudySession(lastStartTime.plusMinutes(WORK_DURATION)) 只在 StudySessionsServiceImpl.startOrContinueStudySession() 中出现(全仓唯一)。而 pauseStudySession()StudySessionsEntity.endedStudySession()(内部 pausedStudySession(now))都是直接 effective += now - lastStartTime,没有遗忘检测。
  • 影响:页面不刷新、停留在前台时,pause / ended 两条路径依然可达。典型场景:番茄钟响起后用户离开,3 小时后回到同一个页面点「休息」(StartTask.vuerestTimer()stopTimer() → pause 接口),这 3 小时被全额计入有效学习时间。是否被截断取决于用户有没有刷新过页面。
  • 理由:这恰好落在 D-02 想覆盖的「忘了点暂停/结束」场景里,属于检测点遗漏,会让同一行为产生两种统计结果,直接损害时间数据的可信度。
  • 建议:抽出统一的结算时间函数(如 resolveSettlementTime(session)now - lastStartTime > 50min 时返回 lastStartTime + 25min,否则返回 now),在 pause / ended / 定时收敛三条路径共用;番茄钟长度与两类阈值统一配置化。
  • 验收:构造「ONGOING 且 last_start_time 为 3 小时前」的会话,分别调用 pause 与 ended 接口,断言 effective_time 被截断到 25 分钟;StudySessionsEntityTest 同步补用例。

LPT-03 isOverTime() 误报「超时 25 分钟」

  • 未完成
  • 涉及仓库lpt-be
  • 依据StudySessionsEntity.isOverTime() 只比较 last_start_time 与当前时间,不看 end_timesession_statestartOrContinueStudySession() 用同一判定既驱动截断又驱动 systemMessage
  • 影响:已暂停很久的会话(时间早已在暂停时正确入账),每次回到页面都会弹「上段学习任务已经超时 25 分钟,将仅计算为 25 分钟的有效学习时间」。
  • 建议:拆分「是否需要截断」与「是否需要提示」两个判定,并纳入 end_time / state
  • 验收:对 state=PAUSEDend_time 已存在的会话,接口不返回 systemMessage

LPT-04 结束会话不幂等,会产生重复学习报告

  • 未完成
  • 涉及仓库lpt-be
  • 依据StudySessionsEntity.endedStudySession()state = ENDED 时只 log.warn 并原样返回;StudySessionsServiceImpl.endedStudySession() 随后无条件 studyReportsMapper.insert(...)
  • 影响:重复点击、网络重试即可写入多条报告 → 复习 Feed 重复项、标准导图重复节点、统计重复计数。
  • 建议state != ENDED 才允许写报告,否则幂等返回;接口层可加幂等键。
  • 验收:对同一 sessionNum 连续调用两次结束接口,第二次不新增报告(断言报告条数)。

LPT-05 缺少服务端兜底:悬挂会话与超长会话无人收敛

  • 未完成
  • 涉及仓库lpt-be
  • 依据:全仓无 @Scheduled / EnableSchedulinggetActiveSession() 直接返回最新的 ONGOING/PAUSED 会话;遗忘检测只在用户主动访问该任务页面时执行(见 LPT-01)。
  • 影响
    • 会话可长期停留「进行中/暂停」,只能等用户回到该任务页才被被动修正;用户不访问时「今日有效学习时间」等统计是滞后或错误的。
    • 用户始终不回到该任务时,ONGOING 会话永久悬挂,并被 LPT-07 的活跃会话守卫用来阻塞新任务。
  • 建议
    1. 增加超长会话硬兜底:ONGOING 且 now - last_start_time 超过第二阈值(暂定 2 小时)时强制暂停、按遗忘规则(D-02)截断,并打异常标记,供前端提示「本次会话疑似忘记结束」。
    2. 收敛方式:惰性收敛(读取时收敛)+ 低频定时扫描兜底,二选一或并用。
    3. 三类阈值(25 分钟番茄钟、50 分钟遗忘判定、2 小时强制暂停)统一配置化,不要散落在实体常量里。
  • 验收:制造一个 3 小时前的 ONGOING 会话,在不访问该任务页面的前提下,惰性/定时收敛后状态为 PAUSED 且有效时间已按 D-02 截断;正常进行中的会话不受影响。

5. P1 会挡路的功能缺口

LPT-06 「误操作结束(零数据关闭会话)」文档已描述但代码未实现

  • 未完成
  • 涉及仓库lpt-docs + lpt-be+ lpt-fe
  • 依据lpt-docs/docs/concepts/study-session.mdlearning-loop.mdlearning-expectation.mdconcepts/README.md 均把该能力描述为已有设计;但 lpt-be/srclpt-fe/src 中检索「误操作」零命中,也不存在任何删除会话的端点。
  • 影响:误点「开始学习」后必须填写学习预期、必须走完整流程,最后留下一条 0 时间垃圾会话(再被 LPT-02 清零)。文档与实现漂移,代理读文档会得到错误结论。
  • 建议:二选一并明确记录——(a) 实现:仅当会话无知识碎片时可用,需输入确认语「我误操作导致开启了本次学习」,删除会话与学习预期且不产生报告;(b) 从 lpt-docs 撤回该能力描述。
  • 验收:文档与实现一致;若实现,需覆盖「有碎片时拒绝」「确认语错误时拒绝」两条负面用例。

LPT-07 活跃会话拦截没有出路,会永久堵住新任务

  • 未完成
  • 涉及仓库lpt-fe+ lpt-be 需提供结束/放弃能力)
  • 依据Study.vuestartTask() 在发现其他活跃会话后只提供「前往继续 / 取消」,取消分支为空函数,不跳转。
  • 影响:一个搁置数月的暂停会话会阻止开始任何新任务(所有任务入口都撞同一个守卫),用户必须先找回那个任务手动结束。
  • 建议:弹窗补充「结束它并开始新任务」或「放弃该会话」出口;与 LPT-06 的收敛/关闭能力复用。
  • 验收:存在陈旧活跃会话时,用户可在一次交互内开始新任务。

LPT-08 学习报告与知识碎片不可修订

  • 未完成
  • 涉及仓库lpt-be+ lpt-fe
  • 依据:端点清单——StudyReportFragments 只有 POST / PUT /{id} / GETStudyReports 只有创建与查询,没有 PUT、没有 DELETE;碎片也没有 DELETE。
  • 影响:写错不能改,也不能删除,只能改数据库。
  • 理由:知识碎片是学习报告、复习 Feed、标准导图的共同原料,记录成本上升会直接降低记录率,连锁削弱整个复习闭环。
  • 建议:报告支持编辑与删除;碎片支持删除;删除需考虑对标准导图、回忆记录、统计的影响(软删除 + 标记重建导图)。
  • 验收:报告/碎片可编辑与删除;删除后 Feed 与统计不再包含该条。

LPT-09 复习 Feed 只读,没有反馈动作

  • 未完成
  • 涉及仓库lpt-be+ lpt-fe
  • 依据:Feed 仅提供列表/详情/回忆对比;ReviewServiceImpl.reviewNeedWeight() 的智能权重只有两个信号——时间衰减(createdTime)与任务最近一次回忆覆盖率。
  • 影响:无法表达「这条其实很熟」与「这条完全没印象」;权重长期只有两个偏置,也没有「已复习」状态,同一批内容会反复出现。
  • 建议:加轻量动作(稍后再看 / 已掌握),把「上次复习时间」变成第三个权重信号;保持既有「无压力、偶遇式」设计,不引入强制清单。
  • 验收:用户反馈动作可落库并在下一次 Feed 排序中生效。

LPT-10 无数据导出

  • 未完成
  • 涉及仓库lpt-be+ lpt-fe
  • 依据:端点清单中没有任何导出能力。
  • 理由lpt-docs 明确系统定位是「辅助学习的工具」,工具型系统应保证数据可携带;同时可缓解备份粒度过粗的问题(见 LPT-25)。
  • 建议:导出学习报告、知识碎片、任务统计为 Markdown / JSON,按任务或时间范围筛选。
  • 验收:导出内容可独立阅读,包含报告/碎片原文与时间信息。

6. P2 AI 链路(收益成本比最高)

LPT-11 recallCompare 在事务中同步等待 AI,长事务占用数据库连接

  • 未完成
  • 涉及仓库lpt-be
  • 依据StandardMindMapServiceImpl.recallCompare() 标注 @Transactional,内部同步调用 AiServiceClient.compareRecall()(轮询上限 lpt.ai-service.timeout-seconds: 600)。Hikari 连接池未显式配置,为默认 10。
  • 影响:并发几个回忆对比即可耗尽连接池,导致全站请求阻塞。这是建议最先修的一条:把 AI 调用移出事务即可,改动小、风险低。
  • 建议:先做纯读的对比计算(事务外),再开短事务写入回忆记录;或引入任务表 + 异步回写(与 LPT-12 一起做)。
  • 验收:AI 调用期间不持有数据库连接(可通过连接池监控或长事务日志证明)。

LPT-12 AI 全链路同步阻塞,用户需干等最长 10 分钟

  • 未完成
  • 涉及仓库lpt-be + lpt-fe + lpt-ai
  • 依据lpt-fe/src/utils/request.tstimeout: 600000lpt-beAiServiceClient.submitAndWait() 内部轮询最长 600sGET /study-sessions/{sessionNum}/report-draft 这类 GET 也会触发 10 分钟阻塞并计费;ReviewRecall.vue 存在「AI 对比分析中,已等待 N 秒…」的分档文案。
  • 影响:页面只能干等;刷新或断开后请求丢失,AI 已消耗的 token 无法回收,结果也不落库。
  • 建议lpt-be 建 AI 任务表(提交后立即返回 taskId),前端轮询 lpt-be;结果回写后前端再取。生成期间允许离开页面再回来。
  • 验收:提交 AI 任务后接口在秒级返回任务标识;任务结果可重复获取,无需重试即可拿到。

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 个任务时耗时不再线性增长;可重试错误不产生失败结论。

LPT-14 防并发锁是 JVM 内存锁

  • 未完成
  • 涉及仓库lpt-be
  • 依据StandardMindMapServiceImplConcurrentHashMap<String, AtomicBoolean> generatingTasks
  • 影响:多副本部署时锁失效;「生成中」的重复请求会返回 queryByTaskNum() 的结果(可能为 null)而被前端当成成功。
  • 建议:改为数据库/分布式锁,并明确「生成中」的对外语义(返回任务状态而不是实体)。
  • 验收:多副本场景下同一任务不会并发生成;生成中重复请求返回明确的进行中状态。

7. P3 检索与数据增长

LPT-15 业务主表缺索引,而每条查询都被追加租户条件

  • 未完成
  • 涉及仓库lpt-be
  • 依据V20250629_1__LPT_dataBase_init.sqlstudy_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 显示关键查询走索引而非全表扫描。

LPT-16 Feed 使用 ORDER BY RAND()

  • 未完成
  • 涉及仓库lpt-be
  • 依据ReviewServiceImpl.getReviewFeed()getSmartFeed() 对报告表和碎片表分别使用 ORDER BY RAND() LIMIT ...smart 模式抽样量为 limit × 5(上限 500)。
  • 影响:MySQL 上属全表随机排序,单次请求两次全表随机。
  • 建议:改为 id 区间随机、预生成候选池或「按时间窗口随机 + 按需补充」。
  • 验收Feed 接口不产生全表随机排序(EXPLAIN 可验证)。

LPT-17 取「每任务最近一次回忆覆盖率」时拉取全部回忆记录

  • 未完成
  • 涉及仓库lpt-be
  • 依据ReviewServiceImpl.latestRecallRatioByTask()taskNum in (...) 查询全部记录(含体积较大的 compare_result JSON)后只在内存里取每条任务的第一条。
  • 影响:数据传输量与内存占用随复习次数线性增长。
  • 建议:改为分组取最新(子查询/MAX(created_time)),或把最近覆盖率冗余到任务表/导图表。
  • 验收:该查询返回行数等于任务数而非回忆记录数。

LPT-18 统计聚合在内存中完成且无时间范围

  • 未完成
  • 涉及仓库lpt-be
  • 依据ReviewServiceImpl.fillReviewTaskStats() 加载所有任务的全部会话到内存再聚合,无时间窗、无分页。
  • 影响:使用越久越慢,且会随会话数增长而内存膨胀。
  • 建议:下推为 SQL 聚合(含今日/本周条件),必要时物化统计表。
  • 验收:统计接口加载的会话行数与任务数同阶,不随历史会话数线性增长。

LPT-19 Feed 返回全文

  • 未完成
  • 涉及仓库lpt-be + lpt-fe
  • 依据ReviewFeedItem.content 直接返回报告/碎片全文,getReviewFeed(100, "smart") 一次最多 100 条;前端只展示前几十个字(详情页才需要全文)。
  • 建议:Feed 返回摘要字段(含长度标记),详情接口按需拉全文。
  • 验收:Feed 响应体积显著下降且展示行为不变。

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 安全与运维

LPT-21 数据库凭证明文入库

  • 未完成
  • 涉及仓库lpt-be + lpt-infra
  • 依据application-prod.ymlapplication-dev.ymlapplication-local.yml 均包含明文数据库账号密码(三个环境同一份)。lpt-infra 已通过 lpt-secrets 注入 SPRING_DATASOURCE_PASSWORD 覆盖,但配置文件仍在 Git 历史中。
  • 建议:轮换凭证 → 配置文件只保留占位符/环境变量引用 → 清理历史(或至少视为已泄露处理)。
  • 验收:仓库中不再出现明文凭证;本地开发通过环境变量或本地未跟踪配置提供。
  • 注意不要把任何密码值复制进本文档。

LPT-22 Actuator 端点鉴权边界未固定

  • 未完成
  • 涉及仓库lpt-be
  • 依据:线上只读探测确认 /api/actuator/health/api/actuator/info 未登录返回 200,而 /api/actuator/env 与业务接口返回 401(已注册的 actuator 端点不受 SaInterceptor 约束)。
  • 影响:当前探针正常,但一旦放开 metrics/prometheus 等端点,会连带公开。
  • 建议:显式固定暴露白名单,或把管理端点放到独立端口并加网络策略。
  • 验收:未鉴权可访问的端点清单与预期完全一致。

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 任务。

LPT-24 生产环境打印全部 SQL

  • 未完成
  • 涉及仓库lpt-be
  • 依据application.ymlmybatis-plus.configuration.log-impl: org.apache.ibatis.logging.stdout.StdOutImpl(注释里保留了 Slf4jImpl 备选)。
  • 影响:生产日志包含全部 SQL 与参数(含学习内容),噪音大且不利于排查。
  • 建议:改为 Slf4jImpl 并按环境分级,生产默认不打印参数。
  • 验收:生产环境不再输出逐条 SQL。

LPT-25 备份只在集群内同一 PVC

  • 未完成
  • 涉及仓库lpt-infra
  • 依据mysql-backup-cronjob.yaml 每日 mysqldump --all-databases 写入同一集群的 mysql-backup PVC,保留 14 天,且 dev 与 prod 混在同一 dump。
  • 影响:集群级故障即全丢;无法按环境恢复。
  • 建议:增加异地/对象存储副本,按库分文件,并定期做恢复演练。
  • 验收:可在集群外取得可恢复的备份,并完成一次演练记录。

LPT-26 死代码与死表清理

  • 未完成
  • 涉及仓库lpt-be + lpt-fe
  • 依据TestController/test/{id})与 test_table 仍在生产代码中;review_mind_maps 表自 V20260620_1 创建后无任何代码引用也未 droplpt-fe/src/api/review.ts 仍导出 /review/mind-map/*(含 xmind 上传解析)相关调用,而后端已无对应端点。
  • 建议:确认无外部调用后删除端点/表/前端死代码;若保留,需在 lpt-docs 说明定位。
  • 验收:前后端不再存在无实现的接口调用;数据库无孤儿表。

9. 建议落地顺序

  1. LPT-11(AI 移出事务)—— 最小改动,避免全站卡死。
  2. LPT-01 / LPT-04(结算时间统一、结束幂等)—— 一批改完即修正时间数据口径,建议同时补用例。
  3. LPT-05 / LPT-06 / LPT-07(服务端收敛与 2 小时硬兜底、误操作结束的决定与实现、活跃会话出口)。
  4. LPT-12 / LPT-13AI 异步化与队列可靠性)。
  5. LPT-15 ~ LPT-18(索引与聚合下推)—— 数据量还小时成本最低。

10. 待确认的设计变更(确认前不要实施)

DC-01 内容归属与待归类

  • 文档lpt-docs/docs/concepts/knowledge-ownership.md(草案,含三个待决问题)、lpt-docs/docs/design/system-structure.md(设计总纲)
  • 背景:学习 A 内容时顺手记下的旁支知识(例:学 AI 时记下“1B = one billion”)目前会被算作 A 的知识,进 A 的导图与回忆对比基准。根因是把「记录地点」当成了「内容归属」。
  • 变更内容:拆分记录上下文(不可变,时间统计用)与内容归属(可变、单一、可为空);时间按会话统计、内容按归属聚合;新增待归类视图;归类动作挂在复习 Feed 里;AI 只给归属建议;待归类内容可升格为学习任务。
  • 影响范围:碎片表结构、标准导图取材、按任务的复习与检索(StandardMindMapServiceImpl.doGenerate()ReviewServiceImpl.getTaskReview()getTaskFragments() / getTaskReports())。时间与状态机不受影响。
  • 前置条件:需维护者确认草案文末三个待决问题(碎片是否可脱离会话、报告是否结构化、归属是否保持单一)。
  • 实施要求:一次替换干净,禁止出现“导图按归属、检索仍按会话”的双口径并存;归属变更后需标记导图待更新。

附录 A 核对方式(可复现)

  • 代码核对:阅读 lpt-beservice/impl/*entity/*controller/*config/*resources/application*.ymlresources/db/migration/*lpt-fe/srcutils/request.tscomponents/*api/*lpt-ai/src 全部;lpt-infradev/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/srcrg -n "ORDER BY RAND" lpt-be/srcrg -n "unique" lpt-be/src/main/resources/db/migration