Browse Source

docs: 报告快速分析实现计划 + PROJECT-OVERVIEW 索引更新

- 5 任务计划:Redis 配置 + AIChatController 端点 + api.js 封装 + report-detail 浮层 + 验证
- 复用 aiService.sendMessage(含记忆层+镜像),LangGraph 零改动
- 免费版 Redis INCR 每日 1 次 fail-open,付费版多轮
Xiaogang Liao 1 week ago
parent
commit
c6131d6d19

+ 1 - 0
docs/superpowers/PROJECT-OVERVIEW.md

@@ -379,6 +379,7 @@
 
 | 文件 | 状态 | 说明 |
 |------|:----:|------|
+| `2026-09-10-report-quick-analyze.md` | 🟡 待执行(5 任务:Redis 依赖/配置 + AIChatController 抽 buildChatInputs 新增 report/analyze 端点 + api.js 封装 + report-detail 浮层 + 验证) | 报告快速分析实现计划 |
 | `2026-09-10-key-person-expansion-octopus.md` | 🟡 待执行(12 任务:迁移305-307 三表 + 3实体/Mapper/Service/Controller + 新 OctopusDiagram 三层画布 + 成效/关键人录入页 + action-detail 集成 + API 文档同步 + 旧章鱼图清理) | 关键人拓展章鱼图替换实现计划 |
 | `2026-08-18-homepage-health-funnel.md` | ✅ 已完成(登录态四段式重排 + 自测大卡 + 方法与计算属性 + 样式;未登录态零改动) | 首页健康漏斗实现计划 |
 | `2026-07-25-e2e-test-rearchitecture-phase1.md` | ✅ Phase 1 complete | E2E 测试体系 Phase 1 实施 |

+ 662 - 0
docs/superpowers/plans/2026-09-10-report-quick-analyze.md

