Просмотр исходного кода

fix(report): T29-B1 报告代传功能修复第一批 — 主链路打通 + 数据正确性

B1-1 proxyParsePreview 重构:先建草稿拿 draftId(payload_json NOT NULL 先写空载荷 JSON,
解析后 updatePayload 覆盖)→ updateNote 备注 → LangGraph 解析(失败回退本地 Java 解析)→
返回 draftId/payload/matchedMemberId/extracted*(含 report_number/report_date)/needBind;
解析失败 markDiscarded 丢弃空载荷草稿
B1-2 setProxyUploader 增加 note 参数写 proxy_upload_note;proxyConfirm 从草稿 remark 取回
B1-3 buildReportFromPayload remark 优先草稿 remark,否则默认文案
B1-4 管理端/老师端详情弹窗改取 res.data.report 层
B1-5 triggerAiAnalysis 返回结果 Map:失败不再静默(quota/langgraph_failed + message 回传),
成功写 ai_analysis 并回传 aiAnalysis
B1-6 列表补展示字段:admin-list 每行 title/createdAt/proxyUploaderName;
teacher /report/list 实体补 subjectName/proxyUploaderName(@TableField exist=false);
详情也回填 proxyUploaderName
前端预览三行 score 字段对齐后端 payload.summary.* 结构
Sisyphus 2 недель назад
Родитель
Сommit
7680b21e77

+ 154 - 25
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java

@@ -357,8 +357,13 @@ public class HealthReportController {
         List<HealthReport> merged = mergeDanReports(reports, memberId, familyId, visibleForDan, userId);
         List<HealthReport> merged = mergeDanReports(reports, memberId, familyId, visibleForDan, userId);
 
 
         if (size > 0) {
         if (size > 0) {
-            return Result.success(limitLatest(merged, size));
+            // B1-6:列表补展示字段(报告对象姓名 / 代上传人姓名),实体上的 @TableField(exist=false) 字段
+            List<HealthReport> limited = limitLatest(merged, size);
+            healthReportService.fillSubjectAndProxyNames(limited);
+            return Result.success(limited);
         }
         }
+        // B1-6:列表补展示字段(报告对象姓名 / 代上传人姓名)
+        healthReportService.fillSubjectAndProxyNames(merged);
         return Result.success(merged);
         return Result.success(merged);
     }
     }
 
 
@@ -1188,6 +1193,9 @@ public class HealthReportController {
             result.put("extractedName", parsed.getPersonName());
             result.put("extractedName", parsed.getPersonName());
             result.put("extractedGender", parsed.getGender());
             result.put("extractedGender", parsed.getGender());
             result.put("extractedAge", parsed.getAge());
             result.put("extractedAge", parsed.getAge());
+            // B1-1:前端预览三行(被检测人/报告编号/报告日期)还读这两个字段,补齐避免恒 '-'
+            result.put("extractedReportNumber", parsed.getReportNumber());
+            result.put("extractedReportDate", parsed.getReportDate());
             result.put("needBind", matchResult.getMatchedMemberId() == null && familyId != null);
             result.put("needBind", matchResult.getMatchedMemberId() == null && familyId != null);
             return Result.success(result);
             return Result.success(result);
         }
         }
@@ -2611,6 +2619,14 @@ public class HealthReportController {
 
 
     /**
     /**
      * 规划师/营养师代用户上传报告(预览模式,仅解析不入库)
      * 规划师/营养师代用户上传报告(预览模式,仅解析不入库)
+     *
+     * <p>T29-B1-1:重构为与「两阶段入库」一致的模式——先建草稿拿到 draftId,再解析并回写草稿载荷,
+     * 前端「确认入库」才能拿到 draftId(原先透传 LangGraph 原始返回,恒无 draftId → 前端卡死)。</p>
+     * <p>链路:storeFile → createDraft → updateNote(备注) → getFilePath → LangGraph 解析
+     * (失败回退本地 Java 解析)→ 返回 draftId/payload/成员匹配/提取字段。</p>
+     *
+     * <p>权限编排说明:① 角色校验 isProxyUploader;② familyId 非空校验;
+     * ③ 家庭访问拦截器兜底(代传绑定校验由 T29-B2 补充)。</p>
      */
      */
     @Operation(summary = "代上传报告(预览)")
     @Operation(summary = "代上传报告(预览)")
     @PostMapping("/report/proxy-parse")
     @PostMapping("/report/proxy-parse")
@@ -2638,27 +2654,53 @@ public class HealthReportController {
             return Result.error("familyId不能为空");
             return Result.error("familyId不能为空");
         }
         }
 
 
