Переглянути джерело

chore: auto bump version and changelog [skip ci]

iwt 1 тиждень тому
батько
коміт
6718fa887e

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

@@ -644,11 +644,14 @@ public class HealthReportController {
         String originalFilename = file.getOriginalFilename();
 
         try {
+            // 先读取文件字节:图片用于安全检测,PDF用于MD5去重(storeFile 后 temp 文件会被转移,无法再读)
+            byte[] fileBytes = file.getBytes();
+            String fileHash = computeMd5(fileBytes);
+
             // 图片类型:安全检测 + 保存
             if ("image".equals(type) || isImageContent(contentType)) {
                 try {
-                    byte[] imageBytes = file.getBytes();
-                    if (!wechatService.checkImage(imageBytes)) {
+                    if (!wechatService.checkImage(fileBytes)) {
                         return Result.error("图片内容包含敏感信息,上传失败");
                     }
                 } catch (IOException e) {
@@ -663,6 +666,25 @@ public class HealthReportController {
                 }
             }
 
+            // 重复上传检测:同一 MD5 已入库或正在采集中则直接返回,不再重复解析
+            Long existingReportId = healthReportService.findReportIdByFileHash(fileHash, reportTypeOfUpload(type, familyId));
+            if (existingReportId != null) {
+                Map<String, Object> dup = new LinkedHashMap<>();
+                dup.put("duplicate", true);
+                dup.put("reportId", existingReportId);
+                dup.put("message", "该报告已上传过,无需重复解析");
+                return Result.success(dup);
+            }
+            Long collectingDraftId = healthReportService.findCollectingDraftIdByFileHash(fileHash);
+            if (collectingDraftId != null) {
+                Map<String, Object> dup = new LinkedHashMap<>();
+                dup.put("duplicate", true);
+                dup.put("draftId", collectingDraftId);
+                dup.put("collecting", true);
+                dup.put("message", "该报告正在解析中,请稍候");
+                return Result.success(dup);
+            }
+
             String fileUrl = storageService.storeFile(file, "health-reports");
             String reportType = familyId != null && !("pdf".equals(type) || "auto".equals(type))
                     ? "physical_exam" : "gut_flora";
@@ -672,7 +694,7 @@ public class HealthReportController {
             String payloadJson = objectMapper.writeValueAsString(emptyPayload);
 
             HealthReportDraft draft = healthReportDraftService.createDraft(
-                    userId, familyId, reportType, fileUrl, originalFilename, payloadJson);
+                    userId, familyId, reportType, fileUrl, originalFilename, payloadJson, fileHash);
 
             // 提交异步采集:指纹判定类型后走专用脚本/opencode,完成自动入库,前端轮询 parse_status
             reportCollectService.submitAsyncCollect(draft.getId());
@@ -691,6 +713,32 @@ public class HealthReportController {
         }
     }
 
+    /**
+     * 计算上传文件的MD5哈希(用于重复上传去重)
+     */
+    private String computeMd5(byte[] data) {
+        try {
+            java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
+            byte[] digest = md.digest(data);
+            StringBuilder sb = new StringBuilder();
+            for (byte b : digest) {
+                sb.append(String.format("%02x", b));
+            }
+            return sb.toString();
+        } catch (Exception e) {
+            log.warn("计算文件MD5失败: {}", e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * 根据上传类型推导报告类型(与下方 reportType 计算保持一致,供去重查询使用)
+     */
+    private String reportTypeOfUpload(String type, Long familyId) {
+        return familyId != null && !("pdf".equals(type) || "auto".equals(type))
+                ? "physical_exam" : "gut_flora";
+    }
+
     /**
      * 查询异步采集任务状态(前端轮询):返回 parse_status/parse_method/parse_error/reportId
      */

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

@@ -28,7 +28,16 @@ public class HealthReportDraftService {
      * 创建草稿。返回带 id 的实体。
      */
     public HealthReportDraft createDraft(Long userId, Long familyId, String reportType,
-                                         String fileUrl, String originalFilename, String payloadJson) {
+                                          String fileUrl, String originalFilename, String payloadJson) {
+        return createDraft(userId, familyId, reportType, fileUrl, originalFilename, payloadJson, null);
+    }
+
+    /**
+     * 创建草稿(含文件哈希,用于重复上传检测)。
+     */
+    public HealthReportDraft createDraft(Long userId, Long familyId, String reportType,
+                                          String fileUrl, String originalFilename,
+                                          String payloadJson, String fileHash) {
         HealthReportDraft draft = new HealthReportDraft();
         draft.setUserId(userId);
         draft.setFamilyId(familyId);
@@ -36,13 +45,15 @@ public class HealthReportDraftService {
         draft.setFileUrl(fileUrl);
         draft.setOriginalFilename(originalFilename);
         draft.setPayloadJson(payloadJson);
+        draft.setFileHash(fileHash);
         draft.setStatus("pending");
         Date now = new Date();
         draft.setCreatedAt(now);
         draft.setUpdatedAt(now);
         draft.setExpireAt(new Date(now.getTime() + DRAFT_TTL_MS));
         draftMapper.insert(draft);
-        log.info("草稿已创建: draftId={}, userId={}, reportType={}", draft.getId(), userId, reportType);
+        log.info("草稿已创建: draftId={}, userId={}, reportType={}, fileHash={}",
+                draft.getId(), userId, reportType, fileHash);
         return draft;
     }
 

+ 1 - 4
cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java

@@ -184,10 +184,7 @@ public class HealthReportService {
         );
     }
 
-    /**
-     * 获取用户的所有健康报告(通过 subjectId 或 userId 关联)
-     */
-    public List<HealthReport> getUserReports(Long userId) {
+    public HealthReport getLatestReport(Long userId) {
         return healthReportMapper.selectList(
                 new LambdaQueryWrapper<HealthReport>()
                         .and(w -> w.eq(HealthReport::getSubjectId, userId)

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-de09a59dc7c7a4b0eac25b07b27fa1586f756cf8
+ac456febee2e1be55718e3517a6bcb0c6e7beb22

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1343",
+  "version": "1.0.1344",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 6 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,12 @@
 
 ---
 
+## v1.0.1344 (2026-09-09)
+
+### 文档
+- 团队层级树实现计划(13 Tasks,后4+前7)
+
+
 ## v1.0.1343 (2026-09-09)
 
 ### 新功能

+ 7 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1343
+> 当前版本: v1.0.1344
 
 ## 历史版本
 
@@ -8,6 +8,12 @@
 
 ---
 
+## v1.0.1344 (2026-09-09)
+
+### 文档
+- 团队层级树实现计划(13 Tasks,后4+前7)
+
+
 ## v1.0.1343 (2026-09-09)
 
 ### 新功能