2026-09-13-report-quick-analyze-extend-design.md 8.2 KB

报告快速分析扩展:上传后即可用

日期: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

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

/**
 * 基于草稿 ID 构建上下文(用于快速分析)
 * @param userId 当前用户
 * @param draftId 草稿 ID
 * @return 上下文 Map,包含 reportDetail(从 payloadJson 还原)
 */
public Map<String, Object> buildContextFromDraft(Long userId, Long draftId) {
    Map<String, Object> ctx = buildContext(userId);

    if (draftId == null) return ctx;

    HealthReportDraft draft = healthReportDraftService.getDraftById(draftId);
    if (draft == null || !draft.getUserId().equals(userId)) {
        return ctx; // 权限不通过静默返回基础上下文
    }

    // 解析 payloadJson
    Map<String, Object> 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 修改

private Map<String, Object> buildChatInputs(Long userId, Map<String, String> 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<String, Object> 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 校验修改

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 扩展

// 报告快速分析(免费版每日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 入口条件

<!-- 仅查看模式:reportId 或 draftId 存在时 -->
<view class="quick-analyze-fab" v-if="!isEditing && (reportId || draftId)" @tap="openQuickAnalyze">

4.6 report-confirm.vue 入口条件

<!-- 解析完成、非解析中 -->
<view class="quick-analyze-fab" v-if="!parsing && draftId" @tap="openQuickAnalyze">

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 类名,避免重复维护