面向 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 表达式)
| 文件 | 操作 | 职责 |
|---|---|---|
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 |
| 文件 | 操作 | 职责 |
|---|---|---|
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 |
文件:
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyContextService.java背景: 现有 buildContext(userId, reportId) 读已入库报告;草稿场景需读 HealthReportDraft.payloadJson 并解析为 Map 注入上下文。
在 import 区(现有 import 在 20-62 行)添加:
import com.etotem.cfc.service.HealthReportDraftService;
import com.fasterxml.jackson.databind.ObjectMapper;
在 @Resource private ReportSurveyService reportSurveyService;(约 36 行)之后添加:
@Resource
private HealthReportDraftService healthReportDraftService;
@Resource
private ObjectMapper objectMapper;
在 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;
}
运行: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 支持草稿上下文"
文件:
cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java背景: 现有 buildChatInputs 只处理 reportId/surveyId/selfCheckId;需加 draftId 分支。reportAnalyze 校验需接受 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);
}
在 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 行)删除。
运行: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 参数的快速分析"
文件:
修改: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 参数"
文件:
cfc-frontend/pages/health/gut-flora-detail.vue策略: 直接复制 report-detail.vue 的 quickAnalyze data/computed/methods/template/style,仅改入口条件与 import。
第 329 行 import 行添加 reportQuickAnalyze, getMyMembership:
import { getReportDetail, editHealthReport, queryKnowledgeBase, queryIndicatorKnowledge, getGutFloraAnalysis, confirmReportPreview, discardReportDraft, parseReportDraft, getHealthFoods, getFoodCautionList, getFamilyMemberList, addFamilyMember, reportQuickAnalyze, getMyMembership } from '../../utils/api.js'
在 extractedAge: ''(约 394 行)之后添加:
// 报告快速分析浮层
quickAnalyze: {
show: false,
sending: false,
messages: [], // [{role:'user'|'ai', content}]
inputText: '',
conversationId: '',
usedToday: false // 免费版今日已用(前端预判,最终以后端为准)
}
在 draftMemberText computed 后(约 460 行)添加:
quickQuestions: function() {
return [
'这份报告最需要关注哪些异常指标?',
'报告中提示了哪些健康风险?',
'有哪些改善建议?'
]
}
在 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
})
}
在 </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>
在 <style scoped> 末尾(约 1310 行)添加 report-detail.vue 的 .quick-analyze-* 完整样式(第 2292-2398 行)。
运行: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)"
文件:
修改: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'
在 extractedAge: ''(约 394 行)之后添加(同 gut-flora-detail):
// 报告快速分析浮层
quickAnalyze: {
show: false,
sending: false,
messages: [],
inputText: '',
conversationId: '',
usedToday: false
}
在 memberDisplayText computed 后(约 441 行)添加:
quickQuestions: function() {
return [
'这份报告最需要关注哪些异常指标?',
'报告中提示了哪些健康风险?',
'有哪些改善建议?'
]
}
在 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
})
}
在 </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>
在 <style scoped> 末尾添加同一套 .quick-analyze-* 样式。
运行: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)"
文件:
验证:后端编译 + 手动验证清单
[ ] 步骤 1:后端全量编译
运行:cd cfc-backend && mvn clean compile -q -Dmaven.test.skip=true
预期:BUILD SUCCESS
| 场景 | 预期 |
|---|---|
| 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
# 确认无遗漏
规格覆盖度检查:
占位符扫描: 无 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 行),一致