Przeglądaj źródła

feat(health): 报告上传统一指纹自动识别,注册 dan_a2 类型并补充采集管道

- 迁移323: report_type_registry 注册 dan_a2(素质测评A2)+ report_fingerprint 默认指纹规则(大五人格/人格特质/核心素养等)
- HealthReportDraft 增加 memberId 归属成员字段;createDraft 支持 fileHash + memberId;新增 updateReportType
- ReportCollectService 指纹识别分流 dan_a2 采集管道
- 前端 report-upload.vue 去掉手动选择报告类型,统一上传由后台自动识别,需指定归属成员
liaoxg 1 dzień temu
rodzic
commit
21f8f67407

+ 54 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -10868,6 +10868,26 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
         // 迁移312: 为 gut_flora 补默认指纹规则(空表会导致指纹引擎永远 unknown)
         // 迁移312: 为 gut_flora 补默认指纹规则(空表会导致指纹引擎永远 unknown)
         seedGutFloraFingerprints();
         seedGutFloraFingerprints();
 
 
+        // 迁移323: 注册素质测评A2报告类型(dan家族)+ 默认指纹规则
+        // 统一上传后指纹识别为 dan_a2 → ReportCollectService 按 report_type_registry.family=dan 分流走 DAN 采集管道
+        try {
+            Integer danA2Count = jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*) FROM report_type_registry WHERE type_id = ?", Integer.class, "dan_a2");
+            if (danA2Count == null || danA2Count == 0) {
+                jdbcTemplate.update(
+                        "INSERT INTO report_type_registry (type_id, family, display_name, description, parser_path, " +
+                        "min_confidence, is_active, source, review_status, version, created_at, updated_at) " +
+                        "VALUES ('dan_a2', 'dan', '素质测评A2', '核心素养评估报告:大五人格/认知能力/社会关系等', " +
+                        "NULL, 30, 1, 'prebuilt', 'approved', '1.0', NOW(), NOW())");
+                log.info("迁移323: 已注册报告类型 dan_a2(dan家族)");
+            } else {
+                log.info("迁移323: 报告类型 dan_a2 已存在,跳过");
+            }
+        } catch (Exception e) {
+            log.warn("迁移323: 注册 dan_a2 类型失败: {}", e.getMessage());
+        }
+        seedDanA2Fingerprints();
+
         // 迁移313: 修复 meal_configs 历史双编码 participant_member_ids(共餐配置前端曾 JSON.stringify 二次编码,存成了 "\"[1,2]\"",
         // 迁移313: 修复 meal_configs 历史双编码 participant_member_ids(共餐配置前端曾 JSON.stringify 二次编码,存成了 "\"[1,2]\"",
         //          导致再次进入页面时无法识别选中成员)。仅处理以双引号包裹的值,不触碰正常 "[]..." 格式
         //          导致再次进入页面时无法识别选中成员)。仅处理以双引号包裹的值,不触碰正常 "[]..." 格式
         try {
         try {
@@ -11275,4 +11295,38 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
             // 索引已存在,忽略错误
             // 索引已存在,忽略错误
         }
         }
     }
     }
+
+    /**
+     * 迁移323: 为 dan_a2(素质测评A2)补默认指纹规则。
+     * 空表会导致指纹引擎永远 unknown;A2 报告特征词取自 DanReportParseService.parseA2Report 的章节识别词。
+     * dan_a2 注册表 min_confidence=30,两高权重词同时命中即判定成功,避免单词误判。
+     */
+    private void seedDanA2Fingerprints() {
+        try {
+            Integer count = jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*) FROM report_fingerprint WHERE type_id = ?", Integer.class, "dan_a2");
+            if (count != null && count > 0) {
+                log.info("迁移323: dan_a2 指纹规则已存在,跳过");
+                return;
+            }
+            String[][] rows = {
+                    {"大五人格", "20", "0", "1"},
+                    {"人格特质", "15", "0", "2"},
+                    {"核心素养", "15", "0", "3"},
+                    {"认知能力", "10", "0", "4"},
+                    {"感知觉", "10", "0", "5"},
+                    {"评估报告", "8", "0", "6"},
+                    {"综合评估", "8", "0", "7"},
+            };
+            for (String[] row : rows) {
+                jdbcTemplate.update(
+                        "INSERT INTO report_fingerprint (type_id, pattern, weight, is_exclusion, sort_order, created_at) "
+                                + "VALUES ('dan_a2', ?, ?, ?, ?, NOW())",
+                        row[0], Integer.valueOf(row[1]), Integer.valueOf(row[2]), Integer.valueOf(row[3]));
+            }
+            log.info("迁移323: 已写入 dan_a2 默认指纹规则 {} 条", rows.length);
+        } catch (Exception e) {
+            log.warn("迁移323: 写入 dan_a2 指纹规则失败: {}", e.getMessage());
+        }
+    }
 }
 }

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

