Przeglądaj źródła

feat(mind): CompatibilityService算法引擎+9个DTO定义+FamilyMemberAttributeService fromJson/toJson公开

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Xiaogang Liao 2 miesięcy temu
rodzic
commit
76464418af

+ 12 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/BloodTypeCompatibilityVO.java

@@ -0,0 +1,12 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class BloodTypeCompatibilityVO {
+    private String bloodType1;
+    private String bloodType2;
+    private Integer compatibilityScore;
+    private String description;
+    private String advice;
+}

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/CompatibilityResultVO.java

@@ -0,0 +1,15 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class CompatibilityResultVO {
+    private FamilyMemberVO member1;
+    private FamilyMemberVO member2;
+    private ZodiacCompatibilityVO zodiac;
+    private WuxingComplementarityVO wuxing;
+    private BloodTypeCompatibilityVO bloodType;
+    private LifeNumberRelationshipVO lifeNumber;
+    private Integer overallScore;
+    private String overallAdvice;
+}

+ 14 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyDashboardVO.java

@@ -0,0 +1,14 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+import java.util.List;
+
+@Data
+public class FamilyDashboardVO {
+    private Long familyId;
+    private String familyName;
+    private List<FamilyMemberTraditionVO> members;
+    private FamilyWuxingVO familyWuxing;
+    private List<RelationshipSummaryVO> relationships;
+    private String weeklyInsight;
+}

+ 18 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberTraditionVO.java

@@ -0,0 +1,18 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+import java.util.Map;
+
+@Data
+public class FamilyMemberTraditionVO {
+    private Long memberId;
+    private String nickname;
+    private String memberType;
+    private String zodiac;
+    private String bloodType;
+    private Integer lifeNumber;
+    private Map<String, String> eightCharacters;
+    private Map<String, Integer> wuxingElements;
+    private Integer mindBaseScore;
+    private Integer wisdomBaseScore;
+}

+ 14 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyWuxingVO.java

@@ -0,0 +1,14 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class FamilyWuxingVO {
+    private Map<String, Integer> aggregatePercentages;
+    private String strongestElement;
+    private String weakestElement;
+    private String analysis;
+    private String balancedMemberName;
+}

+ 14 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/LifeNumberRelationshipVO.java

@@ -0,0 +1,14 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class LifeNumberRelationshipVO {
+    private Integer lifeNumber1;
+    private Integer lifeNumber2;
+    private Integer relationshipNumber;
+    private String title;
+    private String description;
+    private String advice;
+    private String colorHex;
+}

+ 16 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/RelationshipSummaryVO.java

@@ -0,0 +1,16 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class RelationshipSummaryVO {
+    private Long memberId1;
+    private String nickname1;
+    private Long memberId2;
+    private String nickname2;
+    private Integer overallScore;
+    private String pairingType;
+    private String summary;
+    private String zodiacPairingType;
+    private Integer zodiacScore;
+}

+ 12 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/WuxingComplementarityVO.java

@@ -0,0 +1,12 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+import java.util.Map;
+
+@Data
+public class WuxingComplementarityVO {
+    private Map<String, Integer> wuxing1;
+    private Map<String, Integer> wuxing2;
+    private Integer complementarityScore;
+    private String analysis;
+}

+ 13 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/ZodiacCompatibilityVO.java

@@ -0,0 +1,13 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class ZodiacCompatibilityVO {
+    private String zodiacCode1;
+    private String zodiacCode2;
+    private String pairingType;
+    private Integer compatibilityScore;
+    private String description;
+    private String advice;
+}

+ 578 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/CompatibilityService.java

@@ -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;
+        }
+    }
+}

+ 3 - 3
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberAttributeService.java

@@ -412,7 +412,7 @@ public class FamilyMemberAttributeService extends ServiceImpl<FamilyMemberAttrib
     /**
      * Simple JSON serializer for Map
      */
-    private String toJson(Map<String, ?> map) {
+    public String toJson(Map<String, ?> map) {
         StringBuilder sb = new StringBuilder("{");
         boolean first = true;
         for (Map.Entry<String, ?> entry : map.entrySet()) {
@@ -434,7 +434,7 @@ public class FamilyMemberAttributeService extends ServiceImpl<FamilyMemberAttrib
      * Simple JSON parser for Map<String, Object>
      */
     @SuppressWarnings("unchecked")
-    private Map<String, Object> fromJson(String json) {
+    public Map<String, Object> fromJson(String json) {
         Map<String, Object> map = new LinkedHashMap<>();
         if (json == null || json.isEmpty() || !json.startsWith("{")) {
             return map;
@@ -466,7 +466,7 @@ public class FamilyMemberAttributeService extends ServiceImpl<FamilyMemberAttrib
     /**
      * Simple JSON parser for Map<String, String>
      */
-    private Map<String, String> fromJson(String json, @SuppressWarnings("unused") boolean stringValues) {
+    public Map<String, String> fromJson(String json, @SuppressWarnings("unused") boolean stringValues) {
         Map<String, String> map = new LinkedHashMap<>();
         if (json == null || json.isEmpty() || !json.startsWith("{")) {
             return map;