Преглед изворни кода

feat(self-check): WuxingSourcingAdviceVO 改造为 AI 建议 VO + SelfCheckAnalysisService

iwt пре 2 недеља
родитељ
комит
a9a8b2eb6a

+ 13 - 45
cfc-backend/src/main/java/com/etotem/cfc/dto/WuxingSourcingAdviceVO.java

@@ -1,65 +1,33 @@
 package com.etotem.cfc.dto;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.Data;
+import java.util.List;
 
-/**
- * 五行相生寻源建议(P0-2 五行相生寻源建议引擎输出)
- * 对应书稿《附录-工具包汇总》附录六五行相生速查表
- */
 @Data
 public class WuxingSourcingAdviceVO {
 
-    /** 低分维度 code(body/wisdom/wealth/action/mind) */
     private String dimension;
-
-    /** 低分维度中文名(身/智/富/行/心) */
     private String dimensionName;
-
-    /** 低分维度五行(土/金/水/木/火) */
     private String element;
-
-    /** 低分维度主题色 */
     private String color;
-
-    /** 自检得分(满分9) */
     private Integer score;
-
-    /** 等级:healthy健康 / attention留意 / tense紧绷 */
     private String level;
-
-    /** 等级中文 */
     private String levelName;
 
-    /** 相生寻源:上游维度 code */
-    private String upstreamDimension;
-
-    /** 相生寻源:上游维度中文名 */
-    private String upstreamName;
-
-    /** 相生寻源:上游五行 */
-    private String upstreamElement;
-
-    /** 相生寻源:上游主题色 */
-    private String upstreamColor;
-
-    /** 相生寻源:为什么补上游更有效 */
-    private String upstreamReason;
-
-    /** 相克寻源:压制维度 code */
-    private String restrainerDimension;
-
-    /** 相克寻源:压制维度中文名 */
-    private String restrainerName;
+    /** AI 解读(替换原 upstreamReason) */
+    private String interpretation;
 
-    /** 相克寻源:压制五行 */
-    private String restrainerElement;
+    /** AI 微行动列表 */
+    private List<String> microActions;
 
-    /** 相克寻源:压制主题色 */
-    private String restrainerColor;
+    /** AI 补充洞察 */
+    private String aiInsight;
 
-    /** 相克寻源:谁在压制它 */
-    private String restrainerReason;
+    /** 家庭整体洞察(仅第一条携带) */
+    private String familyInsight;
 
-    /** 简单应对建议 */
-    private String action;
+    /** 是否使用降级静态建议 */
+    @JsonProperty("fallbackUsed")
+    private Boolean fallbackUsed;
 }

+ 147 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/SelfCheckAnalysisService.java