@@ -717,7 +717,7 @@ public class HealthReportController {
             String payloadJson = objectMapper.writeValueAsString(emptyPayload);
             String payloadJson = objectMapper.writeValueAsString(emptyPayload);
 
 
             HealthReportDraft draft = healthReportDraftService.createDraft(
             HealthReportDraft draft = healthReportDraftService.createDraft(
-                    userId, familyId, reportType, fileUrl, originalFilename, payloadJson, fileHash);
+                    userId, familyId, reportType, fileUrl, originalFilename, payloadJson, fileHash, memberId);
 
 
             // 提交异步采集:指纹判定类型后走专用脚本/opencode,完成自动入库,前端轮询 parse_status
             // 提交异步采集:指纹判定类型后走专用脚本/opencode,完成自动入库,前端轮询 parse_status
             reportCollectService.submitAsyncCollect(draft.getId());
             reportCollectService.submitAsyncCollect(draft.getId());

+ 4 - 1
cfc-backend/src/main/java/com/etotem/cfc/entity/HealthReportDraft.java

@@ -24,7 +24,10 @@ public class HealthReportDraft implements Serializable {
     /** 家庭ID(可选,用于成员匹配) */
     /** 家庭ID(可选,用于成员匹配) */
     private Long familyId;
     private Long familyId;
 
 
-    /** 报告类型: physical_exam / gut_flora */
+    /** 报告归属成员ID(family_members.id,A2/DAN 采集必需) */
+    private Long memberId;
+
+    /** 报告类型: physical_exam / gut_flora / dan_a2 */
     private String reportType;
     private String reportType;
 
 
     /** 上传的文件URL(PDF或图片) */
     /** 上传的文件URL(PDF或图片) */

+ 23 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportDraftService.java

@@ -38,9 +38,19 @@ public class HealthReportDraftService {
     public HealthReportDraft createDraft(Long userId, Long familyId, String reportType,
     public HealthReportDraft createDraft(Long userId, Long familyId, String reportType,
                                           String fileUrl, String originalFilename,
                                           String fileUrl, String originalFilename,
                                           String payloadJson, String fileHash) {
                                           String payloadJson, String fileHash) {
+        return createDraft(userId, familyId, reportType, fileUrl, originalFilename, payloadJson, fileHash, null);
+    }
+
+    /**
+     * 创建草稿(含文件哈希 + 归属成员ID,A2/DAN 自动采集必需)。
+     */
+    public HealthReportDraft createDraft(Long userId, Long familyId, String reportType,
+                                          String fileUrl, String originalFilename,
+                                          String payloadJson, String fileHash, Long memberId) {
         HealthReportDraft draft = new HealthReportDraft();
         HealthReportDraft draft = new HealthReportDraft();
         draft.setUserId(userId);
         draft.setUserId(userId);
         draft.setFamilyId(familyId);
         draft.setFamilyId(familyId);
+        draft.setMemberId(memberId);
         draft.setReportType(reportType);
         draft.setReportType(reportType);
         draft.setFileUrl(fileUrl);
         draft.setFileUrl(fileUrl);
         draft.setOriginalFilename(originalFilename);
         draft.setOriginalFilename(originalFilename);
@@ -52,8 +62,8 @@ public class HealthReportDraftService {
         draft.setUpdatedAt(now);
         draft.setUpdatedAt(now);
         draft.setExpireAt(new Date(now.getTime() + DRAFT_TTL_MS));
         draft.setExpireAt(new Date(now.getTime() + DRAFT_TTL_MS));
         draftMapper.insert(draft);
         draftMapper.insert(draft);
-        log.info("草稿已创建: draftId={}, userId={}, reportType={}, fileHash={}",
-                draft.getId(), userId, reportType, fileHash);
+        log.info("草稿已创建: draftId={}, userId={}, reportType={}, memberId={}, fileHash={}",
+                draft.getId(), userId, reportType, memberId, fileHash);
         return draft;
         return draft;
     }
     }
 
 
@@ -196,6 +206,17 @@ public class HealthReportDraftService {
         draftMapper.update(null, uw);
         draftMapper.update(null, uw);
     }
     }
 
 
+    /**
+     * 报告类型变更后单独更新(DAN 识别完成:draft.reportType 置为 dan_a2,前端按此跳转报告页)
+     */
+    public void updateReportType(Long draftId, String reportType) {
+        LambdaUpdateWrapper<HealthReportDraft> uw = new LambdaUpdateWrapper<>();
+        uw.eq(HealthReportDraft::getId, draftId)
+          .set(HealthReportDraft::getReportType, reportType)
+          .set(HealthReportDraft::getUpdatedAt, new Date());
+        draftMapper.update(null, uw);
+    }
+
     /**
     /**
      * 异步解析失败记录错误
      * 异步解析失败记录错误
      */
      */

+ 148 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ReportCollectService.java

@@ -1,8 +1,13 @@
 package com.etotem.cfc.service;
 package com.etotem.cfc.service;
 
 
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.DanReportUploadVO;
 import com.etotem.cfc.dto.ParsedReportPayload;
 import com.etotem.cfc.dto.ParsedReportPayload;
 import com.etotem.cfc.entity.*;
 import com.etotem.cfc.entity.*;
+import com.etotem.cfc.mapper.DanReportUploadMapper;
 import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.mapper.ReportTypeRegistryMapper;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -66,6 +71,18 @@ public class ReportCollectService {
     @Resource
     @Resource
     private FamilyMemberMapper familyMemberMapper;
     private FamilyMemberMapper familyMemberMapper;
 
 
+    @Resource
+    private ReportTypeRegistryMapper reportTypeRegistryMapper;
+
+    @Resource
+    private DanReportUploadMapper danReportUploadMapper;
+
+    @Resource
+    private DanReportParseService danReportParseService;
+
+    @Resource
+    private DanReportUploadService danReportUploadService;
+
     @Resource(name = "taskExecutor")
     @Resource(name = "taskExecutor")
     private Executor taskExecutor;
     private Executor taskExecutor;
 
 
@@ -148,6 +165,19 @@ public class ReportCollectService {
             Map<String, Object> fp = fingerprintService.detect(pdfPath, draft.getOriginalFilename());
             Map<String, Object> fp = fingerprintService.detect(pdfPath, draft.getOriginalFilename());
             String typeId = (String) fp.get("typeId");
             String typeId = (String) fp.get("typeId");
 
 
+            // DAN 家族分流:指纹识别到 dan_* 类型(如 dan_a2)→ Java 同步解析 + DAN 入库,
+            // 不进入健康报告管道(parserPath 为空的 dan 类型指纹也不会走 LangGraph 脚本分支)
+            if (typeId != null && !"unknown".equals(typeId)) {
+                ReportTypeRegistry registry = reportTypeRegistryMapper.selectOne(
+                        new LambdaQueryWrapper<ReportTypeRegistry>()
+                                .eq(ReportTypeRegistry::getTypeId, typeId)
+                                .last("LIMIT 1"));
+                if (registry != null && "dan".equals(registry.getFamily())) {
+                    collectDanReport(draft, pdfPath, registry);
+                    return;
+                }
+            }
+
             ParsedReportPayload.Payload payload;
             ParsedReportPayload.Payload payload;
             String method;
             String method;
             if (!"unknown".equals(typeId) && fp.get("parserPath") != null) {
             if (!"unknown".equals(typeId) && fp.get("parserPath") != null) {
@@ -189,6 +219,124 @@ public class ReportCollectService {
         }
         }
     }
     }
 
 
+    /**
+     * DAN 家族报告采集:指纹识别到 dan_* 类型(如 dan_a2)后走 Java 同步解析 + DAN 入库。
+     * 复用 DanReportUploadService.confirmUpload(@Transactional:result 入库 / 展示块 / 汇总 / 五维刷新 / 自动方案),
+     * 完成草稿闭环:reportType 置为识别类型 + markCollectCompleted(reportId = dan upload id),
+     * 前端 collect/status 据此跳转 dan-assessment/report-result?reportId=...
+     */
+    private void collectDanReport(HealthReportDraft draft, String pdfPath, ReportTypeRegistry registry) {
+        Long draftId = draft.getId();
+        Long memberId = draft.getMemberId();
+        if (memberId == null) {
+            healthReportDraftService.markCollectFailed(draftId,
+                    "已识别为" + registry.getDisplayName() + "报告,请重新上传并选择报告归属成员");
+            return;
+        }
+        String dimension = resolveDanDimension(registry.getTypeId());
+        try {
+            // Java 同步解析(与 uploadAndParse 同一解析器)
+            DanReportParseService.DanParsedReport parsed;
+            try (FileInputStream fis = new FileInputStream(new File(pdfPath))) {
+                parsed = danReportParseService.parse(fis, dimension);
+            }
+            if (parsed == null || parsed.getItems() == null || parsed.getItems().isEmpty()) {
+                healthReportDraftService.markCollectFailed(draftId,
+                        "已识别为" + registry.getDisplayName() + "报告,但未能提取有效指标");
+                return;
+            }
+
+            // 构建 DAN 上传记录(对齐 uploadAndParse 字段)
+            DanReportUpload upload = new DanReportUpload();
+            upload.setFamilyMemberId(memberId);
+            upload.setUploaderUserId(draft.getUserId());
+            upload.setDimension(dimension);
+            Long familyId = draft.getFamilyId();
+            if (familyId == null) {
+                familyId = resolveDanFamilyId(memberId);
+            }
+            upload.setFamilyId(familyId);
+            upload.setFileUrl(draft.getFileUrl());
+            upload.setFileId(toShortFileId(draft.getFileUrl()));
+            upload.setFileName(draft.getOriginalFilename());
+            upload.setFileSize(new File(pdfPath).length());
+            upload.setUploadedAt(new Date());
+            upload.setDraftStatus(DanReportUpload.DRAFT_PENDING);
+            upload.setParseStatus(DanReportUpload.PARSE_SUCCESS);
+            upload.setParserVersion("v1");
+            upload.setParsedItems(objectMapper.writeValueAsString(parsed.getItems()));
+            upload.setParsedExtra(objectMapper.writeValueAsString(parsed.getExtra()));
+            upload.setParsedSummary(parsed.getSummary());
+            upload.setParsedSuggestions(parsed.getSuggestions());
+            upload.setReportType(parsed.getReportType());
+            upload.setParsedAt(new Date());
+            upload.setEditCount(0);
+            upload.setCreatedAt(new Date());
+            upload.setUpdatedAt(new Date());
+            danReportUploadMapper.insert(upload);
+
+            // 自动确认入库(result + blocks + 汇总 + 五维 + 自动方案)
+            Map<String, Object> extra = parsed.getExtra();
+            String childName = extra != null && extra.get("name") != null ? String.valueOf(extra.get("name")) : "";
+            String assessmentDate = extra != null && extra.get("reportDate") != null
+                    ? String.valueOf(extra.get("reportDate")) : "";
+            Result<DanReportUploadVO> confirm = danReportUploadService.confirmUpload(
+                    upload.getId(), draft.getUserId(), childName, assessmentDate);
+            if (confirm == null || confirm.getCode() == null || confirm.getCode() != 200) {
+                healthReportDraftService.markCollectFailed(draftId,
+                        "DAN报告确认入库失败: " + (confirm != null ? confirm.getMessage() : "无响应"));
+                return;
+            }
+
+            // 草稿闭环:reportType 置为识别类型(前端按 dan_* 跳转)+ completed
+            healthReportDraftService.updateReportType(draftId, registry.getTypeId());
+            healthReportDraftService.markCollectCompleted(draftId, upload.getId());
+            log.info("DAN家族采集完成 draftId={}, typeId={}, danUploadId={}, dimension={}",
+                    draftId, registry.getTypeId(), upload.getId(), dimension);
+        } catch (Exception e) {
+            log.error("DAN家族采集失败 draftId={}, typeId={}", draftId, registry.getTypeId(), e);
+            healthReportDraftService.markCollectFailed(draftId, e.getMessage());
+        }
+    }
+
+    /** dan 家族 typeId → 测评维度:dan_a2 → mind;已知 dan_<dim> 直接映射,否则默认 mind */
+    private String resolveDanDimension(String typeId) {
+        if (typeId != null && typeId.startsWith("dan_")) {
+            String dim = typeId.substring(4);
+            if (DanReportUpload.DIM_MIND.equals(dim) || DanReportUpload.DIM_WISDOM.equals(dim)
+                    || DanReportUpload.DIM_COGNITION.equals(dim) || DanReportUpload.DIM_LEARNING.equals(dim)
+                    || DanReportUpload.DIM_BEHAVIOR.equals(dim) || DanReportUpload.DIM_CAREER.equals(dim)
+                    || DanReportUpload.DIM_CAMPUS.equals(dim) || DanReportUpload.DIM_ADOLESCENT.equals(dim)) {
+                return dim;
+            }
+        }
+        return DanReportUpload.DIM_MIND;
+    }
+
+    private Long resolveDanFamilyId(Long memberId) {
+        if (memberId == null) {
+            return null;
+        }
+        FamilyMember member = familyMemberMapper.selectById(memberId);
+        return member != null ? member.getFamilyId() : null;
+    }
+
+    /** file_id 存相对路径或文件名(对齐 DanReportUploadService.toShortFileId,避免 Data truncation) */
+    private String toShortFileId(String fileUrl) {
+        if (fileUrl == null || fileUrl.isEmpty()) {
+            return null;
+        }
+        int uploadsIdx = fileUrl.indexOf("/uploads/");
+        if (uploadsIdx >= 0) {
+            return fileUrl.substring(uploadsIdx + 1);
+        }
+        int slash = fileUrl.lastIndexOf('/');
+        if (slash >= 0 && slash < fileUrl.length() - 1) {
+            return fileUrl.substring(slash + 1);
+        }
+        return fileUrl.length() > 500 ? fileUrl.substring(0, 500) : fileUrl;
+    }
+
     private void harmReportPayloadValid(ParsedReportPayload.Payload payload) {
     private void harmReportPayloadValid(ParsedReportPayload.Payload payload) {
         if (payload.getSummary() == null && (payload.getIndicators() == null || payload.getIndicators().isEmpty())
         if (payload.getSummary() == null && (payload.getIndicators() == null || payload.getIndicators().isEmpty())
                 && (payload.getGutFlora() == null || payload.getGutFlora().isEmpty())) {
                 && (payload.getGutFlora() == null || payload.getGutFlora().isEmpty())) {

+ 2 - 1
cfc-backend/src/main/resources/schema.sql

@@ -1872,7 +1872,8 @@ CREATE TABLE IF NOT EXISTS health_report_drafts (
     id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
     id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
     user_id BIGINT NOT NULL COMMENT '上传人用户ID',
     user_id BIGINT NOT NULL COMMENT '上传人用户ID',
     family_id BIGINT COMMENT '家庭ID',
     family_id BIGINT COMMENT '家庭ID',
-    report_type VARCHAR(32) NOT NULL COMMENT '报告类型: physical_exam/gut_flora',
+    member_id BIGINT DEFAULT NULL COMMENT '报告归属成员ID(family_members.id),A2/DAN自动采集必需',
+    report_type VARCHAR(32) NOT NULL COMMENT '报告类型: physical_exam/gut_flora/dan_a2',
     file_url VARCHAR(500) COMMENT '上传的文件URL',
     file_url VARCHAR(500) COMMENT '上传的文件URL',
     file_hash VARCHAR(64) DEFAULT NULL COMMENT '文件MD5哈希,用于重复上传检测',
     file_hash VARCHAR(64) DEFAULT NULL COMMENT '文件MD5哈希,用于重复上传检测',
     original_filename VARCHAR(255) COMMENT '原始文件名',
     original_filename VARCHAR(255) COMMENT '原始文件名',

+ 19 - 166
cfc-frontend/pages/health/report-upload.vue

@@ -6,31 +6,8 @@
       <text class="tip-text">{{ tipText }}</text>
       <text class="tip-text">{{ tipText }}</text>
     </view>
     </view>
 
 
-    <!-- 报告类型 -->
-    <view class="form-section type-section">
-      <text class="form-label">请选择报告类型</text>
-      <view class="type-methods">
-        <view
-          class="type-card type-gut"
-          :class="{ 'type-card-active': reportKind === 'gut' }"
-          @click="selectKind('gut')">
-          <text class="type-card-icon">🦠</text>
-          <text class="type-card-title">菌群检测报告</text>
-          <text class="type-card-desc">肠道菌群 PDF,自动解析菌群指标</text>
-        </view>
-        <view
-          class="type-card type-a2"
-          :class="{ 'type-card-active': reportKind === 'a2' }"
-          @click="selectKind('a2')">
-          <text class="type-card-icon">🧠</text>
-          <text class="type-card-title">素质测评 A2</text>
-          <text class="type-card-desc">核心素养 PDF,解析大五人格等维度</text>
-        </view>
-      </view>
-    </view>
-
-    <!-- A2 需指定成员 -->
-    <view class="form-section" v-if="reportKind === 'a2'">
+    <!-- 成员归属(统一上传:后台按指纹自动识别报告类型,需指定成员以正确归属) -->
+    <view class="form-section">
       <view class="member-box">
       <view class="member-box">
         <text class="form-label">报告属于哪位成员</text>
         <text class="form-label">报告属于哪位成员</text>
         <view class="member-list" v-if="familyMembers.length > 0">
         <view class="member-list" v-if="familyMembers.length > 0">
@@ -55,12 +32,12 @@
         <view class="upload-card card-chat" @click="pickFromChat">
         <view class="upload-card card-chat" @click="pickFromChat">
           <view class="upload-card-icon">💬</view>
           <view class="upload-card-icon">💬</view>
           <text class="upload-card-title">从聊天选取</text>
           <text class="upload-card-title">从聊天选取</text>
-          <text class="upload-card-desc">{{ reportKind === 'a2' ? '微信聊天中的 A2 PDF' : '微信聊天中的 PDF/图片' }}</text>
+          <text class="upload-card-desc">微信聊天中的 PDF / 图片报告</text>
         </view>
         </view>
         <view class="upload-card card-photo" @click="takePhoto">
         <view class="upload-card card-photo" @click="takePhoto">
           <view class="upload-card-icon">📷</view>
           <view class="upload-card-icon">📷</view>
           <text class="upload-card-title">拍照上传</text>
           <text class="upload-card-title">拍照上传</text>
-          <text class="upload-card-desc">{{ reportKind === 'a2' ? 'A2 仅支持 PDF 文件' : '拍摄纸质报告或舌象' }}</text>
+          <text class="upload-card-desc">拍摄纸质报告(PDF 报告更佳)</text>
         </view>
         </view>
       </view>
       </view>
 
 
@@ -150,9 +127,9 @@
       </view>
       </view>
       <view class="guide-tips">
       <view class="guide-tips">
         <text class="guide-tips-title">温馨提示</text>
         <text class="guide-tips-title">温馨提示</text>
-        <text class="guide-tips-item">· 请先选择报告类型,再从聊天选取文件</text>
-        <text class="guide-tips-item">· 素质测评 A2 仅支持 PDF,需指定家庭成员</text>
-        <text class="guide-tips-item">· 请确报告清晰完整,以便准确解析</text>
+        <text class="guide-tips-item">· 上传后无需选择类型,后台自动识别报告种类并解析</text>
+        <text class="guide-tips-item">· 请先选择报告归属成员,再上传文件</text>
+        <text class="guide-tips-item">· 请确报告清晰完整,以便准确解析</text>
         <text class="guide-tips-item">· 确认入库后将自动生成成长方案,可再分解为每日任务</text>
         <text class="guide-tips-item">· 确认入库后将自动生成成长方案,可再分解为每日任务</text>
       </view>
       </view>
     </view>
     </view>
@@ -160,7 +137,7 @@
 </template>
 </template>
 
 
 <script>
 <script>
-import { uploadReportOnly, getReportCollectStatus, danParsePreview, danConfirmReport, getFamilyMemberList } from '../../utils/api.js'
+import { uploadReportOnly, getReportCollectStatus, getFamilyMemberList } from '../../utils/api.js'
 
 
 export default {
 export default {
   data() {
   data() {
@@ -182,18 +159,12 @@ export default {
   },
   },
   computed: {
   computed: {
     tipText: function() {
     tipText: function() {
-      if (this.reportKind === 'a2') {
-        return '已选择素质测评 A2,请指定成员后上传核心素养 PDF'
-      }
-      if (this.reportKind === 'gut') {
-        return '已选择菌群检测报告,上传后后台自动解析菌群指标'
-      }
-      return '请先选择报告类型:菌群检测,或素质测评 A2'
+      return '上传报告后由系统自动识别类型并解析:菌群检测、素质测评 A2、体检报告等'
     }
     }
   },
   },
   onLoad: function(options) {
   onLoad: function(options) {
     this.memberId = options.memberId ? parseInt(options.memberId) : null
     this.memberId = options.memberId ? parseInt(options.memberId) : null
-    if (options.type === 'a2' || options.reportKind === 'a2') {
+if (options.type === 'a2' || options.reportKind === 'a2') {
       this.reportKind = 'a2'
       this.reportKind = 'a2'
     } else if (options.type === 'gut' || options.reportKind === 'gut') {
     } else if (options.type === 'gut' || options.reportKind === 'gut') {
       this.reportKind = 'gut'
       this.reportKind = 'gut'
@@ -207,9 +178,6 @@ export default {
     this.stopCollectPolling()
     this.stopCollectPolling()
   },
   },
   methods: {
   methods: {
-    selectKind: function(kind) {
-      this.reportKind = kind
-    },
     resolveMemberId: function(member) {
     resolveMemberId: function(member) {
       if (!member) return null
       if (!member) return null
       var raw = member.id || member.userId
       var raw = member.id || member.userId
@@ -258,14 +226,7 @@ export default {
         self.familyMembers = []
         self.familyMembers = []
       })
       })
     },
     },
-    ensureTypeSelected: function() {
-      if (this.reportKind === 'gut' || this.reportKind === 'a2') {
-        return true
-      }
-      uni.showToast({ title: '请先选择报告类型', icon: 'none' })
-      return false
-    },
-    ensureA2Member: function() {
+    ensureMember: function() {
       if (this.memberId) return true
       if (this.memberId) return true
       uni.showToast({ title: '请先选择报告所属成员', icon: 'none' })
       uni.showToast({ title: '请先选择报告所属成员', icon: 'none' })
       return false
       return false
@@ -294,13 +255,8 @@ export default {
     },
     },
     pickFromChat: function() {
     pickFromChat: function() {
       var self = this
       var self = this
-      if (!this.ensureTypeSelected()) return
-      if (this.reportKind === 'a2' && !this.ensureA2Member()) return
+      if (!this.ensureMember()) return
       this.ensureAgreed(function() {
       this.ensureAgreed(function() {
-        if (self.reportKind === 'a2') {
-          self.chooseChatFile('file', '聊天文件')
-          return
-        }
         // type='all' 的选择器默认停留在图片 tab,用户难以发现可切换选文件。
         // type='all' 的选择器默认停留在图片 tab,用户难以发现可切换选文件。
         // 改为先弹 actionSheet 让用户明确选择图片或文件,分别走对应 type。
         // 改为先弹 actionSheet 让用户明确选择图片或文件,分别走对应 type。
         uni.showActionSheet({
         uni.showActionSheet({
@@ -336,11 +292,7 @@ export default {
     },
     },
     takePhoto: function() {
     takePhoto: function() {
       var self = this
       var self = this
-      if (!this.ensureTypeSelected()) return
-      if (this.reportKind === 'a2') {
-        uni.showToast({ title: '素质测评 A2 请上传 PDF 文件', icon: 'none' })
-        return
-      }
+      if (!this.ensureMember()) return
       this.ensureAgreed(function() {
       this.ensureAgreed(function() {
         uni.chooseImage({
         uni.chooseImage({
           count: 1,
           count: 1,
@@ -355,17 +307,13 @@ export default {
     },
     },
     uploadReport: function(filePath, fileName) {
     uploadReport: function(filePath, fileName) {
       if (!filePath) return
       if (!filePath) return
-      if (this.reportKind === 'a2') {
-        this.uploadA2Report(filePath, fileName)
-        return
-      }
       var self = this
       var self = this
       self.uploading = true
       self.uploading = true
       self.uploadingText = '正在上传文件...'
       self.uploadingText = '正在上传文件...'
       uni.showLoading({ title: '上传中...' })
       uni.showLoading({ title: '上传中...' })
       
       
-      // 菌群检测:走健康报告异步采集
-      uploadReportOnly(filePath, self.uploadType).then(function(res) {
+// 统一上传:舌诊照片(uploadType=tongue)走舌诊专用流程;其余 type=auto,后台指纹判定后走对应采集管道(dan_a2 → DAN入库 / 其余 → 健康报告)
+      uploadReportOnly(filePath, self.uploadType || 'auto', self.memberId).then(function(res) {
         uni.hideLoading()
         uni.hideLoading()
         self.uploading = false
         self.uploading = false
         var data = res.data || {}
         var data = res.data || {}
@@ -417,58 +365,6 @@ export default {
         uni.showToast({ title: msg, icon: 'none' })
         uni.showToast({ title: msg, icon: 'none' })
       })
       })
     },
     },
-    uploadA2Report: function(filePath, fileName) {
-      if (!this.ensureA2Member()) return
-      var name = (fileName || '').toLowerCase()
-      if (name && name.indexOf('.pdf') < 0 && name !== '聊天文件') {
-        uni.showToast({ title: '素质测评 A2 仅支持 PDF', icon: 'none' })
-        return
-      }
-      var self = this
-      self.uploading = true
-      self.uploadingText = '正在解析 A2 报告...'
-      uni.showLoading({ title: '解析中...' })
-      danParsePreview(filePath, 'mind', self.memberId).then(function(res) {
-        var data = (res && res.data) || {}
-        var uploadId = data.uploadId || data.draftId
-        if (!uploadId) {
-          uni.hideLoading()
-          self.uploading = false
-          uni.showToast({ title: (res && res.message) || 'A2 解析失败', icon: 'none' })
-          return
-        }
-        var extra = {}
-        try {
-          extra = typeof data.extraJson === 'string' ? JSON.parse(data.extraJson) : (data.extraJson || {})
-        } catch (e) {
-          extra = {}
-        }
-        var childName = extra.name || ''
-        var assessmentDate = extra.reportDate || ''
-        if (assessmentDate && assessmentDate.length >= 10) {
-          assessmentDate = assessmentDate.substring(0, 10)
-        }
-        return danConfirmReport({
-          draftId: uploadId,
-          childName: childName,
-          assessmentDate: assessmentDate
-        }).then(function(confirmRes) {
-          uni.hideLoading()
-          self.uploading = false
-          var confirmData = (confirmRes && confirmRes.data) || {}
-          var reportId = confirmData.uploadId || uploadId
-          uni.showToast({ title: 'A2 报告已入库', icon: 'success' })
-          setTimeout(function() {
-            uni.redirectTo({ url: '/pages/dan-assessment/report-result?reportId=' + reportId })
-          }, 600)
-        })
-      }).catch(function(err) {
-        uni.hideLoading()
-        self.uploading = false
-        var msg = (err && err.message) || 'A2 解析失败,请重试'
-        uni.showToast({ title: msg, icon: 'none', duration: 3000 })
-      })
-    },
     stopCollectPolling: function() {
     stopCollectPolling: function() {
       if (this.collectTimer) {
       if (this.collectTimer) {
         clearInterval(this.collectTimer)
         clearInterval(this.collectTimer)
@@ -504,8 +400,10 @@ export default {
           var reportType = data.reportType || 'gut_flora'
           var reportType = data.reportType || 'gut_flora'
           setTimeout(function() {
           setTimeout(function() {
             if (reportId) {
             if (reportId) {
-              // 统一跳转到健康报告详情页(后端指纹识别后统一入库到 health_report 表)
-              if (reportType === 'gut_flora') {
+              // 后端指纹识别后统一入库:dan_a2 → DAN 报告页,其余 → 健康报告详情页
+              if (reportType === 'dan_a2') {
+                uni.redirectTo({ url: '/pages/dan-assessment/report-result?reportId=' + reportId })
+              } else if (reportType === 'gut_flora') {
                 uni.redirectTo({ url: '/pages/health/gut-flora-detail?reportId=' + reportId })
                 uni.redirectTo({ url: '/pages/health/gut-flora-detail?reportId=' + reportId })
               } else {
               } else {
                 uni.redirectTo({ url: '/pages/health/report-detail?reportType=' + reportType + '&reportId=' + reportId })
                 uni.redirectTo({ url: '/pages/health/report-detail?reportType=' + reportType + '&reportId=' + reportId })
@@ -577,51 +475,6 @@ export default {
 .form-section {
 .form-section {
   margin: 0 30rpx;
   margin: 0 30rpx;
 }
 }
-.type-section {
-  margin-bottom: 20rpx;
-}
-.type-methods {
-  display: flex;
-  flex-direction: row;
-  gap: 16rpx;
-}
-.type-card {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  align-items: flex-start;
-  padding: 24rpx 20rpx;
-  border-radius: 16rpx;
-  background: #fff;
-  border: 2rpx solid #E2E8F0;
-}
-.type-card-active {
-  border-color: #5B9BD5;
-  background: #F0F7FF;
-}
-.type-gut.type-card-active {
-  border-color: #22C55E;
-  background: #F0FDF4;
-}
-.type-a2.type-card-active {
-  border-color: #8B5CF6;
-  background: #F5F3FF;
-}
-.type-card-icon {
-  font-size: 40rpx;
-  margin-bottom: 10rpx;
-}
-.type-card-title {
-  font-size: 26rpx;
-  font-weight: 600;
-  color: #1E293B;
-  margin-bottom: 6rpx;
-}
-.type-card-desc {
-  font-size: 20rpx;
-  color: #94A3B8;
-  line-height: 1.5;
-}
 .member-box {
 .member-box {
   background: #fff;
   background: #fff;
   border-radius: 16rpx;
   border-radius: 16rpx;