Răsfoiți Sursa

fix(reports): 采集链路三重加固——防止非菌群报告/类型不匹配报告静默入库

背景:family59 认知能力测评 PDF 被误当 gut_flora 入库(草稿107/报告54),
采集校验 matched 1/23 pass=false 仍照常入库,前端显示"内容未采集完整"。

加固(ReportCollectService.collect 健康管道):
① 采集指标校验 pass=false → 阻止自动入库,标记失败由管理端人工处理/重传
② gut_flora 家族类型解析结果必须含菌群实质数据(gutFlora/indicators 非空),
   只解析出 overview 的报告判定为内容不符
③ 指纹识别类型与用户所选报告类型不一致(非 DAN)→ 阻止入库并明确提示

runCollectValidation 三个重载改为返回 boolean(跳过校验返回 true)。
DAN 管道分流不受影响(指纹命中 dan_* 仍走 collectDanReport)。
Xiaogang Liao 1 săptămână în urmă
părinte
comite
0daae2c533

+ 56 - 8
cfc-backend/src/main/java/com/etotem/cfc/service/ReportCollectService.java

@@ -185,6 +185,16 @@ public class ReportCollectService {
                 }
             }
 
+            // 加固③:指纹成功识别出的类型与用户所选报告类型不一致(非 DAN)→ 阻止自动入库,
+            // 防止认知能力测评等报告误入健康管道(如 gut_flora)后以错误类型静默入库
+            if (!"unknown".equals(typeId) && !typeId.equals(draft.getReportType())) {
+                log.warn("报告类型不匹配 draftId={}, 指纹识别={}, 所选={}", draftId, typeId, draft.getReportType());
+                healthReportDraftService.markCollectFailed(draftId,
+                        "报告内容与所选类型不符(指纹识别为「" + typeId + "」,所选「" + draft.getReportType()
+                                + "」),请核对报告类型后重新上传");
+                return;
+            }
+
             // 采集指标清单(用于驱动非 DAN 的 LangGraph LLM 解析)
             String validationTypeId = (!"unknown".equals(typeId)) ? typeId : draft.getReportType();
             List<String> expectedIndicators = loadExpectedIndicators(validationTypeId);
@@ -223,11 +233,29 @@ public class ReportCollectService {
             }
 
             harmReportPayloadValid(payload);
+            // 加固②:已知肠道菌群家族类型必须含菌群实质数据(菌群检出列表/指标清单),
+            // 防止认知测评等非菌群报告只解析出 overview 也静默入库
+            if (isGutFloraType(validationTypeId) && !hasGutContent(payload)) {
+                log.warn("肠道菌群报告解析结果缺少菌群数据,阻止入库 draftId={}, typeId={}",
+                        draftId, validationTypeId);
+                healthReportDraftService.markCollectFailed(draftId,
+                        "解析结果缺少菌群检测数据(报告内容与所选类型不符),请在解析状态页人工处理或重新上传");
+                return;
+            }
             // 采集指标完整性校验(report_type_registry.expected_indicators 定义驱动)
+            // 加固①:校验不通过(pass=false)阻止自动入库,标记失败由管理端人工处理
+            boolean validationPass;
             try {
-                runCollectValidation(draftId, validationTypeId, payload);
+                validationPass = runCollectValidation(draftId, validationTypeId, payload);
             } catch (Exception e) {
                 log.warn("采集指标校验失败 draftId={}, typeId={}", draftId, validationTypeId, e);
+                validationPass = true;
+            }
+            if (!validationPass) {
+                log.warn("采集指标校验未通过,阻止入库 draftId={}, typeId={}", draftId, validationTypeId);
+                healthReportDraftService.markCollectFailed(draftId,
+                        "采集指标校验未通过(报告内容与所选类型不符或解析不完整),请在解析状态页人工处理或重新上传");
+                return;
             }
             healthReportDraftService.updateParseMethod(draftId, method);
             healthReportDraftService.updatePayload(draftId, objectMapper.writeValueAsString(payload));
@@ -1045,14 +1073,14 @@ public class ReportCollectService {
     /**
      * 健康(GUT/混检)管道校验入口:汇总 payload 中全部可采集指标名称后按定义比对
      */
-    private void runCollectValidation(Long draftId, String typeId, ParsedReportPayload.Payload payload) {
-        runCollectValidation(draftId, typeId, collectHealthNames(payload));
+    private boolean runCollectValidation(Long draftId, String typeId, ParsedReportPayload.Payload payload) {
+        return runCollectValidation(draftId, typeId, collectHealthNames(payload));
     }
 
     /**
      * DAN 管道校验入口:汇总解析 items 的 name/code 后按定义比对
      */
-    private void runCollectValidation(Long draftId, String typeId, DanReportParseService.DanParsedReport parsed) {
+    private boolean runCollectValidation(Long draftId, String typeId, DanReportParseService.DanParsedReport parsed) {
         Set<String> names = new HashSet<>();
         if (parsed != null && parsed.getItems() != null) {
             for (DanReportParseService.DataItem item : parsed.getItems()) {
@@ -1060,7 +1088,7 @@ public class ReportCollectService {
                 addCollectName(names, item.getCode());
             }
         }
-        runCollectValidation(draftId, typeId, names);
+        return runCollectValidation(draftId, typeId, names);
     }
 
     /**
@@ -1068,13 +1096,13 @@ public class ReportCollectService {
      * 该类型无指标项定义(expected_indicators 为空 / 类型未注册)时跳过,不影响采集主流程。
      * 结果 JSON(typeId/expected/matched/pass/missing)写入草稿 collect_validation,缺失项记入 WARN 日志。
      */
-    private void runCollectValidation(Long draftId, String typeId, Set<String> collectedNames) {
+    private boolean runCollectValidation(Long draftId, String typeId, Set<String> collectedNames) {
         if (draftId == null || typeId == null || collectedNames == null || collectedNames.isEmpty()) {
-            return;
+            return true;
         }
         List<String> expected = loadExpectedIndicators(typeId);
         if (expected == null || expected.isEmpty()) {
-            return;
+            return true;
         }
         List<String> missing = new ArrayList<>();
         int matched = 0;
@@ -1102,6 +1130,26 @@ public class ReportCollectService {
         } catch (Exception e) {
             log.warn("记录采集校验结果失败 draftId={}, typeId={}", draftId, typeId, e);
         }
+        return missing.isEmpty();
+    }
+
+    /** 是否为肠道菌群家族类型(report_type_registry.family = gut_flora) */
+    private boolean isGutFloraType(String typeId) {
+        if (typeId == null) {
+            return false;
+        }
+        ReportTypeRegistry registry = reportTypeRegistryMapper.selectOne(
+                new LambdaQueryWrapper<ReportTypeRegistry>()
+                        .eq(ReportTypeRegistry::getTypeId, typeId)
+                        .last("LIMIT 1"));
+        return registry != null && "gut_flora".equals(registry.getFamily());
+    }
+
+    /** 菌群实质数据判定:菌群检出列表或指标清单至少一项非空 */
+    private boolean hasGutContent(ParsedReportPayload.Payload payload) {
+        return payload != null
+                && ((payload.getGutFlora() != null && !payload.getGutFlora().isEmpty())
+                        || (payload.getIndicators() != null && !payload.getIndicators().isEmpty()));
     }
 
     /** 读取类型指标项定义(report_type_registry.expected_indicators JSON 数组);未定义返回 null */