|
@@ -1,14 +1,19 @@
|
|
|
package com.etotem.cfc.service;
|
|
package com.etotem.cfc.service;
|
|
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|
|
|
|
+import com.etotem.cfc.dto.MemberMatchResult;
|
|
|
|
|
+import com.etotem.cfc.dto.ParsedReportPayload;
|
|
|
import com.etotem.cfc.entity.*;
|
|
import com.etotem.cfc.entity.*;
|
|
|
import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
|
import com.etotem.cfc.mapper.HealthDiseaseRiskMapper;
|
|
import com.etotem.cfc.mapper.HealthDiseaseRiskMapper;
|
|
|
import com.etotem.cfc.mapper.HealthGutFloraMapper;
|
|
import com.etotem.cfc.mapper.HealthGutFloraMapper;
|
|
|
import com.etotem.cfc.mapper.HealthIndicatorMapper;
|
|
import com.etotem.cfc.mapper.HealthIndicatorMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.HealthReportDraftMapper;
|
|
|
import com.etotem.cfc.mapper.HealthReportMapper;
|
|
import com.etotem.cfc.mapper.HealthReportMapper;
|
|
|
import com.etotem.cfc.mapper.HealthReportPermissionMapper;
|
|
import com.etotem.cfc.mapper.HealthReportPermissionMapper;
|
|
|
import com.etotem.cfc.mapper.UserMapper;
|
|
import com.etotem.cfc.mapper.UserMapper;
|
|
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.dao.DuplicateKeyException;
|
|
import org.springframework.dao.DuplicateKeyException;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
@@ -16,8 +21,9 @@ import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
import javax.annotation.Resource;
|
|
|
import java.util.ArrayList;
|
|
import java.util.ArrayList;
|
|
|
-import java.util.Date;
|
|
|
|
|
import java.util.Collections;
|
|
import java.util.Collections;
|
|
|
|
|
+import java.util.Comparator;
|
|
|
|
|
+import java.util.Date;
|
|
|
import java.util.HashMap;
|
|
import java.util.HashMap;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
@@ -51,6 +57,12 @@ public class HealthReportService {
|
|
|
@Resource
|
|
@Resource
|
|
|
private HealthReportPermissionMapper healthReportPermissionMapper;
|
|
private HealthReportPermissionMapper healthReportPermissionMapper;
|
|
|
|
|
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private HealthReportDraftMapper draftMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private ObjectMapper objectMapper;
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 创建健康报告(含指标列表)
|
|
* 创建健康报告(含指标列表)
|
|
|
*/
|
|
*/
|
|
@@ -460,6 +472,203 @@ public class HealthReportService {
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 多字段匹配家庭成员 — 基于姓名+性别+年龄综合评分
|
|
|
|
|
+ * 返回置信度最高的匹配结果及候选列表
|
|
|
|
|
+ */
|
|
|
|
|
+ public MemberMatchResult matchMemberByProfile(String name, String gender, Integer age, Long familyId) {
|
|
|
|
|
+ MemberMatchResult result = new MemberMatchResult();
|
|
|
|
|
+ result.setCandidates(new ArrayList<>());
|
|
|
|
|
+
|
|
|
|
|
+ if (familyId == null) {
|
|
|
|
|
+ result.setMatchedMemberId(null);
|
|
|
|
|
+ result.setConfidence(0.0);
|
|
|
|
|
+ result.setMatchLabel("no_match");
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 收集所有候选成员(FamilyMember + User)
|
|
|
|
|
+ List<FamilyMember> familyMembers = familyMemberMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<FamilyMember>()
|
|
|
|
|
+ .eq(FamilyMember::getFamilyId, familyId)
|
|
|
|
|
+ );
|
|
|
|
|
+ List<User> users = userMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<User>()
|
|
|
|
|
+ .eq(User::getFamilyId, familyId)
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ List<MemberMatchResult.MemberCandidate> allCandidates = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ // 评估 FamilyMember
|
|
|
|
|
+ if (familyMembers != null) {
|
|
|
|
|
+ for (FamilyMember fm : familyMembers) {
|
|
|
|
|
+ MemberMatchResult.MemberCandidate candidate = new MemberMatchResult.MemberCandidate();
|
|
|
|
|
+ candidate.setId(fm.getId());
|
|
|
|
|
+ candidate.setNickname(fm.getNickname());
|
|
|
|
|
+ candidate.setName(fm.getNickname());
|
|
|
|
|
+ candidate.setGender(fm.getGender());
|
|
|
|
|
+ candidate.setAge(fm.getAge());
|
|
|
|
|
+
|
|
|
|
|
+ double score = 0.0;
|
|
|
|
|
+ List<String> matchedFields = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ if (name != null && !name.trim().isEmpty()) {
|
|
|
|
|
+ double nameScore = computeNameSimilarity(name.trim(), fm.getNickname());
|
|
|
|
|
+ score += nameScore * 0.6;
|
|
|
|
|
+ if (nameScore > 0.5) matchedFields.add("name");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (gender != null && !gender.trim().isEmpty()) {
|
|
|
|
|
+ String dbGender = fm.getGender();
|
|
|
|
|
+ if ("male".equalsIgnoreCase(gender) && "male".equals(dbGender)) {
|
|
|
|
|
+ score += 0.2;
|
|
|
|
|
+ matchedFields.add("gender");
|
|
|
|
|
+ } else if ("female".equalsIgnoreCase(gender) && "female".equals(dbGender)) {
|
|
|
|
|
+ score += 0.2;
|
|
|
|
|
+ matchedFields.add("gender");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (age != null && age > 0) {
|
|
|
|
|
+ int dbAge = fm.getAge();
|
|
|
|
|
+ if (dbAge > 0) {
|
|
|
|
|
+ int diff = Math.abs(age - dbAge);
|
|
|
|
|
+ if (diff <= 1) {
|
|
|
|
|
+ score += 0.2;
|
|
|
|
|
+ matchedFields.add("age");
|
|
|
|
|
+ } else if (diff <= 3) {
|
|
|
|
|
+ score += 0.1;
|
|
|
|
|
+ matchedFields.add("age");
|
|
|
|
|
+ } else if (diff <= 5) {
|
|
|
|
|
+ score += 0.05;
|
|
|
|
|
+ matchedFields.add("age");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ score = Math.min(score, 1.0);
|
|
|
|
|
+ candidate.setSimilarityScore(Math.round(score * 1000.0) / 1000.0);
|
|
|
|
|
+ candidate.setMatchFields(String.join(",", matchedFields));
|
|
|
|
|
+
|
|
|
|
|
+ if (score > 0) {
|
|
|
|
|
+ allCandidates.add(candidate);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 评估 User(父母)
|
|
|
|
|
+ if (users != null) {
|
|
|
|
|
+ for (User u : users) {
|
|
|
|
|
+ String displayName = u.getRealName() != null ? u.getRealName() : u.getNickname();
|
|
|
|
|
+ if (displayName == null || displayName.trim().isEmpty()) continue;
|
|
|
|
|
+
|
|
|
|
|
+ MemberMatchResult.MemberCandidate candidate = new MemberMatchResult.MemberCandidate();
|
|
|
|
|
+ candidate.setId(u.getId());
|
|
|
|
|
+ candidate.setName(displayName);
|
|
|
|
|
+ candidate.setGender(u.getGender());
|
|
|
|
|
+ candidate.setNickname(u.getNickname());
|
|
|
|
|
+
|
|
|
|
|
+ // 从生日计算年龄
|
|
|
|
|
+ Integer userAge = null;
|
|
|
|
|
+ if (u.getBirthday() != null) {
|
|
|
|
|
+ java.time.LocalDate birth = u.getBirthday().toInstant()
|
|
|
|
|
+ .atZone(java.time.ZoneId.systemDefault()).toLocalDate();
|
|
|
|
|
+ userAge = java.time.Period.between(birth, java.time.LocalDate.now()).getYears();
|
|
|
|
|
+ }
|
|
|
|
|
+ candidate.setAge(userAge);
|
|
|
|
|
+
|
|
|
|
|
+ double score = 0.0;
|
|
|
|
|
+ List<String> matchedFields = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ if (name != null && !name.trim().isEmpty()) {
|
|
|
|
|
+ double nameScore = computeNameSimilarity(name.trim(), displayName);
|
|
|
|
|
+ score += nameScore * 0.6;
|
|
|
|
|
+ if (nameScore > 0.5) matchedFields.add("name");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (gender != null && !gender.trim().isEmpty()) {
|
|
|
|
|
+ String dbGender = u.getGender();
|
|
|
|
|
+ if ("male".equalsIgnoreCase(gender) && "male".equals(dbGender)) {
|
|
|
|
|
+ score += 0.2;
|
|
|
|
|
+ matchedFields.add("gender");
|
|
|
|
|
+ } else if ("female".equalsIgnoreCase(gender) && "female".equals(dbGender)) {
|
|
|
|
|
+ score += 0.2;
|
|
|
|
|
+ matchedFields.add("gender");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (age != null && age > 0 && userAge != null && userAge > 0) {
|
|
|
|
|
+ int diff = Math.abs(age - userAge);
|
|
|
|
|
+ if (diff <= 2) {
|
|
|
|
|
+ score += 0.2;
|
|
|
|
|
+ matchedFields.add("age");
|
|
|
|
|
+ } else if (diff <= 5) {
|
|
|
|
|
+ score += 0.1;
|
|
|
|
|
+ matchedFields.add("age");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ score = Math.min(score, 1.0);
|
|
|
|
|
+ candidate.setSimilarityScore(Math.round(score * 1000.0) / 1000.0);
|
|
|
|
|
+ candidate.setMatchFields(String.join(",", matchedFields));
|
|
|
|
|
+
|
|
|
|
|
+ if (score > 0) {
|
|
|
|
|
+ allCandidates.add(candidate);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 按相似度降序排序
|
|
|
|
|
+ allCandidates.sort(Comparator.comparing(MemberMatchResult.MemberCandidate::getSimilarityScore).reversed());
|
|
|
|
|
+
|
|
|
|
|
+ // 取前5个候选
|
|
|
|
|
+ int limit = Math.min(5, allCandidates.size());
|
|
|
|
|
+ result.setCandidates(allCandidates.subList(0, limit));
|
|
|
|
|
+
|
|
|
|
|
+ if (limit > 0 && allCandidates.get(0).getSimilarityScore() >= 0.6) {
|
|
|
|
|
+ result.setMatchedMemberId(allCandidates.get(0).getId());
|
|
|
|
|
+ result.setConfidence(allCandidates.get(0).getSimilarityScore());
|
|
|
|
|
+ result.setMatchLabel(allCandidates.get(0).getSimilarityScore() >= 0.9 ? "exact" : "profile_match");
|
|
|
|
|
+ } else {
|
|
|
|
|
+ result.setMatchedMemberId(null);
|
|
|
|
|
+ result.setConfidence(limit > 0 ? allCandidates.get(0).getSimilarityScore() : 0.0);
|
|
|
|
|
+ result.setMatchLabel("no_match");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 计算两个姓名的相似度(0.0-1.0)
|
|
|
|
|
+ */
|
|
|
|
|
+ private double computeNameSimilarity(String reportName, String dbName) {
|
|
|
|
|
+ if (reportName == null || dbName == null) return 0.0;
|
|
|
|
|
+ String a = reportName.trim().toLowerCase();
|
|
|
|
|
+ String b = dbName.trim().toLowerCase();
|
|
|
|
|
+
|
|
|
|
|
+ // 精确匹配
|
|
|
|
|
+ if (a.equals(b)) return 1.0;
|
|
|
|
|
+
|
|
|
|
|
+ // 互相包含
|
|
|
|
|
+ if (a.contains(b) || b.contains(a)) return 0.85;
|
|
|
|
|
+
|
|
|
|
|
+ // 前2字匹配
|
|
|
|
|
+ if (a.length() >= 2 && b.length() >= 2) {
|
|
|
|
|
+ String aPrefix = a.substring(0, 2);
|
|
|
|
|
+ String bPrefix = b.substring(0, 2);
|
|
|
|
|
+ if (aPrefix.equals(bPrefix)) return 0.7;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 最后一个字相同
|
|
|
|
|
+ if (a.length() > 0 && b.length() > 0) {
|
|
|
|
|
+ if (String.valueOf(a.charAt(a.length() - 1)).equals(String.valueOf(b.charAt(b.length() - 1)))) {
|
|
|
|
|
+ return 0.4;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return 0.0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Get mind-related indicators from the latest gut microbiome report.
|
|
* Get mind-related indicators from the latest gut microbiome report.
|
|
|
* Filters indicators by categories related to mood/emotion:
|
|
* Filters indicators by categories related to mood/emotion:
|
|
@@ -569,4 +778,268 @@ public class HealthReportService {
|
|
|
.eq(HealthReportPermission::getUserId, userId)
|
|
.eq(HealthReportPermission::getUserId, userId)
|
|
|
) > 0;
|
|
) > 0;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // ======================== 自动创建成员 + 审核 ========================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从PDF身份信息自动创建家庭成员
|
|
|
|
|
+ * 如果姓名已存在则返回已有成员ID;否则创建新成员并返回新ID
|
|
|
|
|
+ */
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public Long autoCreateMemberFromReport(String name, String gender, Integer age,
|
|
|
|
|
+ Long familyId, String reportType,
|
|
|
|
|
+ String fileUrl, String originalFilename,
|
|
|
|
|
+ String payloadJson) {
|
|
|
|
|
+ if (name == null || name.trim().isEmpty()) {
|
|
|
|
|
+ throw new IllegalArgumentException("姓名不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (familyId == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("familyId不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String cleanName = name.trim().replaceAll("\\s+", "");
|
|
|
|
|
+
|
|
|
|
|
+ // 第1步:尝试匹配已有成员
|
|
|
|
|
+ Long existingMemberId = matchFamilyMemberByName(cleanName, familyId);
|
|
|
|
|
+ if (existingMemberId != null) {
|
|
|
|
|
+ log.info("姓名[{}]已匹配到现有成员 id={}", cleanName, existingMemberId);
|
|
|
|
|
+ return existingMemberId;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 第2步:创建新家庭成员
|
|
|
|
|
+ FamilyMember newMember = new FamilyMember();
|
|
|
|
|
+ newMember.setFamilyId(familyId);
|
|
|
|
|
+ newMember.setNickname(cleanName);
|
|
|
|
|
+ newMember.setGender(gender);
|
|
|
|
|
+ newMember.setRoleOverride("auto");
|
|
|
|
|
+ newMember.setShowToFamily(1);
|
|
|
|
|
+ newMember.setTotalPoints(0);
|
|
|
|
|
+ Date now = new Date();
|
|
|
|
|
+ newMember.setCreatedAt(now);
|
|
|
|
|
+ newMember.setUpdatedAt(now);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果有年龄,反推出生日期
|
|
|
|
|
+ if (age != null && age > 0) {
|
|
|
|
|
+ java.time.LocalDate birthDate = java.time.LocalDate.now()
|
|
|
|
|
+ .minusYears(age)
|
|
|
|
|
+ .minusDays(java.time.LocalDate.now().getDayOfYear() - 1);
|
|
|
|
|
+ newMember.setBirthday(java.sql.Date.valueOf(birthDate));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ familyMemberMapper.insert(newMember);
|
|
|
|
|
+ log.info("已从健康报告自动创建新成员: id={}, name={}, gender={}, age={}, familyId={}",
|
|
|
|
|
+ newMember.getId(), cleanName, gender, age, familyId);
|
|
|
|
|
+
|
|
|
|
|
+ return newMember.getId();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 审核确认报告 — 将草稿转为正式报告并绑定到指定成员
|
|
|
|
|
+ */
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public Map<String, Object> auditConfirm(Long draftId, Long subjectId, Long auditorId) {
|
|
|
|
|
+ HealthReportDraft draft = getActiveDraftOrThrow(draftId);
|
|
|
|
|
+ if (draft == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("草稿不存在或已过期");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ ParsedReportPayload.Payload payload = objectMapper.readValue(
|
|
|
|
|
+ draft.getPayloadJson(), ParsedReportPayload.Payload.class);
|
|
|
|
|
+
|
|
|
|
|
+ Long matchedMemberId = subjectId != null ? subjectId
|
|
|
|
|
+ : matchFamilyMemberByName(
|
|
|
|
|
+ payload.getSummary() != null ? payload.getSummary().getPersonName() : null,
|
|
|
|
|
+ draft.getFamilyId());
|
|
|
|
|
+
|
|
|
|
|
+ HealthReport report = buildReportFromPayload(draft, payload, matchedMemberId);
|
|
|
|
|
+ List<HealthIndicator> indicators = buildIndicatorsFromPayload(payload);
|
|
|
|
|
+ List<HealthGutFlora> gutFlora = buildGutFloraFromPayload(payload);
|
|
|
|
|
+ List<HealthDiseaseRisk> diseaseRisks = buildDiseaseRisksFromPayload(payload);
|
|
|
|
|
+
|
|
|
|
|
+ HealthReport created = createReport(report, indicators, gutFlora, diseaseRisks);
|
|
|
|
|
+
|
|
|
|
|
+ if (matchedMemberId != null) {
|
|
|
|
|
+ bindReportToMember(created.getId(), matchedMemberId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ markDraftConfirmed(draftId, created.getId());
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
|
|
+ result.put("reportId", created.getId());
|
|
|
|
|
+ result.put("subjectId", matchedMemberId);
|
|
|
|
|
+ result.put("status", "confirmed");
|
|
|
|
|
+ return result;
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("审核确认失败", e);
|
|
|
|
|
+ throw new RuntimeException("审核确认失败: " + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 审核丢弃报告 — 删除草稿,不入库
|
|
|
|
|
+ */
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public void auditDiscard(Long draftId) {
|
|
|
|
|
+ HealthReportDraft draft = getActiveDraftOrThrow(draftId);
|
|
|
|
|
+ if (draft == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("草稿不存在或已过期");
|
|
|
|
|
+ }
|
|
|
|
|
+ markDraftDiscarded(draftId);
|
|
|
|
|
+ log.info("审核丢弃草稿: draftId={}", draftId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取待审核的草稿列表(分页)
|
|
|
|
|
+ */
|
|
|
|
|
+ public List<HealthReportDraft> listPendingAudits(int page, int size) {
|
|
|
|
|
+ // MyBatis-Plus 分页查询
|
|
|
|
|
+ com.baomidou.mybatisplus.extension.plugins.pagination.Page<HealthReportDraft> p =
|
|
|
|
|
+ new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(page, size);
|
|
|
|
|
+ LambdaQueryWrapper<HealthReportDraft> wrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
+ wrapper.eq(HealthReportDraft::getStatus, "pending")
|
|
|
|
|
+ .orderByDesc(HealthReportDraft::getCreatedAt);
|
|
|
|
|
+ p = draftMapper.selectPage(p, wrapper);
|
|
|
|
|
+ return p.getRecords();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取草稿(内部方法)
|
|
|
|
|
+ */
|
|
|
|
|
+ private HealthReportDraft getActiveDraftOrThrow(Long draftId) {
|
|
|
|
|
+ HealthReportDraft draft = draftMapper.selectById(draftId);
|
|
|
|
|
+ if (draft == null || !"pending".equals(draft.getStatus())) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (draft.getExpireAt() != null && draft.getExpireAt().before(new Date())) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return draft;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void markDraftConfirmed(Long draftId, Long reportId) {
|
|
|
|
|
+ LambdaUpdateWrapper<HealthReportDraft> uw = new LambdaUpdateWrapper<>();
|
|
|
|
|
+ uw.eq(HealthReportDraft::getId, draftId)
|
|
|
|
|
+ .set(HealthReportDraft::getStatus, "confirmed")
|
|
|
|
|
+ .set(HealthReportDraft::getConfirmedReportId, reportId)
|
|
|
|
|
+ .set(HealthReportDraft::getUpdatedAt, new Date());
|
|
|
|
|
+ draftMapper.update(null, uw);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void markDraftDiscarded(Long draftId) {
|
|
|
|
|
+ LambdaUpdateWrapper<HealthReportDraft> uw = new LambdaUpdateWrapper<>();
|
|
|
|
|
+ uw.eq(HealthReportDraft::getId, draftId)
|
|
|
|
|
+ .set(HealthReportDraft::getStatus, "discarded")
|
|
|
|
|
+ .set(HealthReportDraft::getUpdatedAt, new Date());
|
|
|
|
|
+ draftMapper.update(null, uw);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ======================== Payload builders (shared with controller) ========================
|
|
|
|
|
+
|
|
|
|
|
+ HealthReport buildReportFromPayload(HealthReportDraft draft, ParsedReportPayload.Payload payload, Long subjectId) {
|
|
|
|
|
+ HealthReport report = new HealthReport();
|
|
|
|
|
+ report.setUserId(draft.getUserId());
|
|
|
|
|
+ report.setFamilyId(draft.getFamilyId());
|
|
|
|
|
+ report.setSubjectId(subjectId);
|
|
|
|
|
+ report.setReportType(draft.getReportType());
|
|
|
|
|
+ report.setFileUrl(draft.getFileUrl());
|
|
|
|
|
+
|
|
|
|
|
+ ParsedReportPayload.Summary s = payload.getSummary();
|
|
|
|
|
+ if (s != null) {
|
|
|
|
|
+ report.setOverallScore(s.getOverallScore());
|
|
|
|
|
+ report.setGutHealthScore(s.getGutHealthScore());
|
|
|
|
|
+ report.setChronicDiseaseScore(s.getChronicDiseaseScore());
|
|
|
|
|
+ report.setNutritionScore(s.getNutritionScore());
|
|
|
|
|
+ report.setBalanceScore(s.getBalanceScore());
|
|
|
|
|
+ report.setDiversityScore(s.getDiversityScore());
|
|
|
|
|
+ report.setBeneficialScore(s.getBeneficialScore());
|
|
|
|
|
+ report.setHarmfulScore(s.getHarmfulScore());
|
|
|
|
|
+ report.setCoreGenusScore(s.getCoreGenusScore());
|
|
|
|
|
+ report.setGutAge(s.getGutAge());
|
|
|
|
|
+ report.setGutType(s.getGutType());
|
|
|
|
|
+ report.setReportNumber(s.getReportNumber());
|
|
|
|
|
+ report.setPersonName(s.getPersonName());
|
|
|
|
|
+
|
|
|
|
|
+ if (s.getReportDate() != null && !s.getReportDate().isEmpty()) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
|
|
|
|
|
+ report.setReportDate(sdf.parse(s.getReportDate()));
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ report.setReportDate(new Date());
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ report.setReportDate(new Date());
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ report.setReportDate(new Date());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ report.setRemark("募极生物肠道菌群检测报告");
|
|
|
|
|
+ return report;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<HealthIndicator> buildIndicatorsFromPayload(ParsedReportPayload.Payload payload) {
|
|
|
|
|
+ List<ParsedReportPayload.Indicator> list = payload.getIndicators();
|
|
|
|
|
+ if (list == null || list.isEmpty()) return new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ List<HealthIndicator> indicators = new ArrayList<>();
|
|
|
|
|
+ for (int i = 0; i < list.size(); i++) {
|
|
|
|
|
+ ParsedReportPayload.Indicator pi = list.get(i);
|
|
|
|
|
+ HealthIndicator ind = new HealthIndicator();
|
|
|
|
|
+ ind.setCategory(pi.getCategory());
|
|
|
|
|
+ ind.setIndicatorName(pi.getIndicatorName());
|
|
|
|
|
+ ind.setIndicatorValue(pi.getIndicatorValue());
|
|
|
|
|
+ ind.setUnit(pi.getUnit());
|
|
|
|
|
+ ind.setRefRange(pi.getRefRange());
|
|
|
|
|
+ ind.setStatus(pi.getStatus());
|
|
|
|
|
+ ind.setSymptoms(pi.getSymptoms());
|
|
|
|
|
+ ind.setSortOrder(i);
|
|
|
|
|
+ indicators.add(ind);
|
|
|
|
|
+ }
|
|
|
|
|
+ return indicators;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<HealthGutFlora> buildGutFloraFromPayload(ParsedReportPayload.Payload payload) {
|
|
|
|
|
+ List<HealthGutFlora> result = new ArrayList<>();
|
|
|
|
|
+ if (payload.getGutFlora() != null) {
|
|
|
|
|
+ for (ParsedReportPayload.Flora pf : payload.getGutFlora()) {
|
|
|
|
|
+ result.add(convertToGutFlora(pf));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (payload.getProbioticSpecies() != null) {
|
|
|
|
|
+ for (ParsedReportPayload.Flora pf : payload.getProbioticSpecies()) {
|
|
|
|
|
+ result.add(convertToGutFlora(pf));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private HealthGutFlora convertToGutFlora(ParsedReportPayload.Flora pf) {
|
|
|
|
|
+ HealthGutFlora gf = new HealthGutFlora();
|
|
|
|
|
+ gf.setBacteriaName(pf.getBacteriaName());
|
|
|
|
|
+ gf.setBacteriaValue(pf.getBacteriaValue());
|
|
|
|
|
+ gf.setNormalRange(pf.getNormalRange());
|
|
|
|
|
+ gf.setPopulationLevel(pf.getPopulationLevel());
|
|
|
|
|
+ gf.setDetectionRate(pf.getDetectionRate());
|
|
|
|
|
+ gf.setDescription(pf.getDescription());
|
|
|
|
|
+ gf.setCategory(pf.getCategory());
|
|
|
|
|
+ gf.setLevel(pf.getLevel());
|
|
|
|
|
+ return gf;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<HealthDiseaseRisk> buildDiseaseRisksFromPayload(ParsedReportPayload.Payload payload) {
|
|
|
|
|
+ List<ParsedReportPayload.DiseaseRisk> list = payload.getDiseaseRisks();
|
|
|
|
|
+ if (list == null || list.isEmpty()) return new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ List<HealthDiseaseRisk> risks = new ArrayList<>();
|
|
|
|
|
+ for (ParsedReportPayload.DiseaseRisk pr : list) {
|
|
|
|
|
+ HealthDiseaseRisk dr = new HealthDiseaseRisk();
|
|
|
|
|
+ dr.setDiseaseName(pr.getDiseaseName());
|
|
|
|
|
+ dr.setRiskValue(pr.getRiskValue());
|
|
|
|
|
+ dr.setRiskLevel(pr.getRiskLevel());
|
|
|
|
|
+ risks.add(dr);
|
|
|
|
|
+ }
|
|
|
|
|
+ return risks;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|