@@ -0,0 +1,147 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.FiveDimensionSelfCheck;
+import com.etotem.cfc.mapper.FiveDimensionSelfCheckMapper;
+import com.fasterxml.jackson.core.type.TypeReference;
+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 SelfCheckAnalysisService {
+
+    private static final Logger log = LoggerFactory.getLogger(SelfCheckAnalysisService.class);
+
+    @Resource
+    private AiGateway aiGateway;
+
+    @Resource
+    private FiveDimensionSelfCheckMapper selfCheckMapper;
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    /**
+     * 生成自检建议(AI 优先,失败返回 adviceJson=null + fallbackUsed=true)
+     */
+    public Map<String, Object> generateAdvice(Long userId, Map<String, Integer> scoreMap, List<Integer> questionIds) {
+        try {
+            List<Map<String, Object>> recentHistory = getRecentHistory(userId, 3);
+            Map<String, Object> scoresWithMeta = new LinkedHashMap<>();
+            for (Map.Entry<String, Integer> entry : scoreMap.entrySet()) {
+                Map<String, Object> m = new LinkedHashMap<>();
+                m.put("dimension", entry.getKey());
+                m.put("dimensionName", getDimensionName(entry.getKey()));
+                m.put("score", entry.getValue());
+                scoresWithMeta.put(entry.getKey(), m);
+            }
+            Map<String, Object> inputs = new LinkedHashMap<>();
+            inputs.put("scores", scoresWithMeta);
+            inputs.put("questionIds", questionIds);
+            inputs.put("userId", userId);
+            inputs.put("recentHistory", recentHistory);
+            Map<String, Object> result = aiGateway.generateSelfCheckAdvice(inputs);
+            if (result == null) {
+                log.info("AI 自检建议生成失败,fallback");
+                Map<String, Object> fallback = new LinkedHashMap<>();
+                fallback.put("adviceJson", null);
+                fallback.put("fallbackUsed", true);
+                return fallback;
+            }
+            String adviceJson = (String) result.get("advice_json");
+            Boolean fallbackUsed = (Boolean) result.getOrDefault("fallback_used", false);
+            Map<String, Object> advResult = new LinkedHashMap<>();
+            advResult.put("adviceJson", adviceJson);
+            advResult.put("fallbackUsed", fallbackUsed);
+            return advResult;
+        } catch (Exception e) {
+            log.warn("自检建议生成异常: {}", e.getMessage());
+            Map<String, Object> fallback = new LinkedHashMap<>();
+            fallback.put("adviceJson", null);
+            fallback.put("fallbackUsed", true);
+            return fallback;
+        }
+    }
+
+    /**
+     * 生成趋势分析(AI 优先,失败返回空 insight)
+     */
+    public Map<String, Object> generateTrend(Long userId) {
+        try {
+            List<Map<String, Object>> history = getRecentHistory(userId, 3);
+            Map<String, Object> inputs = new LinkedHashMap<>();
+            inputs.put("history", history);
+            inputs.put("userId", userId);
+            Map<String, Object> result = aiGateway.generateSelfCheckTrend(inputs);
+            if (result == null) {
+                Map<String, Object> empty = new LinkedHashMap<>();
+                empty.put("aiInsight", "");
+                empty.put("trendSummary", "");
+                return empty;
+            }
+            Map<String, Object> trendResult = new LinkedHashMap<>();
+            trendResult.put("aiInsight", result.getOrDefault("aiInsight", ""));
+            trendResult.put("trendSummary", result.getOrDefault("trendSummary", ""));
+            return trendResult;
+        } catch (Exception e) {
+            log.warn("趋势分析异常: {}", e.getMessage());
+            Map<String, Object> empty = new LinkedHashMap<>();
+            empty.put("aiInsight", "");
+            empty.put("trendSummary", "");
+            return empty;
+        }
+    }
+
+    /** 获取最近 N 次自检历史(供 AI 和前端使用) */
+    public List<Map<String, Object>> getRecentHistory(Long userId, int limit) {
+        try {
+            List<FiveDimensionSelfCheck> records = selfCheckMapper.selectList(
+                    new LambdaQueryWrapper<FiveDimensionSelfCheck>()
+                            .eq(FiveDimensionSelfCheck::getUserId, userId)
+                            .orderByDesc(FiveDimensionSelfCheck::getCreatedAt)
+                            .last("LIMIT " + limit)
+            );
+            List<Map<String, Object>> result = new ArrayList<>();
+            for (FiveDimensionSelfCheck r : records) {
+                Map<String, Object> m = new LinkedHashMap<>();
+                m.put("createdAt", r.getCreatedAt() != null ? r.getCreatedAt().toString() : "");
+                m.put("totalScore", r.getTotalScore());
+                if (r.getScoresJson() != null) {
+                    try {
+                        Map<String, Integer> scores = objectMapper.readValue(r.getScoresJson(),
+                                new TypeReference<Map<String, Integer>>() {});
+                        List<Map<String, Object>> dims = new ArrayList<>();
+                        for (Map.Entry<String, Integer> e : scores.entrySet()) {
+                            Map<String, Object> d = new LinkedHashMap<>();
+                            d.put("dimension", e.getKey());
+                            d.put("name", getDimensionName(e.getKey()));
+                            d.put("score", e.getValue());
+                            dims.add(d);
+                        }
+                        m.put("dimensions", dims);
+                    } catch (Exception ignored) {}
+                }
+                result.add(m);
+            }
+            return result;
+        } catch (Exception e) {
+            log.warn("获取自检历史失败: {}", e.getMessage());
+            return Collections.emptyList();
+        }
+    }
+
+    private String getDimensionName(String dim) {
+        switch (dim) {
+            case "body": return "身";
+            case "wisdom": return "智";
+            case "wealth": return "富";
+            case "action": return "行";
+            case "mind": return "心";
+            default: return dim;
+        }
+    }
+}