|
|
@@ -0,0 +1,578 @@
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.etotem.cfc.dto.*;
|
|
|
+import com.etotem.cfc.entity.*;
|
|
|
+import com.etotem.cfc.entity.Family;
|
|
|
+import com.etotem.cfc.entity.FamilyMember;
|
|
|
+import com.etotem.cfc.entity.FamilyMemberAttributes;
|
|
|
+import com.etotem.cfc.entity.ZodiacPairing;
|
|
|
+import com.etotem.cfc.entity.BloodTypePairing;
|
|
|
+import com.etotem.cfc.entity.LifeNumberRelationship;
|
|
|
+import com.etotem.cfc.mapper.*;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.util.*;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class CompatibilityService {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private FamilyMemberAttributeService familyMemberAttributeService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private FamilyMemberMapper familyMemberMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private FamilyMapper familyMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private ZodiacPairingMapper zodiacPairingMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private BloodTypePairingMapper bloodTypePairingMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private LifeNumberRelationshipMapper lifeNumberRelationshipMapper;
|
|
|
+
|
|
|
+ private static final String[] ZODIAC_CODES = {"rat", "ox", "tiger", "rabbit", "dragon", "snake",
|
|
|
+ "horse", "sheep", "monkey", "rooster", "dog", "pig"};
|
|
|
+
|
|
|
+ private static final String[] WUXING_ELEMENTS = {"wood", "fire", "earth", "metal", "water"};
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Full duo compatibility analysis
|
|
|
+ */
|
|
|
+ public CompatibilityResultVO computeCompatibility(Long memberId1, String memberType1,
|
|
|
+ Long memberId2, String memberType2) {
|
|
|
+ FamilyMemberVO member1 = buildMemberVO(memberId1);
|
|
|
+ FamilyMemberVO member2 = buildMemberVO(memberId2);
|
|
|
+
|
|
|
+ FamilyMemberAttributes attrs1 = familyMemberAttributeService.getByMember(memberId1, memberType1);
|
|
|
+ FamilyMemberAttributes attrs2 = familyMemberAttributeService.getByMember(memberId2, memberType2);
|
|
|
+
|
|
|
+ CompatibilityResultVO result = new CompatibilityResultVO();
|
|
|
+ result.setMember1(member1);
|
|
|
+ result.setMember2(member2);
|
|
|
+
|
|
|
+ // Zodiac
|
|
|
+ ZodiacCompatibilityVO zodiac = computeZodiac(attrs1, attrs2);
|
|
|
+ result.setZodiac(zodiac);
|
|
|
+
|
|
|
+ // Wuxing
|
|
|
+ WuxingComplementarityVO wuxing = computeWuxing(attrs1, attrs2);
|
|
|
+ result.setWuxing(wuxing);
|
|
|
+
|
|
|
+ // Blood type
|
|
|
+ BloodTypeCompatibilityVO bloodType = computeBloodType(attrs1, attrs2);
|
|
|
+ result.setBloodType(bloodType);
|
|
|
+
|
|
|
+ // Life number
|
|
|
+ LifeNumberRelationshipVO lifeNumber = computeLifeNumber(attrs1, attrs2);
|
|
|
+ result.setLifeNumber(lifeNumber);
|
|
|
+
|
|
|
+ // Overall: zodiac 40% + wuxing 30% + blood 15% + lifeNumber 15%
|
|
|
+ int zodiacScore = zodiac != null ? zodiac.getCompatibilityScore() : 50;
|
|
|
+ int wuxingScore = wuxing != null ? wuxing.getComplementarityScore() : 50;
|
|
|
+ int bloodScore = bloodType != null ? bloodType.getCompatibilityScore() : 50;
|
|
|
+ int lifeScore = lifeNumber != null ? 70 : 50; // life number doesn't have a score, base 70 for having one
|
|
|
+
|
|
|
+ int overall = (zodiacScore * 40 + wuxingScore * 30 + bloodScore * 15 + lifeScore * 15) / 100;
|
|
|
+ result.setOverallScore(overall);
|
|
|
+
|
|
|
+ // Overall advice
|
|
|
+ result.setOverallAdvice(generateOverallAdvice(overall, zodiac, wuxing, bloodType, lifeNumber));
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Family dashboard: all members + aggregated wuxing + pairwise summaries
|
|
|
+ */
|
|
|
+ public FamilyDashboardVO getFamilyDashboard(Long familyId, Long perspectiveMemberId, String perspectiveMemberType) {
|
|
|
+ FamilyDashboardVO dashboard = new FamilyDashboardVO();
|
|
|
+
|
|
|
+ // Get all family members
|
|
|
+ LambdaQueryWrapper<FamilyMember> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ wrapper.eq(FamilyMember::getFamilyId, familyId);
|
|
|
+ List<FamilyMember> members = familyMemberMapper.selectList(wrapper);
|
|
|
+ if (members.isEmpty()) return dashboard;
|
|
|
+
|
|
|
+ Family family = familyMapper.selectById(familyId);
|
|
|
+ dashboard.setFamilyId(familyId);
|
|
|
+ dashboard.setFamilyName(family != null ? family.getName() : null);
|
|
|
+
|
|
|
+ // Build member tradition data
|
|
|
+ List<FamilyMemberTraditionVO> traditionList = new ArrayList<>();
|
|
|
+ for (FamilyMember member : members) {
|
|
|
+ FamilyMemberTraditionVO vo = buildMemberTradition(member);
|
|
|
+ traditionList.add(vo);
|
|
|
+ }
|
|
|
+ dashboard.setMembers(traditionList);
|
|
|
+
|
|
|
+ // Family wuxing aggregation
|
|
|
+ dashboard.setFamilyWuxing(aggregateWuxing(traditionList));
|
|
|
+
|
|
|
+ // Pairwise summaries
|
|
|
+ List<RelationshipSummaryVO> relationships = new ArrayList<>();
|
|
|
+ for (int i = 0; i < traditionList.size(); i++) {
|
|
|
+ for (int j = i + 1; j < traditionList.size(); j++) {
|
|
|
+ FamilyMemberTraditionVO m1 = traditionList.get(i);
|
|
|
+ FamilyMemberTraditionVO m2 = traditionList.get(j);
|
|
|
+ RelationshipSummaryVO summary = computeRelationshipSummary(m1, m2);
|
|
|
+ relationships.add(summary);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ dashboard.setRelationships(relationships);
|
|
|
+
|
|
|
+ // Weekly insight
|
|
|
+ dashboard.setWeeklyInsight(generateWeeklyInsight(traditionList, relationships));
|
|
|
+
|
|
|
+ return dashboard;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== Zodiac compatibility ==========
|
|
|
+
|
|
|
+ private ZodiacCompatibilityVO computeZodiac(FamilyMemberAttributes attrs1, FamilyMemberAttributes attrs2) {
|
|
|
+ if (attrs1 == null || attrs2 == null) return null;
|
|
|
+ String zodiac1 = attrs1.getZodiac();
|
|
|
+ String zodiac2 = attrs2.getZodiac();
|
|
|
+ if (zodiac1 == null || zodiac2 == null) return null;
|
|
|
+
|
|
|
+ LambdaQueryWrapper<ZodiacPairing> qw = new LambdaQueryWrapper<>();
|
|
|
+ // Look up both orderings
|
|
|
+ qw.eq(ZodiacPairing::getZodiacCode1, zodiac1);
|
|
|
+ qw.eq(ZodiacPairing::getZodiacCode2, zodiac2);
|
|
|
+ ZodiacPairing pairing = zodiacPairingMapper.selectOne(qw);
|
|
|
+ if (pairing == null) {
|
|
|
+ // Try reverse order
|
|
|
+ LambdaQueryWrapper<ZodiacPairing> reverseQw = new LambdaQueryWrapper<>();
|
|
|
+ reverseQw.eq(ZodiacPairing::getZodiacCode1, zodiac2);
|
|
|
+ reverseQw.eq(ZodiacPairing::getZodiacCode2, zodiac1);
|
|
|
+ pairing = zodiacPairingMapper.selectOne(reverseQw);
|
|
|
+ }
|
|
|
+
|
|
|
+ ZodiacCompatibilityVO vo = new ZodiacCompatibilityVO();
|
|
|
+ vo.setZodiacCode1(zodiac1);
|
|
|
+ vo.setZodiacCode2(zodiac2);
|
|
|
+ if (pairing != null) {
|
|
|
+ vo.setPairingType(pairing.getPairingType());
|
|
|
+ vo.setCompatibilityScore(pairing.getCompatibilityScore());
|
|
|
+ vo.setDescription(pairing.getDescription());
|
|
|
+ vo.setAdvice(pairing.getAdvice());
|
|
|
+ } else {
|
|
|
+ vo.setPairingType("ping");
|
|
|
+ vo.setCompatibilityScore(50);
|
|
|
+ vo.setDescription("暂无配对数据");
|
|
|
+ vo.setAdvice("互相尊重,用心相处");
|
|
|
+ }
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== Wuxing complementarity ==========
|
|
|
+
|
|
|
+ private WuxingComplementarityVO computeWuxing(FamilyMemberAttributes attrs1, FamilyMemberAttributes attrs2) {
|
|
|
+ if (attrs1 == null || attrs2 == null) return null;
|
|
|
+
|
|
|
+ Map<String, Integer> wx1 = parseWuxingMap(attrs1.getWuxingElements());
|
|
|
+ Map<String, Integer> wx2 = parseWuxingMap(attrs2.getWuxingElements());
|
|
|
+ if (wx1.isEmpty() || wx2.isEmpty()) return null;
|
|
|
+
|
|
|
+ // Compute complementarity: for each element, reward shared strength AND complementary difference
|
|
|
+ int totalComp = 0;
|
|
|
+ StringBuilder analysis = new StringBuilder();
|
|
|
+ List<String> complementaryElements = new ArrayList<>();
|
|
|
+ List<String> sharedElements = new ArrayList<>();
|
|
|
+
|
|
|
+ for (String elem : WUXING_ELEMENTS) {
|
|
|
+ int v1 = wx1.getOrDefault(elem, 0);
|
|
|
+ int v2 = wx2.getOrDefault(elem, 0);
|
|
|
+ int avg = (v1 + v2) / 2;
|
|
|
+ int diff = Math.abs(v1 - v2);
|
|
|
+ // complementarityScore for this element: avg (shared strength) + diff (complementary gap)
|
|
|
+ // Scale: max per element score is ~100 (when avg=50 and diff=50)
|
|
|
+ int elemComp = Math.min(100, avg + diff);
|
|
|
+ totalComp += elemComp;
|
|
|
+
|
|
|
+ if (diff >= 20) {
|
|
|
+ complementaryElements.add(elem);
|
|
|
+ }
|
|
|
+ if (avg >= 30 && diff < 15) {
|
|
|
+ sharedElements.add(elem);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ int avgScore = totalComp / 5;
|
|
|
+
|
|
|
+ // Build analysis text
|
|
|
+ if (!complementaryElements.isEmpty()) {
|
|
|
+ analysis.append("五行互补: ");
|
|
|
+ analysis.append(String.join("、", complementaryElements));
|
|
|
+ analysis.append("元素互补性强,彼此弥补短板。");
|
|
|
+ }
|
|
|
+ if (!sharedElements.isEmpty()) {
|
|
|
+ if (analysis.length() > 0) analysis.append(" ");
|
|
|
+ analysis.append("五行共鸣: ");
|
|
|
+ analysis.append(String.join("、", sharedElements));
|
|
|
+ analysis.append("元素能量相近,容易产生默契。");
|
|
|
+ }
|
|
|
+ if (complementaryElements.isEmpty() && sharedElements.isEmpty()) {
|
|
|
+ analysis.append("五行分布较为独立,各有所长。");
|
|
|
+ }
|
|
|
+
|
|
|
+ WuxingComplementarityVO vo = new WuxingComplementarityVO();
|
|
|
+ vo.setWuxing1(wx1);
|
|
|
+ vo.setWuxing2(wx2);
|
|
|
+ vo.setComplementarityScore(avgScore);
|
|
|
+ vo.setAnalysis(analysis.toString());
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== Blood type compatibility ==========
|
|
|
+
|
|
|
+ private BloodTypeCompatibilityVO computeBloodType(FamilyMemberAttributes attrs1, FamilyMemberAttributes attrs2) {
|
|
|
+ if (attrs1 == null || attrs2 == null) return null;
|
|
|
+ String bt1 = attrs1.getBloodType();
|
|
|
+ String bt2 = attrs2.getBloodType();
|
|
|
+ if (bt1 == null || bt2 == null) return null;
|
|
|
+
|
|
|
+ LambdaQueryWrapper<BloodTypePairing> qw = new LambdaQueryWrapper<>();
|
|
|
+ qw.eq(BloodTypePairing::getBloodType1, bt1);
|
|
|
+ qw.eq(BloodTypePairing::getBloodType2, bt2);
|
|
|
+ BloodTypePairing pairing = bloodTypePairingMapper.selectOne(qw);
|
|
|
+ if (pairing == null) {
|
|
|
+ LambdaQueryWrapper<BloodTypePairing> reverseQw = new LambdaQueryWrapper<>();
|
|
|
+ reverseQw.eq(BloodTypePairing::getBloodType1, bt2);
|
|
|
+ reverseQw.eq(BloodTypePairing::getBloodType2, bt1);
|
|
|
+ pairing = bloodTypePairingMapper.selectOne(reverseQw);
|
|
|
+ }
|
|
|
+
|
|
|
+ BloodTypeCompatibilityVO vo = new BloodTypeCompatibilityVO();
|
|
|
+ vo.setBloodType1(bt1);
|
|
|
+ vo.setBloodType2(bt2);
|
|
|
+ if (pairing != null) {
|
|
|
+ vo.setCompatibilityScore(pairing.getCompatibilityScore());
|
|
|
+ vo.setDescription(pairing.getDescription());
|
|
|
+ vo.setAdvice(pairing.getAdvice());
|
|
|
+ } else {
|
|
|
+ vo.setCompatibilityScore(50);
|
|
|
+ vo.setDescription("暂无配对数据");
|
|
|
+ vo.setAdvice("血型只是参考,用心相处最重要");
|
|
|
+ }
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== Life number relationship ==========
|
|
|
+
|
|
|
+ private LifeNumberRelationshipVO computeLifeNumber(FamilyMemberAttributes attrs1, FamilyMemberAttributes attrs2) {
|
|
|
+ if (attrs1 == null || attrs2 == null) return null;
|
|
|
+
|
|
|
+ Integer lifeNum1 = extractLifeNumber(attrs1.getBehaviorModifier());
|
|
|
+ Integer lifeNum2 = extractLifeNumber(attrs2.getBehaviorModifier());
|
|
|
+ if (lifeNum1 == null || lifeNum2 == null) return null;
|
|
|
+
|
|
|
+ // Relationship number: digital root of sum
|
|
|
+ int sum = lifeNum1 + lifeNum2;
|
|
|
+ int relNum = sum;
|
|
|
+ while (relNum >= 10) {
|
|
|
+ int n = relNum;
|
|
|
+ relNum = 0;
|
|
|
+ while (n > 0) {
|
|
|
+ relNum += n % 10;
|
|
|
+ n /= 10;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ LifeNumberRelationship relationship = lifeNumberRelationshipMapper.selectById(relNum);
|
|
|
+
|
|
|
+ LifeNumberRelationshipVO vo = new LifeNumberRelationshipVO();
|
|
|
+ vo.setLifeNumber1(lifeNum1);
|
|
|
+ vo.setLifeNumber2(lifeNum2);
|
|
|
+ vo.setRelationshipNumber(relNum);
|
|
|
+ if (relationship != null) {
|
|
|
+ vo.setTitle(relationship.getTitle());
|
|
|
+ vo.setDescription(relationship.getDescription());
|
|
|
+ vo.setAdvice(relationship.getAdvice());
|
|
|
+ vo.setColorHex(relationship.getColorHex());
|
|
|
+ }
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== Dashboard helpers ==========
|
|
|
+
|
|
|
+ private FamilyMemberTraditionVO buildMemberTradition(FamilyMember member) {
|
|
|
+ FamilyMemberTraditionVO vo = new FamilyMemberTraditionVO();
|
|
|
+ vo.setMemberId(member.getId());
|
|
|
+ vo.setNickname(member.getNickname());
|
|
|
+ vo.setMemberType("child");
|
|
|
+
|
|
|
+ FamilyMemberAttributes attrs = familyMemberAttributeService.getByMember(member.getId(), "child");
|
|
|
+ if (attrs == null) {
|
|
|
+ attrs = familyMemberAttributeService.getByMember(member.getId(), "parent");
|
|
|
+ if (attrs != null) vo.setMemberType("parent");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (attrs != null) {
|
|
|
+ vo.setZodiac(attrs.getZodiac());
|
|
|
+ vo.setBloodType(attrs.getBloodType());
|
|
|
+ vo.setMindBaseScore(attrs.getMindBaseScore());
|
|
|
+ vo.setWisdomBaseScore(attrs.getWisdomBaseScore());
|
|
|
+ vo.setEightCharacters(parseJsonToMap(attrs.getEightCharacters()));
|
|
|
+ vo.setWuxingElements(parseWuxingMap(attrs.getWuxingElements()));
|
|
|
+ vo.setLifeNumber(extractLifeNumber(attrs.getBehaviorModifier()));
|
|
|
+ }
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ private FamilyWuxingVO aggregateWuxing(List<FamilyMemberTraditionVO> members) {
|
|
|
+ FamilyWuxingVO vo = new FamilyWuxingVO();
|
|
|
+ Map<String, Integer> totals = new LinkedHashMap<>();
|
|
|
+ for (String elem : WUXING_ELEMENTS) {
|
|
|
+ totals.put(elem, 0);
|
|
|
+ }
|
|
|
+ int count = 0;
|
|
|
+ String maxMember = null;
|
|
|
+ int maxBalanceScore = -1;
|
|
|
+
|
|
|
+ for (FamilyMemberTraditionVO m : members) {
|
|
|
+ if (m.getWuxingElements() != null) {
|
|
|
+ count++;
|
|
|
+ // Calculate balance score for this member
|
|
|
+ int minVal = Integer.MAX_VALUE;
|
|
|
+ int maxVal = 0;
|
|
|
+ for (String elem : WUXING_ELEMENTS) {
|
|
|
+ int val = m.getWuxingElements().getOrDefault(elem, 0);
|
|
|
+ totals.put(elem, totals.get(elem) + val);
|
|
|
+ if (val < minVal) minVal = val;
|
|
|
+ if (val > maxVal) maxVal = val;
|
|
|
+ }
|
|
|
+ int balance = 100 - (maxVal - minVal);
|
|
|
+ if (balance > maxBalanceScore) {
|
|
|
+ maxBalanceScore = balance;
|
|
|
+ maxMember = m.getNickname();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Integer> pctMap = new LinkedHashMap<>();
|
|
|
+ String strongest = null;
|
|
|
+ String weakest = null;
|
|
|
+ int maxPct = 0;
|
|
|
+ int minPct = Integer.MAX_VALUE;
|
|
|
+ int totalAll = 0;
|
|
|
+
|
|
|
+ for (String elem : WUXING_ELEMENTS) {
|
|
|
+ int val = count > 0 ? totals.get(elem) * 100 / (count * 100) : 0;
|
|
|
+ pctMap.put(elem, val);
|
|
|
+ totalAll += val;
|
|
|
+ if (val > maxPct) { maxPct = val; strongest = elem; }
|
|
|
+ if (val < minPct) { minPct = val; weakest = elem; }
|
|
|
+ }
|
|
|
+
|
|
|
+ vo.setAggregatePercentages(pctMap);
|
|
|
+ vo.setStrongestElement(strongest);
|
|
|
+ vo.setWeakestElement(weakest);
|
|
|
+ vo.setBalancedMemberName(maxMember);
|
|
|
+
|
|
|
+ StringBuilder analysis = new StringBuilder();
|
|
|
+ if (strongest != null) {
|
|
|
+ analysis.append("你家");
|
|
|
+ analysis.append(elementLabel(strongest));
|
|
|
+ analysis.append("能量最强");
|
|
|
+ }
|
|
|
+ if (weakest != null && minPct < 15) {
|
|
|
+ if (analysis.length() > 0) analysis.append(",");
|
|
|
+ analysis.append(elementLabel(weakest));
|
|
|
+ analysis.append("能量偏弱");
|
|
|
+ }
|
|
|
+ if (maxMember != null) {
|
|
|
+ if (analysis.length() > 0) analysis.append("。");
|
|
|
+ analysis.append(maxMember);
|
|
|
+ analysis.append("的五行最为均衡,是家庭的稳定器。");
|
|
|
+ }
|
|
|
+ if (analysis.length() == 0) {
|
|
|
+ analysis.append("家庭五行分布均衡,整体和谐。");
|
|
|
+ }
|
|
|
+ vo.setAnalysis(analysis.toString());
|
|
|
+
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ private RelationshipSummaryVO computeRelationshipSummary(FamilyMemberTraditionVO m1, FamilyMemberTraditionVO m2) {
|
|
|
+ RelationshipSummaryVO summary = new RelationshipSummaryVO();
|
|
|
+ summary.setMemberId1(m1.getMemberId());
|
|
|
+ summary.setNickname1(m1.getNickname());
|
|
|
+ summary.setMemberId2(m2.getMemberId());
|
|
|
+ summary.setNickname2(m2.getNickname());
|
|
|
+
|
|
|
+ // Quick zodiac lookup
|
|
|
+ int zodiacScore = 50;
|
|
|
+ String pairingType = "ping";
|
|
|
+ if (m1.getZodiac() != null && m2.getZodiac() != null) {
|
|
|
+ LambdaQueryWrapper<ZodiacPairing> qw = new LambdaQueryWrapper<>();
|
|
|
+ qw.eq(ZodiacPairing::getZodiacCode1, m1.getZodiac());
|
|
|
+ qw.eq(ZodiacPairing::getZodiacCode2, m2.getZodiac());
|
|
|
+ ZodiacPairing p = zodiacPairingMapper.selectOne(qw);
|
|
|
+ if (p == null) {
|
|
|
+ LambdaQueryWrapper<ZodiacPairing> rqw = new LambdaQueryWrapper<>();
|
|
|
+ rqw.eq(ZodiacPairing::getZodiacCode1, m2.getZodiac());
|
|
|
+ rqw.eq(ZodiacPairing::getZodiacCode2, m1.getZodiac());
|
|
|
+ p = zodiacPairingMapper.selectOne(rqw);
|
|
|
+ }
|
|
|
+ if (p != null) {
|
|
|
+ zodiacScore = p.getCompatibilityScore();
|
|
|
+ pairingType = p.getPairingType();
|
|
|
+ summary.setSummary(p.getDescription());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ summary.setZodiacScore(zodiacScore);
|
|
|
+ summary.setZodiacPairingType(pairingType);
|
|
|
+
|
|
|
+ // Overall = zodiac + wuxing (if available)
|
|
|
+ int wuxingScore = 50;
|
|
|
+ if (m1.getWuxingElements() != null && m2.getWuxingElements() != null) {
|
|
|
+ int total = 0;
|
|
|
+ for (String elem : WUXING_ELEMENTS) {
|
|
|
+ int v1 = m1.getWuxingElements().getOrDefault(elem, 0);
|
|
|
+ int v2 = m2.getWuxingElements().getOrDefault(elem, 0);
|
|
|
+ total += Math.min(100, (v1 + v2) / 2 + Math.abs(v1 - v2));
|
|
|
+ }
|
|
|
+ wuxingScore = total / 5;
|
|
|
+ }
|
|
|
+ int overall = (zodiacScore * 55 + wuxingScore * 45) / 100;
|
|
|
+ summary.setOverallScore(overall);
|
|
|
+
|
|
|
+ if (summary.getSummary() == null) {
|
|
|
+ if (overall >= 80) {
|
|
|
+ summary.setSummary("非常融洽");
|
|
|
+ } else if (overall >= 60) {
|
|
|
+ summary.setSummary("相处和谐");
|
|
|
+ } else if (overall >= 40) {
|
|
|
+ summary.setSummary("需要磨合");
|
|
|
+ } else {
|
|
|
+ summary.setSummary("需要更多包容");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return summary;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== Insights ==========
|
|
|
+
|
|
|
+ private String generateWeeklyInsight(List<FamilyMemberTraditionVO> members, List<RelationshipSummaryVO> relationships) {
|
|
|
+ List<String> insights = new ArrayList<>();
|
|
|
+
|
|
|
+ // Find the best relationship
|
|
|
+ RelationshipSummaryVO best = null;
|
|
|
+ RelationshipSummaryVO worst = null;
|
|
|
+ for (RelationshipSummaryVO r : relationships) {
|
|
|
+ if (best == null || r.getOverallScore() > best.getOverallScore()) best = r;
|
|
|
+ if (worst == null || r.getOverallScore() < worst.getOverallScore()) worst = r;
|
|
|
+ }
|
|
|
+ if (best != null && best.getOverallScore() >= 80) {
|
|
|
+ insights.add(best.getNickname1() + "和" + best.getNickname2() + "是天生的好搭档,本周可以一起完成一个家庭任务");
|
|
|
+ }
|
|
|
+ if (worst != null && worst.getOverallScore() < 50) {
|
|
|
+ insights.add(worst.getNickname1() + "和" + worst.getNickname2() + "的命理需要磨合,本周建议多安排一些耐心沟通的时间");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (insights.isEmpty()) {
|
|
|
+ insights.add("本周家庭关系和谐,享受彼此的陪伴吧");
|
|
|
+ }
|
|
|
+
|
|
|
+ return String.join(";", insights);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String generateOverallAdvice(int overall, ZodiacCompatibilityVO zodiac,
|
|
|
+ WuxingComplementarityVO wuxing,
|
|
|
+ BloodTypeCompatibilityVO bloodType,
|
|
|
+ LifeNumberRelationshipVO lifeNumber) {
|
|
|
+ List<String> parts = new ArrayList<>();
|
|
|
+ if (overall >= 80) {
|
|
|
+ parts.add("你们是天生的好搭档,命理高度契合。");
|
|
|
+ } else if (overall >= 60) {
|
|
|
+ parts.add("你们的关系有很好的基础,稍微注意互相包容会更完美。");
|
|
|
+ } else if (overall >= 40) {
|
|
|
+ parts.add("你们虽然有差异,但差异正是互补的来源。");
|
|
|
+ } else {
|
|
|
+ parts.add("你们需要更多耐心和理解来经营关系。");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (zodiac != null && zodiac.getAdvice() != null) {
|
|
|
+ parts.add("生肖方面:" + zodiac.getAdvice());
|
|
|
+ }
|
|
|
+ if (bloodType != null && bloodType.getAdvice() != null) {
|
|
|
+ parts.add("血型方面:" + bloodType.getAdvice());
|
|
|
+ }
|
|
|
+ if (lifeNumber != null && lifeNumber.getAdvice() != null) {
|
|
|
+ parts.add("灵数方面:" + lifeNumber.getAdvice());
|
|
|
+ }
|
|
|
+ return String.join("", parts);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== Parsing helpers ==========
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private Map<String, Integer> parseWuxingMap(String json) {
|
|
|
+ Map<String, Integer> result = new LinkedHashMap<>();
|
|
|
+ if (json == null || json.isEmpty()) return result;
|
|
|
+ try {
|
|
|
+ Map<String, Object> raw = familyMemberAttributeService.fromJson(json);
|
|
|
+ for (String elem : WUXING_ELEMENTS) {
|
|
|
+ Object val = raw.get(elem);
|
|
|
+ if (val instanceof Number) {
|
|
|
+ result.put(elem, ((Number) val).intValue());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception ignored) {}
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private Map<String, String> parseJsonToMap(String json) {
|
|
|
+ Map<String, String> result = new LinkedHashMap<>();
|
|
|
+ if (json == null || json.isEmpty()) return result;
|
|
|
+ try {
|
|
|
+ Map<String, Object> raw = familyMemberAttributeService.fromJson(json);
|
|
|
+ for (Map.Entry<String, Object> entry : raw.entrySet()) {
|
|
|
+ result.put(entry.getKey(), String.valueOf(entry.getValue()));
|
|
|
+ }
|
|
|
+ } catch (Exception ignored) {}
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Integer extractLifeNumber(String behaviorModifier) {
|
|
|
+ if (behaviorModifier == null || behaviorModifier.isEmpty()) return null;
|
|
|
+ try {
|
|
|
+ Map<String, Object> map = familyMemberAttributeService.fromJson(behaviorModifier);
|
|
|
+ Object ln = map.get("lifeNumber");
|
|
|
+ if (ln instanceof Number) {
|
|
|
+ return ((Number) ln).intValue();
|
|
|
+ }
|
|
|
+ } catch (Exception ignored) {}
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private FamilyMemberVO buildMemberVO(Long memberId) {
|
|
|
+ FamilyMember member = familyMemberMapper.selectById(memberId);
|
|
|
+ if (member == null) return null;
|
|
|
+ FamilyMemberVO vo = new FamilyMemberVO();
|
|
|
+ vo.setId(member.getId());
|
|
|
+ vo.setNickname(member.getNickname());
|
|
|
+ vo.setMemberType(member.getRelationshipType() != null ? "parent" : "child");
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String elementLabel(String elem) {
|
|
|
+ switch (elem) {
|
|
|
+ case "wood": return "木";
|
|
|
+ case "fire": return "火";
|
|
|
+ case "earth": return "土";
|
|
|
+ case "metal": return "金";
|
|
|
+ case "water": return "水";
|
|
|
+ default: return elem;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|