|
|
@@ -0,0 +1,529 @@
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.etotem.cfc.dto.*;
|
|
|
+import com.etotem.cfc.entity.FamilyMember;
|
|
|
+import com.etotem.cfc.entity.InsurancePolicy;
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
+import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
|
+import com.etotem.cfc.mapper.InsurancePolicyMapper;
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.*;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 家庭保险规划服务
|
|
|
+ * <p>
|
|
|
+ * 根据家庭成员的身/心/智维度评分 + 已有保单数据,
|
|
|
+ * 分析覆盖缺口,生成个性化保险规划建议。
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+public class InsurancePlanningService {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private UserMapper userMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private EnergyService energyService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private InsurancePolicyMapper insurancePolicyMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private FamilyMemberMapper familyMemberMapper;
|
|
|
+
|
|
|
+ private static final DateTimeFormatter DF = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成家庭保险规划报告
|
|
|
+ */
|
|
|
+ public PlanningReportDTO generatePlanningReport(Long userId) {
|
|
|
+ User user = userMapper.selectById(userId);
|
|
|
+ if (user == null || user.getFamilyId() == null) {
|
|
|
+ return buildEmptyReport();
|
|
|
+ }
|
|
|
+
|
|
|
+ Long familyId = user.getFamilyId();
|
|
|
+
|
|
|
+ // 1. 获取家庭能量数据
|
|
|
+ EnergySandboxDTO sandbox = energyService.calculateFamilyEnergy(familyId);
|
|
|
+
|
|
|
+ // 2. 获取所有家庭成员的已有保单
|
|
|
+ List<User> familyUsers = userMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<User>().eq(User::getFamilyId, familyId));
|
|
|
+ List<Long> userIds = familyUsers.stream()
|
|
|
+ .map(User::getId)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ List<InsurancePolicy> allPolicies = new ArrayList<>();
|
|
|
+ if (!userIds.isEmpty()) {
|
|
|
+ LambdaQueryWrapper<InsurancePolicy> policyWrapper = new LambdaQueryWrapper<InsurancePolicy>()
|
|
|
+ .in(InsurancePolicy::getUserId, userIds)
|
|
|
+ .or()
|
|
|
+ .in(InsurancePolicy::getChildId,
|
|
|
+ familyMemberMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<FamilyMember>()
|
|
|
+ .eq(FamilyMember::getFamilyId, familyId))
|
|
|
+ .stream()
|
|
|
+ .map(m -> m.getId())
|
|
|
+ .collect(Collectors.toList()));
|
|
|
+ allPolicies = insurancePolicyMapper.selectList(policyWrapper);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 构建成员覆盖数据
|
|
|
+ List<MemberCoverageDTO> memberCoverages = buildMemberCoverages(sandbox.getMembers(), allPolicies);
|
|
|
+
|
|
|
+ // 4. 分析覆盖缺口
|
|
|
+ List<InsuranceGapDTO> gaps = analyzeGaps(memberCoverages);
|
|
|
+
|
|
|
+ // 5. 计算综合评分
|
|
|
+ int coverageScore = calcCoverageScore(memberCoverages, gaps);
|
|
|
+ String overallStatus = calcOverallStatus(coverageScore);
|
|
|
+ String overallRecommendation = buildOverallRecommendation(coverageScore, gaps);
|
|
|
+
|
|
|
+ // 6. 统计优先级
|
|
|
+ int high = 0, medium = 0, low = 0;
|
|
|
+ for (InsuranceGapDTO g : gaps) {
|
|
|
+ if ("high".equals(g.getPriority())) high++;
|
|
|
+ else if ("medium".equals(g.getPriority())) medium++;
|
|
|
+ else low++;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 7. 构建报告
|
|
|
+ PlanningReportDTO report = new PlanningReportDTO();
|
|
|
+ report.setGeneratedAt(LocalDate.now().format(DF));
|
|
|
+ report.setFamilyId(familyId);
|
|
|
+ report.setFamilyBodyScore(sandbox.getBodyScore());
|
|
|
+ report.setFamilyMindScore(sandbox.getMindScore());
|
|
|
+ report.setFamilyWisdomScore(sandbox.getWisdomScore());
|
|
|
+ report.setFamilyOverallScore(sandbox.getOverallScore());
|
|
|
+ report.setMemberCount(memberCoverages.size());
|
|
|
+ report.setMemberCoverages(memberCoverages);
|
|
|
+ report.setGaps(gaps);
|
|
|
+ report.setHighPriorityGapCount(high);
|
|
|
+ report.setMediumPriorityGapCount(medium);
|
|
|
+ report.setLowPriorityGapCount(low);
|
|
|
+ report.setCoverageScore(coverageScore);
|
|
|
+ report.setOverallStatus(overallStatus);
|
|
|
+ report.setOverallRecommendation(overallRecommendation);
|
|
|
+
|
|
|
+ return report;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== 构建成员覆盖数据 =====
|
|
|
+
|
|
|
+ private List<MemberCoverageDTO> buildMemberCoverages(List<MemberEnergyDTO> members, List<InsurancePolicy> policies) {
|
|
|
+ if (members == null || members.isEmpty()) return new ArrayList<>();
|
|
|
+
|
|
|
+ // 按 memberId 分组保单
|
|
|
+ Map<Long, List<InsurancePolicy>> policyMap = policies.stream()
|
|
|
+ .collect(Collectors.groupingBy(p -> {
|
|
|
+ if (p.getChildId() != null) return p.getChildId();
|
|
|
+ return p.getUserId() != null ? -p.getUserId() : 0L;
|
|
|
+ }));
|
|
|
+
|
|
|
+ List<MemberCoverageDTO> result = new ArrayList<>();
|
|
|
+ for (MemberEnergyDTO m : members) {
|
|
|
+ MemberCoverageDTO dto = new MemberCoverageDTO();
|
|
|
+ dto.setMemberId(m.getMemberId());
|
|
|
+ dto.setMemberType(m.getMemberType());
|
|
|
+ dto.setName(m.getName());
|
|
|
+ dto.setAvatar(m.getAvatar());
|
|
|
+ dto.setFamilyRole(m.getFamilyRole());
|
|
|
+ dto.setBodyScore(m.getBodyScore());
|
|
|
+ dto.setMindScore(m.getMindScore());
|
|
|
+ dto.setWisdomScore(m.getWisdomScore());
|
|
|
+
|
|
|
+ // 年龄:从 familyMember 获取
|
|
|
+ try {
|
|
|
+ FamilyMember fm = familyMemberMapper.selectById(m.getMemberId());
|
|
|
+ if (fm != null) {
|
|
|
+ dto.setAge(fm.getAge());
|
|
|
+ dto.setGender(fm.getGender());
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.debug("获取成员年龄失败, memberId={}", m.getMemberId());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保单数据
|
|
|
+ List<InsurancePolicy> memberPolicies = policyMap.getOrDefault(m.getMemberId(), Collections.emptyList());
|
|
|
+ memberPolicies.addAll(policyMap.getOrDefault(-m.getMemberId(), Collections.emptyList()));
|
|
|
+
|
|
|
+ dto.setPolicyCount(memberPolicies.size());
|
|
|
+ dto.setExistingPolicyTypes(memberPolicies.stream()
|
|
|
+ .map(InsurancePolicy::getPolicyType)
|
|
|
+ .filter(t -> t != null && !t.isEmpty())
|
|
|
+ .distinct()
|
|
|
+ .collect(Collectors.toList()));
|
|
|
+ long totalSum = memberPolicies.stream()
|
|
|
+ .filter(p -> p.getSumInsured() != null)
|
|
|
+ .mapToLong(InsurancePolicy::getSumInsured)
|
|
|
+ .sum();
|
|
|
+ dto.setTotalSumInsured(totalSum);
|
|
|
+
|
|
|
+ // 覆盖状态分析
|
|
|
+ dto.setMedicalCoverage(calcMedicalCoverage(memberPolicies, m.getBodyScore()));
|
|
|
+ dto.setCriticalIllnessCoverage(calcCriticalIllnessCoverage(memberPolicies, m.getBodyScore()));
|
|
|
+ dto.setEducationCoverage(calcEducationCoverage(memberPolicies, m.getWisdomScore(), "child".equals(m.getMemberType())));
|
|
|
+ dto.setAccidentCoverage(calcAccidentCoverage(memberPolicies));
|
|
|
+ dto.setMentalHealthCoverage(calcMentalHealthCoverage(memberPolicies, m.getMindScore()));
|
|
|
+
|
|
|
+ result.add(dto);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== 覆盖状态计算 =====
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 医疗险覆盖:
|
|
|
+ * - comprehensive: 有医疗险 + bodyScore >= 60
|
|
|
+ * - adequate: 有医疗险 + bodyScore 40-59
|
|
|
+ * - basic: 有医疗险 + bodyScore < 40
|
|
|
+ * - none: 无医疗险
|
|
|
+ */
|
|
|
+ private String calcMedicalCoverage(List<InsurancePolicy> policies, Integer bodyScore) {
|
|
|
+ boolean hasMedical = policies.stream()
|
|
|
+ .anyMatch(p -> "medical".equals(p.getPolicyType()));
|
|
|
+ if (!hasMedical) return "none";
|
|
|
+ if (bodyScore == null) return "adequate";
|
|
|
+ if (bodyScore >= 60) return "comprehensive";
|
|
|
+ if (bodyScore >= 40) return "adequate";
|
|
|
+ return "basic";
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 重疾险覆盖:
|
|
|
+ * - comprehensive: 有重疾险 + bodyScore >= 50
|
|
|
+ * - adequate: 有重疾险
|
|
|
+ * - none: 无重疾险
|
|
|
+ */
|
|
|
+ private String calcCriticalIllnessCoverage(List<InsurancePolicy> policies, Integer bodyScore) {
|
|
|
+ boolean hasCI = policies.stream()
|
|
|
+ .anyMatch(p -> "critical_illness".equals(p.getPolicyType()) || "life".equals(p.getPolicyType()));
|
|
|
+ if (!hasCI) return "none";
|
|
|
+ if (bodyScore != null && bodyScore >= 50) return "comprehensive";
|
|
|
+ return "adequate";
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 教育金险覆盖:
|
|
|
+ * - comprehensive: 有教育金险 + wisdomScore >= 60
|
|
|
+ * - adequate: 有教育金险
|
|
|
+ * - none: 无教育金险
|
|
|
+ * 只对 child 类型有意义
|
|
|
+ */
|
|
|
+ private String calcEducationCoverage(List<InsurancePolicy> policies, Integer wisdomScore, boolean isChild) {
|
|
|
+ if (!isChild) return "not_applicable";
|
|
|
+ boolean hasEdu = policies.stream()
|
|
|
+ .anyMatch(p -> "education".equals(p.getPolicyType()));
|
|
|
+ if (!hasEdu) return "none";
|
|
|
+ if (wisdomScore != null && wisdomScore >= 60) return "comprehensive";
|
|
|
+ return "adequate";
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 意外险覆盖:
|
|
|
+ * - comprehensive: 有意外险
|
|
|
+ * - none: 无意外险
|
|
|
+ */
|
|
|
+ private String calcAccidentCoverage(List<InsurancePolicy> policies) {
|
|
|
+ boolean hasAccident = policies.stream()
|
|
|
+ .anyMatch(p -> "accident".equals(p.getPolicyType()));
|
|
|
+ return hasAccident ? "comprehensive" : "none";
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 心理健康险覆盖:
|
|
|
+ * - adequate: 有相关险种或 mindScore >= 70
|
|
|
+ * - basic: mindScore 50-69
|
|
|
+ * - none: mindScore < 50 且无专门险种
|
|
|
+ */
|
|
|
+ private String calcMentalHealthCoverage(List<InsurancePolicy> policies, Integer mindScore) {
|
|
|
+ boolean hasMental = policies.stream()
|
|
|
+ .anyMatch(p -> "mental_health".equals(p.getPolicyType()) || "health".equals(p.getPolicyType()));
|
|
|
+ if (mindScore == null) mindScore = 0;
|
|
|
+ if (hasMental) return "adequate";
|
|
|
+ if (mindScore >= 70) return "adequate";
|
|
|
+ if (mindScore >= 50) return "basic";
|
|
|
+ return "none";
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== 缺口分析 =====
|
|
|
+
|
|
|
+ private List<InsuranceGapDTO> analyzeGaps(List<MemberCoverageDTO> members) {
|
|
|
+ List<InsuranceGapDTO> gaps = new ArrayList<>();
|
|
|
+
|
|
|
+ for (MemberCoverageDTO m : members) {
|
|
|
+ // 1. 医疗险缺口(身维度)
|
|
|
+ InsuranceGapDTO medicalGap = analyzeMedicalGap(m);
|
|
|
+ if (medicalGap != null) gaps.add(medicalGap);
|
|
|
+
|
|
|
+ // 2. 重疾险缺口(身维度严重时)
|
|
|
+ InsuranceGapDTO ciGap = analyzeCriticalIllnessGap(m);
|
|
|
+ if (ciGap != null) gaps.add(ciGap);
|
|
|
+
|
|
|
+ // 3. 心理健康险缺口(心维度)
|
|
|
+ InsuranceGapDTO mentalGap = analyzeMentalHealthGap(m);
|
|
|
+ if (mentalGap != null) gaps.add(mentalGap);
|
|
|
+
|
|
|
+ // 4. 教育金险缺口(仅孩子,智维度)
|
|
|
+ if ("child".equals(m.getMemberType())) {
|
|
|
+ InsuranceGapDTO eduGap = analyzeEducationGap(m);
|
|
|
+ if (eduGap != null) gaps.add(eduGap);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 意外险缺口
|
|
|
+ InsuranceGapDTO accidentGap = analyzeAccidentGap(m);
|
|
|
+ if (accidentGap != null) gaps.add(accidentGap);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按紧迫度降序排列
|
|
|
+ gaps.sort((a, b) -> b.getUrgencyScore().compareTo(a.getUrgencyScore()));
|
|
|
+ return gaps;
|
|
|
+ }
|
|
|
+
|
|
|
+ private InsuranceGapDTO analyzeMedicalGap(MemberCoverageDTO m) {
|
|
|
+ if ("comprehensive".equals(m.getMedicalCoverage()) || "adequate".equals(m.getMedicalCoverage())) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ InsuranceGapDTO gap = new InsuranceGapDTO();
|
|
|
+ gap.setMemberId(m.getMemberId());
|
|
|
+ gap.setMemberName(m.getName());
|
|
|
+ gap.setFamilyRole(m.getFamilyRole());
|
|
|
+ gap.setGapType("medical");
|
|
|
+ gap.setGapTypeName("医疗险");
|
|
|
+ gap.setRelatedDimension("body");
|
|
|
+ gap.setCoverageLevel(m.getMedicalCoverage());
|
|
|
+
|
|
|
+ int bodyScore = m.getBodyScore() != null ? m.getBodyScore() : 50;
|
|
|
+
|
|
|
+ if ("basic".equals(m.getMedicalCoverage())) {
|
|
|
+ gap.setGapDescription(String.format("%s健康评分%d分,已有基础医疗险,建议升级至综合医疗险以覆盖更多门诊和特效药费用",
|
|
|
+ m.getName(), bodyScore));
|
|
|
+ gap.setPriority("medium");
|
|
|
+ gap.setUrgencyScore(60);
|
|
|
+ } else {
|
|
|
+ // none
|
|
|
+ gap.setGapDescription(String.format("%s健康评分%d分,暂无医疗险。身体是财富的基石,强烈建议尽快配置医疗险,避免高额医疗费用风险",
|
|
|
+ m.getName(), bodyScore));
|
|
|
+ gap.setPriority("high");
|
|
|
+ gap.setUrgencyScore(80 + (50 - bodyScore) / 2);
|
|
|
+ }
|
|
|
+
|
|
|
+ gap.setRecommendedTypes(Arrays.asList("医疗险", "住院津贴险"));
|
|
|
+ gap.setRecommendedMinCoverage(500000L);
|
|
|
+ gap.setRecommendedMaxCoverage(2000000L);
|
|
|
+
|
|
|
+ return gap;
|
|
|
+ }
|
|
|
+
|
|
|
+ private InsuranceGapDTO analyzeCriticalIllnessGap(MemberCoverageDTO m) {
|
|
|
+ if ("comprehensive".equals(m.getCriticalIllnessCoverage()) || "adequate".equals(m.getCriticalIllnessCoverage())) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (m.getBodyScore() != null && m.getBodyScore() < 60) {
|
|
|
+ InsuranceGapDTO gap = new InsuranceGapDTO();
|
|
|
+ gap.setMemberId(m.getMemberId());
|
|
|
+ gap.setMemberName(m.getName());
|
|
|
+ gap.setFamilyRole(m.getFamilyRole());
|
|
|
+ gap.setGapType("critical_illness");
|
|
|
+ gap.setGapTypeName("重疾险");
|
|
|
+ gap.setRelatedDimension("body");
|
|
|
+ gap.setCoverageLevel(m.getCriticalIllnessCoverage());
|
|
|
+
|
|
|
+ int bodyScore = m.getBodyScore();
|
|
|
+ gap.setPriority("high");
|
|
|
+ gap.setUrgencyScore(70 + (60 - bodyScore));
|
|
|
+ gap.setGapDescription(String.format("%s健康评分%d分,身体底评分偏低,面临较高重疾风险。建议配置重疾险,一旦确诊即可获赔保险金",
|
|
|
+ m.getName(), bodyScore));
|
|
|
+ gap.setRecommendedTypes(Arrays.asList("重疾险", "防癌险"));
|
|
|
+ gap.setRecommendedMinCoverage(300000L);
|
|
|
+ gap.setRecommendedMaxCoverage(1000000L);
|
|
|
+
|
|
|
+ return gap;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private InsuranceGapDTO analyzeMentalHealthGap(MemberCoverageDTO m) {
|
|
|
+ String level = m.getMentalHealthCoverage();
|
|
|
+ if ("adequate".equals(level)) return null;
|
|
|
+
|
|
|
+ int mindScore = m.getMindScore() != null ? m.getMindScore() : 50;
|
|
|
+
|
|
|
+ if ("basic".equals(level)) {
|
|
|
+ InsuranceGapDTO gap = new InsuranceGapDTO();
|
|
|
+ gap.setMemberId(m.getMemberId());
|
|
|
+ gap.setMemberName(m.getName());
|
|
|
+ gap.setFamilyRole(m.getFamilyRole());
|
|
|
+ gap.setGapType("mental_health");
|
|
|
+ gap.setGapTypeName("心理健康险");
|
|
|
+ gap.setRelatedDimension("mind");
|
|
|
+ gap.setCoverageLevel(level);
|
|
|
+ gap.setPriority("medium");
|
|
|
+ gap.setUrgencyScore(50);
|
|
|
+ gap.setGapDescription(String.format("%s心理评分%d分,建议关注情绪管理,可考虑附加心理咨询服务的健康险",
|
|
|
+ m.getName(), mindScore));
|
|
|
+ gap.setRecommendedTypes(Arrays.asList("健康险(含心理咨询)", "EMI保险"));
|
|
|
+ gap.setRecommendedMinCoverage(100000L);
|
|
|
+ gap.setRecommendedMaxCoverage(500000L);
|
|
|
+ return gap;
|
|
|
+ }
|
|
|
+
|
|
|
+ // none
|
|
|
+ if (mindScore < 70) {
|
|
|
+ InsuranceGapDTO gap = new InsuranceGapDTO();
|
|
|
+ gap.setMemberId(m.getMemberId());
|
|
|
+ gap.setMemberName(m.getName());
|
|
|
+ gap.setFamilyRole(m.getFamilyRole());
|
|
|
+ gap.setGapType("mental_health");
|
|
|
+ gap.setGapTypeName("心理健康险");
|
|
|
+ gap.setRelatedDimension("mind");
|
|
|
+ gap.setCoverageLevel(level);
|
|
|
+ gap.setPriority("medium");
|
|
|
+ gap.setUrgencyScore(40 + (70 - mindScore) / 2);
|
|
|
+ gap.setGapDescription(String.format("%s心理评分%d分,情绪健康需要关注。建议配置心理健康相关保障,覆盖心理咨询和情绪疏导费用",
|
|
|
+ m.getName(), mindScore));
|
|
|
+ gap.setRecommendedTypes(Arrays.asList("健康险(含心理咨询)", "EMI保险"));
|
|
|
+ gap.setRecommendedMinCoverage(50000L);
|
|
|
+ gap.setRecommendedMaxCoverage(300000L);
|
|
|
+ return gap;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private InsuranceGapDTO analyzeEducationGap(MemberCoverageDTO m) {
|
|
|
+ if ("comprehensive".equals(m.getEducationCoverage()) || "adequate".equals(m.getEducationCoverage())
|
|
|
+ || "not_applicable".equals(m.getEducationCoverage())) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ int wisdomScore = m.getWisdomScore() != null ? m.getWisdomScore() : 50;
|
|
|
+
|
|
|
+ InsuranceGapDTO gap = new InsuranceGapDTO();
|
|
|
+ gap.setMemberId(m.getMemberId());
|
|
|
+ gap.setMemberName(m.getName());
|
|
|
+ gap.setFamilyRole(m.getFamilyRole());
|
|
|
+ gap.setGapType("education");
|
|
|
+ gap.setGapTypeName("教育金险");
|
|
|
+ gap.setRelatedDimension("wisdom");
|
|
|
+ gap.setCoverageLevel(m.getEducationCoverage());
|
|
|
+ gap.setPriority("medium");
|
|
|
+ gap.setUrgencyScore(50 + (70 - wisdomScore) / 3);
|
|
|
+ gap.setGapDescription(String.format("%s智慧评分%d分,正处于成长关键期。教育金险可强制储备教育资金,专款专用",
|
|
|
+ m.getName(), wisdomScore));
|
|
|
+ gap.setRecommendedTypes(Arrays.asList("教育金险", "婚嫁金险", "创业金险"));
|
|
|
+ gap.setRecommendedMinCoverage(100000L);
|
|
|
+ gap.setRecommendedMaxCoverage(500000L);
|
|
|
+
|
|
|
+ return gap;
|
|
|
+ }
|
|
|
+
|
|
|
+ private InsuranceGapDTO analyzeAccidentGap(MemberCoverageDTO m) {
|
|
|
+ if ("comprehensive".equals(m.getAccidentCoverage())) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ InsuranceGapDTO gap = new InsuranceGapDTO();
|
|
|
+ gap.setMemberId(m.getMemberId());
|
|
|
+ gap.setMemberName(m.getName());
|
|
|
+ gap.setFamilyRole(m.getFamilyRole());
|
|
|
+ gap.setGapType("accident");
|
|
|
+ gap.setGapTypeName("意外险");
|
|
|
+ gap.setRelatedDimension("body");
|
|
|
+ gap.setCoverageLevel(m.getAccidentCoverage());
|
|
|
+ gap.setPriority("high");
|
|
|
+ gap.setUrgencyScore(85);
|
|
|
+ gap.setGapDescription(String.format("%s暂无意外险。意外风险无处不在,配置意外险保费低、保障高,是性价比最高的保障险种",
|
|
|
+ m.getName()));
|
|
|
+ gap.setRecommendedTypes(Arrays.asList("综合意外险", "交通意外险"));
|
|
|
+ gap.setRecommendedMinCoverage(200000L);
|
|
|
+ gap.setRecommendedMaxCoverage(1000000L);
|
|
|
+
|
|
|
+ return gap;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== 综合评分 =====
|
|
|
+
|
|
|
+ private int calcCoverageScore(List<MemberCoverageDTO> members, List<InsuranceGapDTO> gaps) {
|
|
|
+ if (members.isEmpty()) return 0;
|
|
|
+
|
|
|
+ // 扣分逻辑:每个high优先级缺口-15,medium-8,low-3
|
|
|
+ int penalty = 0;
|
|
|
+ for (InsuranceGapDTO g : gaps) {
|
|
|
+ if ("high".equals(g.getPriority())) penalty += 15;
|
|
|
+ else if ("medium".equals(g.getPriority())) penalty += 8;
|
|
|
+ else penalty += 3;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 基础分 + 有保单的加分
|
|
|
+ int baseScore = 80;
|
|
|
+ for (MemberCoverageDTO m : members) {
|
|
|
+ int memberBase = 20;
|
|
|
+ if (!"none".equals(m.getMedicalCoverage())) memberBase += 10;
|
|
|
+ if (!"none".equals(m.getCriticalIllnessCoverage())) memberBase += 10;
|
|
|
+ if (!"none".equals(m.getAccidentCoverage())) memberBase += 10;
|
|
|
+ if (!"none".equals(m.getMentalHealthCoverage()) && !"not_applicable".equals(m.getMentalHealthCoverage())) memberBase += 5;
|
|
|
+ if (!"none".equals(m.getEducationCoverage()) && !"not_applicable".equals(m.getEducationCoverage())) memberBase += 5;
|
|
|
+ baseScore += memberBase;
|
|
|
+ }
|
|
|
+
|
|
|
+ int score = baseScore - penalty;
|
|
|
+ // 每成员平均
|
|
|
+ int avgScore = score / members.size();
|
|
|
+ return Math.max(0, Math.min(100, avgScore));
|
|
|
+ }
|
|
|
+
|
|
|
+ private String calcOverallStatus(int score) {
|
|
|
+ if (score >= 80) return "充足";
|
|
|
+ if (score >= 60) return "基本";
|
|
|
+ if (score >= 40) return "不足";
|
|
|
+ return "严重不足";
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildOverallRecommendation(int score, List<InsuranceGapDTO> gaps) {
|
|
|
+ long highCount = gaps.stream().filter(g -> "high".equals(g.getPriority())).count();
|
|
|
+ long mediumCount = gaps.stream().filter(g -> "medium".equals(g.getPriority())).count();
|
|
|
+
|
|
|
+ if (score >= 80) {
|
|
|
+ return "您的家庭保险配置较为完善,继续保持。建议每年定期复审,根据家庭成员健康状况变化调整保额。";
|
|
|
+ }
|
|
|
+ if (score >= 60) {
|
|
|
+ return String.format("您的家庭保险基本覆盖了主要风险,但仍有 %d 个高优先级缺口和 %d 个中优先级缺口需要关注。", highCount, mediumCount);
|
|
|
+ }
|
|
|
+ if (score >= 40) {
|
|
|
+ return String.format("您的家庭保险配置存在明显不足,%d 个高优先级缺口需要尽快补全。意外险和医疗险是首要配置险种。", highCount);
|
|
|
+ }
|
|
|
+ return String.format("您的家庭保险严重不足,%d 个高优先级缺口急需补全。保险是家庭财富的防线,建议尽快为每位成员配置意外险和医疗险。", highCount);
|
|
|
+ }
|
|
|
+
|
|
|
+ private PlanningReportDTO buildEmptyReport() {
|
|
|
+ PlanningReportDTO report = new PlanningReportDTO();
|
|
|
+ report.setGeneratedAt(LocalDate.now().format(DF));
|
|
|
+ report.setFamilyBodyScore(0);
|
|
|
+ report.setFamilyMindScore(0);
|
|
|
+ report.setFamilyWisdomScore(0);
|
|
|
+ report.setFamilyOverallScore(0);
|
|
|
+ report.setMemberCount(0);
|
|
|
+ report.setMemberCoverages(new ArrayList<>());
|
|
|
+ report.setGaps(new ArrayList<>());
|
|
|
+ report.setHighPriorityGapCount(0);
|
|
|
+ report.setMediumPriorityGapCount(0);
|
|
|
+ report.setLowPriorityGapCount(0);
|
|
|
+ report.setCoverageScore(0);
|
|
|
+ report.setOverallStatus("无数据");
|
|
|
+ report.setOverallRecommendation("请先加入家庭后再使用保险规划功能");
|
|
|
+ return report;
|
|
|
+ }
|
|
|
+}
|