# 报告快速分析扩展实现计划
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
**目标:** 在 `gut-flora-detail.vue` 和 `report-confirm.vue` 新增「报告快速分析」入口,后端支持 `draftId` 与 `reportId` 双模式,复用现有免费版限制与 AI 链路。
**架构:** 后端 `FamilyContextService` 新增 `buildContextFromDraft`,`AIChatController` 扩展 `buildChatInputs` 与 `reportAnalyze` 校验;前端 `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 || draftId`,为空则报错 |
### 前端(cfc-frontend)
| 文件 | 操作 | 职责 |
|------|------|------|
| `utils/api.js` | 修改 | `reportQuickAnalyze(query, reportId, conversationId, draftId)` 新增第四参数 |
| `pages/health/gut-flora-detail.vue` | 修改 | 复用浮层逻辑/样式,FAB 条件 `!isEditing && (reportId || draftId)` |
| `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 行)添加:
```java
import com.etotem.cfc.service.HealthReportDraftService;
import com.fasterxml.jackson.databind.ObjectMapper;
```
- [ ] **步骤 2:注入依赖**
在 `@Resource private ReportSurveyService reportSurveyService;`(约 36 行)之后添加:
```java
@Resource
private HealthReportDraftService healthReportDraftService;
@Resource
private ObjectMapper objectMapper;
```
(`FamilyContextService` 目前未注入 `ObjectMapper`,需一并补上)
- [ ] **步骤 3:新增 buildContextFromDraft 方法**
在 `buildContext(Long userId, Long reportId, Long surveyId)` 方法结束后(约 222 行)添加:
```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:编译验证**
运行:`cd cfc-backend && mvn clean compile -q -Dmaven.test.skip=true`
预期:BUILD SUCCESS(无输出即成功)
- [ ] **步骤 5:Commit**
```bash
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 行)添加:
```java
String draftIdStr = params.get("draftId");
Long draftId = null;
if (draftIdStr != null && !draftIdStr.trim().isEmpty()) {
draftId = Long.valueOf(draftIdStr);
}
```
修改上下文组装分支(约 155-162 行):
```java
Map 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` 获取,修改校验为:
```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不能为空");
}
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**
```bash
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:修改函数签名**
```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)
}
```
- [ ] **步骤 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 新增可选 draftId 参数"
```
---
## 任务 4:前端 gut-flora-detail.vue 新增快速分析浮层
**文件:**
- 修改:`cfc-frontend/pages/health/gut-flora-detail.vue`
**策略:** 直接复制 `report-detail.vue` 的 `quickAnalyze` data/computed/methods/template/style,仅改入口条件与 import。
- [ ] **步骤 1:import 新增**
第 335 行第一个 import 行添加 `reportQuickAnalyze, getMyMembership`:
```javascript
import { getReportDetail, editHealthReport, queryKnowledgeBase, queryIndicatorKnowledge, getGutFloraAnalysis, confirmReportPreview, discardReportDraft, parseReportDraft, getHealthFoods, getFoodCautionList, getFamilyMemberList, addFamilyMember, reportQuickAnalyze, getMyMembership } from '../../utils/api.js'
```
- [ ] **步骤 2:data 新增 quickAnalyze 对象**
在 `extractedAge: ''`(约 394 行)之后添加:
```javascript
// 报告快速分析浮层
quickAnalyze: {
show: false,
sending: false,
messages: [], // [{role:'user'|'ai', content}]
inputText: '',
conversationId: '',
usedToday: false // 免费版今日已用(前端预判,最终以后端为准)
}
```
- [ ] **步骤 3:computed 新增 quickQuestions**
在 `draftMemberText` computed 后(约 460 行)添加:
```javascript
quickQuestions: function() {
return [
'这份报告最需要关注哪些异常指标?',
'报告中提示了哪些健康风险?',
'有哪些改善建议?'
]
}
```
- [ ] **步骤 4:methods 新增浮层逻辑**
在 `draftMemberText` 方法后(约 460 行)添加:
```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
// 传 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 + 浮层**
在 `` 之后(约 229 行),`` 之前,且在编辑 FAB(✏️)之前插入:
```html
🎯
快速分析
免费版今日次数已用完,升级会员解锁更多分析
{{ msg.content }}
AI 正在分析中...
{{ q }}
发送
```
- [ ] **步骤 6:style 复用**
在 `