# 报告快速分析扩展:上传后即可用 日期:2026-09-13 状态:设计中 ## 1. 背景与目标 当前「报告快速分析」功能(`/api/ai/report/analyze`)仅在 `report-detail.vue`(view 模式)可用。用户上传报告后: - 菌群报告 → 跳转 `gut-flora-detail.vue`(无快速分析入口) - 两阶段入库 → 停留 `report-confirm.vue`(无快速分析入口) **目标**:在 `gut-flora-detail.vue` 和 `report-confirm.vue` 也提供「快速分析」入口,支持已入库的 `reportId` 和未入库的 `draftId`。 --- ## 2. 架构设计 ### 2.1 后端扩展(cfc-backend) ``` POST /api/ai/report/analyze Request: { query, reportId?, draftId?, conversationId?, memberId? } ``` - 端点同时接受 `reportId` 和 `draftId`(至少一个非空) - 后端判断:优先用 `reportId`(已入库,完整上下文),其次用 `draftId`(草稿,读 payloadJson) - 免费版 Redis 计数、会员限制、fail-open 逻辑保持不变 - LangGraph `chat_graph` 零改动 ### 2.2 数据流 ``` 前端页面(gut-flora-detail / report-confirm) │ POST /api/ai/report/analyze { query, reportId/draftId } ▼ AIChatController.reportAnalyze() ├─ 1. 会员级别检查(user.memberLevel) ├─ 2. FREE → Redis 计数(fail-open) ├─ 3. buildChatInputs(userId, params) │ ├─ 有 reportId → familyContextService.buildContext(userId, reportId) │ └─ 只有 draftId → familyContextService.buildContextFromDraft(userId, draftId) [新增] └─ 4. aiService.sendMessage() → LangGraph chat_graph ``` ### 2.3 前端扩展(cfc-frontend) | 页面 | 入口条件 | 传参 | |------|---------|------| | `report-detail.vue` | view 模式 + reportId | reportId(已有) | | `gut-flora-detail.vue` | (!isEditing && !isDraftMode && reportId) \|\| (isDraftMode && draftId) | reportId 或 draftId | | `report-confirm.vue` | !parsing && draftId | draftId | --- ## 3. 改动清单 ### 3.1 后端(cfc-backend) | 文件 | 改动 | |------|------| | `FamilyContextService.java` | 新增 `buildContextFromDraft(Long userId, Long draftId)`,注入 `HealthReportDraftService`,查草稿 → 解析 `payloadJson` → 返回含 `reportDetail` 的上下文 | | `AIChatController.java` | 1. `buildChatInputs`:检测 `params.get("draftId")`,调用新方法
2. `reportAnalyze`:校验改为 `reportId || draftId`,空则报错 | ### 3.2 前端(cfc-frontend) | 文件 | 改动 | |------|------| | `utils/api.js` | `reportQuickAnalyze` 新增可选参数 `draftId` | | `pages/health/gut-flora-detail.vue` | 1. import 新增 `reportQuickAnalyze`、`getMyMembership`
2. data 新增 `quickAnalyze` 对象
3. computed 新增 `quickQuestions`
4. methods 新增 `openQuickAnalyze`、`closeQuickAnalyze`、`sendQuickQuestion`、`sendQuickAnalyze`
5. template:FAB 区域(第 229 行附近)加「🎯 快速分析」按钮 + 浮层结构
6. style:复用 report-detail.vue 样式(`.quick-analyze-*` 类) | | `pages/health/report-confirm.vue` | 同 gut-flora-detail.vue,但入口条件为 `!parsing && draftId` | --- ## 4. 关键实现细节 ### 4.1 FamilyContextService.buildContextFromDraft ```java /** * 基于草稿 ID 构建上下文(用于快速分析) * @param userId 当前用户 * @param draftId 草稿 ID * @return 上下文 Map,包含 reportDetail(从 payloadJson 还原) */ public Map buildContextFromDraft(Long userId, Long draftId) { Map ctx = buildContext(userId); if (draftId == null) return ctx; HealthReportDraft draft = healthReportDraftService.getDraftById(draftId); if (draft == null || !draft.getUserId().equals(userId)) { return ctx; // 权限不通过静默返回基础上下文 } // 解析 payloadJson Map reportDetail = null; try { if (draft.getPayloadJson() != null && !draft.getPayloadJson().isEmpty()) { reportDetail = objectMapper.readValue(draft.getPayloadJson(), Map.class); } } catch (Exception e) { log.warn("解析草稿 payload 失败: {}", e.getMessage()); } if (reportDetail != null) { ctx.put("reportDetail", reportDetail); } return ctx; } ``` ### 4.2 AIChatController.buildChatInputs 修改 ```java private Map buildChatInputs(Long userId, Map params) { // ... 现有 mascot、memberId、surveyId、selfCheckId 逻辑不变 ... Long reportId = null; Long draftId = null; if (reportIdStr != null && !reportIdStr.trim().isEmpty()) { reportId = Long.valueOf(reportIdStr); } String draftIdStr = params.get("draftId"); if (draftIdStr != null && !draftIdStr.trim().isEmpty()) { draftId = Long.valueOf(draftIdStr); } Map inputs; if (reportId != null) { inputs = familyContextService.buildContext(userId, reportId); } else if (draftId != null) { inputs = familyContextService.buildContextFromDraft(userId, draftId); } else { inputs = familyContextService.buildContext(userId); } // ... mascot、portrait、selfCheck、memory 后续逻辑不变 ... } ``` ### 4.3 AIChatController.reportAnalyze 校验修改 ```java String reportIdStr = params.get("reportId"); String draftIdStr = params.get("draftId"); if ((reportIdStr == null || reportIdStr.trim().isEmpty()) && (draftIdStr == null || draftIdStr.trim().isEmpty())) { return Result.error("报告ID或草稿ID不能为空"); } ``` ### 4.4 api.js 扩展 ```javascript // 报告快速分析(免费版每日1次,付费版多轮) export const reportQuickAnalyze = (query, reportId, conversationId, draftId) => { var data = { query: query } if (reportId) data.reportId = reportId if (draftId) data.draftId = draftId if (conversationId) data.conversationId = conversationId return request('/api/ai/report/analyze', 'POST', data) } ``` ### 4.5 gut-flora-detail.vue 入口条件 ```html ``` ### 4.6 report-confirm.vue 入口条件 ```html ``` --- ## 5. 会员限制与免费版策略 - 完全复用现有逻辑:`user.memberLevel` 判定 FREE/FAMILY/PROVIDER - FREE:每日 1 次(Redis 计数 key 唯一,`report:analyze:free:userId:date`,无区分 reportId/draftId) - FREE:前端不传 `conversationId` → LangGraph 每次新建会话 → 锁死多轮 - 付费版:可传 `conversationId` 实现多轮 --- ## 6. 错误处理 | 场景 | 行为 | |------|------| | query 为空 | `Result.error("消息不能为空")` | | reportId 和 draftId 均空 | `Result.error("报告ID或草稿ID不能为空")` | | 草稿不存在/无权限 | 静默降级,仅返回基础上下文(AI 仅做通用回答) | | 免费版超限 | `Result.error("免费版每日仅限 1 次快速分析,升级会员解锁更多")` | | Redis 异常 | fail-open 放行 + warn 日志 | | AI 服务不可用 | `Result.error("AI 服务暂不可用,请稍后重试")` | --- ## 7. 验收标准 | 场景 | 预期 | |------|------| | gut-flora-detail(reportId)点击快速分析 | 正常返回 answer | | gut-flora-detail(draftId)点击快速分析 | 正常返回 answer(基于草稿 payload) | | report-confirm(draftId)点击快速分析 | 正常返回 answer | | 免费版第 2 次(任意页面) | 返回超限错误 | | 付费版多轮追问 | 带 conversationId,AI 记得上文 | | Redis 停掉 | 免费版仍可用(fail-open) | --- ## 8. 风险与注意事项 1. **草稿 payload 结构**:`payloadJson` 是 `ParsedReportPayload.Payload` 的 JSON,结构与 `getReportDetail` 返回的 `reportDetail` 兼容(含 summary/indicators/gutFlora/diseaseRisks 等) 2. **权限校验**:草稿属于上传用户,`getDraftById` 后需校验 `draft.getUserId().equals(userId)` 3. **Redis 计数 key**:复用现有 key,同一用户同一天无论 reportId 还是 draftId 共用 1 次额度 4. **样式复用**:直接复制 report-detail.vue 的 `.quick-analyze-*` CSS 类名,避免重复维护