|
|
@@ -1,18 +1,22 @@
|
|
|
package com.etotem.cfc.service;
|
|
|
|
|
|
-import com.etotem.cfc.service.api.GrowthRecordServiceInterface;
|
|
|
-
|
|
|
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.etotem.cfc.common.DuplicateGrowthRecordException;
|
|
|
+import com.etotem.cfc.dto.ParsedReport;
|
|
|
import com.etotem.cfc.entity.DanAssessmentResult;
|
|
|
import com.etotem.cfc.entity.GrowthRecord;
|
|
|
import com.etotem.cfc.mapper.DanAssessmentResultMapper;
|
|
|
import com.etotem.cfc.mapper.GrowthRecordMapper;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
|
-
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.security.MessageDigest;
|
|
|
+import java.security.NoSuchAlgorithmException;
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
import java.util.Date;
|
|
|
import java.util.List;
|
|
|
+import java.util.UUID;
|
|
|
|
|
|
@Service
|
|
|
public class GrowthRecordService {
|
|
|
@@ -23,24 +27,171 @@ public class GrowthRecordService {
|
|
|
@Resource
|
|
|
private DanAssessmentResultMapper danAssessmentResultMapper;
|
|
|
|
|
|
- public GrowthRecord createRecord(GrowthRecord record) {
|
|
|
- record.setStatus("pending");
|
|
|
+ /**
|
|
|
+ * Create a growth record from a platform-purchased assessment result.
|
|
|
+ * source_type = platform_purchase
|
|
|
+ */
|
|
|
+ public GrowthRecord createFromAssessment(DanAssessmentResult result, ParsedReport parsed) {
|
|
|
+ Date reportDate = parsed != null && parsed.getReportDate() != null
|
|
|
+ ? parsed.getReportDate() : result.getAssessmentDate();
|
|
|
+
|
|
|
+ GrowthRecord record = new GrowthRecord();
|
|
|
+ record.setChildId(result.getChildId());
|
|
|
+ record.setAssessmentId(result.getId());
|
|
|
+ record.setSourceType("platform_purchase");
|
|
|
+ record.setReportDate(reportDate);
|
|
|
+ record.setDanLevel(result.getDanLevel());
|
|
|
+ record.setOverallScore(result.getOverallScore());
|
|
|
+ record.setAssessmentSummary(result.getAnalysisReport());
|
|
|
+ record.setGrowthSuggestions(result.getGrowthSuggestions());
|
|
|
+ record.setStatus("completed");
|
|
|
+ record.setIsLatest(true);
|
|
|
+ record.setExternalSync(true);
|
|
|
+ record.setCreatedAt(new Date());
|
|
|
+ record.setUpdatedAt(new Date());
|
|
|
+ record.setDedupHash(computeDedupHash(result.getChildId(), reportDate, "platform_purchase", null));
|
|
|
+
|
|
|
+ checkDuplicateAndInsert(record);
|
|
|
+ return record;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Create a growth record from user-uploaded report.
|
|
|
+ * source_type = self_upload
|
|
|
+ */
|
|
|
+ public GrowthRecord createFromUpload(Long childId, Long parentId, ParsedReport parsed,
|
|
|
+ String sourceFileUrl, Date manualReportDate) {
|
|
|
+ Date reportDate = manualReportDate != null ? manualReportDate
|
|
|
+ : (parsed != null ? parsed.getReportDate() : null);
|
|
|
+
|
|
|
+ GrowthRecord record = new GrowthRecord();
|
|
|
+ record.setChildId(childId);
|
|
|
+ record.setParentId(parentId);
|
|
|
+ record.setSourceType("self_upload");
|
|
|
+ record.setSourceFileUrl(sourceFileUrl);
|
|
|
+ record.setReportDate(reportDate);
|
|
|
+ if (parsed != null) {
|
|
|
+ record.setDanLevel(parsed.getDanLevel());
|
|
|
+ record.setOverallScore(parsed.getOverallScore());
|
|
|
+ record.setAssessmentSummary(parsed.getAssessmentSummary());
|
|
|
+ record.setGrowthSuggestions(parsed.getGrowthSuggestions());
|
|
|
+ }
|
|
|
+ record.setStatus("completed");
|
|
|
+ record.setIsLatest(true);
|
|
|
+ record.setCreatedAt(new Date());
|
|
|
+ record.setUpdatedAt(new Date());
|
|
|
+ record.setDedupHash(computeDedupHash(childId, reportDate, "self_upload", null));
|
|
|
+
|
|
|
+ checkDuplicateAndInsert(record);
|
|
|
+ return record;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Create a supplement record linked to a parent record.
|
|
|
+ * source_type = supplement, parent_record_id = parentRecordId
|
|
|
+ */
|
|
|
+ public GrowthRecord createSupplement(Long parentRecordId, Long childId, Long parentId,
|
|
|
+ ParsedReport parsed, String sourceFileUrl, Date reportDate) {
|
|
|
+ GrowthRecord parent = growthRecordMapper.selectById(parentRecordId);
|
|
|
+ if (parent == null) {
|
|
|
+ throw new IllegalArgumentException("Parent growth record not found: " + parentRecordId);
|
|
|
+ }
|
|
|
+
|
|
|
+ GrowthRecord record = new GrowthRecord();
|
|
|
+ record.setChildId(childId);
|
|
|
+ record.setParentId(parentId);
|
|
|
+ record.setParentRecordId(parentRecordId);
|
|
|
+ record.setSourceType("supplement");
|
|
|
+ record.setSourceFileUrl(sourceFileUrl);
|
|
|
+ record.setReportDate(reportDate);
|
|
|
+ if (parsed != null) {
|
|
|
+ record.setDanLevel(parsed.getDanLevel());
|
|
|
+ record.setOverallScore(parsed.getOverallScore());
|
|
|
+ record.setAssessmentSummary(parsed.getAssessmentSummary());
|
|
|
+ record.setGrowthSuggestions(parsed.getGrowthSuggestions());
|
|
|
+ }
|
|
|
+ record.setStatus("completed");
|
|
|
+ record.setIsLatest(true);
|
|
|
record.setCreatedAt(new Date());
|
|
|
record.setUpdatedAt(new Date());
|
|
|
+ record.setDedupHash(computeDedupHash(childId, reportDate, "supplement", parentRecordId));
|
|
|
+
|
|
|
+ checkDuplicateAndInsert(record);
|
|
|
+ return record;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Check if a record with the given dedup parameters already exists.
|
|
|
+ */
|
|
|
+ public GrowthRecord checkDuplicate(Long childId, Date reportDate, String sourceType) {
|
|
|
+ if (reportDate == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ String hash = computeDedupHash(childId, reportDate, sourceType, null);
|
|
|
+ return growthRecordMapper.selectOne(new LambdaQueryWrapper<GrowthRecord>()
|
|
|
+ .eq(GrowthRecord::getDedupHash, hash)
|
|
|
+ .eq(GrowthRecord::getIsLatest, true)
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Mark existing records in the same dedup group as not latest.
|
|
|
+ */
|
|
|
+ public void markNotLatest(Long childId, Date reportDate, String sourceType) {
|
|
|
+ String hash = computeDedupHash(childId, reportDate, sourceType, null);
|
|
|
+ List<GrowthRecord> existing = growthRecordMapper.selectList(new LambdaQueryWrapper<GrowthRecord>()
|
|
|
+ .eq(GrowthRecord::getDedupHash, hash)
|
|
|
+ .eq(GrowthRecord::getIsLatest, true));
|
|
|
+ for (GrowthRecord rec : existing) {
|
|
|
+ rec.setIsLatest(false);
|
|
|
+ rec.setUpdatedAt(new Date());
|
|
|
+ growthRecordMapper.updateById(rec);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get all supplement records linked to a parent record.
|
|
|
+ */
|
|
|
+ public List<GrowthRecord> getSupplements(Long recordId) {
|
|
|
+ return growthRecordMapper.selectList(new LambdaQueryWrapper<GrowthRecord>()
|
|
|
+ .eq(GrowthRecord::getParentRecordId, recordId)
|
|
|
+ .orderByDesc(GrowthRecord::getCreatedAt));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== Original methods (enhanced) ==========
|
|
|
+
|
|
|
+ public GrowthRecord createRecord(GrowthRecord record) {
|
|
|
+ record.setStatus("pending");
|
|
|
+ if (record.getCreatedAt() == null) {
|
|
|
+ record.setCreatedAt(new Date());
|
|
|
+ }
|
|
|
+ if (record.getUpdatedAt() == null) {
|
|
|
+ record.setUpdatedAt(new Date());
|
|
|
+ }
|
|
|
+ if (record.getSourceType() == null) {
|
|
|
+ record.setSourceType("manual");
|
|
|
+ }
|
|
|
+ if (record.getIsLatest() == null) {
|
|
|
+ record.setIsLatest(true);
|
|
|
+ }
|
|
|
+ if (record.getDedupHash() == null && record.getChildId() != null && record.getReportDate() != null) {
|
|
|
+ record.setDedupHash(computeDedupHash(record.getChildId(), record.getReportDate(),
|
|
|
+ record.getSourceType(), record.getParentRecordId()));
|
|
|
+ }
|
|
|
growthRecordMapper.insert(record);
|
|
|
return record;
|
|
|
}
|
|
|
|
|
|
public List<GrowthRecord> getRecordsByParent(Long parentId) {
|
|
|
return growthRecordMapper.selectList(new LambdaQueryWrapper<GrowthRecord>()
|
|
|
- .eq(GrowthRecord::getParentId, parentId)
|
|
|
- .orderByDesc(GrowthRecord::getCreatedAt));
|
|
|
+ .eq(GrowthRecord::getParentId, parentId)
|
|
|
+ .orderByDesc(GrowthRecord::getCreatedAt));
|
|
|
}
|
|
|
|
|
|
public List<GrowthRecord> getRecordsByChild(Long childId) {
|
|
|
return growthRecordMapper.selectList(new LambdaQueryWrapper<GrowthRecord>()
|
|
|
- .eq(GrowthRecord::getChildId, childId)
|
|
|
- .orderByDesc(GrowthRecord::getCreatedAt));
|
|
|
+ .eq(GrowthRecord::getChildId, childId)
|
|
|
+ .orderByDesc(GrowthRecord::getCreatedAt));
|
|
|
}
|
|
|
|
|
|
public GrowthRecord getRecordById(Long id) {
|
|
|
@@ -65,18 +216,71 @@ public class GrowthRecordService {
|
|
|
record.setChildId(childId);
|
|
|
record.setParentId(parentId);
|
|
|
record.setAssessmentId(assessmentId);
|
|
|
+ record.setSourceType("platform_purchase");
|
|
|
record.setRecordTitle("DAN测评报告 - " + (result.getAssessmentDate() != null ? result.getAssessmentDate().toString() : ""));
|
|
|
record.setDanLevel(result.getDanLevel());
|
|
|
record.setOverallScore(result.getOverallScore());
|
|
|
+ record.setReportDate(result.getAssessmentDate());
|
|
|
record.setAssessmentSummary(result.getAnalysisReport() != null
|
|
|
- ? result.getAnalysisReport().substring(0, Math.min(200, result.getAnalysisReport().length()))
|
|
|
- : null);
|
|
|
+ ? result.getAnalysisReport().substring(0, Math.min(200, result.getAnalysisReport().length()))
|
|
|
+ : null);
|
|
|
record.setGrowthSuggestions(result.getGrowthSuggestions());
|
|
|
record.setExternalSync(true);
|
|
|
record.setStatus("completed");
|
|
|
+ record.setIsLatest(true);
|
|
|
record.setCreatedAt(new Date());
|
|
|
record.setUpdatedAt(new Date());
|
|
|
- growthRecordMapper.insert(record);
|
|
|
+ record.setDedupHash(computeDedupHash(childId, result.getAssessmentDate(), "platform_purchase", null));
|
|
|
+
|
|
|
+ checkDuplicateAndInsert(record);
|
|
|
return record;
|
|
|
}
|
|
|
+
|
|
|
+ // ========== Private helpers ==========
|
|
|
+
|
|
|
+ private String computeDedupHash(Long childId, Date reportDate, String sourceType, Long parentRecordId) {
|
|
|
+ if (reportDate == null) {
|
|
|
+ String uuid = UUID.randomUUID().toString();
|
|
|
+ String raw = "NO_DATE:" + uuid + "|" + childId + "|" + sourceType;
|
|
|
+ return sha256(raw);
|
|
|
+ }
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
+ String dateStr = sdf.format(reportDate);
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ sb.append(childId).append("|").append(dateStr).append("|").append(sourceType);
|
|
|
+ if (parentRecordId != null) {
|
|
|
+ sb.append("|parent:").append(parentRecordId);
|
|
|
+ }
|
|
|
+ return sha256(sb.toString());
|
|
|
+ }
|
|
|
+
|
|
|
+ private String sha256(String input) {
|
|
|
+ try {
|
|
|
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
|
+ byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
|
|
|
+ StringBuilder hexString = new StringBuilder();
|
|
|
+ for (byte b : hash) {
|
|
|
+ String hex = Integer.toHexString(0xff & b);
|
|
|
+ if (hex.length() == 1) hexString.append('0');
|
|
|
+ hexString.append(hex);
|
|
|
+ }
|
|
|
+ return hexString.toString();
|
|
|
+ } catch (NoSuchAlgorithmException e) {
|
|
|
+ throw new RuntimeException("SHA-256 not available", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void checkDuplicateAndInsert(GrowthRecord record) {
|
|
|
+ String hash = record.getDedupHash();
|
|
|
+ if (hash != null && !hash.startsWith("NO_DATE:")) {
|
|
|
+ GrowthRecord existing = growthRecordMapper.selectOne(new LambdaQueryWrapper<GrowthRecord>()
|
|
|
+ .eq(GrowthRecord::getDedupHash, hash)
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ if (existing != null) {
|
|
|
+ throw new DuplicateGrowthRecordException("Growth record already exists with hash: " + hash
|
|
|
+ + " (existing id: " + existing.getId() + ")");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ growthRecordMapper.insert(record);
|
|
|
+ }
|
|
|
}
|