+        String contentType = file.getContentType();
+        String originalFilename = file.getOriginalFilename();
+        String lcName = originalFilename != null ? originalFilename.toLowerCase() : "";
+        if (!lcName.endsWith(".pdf")
+                && (contentType == null || !contentType.equalsIgnoreCase("application/pdf"))) {
+            return Result.error("仅支持PDF文件");
+        }
+
         try {
         try {
+            // ① 存文件(本地磁盘 / 京东云 OSS)
             String fileUrl = storageService.storeFile(file, "health-reports");
             String fileUrl = storageService.storeFile(file, "health-reports");
-            String absolutePath = storageService.getFilePath(fileUrl);
 
 
-            // 调用 LangGraph 解析
-            Map<String, Object> result = callLangGraphParse(absolutePath, userId, familyId, subjectId);
+            // ② 先建草稿:保证预览必然有 draftId 供 /report/proxy-confirm 入库
+            //    reportType 传 auto 时落 gut_flora(与非代传链路一致)
+            //    health_report_drafts.payload_json 为 NOT NULL 列,先写空载荷 JSON
+            //    (与图片上传链路一致),解析成功后由 updatePayload 覆盖为真实载荷
+            String draftReportType = (reportType == null || reportType.isEmpty() || "auto".equals(reportType))
+                    ? "gut_flora" : reportType;
+            String emptyPayloadJson = objectMapper.writeValueAsString(new ParsedReportPayload.Payload());
+            HealthReportDraft draft = healthReportDraftService.createDraft(
+                    userId, familyId, draftReportType, fileUrl, originalFilename, emptyPayloadJson);
 
 
-            if (result == null) {
-                return Result.error("报告解析失败");
+            // ③ 备注写入草稿 remark(updateNote 实际写 remark 列),确认入库时同步到 proxy_upload_note
+            if (note != null && !note.isEmpty()) {
+                healthReportDraftService.updateNote(draft.getId(), note);
             }
             }
 
 
-            result.put("fileUrl", fileUrl);
-            // 保存草稿
-            Long draftId = (Long) result.get("draftId");
-            if (draftId != null && note != null) {
-                healthReportDraftService.updateNote(draftId, note);
+            // ④ LangGraph 需要本地路径:经 getFilePath 下载/定位到共享卷
+            String absolutePath = storageService.getFilePath(fileUrl);
+
+            // ⑤ 解析(LangGraph 优先,失败回退本地 Java 解析)
+            Map<String, Object> data = parseProxyDraftFile(draft, absolutePath, userId, familyId, subjectId);
+            if (data == null) {
+                // 解析失败:草稿已无后续用途,标记丢弃,避免遗留空载荷 pending 草稿被误确认入库
+                try {
+                    healthReportDraftService.markDiscarded(draft.getId());
+                } catch (Exception ex) {
+                    log.warn("标记代传草稿为已丢弃失败 draftId={}: {}", draft.getId(), ex.getMessage());
+                }
+                return Result.error("报告解析失败,请确认是募极生物肠道菌群报告PDF");
             }
             }
 
 
-            result.put("proxyUploaderId", userId);
-            result.put("proxyUploadNote", note);
-            return Result.success(result);
+            // ⑥ 返回体补代传相关字段(前端既有字段契约)
+            data.put("fileUrl", fileUrl);
+            data.put("proxyUploaderId", userId);
+            data.put("proxyUploadNote", note);
+            return Result.success(data);
 
 
         } catch (Exception e) {
         } catch (Exception e) {
             log.error("代上传解析失败", e);
             log.error("代上传解析失败", e);
@@ -2666,6 +2708,56 @@ public class HealthReportController {
         }
         }
     }
     }
 
 