@@ -0,0 +1,662 @@
+# 报告快速分析功能实现计划
+
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
+
+**目标:** 在报告详情页(`report-detail.vue`)提供「快速分析」功能——用户输入问题,AI 基于当前报告内容做初步解读。免费版(FREE)每日 1 次(Redis 计数),付费版(FAMILY/PROVIDER)多轮对话。
+
+**架构:** 后端在 `AIChatController` 新增 `POST /api/ai/report/analyze` 端点:查 `User.memberLevel` → 免费版 Redis 计数校验 → 复用 `sendMessage` 的上下文组装 + `aiService.sendMessage()` 调 LangGraph `chat_graph`(零 Python 改动)。前端 `report-detail.vue` 底部新增悬浮按钮 + 浮层(消息区/输入框/快捷问题),免费版禁用多轮(不带 conversationId)。
+
+**技术栈:** Spring Boot 2.7.18 (Java 8) + Redis (spring-boot-starter-data-redis) + uni-app Vue 2 小程序(Options API,禁可选链/禁 CSS Grid/禁 `:key` 表达式)。
+
+**设计规格:** `docs/superpowers/specs/2026-09-10-report-quick-analyze-design.md`
+
+---
+
+## 文件结构
+
+### 后端(cfc-backend)
+
+| 文件 | 操作 | 职责 |
+|------|------|------|
+| `pom.xml` | 修改 | 新增 `spring-boot-starter-data-redis` 依赖 |
+| `src/main/resources/application.yml` | 修改 | Redis 连接配置(默认 localhost:6379) |
+| `src/main/java/com/etotem/cfc/config/RedisConfig.java` | 创建 | `RedisTemplate<String, Object>` bean,String 序列化 |
+| `src/main/java/com/etotem/cfc/controller/ai/AIChatController.java` | 修改 | 抽出 `buildChatInputs()` 私有方法 + 新增 `POST /report/analyze` 端点(会员检查 + Redis 计数 + 调 aiService.sendMessage) |
+
+### 前端(cfc-frontend)
+
+| 文件 | 操作 | 职责 |
+|------|------|------|
+| `utils/api.js` | 修改 | 新增 `reportQuickAnalyze(query, reportId, conversationId)` 封装 |
+| `pages/health/report-detail.vue` | 修改 | 底部悬浮按钮 + 浮层(消息区/输入框/3 个快捷问题/加载态/免费版禁用态) |
+
+---
+
+## 任务 1:后端引入 Redis 依赖与配置
+
+**文件:**
+- 修改:`cfc-backend/pom.xml:90`(Validation 依赖之后)
+- 修改:`cfc-backend/src/main/resources/application.yml`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/config/RedisConfig.java`
+
+- [ ] **步骤 1:pom.xml 新增 redis starter**
+
+在 `<dependency>` 块(`spring-boot-starter-validation` 之后,约 90 行)插入:
+
+```xml
+        <!-- Redis -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+```
+
+- [ ] **步骤 2:application.yml 添加 Redis 配置**
+
+在 `spring:` 配置节点下添加(保持 YAML 缩进与现有 `datasource` 平级):
+
+```yaml
+  redis:
+    host: ${REDIS_HOST:localhost}
+    port: ${REDIS_PORT:6379}
+    password: ${REDIS_PASSWORD:}
+    timeout: 3000ms
+    lettuce:
+      pool:
+        max-active: 8
+        max-idle: 8
+        min-idle: 0
+```
+
+- [ ] **步骤 3:创建 RedisConfig.java**
+
+创建 `cfc-backend/src/main/java/com/etotem/cfc/config/RedisConfig.java`:
+
+```java
+package com.etotem.cfc.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+@Configuration
+public class RedisConfig {
+
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(factory);
+        StringRedisSerializer stringSerializer = new StringRedisSerializer();
+        template.setKeySerializer(stringSerializer);
+        template.setValueSerializer(stringSerializer);
+        template.setHashKeySerializer(stringSerializer);
+        template.setHashValueSerializer(stringSerializer);
+        template.afterPropertiesSet();
+        return template;
+    }
+}
+```
+
+- [ ] **步骤 4:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile -q -Dmaven.test.skip=true`
+预期:BUILD SUCCESS(无输出即成功)
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-backend/pom.xml cfc-backend/src/main/resources/application.yml cfc-backend/src/main/java/com/etotem/cfc/config/RedisConfig.java
+git commit -m "feat: 引入 Redis 依赖与 RedisTemplate 配置(报告快速分析计数用)"
+```
+
+---
+
+## 任务 2:AIChatController 抽出 buildChatInputs + 新增 report/analyze 端点
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java`
+
+**背景:** 现有 `sendMessage`(72-166 行)已包含完整的上下文组装逻辑(mascot/portrait/自检/记忆层),需抽出为 `buildChatInputs(userId, params)` 供两个端点复用。新端点 `POST /report/analyze` 只做会员检查 + Redis 计数 + 调 `aiService.sendMessage`。
+
+- [ ] **步骤 1:新增 import(RedisTemplate + LocalDate)**
+
+在 `AIChatController.java` 头部 import 区添加(现有 import 在 10-27 行):
+
+```java
+import org.springframework.data.redis.core.RedisTemplate;
+import java.time.LocalDate;
+import java.time.Duration;
+```
+
+- [ ] **步骤 2:注入 RedisTemplate 依赖**
+
+在 `private com.fasterxml.jackson.databind.ObjectMapper objectMapper;`(约 72 行)之后添加:
+
+```java
+    @Resource
+    private RedisTemplate<String, Object> redisTemplate;
+```
+
+- [ ] **步骤 3:抽出 buildChatInputs 私有方法**
+
+现有 `sendMessage` 内 93-155 行(从 `// 获取用户mascot设置` 到 `inputs = aiService.enrichInputsWithMemory(...)`)是上下文组装逻辑。抽出为新私有方法(保留原有 `sendMessage` 调用它):
+
+```java
+    /**
+     * 组装 AI 对话上下文(mascot + 家庭/报告 + 画像 + 自检 + 记忆层)
+     * 供 /chat/send 和 /report/analyze 共用
+     */
+    private Map<String, Object> buildChatInputs(Long userId, Map<String, String> params) {
+        String conversationId = params.get("conversationId");
+        String reportIdStr = params.get("reportId");
+        String surveyIdStr = params.get("surveyId");
+        String selfCheckIdStr = params.get("selfCheckId");
+
+        // 获取用户mascot设置
+        com.etotem.cfc.entity.User user = userService.getUserInfo(userId);
+        String mascotCode = user != null ? user.getMascot() : null;
+
+        // 组装家庭上下文(如果指定reportId/surveyId,注入报告+问卷数据)
+        Long reportId = null;
+        Long surveyId = null;
+        if (reportIdStr != null && !reportIdStr.trim().isEmpty()) {
+            reportId = Long.valueOf(reportIdStr);
+        }
+        if (surveyIdStr != null && !surveyIdStr.trim().isEmpty()) {
+            surveyId = Long.valueOf(surveyIdStr);
+        }
+        Map<String, Object> inputs;
+        if (reportId != null && surveyId != null) {
+            inputs = familyContextService.buildContext(userId, reportId, surveyId);
+        } else if (reportId != null) {
+            inputs = familyContextService.buildContext(userId, reportId);
+        } else {
+            inputs = familyContextService.buildContext(userId);
+        }
+
+        // 注入mascot信息到LangGraph inputs
+        if (mascotCode != null && !mascotCode.isEmpty()) {
+            com.etotem.cfc.enums.MascotEnum mascot = com.etotem.cfc.enums.MascotEnum.fromCode(mascotCode);
+            if (mascot != null) {
+                inputs.put("mascot_name", mascot.name());
+                inputs.put("mascot_gender", mascot.gender());
+                inputs.put("mascot_persona", mascot.persona());
+            }
+        } else {
+            inputs.put("mascot_name", "小助手");
+            inputs.put("mascot_gender", "");
+            inputs.put("mascot_persona", "");
+        }
+
+        // 画像注入
+        String chatMemberIdStr = params.get("memberId");
+        Long chatMemberId = chatMemberIdStr != null && !chatMemberIdStr.trim().isEmpty()
+                ? Long.valueOf(chatMemberIdStr) : null;
+        String chatPortrait = portraitService.buildPortraitPrompt(userId, chatMemberId);
+        if (chatPortrait != null) {
+            inputs.put("portrait_prompt", chatPortrait);
+        }
+
+        // 注入自检上下文(P1-2)
+        if (selfCheckIdStr != null && !selfCheckIdStr.trim().isEmpty()) {
+            try {
+                Long selfCheckId = Long.valueOf(selfCheckIdStr);
+                com.etotem.cfc.entity.FiveDimensionSelfCheck selfCheck = selfCheckMapper.selectById(selfCheckId);
+                if (selfCheck != null) {
+                    java.util.Map<String, Object> sc = new java.util.LinkedHashMap<>();
+                    sc.put("totalScore", selfCheck.getTotalScore());
+                    sc.put("scores", selfCheck.getScoresJson());
+                    sc.put("createdAt", selfCheck.getCreatedAt() != null ? selfCheck.getCreatedAt().toString() : "");
+                    inputs.put("self_check_result", objectMapper.writeValueAsString(sc));
+                }
+            } catch (Exception e) {
+                log.warn("注入自检上下文失败: {}", e.getMessage());
+            }
+        }
+
+        // AI记忆层注入(会话摘要+关键事实)
+        inputs = aiService.enrichInputsWithMemory(userId, conversationId, inputs);
+        return inputs;
+    }
+```
+
+**重构 sendMessage:** 将原 `sendMessage` 中 93-155 行替换为:
+
+```java
+        // 复用上下文组装(mascot + 家庭/报告 + 画像 + 自检 + 记忆层)
+        Map<String, Object> inputs = buildChatInputs(userId, params);
+```
+
+- [ ] **步骤 4:新增 /report/analyze 端点**
+
+在 `sendMessage` 方法结束后(`}` 之后)添加:
+
+```java
+    @Operation(summary = "报告快速分析(免费版每日1次,付费版多轮)")
+    @PostMapping("/report/analyze")
+    public Result<Map<String, Object>> reportAnalyze(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, String> params) {
+        String query = params.get("query");
+        String reportIdStr = params.get("reportId");
+        if (query == null || query.trim().isEmpty()) {
+            return Result.error("消息不能为空");
+        }
+        if (reportIdStr == null || reportIdStr.trim().isEmpty()) {
+            return Result.error("报告ID不能为空");
+        }
+        Long reportId = Long.valueOf(reportIdStr);
+
+        // 1. 会员级别检查
+        com.etotem.cfc.entity.User user = userService.getUserInfo(userId);
+        String memberLevel = user != null ? user.getMemberLevel() : "FREE";
+        boolean isFree = memberLevel == null || "FREE".equals(memberLevel);
+
+        // 2. 免费版 Redis 计数(每日 1 次;Redis 异常时 fail-open 放行)
+        if (isFree) {
+            try {
+                String key = "report:analyze:free:" + userId + ":" + LocalDate.now();
+                Long count = redisTemplate.opsForValue().increment(key);
+                if (count != null && count == 1L) {
+                    redisTemplate.expire(key, Duration.ofDays(1));
+                }
+                if (count != null && count > 1L) {
+                    return Result.error("免费版每日仅限 1 次快速分析,升级会员解锁更多");
+                }
+            } catch (Exception e) {
+                log.warn("Redis 计数失败,放行: {}", e.getMessage());
+            }
+        }
+
+        // 3. 组装上下文(免费版不带 conversationId → LangGraph 每次新建会话,锁死多轮)
+        if (isFree) {
+            params.remove("conversationId");
+        }
+        Map<String, Object> inputs = buildChatInputs(userId, params);
+
+        // 4. 调 LangGraph(经 aiService.sendMessage 含记忆层+镜像)
+        Map<String, Object> difyResp = aiService.sendMessage(
+                query, String.valueOf(userId),
+                params.get("conversationId"), inputs);
+
+        if (difyResp == null || difyResp.isEmpty()) {
+            return Result.error("AI 服务暂不可用,请稍后重试");
+        }
+        String answer = (String) difyResp.getOrDefault("answer", "");
+        String cleanAnswer = taskParseService.stripTaskMarkers(answer);
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("answer", cleanAnswer);
+        result.put("conversationId", difyResp.getOrDefault("conversationId", ""));
+        return Result.success(result);
+    }
+```
+
+- [ ] **步骤 5:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile -q -Dmaven.test.skip=true`
+预期:BUILD SUCCESS(无输出即成功)
+注意:若 `User.getMemberLevel()` 不存在,检查 `entity/User.java` 是否有 `memberLevel` 字段(AGENTS.md 显示 `User.java:136` 有 `private String memberLevel;`)
+
+- [ ] **步骤 6:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java
+git commit -m "feat: 新增报告快速分析端点 /api/ai/report/analyze(免费版Redis每日1次+付费版多轮)"
+```
+
+---
+
+## 任务 3:前端 api.js 新增 reportQuickAnalyze 封装
+
+**文件:**
+- 修改:`cfc-frontend/utils/api.js:1426`(`aiDeleteConversation` 之后)
+
+- [ ] **步骤 1:新增封装函数**
+
+在 `aiDeleteConversation` 函数后添加:
+
+```javascript
+// 报告快速分析(免费版每日1次,付费版多轮)
+export const reportQuickAnalyze = (query, reportId, conversationId) => {
+  var data = { query: query, reportId: reportId }
+  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**
+
+```bash
+git add cfc-frontend/utils/api.js
+git commit -m "feat(frontend): api.js 新增 reportQuickAnalyze 封装"
+```
+
+---
+
+## 任务 4:report-detail.vue 底部悬浮按钮 + 浮层
+
+**文件:**
+- 修改:`cfc-frontend/pages/health/report-detail.vue`
+
+**背景:** `report-detail.vue` 共 2162+ 行(大文件),本次只做增量添加,不重构。按钮显示在 view 模式(`pageMode==='view'`)且 `reportId` 存在时。浮层为固定定位弹层,含消息区、输入框、3 个快捷问题。
+
+- [ ] **步骤 1:script 引入 reportQuickAnalyze + 新增 data**
+
+`import` 行(394 行)添加 `reportQuickAnalyze`:
+
+```javascript
+import { getReportDetail, getDanReportDetail, getReportPayload, editHealthReport, parseReportDraft, confirmReportPreview, discardReportDraft, confirmTongue, discardTongue, getFamilyMemberList, addFamilyMember, queryIndicatorKnowledge, queryBacteriaKnowledge, reportQuickAnalyze } from '../../utils/api.js'
+```
+
+`data()` 内 `groupExpandList: []` 附近添加:
+
+```javascript
+      // 报告快速分析浮层
+      quickAnalyze: {
+        show: false,
+        sending: false,
+        messages: [],      // [{role:'user'|'ai', content}]
+        inputText: '',
+        conversationId: '',
+        usedToday: false    // 免费版今日已用(前端预判,最终以后端为准)
+      }
+```
+
+- [ ] **步骤 2:新增快捷问题数组(computed 或 data 常量)**
+
+在 `computed` 中添加(free 版 3 个预设问题):
+
+```javascript
+    quickQuestions: function() {
+      return [
+        '这份报告最需要关注哪些异常指标?',
+        '报告中提示了哪些健康风险?',
+        '有哪些改善建议?'
+      ]
+    }
+```
+
+- [ ] **步骤 3:新增浮层 methods**
+
+在 `methods` 中(`loadData` 附近)添加:
+
+```javascript
+    // ===== 报告快速分析 =====
+    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
+      reportQuickAnalyze(text, this.reportId, this.quickAnalyze.conversationId).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
+      })
+    }
+```
+
+> **注意:** `getMyMembership` 需从 `../../utils/api.js` import。若 `report-detail.vue` 当前未 import,加到 import 行。
+
+- [ ] **步骤 4:模板添加悬浮按钮 + 浮层**
+
+在 `<template>` 的 `</scroll-view>` 之后、`</view>`(根容器)之前插入:
+
+```html
+      <!-- 报告快速分析入口(仅查看模式) -->
+      <view class="quick-analyze-fab" v-if="pageMode==='view' && reportId" @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>
+```
+
+- [ ] **步骤 5:添加样式**
+
+在 `<style scoped>` 末尾(`.bottom-spacer` 样式之后)添加:
+
+```css
+/* ===== 报告快速分析 ===== */
+.quick-analyze-fab {
+  position: fixed;
+  right: 30rpx;
+  bottom: 60rpx;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  background: linear-gradient(135deg, #5B9BD5, #3B82F6);
+  border-radius: 44rpx;
+  padding: 18rpx 30rpx;
+  box-shadow: 0 4rpx 16rpx rgba(59, 130, 246, 0.4);
+  z-index: 90;
+}
+.quick-analyze-fab-icon { font-size: 32rpx; margin-right: 10rpx; }
+.quick-analyze-fab-text { font-size: 28rpx; color: #fff; font-weight: 500; }
+.quick-analyze-fab:active { opacity: 0.85; }
+
+.quick-analyze-overlay {
+  position: fixed;
+  top: 0; left: 0; right: 0; bottom: 0;
+  background: rgba(0, 0, 0, 0.45);
+  z-index: 98;
+}
+.quick-analyze-panel {
+  position: fixed;
+  left: 30rpx; right: 30rpx;
+  bottom: 40rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx;
+  z-index: 99;
+  display: flex;
+  flex-direction: column;
+  max-height: 70vh;
+}
+.quick-analyze-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 16rpx;
+}
+.quick-analyze-title { font-size: 30rpx; font-weight: bold; color: #333; }
+.quick-analyze-close { font-size: 32rpx; color: #999; padding: 8rpx; }
+.quick-analyze-hint {
+  font-size: 24rpx;
+  color: #E65100;
+  background: #FFF3E0;
+  border-radius: 12rpx;
+  padding: 12rpx 16rpx;
+  margin-bottom: 12rpx;
+}
+.quick-analyze-messages {
+  max-height: 50vh;
+  margin-bottom: 16rpx;
+}
+.quick-analyze-message {
+  margin-bottom: 12rpx;
+  display: flex;
+  flex-direction: column;
+}
+.quick-analyze-message.msg-user { align-items: flex-end; }
+.quick-analyze-message.msg-ai { align-items: flex-start; }
+.quick-analyze-msg-text {
+  font-size: 26rpx;
+  color: #333;
+  background: #F3F4F6;
+  border-radius: 14rpx;
+  padding: 14rpx 18rpx;
+  max-width: 80%;
+  line-height: 1.5;
+}
+.msg-user .quick-analyze-msg-text { background: #5B9BD5; color: #fff; }
+.quick-analyze-loading { display: flex; justify-content: center; padding: 12rpx 0; }
+.quick-analyze-loading-text { font-size: 24rpx; color: #999; }
+.quick-analyze-chips { display: flex; flex-direction: column; margin-bottom: 16rpx; }
+.quick-analyze-chip {
+  background: #EFF6FF;
+  border: 1rpx solid #BFDBFE;
+  border-radius: 12rpx;
+  padding: 14rpx 18rpx;
+  margin-bottom: 10rpx;
+}
+.quick-analyze-chip:active { opacity: 0.8; }
+.quick-analyze-chip-text { font-size: 26rpx; color: #1D4ED8; }
+.quick-analyze-input-bar {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+.quick-analyze-input {
+  flex: 1;
+  height: 72rpx;
+  background: #F3F4F6;
+  border-radius: 36rpx;
+  padding: 0 28rpx;
+  font-size: 26rpx;
+  color: #333;
+  margin-right: 16rpx;
+}
+.quick-analyze-send {
+  background: #5B9BD5;
+  border-radius: 36rpx;
+  padding: 16rpx 34rpx;
+}
+.quick-analyze-send.send-disabled { opacity: 0.5; }
+.quick-analyze-send-text { font-size: 26rpx; color: #fff; }
+```
+
+- [ ] **步骤 6:语法校验**
+
+运行:`cd cfc-frontend && node -e "const fs=require('fs');const src=fs.readFileSync('pages/health/report-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
+
+- [ ] **步骤 7:Commit**
+
+```bash
+git add cfc-frontend/pages/health/report-detail.vue
+git commit -m "feat(frontend): 报告详情页新增快速分析浮层(免费版单次/付费版多轮)"
+```
+
+---
+
+## 任务 5:验证
+
+**文件:**
+- 验证:`cfc-backend` 编译 + 手动验证清单
+
+- [ ] **步骤 1:后端全量编译**
+
+运行:`cd cfc-backend && mvn clean compile -q -Dmaven.test.skip=true`
+预期:BUILD SUCCESS(无输出即成功)
+
+- [ ] **步骤 2:手动验证清单**
+
+| 场景 | 预期 |
+|------|------|
+| 免费版第 1 次提问 | 返回 answer |
+| 免费版第 2 次提问 | 返回「免费版每日仅限 1 次快速分析,升级会员解锁更多」 |
+| 免费版次日 | 计数重置(Redis TTL 24h) |
+| 付费版多轮 | 追问带 conversationId,AI 记得上文 |
+| 报告上下文注入 | 回答体现报告内容(指标/菌群) |
+| Redis 停掉 | 免费版仍可用(fail-open),后端日志记 warn |
+
+- [ ] **步骤 3:最终 Commit(若有无需变更则跳过)**
+
+```bash
+git status --short
+# 确认无遗漏
+```
+
+---
+
+## 自检结果
+
+**规格覆盖度检查:**
+- ✅ 会员限制(FREE 每日 1 次 Redis 计数)→ 任务 2 步骤 4
+- ✅ 付费版多轮(conversationId 回传)→ 任务 2 步骤 4 + 任务 4 步骤 3
+- ✅ 免费版锁死多轮(remove conversationId)→ 任务 2 步骤 4
+- ✅ Redis 依赖 + 配置 → 任务 1
+- ✅ 复用 buildChatInputs → 任务 2 步骤 3
+- ✅ 前端浮层(按钮/输入框/快捷问题/加载态/禁用态)→ 任务 4
+- ✅ api.js 封装 → 任务 3
+- ✅ Redis fail-open 降级 → 任务 2 步骤 4
+- ✅ 错误处理(空 query/空 reportId/超限/AI 不可用)→ 任务 2 步骤 4
+
+**占位符扫描:** 无 TODO/待定/后续实现 等占位符。
+
+**类型一致性:**
+- `buildChatInputs(Long userId, Map<String, String> params)` → 任务 2 步骤 3 定义,步骤 4 调用,签名一致
+- `reportQuickAnalyze(query, reportId, conversationId)` → 任务 3 定义,任务 4 步骤 3 调用,签名一致
+- `quickAnalyze.usedToday` → 任务 4 步骤 1 定义,步骤 3/4 使用,一致
+- `reportId` → 任务 4 步骤 3 使用 `this.reportId`,data 已有(563 行 `reportId: null`),一致