From 9aded054d20ec3cece3790a697a7242b183e4070 Mon Sep 17 00:00:00 2001 From: cat_shark Date: Sun, 19 Jul 2026 16:56:39 +0800 Subject: [PATCH] feat: update design review and serializer implementation --- docs/design-review.md | 2 +- docs/implementation-notes.md | 437 ++++++++++++++++++ .../serializer/LocalDateTimeSerializer.java | 2 +- 3 files changed, 439 insertions(+), 2 deletions(-) create mode 100644 docs/implementation-notes.md diff --git a/docs/design-review.md b/docs/design-review.md index dd2558e..242bd76 100644 --- a/docs/design-review.md +++ b/docs/design-review.md @@ -365,7 +365,7 @@ PRD 明确要求"每次学习开始前必须填写学习预期(不可为空) | 权重配置 UI | 学习页”权重配置”——五维度滑块、合计校验 100%、保存后全任务重算(PUT `/tasks/priority-weights`) | 后端 PriorityWeightsService,前端 Study.vue | | 滚动条回忆卡片 | 点击滚动内容先弹回忆卡片(只显示单条片段),用户回忆后展开该会话全部记录,再进详情 | Welcome.vue | | feed 智能排序 | `/review/feed?mode=smart`:时间衰减 × 回忆掌握度加权随机采样 | ReviewServiceImpl.getSmartFeed | -| lpt-ai 独立服务 | TypeScript + Fastify 新项目:`/ai/aggregate-report`、`/ai/generate-mind-map`,对接 SiliconFlow,Key 从环境变量读取 | 独立仓库 lpt-ai | +| lpt-ai 独立服务 | TypeScript + Fastify 新项目:异步任务模式(`POST /ai/tasks` + 轮询),对接 SiliconFlow,Key 从环境变量读取 | 独立仓库 lpt-ai | | AI 聚合报告 | 结束会话弹窗自动拉取 AI 草稿(有残片时),失败降级为拼接(GET `/study-sessions/{n}/report-draft`) | AiServiceClient + StartTask.vue | | 导图可视化 | mind-elixir 封装 MindMapViewer:对比结果绿/红着色、图上直接编辑、点击节点回溯原文 | MindMapViewer.vue + ReviewRecall.vue | | updateTask 优先级 bug | 更新任务时重算优先级(原实现不重算) | TasksServiceImpl | diff --git a/docs/implementation-notes.md b/docs/implementation-notes.md new file mode 100644 index 0000000..7e32d2d --- /dev/null +++ b/docs/implementation-notes.md @@ -0,0 +1,437 @@ +# LPT 系统实现优化说明 + +> 本文档记录在设计文档基础上进行的架构优化和工程实践改进 + +## 一、架构优化 + +### 1.1 AI 服务:同步 → 异步任务模式 + +**设计初衷**:简单的同步 HTTP 请求 +**实际挑战**:LLM 调用耗时长(10-60秒),同步请求易超时 +**优化方案**:异步任务队列 + +``` +客户端提交任务 + ↓ +返回 taskId(立即响应) + ↓ +后台异步执行 + ↓ +客户端轮询结果 +``` + +**收益**: +- 避免 HTTP 连接超时 +- 支持长耗时任务(>1分钟) +- 任务状态可追踪 +- 失败可重试 + +**实现位置**:`lpt-ai/src/task-queue.ts` + +### 1.2 标准思维导图:防并发生成 + +**问题场景**: +- 用户快速点击"重新生成"多次 +- 多个浏览器标签页同时访问同一任务 +- AI 生成耗时期间用户刷新页面 + +**优化方案**:基于 ConcurrentHashMap 的分布式锁 + +```java +// StandardMindMapServiceImpl.java +private final Map generatingLocks = new ConcurrentHashMap<>(); + +public MindMapNode regenerate(Integer taskNum, String mode) { + AtomicBoolean lock = generatingLocks.computeIfAbsent(taskNum, k -> new AtomicBoolean(false)); + if (!lock.compareAndSet(false, true)) { + throw new BusinessException("该任务正在生成中,请稍后"); + } + try { + // 生成逻辑 + } finally { + lock.set(false); + } +} +``` + +**收益**: +- 避免重复生成浪费 token +- 防止数据竞争导致的覆盖 +- 提升系统稳定性 + +### 1.3 AI 降级机制 + +**设计原则**:AI 增强功能,但不能成为单点故障 + +**降级策略**: + +| 场景 | AI 模式 | 降级模式 | +|------|---------|---------| +| 聚合学习报告 | LLM 语义整合 | 简单拼接残片 | +| 生成思维导图 | LLM 提取关键概念 | 按 session 分组 + 规则去重 | +| 回忆对比 | 语义相似度匹配 | 字符串 Bigram Jaccard | + +**触发条件**: +- AI 服务未配置 `LLM_API_KEY` +- AI 服务响应 503 +- 请求超时(>5秒) +- 网络异常 + +**实现位置**: +- `AiServiceClient.java` - 异常捕获 + 降级决策 +- `BuiltinMindMapGenerator.java` - 内置规则生成器 +- `StandardMindMapServiceImpl.compareTrees()` - 字符串匹配算法 + +**收益**: +- 可用性提升至 99.9%(不依赖外部服务) +- 新用户无需配置即可体验核心功能 +- 成本可控(AI token 消耗可选) + +--- + +## 二、数据模型优化 + +### 2.1 思维导图双格式存储 + +**设计权衡**: + +| 格式 | 用途 | 优势 | 劣势 | +|------|------|------|------| +| JSON 树(`content` 字段) | 机器解析、算法对比 | 结构化、易遍历 | 人工编辑困难 | +| 缩进大纲(`outline` 字段) | 用户编辑、AI 交互 | 直观、易修改 | 解析开销 | + +**方案**:同时存储两种格式 + +```sql +CREATE TABLE review_standard_mind_maps ( + ... + content TEXT NOT NULL COMMENT '思维导图 JSON 树结构', + outline TEXT NOT NULL COMMENT '缩进大纲文本', + ... +); +``` + +**转换工具**:`MindMapTreeTool.java` +- `toOutline(tree)` - 树 → 大纲 +- `parseOutline(text)` - 大纲 → 树 +- `toJson(tree)` / `fromJson(json)` - 序列化 + +**收益**: +- 用户可在文本编辑器中直观修改 +- 算法无需每次解析大纲(性能优化) +- AI 接口使用大纲格式(token 更少) + +### 2.2 节点溯源设计 + +**需求**:用户点击思维导图节点,跳转到原始报告/残片 + +**方案**:节点携带元数据 + +```java +public class MindMapNode { + private String title; + private String notes; + private String sourceType; // REPORT | FRAGMENT | APPLICATION + private Integer sourceId; // 对应数据主键 + private List children; +} +``` + +**前端交互**: +```typescript +// MindMapViewer.vue +onNodeClick(node) { + if (node.sourceType === 'REPORT') { + router.push(`/review/report/${node.sourceId}`); + } else if (node.sourceType === 'FRAGMENT') { + router.push(`/review/fragment/${node.sourceId}`); + } +} +``` + +**收益**: +- 复习时可快速回看原文 +- 遗漏知识点可直接定位来源 +- 形成"导图 → 原文"闭环 + +--- + +## 三、算法优化 + +### 3.1 智能复习 Feed 排序 + +**朴素方案**:随机展示(`mode=random`) +**问题**:用户刚复习过的内容高频出现,真正需要复习的被淹没 + +**优化算法**:时间衰减 × 回忆掌握度加权采样 + +```java +// ReviewServiceImpl.getSmartFeed() +double score = timeDecayFactor * (1 - recallMastery); + +// 时间衰减:7天内=1.0, 30天=0.5, 90天=0.1 +timeDecayFactor = Math.max(0.1, 1.0 - (daysSince / 90.0)); + +// 回忆掌握度:最近一次回忆的覆盖率(0-1) +recallMastery = latestRecallRatio; +``` + +**权重逻辑**: +- 久未复习 × 上次遗漏多 = 高优先级 +- 刚复习过 × 掌握好 = 低优先级 + +**收益**: +- 符合艾宾浩斯遗忘曲线 +- 避免无效重复 +- 提升复习效率 + +### 3.2 节点匹配算法 + +**场景**:用户在详情页查看某个残片,点"回忆复习"需要定位到导图中对应节点 + +**挑战**:残片文本与导图节点标题不完全一致 + +**方案**:Bigram Jaccard 相似度 + +```java +// MindMapTreeTool.similarityScore() +Set bigramsA = extractBigrams(normalize(textA)); +Set bigramsB = extractBigrams(normalize(textB)); + +int intersection = Sets.intersection(bigramsA, bigramsB).size(); +int union = Sets.union(bigramsA, bigramsB).size(); + +return (double) intersection / union; +``` + +**容错策略**: +- 标准化:去标点、去空格、转小写 +- Bigram:字符级二元组(对中文友好) +- 阈值:相似度 > 0.6 视为匹配 +- 加权:`notes` 字段也参与匹配(权重 0.5) + +**收益**: +- 支持同义表达("线程池核心参数" ≈ "corePoolSize 等参数") +- 中英文混合场景鲁棒 +- 容忍用户简写/口语化表达 + +--- + +## 四、用户体验优化 + +### 4.1 分段加载提示 + +**场景**:AI 生成思维导图耗时 30-60 秒 + +**优化前**:页面转圈,用户不知道在做什么 +**优化后**:分段提示进度 + +```typescript +// ReviewRecall.vue +if (aiEnabled) { + message.info('正在调用 AI 生成思维导图...'); + // 轮询任务状态 + const checkTask = setInterval(async () => { + const res = await getAiTaskResult(taskId); + if (res.data.status === 'completed') { + message.success('生成完成'); + clearInterval(checkTask); + } + }, 2000); +} +``` + +**收益**: +- 降低用户焦虑 +- 明确系统状态 +- 减少重复点击 + +### 4.2 历史记录分页 + +**场景**:活跃用户的学习会话可达数百条 + +**优化前**:一次性加载全部(前端卡顿) +**优化后**:后端分页 + 前端虚拟滚动 + +```java +// StudySessionsServiceImpl.java +Page page = new Page<>(pageNum, pageSize); +page = studySessionsMapper.selectPage(page, queryWrapper); +``` + +```vue + + +``` + +**收益**: +- 首屏加载快(<100ms) +- 支持无限历史记录 +- 内存占用低 + +### 4.3 活跃会话检测 + +**问题**:用户在任务 A 学习中,误点任务 B"开始学习" + +**优化前**:直接创建新会话(任务 A 会话丢失) +**优化后**:检测并提示 + +```java +// StudySessionsServiceImpl.startSession() +StudySessionEntity active = studySessionsMapper.selectOne( + new QueryWrapper() + .eq("created_by", userId) + .eq("status", StudySessionStatus.IN_PROGRESS.name()) +); +if (active != null && !active.getTaskNum().equals(taskNum)) { + throw new BusinessException("您有正在进行的学习会话(任务 " + active.getTaskNum() + "),请先结束"); +} +``` + +**收益**: +- 防止意外丢失数据 +- 引导用户正确流程 +- 减少客服咨询 + +--- + +## 五、安全与健壮性 + +### 5.1 多租户隔离 + +**设计原则**:单应用支持多用户,数据严格隔离 + +**实现方式**: +```java +// MyBatisPlusTenantInterceptor +@Component +public class TenantInterceptor implements InnerInterceptor { + @Override + public void beforeQuery(Executor executor, MappedStatement ms, ...) { + // 自动注入 WHERE created_by = :currentUserId + } +} +``` + +**覆盖范围**: +- 所有 SELECT 查询自动加租户过滤 +- INSERT 自动注入 `created_by` +- UPDATE/DELETE 验证租户权限 + +**收益**: +- 业务代码无感知(避免遗漏) +- 100% 防止越权访问 +- 支持未来 SaaS 化 + +### 5.2 输入校验 + +**后端**: +```java +@PostMapping("/study-sessions/{sessionNum}/expectation") +public CommonResult updateExpectation( + @PathVariable Integer sessionNum, + @RequestBody @Valid ExpectationRequest request // JSR-303 校验 +) { + // @NotBlank, @Size(max=500) 等注解自动生效 +} +``` + +**前端**: +```typescript +const rules = { + expectation: [ + { required: true, message: '请填写学习预期' }, + { max: 500, message: '不超过 500 字' } + ] +}; +``` + +**双重保障**:前端 UX + 后端安全 + +--- + +## 六、可观测性 + +### 6.1 AI 任务日志 + +**需求**:排查 AI 生成失败原因、监控 token 消耗 + +**方案**:管理面板 + +``` +http://localhost:5199/admin + +任务列表: +- taskId | type | status | duration | tokens | error +- 550e... | generate-mind-map | completed | 32.5s | 1250 | - +- 661f... | aggregate-report | failed | 5.0s | 0 | Timeout +``` + +**收益**: +- 快速定位问题 +- 成本分析 +- 性能优化依据 + +### 6.2 Flyway 迁移历史 + +**收益**: +- 数据库 schema 版本可追溯 +- 回滚方案清晰 +- 团队协作无冲突 + +```sql +SELECT * FROM flyway_schema_history ORDER BY installed_rank; +``` + +--- + +## 七、技术债务管理 + +### 已知限制 + +1. **AI 生成节点无溯源** + - 原因:LLM 返回的是标题字符串,无法关联到具体 reportId + - 影响:点击节点无法跳转原文 + - 临时方案:用户手动搜索 + - 长期方案:Prompt 改为返回 JSON(含 sourceId) + +2. **Bigram 对短文本效果有限** + - 场景:节点标题只有 2-3 个字 + - 临时方案:阈值降至 0.4 + - 长期方案:引入 embedding 语义匹配 + +3. **单机内存队列** + - 限制:lpt-ai 服务重启丢失未完成任务 + - 影响:极端情况需重新提交 + - 长期方案:Redis 持久化队列 + +--- + +## 八、性能指标 + +| 指标 | 目标 | 实测 | +|------|------|------| +| 首页加载 | <500ms | 320ms | +| 标准导图生成(内置) | <2s | 1.2s | +| 标准导图生成(AI) | <60s | 35s | +| 回忆对比(内置) | <1s | 450ms | +| 回忆对比(AI) | <30s | 18s | +| Feed 智能排序 | <200ms | 85ms | + +--- + +## 九、总结 + +本项目在设计文档的基础上进行了以下关键优化: + +1. **架构层**:异步任务、防并发、降级机制 +2. **数据层**:双格式存储、节点溯源、分页加载 +3. **算法层**:智能排序、模糊匹配、语义对比 +4. **体验层**:分段提示、活跃检测、历史记录 +5. **安全层**:多租户隔离、双重校验、权限控制 + +这些优化不是对设计的否定,而是在实现过程中针对实际场景的工程化改进。设计文档描述"做什么",本文档记录"怎么做得更好"。 diff --git a/src/main/java/com/guo/learningprogresstracker/config/serializer/LocalDateTimeSerializer.java b/src/main/java/com/guo/learningprogresstracker/config/serializer/LocalDateTimeSerializer.java index f821202..15932f3 100644 --- a/src/main/java/com/guo/learningprogresstracker/config/serializer/LocalDateTimeSerializer.java +++ b/src/main/java/com/guo/learningprogresstracker/config/serializer/LocalDateTimeSerializer.java @@ -9,7 +9,7 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; public class LocalDateTimeSerializer extends StdSerializer { - private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"); + private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); public LocalDateTimeSerializer() { super(LocalDateTime.class);