Browse Source

feat: TianpanService 引入缓存逻辑 - DB持久化每日运势和年度能量

- 新增 TianpanDailyFortune/TianpanMemberAnnualEnergy 实体
- 新增 TianpanDailyFortuneMapper/TianpanMemberAnnualEnergyMapper
- buildDailyFortune: cache-aside 模式,优先读 DB 无则计算写入
- buildAnnualEnergy: cache-aside 模式,按 member+year 缓存年度能量
- schema.sql 同步两表 DDL
Xiaogang Liao 2 months ago
parent
commit
81a367bd41

+ 39 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/TianpanMemberAnnualEnergy.java

@@ -0,0 +1,39 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("tianpan_member_annual_energy")
+public class TianpanMemberAnnualEnergy {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long memberId;
+
+    private Integer year;
+
+    private Integer dimensionsBody;
+
+    private Integer dimensionsMind;
+
+    private Integer dimensionsWisdom;
+
+    private Integer dimensionsAction;
+
+    private Integer dimensionsWealth;
+
+    private String jupiterPhase;
+
+    private String saturnPhase;
+
+    private String highlights;
+
+    private LocalDateTime createdAt;
+
+    private LocalDateTime updatedAt;
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/TianpanDailyFortuneMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.TianpanDailyFortune;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface TianpanDailyFortuneMapper extends BaseMapper<TianpanDailyFortune> {
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/TianpanMemberAnnualEnergyMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.TianpanMemberAnnualEnergy;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface TianpanMemberAnnualEnergyMapper extends BaseMapper<TianpanMemberAnnualEnergy> {
+}

+ 164 - 40
cfc-backend/src/main/java/com/etotem/cfc/service/TianpanService.java

@@ -5,7 +5,10 @@ import com.etotem.cfc.dto.*;
 import com.etotem.cfc.entity.*;
 import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.FamilyMapper;
+import com.etotem.cfc.mapper.TianpanDailyFortuneMapper;
+import com.etotem.cfc.mapper.TianpanMemberAnnualEnergyMapper;
 import com.etotem.cfc.mapper.ZodiacPairingMapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
@@ -13,6 +16,8 @@ import org.springframework.stereotype.Service;
 import javax.annotation.Resource;
 import java.util.*;
 import java.util.stream.Collectors;
+import java.time.LocalDate;
+import java.util.stream.Collectors;
 
 /**
  * 家庭天盘服务
@@ -57,6 +62,14 @@ public class TianpanService {
     @Resource
     private ZodiacPairingMapper zodiacPairingMapper;
 
+    @Resource
+    private TianpanDailyFortuneMapper tianpanDailyFortuneMapper;
+
+    @Resource
+    private TianpanMemberAnnualEnergyMapper tianpanMemberAnnualEnergyMapper;
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
     /**
      * 构建家庭天盘仪表盘
      * 
@@ -306,12 +319,25 @@ public class TianpanService {
 
     /**
      * 构建年度能量
-     * 
-     * @param memberId 成员 ID
-     * @param year 目标年份
-     * @return 年度能量
      */
     public MemberAnnualEnergyVO buildAnnualEnergy(Long memberId, Integer year) {
+        LambdaQueryWrapper<TianpanMemberAnnualEnergy> qw = new LambdaQueryWrapper<>();
+        qw.eq(TianpanMemberAnnualEnergy::getMemberId, memberId)
+          .eq(TianpanMemberAnnualEnergy::getYear, year);
+        TianpanMemberAnnualEnergy cached = tianpanMemberAnnualEnergyMapper.selectOne(qw);
+        if (cached != null) {
+            return toMemberAnnualEnergyVO(cached);
+        }
+
+        MemberAnnualEnergyVO vo = computeAnnualEnergy(memberId, year);
+
+        TianpanMemberAnnualEnergy entity = toMemberAnnualEnergyEntity(vo, memberId, year);
+        tianpanMemberAnnualEnergyMapper.insert(entity);
+
+        return vo;
+    }
+
+    private MemberAnnualEnergyVO computeAnnualEnergy(Long memberId, Integer year) {
         FamilyMember member = familyMemberMapper.selectById(memberId);
         if (member == null) {
             return null;
@@ -325,7 +351,6 @@ public class TianpanService {
             return null;
         }
 
-        // 获取出生信息
         Integer birthYear = null;
         if (attrs.getBirthDatetime() != null) {
             Calendar cal = Calendar.getInstance();
@@ -338,8 +363,7 @@ public class TianpanService {
             return null;
         }
 
-        // 计算年度能量
-        ZodiacAnnualEnergyService.AnnualEnergyResult result = 
+        ZodiacAnnualEnergyService.AnnualEnergyResult result =
             zodiacAnnualEnergyService.calculate(birthYear, zodiacCode, year);
 
         MemberAnnualEnergyVO vo = new MemberAnnualEnergyVO();
@@ -350,66 +374,114 @@ public class TianpanService {
         vo.setJupiterPhase(result.getJupiterPhase());
         vo.setSaturnPhase(result.getSaturnPhase());
 
-        // 生成亮点
         List<String> highlights = new ArrayList<>();
         Map<String, Integer> dims = result.toMap();
-        if (dims.get("body") >= 70) {
-            highlights.add("身体健康能量充沛");
-        }
-        if (dims.get("mind") >= 70) {
-            highlights.add("心理能量饱满");
-        }
-        if (dims.get("wisdom") >= 70) {
-            highlights.add("智慧能量开启");
-        }
-        if (dims.get("action") >= 70) {
-            highlights.add("行动力强劲");
-        }
-        if (dims.get("wealth") >= 70) {
-            highlights.add("财富能量旺盛");
-        }
-        if ("benming".equals(result.getYearRelation())) {
-            highlights.add("本命年,稳中求进");
+        if (dims.get("body") >= 70) { highlights.add("身体健康能量充沛"); }
+        if (dims.get("mind") >= 70) { highlights.add("心理能量饱满"); }
+        if (dims.get("wisdom") >= 70) { highlights.add("智慧能量开启"); }
+        if (dims.get("action") >= 70) { highlights.add("行动力强劲"); }
+        if (dims.get("wealth") >= 70) { highlights.add("财富能量旺盛"); }
+        if ("benming".equals(result.getYearRelation())) { highlights.add("本命年,稳中求进"); }
+        if ("liuhe".equals(result.getYearRelation())) { highlights.add("六合吉年,贵人相助"); }
+        if ("chong".equals(result.getYearRelation())) { highlights.add("冲太岁,注意调整"); }
+        vo.setHighlights(highlights);
+
+        return vo;
+    }
+
+    private MemberAnnualEnergyVO toMemberAnnualEnergyVO(TianpanMemberAnnualEnergy e) {
+        MemberAnnualEnergyVO vo = new MemberAnnualEnergyVO();
+        vo.setMemberId(e.getMemberId());
+        vo.setYear(e.getYear());
+
+        FamilyMember member = familyMemberMapper.selectById(e.getMemberId());
+        if (member != null) {
+            vo.setNickname(member.getNickname());
         }
-        if ("liuhe".equals(result.getYearRelation())) {
-            highlights.add("六合吉年,贵人相助");
+
+        Map<String, Integer> dims = new LinkedHashMap<>();
+        dims.put("body", e.getDimensionsBody());
+        dims.put("mind", e.getDimensionsMind());
+        dims.put("wisdom", e.getDimensionsWisdom());
+        dims.put("action", e.getDimensionsAction());
+        dims.put("wealth", e.getDimensionsWealth());
+        vo.setDimensions(dims);
+
+        vo.setJupiterPhase(e.getJupiterPhase());
+        vo.setSaturnPhase(e.getSaturnPhase());
+
+        if (e.getHighlights() != null) {
+            try { vo.setHighlights(objectMapper.readValue(e.getHighlights(), List.class)); } catch (Exception ex) { /* ignore */ }
         }
-        if ("chong".equals(result.getYearRelation())) {
-            highlights.add("冲太岁,注意调整");
+        return vo;
+    }
+
+    private TianpanMemberAnnualEnergy toMemberAnnualEnergyEntity(MemberAnnualEnergyVO vo, Long memberId, Integer year) {
+        TianpanMemberAnnualEnergy e = new TianpanMemberAnnualEnergy();
+        e.setMemberId(memberId);
+        e.setYear(year);
+
+        Map<String, Integer> dims = vo.getDimensions();
+        if (dims != null) {
+            e.setDimensionsBody(dims.get("body") != null ? dims.get("body") : 0);
+            e.setDimensionsMind(dims.get("mind") != null ? dims.get("mind") : 0);
+            e.setDimensionsWisdom(dims.get("wisdom") != null ? dims.get("wisdom") : 0);
+            e.setDimensionsAction(dims.get("action") != null ? dims.get("action") : 0);
+            e.setDimensionsWealth(dims.get("wealth") != null ? dims.get("wealth") : 0);
+        } else {
+            e.setDimensionsBody(0);
+            e.setDimensionsMind(0);
+            e.setDimensionsWisdom(0);
+            e.setDimensionsAction(0);
+            e.setDimensionsWealth(0);
         }
-        vo.setHighlights(highlights);
 
-        return vo;
+        e.setJupiterPhase(vo.getJupiterPhase());
+        e.setSaturnPhase(vo.getSaturnPhase());
+
+        try { e.setHighlights(objectMapper.writeValueAsString(vo.getHighlights())); } catch (Exception ex) { /* ignore */ }
+
+        return e;
     }
 
     /**
      * 构建每日运势
-     * 
-     * @param familyId 家庭 ID
-     * @return 每日运势
      */
     public DailyFortuneVO buildDailyFortune(Long familyId) {
-        DailyFortuneVO vo = new DailyFortuneVO();
+        String today = formatDate(new Date());
+
+        LambdaQueryWrapper<TianpanDailyFortune> qw = new LambdaQueryWrapper<>();
+        qw.eq(TianpanDailyFortune::getFamilyId, familyId)
+          .eq(TianpanDailyFortune::getDate, today);
+        TianpanDailyFortune cached = tianpanDailyFortuneMapper.selectOne(qw);
+        if (cached != null) {
+            return toDailyFortuneVO(cached);
+        }
+
+        DailyFortuneVO vo = computeDailyFortune(familyId);
+
+        TianpanDailyFortune entity = toDailyFortuneEntity(vo, familyId, today);
+        tianpanDailyFortuneMapper.insert(entity);
+
+        return vo;
+    }
 
+    private DailyFortuneVO computeDailyFortune(Long familyId) {
+        DailyFortuneVO vo = new DailyFortuneVO();
         Calendar cal = Calendar.getInstance();
         vo.setDate(formatDate(cal.getTime()));
         vo.setLunarDate("农历" + getLunarDate(cal.getTime()));
         vo.setDayGanzhi(getDayGanzhi(cal.getTime()));
 
-        // 五行
         DailyFortuneVO.FiveElementsVO fiveElements = new DailyFortuneVO.FiveElementsVO();
         fiveElements.setMain(getDayElement(cal.getTime()));
         fiveElements.setDetail(getDayElementDetail(cal.getTime()));
         vo.setFiveElements(fiveElements);
 
-        // 吉凶
         vo.setAuspicious(Arrays.asList("宜祭祀", "宜祈福", "宜聚会"));
         vo.setInauspicious(Arrays.asList("忌远行", "忌动土"));
-
-        // 家庭建议
         vo.setFamilyAdvice("今日家庭能量和谐,适合安排家庭活动");
 
-        // 焦点成员提示
         DailyFortuneVO.FocusMemberTip focus = new DailyFortuneVO.FocusMemberTip();
         focus.setName("家庭中最活跃的成员");
         focus.setTip("多倾听他的想法");
@@ -419,6 +491,58 @@ public class TianpanService {
         return vo;
     }
 
+    private DailyFortuneVO toDailyFortuneVO(TianpanDailyFortune e) {
+        DailyFortuneVO vo = new DailyFortuneVO();
+        vo.setDate(e.getDate());
+        vo.setLunarDate(e.getLunarDate());
+        vo.setDayGanzhi(e.getDayGanzhi());
+
+        if (e.getFiveElementsMain() != null) {
+            DailyFortuneVO.FiveElementsVO fe = new DailyFortuneVO.FiveElementsVO();
+            fe.setMain(e.getFiveElementsMain());
+            fe.setDetail(e.getFiveElementsDetail());
+            vo.setFiveElements(fe);
+        }
+
+        if (e.getAuspicious() != null) {
+            try { vo.setAuspicious(objectMapper.readValue(e.getAuspicious(), List.class)); } catch (Exception ex) { /* ignore */ }
+        }
+        if (e.getInauspicious() != null) {
+            try { vo.setInauspicious(objectMapper.readValue(e.getInauspicious(), List.class)); } catch (Exception ex) { /* ignore */ }
+        }
+        vo.setFamilyAdvice(e.getFamilyAdvice());
+
+        if (e.getFocusMemberName() != null) {
+            DailyFortuneVO.FocusMemberTip focus = new DailyFortuneVO.FocusMemberTip();
+            focus.setName(e.getFocusMemberName());
+            focus.setTip(e.getFocusMemberTip());
+            focus.setWuxingTip(e.getWuxingTip());
+            vo.setFocusMember(focus);
+        }
+        return vo;
+    }
+
+    private TianpanDailyFortune toDailyFortuneEntity(DailyFortuneVO vo, Long familyId, String today) {
+        TianpanDailyFortune e = new TianpanDailyFortune();
+        e.setFamilyId(familyId);
+        e.setDate(today);
+        e.setLunarDate(vo.getLunarDate());
+        e.setDayGanzhi(vo.getDayGanzhi());
+        if (vo.getFiveElements() != null) {
+            e.setFiveElementsMain(vo.getFiveElements().getMain());
+            e.setFiveElementsDetail(vo.getFiveElements().getDetail());
+        }
+        try { e.setAuspicious(objectMapper.writeValueAsString(vo.getAuspicious())); } catch (Exception ex) { /* ignore */ }
+        try { e.setInauspicious(objectMapper.writeValueAsString(vo.getInauspicious())); } catch (Exception ex) { /* ignore */ }
+        e.setFamilyAdvice(vo.getFamilyAdvice());
+        if (vo.getFocusMember() != null) {
+            e.setFocusMemberName(vo.getFocusMember().getName());
+            e.setFocusMemberTip(vo.getFocusMember().getTip());
+            e.setWuxingTip(vo.getFocusMember().getWuxingTip());
+        }
+        return e;
+    }
+
     /**
      * 计算兼容性(Controller 调用入口)
      */

+ 44 - 0
cfc-backend/src/main/resources/schema.sql

@@ -2624,3 +2624,47 @@ CREATE TABLE IF NOT EXISTS pending_refund (
     INDEX idx_next_retry (next_retry_time),
     INDEX idx_withdrawal_request (withdrawal_request_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='待退款记录表';
+
+
+-- 家庭每日运势表(迁移 79)
+CREATE TABLE IF NOT EXISTS tianpan_daily_fortune (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    family_id BIGINT NOT NULL COMMENT '家庭ID',
+    date VARCHAR(20) NOT NULL COMMENT '日期 YYYY-MM-DD',
+    lunar_date VARCHAR(50) COMMENT '农历日期',
+    day_ganzhi VARCHAR(10) COMMENT '日柱',
+    five_elements_main VARCHAR(20) COMMENT '当日五行主气',
+    five_elements_detail VARCHAR(100) COMMENT '五行详情',
+    auspicious TEXT COMMENT '宜做的事(JSON数组)',
+    inauspicious TEXT COMMENT '忌做的事(JSON数组)',
+    family_advice TEXT COMMENT '家庭建议',
+    focus_member_name VARCHAR(50) COMMENT '关注成员',
+    focus_member_tip TEXT COMMENT '成员提示',
+    wuxing_tip TEXT COMMENT '五行提示',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_family_date (family_id, date),
+    INDEX idx_family_id (family_id),
+    INDEX idx_date (date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭每日运势表';
+
+
+-- 成员年度能量表(迁移 80)
+CREATE TABLE IF NOT EXISTS tianpan_member_annual_energy (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    member_id BIGINT NOT NULL COMMENT '成员ID',
+    year INT NOT NULL COMMENT '年份',
+    dimensions_body INT DEFAULT 0 COMMENT '身能量',
+    dimensions_mind INT DEFAULT 0 COMMENT '心能量',
+    dimensions_wisdom INT DEFAULT 0 COMMENT '智能量',
+    dimensions_action INT DEFAULT 0 COMMENT '行能量',
+    dimensions_wealth INT DEFAULT 0 COMMENT '富能量',
+    jupiter_phase VARCHAR(50) COMMENT '木星相位',
+    saturn_phase VARCHAR(50) COMMENT '土星相位',
+    highlights TEXT COMMENT '年度亮点(JSON数组)',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_member_year (member_id, year),
+    INDEX idx_member_id (member_id),
+    INDEX idx_year (year)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成员年度能量表';