|
@@ -0,0 +1,561 @@
|
|
|
|
|
+# 报告快速分析扩展实现计划
|
|
|
|
|
+
|
|
|
|
|
+> **面向 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` 参数,调用新方法<br>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;
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **步骤 3:新增 buildContextFromDraft 方法**
|
|
|
|
|
+
|
|
|
|
|
+在 `buildContext(Long userId, Long reportId, Long surveyId)` 方法结束后(约 222 行)添加:
|
|
|
|
|
+```java
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 基于草稿 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**
|
|
|
|
|
+
|
|
|
|
|
+```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<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` 获取,修改校验为:
|
|
|
|
|
+```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 新增**
|
|
|
|
|
+
|
|
|
|
|
+第 329 行 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 + 浮层**
|
|
|
|
|
+
|
|
|
|
|
+在 `</scroll-view>` 之后(约 223 行),`<health-knowledge-popup>` 之前插入:
|
|
|
|
|
+```html
|
|
|
|
|
+ <!-- 报告快速分析入口(仅查看模式,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**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+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`:
|
|
|
|
|
+```javascript
|
|
|
|
|
+import { confirmReportPreview, discardReportDraft, confirmTongue, discardTongue, parseReportDraft, reportQuickAnalyze, getMyMembership } from '../../utils/api.js'
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **步骤 2:data 新增 quickAnalyze 对象**
|
|
|
|
|
+
|
|
|
|
|
+在 `extractedAge: ''`(约 394 行)之后添加(同 gut-flora-detail):
|
|
|
|
|
+```javascript
|
|
|
|
|
+ // 报告快速分析浮层
|
|
|
|
|
+ quickAnalyze: {
|
|
|
|
|
+ show: false,
|
|
|
|
|
+ sending: false,
|
|
|
|
|
+ messages: [],
|
|
|
|
|
+ inputText: '',
|
|
|
|
|
+ conversationId: '',
|
|
|
|
|
+ usedToday: false
|
|
|
|
|
+ }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **步骤 3:computed 新增 quickQuestions**
|
|
|
|
|
+
|
|
|
|
|
+在 `memberDisplayText` computed 后(约 441 行)添加:
|
|
|
|
|
+```javascript
|
|
|
|
|
+ quickQuestions: function() {
|
|
|
|
|
+ return [
|
|
|
|
|
+ '这份报告最需要关注哪些异常指标?',
|
|
|
|
|
+ '报告中提示了哪些健康风险?',
|
|
|
|
|
+ '有哪些改善建议?'
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **步骤 4:methods 新增浮层逻辑**
|
|
|
|
|
+
|
|
|
|
|
+在 `memberDisplayText` 方法后添加(同 gut-flora-detail,但传参只需 `draftId`):
|
|
|
|
|
+```javascript
|
|
|
|
|
+ // ===== 报告快速分析 =====
|
|
|
|
|
+ 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>` 之前插入:
|
|
|
|
|
+```html
|
|
|
|
|
+ <!-- 报告快速分析入口(解析完成、非解析中) -->
|
|
|
|
|
+ <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**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+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(若有无需变更则跳过)**
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+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 行),一致
|