Explorar el Código

feat: InnatePortraitService 先天画像聚合服务 + AiGateway generateInnateReading stub

Xiaogang Liao hace 2 semanas
padre
commit
833f050537

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java

@@ -226,6 +226,13 @@ public class AiGateway {
         }
     }
 
+    /**
+     * 先天画像 AI 解读(stub,任务8完善为真实HTTP调用)
+     */
+    public Map<String, Object> generateInnateReading(Object portrait) {
+        return null;
+    }
+
     private List<Map<String, Object>> parseTasks(JsonNode tasksNode) {
         List<Map<String, Object>> tasks = new ArrayList<>();
         if (tasksNode != null && tasksNode.isArray()) {

+ 290 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/InnatePortraitService.java

@@ -0,0 +1,290 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.dto.InnatePortraitReportVO;
+import com.etotem.cfc.dto.InnatePortraitVO;
+import com.etotem.cfc.dto.InnateTrajectoryVO;
+import com.etotem.cfc.dto.NumSoulDetailVO;
+import com.etotem.cfc.entity.*;
+import com.etotem.cfc.mapper.*;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+@Service
+public class InnatePortraitService {
+
+    private static final Logger log = LoggerFactory.getLogger(InnatePortraitService.class);
+
+    @Resource
+    private FamilyMemberAttributeService familyMemberAttributeService;
+
+    @Resource
+    private InnatePortraitConfigMapper innatePortraitConfigMapper;
+
+    @Resource
+    private InnatePortraitReportMapper innatePortraitReportMapper;
+
+    @Resource
+    private NumSoulDetailConfigMapper numSoulDetailConfigMapper;
+
+    @Resource
+    private AiGateway aiGateway;
+
+    @Resource
+    private EnergyBalanceMapper energyBalanceMapper;
+
+    @Resource
+    private EnergyDimensionMapper energyDimensionMapper;
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    public InnatePortraitVO getInnatePortrait(Long memberId, String memberType) {
+        FamilyMemberAttributes attrs = familyMemberAttributeService.getByMember(memberId, memberType);
+        if (attrs == null) {
+            return null;
+        }
+
+        InnatePortraitVO vo = new InnatePortraitVO();
+        vo.setMemberId(memberId);
+        vo.setMemberType(memberType);
+        vo.setEightCharacters(parseJsonString(attrs.getEightCharacters()));
+        vo.setWuxingElements(parseJsonInt(attrs.getWuxingElements()));
+        vo.setZodiac(attrs.getZodiac());
+        vo.setBloodType(attrs.getBloodType());
+        vo.setMindBaseScore(familyMemberAttributeService.calcInnateScore(memberId, memberType, "mind"));
+        vo.setWisdomBaseScore(familyMemberAttributeService.calcInnateScore(memberId, memberType, "wisdom"));
+
+        if (attrs.getBirthDatetime() != null) {
+            Calendar cal = Calendar.getInstance();
+            cal.setTime(attrs.getBirthDatetime());
+            vo.setNumSoul(calculateNumSoul(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)));
+        }
+
+        vo.setSourceContributions(listSourceContributions());
+        return vo;
+    }
+
+    public NumSoulDetailVO calculateNumSoul(int year, int month, int day) {
+        NumSoulDetailVO vo = new NumSoulDetailVO();
+        int lifePath = NumSoulCalculator.calcLifePath(year, month, day);
+        int[] talent = NumSoulCalculator.calcTalent(year, month, day);
+        int birthday = NumSoulCalculator.calcBirthday(day);
+        int destiny = NumSoulCalculator.calcDestiny(year, month, day);
+
+        vo.setLifePath(lifePath);
+        vo.setTalent1(talent[0]);
+        vo.setTalent2(talent[1]);
+        vo.setBirthday(birthday);
+        vo.setDestiny(destiny);
+
+        NumSoulDetailConfig lifeCfg = getNumSoulConfig("life_path", lifePath);
+        if (lifeCfg != null) {
+            vo.setLifePathTitle(lifeCfg.getTitle());
+            vo.setLifePathKeywords(lifeCfg.getKeywords());
+            vo.setLifePathAdvice(lifeCfg.getMindAdvice());
+            vo.setColorHex(lifeCfg.getColorHex());
+        }
+        NumSoulDetailConfig talentCfg = getNumSoulConfig("talent", talent[0]);
+        if (talentCfg != null) {
+            vo.setTalentTitle(talentCfg.getTitle());
+        }
+        NumSoulDetailConfig birthdayCfg = getNumSoulConfig("birthday", birthday);
+        if (birthdayCfg != null) {
+            vo.setBirthdayTitle(birthdayCfg.getTitle());
+        }
+        NumSoulDetailConfig destinyCfg = getNumSoulConfig("destiny", destiny);
+        if (destinyCfg != null) {
+            vo.setDestinyTitle(destinyCfg.getTitle());
+        }
+        return vo;
+    }
+
+    private NumSoulDetailConfig getNumSoulConfig(String type, int value) {
+        LambdaQueryWrapper<NumSoulDetailConfig> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(NumSoulDetailConfig::getNumberType, type)
+               .eq(NumSoulDetailConfig::getNumberValue, value);
+        return numSoulDetailConfigMapper.selectOne(wrapper);
+    }
+
+    private List<Map<String, Object>> listSourceContributions() {
+        List<Map<String, Object>> list = new ArrayList<>();
+        List<InnatePortraitConfig> configs = innatePortraitConfigMapper.selectList(
+                new LambdaQueryWrapper<InnatePortraitConfig>().orderByAsc(InnatePortraitConfig::getSortOrder));
+        for (InnatePortraitConfig cfg : configs) {
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("sourceType", cfg.getSourceType());
+            item.put("sourceName", cfg.getSourceName());
+            item.put("weight", cfg.getWeight());
+            item.put("enabled", cfg.getEnabled());
+            list.add(item);
+        }
+        return list;
+    }
+
+    public InnatePortraitReportVO generateAiReading(Long memberId, String memberType, Long familyId) {
+        InnatePortraitReport report = innatePortraitReportMapper.selectOne(
+                new LambdaQueryWrapper<InnatePortraitReport>()
+                        .eq(InnatePortraitReport::getMemberId, memberId)
+                        .eq(InnatePortraitReport::getMemberType, memberType));
+        if (report != null && report.getAiReading() != null && !report.getAiReading().isEmpty()) {
+            InnatePortraitReportVO cached = new InnatePortraitReportVO();
+            cached.setPortrait(getInnatePortrait(memberId, memberType));
+            cached.setAiReading(report.getAiReading());
+            cached.setGeneratedAt(String.valueOf(report.getGeneratedAt()));
+            cached.setFromCache(true);
+            cached.setFallbackUsed(false);
+            return cached;
+        }
+
+        InnatePortraitVO portrait = getInnatePortrait(memberId, memberType);
+        if (portrait == null) {
+            return null;
+        }
+
+        String aiReading = null;
+        try {
+            Map<String, Object> result = aiGateway.generateInnateReading(portrait);
+            if (result != null && result.get("reading") != null) {
+                aiReading = (String) result.get("reading");
+            }
+        } catch (Exception e) {
+            log.warn("AI解读生成失败,降级模板: {}", e.getMessage());
+        }
+
+        boolean fallbackUsed = false;
+        if (aiReading == null || aiReading.isEmpty()) {
+            aiReading = buildFallbackReading(portrait);
+            fallbackUsed = true;
+        }
+
+        InnatePortraitReport newReport = new InnatePortraitReport();
+        newReport.setMemberId(memberId);
+        newReport.setMemberType(memberType);
+        newReport.setFamilyId(familyId);
+        newReport.setPortraitJson(safeToJson(portrait));
+        newReport.setAiReading(aiReading);
+        newReport.setGeneratedAt(new Date());
+        if (report != null) {
+            newReport.setId(report.getId());
+            innatePortraitReportMapper.updateById(newReport);
+        } else {
+            innatePortraitReportMapper.insert(newReport);
+        }
+
+        InnatePortraitReportVO vo = new InnatePortraitReportVO();
+        vo.setPortrait(portrait);
+        vo.setAiReading(aiReading);
+        vo.setGeneratedAt(String.valueOf(newReport.getGeneratedAt()));
+        vo.setFromCache(false);
+        vo.setFallbackUsed(fallbackUsed);
+        return vo;
+    }
+
+    private String buildFallbackReading(InnatePortraitVO portrait) {
+        StringBuilder sb = new StringBuilder();
+        if (portrait.getZodiac() != null) {
+            sb.append("生肖").append(portrait.getZodiac()).append(",");
+        }
+        if (portrait.getNumSoul() != null && portrait.getNumSoul().getLifePath() != null) {
+            sb.append("生命灵数").append(portrait.getNumSoul().getLifePath());
+            if (portrait.getNumSoul().getLifePathTitle() != null) {
+                sb.append("(").append(portrait.getNumSoul().getLifePathTitle()).append(")");
+            }
+            sb.append(",");
+        }
+        sb.append("心先天基础分 ").append(portrait.getMindBaseScore()).append("。");
+        sb.append("建议通过情绪打卡、家庭互动与阅读持续滋养心能量。");
+        return sb.toString();
+    }
+
+    public InnateTrajectoryVO getTrajectory(Long memberId, String memberType, Long familyId) {
+        InnateTrajectoryVO vo = new InnateTrajectoryVO();
+        InnatePortraitVO portrait = getInnatePortrait(memberId, memberType);
+        if (portrait == null) {
+            return null;
+        }
+        vo.setInnatePortrait(portrait);
+
+        Map<String, Integer> innateScores = new LinkedHashMap<>();
+        innateScores.put("mind", portrait.getMindBaseScore());
+        innateScores.put("wisdom", portrait.getWisdomBaseScore());
+        vo.setInnateDimensionScores(innateScores);
+
+        Map<String, Object> currentEnergy = new LinkedHashMap<>();
+        Map<String, Object> innateVsCurrent = new LinkedHashMap<>();
+        List<EnergyDimension> allDims = energyDimensionMapper.selectList(null);
+        Integer mindCurrent = null;
+        for (EnergyDimension dim : allDims) {
+            if (dim.getStatus() != null && dim.getStatus() != 1) continue;
+            EnergyBalance balance = energyBalanceMapper.selectOne(
+                    new LambdaQueryWrapper<EnergyBalance>()
+                            .eq(EnergyBalance::getChildId, memberId)
+                            .eq(EnergyBalance::getDimensionId, dim.getId()));
+            int bal = balance != null && balance.getBalance() != null ? balance.getBalance() : 0;
+            currentEnergy.put(dim.getCode() + "Balance", bal);
+            if (balance != null && balance.getTotalEarned() != null) {
+                currentEnergy.put(dim.getCode() + "TotalEarned", balance.getTotalEarned());
+            }
+            if ("mind".equals(dim.getCode())) {
+                mindCurrent = bal;
+            }
+        }
+        vo.setCurrentEnergy(currentEnergy);
+
+        int mindInnate = portrait.getMindBaseScore() != null ? portrait.getMindBaseScore() : 0;
+        int mindCur = mindCurrent != null ? mindCurrent : 0;
+        Map<String, Object> vs = new LinkedHashMap<>();
+        vs.put("mindInnate", mindInnate);
+        vs.put("mindCurrent", mindCur);
+        vs.put("growth", mindCur >= mindInnate ? "+" + (mindCur - mindInnate) : String.valueOf(mindCur - mindInnate));
+        vo.setInnateVsCurrent(vs);
+
+        if (mindCur >= mindInnate) {
+            vo.setGrowthTrend("先天心能量基础分 " + mindInnate + ",后天通过情绪打卡/家庭互动成长至 " + mindCur + ",持续向好。");
+        } else {
+            vo.setGrowthTrend("先天心能量基础分 " + mindInnate + ",当前能量 " + mindCur + ",建议增加情绪打卡与家庭共处时间。");
+        }
+        vo.setAdvice("保持情绪打卡频率,增加家庭共处时间,通过阅读与亲子对话持续滋养心能量。");
+        return vo;
+    }
+
+    private Map<String, String> parseJsonString(String json) {
+        Map<String, Object> raw = parseJson(json);
+        Map<String, String> result = new LinkedHashMap<>();
+        for (Map.Entry<String, Object> e : raw.entrySet()) {
+            result.put(e.getKey(), String.valueOf(e.getValue()));
+        }
+        return result;
+    }
+
+    private Map<String, Object> parseJson(String json) {
+        if (json == null || json.isEmpty()) return new HashMap<>();
+        try {
+            return objectMapper.readValue(json, Map.class);
+        } catch (Exception e) {
+            return new HashMap<>();
+        }
+    }
+
+    private Map<String, Integer> parseJsonInt(String json) {
+        Map<String, Object> raw = parseJson(json);
+        Map<String, Integer> result = new LinkedHashMap<>();
+        for (Map.Entry<String, Object> e : raw.entrySet()) {
+            result.put(e.getKey(), e.getValue() instanceof Number ? ((Number) e.getValue()).intValue() : 0);
+        }
+        return result;
+    }
+
+    private String safeToJson(Object obj) {
+        try {
+            return objectMapper.writeValueAsString(obj);
+        } catch (Exception e) {
+            return "{}";
+        }
+    }
+}