+    /**
+     * 代传草稿文件解析:LangGraph 优先,失败回退本地 Java 解析。
+     * 两路都会把解析载荷写回 draft.payload_json,并返回完整预览结果
+     * (draftId / payload / matchedMemberId / extracted 各字段 / needBind)。
+     *
+     * @return 解析结果体;文件不存在或两路均失败时返回 null
+     */
+    private Map<String, Object> parseProxyDraftFile(HealthReportDraft draft, String absolutePath,
+                                                     Long userId, Long familyId, Long subjectId) {
+        File localFile = new File(absolutePath);
+        if (!localFile.exists()) {
+            log.warn("代传解析:文件不存在 draftId={}, path={}", draft.getId(), absolutePath);
+            return null;
+        }
+        try {
+            Map<String, Object> lgData = callLangGraphParse(absolutePath, userId, familyId, subjectId);
+            if (lgData != null) {
+                Map<String, Object> data = buildParseResultFromLg(lgData, draft, familyId).getData();
+                putExtractedReportMeta(data, lgData);
+                return data;
+            }
+            log.warn("代传解析:LangGraph 无有效返回,回退本地 Java 解析 draftId={}", draft.getId());
+        } catch (Exception e) {
+            log.warn("代传解析:LangGraph 解析异常,回退本地 Java 解析 draftId={}: {}",
+                    draft.getId(), e.getMessage());
+        }
+        try {
+            return parseLocalFallback(absolutePath, draft, familyId, userId).getData();
+        } catch (Exception e) {
+            log.error("代传解析:本地 Java 解析失败 draftId={}", draft.getId(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 补齐 LangGraph 预览结果的 extractedReportNumber / extractedReportDate。
+     * buildParseResultFromLg 只回填 extractedName/Gender/Age,而前端预览页三行
+     * (被检测人/报告编号/报告日期)还读这两个字段,缺省会一直显示 '-'。
+     */
+    private void putExtractedReportMeta(Map<String, Object> data, Map<String, Object> lgData) {
+        if (data == null || lgData == null) {
+            return;
+        }
+        Map<String, Object> overview = (Map<String, Object>) lgData.get("overview");
+        if (overview != null) {
+            data.put("extractedReportNumber", objectToString(overview.get("report_number")));
+            data.put("extractedReportDate", objectToString(overview.get("report_date")));
+        }
+    }
+
     /**
     /**
      * 规划师/营养师代确认报告草稿(入库)
      * 规划师/营养师代确认报告草稿(入库)
      */
      */
@@ -2690,6 +2782,10 @@ public class HealthReportController {
         }
         }
 
 
         try {
         try {
+            // 先取草稿备注(代传 note):确认后写入 health_reports.proxy_upload_note
+            HealthReportDraft draft = healthReportDraftService.getDraftById(draftId);
+            String proxyUploadNote = draft != null ? draft.getRemark() : null;
+
             // 代上传模式跳过草稿归属校验
             // 代上传模式跳过草稿归属校验
             Map<String, Object> result = healthReportService.auditConfirm(draftId, subjectId, userId);
             Map<String, Object> result = healthReportService.auditConfirm(draftId, subjectId, userId);
             Long reportId = (Long) result.get("reportId");
             Long reportId = (Long) result.get("reportId");
@@ -2704,14 +2800,23 @@ public class HealthReportController {
                 }
                 }
             }
             }
 
 
-            // 设置代上传信息
+            // 设置代上传人 + 代上传备注
             if (reportId != null) {
             if (reportId != null) {
-                healthReportService.setProxyUploader(reportId, userId);
+                healthReportService.setProxyUploader(reportId, userId, proxyUploadNote);
             }
             }
 
 
-            // 触发 AI 解读
+            // 触发 AI 解读(T29-B1-5:失败不再静默吞错,结果/失败原因随响应回传前端)
+            Map<String, Object> aiResult = new HashMap<>();
             if (reportId != null && subjectId != null) {
             if (reportId != null && subjectId != null) {
-                triggerAiAnalysis(reportId, subjectId, userId);
+                aiResult = triggerAiAnalysis(reportId, subjectId, userId);
+            }
+            if (Boolean.TRUE.equals(aiResult.get("success"))) {
+                result.put("aiAnalysis", aiResult.get("analysis"));
+                result.put("aiAnalysisStatus", "success");
+            } else {
+                result.put("aiAnalysisStatus", aiResult.get("reason"));
+                Object aiMessage = aiResult.get("message") != null ? aiResult.get("message") : aiResult.get("toast");
+                result.put("aiAnalysisMessage", aiMessage);
             }
             }
 
 
             return Result.success(result);
             return Result.success(result);
@@ -2747,15 +2852,24 @@ public class HealthReportController {
     }
     }
 
 
     /**
     /**
-     * 触发 AI 解读
+     * 触发 AI 解读(T29-B1-5:失败不再静默吞错,结果与失败原因回传调用方)
+     *
+     * @return {success, analysis, traceId} 成功;
+     *         {success=false, reason="quota", toast} 配额不足;
+     *         {success=false, reason="langgraph_failed", message} LangGraph 失败或返回空解读
      */
      */
-    private void triggerAiAnalysis(Long reportId, Long subjectId, Long proxyUploaderId) {
+    private Map<String, Object> triggerAiAnalysis(Long reportId, Long subjectId, Long proxyUploaderId) {
+        Map<String, Object> aiResult = new HashMap<>();
+        aiResult.put("success", false);
+
         // BASIC 配额检查(报告 AI 解读,2026-10-23 新口径:BASIC 1 次/月;FAMILY 及以上不限次;FREE 维持既有口径扣减)
         // BASIC 配额检查(报告 AI 解读,2026-10-23 新口径:BASIC 1 次/月;FAMILY 及以上不限次;FREE 维持既有口径扣减)
         if (proxyUploaderId != null) {
         if (proxyUploaderId != null) {
             com.etotem.cfc.dto.QuotaConsumeResult quotaResult = basicQuotaService.consumeMemberQuota(proxyUploaderId, "REPORT_READING", false);
             com.etotem.cfc.dto.QuotaConsumeResult quotaResult = basicQuotaService.consumeMemberQuota(proxyUploaderId, "REPORT_READING", false);
             if (!quotaResult.isSuccess()) {
             if (!quotaResult.isSuccess()) {
                 log.info("报告 AI 解读配额不足: userId={}, toast={}", proxyUploaderId, quotaResult.getToastMsg());
                 log.info("报告 AI 解读配额不足: userId={}, toast={}", proxyUploaderId, quotaResult.getToastMsg());
-                return;
+                aiResult.put("reason", "quota");
+                aiResult.put("toast", quotaResult.getToastMsg());
+                return aiResult;
             }
             }
         }
         }
 
 
@@ -2769,19 +2883,34 @@ public class HealthReportController {
                     langgraphBaseUrl + "/api/v1/analyze", analyzeReq, Map.class);
                     langgraphBaseUrl + "/api/v1/analyze", analyzeReq, Map.class);
             if (resp.getBody() != null) {
             if (resp.getBody() != null) {
                 Map<String, Object> body = resp.getBody();
                 Map<String, Object> body = resp.getBody();
-                String analysis = (String) body.get("analysis");
+                String analysis = objectToString(body.get("analysis"));
+                String traceId = objectToString(body.get("trace_id"));
                 if (analysis != null && !analysis.isEmpty()) {
                 if (analysis != null && !analysis.isEmpty()) {
                     Map<String, Object> aiData = new HashMap<>();
                     Map<String, Object> aiData = new HashMap<>();
                     aiData.put("analysis", analysis);
                     aiData.put("analysis", analysis);
-                    aiData.put("traceId", body.get("trace_id"));
+                    aiData.put("traceId", traceId);
                     aiData.put("proxyUploaderId", proxyUploaderId);
                     aiData.put("proxyUploaderId", proxyUploaderId);
                     healthReportService.setAiAnalysis(reportId, aiData);
                     healthReportService.setAiAnalysis(reportId, aiData);
-                    log.info("AI解读已保存: reportId={}, traceId={}", reportId, body.get("trace_id"));
+                    log.info("AI解读已保存: reportId={}, traceId={}", reportId, traceId);
+                    aiResult.put("success", true);
+                    aiResult.put("analysis", analysis);
+                    aiResult.put("traceId", traceId);
+                    return aiResult;
                 }
                 }
+                aiResult.put("reason", "langgraph_failed");
+                aiResult.put("message", "AI解读服务返回空结果");
+                log.warn("AI解读返回空结果: reportId={}, body={}", reportId, body);
+            } else {
+                aiResult.put("reason", "langgraph_failed");
+                aiResult.put("message", "AI解读服务无响应");
+                log.warn("AI解读服务无响应: reportId={}", reportId);
             }
             }
         } catch (Exception e) {
         } catch (Exception e) {
-            log.warn("AI解读触发失败: {}", e.getMessage());
+            aiResult.put("reason", "langgraph_failed");
+            aiResult.put("message", "AI解读服务异常: " + e.getMessage());
+            log.warn("AI解读触发失败: {}", e.getMessage(), e);
         }
         }
