日期:2026-09-13 状态:设计中
当前「报告快速分析」功能(/api/ai/report/analyze)仅在 report-detail.vue(view 模式)可用。用户上传报告后:
gut-flora-detail.vue(无快速分析入口)report-confirm.vue(无快速分析入口)目标:在 gut-flora-detail.vue 和 report-confirm.vue 也提供「快速分析」入口,支持已入库的 reportId 和未入库的 draftId。
POST /api/ai/report/analyze
Request: { query, reportId?, draftId?, conversationId?, memberId? }
reportId 和 draftId(至少一个非空)reportId(已入库,完整上下文),其次用 draftId(草稿,读 payloadJson)chat_graph 零改动前端页面(gut-flora-detail / report-confirm)
│ POST /api/ai/report/analyze { query, reportId/draftId }
▼
AIChatController.reportAnalyze()
├─ 1. 会员级别检查(user.memberLevel)
├─ 2. FREE → Redis 计数(fail-open)
├─ 3. buildChatInputs(userId, params)
│ ├─ 有 reportId → familyContextService.buildContext(userId, reportId)
│ └─ 只有 draftId → familyContextService.buildContextFromDraft(userId, draftId) [新增]
└─ 4. aiService.sendMessage() → LangGraph chat_graph
| 页面 | 入口条件 | 传参 |
|---|---|---|
report-detail.vue |
view 模式 + reportId | reportId(已有) |
gut-flora-detail.vue |
(!isEditing && !isDraftMode && reportId) || (isDraftMode && draftId) | reportId 或 draftId |
report-confirm.vue |
!parsing && draftId | draftId |
| 文件 | 改动 |
|---|---|
FamilyContextService.java |
新增 buildContextFromDraft(Long userId, Long draftId),注入 HealthReportDraftService,查草稿 → 解析 payloadJson → 返回含 reportDetail 的上下文 |
AIChatController.java |
1. buildChatInputs:检测 params.get("draftId"),调用新方法2. reportAnalyze:校验改为 `reportId |
| 文件 | 改动 |
|---|---|
utils/api.js |
reportQuickAnalyze 新增可选参数 draftId |
pages/health/gut-flora-detail.vue |
1. import 新增 reportQuickAnalyze、getMyMembership2. data 新增 quickAnalyze 对象3. computed 新增 quickQuestions4. methods 新增 openQuickAnalyze、closeQuickAnalyze、sendQuickQuestion、sendQuickAnalyze5. template:FAB 区域(第 229 行附近)加「🎯 快速分析」按钮 + 浮层结构 6. style:复用 report-detail.vue 样式( .quick-analyze-* 类) |
pages/health/report-confirm.vue |
同 gut-flora-detail.vue,但入口条件为 !parsing && draftId |
/**
* 基于草稿 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;
}
private Map<String, Object> buildChatInputs(Long userId, Map<String, String> params) {
// ... 现有 mascot、memberId、surveyId、selfCheckId 逻辑不变 ...
Long reportId = null;
Long draftId = null;
if (reportIdStr != null && !reportIdStr.trim().isEmpty()) {
reportId = Long.valueOf(reportIdStr);
}
String draftIdStr = params.get("draftId");
if (draftIdStr != null && !draftIdStr.trim().isEmpty()) {
draftId = Long.valueOf(draftIdStr);
}
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);
}
// ... mascot、portrait、selfCheck、memory 后续逻辑不变 ...
}
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不能为空");
}
// 报告快速分析(免费版每日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)
}
<!-- 仅查看模式:reportId 或 draftId 存在时 -->
<view class="quick-analyze-fab" v-if="!isEditing && (reportId || draftId)" @tap="openQuickAnalyze">
<!-- 解析完成、非解析中 -->
<view class="quick-analyze-fab" v-if="!parsing && draftId" @tap="openQuickAnalyze">
user.memberLevel 判定 FREE/FAMILY/PROVIDERreport:analyze:free:userId:date,无区分 reportId/draftId)conversationId → LangGraph 每次新建会话 → 锁死多轮conversationId 实现多轮| 场景 | 行为 |
|---|---|
| query 为空 | Result.error("消息不能为空") |
| reportId 和 draftId 均空 | Result.error("报告ID或草稿ID不能为空") |
| 草稿不存在/无权限 | 静默降级,仅返回基础上下文(AI 仅做通用回答) |
| 免费版超限 | Result.error("免费版每日仅限 1 次快速分析,升级会员解锁更多") |
| Redis 异常 | fail-open 放行 + warn 日志 |
| AI 服务不可用 | Result.error("AI 服务暂不可用,请稍后重试") |
| 场景 | 预期 |
|---|---|
| gut-flora-detail(reportId)点击快速分析 | 正常返回 answer |
| gut-flora-detail(draftId)点击快速分析 | 正常返回 answer(基于草稿 payload) |
| report-confirm(draftId)点击快速分析 | 正常返回 answer |
| 免费版第 2 次(任意页面) | 返回超限错误 |
| 付费版多轮追问 | 带 conversationId,AI 记得上文 |
| Redis 停掉 | 免费版仍可用(fail-open) |
payloadJson 是 ParsedReportPayload.Payload 的 JSON,结构与 getReportDetail 返回的 reportDetail 兼容(含 summary/indicators/gutFlora/diseaseRisks 等)getDraftById 后需校验 draft.getUserId().equals(userId).quick-analyze-* CSS 类名,避免重复维护