2026-09-13-report-quick-analyze-extend.md 22 KB

报告快速分析扩展实现计划

面向 AI 代理的工作者: 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(- [ ])语法来跟踪进度。

目标:gut-flora-detail.vuereport-confirm.vue 新增「报告快速分析」入口,后端支持 draftIdreportId 双模式,复用现有免费版限制与 AI 链路。

架构: 后端 FamilyContextService 新增 buildContextFromDraftAIChatController 扩展 buildChatInputsreportAnalyze 校验;前端 api.js 扩展 reportQuickAnalyze 可选 draftId,两个 Vue 页面复用 report-detail.vue 的浮层结构与样式。

技术栈: Spring Boot 2.7.18 (Java 8) + MyBatis-Plus + Redis + uni-app Vue 2 小程序(Options API,禁可选链/禁 CSS Grid/禁 :key 表达式)


文件结构

后端(cfc-backend)

文件 操作 职责
src/main/java/com/etotem/cfc/service/FamilyContextService.java 修改 注入 HealthReportDraftService + ObjectMapper,新增 buildContextFromDraft(userId, draftId)
src/main/java/com/etotem/cfc/controller/ai/AIChatController.java 修改 1. buildChatInputs:识别 draftId 参数,调用新方法
2. reportAnalyze:校验 `reportId

前端(cfc-frontend)

文件 操作 职责
utils/api.js 修改 reportQuickAnalyze(query, reportId, conversationId, draftId) 新增第四参数
pages/health/gut-flora-detail.vue 修改 复用浮层逻辑/样式,FAB 条件 `!isEditing && (reportId
pages/health/report-confirm.vue 修改 复用浮层逻辑/样式,FAB 条件 !parsing && draftId

任务 1:后端 FamilyContextService 新增 buildContextFromDraft

文件:

  • 修改:cfc-backend/src/main/java/com/etotem/cfc/service/FamilyContextService.java

背景: 现有 buildContext(userId, reportId) 读已入库报告;草稿场景需读 HealthReportDraft.payloadJson 并解析为 Map 注入上下文。

  • 步骤 1:新增 import

在 import 区(现有 import 在 20-62 行)添加:

import com.etotem.cfc.service.HealthReportDraftService;
import com.fasterxml.jackson.databind.ObjectMapper;
  • 步骤 2:注入依赖

@Resource private ReportSurveyService reportSurveyService;(约 36 行)之后添加:

    @Resource
    private HealthReportDraftService healthReportDraftService;

    @Resource
    private ObjectMapper objectMapper;
  • 步骤 3:新增 buildContextFromDraft 方法

buildContext(Long userId, Long reportId, Long surveyId) 方法结束后(约 222 行)添加:

    /**
     * 基于草稿 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:编译验证

运行:cd cfc-backend && mvn clean compile -q -Dmaven.test.skip=true 预期:BUILD SUCCESS(无输出即成功)

  • [ ] 步骤 5:Commit

    git add cfc-backend/src/main/java/com/etotem/cfc/service/FamilyContextService.java
    git commit -m "feat: FamilyContextService 新增 buildContextFromDraft 支持草稿上下文"
    

任务 2:后端 AIChatController 扩展 buildChatInputs + reportAnalyze

文件:

  • 修改:cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java

背景: 现有 buildChatInputs 只处理 reportId/surveyId/selfCheckId;需加 draftId 分支。reportAnalyze 校验需接受 draftId

  • 步骤 1:修改 buildChatInputs 增加 draftId 处理

buildChatInputs 方法内(约 136-207 行),surveyId 解析之后(约 154 行)添加:

        String draftIdStr = params.get("draftId");
        Long draftId = null;
        if (draftIdStr != null && !draftIdStr.trim().isEmpty()) {
            draftId = Long.valueOf(draftIdStr);
        }

修改上下文组装分支(约 155-162 行):

        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);
        }
  • 步骤 2:修改 reportAnalyze 校验

reportAnalyze 方法内(约 213-265 行),reportIdStr 获取后(约 215 行)添加 draftIdStr 获取,修改校验为:

        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不能为空");
        }
        Long reportId = null;
        if (reportIdStr != null && !reportIdStr.trim().isEmpty()) {
            reportId = Long.valueOf(reportIdStr);
        }

原有 Long reportId = Long.valueOf(reportIdStr);(约 222 行)删除。

  • 步骤 3:编译验证

运行:cd cfc-backend && mvn clean compile -q -Dmaven.test.skip=true 预期:BUILD SUCCESS

  • [ ] 步骤 4:Commit

    git add cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java
    git commit -m "feat: AIChatController 支持 draftId 参数的快速分析"
    

任务 3:前端 api.js 扩展 reportQuickAnalyze

文件:

  • 修改:cfc-frontend/utils/api.js:1420

  • [ ] 步骤 1:修改函数签名

    // 报告快速分析(免费版每日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)
    }
    
  • [ ] 步骤 2:语法校验

运行:node -e "const fs=require('fs');const src=fs.readFileSync('cfc-frontend/utils/api.js','utf8');const m=src.match(/export const reportQuickAnalyze[\s\S]*?\n}/);if(!m){console.log('FAIL');process.exit(1)}console.log('OK')" 预期:OK

  • [ ] 步骤 3:Commit

    git add cfc-frontend/utils/api.js
    git commit -m "feat(frontend): api.js reportQuickAnalyze 新增可选 draftId 参数"
    

任务 4:前端 gut-flora-detail.vue 新增快速分析浮层

文件:

  • 修改:cfc-frontend/pages/health/gut-flora-detail.vue

策略: 直接复制 report-detail.vuequickAnalyze data/computed/methods/template/style,仅改入口条件与 import。

  • 步骤 1:import 新增

第 329 行 import 行添加 reportQuickAnalyze, getMyMembership

import { getReportDetail, editHealthReport, queryKnowledgeBase, queryIndicatorKnowledge, getGutFloraAnalysis, confirmReportPreview, discardReportDraft, parseReportDraft, getHealthFoods, getFoodCautionList, getFamilyMemberList, addFamilyMember, reportQuickAnalyze, getMyMembership } from '../../utils/api.js'
  • 步骤 2:data 新增 quickAnalyze 对象

extractedAge: ''(约 394 行)之后添加:

      // 报告快速分析浮层
      quickAnalyze: {
        show: false,
        sending: false,
        messages: [],      // [{role:'user'|'ai', content}]
        inputText: '',
        conversationId: '',
        usedToday: false    // 免费版今日已用(前端预判,最终以后端为准)
      }
  • 步骤 3:computed 新增 quickQuestions

draftMemberText computed 后(约 460 行)添加:

    quickQuestions: function() {
      return [
        '这份报告最需要关注哪些异常指标?',
        '报告中提示了哪些健康风险?',
        '有哪些改善建议?'
      ]
    }
  • 步骤 4:methods 新增浮层逻辑

draftMemberText 方法后(约 460 行)添加:

    // ===== 报告快速分析 =====
    openQuickAnalyze: function() {
      this.quickAnalyze.show = true
      var self = this
      // 预判免费版今日额度(仅用于 UI 展示,最终以后端校验为准)
      getMyMembership().then(function(res) {
        var level = res.data && res.data.memberLevel
        if (level && level !== 'FREE') {
          self.quickAnalyze.usedToday = false
        }
      }).catch(function() {})
    },
    closeQuickAnalyze: function() {
      this.quickAnalyze.show = false
    },
    sendQuickQuestion: function(q) {
      if (q && q.trim()) {
        this.quickAnalyze.inputText = q
        this.sendQuickAnalyze()
      }
    },
    sendQuickAnalyze: function() {
      var self = this
      var text = (this.quickAnalyze.inputText || '').trim()
      if (!text || this.quickAnalyze.sending) return
      this.quickAnalyze.messages.push({ role: 'user', content: text })
      this.quickAnalyze.inputText = ''
      this.quickAnalyze.sending = true
      // 传 reportId 或 draftId
      var rid = this.reportId
      var did = this.draftId
      reportQuickAnalyze(text, rid, this.quickAnalyze.conversationId, did).then(function(res) {
        if (res.code === 200 && res.data) {
          self.quickAnalyze.messages.push({ role: 'ai', content: res.data.answer || '(无回答)' })
          if (res.data.conversationId) {
            self.quickAnalyze.conversationId = res.data.conversationId
          }
        } else {
          var msg = res.message || '分析失败'
          if (msg.indexOf('每日仅限') !== -1) {
            self.quickAnalyze.usedToday = true
          }
          self.quickAnalyze.messages.push({ role: 'ai', content: msg })
        }
      }).catch(function() {
        self.quickAnalyze.messages.push({ role: 'ai', content: 'AI 服务暂不可用,请稍后重试' })
      }).finally(function() {
        self.quickAnalyze.sending = false
      })
    }
  • 步骤 5:template 新增 FAB + 浮层

</scroll-view> 之后(约 223 行),<health-knowledge-popup> 之前插入:

    <!-- 报告快速分析入口(仅查看模式,reportId 或 draftId 存在) -->
    <view class="quick-analyze-fab" v-if="!isEditing && (reportId || draftId)" @tap="openQuickAnalyze">
      <text class="quick-analyze-fab-icon">🎯</text>
      <text class="quick-analyze-fab-text">快速分析</text>
    </view>

    <!-- 快速分析浮层 -->
    <view class="quick-analyze-overlay" v-if="quickAnalyze.show" @tap="closeQuickAnalyze"></view>
    <view class="quick-analyze-panel" v-if="quickAnalyze.show">
      <view class="quick-analyze-header">
        <text class="quick-analyze-title">报告快速分析</text>
        <text class="quick-analyze-close" @tap="closeQuickAnalyze">✕</text>
      </view>
      <view class="quick-analyze-hint" v-if="quickAnalyze.usedToday">免费版今日次数已用完,升级会员解锁更多分析</view>
      <scroll-view class="quick-analyze-messages" scroll-y>
        <view class="quick-analyze-message" v-for="(msg, qidx) in quickAnalyze.messages" :key="qidx" :class="'msg-' + msg.role">
          <text class="quick-analyze-msg-text">{{ msg.content }}</text>
        </view>
        <view class="quick-analyze-loading" v-if="quickAnalyze.sending">
          <text class="quick-analyze-loading-text">AI 正在分析中...</text>
        </view>
      </scroll-view>
      <view class="quick-analyze-chips" v-if="quickAnalyze.messages.length === 0">
        <view class="quick-analyze-chip" v-for="(q, qi) in quickQuestions" :key="qi" @tap="sendQuickQuestion(q)">
          <text class="quick-analyze-chip-text">{{ q }}</text>
        </view>
      </view>
      <view class="quick-analyze-input-bar">
        <input class="quick-analyze-input" v-model="quickAnalyze.inputText" placeholder="输入你想问的问题..."
          :disabled="quickAnalyze.sending || quickAnalyze.usedToday" confirm-type="send" @confirm="sendQuickAnalyze" />
        <view class="quick-analyze-send" :class="{ 'send-disabled': quickAnalyze.sending || quickAnalyze.usedToday }" @tap="sendQuickAnalyze">
          <text class="quick-analyze-send-text">发送</text>
        </view>
      </view>
    </view>
  • 步骤 6:style 复用

<style scoped> 末尾(约 1310 行)添加 report-detail.vue.quick-analyze-* 完整样式(第 2292-2398 行)。

  • 步骤 7:语法校验

运行:cd cfc-frontend && node -e "const fs=require('fs');const src=fs.readFileSync('pages/health/gut-flora-detail.vue','utf8');const m=src.match(/<script>([\s\S]*?)<\/script>/);new Function(m[1].replace(/import[\s\S]*?(?=\n)/g,'').replace(/export default/,'const x='));console.log('OK')" 预期:OK

  • [ ] 步骤 8:Commit

    git add cfc-frontend/pages/health/gut-flora-detail.vue
    git commit -m "feat(frontend): gut-flora-detail 新增报告快速分析浮层(支持 reportId/draftId)"
    

任务 5:前端 report-confirm.vue 新增快速分析浮层

文件:

  • 修改:cfc-frontend/pages/health/report-confirm.vue

  • [ ] 步骤 1:import 新增

第 350 行 import 行添加 reportQuickAnalyze, getMyMembership

import { confirmReportPreview, discardReportDraft, confirmTongue, discardTongue, parseReportDraft, reportQuickAnalyze, getMyMembership } from '../../utils/api.js'
  • 步骤 2:data 新增 quickAnalyze 对象

extractedAge: ''(约 394 行)之后添加(同 gut-flora-detail):

      // 报告快速分析浮层
      quickAnalyze: {
        show: false,
        sending: false,
        messages: [],
        inputText: '',
        conversationId: '',
        usedToday: false
      }
  • 步骤 3:computed 新增 quickQuestions

memberDisplayText computed 后(约 441 行)添加:

    quickQuestions: function() {
      return [
        '这份报告最需要关注哪些异常指标?',
        '报告中提示了哪些健康风险?',
        '有哪些改善建议?'
      ]
    }
  • 步骤 4:methods 新增浮层逻辑

memberDisplayText 方法后添加(同 gut-flora-detail,但传参只需 draftId):

    // ===== 报告快速分析 =====
    openQuickAnalyze: function() {
      this.quickAnalyze.show = true
      var self = this
      getMyMembership().then(function(res) {
        var level = res.data && res.data.memberLevel
        if (level && level !== 'FREE') {
          self.quickAnalyze.usedToday = false
        }
      }).catch(function() {})
    },
    closeQuickAnalyze: function() {
      this.quickAnalyze.show = false
    },
    sendQuickQuestion: function(q) {
      if (q && q.trim()) {
        this.quickAnalyze.inputText = q
        this.sendQuickAnalyze()
      }
    },
    sendQuickAnalyze: function() {
      var self = this
      var text = (this.quickAnalyze.inputText || '').trim()
      if (!text || this.quickAnalyze.sending) return
      this.quickAnalyze.messages.push({ role: 'user', content: text })
      this.quickAnalyze.inputText = ''
      this.quickAnalyze.sending = true
      reportQuickAnalyze(text, null, this.quickAnalyze.conversationId, this.draftId).then(function(res) {
        if (res.code === 200 && res.data) {
          self.quickAnalyze.messages.push({ role: 'ai', content: res.data.answer || '(无回答)' })
          if (res.data.conversationId) {
            self.quickAnalyze.conversationId = res.data.conversationId
          }
        } else {
          var msg = res.message || '分析失败'
          if (msg.indexOf('每日仅限') !== -1) {
            self.quickAnalyze.usedToday = true
          }
          self.quickAnalyze.messages.push({ role: 'ai', content: msg })
        }
      }).catch(function() {
        self.quickAnalyze.messages.push({ role: 'ai', content: 'AI 服务暂不可用,请稍后重试' })
      }).finally(function() {
        self.quickAnalyze.sending = false
      })
    }
  • 步骤 5:template 新增 FAB + 浮层

</view>(form-section 结束)之后、<view class="bottom-spacer"></view> 之前插入:

    <!-- 报告快速分析入口(解析完成、非解析中) -->
    <view class="quick-analyze-fab" v-if="!parsing && draftId" @tap="openQuickAnalyze">
      <text class="quick-analyze-fab-icon">🎯</text>
      <text class="quick-analyze-fab-text">快速分析</text>
    </view>

    <!-- 快速分析浮层 -->
    <view class="quick-analyze-overlay" v-if="quickAnalyze.show" @tap="closeQuickAnalyze"></view>
    <view class="quick-analyze-panel" v-if="quickAnalyze.show">
      <view class="quick-analyze-header">
        <text class="quick-analyze-title">报告快速分析</text>
        <text class="quick-analyze-close" @tap="closeQuickAnalyze">✕</text>
      </view>
      <view class="quick-analyze-hint" v-if="quickAnalyze.usedToday">免费版今日次数已用完,升级会员解锁更多分析</view>
      <scroll-view class="quick-analyze-messages" scroll-y>
        <view class="quick-analyze-message" v-for="(msg, qidx) in quickAnalyze.messages" :key="qidx" :class="'msg-' + msg.role">
          <text class="quick-analyze-msg-text">{{ msg.content }}</text>
        </view>
        <view class="quick-analyze-loading" v-if="quickAnalyze.sending">
          <text class="quick-analyze-loading-text">AI 正在分析中...</text>
        </view>
      </scroll-view>
      <view class="quick-analyze-chips" v-if="quickAnalyze.messages.length === 0">
        <view class="quick-analyze-chip" v-for="(q, qi) in quickQuestions" :key="qi" @tap="sendQuickQuestion(q)">
          <text class="quick-analyze-chip-text">{{ q }}</text>
        </view>
      </view>
      <view class="quick-analyze-input-bar">
        <input class="quick-analyze-input" v-model="quickAnalyze.inputText" placeholder="输入你想问的问题..."
          :disabled="quickAnalyze.sending || quickAnalyze.usedToday" confirm-type="send" @confirm="sendQuickAnalyze" />
        <view class="quick-analyze-send" :class="{ 'send-disabled': quickAnalyze.sending || quickAnalyze.usedToday }" @tap="sendQuickAnalyze">
          <text class="quick-analyze-send-text">发送</text>
        </view>
      </view>
    </view>
  • 步骤 6:style 复用

<style scoped> 末尾添加同一套 .quick-analyze-* 样式。

  • 步骤 7:语法校验

运行:cd cfc-frontend && node -e "const fs=require('fs');const src=fs.readFileSync('pages/health/report-confirm.vue','utf8');const m=src.match(/<script>([\s\S]*?)<\/script>/);new Function(m[1].replace(/import[\s\S]*?(?=\n)/g,'').replace(/export default/,'const x='));console.log('OK')" 预期:OK

  • [ ] 步骤 8:Commit

    git add cfc-frontend/pages/health/report-confirm.vue
    git commit -m "feat(frontend): report-confirm 新增报告快速分析浮层(支持 draftId)"
    

任务 6:验证

文件:

  • 验证:后端编译 + 手动验证清单

  • [ ] 步骤 1:后端全量编译

运行:cd cfc-backend && mvn clean compile -q -Dmaven.test.skip=true 预期:BUILD SUCCESS

  • 步骤 2:手动验证清单
场景 预期
gut-flora-detail(reportId 模式)点击快速分析 正常返回 answer
gut-flora-detail(draftId 模式)点击快速分析 正常返回 answer(基于草稿 payload)
report-confirm(draftId)点击快速分析 正常返回 answer
免费版第 2 次(任意页面) 返回超限错误「免费版每日仅限 1 次快速分析,升级会员解锁更多」
付费版多轮追问 带 conversationId,AI 记得上文
Redis 停掉 免费版仍可用(fail-open),后端日志记 warn
  • [ ] 步骤 3:最终 Commit(若有无需变更则跳过)

    git status --short
    # 确认无遗漏
    

自检结果

规格覆盖度检查:

  • ✅ 后端 draftId 支持(FamilyContextService + AIChatController)→ 任务 1、2
  • ✅ 前端 api.js 扩展 draftId 参数 → 任务 3
  • ✅ gut-flora-detail 浮层(reportId/draftId 双模式) → 任务 4
  • ✅ report-confirm 浮层(draftId 模式) → 任务 5
  • ✅ 样式复用 → 任务 4、6
  • ✅ 免费版限制/会员校验/Redis fail-open 复用现有 → 任务 2
  • ✅ 错误处理(空参数/超限/AI 不可用) → 任务 2、验证步骤 2

占位符扫描: 无 TODO/待定/后续实现 等占位符。

类型一致性:

  • buildContextFromDraft(Long userId, Long draftId) → 任务 1 步骤 3 定义,任务 2 步骤 1 调用,签名一致
  • reportQuickAnalyze(query, reportId, conversationId, draftId) → 任务 3 定义,任务 4/5 步骤 4 调用,签名一致
  • quickAnalyze data 结构 → 任务 4/5 步骤 2 定义,步骤 4/5 使用,一致
  • draftId → 任务 4 步骤 5 使用 this.draftId(data 已有,第 374 行),任务 5 步骤 5 使用 this.draftId(data 已有,第 356 行),一致