+        return aiResult;
     }
     }
 
 
     /**
     /**

+ 8 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/HealthReport.java

@@ -119,6 +119,14 @@ public class HealthReport implements Serializable {
     /** AI解读结果(JSON: {analysis, recommendations, traceId}) */
     /** AI解读结果(JSON: {analysis, recommendations, traceId}) */
     private String aiAnalysis;
     private String aiAnalysis;
 
 
+    /** 报告对象姓名(列表展示用,非库列:subjectId → family_members 昵称 / users 姓名) */
+    @TableField(exist = false)
+    private String subjectName;
+
+    /** 代上传人姓名(列表展示用,非库列:proxy_uploader_id → users 姓名) */
+    @TableField(exist = false)
+    private String proxyUploaderName;
+
     @TableField("shannon_index")
     @TableField("shannon_index")
     private String shannonIndex;
     private String shannonIndex;
 
 

+ 103 - 3
cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java

@@ -607,6 +607,10 @@ public class HealthReportService {
         Map<String, Object> detail = new HashMap<>();
         Map<String, Object> detail = new HashMap<>();
 
 
         HealthReport report = healthReportMapper.selectById(reportId);
         HealthReport report = healthReportMapper.selectById(reportId);
+        // B1-6:详情弹窗也需展示代上传人姓名(@TableField(exist=false) 字段)
+        if (report != null) {
+            report.setProxyUploaderName(resolveProxyUploaderName(report.getProxyUploaderId()));
+        }
         detail.put("report", report);
         detail.put("report", report);
 
 
         if (report != null) {
         if (report != null) {
@@ -1204,6 +1208,21 @@ public class HealthReportService {
             if (m.get("childName") == null) {
             if (m.get("childName") == null) {
                 m.put("childName", r.getPersonName());
                 m.put("childName", r.getPersonName());
             }
             }
+            // B1-6:代传列表展示字段 —— 标题(姓名+类型标签,缺省用报告编号)、上传时间、代上传人
+            String baseName = r.getPersonName() != null && !r.getPersonName().isEmpty()
+                    ? r.getPersonName() : (String) m.get("childName");
+            String title;
+            if (baseName != null && !baseName.isEmpty()) {
+                title = baseName + "-" + reportTypeLabel(r.getReportType());
+            } else if (r.getReportNumber() != null && !r.getReportNumber().isEmpty()) {
+                title = r.getReportNumber();
+            } else {
+                title = "报告" + r.getId();
+            }
+            m.put("title", title);
+            m.put("createdAt", r.getCreatedAt());
+            String uploaderName = resolveUserName(r.getProxyUploaderId());
+            m.put("proxyUploaderName", uploaderName != null ? uploaderName : "-");
             records.add(m);
             records.add(m);
         }
         }
 
 
@@ -1215,6 +1234,83 @@ public class HealthReportService {
         return result;
         return result;
     }
     }
 
 
+    // ======================== 列表展示字段回填(T29-B1-6) ========================
+
+    /** 报告类型展示标签(与前端 typeLabel 映射保持一致) */
+    String reportTypeLabel(String reportType) {
+        if (reportType == null) return "-";
+        switch (reportType) {
+            case "gut_flora": return "菌群检测";
+            case "physical_exam": return "体检报告";
+            case "dan":
+            case "dan_a2": return "DAN测评";
+            case "insurance": return "保单";
+            case "fortune": return "家庭周报";
+            default: return reportType;
+        }
+    }
+
+    /** 用户展示名:realName 优先,缺省 nickname;查不到返回 null */
+    private String resolveUserName(Long userId) {
+        if (userId == null) return null;
+        try {
+            User u = userMapper.selectById(userId);
+            if (u == null) return null;
+            String name = u.getRealName() != null && !u.getRealName().trim().isEmpty()
+                    ? u.getRealName() : u.getNickname();
+            return (name == null || name.trim().isEmpty()) ? null : name;
+        } catch (Exception e) {
+            log.warn("查询用户姓名失败 userId={}: {}", userId, e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * 报告对象展示名(subjectId → 报告所属家庭内的成员昵称,其次 users 姓名,再次跨家庭成员行昵称)。
+     * subjectId 在确认链路已归一化为 family_members.id(T25 A方案),但历史数据可能混有 users.id,故双表回查。
+     */
+    String resolveSubjectName(Long subjectId, Long familyId) {
+        if (subjectId == null) return null;
+        try {
+            FamilyMember member = familyMemberMapper.selectById(subjectId);
+            if (member != null && familyId != null && familyId.equals(member.getFamilyId())) {
+                return member.getNickname() != null && !member.getNickname().trim().isEmpty()
+                        ? member.getNickname() : ("成员" + member.getId());
+            }
+            String userName = resolveUserName(subjectId);
+            if (userName != null) return userName;
+            if (member != null) {
+                return member.getNickname() != null && !member.getNickname().trim().isEmpty()
+                        ? member.getNickname() : ("成员" + member.getId());
+            }
+        } catch (Exception e) {
+            log.warn("查询报告对象姓名失败 subjectId={}: {}", subjectId, e.getMessage());
+        }
+        return null;
+    }
+
+    /** 代传人展示名(proxy_uploader_id → users 姓名,查不到返回 null,前端兜底 '-') */
+    String resolveProxyUploaderName(Long proxyUploaderId) {
+        return resolveUserName(proxyUploaderId);
+    }
+
+    /**
+     * 列表展示字段回填(@TableField(exist=false) 展示字段,仅列表/详情接口需要):
+     * subjectName / proxyUploaderName。缺省时 subjectName 回退 personName,保证前端列不为空。
+     */
+    public void fillSubjectAndProxyNames(List<HealthReport> reports) {
+        if (reports == null || reports.isEmpty()) return;
+        for (HealthReport r : reports) {
+            String subjectName = resolveSubjectName(r.getSubjectId(), r.getFamilyId());
+            if (subjectName == null) {
+                subjectName = r.getPersonName() != null && !r.getPersonName().isEmpty()
+                        ? r.getPersonName() : null;
+            }
+            r.setSubjectName(subjectName);
+            r.setProxyUploaderName(resolveProxyUploaderName(r.getProxyUploaderId()));
+        }
+    }
+
     public HealthReport getReportById(Long reportId) {
     public HealthReport getReportById(Long reportId) {
         return healthReportMapper.selectById(reportId);
         return healthReportMapper.selectById(reportId);
     }
     }
@@ -1627,7 +1723,10 @@ public class HealthReportService {
             report.setReportDate(new Date());
             report.setReportDate(new Date());
         }
         }
 
 
-        report.setRemark("募极生物肠道菌群检测报告");
+        // T29-B1-3:代传场景草稿带 remark 时优先使用(非代传草稿 remark 为空 → 行为不变)
+        String draftRemark = draft.getRemark();
+        report.setRemark(draftRemark != null && !draftRemark.isEmpty()
+                ? draftRemark : "募极生物肠道菌群检测报告");
         return report;
         return report;
     }
     }
 
 
@@ -2054,12 +2153,13 @@ public class HealthReportService {
     }
     }
 
 
     /**
     /**
-     * 设置代上传人信息
+     * 设置代上传人信息(含代传备注,T29-B1-2:note 写入 health_reports.proxy_upload_note)
      */
      */
