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