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

feat: add growth record upload, supplement, dedup-check endpoints

Add 5 new endpoints to GrowthRecordController: uploadAndParse, createFromUpload, createSupplement, checkDuplicate, getSupplements. Inject StorageService + ReportParseService. Add buildParsedReportFromMap helper for flat-form submission.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
liaoxg 2 месяцев назад
Родитель
Сommit
ba9506533d

+ 189 - 7
cfc-backend/src/main/java/com/etotem/cfc/controller/growth/GrowthRecordController.java

@@ -1,22 +1,27 @@
 package com.etotem.cfc.controller.growth;
 
 import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.ParsedReport;
 import com.etotem.cfc.entity.Family;
 import com.etotem.cfc.entity.GrowthRecord;
 import com.etotem.cfc.service.AssessmentOrderService;
 import com.etotem.cfc.service.GrowthRecordService;
+import com.etotem.cfc.service.ReportParseService;
+import com.etotem.cfc.service.StorageService;
 import com.etotem.cfc.mapper.FamilyMapper;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import javax.annotation.Resource;
-
+import java.text.SimpleDateFormat;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-@Tag(name = "成长档案", description = "成长档案创建、查询、外部同步等接口")
+@Tag(name = "成长档案", description = "成长档案创建、查询、上传解析、补充记录等接口")
 @RestController
 @RequestMapping("/api/growth")
 public class GrowthRecordController {
@@ -30,6 +35,14 @@ public class GrowthRecordController {
     @Resource
     private FamilyMapper familyMapper;
 
+    @Resource
+    private StorageService storageService;
+
+    @Resource
+    private ReportParseService reportParseService;
+
+    // ========== Original endpoints (enhanced) ==========
+
     @Operation(summary = "创建成长档案")
     @PostMapping("/record/create")
     public Result<GrowthRecord> createRecord(@RequestBody GrowthRecord record,
@@ -47,7 +60,6 @@ public class GrowthRecordController {
     public Result<Map<String, Object>> createRecordWithOrder(
             @RequestBody Map<String, Object> body,
             @RequestAttribute("userId") Long userId) {
-        // 解析成长档案字段
         GrowthRecord record = new GrowthRecord();
         if (body.get("childId") != null) {
             record.setChildId(Long.valueOf(body.get("childId").toString()));
@@ -65,10 +77,8 @@ public class GrowthRecordController {
             return Result.error("请选择孩子");
         }
 
-        // 创建成长档案
         GrowthRecord growthResult = growthRecordService.createRecord(record);
 
-        // 检查是否同时购买测评
         boolean alsoPurchase = body.get("alsoPurchaseAssessment") != null
                 && Boolean.parseBoolean(body.get("alsoPurchaseAssessment").toString());
 
@@ -86,13 +96,11 @@ public class GrowthRecordController {
                 return Result.error("购买测评需要 familyId 和 packageId");
             }
 
-            // 自动分配规划师
             Family family = familyMapper.selectById(familyId);
             Long guideId = (family != null && family.getTeacherId() != null)
                     ? family.getTeacherId() : null;
             String guideName = null;
 
-            // 解析价格
             Long totalPrice = body.get("totalPrice") != null
                     ? Long.valueOf(body.get("totalPrice").toString()) : 0L;
             Long discountAmount = body.get("discountAmount") != null
@@ -164,4 +172,178 @@ public class GrowthRecordController {
         }
         return Result.success(record);
     }
+
+    // ========== New endpoints: upload, parse, supplement, dedup ==========
+
+    @Operation(summary = "上传测评报告PDF并解析(Stage 1)")
+    @PostMapping("/record/upload-and-parse")
+    public Result<Map<String, Object>> uploadAndParse(
+            @RequestParam("file") MultipartFile file) {
+        if (file.isEmpty()) {
+            return Result.error("请选择文件");
+        }
+        String sourceFileUrl;
+        try {
+            sourceFileUrl = storageService.storeFile(file);
+        } catch (Exception e) {
+            return Result.error("文件存储失败: " + e.getMessage());
+        }
+
+        String localPath = storageService.getFilePath(sourceFileUrl);
+        ParsedReport parsed = reportParseService.parseReport(localPath);
+
+        String reportDateStr = null;
+        if (parsed.getReportDate() != null) {
+            reportDateStr = new SimpleDateFormat("yyyy-MM-dd").format(parsed.getReportDate());
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("sourceFileUrl", sourceFileUrl);
+        result.put("parsed", parsed);
+        result.put("reportDateStr", reportDateStr);
+        result.put("hasDate", parsed.getReportDate() != null);
+        return Result.success(result);
+    }
+
+    @Operation(summary = "上传解析后创建成长档案(Stage 2)")
+    @PostMapping("/record/create-from-upload")
+    public Result<GrowthRecord> createFromUpload(@RequestBody Map<String, Object> body,
+                                                  @RequestAttribute("userId") Long userId) {
+        Long childId = body.get("childId") != null ? Long.valueOf(body.get("childId").toString()) : null;
+        if (childId == null) {
+            return Result.error("请选择孩子");
+        }
+        String sourceFileUrl = body.get("sourceFileUrl") != null ? body.get("sourceFileUrl").toString() : null;
+        if (sourceFileUrl == null || sourceFileUrl.isEmpty()) {
+            return Result.error("缺少源文件URL");
+        }
+
+        ParsedReport parsed = buildParsedReportFromMap(body);
+
+        // Manual report date override (user-entered if PDF parsing failed)
+        Date manualReportDate = null;
+        if (body.get("manualReportDate") != null) {
+            try {
+                manualReportDate = new SimpleDateFormat("yyyy-MM-dd")
+                        .parse(body.get("manualReportDate").toString());
+            } catch (Exception e) {
+                return Result.error("日期格式错误,请使用 yyyy-MM-dd 格式");
+            }
+        }
+
+        GrowthRecord record = growthRecordService.createFromUpload(
+                childId, userId, parsed, sourceFileUrl, manualReportDate);
+        return Result.success(record);
+    }
+
+    @Operation(summary = "创建补充记录(补充材料)")
+    @PostMapping("/record/supplement")
+    public Result<GrowthRecord> createSupplement(@RequestBody Map<String, Object> body,
+                                                  @RequestAttribute("userId") Long userId) {
+        Long parentRecordId = body.get("parentRecordId") != null
+                ? Long.valueOf(body.get("parentRecordId").toString()) : null;
+        Long childId = body.get("childId") != null ? Long.valueOf(body.get("childId").toString()) : null;
+        if (parentRecordId == null || childId == null) {
+            return Result.error("缺少必要参数: parentRecordId, childId");
+        }
+        String sourceFileUrl = body.get("sourceFileUrl") != null ? body.get("sourceFileUrl").toString() : null;
+
+        ParsedReport parsed = buildParsedReportFromMap(body);
+
+        Date reportDate = null;
+        if (body.get("reportDate") != null) {
+            try {
+                reportDate = new SimpleDateFormat("yyyy-MM-dd")
+                        .parse(body.get("reportDate").toString());
+            } catch (Exception e) {
+                return Result.error("日期格式错误,请使用 yyyy-MM-dd 格式");
+            }
+        }
+
+        GrowthRecord record = growthRecordService.createSupplement(
+                parentRecordId, childId, userId, parsed, sourceFileUrl, reportDate);
+        return Result.success(record);
+    }
+
+    @Operation(summary = "检查是否已存在相同哈希的记录(去重)")
+    @PostMapping("/record/check-duplicate")
+    public Result<Map<String, Object>> checkDuplicate(@RequestBody Map<String, Object> body) {
+        Long childId = body.get("childId") != null ? Long.valueOf(body.get("childId").toString()) : null;
+        String sourceType = body.get("sourceType") != null ? body.get("sourceType").toString() : "self_upload";
+        if (childId == null) {
+            return Result.error("缺少 childId");
+        }
+        Date reportDate = null;
+        if (body.get("reportDate") != null) {
+            try {
+                reportDate = new SimpleDateFormat("yyyy-MM-dd")
+                        .parse(body.get("reportDate").toString());
+            } catch (Exception e) {
+                return Result.error("日期格式错误");
+            }
+        }
+
+        GrowthRecord existing = growthRecordService.checkDuplicate(childId, reportDate, sourceType);
+        Map<String, Object> result = new HashMap<>();
+        result.put("duplicate", existing != null);
+        if (existing != null) {
+            result.put("existingRecord", existing);
+        }
+        return Result.success(result);
+    }
+
+    @Operation(summary = "获取某条记录的所有补充记录")
+    @PostMapping("/record/{id}/supplements")
+    public Result<List<GrowthRecord>> getSupplements(@PathVariable Long id) {
+        List<GrowthRecord> supplements = growthRecordService.getSupplements(id);
+        return Result.success(supplements);
+    }
+
+    // ========== Private helpers ==========
+
+    private ParsedReport buildParsedReportFromMap(Map<String, Object> body) {
+        ParsedReport.ParsedReportBuilder builder = ParsedReport.builder();
+
+        if (body.get("danLevel") != null) {
+            builder.danLevel(body.get("danLevel").toString());
+        }
+        if (body.get("overallScore") != null) {
+            builder.overallScore(Integer.valueOf(body.get("overallScore").toString()));
+        }
+        if (body.get("attentionScore") != null) {
+            builder.attentionScore(Integer.valueOf(body.get("attentionScore").toString()));
+        }
+        if (body.get("focusScore") != null) {
+            builder.focusScore(Integer.valueOf(body.get("focusScore").toString()));
+        }
+        if (body.get("memoryScore") != null) {
+            builder.memoryScore(Integer.valueOf(body.get("memoryScore").toString()));
+        }
+        if (body.get("logicScore") != null) {
+            builder.logicScore(Integer.valueOf(body.get("logicScore").toString()));
+        }
+        if (body.get("perceptionScore") != null) {
+            builder.perceptionScore(Integer.valueOf(body.get("perceptionScore").toString()));
+        }
+        if (body.get("spatialScore") != null) {
+            builder.spatialScore(Integer.valueOf(body.get("spatialScore").toString()));
+        }
+        if (body.get("processingSpeedScore") != null) {
+            builder.processingSpeedScore(Integer.valueOf(body.get("processingSpeedScore").toString()));
+        }
+        if (body.get("emotionScore") != null) {
+            builder.emotionScore(Integer.valueOf(body.get("emotionScore").toString()));
+        }
+        if (body.get("resilienceScore") != null) {
+            builder.resilienceScore(Integer.valueOf(body.get("resilienceScore").toString()));
+        }
+        if (body.get("assessmentSummary") != null) {
+            builder.assessmentSummary(body.get("assessmentSummary").toString());
+        }
+        if (body.get("growthSuggestions") != null) {
+            builder.growthSuggestions(body.get("growthSuggestions").toString());
+        }
+
+        return builder.build();
+    }
 }