-    public void setProxyUploader(Long reportId, Long proxyUploaderId) {
+    public void setProxyUploader(Long reportId, Long proxyUploaderId, String proxyUploadNote) {
         HealthReport report = healthReportMapper.selectById(reportId);
         HealthReport report = healthReportMapper.selectById(reportId);
         if (report != null) {
         if (report != null) {
             report.setProxyUploaderId(proxyUploaderId);
             report.setProxyUploaderId(proxyUploaderId);
+            report.setProxyUploadNote(proxyUploadNote);
             report.setUpdatedAt(new Date());
             report.setUpdatedAt(new Date());
             healthReportMapper.updateById(report);
             healthReportMapper.updateById(report);
         }
         }

+ 5 - 4
cfc-web/src/views/admin/ProxyReportUpload.vue

@@ -39,9 +39,9 @@
         <el-descriptions-item label="被检测人">{{ previewResult.extractedName || '-' }}</el-descriptions-item>
         <el-descriptions-item label="被检测人">{{ previewResult.extractedName || '-' }}</el-descriptions-item>
         <el-descriptions-item label="报告编号">{{ previewResult.extractedReportNumber || '-' }}</el-descriptions-item>
         <el-descriptions-item label="报告编号">{{ previewResult.extractedReportNumber || '-' }}</el-descriptions-item>
         <el-descriptions-item label="报告日期">{{ previewResult.extractedReportDate || '-' }}</el-descriptions-item>
         <el-descriptions-item label="报告日期">{{ previewResult.extractedReportDate || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="健康总分">{{ previewResult.payload && previewResult.payload.overallScore ? previewResult.payload.overallScore : '-' }}</el-descriptions-item>
-        <el-descriptions-item label="菌群健康分">{{ previewResult.payload && previewResult.payload.gutHealthScore ? previewResult.payload.gutHealthScore : '-' }}</el-descriptions-item>
-        <el-descriptions-item label="肠型">{{ previewResult.payload && previewResult.payload.gutType ? previewResult.payload.gutType : '-' }}</el-descriptions-item>
+        <el-descriptions-item label="健康总分">{{ previewResult.payload && previewResult.payload.summary && previewResult.payload.summary.overallScore != null ? previewResult.payload.summary.overallScore : '-' }}</el-descriptions-item>
+        <el-descriptions-item label="菌群健康分">{{ previewResult.payload && previewResult.payload.summary && previewResult.payload.summary.gutHealthScore != null ? previewResult.payload.summary.gutHealthScore : '-' }}</el-descriptions-item>
+        <el-descriptions-item label="肠型">{{ previewResult.payload && previewResult.payload.summary && previewResult.payload.summary.gutType ? previewResult.payload.summary.gutType : '-' }}</el-descriptions-item>
       </el-descriptions>
       </el-descriptions>
       <div class="mt-20-right">
       <div class="mt-20-right">
         <el-button @click="previewResult = null">取消</el-button>
         <el-button @click="previewResult = null">取消</el-button>
@@ -258,7 +258,8 @@ export default {
       var self = this
       var self = this
       getHealthReportDetail(row.id).then(function(res) {
       getHealthReportDetail(row.id).then(function(res) {
         if (res && res.code === 200) {
         if (res && res.code === 200) {
-          self.detailReport = res.data
+          // B1-4:详情接口返回嵌套结构 {report:{...}, indicators, ...},取 report 层
+          self.detailReport = (res.data && res.data.report) ? res.data.report : res.data
           self.detailDialogVisible = true
           self.detailDialogVisible = true
         } else {
         } else {
           self.$message.error('加载详情失败')
           self.$message.error('加载详情失败')

+ 5 - 4
cfc-web/src/views/teacher/ProxyReport.vue

@@ -54,9 +54,9 @@
         <el-descriptions-item label="被检测人">{{ previewResult.extractedName || '-' }}</el-descriptions-item>
         <el-descriptions-item label="被检测人">{{ previewResult.extractedName || '-' }}</el-descriptions-item>
         <el-descriptions-item label="报告编号">{{ previewResult.extractedReportNumber || '-' }}</el-descriptions-item>
         <el-descriptions-item label="报告编号">{{ previewResult.extractedReportNumber || '-' }}</el-descriptions-item>
         <el-descriptions-item label="报告日期">{{ previewResult.extractedReportDate || '-' }}</el-descriptions-item>
         <el-descriptions-item label="报告日期">{{ previewResult.extractedReportDate || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="健康总分">{{ previewResult.payload && previewResult.payload.overallScore ? previewResult.payload.overallScore : '-' }}</el-descriptions-item>
-        <el-descriptions-item label="菌群健康分">{{ previewResult.payload && previewResult.payload.gutHealthScore ? previewResult.payload.gutHealthScore : '-' }}</el-descriptions-item>
-        <el-descriptions-item label="肠型">{{ previewResult.payload && previewResult.payload.gutType ? previewResult.payload.gutType : '-' }}</el-descriptions-item>
+        <el-descriptions-item label="健康总分">{{ previewResult.payload && previewResult.payload.summary && previewResult.payload.summary.overallScore != null ? previewResult.payload.summary.overallScore : '-' }}</el-descriptions-item>
+        <el-descriptions-item label="菌群健康分">{{ previewResult.payload && previewResult.payload.summary && previewResult.payload.summary.gutHealthScore != null ? previewResult.payload.summary.gutHealthScore : '-' }}</el-descriptions-item>
+        <el-descriptions-item label="肠型">{{ previewResult.payload && previewResult.payload.summary && previewResult.payload.summary.gutType ? previewResult.payload.summary.gutType : '-' }}</el-descriptions-item>
       </el-descriptions>
       </el-descriptions>
 
 
       <div class="pagination-wrap">
       <div class="pagination-wrap">
@@ -314,7 +314,8 @@ export default {
       try {
       try {
         const res = await getHealthReportDetail(row.id)
         const res = await getHealthReportDetail(row.id)
         if (res.code === 200) {
         if (res.code === 200) {
-          this.detailReport = res.data
+          // B1-4:详情接口返回嵌套结构 {report:{...}, indicators, ...},取 report 层
+          this.detailReport = (res.data && res.data.report) ? res.data.report : res.data
           this.detailDialogVisible = true
           this.detailDialogVisible = true
         }
         }
       } catch (e) {
       } catch (e) {