Browse Source

Merge branch 'cfclub' of http://git.iwintrue.com/liaoxg/cfc into cfclub

E2E Test Bot 2 months ago
parent
commit
8f25ce732c
37 changed files with 4672 additions and 241 deletions
  1. 2 2
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  2. 24 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java
  3. 33 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/FiveDimensionSelfCheckController.java
  4. 13 45
      cfc-backend/src/main/java/com/etotem/cfc/dto/WuxingSourcingAdviceVO.java
  5. 54 0
      cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java
  6. 40 18
      cfc-backend/src/main/java/com/etotem/cfc/service/FiveDimensionSelfCheckService.java
  7. 3 0
      cfc-backend/src/main/java/com/etotem/cfc/service/HealthPlanService.java
  8. 148 0
      cfc-backend/src/main/java/com/etotem/cfc/service/SelfCheckAnalysisService.java
  9. 2 109
      cfc-backend/src/main/java/com/etotem/cfc/service/WuxingSourcingService.java
  10. 53 0
      cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthPlanServiceImpl.java
  11. 1 1
      cfc-backend/src/main/resources/schema.sql
  12. 233 0
      cfc-frontend/components/MemberChipPicker.vue
  13. 70 2
      cfc-frontend/pages/family/self-check-entry.vue
  14. 123 48
      cfc-frontend/pages/family/self-check-result.vue
  15. 8 0
      cfc-frontend/utils/api.js
  16. 1 0
      cfc-langgraph/app/api/chat.py
  17. 54 0
      cfc-langgraph/app/api/self_check.py
  18. 23 0
      cfc-langgraph/app/graphs/chat_graph.py
  19. 116 0
      cfc-langgraph/app/graphs/self_check_analysis_graph.py
  20. 76 0
      cfc-langgraph/app/graphs/self_check_trend_graph.py
  21. 0 0
      cfc-langgraph/app/llm/__init__.py
  22. 3 0
      cfc-langgraph/app/llm/client.py
  23. 2 1
      cfc-langgraph/app/main.py
  24. 1 0
      cfc-langgraph/app/models/common.py
  25. 1 1
      cfc-web/.last_build_commit
  26. 2 2
      cfc-web/package-lock.json
  27. 1 1
      cfc-web/package.json
  28. 90 0
      cfc-web/public/CHANGELOG-v1.0.md
  29. 91 1
      cfc-web/public/CHANGELOG.md
  30. 29 6
      cfc-web/src/views/Users.vue
  31. 13 3
      cfc-web/src/views/admin/CouponManagement.vue
  32. 1 1
      cfc-web/src/views/admin/MembershipCenter.vue
  33. 2 0
      docs/superpowers/PROJECT-OVERVIEW.md
  34. 1303 0
      docs/superpowers/plans/2026-08-31-self-check-ai-integration.md
  35. 1349 0
      docs/superpowers/plans/2026-09-01-cf-commission-redesign.md
  36. 385 0
      docs/superpowers/specs/2026-08-31-self-check-ai-integration-design.md
  37. 322 0
      docs/superpowers/specs/2026-09-01-cf-commission-redesign.md

+ 2 - 2
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -9526,10 +9526,10 @@ public class DatabaseInitializer implements CommandLineRunner {
         migratePlanCenterMenu();
         migratePlanCenterMenu();
         // 迁移268: coupon 表添加 status 列(优惠券停用/启用)
         // 迁移268: coupon 表添加 status 列(优惠券停用/启用)
         migrateCouponStatus();
         migrateCouponStatus();
-// 迁移273: five_dimension_self_checks 表添加 question_ids_json 列(自检轮换用)
+// 迁移276: five_dimension_self_checks 表添加 question_ids_json 列(自检轮换用)
         ensureColumn("five_dimension_self_checks", "question_ids_json", "VARCHAR(255) COMMENT '本次自检使用的题号JSON: [1,5,9,...]'");
         ensureColumn("five_dimension_self_checks", "question_ids_json", "VARCHAR(255) COMMENT '本次自检使用的题号JSON: [1,5,9,...]'");
 
 
-        // 迁移274: 创建 self_check_ignores 表(记录用户忽略自检提醒的动作,作为 15 天周期重置依据)
+        // 迁移277: 创建 self_check_ignores 表(记录用户忽略自检提醒的动作,作为 15 天周期重置依据)
         try {
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS self_check_ignores (" +
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS self_check_ignores (" +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +

+ 24 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java

@@ -62,6 +62,12 @@ public class AIChatController {
     @Resource
     @Resource
     private com.etotem.cfc.service.PortraitService portraitService;
     private com.etotem.cfc.service.PortraitService portraitService;
 
 
+    @Resource
+    private com.etotem.cfc.mapper.FiveDimensionSelfCheckMapper selfCheckMapper;
+
+    @Resource
+    private com.fasterxml.jackson.databind.ObjectMapper objectMapper;
+
     @Operation(summary = "发送聊天消息(支持传入reportId以解读报告)")
     @Operation(summary = "发送聊天消息(支持传入reportId以解读报告)")
     @PostMapping("/chat/send")
     @PostMapping("/chat/send")
     public Result<Map<String, Object>> sendMessage(
     public Result<Map<String, Object>> sendMessage(
@@ -71,6 +77,7 @@ public class AIChatController {
         String conversationId = params.get("conversationId");
         String conversationId = params.get("conversationId");
         String reportIdStr = params.get("reportId");
         String reportIdStr = params.get("reportId");
         String surveyIdStr = params.get("surveyId");
         String surveyIdStr = params.get("surveyId");
+        String selfCheckIdStr = params.get("selfCheckId");
 
 
         if (query == null || query.trim().isEmpty()) {
         if (query == null || query.trim().isEmpty()) {
             return Result.error("消息不能为空");
             return Result.error("消息不能为空");
@@ -121,6 +128,23 @@ public class AIChatController {
             inputs.put("portrait_prompt", chatPortrait);
             inputs.put("portrait_prompt", chatPortrait);
         }
         }
 
 
+        // 注入自检上下文(P1-2)
+        if (selfCheckIdStr != null && !selfCheckIdStr.trim().isEmpty()) {
+            try {
+                Long selfCheckId = Long.valueOf(selfCheckIdStr);
+                com.etotem.cfc.entity.FiveDimensionSelfCheck selfCheck = selfCheckMapper.selectById(selfCheckId);
+                if (selfCheck != null) {
+                    java.util.Map<String, Object> sc = new java.util.LinkedHashMap<>();
+                    sc.put("totalScore", selfCheck.getTotalScore());
+                    sc.put("scores", selfCheck.getScoresJson());
+                    sc.put("createdAt", selfCheck.getCreatedAt() != null ? selfCheck.getCreatedAt().toString() : "");
+                    inputs.put("self_check_result", objectMapper.writeValueAsString(sc));
+                }
+            } catch (Exception e) {
+                log.warn("注入自检上下文失败: {}", e.getMessage());
+            }
+        }
+
         // AI记忆层注入(会话摘要+关键事实)
         // AI记忆层注入(会话摘要+关键事实)
         inputs = aiService.enrichInputsWithMemory(userId, conversationId, inputs);
         inputs = aiService.enrichInputsWithMemory(userId, conversationId, inputs);
 
 

+ 33 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FiveDimensionSelfCheckController.java

@@ -41,6 +41,9 @@ public class FiveDimensionSelfCheckController {
     @Resource
     @Resource
     private UserMapper userMapper;
     private UserMapper userMapper;
 
 
+    @Resource
+    private com.etotem.cfc.service.HealthPlanService healthPlanService;
+
     @Operation(summary = "获取自检问卷题目(首次固定 15 题;再次合并题库随机抽)")
     @Operation(summary = "获取自检问卷题目(首次固定 15 题;再次合并题库随机抽)")
     @PostMapping("/questions")
     @PostMapping("/questions")
     public Result<List<SelfCheckQuestionVO>> getQuestions(
     public Result<List<SelfCheckQuestionVO>> getQuestions(
@@ -132,4 +135,34 @@ public class FiveDimensionSelfCheckController {
                 new Date(ignoredAt.getTime() + 15L * 24 * 60 * 60 * 1000)));
                 new Date(ignoredAt.getTime() + 15L * 24 * 60 * 60 * 1000)));
         return Result.success(resp);
         return Result.success(resp);
     }
     }
+
+    @Operation(summary = "用户点击生成健康计划(基于自检低分维度)")
+    @PostMapping("/generate-plan")
+    public Result<Map<String, Object>> generatePlan(
+            @RequestBody(required = false) Map<String, Object> body,
+            @RequestAttribute("userId") Long userId) {
+        if (userId == null) return Result.error("请先登录");
+        Long checkId = body != null ? com.etotem.cfc.util.ParamUtils.getLong(body.get("checkId")) : null;
+        try {
+            Long planId = healthPlanService.generateFromSelfCheck(userId, checkId);
+            if (planId == null) return Result.error("计划生成失败,请稍后重试");
+            Map<String, Object> resp = new HashMap<>();
+            resp.put("planId", planId);
+            resp.put("status", "draft");
+            return Result.success(resp);
+        } catch (Exception e) {
+            return Result.error("计划生成失败");
+        }
+    }
+
+    @Operation(summary = "获取自检历史趋势分析")
+    @PostMapping("/trend-analysis")
+    public Result<Map<String, Object>> getTrendAnalysis(@RequestAttribute("userId") Long userId) {
+        if (userId == null) return Result.error("请先登录");
+        try {
+            return Result.success(selfCheckService.getTrendAnalysis(userId));
+        } catch (Exception e) {
+            return Result.error("趋势分析失败");
+        }
+    }
 }
 }

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

@@ -1,65 +1,33 @@
 package com.etotem.cfc.dto;
 package com.etotem.cfc.dto;
 
 
+import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.Data;
 import lombok.Data;
+import java.util.List;
 
 
-/**
- * 五行相生寻源建议(P0-2 五行相生寻源建议引擎输出)
- * 对应书稿《附录-工具包汇总》附录六五行相生速查表
- */
 @Data
 @Data
 public class WuxingSourcingAdviceVO {
 public class WuxingSourcingAdviceVO {
 
 
-    /** 低分维度 code(body/wisdom/wealth/action/mind) */
     private String dimension;
     private String dimension;
-
-    /** 低分维度中文名(身/智/富/行/心) */
     private String dimensionName;
     private String dimensionName;
-
-    /** 低分维度五行(土/金/水/木/火) */
     private String element;
     private String element;
-
-    /** 低分维度主题色 */
     private String color;
     private String color;
-
-    /** 自检得分(满分9) */
     private Integer score;
     private Integer score;
-
-    /** 等级:healthy健康 / attention留意 / tense紧绷 */
     private String level;
     private String level;
-
-    /** 等级中文 */
     private String levelName;
     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;
 }
 }

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

@@ -256,6 +256,60 @@ public class AiGateway {
         }
         }
     }
     }
 
 
+    /**
+     * 生成自检 AI 建议(调用 LangGraph self_check_analysis_graph)
+     * @return 含 "advice_json" 和 "fallback_used" 的 Map;失败返回 null
+     */
+    public Map<String, Object> generateSelfCheckAdvice(Map<String, Object> inputs) {
+        if (!enabled || isCircuitOpen()) return null;
+        try {
+            ObjectNode body = objectMapper.valueToTree(inputs);
+            HttpEntity<String> entity = new HttpEntity<>(body.toString(), createJsonHeaders());
+            String url = baseUrl + "/api/v1/self-check/analysis";
+            ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
+            if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
+                JsonNode root = objectMapper.readTree(response.getBody());
+                Map<String, Object> result = new LinkedHashMap<>();
+                result.put("advice_json", root.has("advice_json") ? root.get("advice_json").asText() : null);
+                result.put("fallback_used", root.has("fallback_used") ? root.get("fallback_used").asBoolean() : true);
+                consecutiveFailures.set(0);
+                return result;
+            }
+            return null;
+        } catch (Exception e) {
+            log.warn("AiGateway generateSelfCheckAdvice 调用失败: {}", e.getMessage());
+            recordFailure();
+            return null;
+        }
+    }
+
+    /**
+     * 生成自检趋势分析(调用 LangGraph self_check_trend_graph)
+     * @return 含 "aiInsight" 和 "trendSummary" 的 Map;失败返回 null
+     */
+    public Map<String, Object> generateSelfCheckTrend(Map<String, Object> inputs) {
+        if (!enabled || isCircuitOpen()) return null;
+        try {
+            ObjectNode body = objectMapper.valueToTree(inputs);
+            HttpEntity<String> entity = new HttpEntity<>(body.toString(), createJsonHeaders());
+            String url = baseUrl + "/api/v1/self-check/trend";
+            ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
+            if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
+                JsonNode root = objectMapper.readTree(response.getBody());
+                Map<String, Object> result = new LinkedHashMap<>();
+                result.put("aiInsight", root.has("aiInsight") ? root.get("aiInsight").asText() : "");
+                result.put("trendSummary", root.has("trendSummary") ? root.get("trendSummary").asText() : "");
+                consecutiveFailures.set(0);
+                return result;
+            }
+            return null;
+        } catch (Exception e) {
+            log.warn("AiGateway generateSelfCheckTrend 调用失败: {}", e.getMessage());
+            recordFailure();
+            return null;
+        }
+    }
+
     private List<Map<String, Object>> parseTasks(JsonNode tasksNode) {
     private List<Map<String, Object>> parseTasks(JsonNode tasksNode) {
         List<Map<String, Object>> tasks = new ArrayList<>();
         List<Map<String, Object>> tasks = new ArrayList<>();
         if (tasksNode != null && tasksNode.isArray()) {
         if (tasksNode != null && tasksNode.isArray()) {

+ 40 - 18
cfc-backend/src/main/java/com/etotem/cfc/service/FiveDimensionSelfCheckService.java

@@ -35,7 +35,7 @@ import com.etotem.cfc.util.SortUtil;
  * 对应书稿《终章-绘制你家的幸福全景图》15题自检问卷
  * 对应书稿《终章-绘制你家的幸福全景图》15题自检问卷
  *
  *
  * 计分:A=3分,B=2分,C=1分,D=0分,每维度 3 题满分 9 分
  * 计分:A=3分,B=2分,C=1分,D=0分,每维度 3 题满分 9 分
- * 结果解读:≥7 健康 / 4-6 留意 / ≤3 紧绷(紧绷维度触发五行相生寻源建议)
+ * 结果解读:≥7 健康 / 4-6 留意 / ≤3 紧绷(紧绷维度触发 AI 建议)
  */
  */
 @Service
 @Service
 public class FiveDimensionSelfCheckService {
 public class FiveDimensionSelfCheckService {
@@ -57,6 +57,9 @@ public class FiveDimensionSelfCheckService {
     @Resource
     @Resource
     private SelfCheckIgnoreMapper ignoreMapper;
     private SelfCheckIgnoreMapper ignoreMapper;
 
 
+    @Resource
+    private SelfCheckAnalysisService selfCheckAnalysisService;
+
     private final ObjectMapper objectMapper = new ObjectMapper();
     private final ObjectMapper objectMapper = new ObjectMapper();
 
 
     /** 维度中文名与五行 */
     /** 维度中文名与五行 */
@@ -467,12 +470,18 @@ public class FiveDimensionSelfCheckService {
         }
         }
         Collections.sort(submittedQuestionIds);
         Collections.sort(submittedQuestionIds);
 
 
-        // 生成寻源建议(仅 ≤3 紧绷维度)
+        // 生成 AI 建议(AI 优先,失败降级为空建议)
         Map<String, Integer> scoreMap = new LinkedHashMap<>();
         Map<String, Integer> scoreMap = new LinkedHashMap<>();
         for (SelfCheckResultVO.DimensionScoreVO dvo : dimensions) {
         for (SelfCheckResultVO.DimensionScoreVO dvo : dimensions) {
             scoreMap.put(dvo.getDimension(), dvo.getScore());
             scoreMap.put(dvo.getDimension(), dvo.getScore());
         }
         }
-        List<WuxingSourcingAdviceVO> advices = wuxingSourcingService.getAdvicesForLowScores(scoreMap);
+        String adviceJson = null;
+        try {
+            Map<String, Object> adviceResult = selfCheckAnalysisService.generateAdvice(userId, scoreMap, submittedQuestionIds);
+            adviceJson = (String) adviceResult.get("adviceJson");
+        } catch (Exception e) {
+            log.warn("AI 建议生成异常,使用空建议: {}", e.getMessage());
+        }
 
 
         // 保存记录
         // 保存记录
         FiveDimensionSelfCheck record = new FiveDimensionSelfCheck();
         FiveDimensionSelfCheck record = new FiveDimensionSelfCheck();
@@ -481,7 +490,7 @@ public class FiveDimensionSelfCheckService {
         record.setAnswersJson(toJson(dto.getAnswers()));
         record.setAnswersJson(toJson(dto.getAnswers()));
         record.setScoresJson(toJson(scoreMap));
         record.setScoresJson(toJson(scoreMap));
         record.setTotalScore(totalScore);
         record.setTotalScore(totalScore);
-        record.setAdviceJson(toJson(advices));
+        record.setAdviceJson(adviceJson != null ? adviceJson : "[]");
         record.setQuestionIdsJson(toJson(submittedQuestionIds));
         record.setQuestionIdsJson(toJson(submittedQuestionIds));
         record.setCreatedAt(new Date());
         record.setCreatedAt(new Date());
         selfCheckMapper.insert(record);
         selfCheckMapper.insert(record);
@@ -499,7 +508,7 @@ public class FiveDimensionSelfCheckService {
         vo.setFamilyMemberId(record.getFamilyMemberId());
         vo.setFamilyMemberId(record.getFamilyMemberId());
         vo.setDimensions(dimensions);
         vo.setDimensions(dimensions);
         vo.setTotalScore(totalScore);
         vo.setTotalScore(totalScore);
-        vo.setAdvices(advices);
+        vo.setAdvices(parseAdvices(adviceJson));
         vo.setCreatedAt(record.getCreatedAt());
         vo.setCreatedAt(record.getCreatedAt());
         return vo;
         return vo;
     }
     }
@@ -602,6 +611,18 @@ public class FiveDimensionSelfCheckService {
         return ignore.getCreatedAt();
         return ignore.getCreatedAt();
     }
     }
 
 
+    /**
+     * 获取自检趋势分析结果(含历史列表)
+     */
+    public Map<String, Object> getTrendAnalysis(Long userId) {
+        Map<String, Object> trend = selfCheckAnalysisService.generateTrend(userId);
+        Map<String, Object> resp = new HashMap<>();
+        resp.put("history", selfCheckAnalysisService.getRecentHistory(userId, 3));
+        resp.put("aiInsight", trend.getOrDefault("aiInsight", ""));
+        resp.put("trendSummary", trend.getOrDefault("trendSummary", ""));
+        return resp;
+    }
+
     // === 内部方法 ===
     // === 内部方法 ===
 
 
     private SelfCheckResultVO.DimensionScoreVO buildDimensionScoreVO(String dimension, int score) {
     private SelfCheckResultVO.DimensionScoreVO buildDimensionScoreVO(String dimension, int score) {
@@ -666,6 +687,19 @@ public class FiveDimensionSelfCheckService {
         }
         }
     }
     }
 
 
+    private List<WuxingSourcingAdviceVO> parseAdvices(String adviceJson) {
+        if (adviceJson == null || adviceJson.isEmpty() || "[]".equals(adviceJson)) {
+            return new ArrayList<>();
+        }
+        try {
+            return objectMapper.readValue(adviceJson,
+                    objectMapper.getTypeFactory().constructCollectionType(List.class, WuxingSourcingAdviceVO.class));
+        } catch (Exception e) {
+            log.warn("解析 AI 建议 JSON 失败: {}", e.getMessage());
+            return new ArrayList<>();
+        }
+    }
+
     @SuppressWarnings("unchecked")
     @SuppressWarnings("unchecked")
     private SelfCheckResultVO toVO(FiveDimensionSelfCheck record) {
     private SelfCheckResultVO toVO(FiveDimensionSelfCheck record) {
         SelfCheckResultVO vo = new SelfCheckResultVO();
         SelfCheckResultVO vo = new SelfCheckResultVO();
@@ -694,19 +728,7 @@ public class FiveDimensionSelfCheckService {
         vo.setDimensions(dimensions);
         vo.setDimensions(dimensions);
 
 
         // 解析寻源建议
         // 解析寻源建议
-        if (record.getAdviceJson() != null) {
-            try {
-                List<WuxingSourcingAdviceVO> advices = objectMapper.readValue(
-                        record.getAdviceJson(),
-                        objectMapper.getTypeFactory().constructCollectionType(List.class, WuxingSourcingAdviceVO.class));
-                vo.setAdvices(advices);
-            } catch (Exception e) {
-                log.warn("解析adviceJson失败: {}", e.getMessage());
-                vo.setAdvices(new ArrayList<>());
-            }
-        } else {
-            vo.setAdvices(new ArrayList<>());
-        }
+        vo.setAdvices(parseAdvices(record.getAdviceJson()));
         return vo;
         return vo;
     }
     }
 }
 }

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

@@ -35,4 +35,7 @@ public interface HealthPlanService {
 
 
     /** 查询方案分解后的任务列表(source_type=plan, source_id=planId) */
     /** 查询方案分解后的任务列表(source_type=plan, source_id=planId) */
     List<Task> getPlanTasks(Long planId);
     List<Task> getPlanTasks(Long planId);
+
+    /** 基于自检低分维度自动生成 draft 计划 */
+    Long generateFromSelfCheck(Long userId, Long checkId);
 }
 }

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

@@ -0,0 +1,148 @@
+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<>();
+            java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            for (FiveDimensionSelfCheck r : records) {
+                Map<String, Object> m = new LinkedHashMap<>();
+                m.put("createdAt", r.getCreatedAt() != null ? sdf.format(r.getCreatedAt()) : "");
+                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;
+        }
+    }
+}

+ 2 - 109
cfc-backend/src/main/java/com/etotem/cfc/service/WuxingSourcingService.java

@@ -1,25 +1,15 @@
 package com.etotem.cfc.service;
 package com.etotem.cfc.service;
 
 
-import com.etotem.cfc.dto.WuxingSourcingAdviceVO;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 
 
-import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.Map;
 
 
 /**
 /**
- * 五行相生寻源建议引擎(P0-2)
- * 对应书稿《附录-工具包汇总》附录六「五行相生速查表(主动健康寻源表)」
- *
- * 核心逻辑:
- * 1. 当一个维度亮红灯(分数低)时,先别急着只补它,用两步寻源:
- *    - 相生寻源:看它的上游(谁生它)——是不是上游滋养不足?
- *    - 相克寻源:看谁在压制它——是不是哪个维度过度膨胀?
- * 2. 依据速查表输出:上游维度、压制维度、简单应对
+ * 五行维度元信息工具(P0-2)
+ * 保留维度元数据、等级计算等基础工具方法,寻源逻辑已迁移至 AI 服务。
  *
  *
  * 五行相生:身(土)→智(金)→富(水)→行(木)→心(火)→身
  * 五行相生:身(土)→智(金)→富(水)→行(木)→心(火)→身
- * 五行相克:身克富、智克行、富克心、行克身、心克智
  */
  */
 @Service
 @Service
 public class WuxingSourcingService {
 public class WuxingSourcingService {
@@ -34,103 +24,6 @@ public class WuxingSourcingService {
         DIMENSION_META.put("mind", new String[]{"心", "火", "#FF6B9D"});
         DIMENSION_META.put("mind", new String[]{"心", "火", "#FF6B9D"});
     }
     }
 
 
-    /**
-     * 寻源表(附录六):
-     * 症状维度 → [上游维度, 上游原因, 压制维度, 压制原因, 简单应对]
-     */
-    private static final Map<String, String[]> SOURCING_TABLE = new LinkedHashMap<>();
-    static {
-        SOURCING_TABLE.put("body", new String[]{
-                "mind", "情绪安全了,身体才有行动力",
-                "action", "老好人透支自己,累垮身体",
-                "情绪停机坪;先照顾自己,再照顾别人"
-        });
-        SOURCING_TABLE.put("wisdom", new String[]{
-                "body", "身体状态好,学习效率才高",
-                "mind", "爱太多变成控制,压制独立思考",
-                "睡够再学;给孩子留白"
-        });
-        SOURCING_TABLE.put("wealth", new String[]{
-                "wisdom", "能力上去了,财富才跟着来",
-                "body", "用健康换钱,本末倒置",
-                "先投资自己;别省体检钱"
-        });
-        SOURCING_TABLE.put("action", new String[]{
-                "wealth", "经济宽裕了,才有余力经营关系",
-                "wisdom", "算得太清,关系变冷",
-                "先理财务;少算多听"
-        });
-        SOURCING_TABLE.put("mind", new String[]{
-                "action", "关系顺畅了,内心才安定",
-                "wealth", "钱多了,情薄了",
-                "关系周记;家庭夜谈"
-        });
-    }
-
-    /**
-     * 为单个低分维度生成寻源建议
-     *
-     * @param dimension 维度 code
-     * @param score     自检得分(0-9,可为 null)
-     * @param level     等级(healthy/attention/tense,可为 null,由调用方传入)
-     * @return 寻源建议;维度不存在时返回 null
-     */
-    public WuxingSourcingAdviceVO getAdvice(String dimension, Integer score, String level) {
-        String[] meta = DIMENSION_META.get(dimension);
-        String[] row = SOURCING_TABLE.get(dimension);
-        if (meta == null || row == null) {
-            return null;
-        }
-        WuxingSourcingAdviceVO vo = new WuxingSourcingAdviceVO();
-        vo.setDimension(dimension);
-        vo.setDimensionName(meta[0]);
-        vo.setElement(meta[1]);
-        vo.setColor(meta[2]);
-        vo.setScore(score);
-        vo.setLevel(level != null ? level : (score == null ? null : levelOf(score)));
-        vo.setLevelName(levelName(vo.getLevel()));
-
-        String upstream = row[0];
-        String restrainer = row[2];
-        vo.setUpstreamDimension(upstream);
-        vo.setUpstreamName(DIMENSION_META.get(upstream)[0]);
-        vo.setUpstreamElement(DIMENSION_META.get(upstream)[1]);
-        vo.setUpstreamColor(DIMENSION_META.get(upstream)[2]);
-        vo.setUpstreamReason(row[1]);
-
-        vo.setRestrainerDimension(restrainer);
-        vo.setRestrainerName(DIMENSION_META.get(restrainer)[0]);
-        vo.setRestrainerElement(DIMENSION_META.get(restrainer)[1]);
-        vo.setRestrainerColor(DIMENSION_META.get(restrainer)[2]);
-        vo.setRestrainerReason(row[3]);
-
-        vo.setAction(row[4]);
-        return vo;
-    }
-
-    /**
-     * 为多个低分维度批量生成寻源建议
-     *
-     * @param scoreMap 维度 code → 得分(0-9)
-     * @return 所有分数≤3(紧绷)维度的寻源建议列表
-     */
-    public List<WuxingSourcingAdviceVO> getAdvicesForLowScores(Map<String, Integer> scoreMap) {
-        List<WuxingSourcingAdviceVO> result = new ArrayList<>();
-        if (scoreMap == null) {
-            return result;
-        }
-        for (Map.Entry<String, Integer> entry : scoreMap.entrySet()) {
-            Integer score = entry.getValue();
-            if (score != null && score <= 3) {
-                WuxingSourcingAdviceVO vo = getAdvice(entry.getKey(), score, "tense");
-                if (vo != null) {
-                    result.add(vo);
-                }
-            }
-        }
-        return result;
-    }
-
     /**
     /**
      * 分数 → 等级:≥7 健康 / 4-6 留意 / ≤3 紧绷
      * 分数 → 等级:≥7 健康 / 4-6 留意 / ≤3 紧绷
      */
      */

+ 53 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthPlanServiceImpl.java

@@ -3,12 +3,14 @@ package com.etotem.cfc.service.impl;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.entity.FiveDimensionSelfCheck;
 import com.etotem.cfc.entity.HealthPlan;
 import com.etotem.cfc.entity.HealthPlan;
 import com.etotem.cfc.entity.Task;
 import com.etotem.cfc.entity.Task;
 import com.etotem.cfc.entity.Family;
 import com.etotem.cfc.entity.Family;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.FamilyMapper;
 import com.etotem.cfc.mapper.FamilyMapper;
 import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.mapper.FiveDimensionSelfCheckMapper;
 import com.etotem.cfc.mapper.HealthPlanMapper;
 import com.etotem.cfc.mapper.HealthPlanMapper;
 import com.etotem.cfc.mapper.TaskMapper;
 import com.etotem.cfc.mapper.TaskMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.mapper.UserMapper;
@@ -24,6 +26,7 @@ import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Matcher;
@@ -53,6 +56,12 @@ public class HealthPlanServiceImpl implements HealthPlanService {
     @Resource
     @Resource
     private FamilyMemberMapper familyMemberMapper;
     private FamilyMemberMapper familyMemberMapper;
 
 
+    @Resource
+    private FiveDimensionSelfCheckMapper selfCheckMapper;
+
+    @Resource
+    private com.etotem.cfc.service.AiGateway aiGateway;
+
     @Value("${langgraph.base-url:}")
     @Value("${langgraph.base-url:}")
     private String langgraphBaseUrl;
     private String langgraphBaseUrl;
 
 
@@ -754,6 +763,50 @@ public class HealthPlanServiceImpl implements HealthPlanService {
         return result;
         return result;
     }
     }
 
 
+    @Override
+    public Long generateFromSelfCheck(Long userId, Long checkId) {
+        try {
+            FiveDimensionSelfCheck check;
+            if (checkId != null) {
+                check = selfCheckMapper.selectById(checkId);
+            } else {
+                check = selfCheckMapper.selectOne(
+                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<FiveDimensionSelfCheck>()
+                                .eq(FiveDimensionSelfCheck::getUserId, userId)
+                                .orderByDesc(FiveDimensionSelfCheck::getCreatedAt)
+                                .last("LIMIT 1"));
+            }
+            if (check == null) return null;
+            Map<String, Integer> scores = objectMapper.readValue(check.getScoresJson(),
+                    new com.fasterxml.jackson.core.type.TypeReference<Map<String, Integer>>() {});
+            List<String> lowDims = new ArrayList<>();
+            for (Map.Entry<String, Integer> e : scores.entrySet()) {
+                if (e.getValue() != null && e.getValue() <= 6) lowDims.add(e.getKey());
+            }
+            if (lowDims.isEmpty()) return null;
+            User user = userMapper.selectById(userId);
+            if (user == null || user.getFamilyId() == null) return null;
+            Map<String, Object> inputs = new LinkedHashMap<>();
+            inputs.put("familyId", user.getFamilyId());
+            inputs.put("dimensions", String.join(",", lowDims));
+            inputs.put("goal", "基于五维自检结果,针对低分维度制定改善计划");
+            String planContent = aiGateway.generateHealthPlan(inputs);
+            if (planContent == null) return null;
+            HealthPlan plan = new HealthPlan();
+            plan.setFamilyId(user.getFamilyId());
+            plan.setDimensions(String.join(",", lowDims));
+            plan.setGoal("五维自检自动生成");
+            plan.setPlanContent(planContent);
+            plan.setStatus("draft");
+            plan.setCreatedAt(new Date());
+            healthPlanMapper.insert(plan);
+            return plan.getId();
+        } catch (Exception e) {
+            log.warn("generateFromSelfCheck 失败: {}", e.getMessage());
+            return null;
+        }
+    }
+
     @Override
     @Override
     public Long autoGenerateAndPublish(Long familyId, Long memberId, String reportType) {
     public Long autoGenerateAndPublish(Long familyId, Long memberId, String reportType) {
         if (familyId == null || memberId == null) {
         if (familyId == null || memberId == null) {

+ 1 - 1
cfc-backend/src/main/resources/schema.sql

@@ -5296,7 +5296,7 @@ CREATE TABLE IF NOT EXISTS numsoul_detail_config (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数字能量详细配置';
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数字能量详细配置';
 
 
 -- ============================================================
 -- ============================================================
--- 五维自检忽略提醒记录表(迁移270)
+-- 五维自检忽略提醒记录表(迁移277)
 -- ============================================================
 -- ============================================================
 CREATE TABLE IF NOT EXISTS self_check_ignores (
 CREATE TABLE IF NOT EXISTS self_check_ignores (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
     id BIGINT AUTO_INCREMENT PRIMARY KEY,

+ 233 - 0
cfc-frontend/components/MemberChipPicker.vue

@@ -0,0 +1,233 @@
+<template>
+  <view class="member-chip-picker">
+    <view v-if="loading" class="mcp-loading">
+      <text class="mcp-loading-text">加载成员中...</text>
+    </view>
+
+    <view v-else-if="members.length > 0" class="mcp-chips">
+      <view
+        class="mcp-chip"
+        v-for="(m, i) in members"
+        :key="getMemberKey(m, i)"
+        :class="{ 'mcp-chip-on': isSelected(m.memberId) }"
+        @tap="toggleMember(m.memberId)"
+      >
+        <image
+          v-if="m.avatar"
+          class="mcp-chip-avatar"
+          :src="m.avatar"
+          mode="aspectFill"
+        />
+        <view v-else class="mcp-chip-avatar mcp-chip-avatar-default">
+          <text class="mcp-chip-avatar-text">{{ getInitial(m) }}</text>
+        </view>
+        <text class="mcp-chip-text">{{ m.name || m.nickname || '成员' }}</text>
+      </view>
+
+      <view class="mcp-chip mcp-chip-add" @tap="goAddMember">
+        <text class="mcp-chip-add-icon">+</text>
+        <text class="mcp-chip-text">添加</text>
+      </view>
+    </view>
+
+    <view v-else class="mcp-empty">
+      <text class="mcp-empty-text">暂无家庭成员</text>
+      <view class="mcp-empty-btn" @tap="goAddMember">+ 去添加成员</view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getFamilyMemberList } from '@/utils/api.js'
+
+export default {
+  name: 'MemberChipPicker',
+  props: {
+    // 已选中的成员 id 数组(字符串)
+    selectedIds: {
+      type: Array,
+      default: function() { return [] }
+    }
+  },
+  data: function() {
+    return {
+      members: [],
+      innerSelectedIds: [],
+      loading: false,
+      loaded: false
+    }
+  },
+  watch: {
+    selectedIds: {
+      handler: function(val) {
+        this.innerSelectedIds = (val || []).map(function(id) { return String(id) })
+      },
+      immediate: true
+    }
+  },
+  created: function() {
+    this.reload()
+  },
+  methods: {
+    // ——— :key 方法(避免模板内运算符表达式) ———
+    getMemberKey: function(m, i) {
+      return String(m.memberId || m.id || i)
+    },
+    // ——— 首字(无头像兜底) ———
+    getInitial: function(m) {
+      var name = m.name || m.nickname || ''
+      return name ? name.substring(0, 1) : '?'
+    },
+    isSelected: function(id) {
+      return this.innerSelectedIds.indexOf(String(id)) >= 0
+    },
+    // ——— 加载家庭成员 ———
+    reload: function() {
+      var self = this
+      this.loading = true
+      getFamilyMemberList({ visibleOnly: true }).then(function(res) {
+        self.loading = false
+        if (res && res.code === 200 && res.data) {
+          var raw = res.data instanceof Array ? res.data : ((res.data && res.data.list) || [])
+          var list = []
+          for (var i = 0; i < raw.length; i++) {
+            var m = raw[i]
+            // 同时输出 id/memberId、name/nickname,兼容父组件对成员字段的不同取法
+            list.push({
+              id: m.id,
+              memberId: m.memberId !== undefined ? m.memberId : m.id,
+              name: m.name || m.nickname,
+              nickname: m.nickname || m.name,
+              avatar: m.avatar || '',
+              relationship: m.relationship || m.relativeLabel || m.roleLabel || ''
+            })
+          }
+          self.members = list
+          self.emitState()
+        } else {
+          self.members = []
+          self.emitState()
+        }
+      }).catch(function() {
+        self.loading = false
+        self.members = []
+      })
+    },
+    // ——— 切换选中态 ———
+    toggleMember: function(id) {
+      var sid = String(id)
+      var idx = this.innerSelectedIds.indexOf(sid)
+      if (idx >= 0) {
+        this.innerSelectedIds.splice(idx, 1)
+      } else {
+        this.innerSelectedIds.push(sid)
+      }
+      this.emitState()
+    },
+    // ——— 通知父组件 ———
+    emitState: function() {
+      this.$emit('change', {
+        selectedIds: this.innerSelectedIds.slice(),
+        members: this.members.slice()
+      })
+      if (!this.loaded) {
+        this.loaded = true
+        this.$emit('loaded', {
+          selectedIds: this.innerSelectedIds.slice(),
+          members: this.members.slice()
+        })
+      }
+    },
+    // ——— 添加成员 ———
+    goAddMember: function() {
+      this.$emit('add-member')
+      uni.navigateTo({ url: '/pages/family/add-member' })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.member-chip-picker {
+  width: 100%;
+}
+.mcp-loading {
+  padding: 30rpx 0;
+  text-align: center;
+}
+.mcp-loading-text {
+  font-size: 26rpx;
+  color: #9CA3AF;
+}
+.mcp-chips {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+}
+.mcp-chip {
+  display: flex;
+  align-items: center;
+  padding: 12rpx 24rpx;
+  margin: 0 16rpx 16rpx 0;
+  background: #F3F4F6;
+  border-radius: 999rpx;
+  border: 2rpx solid transparent;
+}
+.mcp-chip-on {
+  background: #FFEDD5;
+  border-color: #F97316;
+}
+.mcp-chip-avatar {
+  width: 40rpx;
+  height: 40rpx;
+  border-radius: 50%;
+  margin-right: 10rpx;
+  overflow: hidden;
+}
+.mcp-chip-avatar-default {
+  background: #E5E7EB;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.mcp-chip-avatar-text {
+  font-size: 22rpx;
+  color: #6B7280;
+}
+.mcp-chip-text {
+  font-size: 26rpx;
+  color: #374151;
+}
+.mcp-chip-on .mcp-chip-text {
+  color: #C2410C;
+  font-weight: 500;
+}
+.mcp-chip-add {
+  background: transparent;
+  border: 2rpx dashed #D1D5DB;
+  color: #9CA3AF;
+}
+.mcp-chip-add-icon {
+  font-size: 28rpx;
+  margin-right: 6rpx;
+  color: #9CA3AF;
+}
+.mcp-empty {
+  padding: 40rpx 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.mcp-empty-text {
+  font-size: 26rpx;
+  color: #9CA3AF;
+  margin-bottom: 16rpx;
+}
+.mcp-empty-btn {
+  padding: 14rpx 40rpx;
+  background: #F97316;
+  color: #FFFFFF;
+  font-size: 26rpx;
+  border-radius: 40rpx;
+}
+</style>

+ 70 - 2
cfc-frontend/pages/family/self-check-entry.vue

@@ -50,6 +50,18 @@
         <text class="sce-status-text">已满 15 天,可再次自检</text>
         <text class="sce-status-text">已满 15 天,可再次自检</text>
       </view>
       </view>
 
 
+      <!-- 趋势分析(P1-1) -->
+      <view class="sce-trend" v-if="trendLoaded">
+        <view class="sce-trend-header">
+          <text class="sce-trend-title">📈 自检趋势</text>
+          <text class="sce-trend-summary" v-if="trendSummary">{{ trendSummary }}</text>
+        </view>
+        <text class="sce-trend-insight" v-if="aiInsight">{{ aiInsight }}</text>
+        <view class="sce-trend-loading" v-if="trendLoading">
+          <text>AI 趋势分析中...</text>
+        </view>
+      </view>
+
       <!-- 按钮区域 -->
       <!-- 按钮区域 -->
       <view class="sce-footer">
       <view class="sce-footer">
         <button v-if="canCheck && lastResult" class="sce-btn sce-btn-primary" @click="retake">
         <button v-if="canCheck && lastResult" class="sce-btn sce-btn-primary" @click="retake">
@@ -70,7 +82,7 @@
 </template>
 </template>
 
 
 <script>
 <script>
-import { getSelfCheckStatus, ignoreSelfCheck } from '@/utils/api'
+import { getSelfCheckStatus, ignoreSelfCheck, getSelfCheckTrendAnalysis } from '@/utils/api'
 import { parseDate } from '@/utils/format.js'
 import { parseDate } from '@/utils/format.js'
 
 
 export default {
 export default {
@@ -79,7 +91,11 @@ export default {
       loading: true,
       loading: true,
       lastResult: null,
       lastResult: null,
       canCheck: false,
       canCheck: false,
-      daysLeft: 0
+      daysLeft: 0,
+      trendLoaded: false,
+      trendLoading: false,
+      aiInsight: '',
+      trendSummary: ''
     }
     }
   },
   },
   computed: {
   computed: {
@@ -95,6 +111,7 @@ export default {
   },
   },
   onLoad: function() {
   onLoad: function() {
     this.loadStatus()
     this.loadStatus()
+    this.loadTrend()
   },
   },
   methods: {
   methods: {
     loadStatus: function() {
     loadStatus: function() {
@@ -137,6 +154,21 @@ export default {
       if (!d) return ''
       if (!d) return ''
       var pad = function(n) { return n < 10 ? '0' + n : '' + n }
       var pad = function(n) { return n < 10 ? '0' + n : '' + n }
       return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
       return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
+    },
+    loadTrend: function() {
+      var self = this
+      this.trendLoading = true
+      getSelfCheckTrendAnalysis().then(function(res) {
+        self.trendLoading = false
+        self.trendLoaded = true
+        if (res.code === 200 && res.data) {
+          self.aiInsight = res.data.aiInsight || ''
+          self.trendSummary = res.data.trendSummary || ''
+        }
+      }).catch(function() {
+        self.trendLoading = false
+        self.trendLoaded = true
+      })
     }
     }
   }
   }
 }
 }
@@ -306,4 +338,40 @@ export default {
   color: #999;
   color: #999;
   font-weight: 400;
   font-weight: 400;
 }
 }
+.sce-trend {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 30rpx 26rpx;
+  margin-bottom: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+}
+.sce-trend-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12rpx;
+}
+.sce-trend-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+}
+.sce-trend-summary {
+  font-size: 24rpx;
+  color: #F97316;
+  font-weight: 600;
+}
+.sce-trend-insight {
+  display: block;
+  font-size: 26rpx;
+  color: #555;
+  line-height: 1.6;
+}
+.sce-trend-loading {
+  padding: 10rpx 0;
+}
+.sce-trend-loading text {
+  font-size: 24rpx;
+  color: #999;
+}
 </style>
 </style>

+ 123 - 48
cfc-frontend/pages/family/self-check-result.vue

@@ -67,80 +67,60 @@
         </view>
         </view>
       </view>
       </view>
 
 
-      <!-- 五行相生寻源建议(P0-2) -->
+      <!-- AI 健康解读(P0-1) -->
       <view class="scr-card" v-if="result.advices && result.advices.length > 0">
       <view class="scr-card" v-if="result.advices && result.advices.length > 0">
         <view class="scr-card-title">
         <view class="scr-card-title">
-          <text class="scr-card-title-text">五行相生 · 寻源建议</text>
-          <text class="scr-card-sub">低分维度 {{ result.advices.length }} 项</text>
+          <text class="scr-card-title-text">AI 健康解读</text>
+          <text class="scr-card-sub">基于你的五维自检结果</text>
         </view>
         </view>
-        <view class="scr-advice-intro">低于6分的维度,先别硬补——从"生"它的上游借力,同时避开"克"它的压制源:</view>
-        <view class="scr-advice-item" v-for="adv in result.advices" :key="adv.dimension">
-          <!-- 低分维度 -->
+        <view v-for="adv in result.advices" :key="adv.dimension" class="scr-advice-item">
           <view class="scr-advice-dim-row">
           <view class="scr-advice-dim-row">
             <view class="scr-advice-dim-badge" :style="{ background: adv.color }">
             <view class="scr-advice-dim-badge" :style="{ background: adv.color }">
               <text class="scr-advice-dim-name">{{ adv.dimensionName }}</text>
               <text class="scr-advice-dim-name">{{ adv.dimensionName }}</text>
-              <text class="scr-advice-dim-elem">{{ adv.element }}</text>
+              <text class="scr-advice-dim-elem">{{ adv.score }}分</text>
             </view>
             </view>
-            <text class="scr-advice-dim-score">{{ adv.score }}分</text>
           </view>
           </view>
-
-          <!-- 相生寻源 -->
-          <view class="scr-advice-flow">
-            <view class="scr-flow-node">
-              <view class="scr-flow-dot" :style="{ background: adv.upstreamColor }"></view>
-              <text class="scr-flow-label">上游 · {{ adv.upstreamName }}({{ adv.upstreamElement }})</text>
-            </view>
-            <text class="scr-flow-arrow">⟶</text>
-            <text class="scr-flow-action">{{ adv.upstreamReason }}</text>
-          </view>
-
-          <!-- 相克寻源 -->
-          <view class="scr-advice-flow">
-            <view class="scr-flow-node">
-              <view class="scr-flow-dot" :style="{ background: adv.restrainerColor }"></view>
-              <text class="scr-flow-label">压制 · {{ adv.restrainerName }}({{ adv.restrainerElement }})</text>
-            </view>
-            <text class="scr-flow-arrow">⛔</text>
-            <text class="scr-flow-action">{{ adv.restrainerReason }}</text>
-          </view>
-
-          <!-- 行动建议 -->
-          <view class="scr-advice-action">
-            <text class="scr-advice-action-label">本周行动</text>
-            <text class="scr-advice-action-text">{{ adv.action }}</text>
-          </view>
-
-          <!-- 一键加入今日微行动(微行动×五维联动) -->
-          <view class="scr-micro-row" v-if="joinedDims[adv.dimension]">
-            <text class="scr-micro-done">✅ 今日微行动已完成,能量已入账</text>
-          </view>
-          <view class="scr-micro-row" v-else>
-            <button class="scr-micro-btn" :loading="joiningDim === adv.dimension" :disabled="joiningDim !== ''" @click="joinMicroAction(adv)">
-              ⚡ 加入今日微行动
-            </button>
+          <text class="scr-advice-interpretation" v-if="adv.interpretation">{{ adv.interpretation }}</text>
+          <view class="scr-advice-actions" v-if="adv.microActions && adv.microActions.length > 0">
+            <text class="scr-advice-action-label">本周行动:</text>
+            <text v-for="(action, i) in adv.microActions" :key="'ma'+i" class="scr-advice-action">{{ i+1 }}. {{ action }}</text>
           </view>
           </view>
         </view>
         </view>
+        <view class="scr-family-insight" v-if="result.advices[0] && result.advices[0].familyInsight">
+          <text class="scr-family-insight-label">【家庭整体洞察】</text>
+          <text class="scr-family-insight-text">{{ result.advices[0].familyInsight }}</text>
+        </view>
       </view>
       </view>
-
       <!-- 全部健康提示 -->
       <!-- 全部健康提示 -->
-      <view class="scr-card" v-else>
+      <view class="scr-card" v-else-if="result.totalScore >= 35">
         <view class="scr-all-healthy">
         <view class="scr-all-healthy">
           <text class="scr-all-healthy-icon">🌿</text>
           <text class="scr-all-healthy-icon">🌿</text>
           <text class="scr-all-healthy-text">五维状态均健康,请继续保持这份平衡!</text>
           <text class="scr-all-healthy-text">五维状态均健康,请继续保持这份平衡!</text>
         </view>
         </view>
       </view>
       </view>
+      <!-- AI 分析暂不可用提示 -->
+      <view class="scr-card" v-else>
+        <view class="scr-all-healthy">
+          <text class="scr-all-healthy-icon">⏳</text>
+          <text class="scr-all-healthy-text">AI 分析暂时不可用,请稍后再试</text>
+        </view>
+      </view>
 
 
-      <!-- 历史与重测 -->
+      <!-- 历史与操作 -->
       <view class="scr-footer">
       <view class="scr-footer">
         <button class="scr-btn scr-btn-primary" @click="goHistory">历史记录</button>
         <button class="scr-btn scr-btn-primary" @click="goHistory">历史记录</button>
         <button class="scr-btn scr-btn-outline" @click="retake">重新自检</button>
         <button class="scr-btn scr-btn-outline" @click="retake">重新自检</button>
+        <button class="scr-btn scr-btn-secondary" v-if="hasLowScore" :loading="planGenerating" @click="generatePlan">
+          生成健康计划
+        </button>
+        <button class="scr-btn scr-btn-ghost" @click="askAI">问问 AI</button>
       </view>
       </view>
     </template>
     </template>
   </view>
   </view>
 </template>
 </template>
 
 
 <script>
 <script>
-import { getSelfCheckLatest, getSelfCheckHistory } from '@/utils/api'
+import { getSelfCheckLatest, getSelfCheckHistory, generateSelfCheckPlan } from '@/utils/api'
 import { microActionToday, microActionComplete } from '@/utils/api'
 import { microActionToday, microActionComplete } from '@/utils/api'
 import { parseDate } from '@/utils/format.js'
 import { parseDate } from '@/utils/format.js'
 
 
@@ -164,7 +144,8 @@ export default {
       },
       },
       // 今日微行动联动状态
       // 今日微行动联动状态
       joiningDim: '',
       joiningDim: '',
-      joinedDims: {}
+      joinedDims: {},
+      planGenerating: false
     }
     }
   },
   },
   computed: {
   computed: {
@@ -176,6 +157,11 @@ export default {
     },
     },
     overallLevelName: function() {
     overallLevelName: function() {
       return this.levelMap[this.overallLevel] || '留意'
       return this.levelMap[this.overallLevel] || '留意'
+    },
+    hasLowScore: function() {
+      return this.result && this.result.advices && this.result.advices.some(function(a) {
+        return a.score != null && a.score <= 6
+      })
     }
     }
   },
   },
   onLoad: function(options) {
   onLoad: function(options) {
@@ -393,6 +379,33 @@ export default {
     },
     },
     retake: function() {
     retake: function() {
       uni.navigateTo({ url: '/pages/family/self-check-entry' })
       uni.navigateTo({ url: '/pages/family/self-check-entry' })
+    },
+    generatePlan: function() {
+      var self = this
+      if (this.planGenerating) return
+      this.planGenerating = true
+      generateSelfCheckPlan({ checkId: this.result && this.result.id }).then(function(res) {
+        self.planGenerating = false
+        if (res.code === 200 && res.data && res.data.planId) {
+          uni.showToast({ title: '计划已生成,可在健康计划页查看', icon: 'success' })
+          setTimeout(function() {
+            uni.navigateTo({ url: '/pages/health-main/index?planId=' + res.data.planId })
+          }, 1000)
+        } else {
+          uni.showToast({ title: (res && res.message) || '计划生成失败', icon: 'none' })
+        }
+      }).catch(function() {
+        self.planGenerating = false
+        uni.showToast({ title: '计划生成失败', icon: 'none' })
+      })
+    },
+    askAI: function() {
+      var selfCheckId = this.result && this.result.id
+      if (!selfCheckId) {
+        uni.showToast({ title: '暂无自检记录', icon: 'none' })
+        return
+      }
+      uni.navigateTo({ url: '/pages/ai/chat?selfCheckId=' + selfCheckId })
     }
     }
   }
   }
 }
 }
@@ -756,4 +769,66 @@ export default {
   color: #F97316;
   color: #F97316;
   border: 2rpx solid #F97316;
   border: 2rpx solid #F97316;
 }
 }
+.scr-btn-secondary {
+  background: linear-gradient(135deg, #10B981, #34D399);
+  color: #fff;
+  font-weight: 600;
+}
+.scr-btn-ghost {
+  background: #fff;
+  color: #666;
+  border: 2rpx solid #D1D5DB;
+}
+.scr-advice-interpretation {
+  display: block;
+  font-size: 26rpx;
+  color: #555;
+  line-height: 1.6;
+  margin: 16rpx 0;
+}
+.scr-advice-actions {
+  display: flex;
+  flex-direction: column;
+  margin-top: 12rpx;
+}
+.scr-advice-action-label {
+  font-size: 24rpx;
+  font-weight: 600;
+  color: #F97316;
+}
+.scr-advice-action {
+  font-size: 24rpx;
+  color: #555;
+  line-height: 1.5;
+}
+.scr-family-insight {
+  background: #F5FAFE;
+  border-radius: 16rpx;
+  padding: 20rpx 24rpx;
+  margin-top: 24rpx;
+}
+.scr-family-insight-label {
+  display: block;
+  font-size: 24rpx;
+  font-weight: 600;
+  color: #F97316;
+  margin-bottom: 8rpx;
+}
+.scr-family-insight-text {
+  display: block;
+  font-size: 26rpx;
+  color: #555;
+  line-height: 1.6;
+}
+.scr-fallback {
+  background: #FFF7ED;
+  border-radius: 12rpx;
+  padding: 16rpx 20rpx;
+  margin-top: 16rpx;
+  text-align: center;
+}
+.scr-fallback text {
+  font-size: 24rpx;
+  color: #999;
+}
 </style>
 </style>

+ 8 - 0
cfc-frontend/utils/api.js

@@ -2281,6 +2281,14 @@ export const ignoreSelfCheck = (data) => {
   return request('/api/family/self-check/ignore', 'POST', data || {})
   return request('/api/family/self-check/ignore', 'POST', data || {})
 }
 }
 
 
+export const generateSelfCheckPlan = (data) => {
+  return request('/api/family/self-check/generate-plan', 'POST', data || {})
+}
+
+export const getSelfCheckTrendAnalysis = () => {
+  return request('/api/family/self-check/trend-analysis', 'POST', {})
+}
+
 // ===== 舌诊相关(两阶段流程) =====
 // ===== 舌诊相关(两阶段流程) =====
 export const parseTonguePreview = (data) => request('/api/health/report/parse-preview?type=tongue', 'POST', data)
 export const parseTonguePreview = (data) => request('/api/health/report/parse-preview?type=tongue', 'POST', data)
 export const confirmTongue = (data) => request('/api/health/report/confirm?type=tongue', 'POST', data)
 export const confirmTongue = (data) => request('/api/health/report/confirm?type=tongue', 'POST', data)

+ 1 - 0
cfc-langgraph/app/api/chat.py

@@ -28,6 +28,7 @@ async def chat(req: ChatRequest):
         "child_id": req.context.child_id if req.context else None,
         "child_id": req.context.child_id if req.context else None,
         "intent": None,
         "intent": None,
         "context": None,
         "context": None,
+        "self_check_result": req.context.self_check_result if req.context and req.context.self_check_result else None,
         "messages": None,
         "messages": None,
         "answer": None,
         "answer": None,
         "tasks": [],
         "tasks": [],

+ 54 - 0
cfc-langgraph/app/api/self_check.py

@@ -0,0 +1,54 @@
+import logging
+from fastapi import APIRouter
+from pydantic import BaseModel
+from typing import Any, Dict, Optional
+from app.graphs.self_check_analysis_graph import get_graph
+from app.graphs.self_check_trend_graph import get_trend_graph
+
+logger = logging.getLogger(__name__)
+router = APIRouter(prefix="/api/v1", tags=["self-check"])
+
+
+class SelfCheckAnalysisRequest(BaseModel):
+    scores: Dict[str, Any]
+    question_ids: list
+    user_id: int
+    recent_history: Optional[list] = None
+
+
+class SelfCheckTrendRequest(BaseModel):
+    history: list
+    user_id: int
+
+
+@router.post("/self-check/analysis")
+async def self_check_analysis(req: SelfCheckAnalysisRequest):
+    graph = get_graph()
+    state = {
+        "scores": req.scores,
+        "question_ids": req.question_ids,
+        "user_id": req.user_id,
+        "recent_history": req.recent_history or [],
+        "advice": None,
+        "error": None,
+    }
+    result = await graph.ainvoke(state)
+    advice = result.get("advice") or {}
+    return {
+        "advice_json": advice.get("advice_json"),
+        "fallback_used": advice.get("fallback_used", True),
+        "error": result.get("error"),
+    }
+
+
+@router.post("/self-check/trend")
+async def self_check_trend(req: SelfCheckTrendRequest):
+    graph = get_trend_graph()
+    state = {"history": req.history, "user_id": req.user_id, "insight": None, "error": None}
+    result = await graph.ainvoke(state)
+    insight = result.get("insight") or {}
+    return {
+        "aiInsight": insight.get("aiInsight", ""),
+        "trendSummary": insight.get("trendSummary", ""),
+        "error": result.get("error"),
+    }

+ 23 - 0
cfc-langgraph/app/graphs/chat_graph.py

@@ -27,6 +27,7 @@ class ChatState(TypedDict):
     child_id: int | None
     child_id: int | None
     intent: Intent | None
     intent: Intent | None
     context: dict | None
     context: dict | None
+    self_check_result: str | None  # P1-2: 自检结果 JSON
     messages: list | None
     messages: list | None
     answer: str | None
     answer: str | None
     tasks: list[dict]
     tasks: list[dict]
@@ -96,6 +97,28 @@ def create_chat_graph():
         if portrait_text:
         if portrait_text:
             messages.insert(1, SystemMessage(content=portrait_text))
             messages.insert(1, SystemMessage(content=portrait_text))
 
 
+        # P1-2: 自检结果注入(如有)
+        self_check_result = state.get("self_check_result")
+        if self_check_result:
+            try:
+                import json as _json
+                sc = _json.loads(self_check_result)
+                sc_text = f"用户最近一次五维自检结果:\n总分 {sc.get('totalScore', 0)}/45,"
+                dims = sc.get('dimensions') or sc.get('scores') or {}
+                if isinstance(dims, list):
+                    dim_lines = [f"{d.get('name','')}: {d.get('score',0)}分" for d in dims]
+                elif isinstance(dims, dict):
+                    dim_lines = [f"{k}: {v}分" for k, v in dims.items()]
+                else:
+                    dim_lines = [str(dims)]
+                sc_text += " | ".join(dim_lines)
+                insight = sc.get('familyInsight') or sc.get('aiInsight')
+                if insight:
+                    sc_text += f"\nAI 洞察: {insight}"
+                messages.insert(1, SystemMessage(content=sc_text))
+            except Exception as _e:
+                logger.warning("解析 self_check_result 失败: %s", _e)
+
         # 注入家庭上下文 + 用户健康现状
         # 注入家庭上下文 + 用户健康现状
         ctx = state.get("context", {})
         ctx = state.get("context", {})
         if isinstance(ctx, dict):
         if isinstance(ctx, dict):

+ 116 - 0
cfc-langgraph/app/graphs/self_check_analysis_graph.py

@@ -0,0 +1,116 @@
+import json
+import logging
+from typing import TypedDict, Optional
+from langgraph.graph import StateGraph, START, END
+from langchain_core.messages import SystemMessage, HumanMessage
+from app.llm.client import get_llm
+from app.monitoring import monitor_agent
+
+logger = logging.getLogger(__name__)
+
+SYSTEM_PROMPT = """你是一位家庭健康顾问,基于五维自检结果(身·智·富·行·心,每维0-9分,满分45)给出个性化建议。
+
+要求:
+1. 对每个低分维度(≤6分)给出1-2句解读和2-3个具体可执行的微行动
+2. 如有历史数据,简要对比趋势(改善/下滑)
+3. 语气温暖口语化,每条解读不超过80字
+4. 最后给出1句家庭整体洞察(30字以内)
+
+返回 JSON(严格格式,不要额外文字):
+{
+  "advice": [
+    {
+      "dimension": "mind",
+      "dimensionName": "心",
+      "interpretation": "你的情绪能量偏低,可能最近压力较大,建议...",
+      "microActions": ["今晚睡前做10分钟深呼吸", "和伴侣约定每周一次夜谈"],
+      "fallbackUsed": false
+    }
+  ],
+  "familyInsight": "建议从行动维度入手,关系顺畅了内心才能安定"
+}
+"""
+
+
+class GraphState(TypedDict):
+    scores: dict
+    question_ids: list
+    user_id: int
+    recent_history: list
+    advice: Optional[dict]
+    error: Optional[str]
+
+
+class SelfCheckAnalysisAgent:
+    def __init__(self):
+        self.llm = get_llm()
+
+    @monitor_agent("self_check_analysis")
+    async def run(self, scores: dict, question_ids: list, user_id: int, recent_history: list) -> dict:
+        try:
+            score_summary = "\n".join(
+                f"{v.get('dimensionName', k)}({k}): {v.get('score', 0)}分"
+                for k, v in scores.items()
+            )
+            history_summary = ""
+            if recent_history:
+                history_summary = "历史趋势:\n" + "\n".join(
+                    f"- {h.get('createdAt', '')}: 总分{h.get('totalScore', 0)}分"
+                    for h in recent_history[:3]
+                )
+            messages = [
+                SystemMessage(content=SYSTEM_PROMPT),
+                HumanMessage(content=f"用户ID: {user_id}\n当前自检得分:\n{score_summary}\n{history_summary}"),
+            ]
+            response = await self.llm.ainvoke(messages)
+            text = response.content.strip()
+            if "```json" in text:
+                text = text.split("```json")[1].split("```")[0].strip()
+            elif "```" in text:
+                text = text.split("```")[1].split("```")[0].strip()
+            data = json.loads(text)
+            advice_list = data.get("advice", [])
+            for item in advice_list:
+                item.setdefault("fallbackUsed", False)
+            if advice_list:
+                advice_list[0]["familyInsight"] = data.get("familyInsight", "")
+            return {"advice": {"advice_json": json.dumps(advice_list, ensure_ascii=False), "fallback_used": False}}
+        except Exception as e:
+            logger.warning("自检建议生成失败: %s", e)
+            return {"advice": {"advice_json": None, "fallback_used": True}}
+
+
+def build_graph():
+    agent = SelfCheckAnalysisAgent()
+
+    def parse_input(state: GraphState) -> GraphState:
+        return state
+
+    async def call_llm(state: GraphState) -> dict:
+        return await agent.run(state["scores"], state["question_ids"], state["user_id"], state["recent_history"])
+
+    def validate(state: GraphState) -> GraphState:
+        adv = state.get("advice")
+        if adv is None or adv.get("advice_json") is None:
+            return {**state, "error": "AI 建议生成失败"}
+        return state
+
+    graph = StateGraph(GraphState)
+    graph.add_node("parse", parse_input)
+    graph.add_node("llm", call_llm)
+    graph.add_node("validate", validate)
+    graph.add_edge(START, "parse")
+    graph.add_edge("parse", "llm")
+    graph.add_edge("llm", "validate")
+    graph.add_edge("validate", END)
+    return graph.compile()
+
+
+_graph = None
+
+
+def get_graph():
+    global _graph
+    if _graph is None:
+        _graph = build_graph()
+    return _graph

+ 76 - 0
cfc-langgraph/app/graphs/self_check_trend_graph.py

@@ -0,0 +1,76 @@
+import json
+import logging
+from typing import TypedDict, Optional
+from langgraph.graph import StateGraph, START, END
+from langchain_core.messages import SystemMessage, HumanMessage
+from app.llm.client import get_llm
+from app.monitoring import monitor_agent
+
+logger = logging.getLogger(__name__)
+
+SYSTEM_PROMPT = """分析用户近3次五维自检趋势,输出:
+- aiInsight: 趋势解读(2-3句,指出最大变化维度和可能原因,口语化)
+- trendSummary: 各维度 delta 简写(如"身-2 智+1 富0 行-1 心+2")
+返回 JSON:{"aiInsight": "...", "trendSummary": "..."}
+"""
+
+
+class TrendState(TypedDict):
+    history: list
+    user_id: int
+    insight: Optional[dict]
+    error: Optional[str]
+
+
+class SelfCheckTrendAgent:
+    def __init__(self):
+        self.llm = get_llm()
+
+    @monitor_agent("self_check_trend")
+    async def run(self, history: list, user_id: int) -> dict:
+        try:
+            if len(history) < 2:
+                return {"insight": {"aiInsight": "自检次数不足,建议完成至少2次自检后查看趋势", "trendSummary": ""}, "error": None}
+            history_text = "\n".join(
+                f"{h.get('createdAt', '')}: 总分{h.get('totalScore', 0)}," +
+                " ".join(f"{d.get('name','')}{d.get('score',0)}分" for d in h.get('dimensions', []))
+                for h in history[-3:]
+            )
+            messages = [
+                SystemMessage(content=SYSTEM_PROMPT),
+                HumanMessage(content=f"用户{user_id}的自检历史:\n{history_text}"),
+            ]
+            response = await self.llm.ainvoke(messages)
+            text = response.content.strip()
+            if "```json" in text:
+                text = text.split("```json")[1].split("```")[0].strip()
+            elif "```" in text:
+                text = text.split("```")[1].split("```")[0].strip()
+            data = json.loads(text)
+            return {"insight": data, "error": None}
+        except Exception as e:
+            logger.warning("趋势分析失败: %s", e)
+            return {"insight": {"aiInsight": "", "trendSummary": ""}, "error": str(e)}
+
+
+def build_trend_graph():
+    agent = SelfCheckTrendAgent()
+
+    async def call_llm(state: TrendState) -> dict:
+        return await agent.run(state["history"], state["user_id"])
+
+    graph = StateGraph(TrendState)
+    graph.add_node("llm", call_llm)
+    graph.add_edge(START, "llm")
+    graph.add_edge("llm", END)
+    return graph.compile()
+
+
+_trend_graph = None
+
+
+def get_trend_graph():
+    global _trend_graph
+    if _trend_graph is None:
+        _trend_graph = build_trend_graph()
+    return _trend_graph

+ 0 - 0
cfc-langgraph/app/llm/__init__.py


+ 3 - 0
cfc-langgraph/app/llm/client.py

@@ -0,0 +1,3 @@
+from src.llm.client import get_llm
+
+__all__ = ["get_llm"]

+ 2 - 1
cfc-langgraph/app/main.py

@@ -3,7 +3,7 @@ import time
 import asyncio
 import asyncio
 import logging
 import logging
 from fastapi import FastAPI, Request
 from fastapi import FastAPI, Request
-from app.api import health, recommend, chat, analyze, tongue, adapter, report_parse, meal, logs, audio, innate_portrait
+from app.api import health, recommend, chat, analyze, tongue, adapter, report_parse, meal, logs, audio, innate_portrait, self_check
 from app import monitoring
 from app import monitoring
 from src.app import router as questionnaire_router
 from src.app import router as questionnaire_router
 from app.middleware import RequestLogMiddleware
 from app.middleware import RequestLogMiddleware
@@ -28,6 +28,7 @@ app.include_router(meal.router)
 app.include_router(audio.router)
 app.include_router(audio.router)
 app.include_router(logs.router)
 app.include_router(logs.router)
 app.include_router(innate_portrait.router)
 app.include_router(innate_portrait.router)
+app.include_router(self_check.router)
 
 
 
 
 @app.on_event("startup")
 @app.on_event("startup")

+ 1 - 0
cfc-langgraph/app/models/common.py

@@ -9,6 +9,7 @@ class UserContext(BaseModel):
     survey_id: Optional[int] = None
     survey_id: Optional[int] = None
     family_id: Optional[int] = None
     family_id: Optional[int] = None
     mascot_code: Optional[str] = None
     mascot_code: Optional[str] = None
+    self_check_result: Optional[str] = None  # P1-2: 自检结果 JSON(五维得分+历史)
 
 
 
 
 class SourceInfo(BaseModel):
 class SourceInfo(BaseModel):

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-fa0c8af5cb98dfd2aa7d677306817f53cb3d79b2
+cbe9ce5740cad43b97a09a651c9d55b66f45da8e

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
 {
   "name": "cfc-web",
   "name": "cfc-web",
-  "version": "1.0.1269",
+  "version": "1.0.1275",
   "lockfileVersion": 3,
   "lockfileVersion": 3,
   "requires": true,
   "requires": true,
   "packages": {
   "packages": {
     "": {
     "": {
       "name": "cfc-web",
       "name": "cfc-web",
-      "version": "1.0.1269",
+      "version": "1.0.1275",
       "dependencies": {
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "cfc-web",
   "name": "cfc-web",
-  "version": "1.0.1270",
+  "version": "1.0.1276",
   "private": true,
   "private": true,
   "scripts": {
   "scripts": {
     "dev": "vue-cli-service serve",
     "dev": "vue-cli-service serve",

+ 90 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,96 @@
 
 
 ---
 ---
 
 
+## v1.0.1276 (2026-09-01)
+
+### Bug 修复
+- 修复 P1-2 自检上下文在 Pydantic 层被静默丢弃
+
+### 文档
+- CF值分佣体系重构实现计划(cfclub分支)
+
+### 其他
+- - T1 数据库迁移(cf_rate_tier/cf_transfer_record 建表 + promotion_tier 精简 + coupon family_id)
+- - T2 新实体/Mapper(CfRateTier / CfTransferRecord)
+- - T3 PlatformPointsService.earn 幂等补丁
+- - T4 CfReferralService(推荐树物化 + 全层级团队规模 + 阶梯匹配)
+- - T5 CfCommissionService 统一分佣核心
+- - T6 推荐绑定物化 + PromotionTier 精简 + refreshRate 刷等级比例
+- - T7 7 处订单结算迁移点切换
+- - T8 旧佣金体系废弃标记
+- - T9 用户查询接口(团队规模/比例/流水/汇总)
+- - T10 CF 转让接口
+- - T11 优惠券绑定家庭 + 家庭券库
+- - T12 管理端阶梯配置接口
+- - T13 小程序推广中心前端改造
+- - T14 测试与 API 文档更新
+- 
+
+
+## v1.0.1275 (2026-09-01)
+
+### Bug 修复
+- 会员中心补listMembershipLevels导入 - 修复等级配置表加载ReferenceError, 等级标签补PREMIUM
+- 用户管理会员等级下拉改为后台动态加载5级, 优惠券管理等级选项同步动态化
+
+
+## v1.0.1274 (2026-09-01)
+
+### 新功能
+- CF值分佣体系重构设计(cfclub分支)
+
+### 其他
+- 设计范围:
+- - 全部订单统一走 CF 值分佣,废弃旧佣金体系
+- - 纯团队规模阶梯分佣(cf_rate_tier 动态可配置)
+- - 同家庭互推上溯到第一个非同家庭引荐人
+- - 返佣返个人 CF 钱包,兑换优惠券挂家庭
+- - 新增 CfCommissionService + cf_transfer_record
+- - 用户可查团队规模与返佣比例
+- - 订单迁移点 7 处 + 前端改造方案
+- 
+
+
+## v1.0.1273 (2026-09-01)
+
+### 新功能
+- 中间页展示自检趋势 AI 解读(P1-1)
+- 结果页改 AI 解读 + 生成计划按钮 + 问问AI(P0-1/P0-2/P1-2)
+- 新增 generateSelfCheckPlan/getSelfCheckTrendAnalysis API
+- AIChat sendMessage 支持 selfCheckId 注入上下文(P1-2)
+- 新增 /generate-plan + /trend-analysis 接口 + HealthPlanService.generateFromSelfCheck
+- 新增 MemberChipPicker 多选成员选择组件,修复 health-plan-summary 缺失组件编译失败
+
+### Bug 修复
+- 删除 WuxingSourcingService 死 import
+
+
+## v1.0.1272 (2026-09-01)
+
+### 新功能
+- 清理 WuxingSourcingService 静态表 + submitSelfCheck 接 AI
+
+
+## v1.0.1271 (2026-09-01)
+
+### 新功能
+- AiGateway 新增 generateSelfCheckAdvice/generateSelfCheckTrend
+- WuxingSourcingAdviceVO 改造为 AI 建议 VO + SelfCheckAnalysisService
+- 新增 self_check 路由模块并注册到 main
+- 新增 self_check_trend_graph 趋势分析实现
+- 新增 self_check_analysis_graph + llm.client 薄包装
+
+### 文档
+- PROJECT-OVERVIEW 添加 AI 集成实现计划索引
+- 五维自检 AI 结合 P0+P1 实现计划
+- PROJECT-OVERVIEW 添加 AI 集成规格索引
+- 五维自检 AI 集成 P0+P1 设计稿(纯AI建议+计划生成+趋势+Chat注入)
+- 优惠券全链路改为家庭维度设计文档
+
+### Bug 修复
+- 迁移编号 273/274 → 276/277,避开并发 dan_report/users 迁移占用
+
+
 ## v1.0.1270 (2026-08-31)
 ## v1.0.1270 (2026-08-31)
 
 
 ### 其他
 ### 其他

+ 91 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 # 更新日志
 
 
-> 当前版本: v1.0.1270
+> 当前版本: v1.0.1276
 
 
 ## 历史版本
 ## 历史版本
 
 
@@ -8,6 +8,96 @@
 
 
 ---
 ---
 
 
+## v1.0.1276 (2026-09-01)
+
+### Bug 修复
+- 修复 P1-2 自检上下文在 Pydantic 层被静默丢弃
+
+### 文档
+- CF值分佣体系重构实现计划(cfclub分支)
+
+### 其他
+- - T1 数据库迁移(cf_rate_tier/cf_transfer_record 建表 + promotion_tier 精简 + coupon family_id)
+- - T2 新实体/Mapper(CfRateTier / CfTransferRecord)
+- - T3 PlatformPointsService.earn 幂等补丁
+- - T4 CfReferralService(推荐树物化 + 全层级团队规模 + 阶梯匹配)
+- - T5 CfCommissionService 统一分佣核心
+- - T6 推荐绑定物化 + PromotionTier 精简 + refreshRate 刷等级比例
+- - T7 7 处订单结算迁移点切换
+- - T8 旧佣金体系废弃标记
+- - T9 用户查询接口(团队规模/比例/流水/汇总)
+- - T10 CF 转让接口
+- - T11 优惠券绑定家庭 + 家庭券库
+- - T12 管理端阶梯配置接口
+- - T13 小程序推广中心前端改造
+- - T14 测试与 API 文档更新
+- 
+
+
+## v1.0.1275 (2026-09-01)
+
+### Bug 修复
+- 会员中心补listMembershipLevels导入 - 修复等级配置表加载ReferenceError, 等级标签补PREMIUM
+- 用户管理会员等级下拉改为后台动态加载5级, 优惠券管理等级选项同步动态化
+
+
+## v1.0.1274 (2026-09-01)
+
+### 新功能
+- CF值分佣体系重构设计(cfclub分支)
+
+### 其他
+- 设计范围:
+- - 全部订单统一走 CF 值分佣,废弃旧佣金体系
+- - 纯团队规模阶梯分佣(cf_rate_tier 动态可配置)
+- - 同家庭互推上溯到第一个非同家庭引荐人
+- - 返佣返个人 CF 钱包,兑换优惠券挂家庭
+- - 新增 CfCommissionService + cf_transfer_record
+- - 用户可查团队规模与返佣比例
+- - 订单迁移点 7 处 + 前端改造方案
+- 
+
+
+## v1.0.1273 (2026-09-01)
+
+### 新功能
+- 中间页展示自检趋势 AI 解读(P1-1)
+- 结果页改 AI 解读 + 生成计划按钮 + 问问AI(P0-1/P0-2/P1-2)
+- 新增 generateSelfCheckPlan/getSelfCheckTrendAnalysis API
+- AIChat sendMessage 支持 selfCheckId 注入上下文(P1-2)
+- 新增 /generate-plan + /trend-analysis 接口 + HealthPlanService.generateFromSelfCheck
+- 新增 MemberChipPicker 多选成员选择组件,修复 health-plan-summary 缺失组件编译失败
+
+### Bug 修复
+- 删除 WuxingSourcingService 死 import
+
+
+## v1.0.1272 (2026-09-01)
+
+### 新功能
+- 清理 WuxingSourcingService 静态表 + submitSelfCheck 接 AI
+
+
+## v1.0.1271 (2026-09-01)
+
+### 新功能
+- AiGateway 新增 generateSelfCheckAdvice/generateSelfCheckTrend
+- WuxingSourcingAdviceVO 改造为 AI 建议 VO + SelfCheckAnalysisService
+- 新增 self_check 路由模块并注册到 main
+- 新增 self_check_trend_graph 趋势分析实现
+- 新增 self_check_analysis_graph + llm.client 薄包装
+
+### 文档
+- PROJECT-OVERVIEW 添加 AI 集成实现计划索引
+- 五维自检 AI 结合 P0+P1 实现计划
+- PROJECT-OVERVIEW 添加 AI 集成规格索引
+- 五维自检 AI 集成 P0+P1 设计稿(纯AI建议+计划生成+趋势+Chat注入)
+- 优惠券全链路改为家庭维度设计文档
+
+### Bug 修复
+- 迁移编号 273/274 → 276/277,避开并发 dan_report/users 迁移占用
+
+
 ## v1.0.1270 (2026-08-31)
 ## v1.0.1270 (2026-08-31)
 
 
 ### 其他
 ### 其他

+ 29 - 6
cfc-web/src/views/Users.vue

@@ -174,6 +174,16 @@
             ></el-option>
             ></el-option>
           </el-select>
           </el-select>
         </el-form-item>
         </el-form-item>
+        <el-form-item label="会员等级">
+          <el-select v-model="editForm.memberLevel" clearable placeholder="选择会员等级" style="width: 100%">
+            <el-option
+              v-for="lv in memberLevelOptions"
+              :key="lv.levelCode"
+              :label="lv.levelName + '(' + lv.levelCode + ')'"
+              :value="lv.levelCode"
+            ></el-option>
+          </el-select>
+        </el-form-item>
         <el-form-item label="角色">
         <el-form-item label="角色">
           <el-checkbox-group v-model="editForm.roles">
           <el-checkbox-group v-model="editForm.roles">
             <el-checkbox
             <el-checkbox
@@ -228,7 +238,7 @@
 
 
 <script>
 <script>
 import SortMixin from '@/mixins/SortMixin'
 import SortMixin from '@/mixins/SortMixin'
-import { getUserList, updateUser, deleteUser, resetUserPassword, searchUsers } from '@/api/admin'
+import { getUserList, updateUser, deleteUser, resetUserPassword, searchUsers, listMembershipLevels } from '@/api/admin'
 
 
 import { getRoleLabel } from '@/utils/permissions'
 import { getRoleLabel } from '@/utils/permissions'
 
 
@@ -270,7 +280,8 @@ export default {
         realName: '',
         realName: '',
         phone: '',
         phone: '',
         roles: [],
         roles: [],
-        referrerId: null
+        referrerId: null,
+        memberLevel: ''
       },
       },
       editRules: {
       editRules: {
         nickname: [
         nickname: [
@@ -290,10 +301,12 @@ export default {
       ],
       ],
       userSearchResults: [],
       userSearchResults: [],
       userSearchLoading: false,
       userSearchLoading: false,
+      memberLevelOptions: [],
     }
     }
   },
   },
   mounted() {
   mounted() {
     this.loadUsers()
     this.loadUsers()
+    this.loadMemberLevels()
   },
   },
   methods: {
   methods: {
     loadList() {
     loadList() {
@@ -327,6 +340,15 @@ const res = await getUserList(params)
         this.loading = false
         this.loading = false
       }
       }
     },
     },
+    async loadMemberLevels() {
+      try {
+        const res = await listMembershipLevels()
+        const list = res.data || []
+        this.memberLevelOptions = Array.isArray(list) ? list.filter(function(l) { return l && typeof l === 'object' && l.levelCode }) : []
+      } catch (e) {
+        this.memberLevelOptions = []
+      }
+    },
     handleSearch() {
     handleSearch() {
       this.pagination.current = 1
       this.pagination.current = 1
       this.loadUsers()
       this.loadUsers()
@@ -358,7 +380,8 @@ const res = await getUserList(params)
         realName: row.realName || '',
         realName: row.realName || '',
         phone: row.phone || '',
         phone: row.phone || '',
         roles: row.roles ? [...row.roles] : (row.role ? [row.role] : []),
         roles: row.roles ? [...row.roles] : (row.role ? [row.role] : []),
-        referrerId: row.referrerId || null
+        referrerId: row.referrerId || null,
+        memberLevel: row.memberLevel || ''
       }
       }
       this.editDialogVisible = true
       this.editDialogVisible = true
     },
     },
@@ -447,11 +470,11 @@ const res = await getUserList(params)
       return String(dateStr).replace('T', ' ').substring(0, 19)
       return String(dateStr).replace('T', ' ').substring(0, 19)
     },
     },
     memberLevelLabel(level) {
     memberLevelLabel(level) {
-      const map = { FREE: '免费', FAMILY: '家庭会员', PROVIDER: '服务商' }
-      return map[level] || '免费'
+      const map = { FREE: '免费会员', FAMILY: '家庭会员', PROVIDER: '标准会员', PREMIUM: '高级家庭会员', LIFETIME: '终身会员' }
+      return map[level] || '免费会员'
     },
     },
     memberLevelType(level) {
     memberLevelType(level) {
-      const map = { FREE: 'info', FAMILY: 'success', PROVIDER: 'warning' }
+      const map = { FREE: 'info', FAMILY: 'success', PROVIDER: 'warning', PREMIUM: 'primary', LIFETIME: 'danger' }
       return map[level] || 'info'
       return map[level] || 'info'
     },
     },
     async searchUsersForReferrer(query) {
     async searchUsersForReferrer(query) {

+ 13 - 3
cfc-web/src/views/admin/CouponManagement.vue

@@ -145,9 +145,7 @@
         </el-form-item>
         </el-form-item>
         <el-form-item v-if="form.grantType === 'JOIN' || form.grantType === 'PERIODIC'" label="适用会员等级">
         <el-form-item v-if="form.grantType === 'JOIN' || form.grantType === 'PERIODIC'" label="适用会员等级">
           <el-select v-model="form.grantLevelCode" placeholder="请选择会员等级" style="width: 100%">
           <el-select v-model="form.grantLevelCode" placeholder="请选择会员等级" style="width: 100%">
-            <el-option label="FREE" value="FREE" />
-            <el-option label="FAMILY" value="FAMILY" />
-            <el-option label="PREMIUM" value="PREMIUM" />
+            <el-option v-for="lv in memberLevelOptions" :key="lv.levelCode" :label="lv.levelName + '(' + lv.levelCode + ')'" :value="lv.levelCode" />
           </el-select>
           </el-select>
         </el-form-item>
         </el-form-item>
         <el-form-item v-if="form.grantType === 'PERIODIC'" label="补发周期">
         <el-form-item v-if="form.grantType === 'PERIODIC'" label="补发周期">
@@ -208,6 +206,7 @@
 <script>
 <script>
 import SortMixin from '@/mixins/SortMixin'
 import SortMixin from '@/mixins/SortMixin'
 import { getCouponList, createCoupon, updateCoupon, deleteCoupon, batchIssueCoupons, toggleCouponStatus } from '@/api/coupon'
 import { getCouponList, createCoupon, updateCoupon, deleteCoupon, batchIssueCoupons, toggleCouponStatus } from '@/api/coupon'
+import { listMembershipLevels } from '@/api/admin'
 import ProductPicker from '@/components/ProductPicker'
 import ProductPicker from '@/components/ProductPicker'
 
 
 export default {
 export default {
@@ -236,6 +235,7 @@ export default {
       batchFamilyIds: '',
       batchFamilyIds: '',
       batchSubmitting: false,
       batchSubmitting: false,
       couponOptions: [],
       couponOptions: [],
+      memberLevelOptions: [],
       keyword: '',
       keyword: '',
       sortableColumns: { name: '名称' }
       sortableColumns: { name: '名称' }
 
 
@@ -243,8 +243,18 @@ export default {
   },
   },
   created() {
   created() {
     this.loadData()
     this.loadData()
+    this.loadMemberLevels()
   },
   },
   methods: {
   methods: {
+    async loadMemberLevels() {
+      try {
+        const res = await listMembershipLevels()
+        const list = res.data || []
+        this.memberLevelOptions = Array.isArray(list) ? list.filter(function(l) { return l && typeof l === 'object' && l.levelCode }) : []
+      } catch (e) {
+        this.memberLevelOptions = []
+      }
+    },
     getEmptyForm() {
     getEmptyForm() {
       return {
       return {
         id: null,
         id: null,

+ 1 - 1
cfc-web/src/views/admin/MembershipCenter.vue

@@ -268,7 +268,7 @@
 <script>
 <script>
 import { searchUsers, updateMembershipUser, updateMembershipLevel, getMembershipStats, listMembershipLevels } from '@/api/admin'
 import { searchUsers, updateMembershipUser, updateMembershipLevel, getMembershipStats, listMembershipLevels } from '@/api/admin'
 
 
-const LEVEL_TAG_TYPE = { FREE: 'info', FAMILY: 'success', PROVIDER: 'warning', LIFETIME: 'danger' }
+const LEVEL_TAG_TYPE = { FREE: 'info', FAMILY: 'success', PROVIDER: 'warning', PREMIUM: 'primary', LIFETIME: 'danger' }
 const UPGRADE_TYPE_LABEL = { pay: '付费升级', consumption: '消费升级', admin: '管理员操作', trial: '试用激活', lifetime: '终身会员授予' }
 const UPGRADE_TYPE_LABEL = { pay: '付费升级', consumption: '消费升级', admin: '管理员操作', trial: '试用激活', lifetime: '终身会员授予' }
 const UPGRADE_TYPE_TAG = { pay: 'success', consumption: 'primary', admin: 'warning', trial: 'info', lifetime: 'danger' }
 const UPGRADE_TYPE_TAG = { pay: 'success', consumption: 'primary', admin: 'warning', trial: 'info', lifetime: 'danger' }
 
 

+ 2 - 0
docs/superpowers/PROJECT-OVERVIEW.md

@@ -366,6 +366,7 @@
 | `2026-08-30-diet-preferences-redesign-design.md` | 🟡 设计稿(3 步引导问卷 + 暖橙主题 + 补全 mustEat/cookingMethods 采集,纯前端零后端改动) | 饮食偏好调研页面重新设计 |
 | `2026-08-30-diet-preferences-redesign-design.md` | 🟡 设计稿(3 步引导问卷 + 暖橙主题 + 补全 mustEat/cookingMethods 采集,纯前端零后端改动) | 饮食偏好调研页面重新设计 |
 | `2026-08-31-coupon-family-based-design.md` | 🟡 设计稿 | 优惠券全链路改为家庭维度(新建 family_coupon / family_coupon_grant_log 表,6 条发放路径全部改家庭,消费/核销同步;API 路径与响应字段保持不变) |
 | `2026-08-31-coupon-family-based-design.md` | 🟡 设计稿 | 优惠券全链路改为家庭维度(新建 family_coupon / family_coupon_grant_log 表,6 条发放路径全部改家庭,消费/核销同步;API 路径与响应字段保持不变) |
 | `2026-08-31-self-check-reminder-design.md` | 🟡 设计稿(15 天复检周期 + 首页入口显示自检分数 + 题目轮换 + 后端忽略记录,纯设计方案) | 五维家庭自检 15 天复检周期与题目轮换设计 |
 | `2026-08-31-self-check-reminder-design.md` | 🟡 设计稿(15 天复检周期 + 首页入口显示自检分数 + 题目轮换 + 后端忽略记录,纯设计方案) | 五维家庭自检 15 天复检周期与题目轮换设计 |
+| `2026-08-31-self-check-ai-integration-design.md` | 🟡 设计稿(P0-1 纯AI替换静态建议 + P0-2 用户点击生成健康计划 + P1-1 历史趋势AI解读 + P1-2 Chat上下文注入,P2延后) | 五维家庭自检 AI 结合设计 |
 | `api/API_REFERENCE.md` | 🟢 已建立(2026-08-18;200+ 接口清单;废弃接口标注;新增接口检查流程) | 后台接口参考文档 |
 | `api/API_REFERENCE.md` | 🟢 已建立(2026-08-18;200+ 接口清单;废弃接口标注;新增接口检查流程) | 后台接口参考文档 |
 
 
 ### 实施计划(plans/)
 ### 实施计划(plans/)
@@ -442,6 +443,7 @@
 | `2026-08-28-plan-standard-content.md` | ✅ 已实施(9 Tasks:LangGraph模型/prompt任务契约/Pydantic校验+正则兜底/regenerate/Java任务生成优先+懒加载回填/Java编译/cfc-web结构化任务编辑/小程序tasks回流/文档验收) | 生成方案标准内容格式实施计划(plan_json.sections[].tasks 唯一事实源;设计稿:2026-08-28-plan-standard-content-design.md) |
 | `2026-08-28-plan-standard-content.md` | ✅ 已实施(9 Tasks:LangGraph模型/prompt任务契约/Pydantic校验+正则兜底/regenerate/Java任务生成优先+懒加载回填/Java编译/cfc-web结构化任务编辑/小程序tasks回流/文档验收) | 生成方案标准内容格式实施计划(plan_json.sections[].tasks 唯一事实源;设计稿:2026-08-28-plan-standard-content-design.md) |
 | `2026-08-28-innate-portrait-mind-dimension.md` | ✅ 已实施(15 Tasks 全栈:迁移270-272/实体/Mapper/DTO/NumSoulCalculator TDD/Service/Controller×2/AiGateway/AiAgent/LangGraph/管理端2页/小程序2页/验证) | 先天画像体系实施计划(星座/八字/数字能量引入心维度;设计稿:2026-08-28-innate-portrait-mind-dimension-design.md) |
 | `2026-08-28-innate-portrait-mind-dimension.md` | ✅ 已实施(15 Tasks 全栈:迁移270-272/实体/Mapper/DTO/NumSoulCalculator TDD/Service/Controller×2/AiGateway/AiAgent/LangGraph/管理端2页/小程序2页/验证) | 先天画像体系实施计划(星座/八字/数字能量引入心维度;设计稿:2026-08-28-innate-portrait-mind-dimension-design.md) |
 | `2026-08-30-dan-report-type-specific-blocks.md` | 🟢 已实施(4 Tasks:assembleDanByType分组/详情重算blocks/前端复用渲染器/文档) | DAN 报告按类型分别处理展示实施计划(A1/A2/B2/B3/B4/B5/B6/C1 八种类型指标分组展示,历史数据生效) |
 | `2026-08-30-dan-report-type-specific-blocks.md` | 🟢 已实施(4 Tasks:assembleDanByType分组/详情重算blocks/前端复用渲染器/文档) | DAN 报告按类型分别处理展示实施计划(A1/A2/B2/B3/B4/B5/B6/C1 八种类型指标分组展示,历史数据生效) |
+| `2026-08-31-self-check-ai-integration.md` | 🟡 待实施(11 Tasks:LangGraph×2 graph + 路由 / Java Service+AiGateway+Controller / 前端结果页+中间页+Chat 注入) | 五维自检 AI 结合 P0+P1 实现计划(设计稿:2026-08-31-self-check-ai-integration-design.md) |
 
 
 ### 计划与设计文档(specs/)
 ### 计划与设计文档(specs/)
 
 

+ 1303 - 0
docs/superpowers/plans/2026-08-31-self-check-ai-integration.md

@@ -0,0 +1,1303 @@
+# 五维家庭自检 AI 结合(P0 + P1)实现计划
+
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
+
+**目标:** 将五维自检结果从静态五行寻源表(`WuxingSourcingService`)替换为 LangGraph AI 生成建议,并叠加健康计划生成、趋势分析、Chat 上下文注入三个能力。
+
+**架构:** LangGraph 新增 2 个 graph(`self_check_analysis_graph`、`self_check_trend_graph`)+ 1 个 api 模块;Java 新增 `SelfCheckAnalysisService` + 2 个 AiGateway 方法 + 3 个 Controller 接口;前端新增 API 方法 + 改造结果页/中间页/Chat。
+
+**技术栈:** Java 8 / Spring Boot 2.7.18 / MyBatis-Plus / Python 3.11 + FastAPI + LangGraph + LangChain / uni-app Vue 2
+
+**规格文档:** `docs/superpowers/specs/2026-08-31-self-check-ai-integration-design.md`
+
+---
+
+## 文件结构
+
+| 文件 | 职责 | 变更 |
+|------|------|------|
+| `cfc-langgraph/app/graphs/self_check_analysis_graph.py` | P0-1:自检建议 AI 生成 graph | 新建 |
+| `cfc-langgraph/app/graphs/self_check_trend_graph.py` | P1-1:趋势分析 AI 生成 graph | 新建 |
+| `cfc-langgraph/app/api/self_check.py` | 注册 P0-1 + P1-1 路由 | 新建 |
+| `cfc-langgraph/app/main.py` | 注册 self_check router | 修改 |
+| `cfc-backend/src/main/java/com/etotem/cfc/dto/WuxingSourcingAdviceVO.java` | VO 改造(去 upstream/restrainer/action,加 AI 字段) | 修改 |
+| `cfc-backend/src/main/java/com/etotem/cfc/service/SelfCheckAnalysisService.java` | P0-1:调 AiGateway 生成建议 | 新建 |
+| `cfc-backend/src/main/java/com/etotem/cfc/service/WuxingSourcingService.java` | 删除 SOURCING_TABLE/getAdvice/getAdvicesForLowScores | 修改 |
+| `cfc-backend/src/main/java/com/etotem/cfc/service/FiveDimensionSelfCheckService.java` | submitSelfCheck 调新 Service;新增 trend 方法 | 修改 |
+| `cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java` | 新增 generateSelfCheckAdvice/generateSelfCheckTrend | 修改 |
+| `cfc-backend/src/main/java/com/etotem/cfc/controller/family/FiveDimensionSelfCheckController.java` | 新增 generatePlan、trendAnalysis 接口;submit 改造 | 修改 |
+| `cfc-backend/src/main/java/com/etotem/cfc/service/HealthPlanService.java` | 新增 generateFromSelfCheck 接口 | 修改 |
+| `cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthPlanServiceImpl.java` | 实现 generateFromSelfCheck | 修改 |
+| `cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java` | sendMessage 支持 selfCheckId 参数 | 修改 |
+| `cfc-frontend/utils/api.js` | 新增 3 个 API 方法 | 修改 |
+| `cfc-frontend/pages/family/self-check-result.vue` | P0-1/P0-2/P1-2 展示改造 | 修改 |
+| `cfc-frontend/pages/family/self-check-entry.vue` | P1-1 趋势展示 | 修改 |
+
+---
+
+## 任务 1:LangGraph — self_check_analysis_graph(P0-1 核心)
+
+**文件:**
+- 创建:`cfc-langgraph/app/graphs/self_check_analysis_graph.py`
+- 创建:`cfc-langgraph/app/api/self_check.py`
+
+- [ ] **步骤 1:创建 self_check_analysis_graph.py**
+
+创建文件 `cfc-langgraph/app/graphs/self_check_analysis_graph.py`:
+
+```python
+import json
+import logging
+from typing import TypedDict, Optional
+from langgraph.graph import StateGraph, START, END
+from langchain_core.messages import SystemMessage, HumanMessage
+from app.llm.client import get_llm
+from app.monitoring import monitor_agent
+
+logger = logging.getLogger(__name__)
+
+SYSTEM_PROMPT = """你是一位家庭健康顾问,基于五维自检结果(身·智·富·行·心,每维0-9分,满分45)给出个性化建议。
+
+要求:
+1. 对每个低分维度(≤6分)给出1-2句解读和2-3个具体可执行的微行动
+2. 如有历史数据,简要对比趋势(改善/下滑)
+3. 语气温暖口语化,每条解读不超过80字
+4. 最后给出1句家庭整体洞察(30字以内)
+
+返回 JSON(严格格式,不要额外文字):
+{
+  "advice": [
+    {
+      "dimension": "mind",
+      "dimensionName": "心",
+      "interpretation": "你的情绪能量偏低,可能最近压力较大,建议...",
+      "microActions": ["今晚睡前做10分钟深呼吸", "和伴侣约定每周一次夜谈"],
+      "fallbackUsed": false
+    }
+  ],
+  "familyInsight": "建议从行动维度入手,关系顺畅了内心才能安定"
+}
+"""
+
+
+class GraphState(TypedDict):
+    scores: dict
+    question_ids: list
+    user_id: int
+    recent_history: list
+    advice: Optional[dict]
+    error: Optional[str]
+
+
+class SelfCheckAnalysisAgent:
+    def __init__(self):
+        self.llm = get_llm()
+
+    @monitor_agent("self_check_analysis")
+    async def run(self, scores: dict, question_ids: list, user_id: int, recent_history: list) -> dict:
+        try:
+            score_summary = "\n".join(
+                f"{v.get('dimensionName', k)}({k}): {v.get('score', 0)}分"
+                for k, v in scores.items()
+            )
+            history_summary = ""
+            if recent_history:
+                history_summary = "历史趋势:\n" + "\n".join(
+                    f"- {h.get('createdAt', '')}: 总分{h.get('totalScore', 0)}分"
+                    for h in recent_history[:3]
+                )
+            messages = [
+                SystemMessage(content=SYSTEM_PROMPT),
+                HumanMessage(content=f"用户ID: {user_id}\n当前自检得分:\n{score_summary}\n{history_summary}"),
+            ]
+            response = await self.llm.ainvoke(messages)
+            text = response.content.strip()
+            if "```json" in text:
+                text = text.split("```json")[1].split("```")[0].strip()
+            elif "```" in text:
+                text = text.split("```")[1].split("```")[0].strip()
+            data = json.loads(text)
+            advice_list = data.get("advice", [])
+            for item in advice_list:
+                item.setdefault("fallbackUsed", False)
+            if advice_list:
+                advice_list[0]["familyInsight"] = data.get("familyInsight", "")
+            return {"advice": {"advice_json": json.dumps(advice_list, ensure_ascii=False), "fallback_used": False}}
+        except Exception as e:
+            logger.warning("自检建议生成失败: %s", e)
+            return {"advice": {"advice_json": None, "fallback_used": True}}
+
+
+def build_graph():
+    agent = SelfCheckAnalysisAgent()
+
+    def parse_input(state: GraphState) -> GraphState:
+        return state
+
+    async def call_llm(state: GraphState) -> dict:
+        return await agent.run(state["scores"], state["question_ids"], state["user_id"], state["recent_history"])
+
+    def validate(state: GraphState) -> GraphState:
+        adv = state.get("advice")
+        if adv is None or adv.get("advice_json") is None:
+            return {**state, "error": "AI 建议生成失败"}
+        return state
+
+    graph = StateGraph(GraphState)
+    graph.add_node("parse", parse_input)
+    graph.add_node("llm", call_llm)
+    graph.add_node("validate", validate)
+    graph.add_edge(START, "parse")
+    graph.add_edge("parse", "llm")
+    graph.add_edge("llm", "validate")
+    graph.add_edge("validate", END)
+    return graph.compile()
+
+
+_graph = None
+
+
+def get_graph():
+    global _graph
+    if _graph is None:
+        _graph = build_graph()
+    return _graph
+```
+
+- [ ] **步骤 2:创建 self_check.py API 模块**
+
+创建文件 `cfc-langgraph/app/api/self_check.py`:
+
+```python
+import logging
+from fastapi import APIRouter
+from pydantic import BaseModel
+from typing import Any, Dict, Optional
+from app.graphs.self_check_analysis_graph import get_graph
+from app.graphs.self_check_trend_graph import get_trend_graph
+
+logger = logging.getLogger(__name__)
+router = APIRouter(prefix="/api/v1", tags=["self-check"])
+
+
+class SelfCheckAnalysisRequest(BaseModel):
+    scores: Dict[str, Any]
+    question_ids: list
+    user_id: int
+    recent_history: Optional[list] = None
+
+
+class SelfCheckTrendRequest(BaseModel):
+    history: list
+    user_id: int
+
+
+@router.post("/self-check/analysis")
+async def self_check_analysis(req: SelfCheckAnalysisRequest):
+    graph = get_graph()
+    state = {
+        "scores": req.scores,
+        "question_ids": req.question_ids,
+        "user_id": req.user_id,
+        "recent_history": req.recent_history or [],
+        "advice": None,
+        "error": None,
+    }
+    result = await graph.ainvoke(state)
+    advice = result.get("advice") or {}
+    return {
+        "advice_json": advice.get("advice_json"),
+        "fallback_used": advice.get("fallback_used", True),
+        "error": result.get("error"),
+    }
+
+
+@router.post("/self-check/trend")
+async def self_check_trend(req: SelfCheckTrendRequest):
+    graph = get_trend_graph()
+    state = {"history": req.history, "user_id": req.user_id, "insight": None, "error": None}
+    result = await graph.ainvoke(state)
+    insight = result.get("insight") or {}
+    return {
+        "aiInsight": insight.get("aiInsight", ""),
+        "trendSummary": insight.get("trendSummary", ""),
+        "error": result.get("error"),
+    }
+```
+
+- [ ] **步骤 3:注册路由**
+
+在 `cfc-langgraph/app/main.py` 中,于 `app.include_router(innate_portrait.router)` 之后追加:
+
+```python
+from app.api import self_check
+app.include_router(self_check.router)
+```
+
+- [ ] **步骤 4:语法校验**
+
+```bash
+cd cfc-langgraph && python -m py_compile app/graphs/self_check_analysis_graph.py app/graphs/self_check_trend_graph.py app/api/self_check.py && echo "OK"
+```
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-langgraph/app/graphs/self_check_analysis_graph.py \
+        cfc-langgraph/app/graphs/self_check_trend_graph.py \
+        cfc-langgraph/app/api/self_check.py \
+        cfc-langgraph/app/main.py
+git commit -m "feat(langgraph): 新增自检建议+trend graph + self_check 路由"
+```
+
+---
+
+## 任务 2:LangGraph — self_check_trend_graph(P1-1 核心)
+
+**文件:**
+- 创建:`cfc-langgraph/app/graphs/self_check_trend_graph.py`
+
+- [ ] **步骤 1:创建 self_check_trend_graph.py**
+
+创建文件 `cfc-langgraph/app/graphs/self_check_trend_graph.py`:
+
+```python
+import json
+import logging
+from typing import TypedDict, Optional
+from langgraph.graph import StateGraph, START, END
+from langchain_core.messages import SystemMessage, HumanMessage
+from app.llm.client import get_llm
+from app.monitoring import monitor_agent
+
+logger = logging.getLogger(__name__)
+
+SYSTEM_PROMPT = """分析用户近3次五维自检趋势,输出:
+- aiInsight: 趋势解读(2-3句,指出最大变化维度和可能原因,口语化)
+- trendSummary: 各维度 delta 简写(如"身-2 智+1 富0 行-1 心+2")
+返回 JSON:{"aiInsight": "...", "trendSummary": "..."}
+"""
+
+
+class TrendState(TypedDict):
+    history: list
+    user_id: int
+    insight: Optional[dict]
+    error: Optional[str]
+
+
+class SelfCheckTrendAgent:
+    def __init__(self):
+        self.llm = get_llm()
+
+    @monitor_agent("self_check_trend")
+    async def run(self, history: list, user_id: int) -> dict:
+        try:
+            if len(history) < 2:
+                return {"insight": {"aiInsight": "自检次数不足,建议完成至少2次自检后查看趋势", "trendSummary": ""}, "error": None}
+            history_text = "\n".join(
+                f"{h.get('createdAt', '')}: 总分{h.get('totalScore', 0)}," +
+                " ".join(f"{d.get('name','')}{d.get('score',0)}分" for d in h.get('dimensions', []))
+                for h in history[-3:]
+            )
+            messages = [
+                SystemMessage(content=SYSTEM_PROMPT),
+                HumanMessage(content=f"用户{user_id}的自检历史:\n{history_text}"),
+            ]
+            response = await self.llm.ainvoke(messages)
+            text = response.content.strip()
+            if "```json" in text:
+                text = text.split("```json")[1].split("```")[0].strip()
+            elif "```" in text:
+                text = text.split("```")[1].split("```")[0].strip()
+            data = json.loads(text)
+            return {"insight": data, "error": None}
+        except Exception as e:
+            logger.warning("趋势分析失败: %s", e)
+            return {"insight": {"aiInsight": "", "trendSummary": ""}, "error": str(e)}
+
+
+def build_trend_graph():
+    agent = SelfCheckTrendAgent()
+
+    async def call_llm(state: TrendState) -> dict:
+        return await agent.run(state["history"], state["user_id"])
+
+    graph = StateGraph(TrendState)
+    graph.add_node("llm", call_llm)
+    graph.add_edge(START, "llm")
+    graph.add_edge("llm", END)
+    return graph.compile()
+
+
+_trend_graph = None
+
+
+def get_trend_graph():
+    global _trend_graph
+    if _trend_graph is None:
+        _trend_graph = build_trend_graph()
+    return _trend_graph
+```
+
+- [ ] **步骤 2:语法校验**
+
+```bash
+cd cfc-langgraph && python -m py_compile app/graphs/self_check_trend_graph.py && echo "OK"
+```
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-langgraph/app/graphs/self_check_trend_graph.py
+git commit -m "feat(langgraph): 新增 self_check_trend_graph 趋势分析实现"
+```
+
+---
+
+## 任务 3:Java — WuxingSourcingAdviceVO 改造 + SelfCheckAnalysisService
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/dto/WuxingSourcingAdviceVO.java`
+- 新建:`cfc-backend/src/main/java/com/etotem/cfc/service/SelfCheckAnalysisService.java`
+
+- [ ] **步骤 1:改造 WuxingSourcingAdviceVO**
+
+将 `cfc-backend/src/main/java/com/etotem/cfc/dto/WuxingSourcingAdviceVO.java` 的字段替换为:
+
+```java
+package com.etotem.cfc.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import java.util.List;
+
+@Data
+public class WuxingSourcingAdviceVO {
+
+    private String dimension;
+    private String dimensionName;
+    private String element;
+    private String color;
+    private Integer score;
+    private String level;
+    private String levelName;
+
+    /** AI 解读(替换原 upstreamReason) */
+    private String interpretation;
+
+    /** AI 微行动列表 */
+    private List<String> microActions;
+
+    /** AI 补充洞察 */
+    private String aiInsight;
+
+    /** 家庭整体洞察(仅第一条携带) */
+    private String familyInsight;
+
+    /** 是否使用降级静态建议 */
+    @JsonProperty("fallbackUsed")
+    private Boolean fallbackUsed;
+}
+```
+
+- [ ] **步骤 2:创建 SelfCheckAnalysisService**
+
+创建文件 `cfc-backend/src/main/java/com/etotem/cfc/service/SelfCheckAnalysisService.java`:
+
+```java
+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");
+                return Map.of("adviceJson", null, "fallbackUsed", true);
+            }
+            String adviceJson = (String) result.get("advice_json");
+            Boolean fallbackUsed = (Boolean) result.getOrDefault("fallback_used", false);
+            return Map.of("adviceJson", adviceJson, "fallbackUsed", fallbackUsed);
+        } catch (Exception e) {
+            log.warn("自检建议生成异常: {}", e.getMessage());
+            return Map.of("adviceJson", null, "fallbackUsed", true);
+        }
+    }
+
+    /**
+     * 生成趋势分析(AI 优先,失败返回空 insight)
+     */
+    public Map<String, Object> generateTrend(Long userId) {
+        try {
+            List<Map<String, Object>> history = getRecentHistory(userId, 3);
+            Map<String, Object> inputs = Map.of("history", history, "userId", userId);
+            Map<String, Object> result = aiGateway.generateSelfCheckTrend(inputs);
+            if (result == null) {
+                return Map.of("aiInsight", "", "trendSummary", "");
+            }
+            return Map.of(
+                    "aiInsight", result.getOrDefault("aiInsight", ""),
+                    "trendSummary", result.getOrDefault("trendSummary", "")
+            );
+        } catch (Exception e) {
+            log.warn("趋势分析异常: {}", e.getMessage());
+            return Map.of("aiInsight", "", "trendSummary", "");
+        }
+    }
+
+    /** 获取最近 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;
+        }
+    }
+}
+```
+
+- [ ] **步骤 3:编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+预期:BUILD SUCCESS
+
+- [ ] **步骤 4:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/WuxingSourcingAdviceVO.java \
+       cfc-backend/src/main/java/com/etotem/cfc/service/SelfCheckAnalysisService.java
+git commit -m "feat(self-check): WuxingSourcingAdviceVO 改造为 AI 建议 VO + SelfCheckAnalysisService"
+```
+
+---
+
+## 任务 4:Java — AiGateway 新增方法
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java`
+
+- [ ] **步骤 1:新增 generateSelfCheckAdvice 方法**
+
+在 `AiGateway.java` 中,于 `generateInnateReading` 方法之后追加:
+
+```java
+/**
+ * 生成自检 AI 建议(调用 LangGraph self_check_analysis_graph)
+ * @return 含 "advice_json" 和 "fallback_used" 的 Map;失败返回 null
+ */
+public Map<String, Object> generateSelfCheckAdvice(Map<String, Object> inputs) {
+    if (!enabled || isCircuitOpen()) return null;
+    try {
+        ObjectNode body = objectMapper.valueToTree(inputs);
+        HttpEntity<String> entity = new HttpEntity<>(body.toString(), createJsonHeaders());
+        String url = baseUrl + "/api/v1/self-check/analysis";
+        ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
+        if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
+            JsonNode root = objectMapper.readTree(response.getBody());
+            Map<String, Object> result = new LinkedHashMap<>();
+            result.put("advice_json", root.has("advice_json") ? root.get("advice_json").asText() : null);
+            result.put("fallback_used", root.has("fallback_used") ? root.get("fallback_used").asBoolean() : true);
+            consecutiveFailures.set(0);
+            return result;
+        }
+        return null;
+    } catch (Exception e) {
+        log.warn("AiGateway generateSelfCheckAdvice 调用失败: {}", e.getMessage());
+        recordFailure();
+        return null;
+    }
+}
+
+/**
+ * 生成自检趋势分析(调用 LangGraph self_check_trend_graph)
+ * @return 含 "aiInsight" 和 "trendSummary" 的 Map;失败返回 null
+ */
+public Map<String, Object> generateSelfCheckTrend(Map<String, Object> inputs) {
+    if (!enabled || isCircuitOpen()) return null;
+    try {
+        ObjectNode body = objectMapper.valueToTree(inputs);
+        HttpEntity<String> entity = new HttpEntity<>(body.toString(), createJsonHeaders());
+        String url = baseUrl + "/api/v1/self-check/trend";
+        ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
+        if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
+            JsonNode root = objectMapper.readTree(response.getBody());
+            Map<String, Object> result = new LinkedHashMap<>();
+            result.put("aiInsight", root.has("aiInsight") ? root.get("aiInsight").asText() : "");
+            result.put("trendSummary", root.has("trendSummary") ? root.get("trendSummary").asText() : "");
+            consecutiveFailures.set(0);
+            return result;
+        }
+        return null;
+    } catch (Exception e) {
+        log.warn("AiGateway generateSelfCheckTrend 调用失败: {}", e.getMessage());
+        recordFailure();
+        return null;
+    }
+}
+```
+
+- [ ] **步骤 2:编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+预期:BUILD SUCCESS
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java
+git commit -m "feat(self-check): AiGateway 新增 generateSelfCheckAdvice/generateSelfCheckTrend"
+```
+
+---
+
+## 任务 5:Java — WuxingSourcingService 清理 + FiveDimensionSelfCheckService 改造
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/WuxingSourcingService.java`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/FiveDimensionSelfCheckService.java`
+
+- [ ] **步骤 1:清理 WuxingSourcingService**
+
+删除 `WuxingSourcingService.java` 中的:
+- `SOURCING_TABLE` 静态字段(第 41-68 行)
+- `getAdvice()` 方法(第 78-109 行)
+- `getAdvicesForLowScores()` 方法(第 117-132 行)
+
+保留:`DIMENSION_META`、`levelOf()`、`levelName()`、`dimensionName()`
+
+- [ ] **步骤 2:FiveDimensionSelfCheckService 改造 submitSelfCheck**
+
+在 `submitSelfCheck` 方法中,将第 472-477 行(`wuxingSourcingService.getAdvicesForLowScores` + 落库 adviceJson)替换为:
+
+```java
+        // 生成 AI 建议(AI 优先,失败降级为空建议)
+        String adviceJson = null;
+        try {
+            Map<String, Object> adviceResult = selfCheckAnalysisService.generateAdvice(userId, scoreMap, submittedQuestionIds);
+            adviceJson = (String) adviceResult.get("adviceJson");
+        } catch (Exception e) {
+            log.warn("AI 建议生成异常,使用空建议: {}", e.getMessage());
+        }
+        record.setAdviceJson(adviceJson != null ? adviceJson : "[]");
+```
+
+同时新增注入:
+
+```java
+    @Resource
+    private SelfCheckAnalysisService selfCheckAnalysisService;
+```
+
+并将 `vo.setAdvices(...)` 改为根据 adviceJson 解析(若无 AI 结果则空列表):
+
+```java
+        vo.setAdvices(parseAdvices(adviceJson));
+```
+
+- [ ] **步骤 3:新增 parseAdvices + trend 方法**
+
+在 Service 中新增:
+
+```java
+    private List<WuxingSourcingAdviceVO> parseAdvices(String adviceJson) {
+        if (adviceJson == null || adviceJson.isEmpty() || "[]".equals(adviceJson)) {
+            return new ArrayList<>();
+        }
+        try {
+            return objectMapper.readValue(adviceJson,
+                    objectMapper.getTypeFactory().constructCollectionType(List.class, WuxingSourcingAdviceVO.class));
+        } catch (Exception e) {
+            log.warn("解析 AI 建议 JSON 失败: {}", e.getMessage());
+            return new ArrayList<>();
+        }
+    }
+
+    /** 获取自检趋势分析结果(含历史列表) */
+    public Map<String, Object> getTrendAnalysis(Long userId) {
+        Map<String, Object> trend = selfCheckAnalysisService.generateTrend(userId);
+        Map<String, Object> resp = new HashMap<>();
+        resp.put("history", selfCheckAnalysisService.getRecentHistory(userId, 3));
+        resp.put("aiInsight", trend.getOrDefault("aiInsight", ""));
+        resp.put("trendSummary", trend.getOrDefault("trendSummary", ""));
+        return resp;
+    }
+```
+
+- [ ] **步骤 4:编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+预期:BUILD SUCCESS
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/WuxingSourcingService.java \
+       cfc-backend/src/main/java/com/etotem/cfc/service/FiveDimensionSelfCheckService.java
+git commit -m "feat(self-check): 清理 WuxingSourcingService 静态表 + submitSelfCheck 接 AI"
+```
+
+---
+
+## 任务 6:Java — Controller 新增接口 + HealthPlanService
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/family/FiveDimensionSelfCheckController.java`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/HealthPlanService.java`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthPlanServiceImpl.java`
+
+- [ ] **步骤 1:Controller 新增 generatePlan 接口**
+
+在 `FiveDimensionSelfCheckController.java` 中追加:
+
+```java
+    @Resource
+    private com.etotem.cfc.service.HealthPlanService healthPlanService;
+
+    @Operation(summary = "用户点击生成健康计划(基于自检低分维度)")
+    @PostMapping("/generate-plan")
+    public Result<Map<String, Object>> generatePlan(
+            @RequestBody(required = false) Map<String, Object> body,
+            @RequestAttribute("userId") Long userId) {
+        if (userId == null) return Result.error("请先登录");
+        Long checkId = body != null ? com.etotem.cfc.util.ParamUtils.getLong(body.get("checkId")) : null;
+        try {
+            Long planId = healthPlanService.generateFromSelfCheck(userId, checkId);
+            if (planId == null) return Result.error("计划生成失败,请稍后重试");
+            Map<String, Object> resp = new HashMap<>();
+            resp.put("planId", planId);
+            resp.put("status", "draft");
+            return Result.success(resp);
+        } catch (Exception e) {
+            return Result.error("计划生成失败");
+        }
+    }
+
+    @Operation(summary = "获取自检历史趋势分析")
+    @PostMapping("/trend-analysis")
+    public Result<Map<String, Object>> getTrendAnalysis(@RequestAttribute("userId") Long userId) {
+        if (userId == null) return Result.error("请先登录");
+        try {
+            return Result.success(selfCheckService.getTrendAnalysis(userId));
+        } catch (Exception e) {
+            return Result.error("趋势分析失败");
+        }
+    }
+```
+
+- [ ] **步骤 2:HealthPlanService 接口新增 generateFromSelfCheck**
+
+在 `HealthPlanService.java` 中追加:
+
+```java
+    /** 基于自检低分维度自动生成 draft 计划 */
+    Long generateFromSelfCheck(Long userId, Long checkId);
+```
+
+- [ ] **步骤 3:HealthPlanServiceImpl 实现 generateFromSelfCheck**
+
+在 `HealthPlanServiceImpl.java` 中追加(需新增 `@Resource private com.etotem.cfc.mapper.FiveDimensionSelfCheckMapper selfCheckMapper;`):
+
+```java
+    @Override
+    public Long generateFromSelfCheck(Long userId, Long checkId) {
+        try {
+            com.etotem.cfc.entity.FiveDimensionSelfCheck check;
+            if (checkId != null) {
+                check = selfCheckMapper.selectById(checkId);
+            } else {
+                check = selfCheckMapper.selectOne(
+                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.etotem.cfc.entity.FiveDimensionSelfCheck>()
+                                .eq(com.etotem.cfc.entity.FiveDimensionSelfCheck::getUserId, userId)
+                                .orderByDesc(com.etotem.cfc.entity.FiveDimensionSelfCheck::getCreatedAt)
+                                .last("LIMIT 1"));
+            }
+            if (check == null) return null;
+            Map<String, Integer> scores = objectMapper.readValue(check.getScoresJson(),
+                    new com.fasterxml.jackson.core.type.TypeReference<Map<String, Integer>>() {});
+            List<String> lowDims = new ArrayList<>();
+            for (Map.Entry<String, Integer> e : scores.entrySet()) {
+                if (e.getValue() != null && e.getValue() <= 6) lowDims.add(e.getKey());
+            }
+            if (lowDims.isEmpty()) return null;
+            com.etotem.cfc.entity.User user = userMapper.selectById(userId);
+            if (user == null || user.getFamilyId() == null) return null;
+            Map<String, Object> inputs = new LinkedHashMap<>();
+            inputs.put("familyId", user.getFamilyId());
+            inputs.put("dimensions", String.join(",", lowDims));
+            inputs.put("goal", "基于五维自检结果,针对低分维度制定改善计划");
+            String planContent = aiGateway.generateHealthPlan(inputs);
+            if (planContent == null) return null;
+            com.etotem.cfc.entity.HealthPlan plan = new com.etotem.cfc.entity.HealthPlan();
+            plan.setFamilyId(user.getFamilyId());
+            plan.setDimensions(String.join(",", lowDims));
+            plan.setGoal("五维自检自动生成");
+            plan.setPlanContent(planContent);
+            plan.setStatus("draft");
+            plan.setCreatedAt(new Date());
+            healthPlanMapper.insert(plan);
+            return plan.getId();
+        } catch (Exception e) {
+            log.warn("generateFromSelfCheck 失败: {}", e.getMessage());
+            return null;
+        }
+    }
+```
+
+- [ ] **步骤 4:编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+预期:BUILD SUCCESS
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/family/FiveDimensionSelfCheckController.java \
+       cfc-backend/src/main/java/com/etotem/cfc/service/HealthPlanService.java \
+       cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthPlanServiceImpl.java
+git commit -m "feat(self-check): 新增 /generate-plan + /trend-analysis 接口 + HealthPlanService.generateFromSelfCheck"
+```
+
+---
+
+## 任务 7:Java — AIChatController P1-2 context 注入
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java`
+
+- [ ] **步骤 1:sendMessage 支持 selfCheckId**
+
+在 `AIChatController.java` 的 `sendMessage` 方法中,于第 72 行(`String surveyIdStr = params.get("surveyId");`)之后追加:
+
+```java
+        String selfCheckIdStr = params.get("selfCheckId");
+```
+
+在第 122 行(`inputs.put("portrait_prompt", chatPortrait);`)之后追加:
+
+```java
+        // 注入自检上下文(P1-2)
+        if (selfCheckIdStr != null && !selfCheckIdStr.trim().isEmpty()) {
+            try {
+                Long selfCheckId = Long.valueOf(selfCheckIdStr);
+                com.etotem.cfc.entity.FiveDimensionSelfCheck selfCheck = selfCheckMapper.selectById(selfCheckId);
+                if (selfCheck != null) {
+                    inputs.put("self_check_result", objectMapper.writeValueAsString(selfCheck));
+                }
+            } catch (Exception e) {
+                log.warn("注入自检上下文失败: {}", e.getMessage());
+            }
+        }
+```
+
+需要新增注入(如未已有):
+
+```java
+    @Resource
+    private com.etotem.cfc.mapper.FiveDimensionSelfCheckMapper selfCheckMapper;
+
+    @Resource
+    private com.fasterxml.jackson.databind.ObjectMapper objectMapper;
+```
+
+- [ ] **步骤 2:编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+预期:BUILD SUCCESS
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java
+git commit -m "feat(self-check): AIChat sendMessage 支持 selfCheckId 注入上下文(P1-2)"
+```
+
+---
+
+## 任务 8:前端 — utils/api.js 新增方法
+
+**文件:**
+- 修改:`cfc-frontend/utils/api.js`
+
+- [ ] **步骤 1:追加 2 个 API 方法**
+
+在 `cfc-frontend/utils/api.js` 中,于 `ignoreSelfCheck` 方法之后追加:
+
+```js
+export const generateSelfCheckPlan = (data) => {
+  return request('/api/family/self-check/generate-plan', 'POST', data || {})
+}
+
+export const getSelfCheckTrendAnalysis = () => {
+  return request('/api/family/self-check/trend-analysis', 'POST', {})
+}
+```
+
+- [ ] **步骤 2:语法校验**
+
+```bash
+node --check cfc-frontend/utils/api.js && echo "api.js OK"
+```
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-frontend/utils/api.js
+git commit -m "feat(self-check): 新增 generateSelfCheckPlan/getSelfCheckTrendAnalysis API"
+```
+
+---
+
+## 任务 9:前端 — self-check-result.vue 改造(P0-1/P0-2/P1-2)
+
+**文件:**
+- 修改:`cfc-frontend/pages/family/self-check-result.vue`
+
+- [ ] **步骤 1:替换寻源建议区块(P0-1)**
+
+将第 70-131 行的 `<view class="scr-card" v-if="result.advices...">` 区块(五行相生寻源建议 + 全部健康提示)整体替换为:
+
+```html
+      <!-- AI 健康解读(P0-1) -->
+      <view class="scr-card" v-if="result.advices && result.advices.length > 0">
+        <view class="scr-card-title">
+          <text class="scr-card-title-text">AI 健康解读</text>
+          <text class="scr-card-sub">基于你的五维自检结果</text>
+        </view>
+        <view v-for="adv in result.advices" :key="adv.dimension" class="scr-advice-item">
+          <view class="scr-advice-dim-row">
+            <view class="scr-advice-dim-badge" :style="{ background: adv.color }">
+              <text class="scr-advice-dim-name">{{ adv.dimensionName }}</text>
+              <text class="scr-advice-dim-elem">{{ adv.score }}分</text>
+            </view>
+          </view>
+          <text class="scr-advice-interpretation" v-if="adv.interpretation">{{ adv.interpretation }}</text>
+          <view class="scr-advice-actions" v-if="adv.microActions && adv.microActions.length > 0">
+            <text class="scr-advice-action-label">本周行动:</text>
+            <text v-for="(action, i) in adv.microActions" :key="'ma'+i" class="scr-advice-action">{{ i+1 }}. {{ action }}</text>
+          </view>
+        </view>
+        <view class="scr-family-insight" v-if="result.advices[0] && result.advices[0].familyInsight">
+          <text class="scr-family-insight-label">💡 家庭整体洞察</text>
+          <text class="scr-family-insight-text">{{ result.advices[0].familyInsight }}</text>
+        </view>
+        <view class="scr-fallback" v-if="result.advices[0] && result.advices[0].fallbackUsed">
+          <text>AI 分析暂时不可用,请稍后再试</text>
+        </view>
+      </view>
+      <!-- 全部健康提示 -->
+      <view class="scr-card" v-else-if="result.totalScore >= 35">
+        <view class="scr-all-healthy">
+          <text class="scr-all-healthy-icon">🌿</text>
+          <text class="scr-all-healthy-text">五维状态均健康,请继续保持这份平衡!</text>
+        </view>
+      </view>
+```
+
+- [ ] **步骤 2:底部按钮区改造(P0-2 + P1-2)**
+
+将第 134-137 行的底部按钮区替换为:
+
+```html
+      <!-- 历史与操作 -->
+      <view class="scr-footer">
+        <button class="scr-btn scr-btn-primary" @click="goHistory">历史记录</button>
+        <button class="scr-btn scr-btn-outline" @click="retake">重新自检</button>
+        <button class="scr-btn scr-btn-secondary" v-if="hasLowScore" :loading="planGenerating" @click="generatePlan">
+          生成健康计划
+        </button>
+        <button class="scr-btn scr-btn-ghost" @click="askAI">问问 AI</button>
+      </view>
+```
+
+- [ ] **步骤 3:script 改造**
+
+在 `data()` 中追加 `planGenerating: false`;在 `computed` 中追加 `hasLowScore`;在 `methods` 中追加 `generatePlan` / `askAI`:
+
+```javascript
+      planGenerating: false,   // data() 内
+    // computed 内
+    hasLowScore: function() {
+      return this.result && this.result.advices && this.result.advices.some(function(a) {
+        return a.score != null && a.score <= 6
+      })
+    },
+    // methods 内
+    generatePlan: function() {
+      var self = this
+      if (this.planGenerating) return
+      this.planGenerating = true
+      generateSelfCheckPlan({ checkId: this.result && this.result.id }).then(function(res) {
+        self.planGenerating = false
+        if (res.code === 200 && res.data && res.data.planId) {
+          uni.showToast({ title: '计划已生成,可在健康计划页查看', icon: 'success' })
+          setTimeout(function() {
+            uni.navigateTo({ url: '/pages/health-main/index?planId=' + res.data.planId })
+          }, 1000)
+        } else {
+          uni.showToast({ title: (res && res.message) || '计划生成失败', icon: 'none' })
+        }
+      }).catch(function() {
+        self.planGenerating = false
+        uni.showToast({ title: '计划生成失败', icon: 'none' })
+      })
+    },
+    askAI: function() {
+      var selfCheckId = this.result && this.result.id
+      if (!selfCheckId) {
+        uni.showToast({ title: '暂无自检记录', icon: 'none' })
+        return
+      }
+      uni.navigateTo({ url: '/pages/ai/chat?selfCheckId=' + selfCheckId })
+    }
+```
+
+- [ ] **步骤 4:import 追加**
+
+在文件头部 import 区追加:
+
+```javascript
+import { generateSelfCheckPlan } from '@/utils/api'
+```
+
+- [ ] **步骤 5:样式追加**
+
+在 `<style>` 块末尾追加(复用 `.scr-btn` 模式):
+
+```css
+.scr-btn-secondary {
+  background: linear-gradient(135deg, #10B981, #34D399);
+  color: #fff;
+  font-weight: 600;
+}
+.scr-btn-ghost {
+  background: #fff;
+  color: #666;
+  border: 2rpx solid #D1D5DB;
+}
+.scr-advice-interpretation {
+  display: block;
+  font-size: 26rpx;
+  color: #555;
+  line-height: 1.6;
+  margin: 16rpx 0;
+}
+.scr-advice-actions {
+  display: flex;
+  flex-direction: column;
+  gap: 8rpx;
+  margin-top: 12rpx;
+}
+.scr-advice-action-label {
+  font-size: 24rpx;
+  font-weight: 600;
+  color: #F97316;
+}
+.scr-advice-action {
+  font-size: 24rpx;
+  color: #555;
+  line-height: 1.5;
+}
+.scr-family-insight {
+  background: #F5FAFE;
+  border-radius: 16rpx;
+  padding: 20rpx 24rpx;
+  margin-top: 24rpx;
+}
+.scr-family-insight-label {
+  display: block;
+  font-size: 24rpx;
+  font-weight: 600;
+  color: #F97316;
+  margin-bottom: 8rpx;
+}
+.scr-family-insight-text {
+  display: block;
+  font-size: 26rpx;
+  color: #555;
+  line-height: 1.6;
+}
+.scr-fallback {
+  background: #FFF7ED;
+  border-radius: 12rpx;
+  padding: 16rpx 20rpx;
+  margin-top: 16rpx;
+  text-align: center;
+}
+.scr-fallback text {
+  font-size: 24rpx;
+  color: #999;
+}
+```
+
+- [ ] **步骤 6:语法校验**
+
+提取 script 块语法校验:
+
+```bash
+node -e "
+var fs = require('fs');
+var content = fs.readFileSync('cfc-frontend/pages/family/self-check-result.vue', 'utf8');
+var m = content.match(/<script>([\s\S]*?)<\/script>/);
+if (m) { new Function(m[1]); console.log('script OK'); } else { console.log('no script'); }
+"
+```
+
+- [ ] **步骤 7:Commit**
+
+```bash
+git add cfc-frontend/pages/family/self-check-result.vue
+git commit -m "feat(self-check): 结果页改 AI 解读 + 生成计划按钮 + 问问AI(P0-1/P0-2/P1-2)"
+```
+
+---
+
+## 任务 10:前端 — self-check-entry.vue 趋势展示(P1-1)
+
+**文件:**
+- 修改:`cfc-frontend/pages/family/self-check-entry.vue`
+
+- [ ] **步骤 1:追加趋势区块**
+
+在 `self-check-entry.vue` 的状态提示(`sce-status`)之后、按钮区(`sce-footer`)之前,插入:
+
+```html
+      <!-- 趋势分析(P1-1) -->
+      <view class="sce-trend" v-if="trendLoaded">
+        <view class="sce-trend-header">
+          <text class="sce-trend-title">📈 自检趋势</text>
+          <text class="sce-trend-summary" v-if="trendSummary">{{ trendSummary }}</text>
+        </view>
+        <text class="sce-trend-insight" v-if="aiInsight">{{ aiInsight }}</text>
+        <view class="sce-trend-loading" v-if="trendLoading">
+          <text>AI 趋势分析中...</text>
+        </view>
+      </view>
+```
+
+- [ ] **步骤 2:script 追加**
+
+在 `data()` 追加 `trendLoaded: false, trendLoading: false, aiInsight: '', trendSummary: ''`;在 `onLoad` 中追加 `this.loadTrend()`;在 `methods` 追加:
+
+```javascript
+    loadTrend: function() {
+      var self = this
+      this.trendLoading = true
+      getSelfCheckTrendAnalysis().then(function(res) {
+        self.trendLoading = false
+        self.trendLoaded = true
+        if (res.code === 200 && res.data) {
+          self.aiInsight = res.data.aiInsight || ''
+          self.trendSummary = res.data.trendSummary || ''
+        }
+      }).catch(function() {
+        self.trendLoading = false
+        self.trendLoaded = true
+      })
+    }
+```
+
+- [ ] **步骤 3:import 追加**
+
+```javascript
+import { getSelfCheckTrendAnalysis } from '@/utils/api'
+```
+
+- [ ] **步骤 4:样式追加**
+
+```css
+.sce-trend {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 30rpx 26rpx;
+  margin-bottom: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+}
+.sce-trend-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12rpx;
+}
+.sce-trend-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+}
+.sce-trend-summary {
+  font-size: 24rpx;
+  color: #F97316;
+  font-weight: 600;
+}
+.sce-trend-insight {
+  display: block;
+  font-size: 26rpx;
+  color: #555;
+  line-height: 1.6;
+}
+.sce-trend-loading {
+  padding: 10rpx 0;
+}
+.sce-trend-loading text {
+  font-size: 24rpx;
+  color: #999;
+}
+```
+
+- [ ] **步骤 5:语法校验**
+
+提取 script 块校验(同任务 9 步骤 6 方法)。
+
+- [ ] **步骤 6:Commit**
+
+```bash
+git add cfc-frontend/pages/family/self-check-entry.vue
+git commit -m "feat(self-check): 中间页展示自检趋势 AI 解读(P1-1)"
+```
+
+---
+
+## 任务 11:最终验证
+
+- [ ] **步骤 1:后端编译**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+预期:BUILD SUCCESS
+
+- [ ] **步骤 2:LangGraph 语法校验**
+
+```bash
+cd cfc-langgraph && python -m py_compile app/graphs/self_check_analysis_graph.py app/graphs/self_check_trend_graph.py app/api/self_check.py app/main.py && echo "OK"
+```
+
+- [ ] **步骤 3:前端语法校验**
+
+```bash
+node --check cfc-frontend/utils/api.js && echo "api.js OK"
+```
+
+- [ ] **步骤 4:提交最终 commit(如有遗漏)**
+
+```bash
+git add -A
+git diff --cached --stat
+git commit -m "feat(self-check): 五维自检 AI 结合 P0+P1 全栈实现"
+```
+
+---
+
+## 自检清单
+
+**规格覆盖度:**
+- [x] P0-1 纯 AI 替换 → 任务 1(LangGraph graph)+ 任务 3/4/5(Java Service/AiGateway/submit)
+- [x] P0-2 用户点击生成计划 → 任务 6(Controller + HealthPlanService)+ 任务 9(前端按钮)
+- [x] P1-1 历史趋势 + AI 解读 → 任务 2(trend graph)+ 任务 6(trend-analysis)+ 任务 10(前端展示)
+- [x] P1-2 自检注入 Chat → 任务 7(AIChatController)+ 任务 9(askAI 按钮)
+
+**占位符扫描:** 无"待定"/"TODO"/"后续实现"
+
+**类型一致性:**
+- `WuxingSourcingAdviceVO` 新字段(interpretation/microActions/aiInsight/familyInsight/fallbackUsed)— 任务 3 定义,任务 5 解析、任务 9 渲染
+- `SelfCheckAnalysisService.generateAdvice/generateTrend/getRecentHistory` — 任务 3 定义,任务 5/6 调用
+- `AiGateway.generateSelfCheckAdvice/generateSelfCheckTrend` — 任务 4 定义,任务 3 调用
+- `HealthPlanService.generateFromSelfCheck` — 任务 6 接口+实现,Controller 调用
+- `generateSelfCheckPlan/getSelfCheckTrendAnalysis` — 任务 8 前端定义,任务 9/10 调用

+ 1349 - 0
docs/superpowers/plans/2026-09-01-cf-commission-redesign.md

@@ -0,0 +1,1349 @@
+# CF 值分佣体系重构 实现计划
+
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
+
+**目标:** 将全部订单的返利/分佣统一迁移到 CF 值体系(个人钱包),废弃旧佣金体系写入;返佣比例由全层级团队规模阶梯决定;支持成员间转让 CF 与家庭共享优惠券。
+
+**架构:** 新建 `CfCommissionService` 作为唯一分佣入口,按订单类型区分「双返(当前人+推荐人)」与「只返推荐人(会员/订阅)」。推荐关系以 `referral_tree`(物化路径)为全层级唯一事实源,绑定即物化并回填存量;比例由 `cf_rate_tier` 阶梯配置按 `total_team_size` 匹配。个人 CF 钱包复用 `user_platform_balance`。
+
+**技术栈:** Spring Boot 2.7.18 + MyBatis-Plus + Java 8;验证命令 `mvn clean compile`。
+
+**前置事实(实现须知):**
+- `referral_tree` 表当前**无任何写入代码**(仅 `CommissionDistService`/`EnergyService` 读取),推荐关系实际存于 `users.referrer_id`(单层)。本计划需:① `bindReferral` 时物化整条祖先链到 `referral_tree`;② 提供存量回填。
+- `PlatformPointsService.earn` **当前无幂等检查**,需补 `(ref_type, ref_id)` 去重。
+- `promotion_tier_config` 表列(`min_team_size_1st/2nd + commission_rate_l1/l2`)与实体(`minTeamSize/profitSharePercent/enabled`)**不一致**,本计划整体废弃该表与实体,改用 `cf_rate_tier`。
+- `CouponService.grant(Long userId, Long couponId, String grantType, String period, String source)` 已存在(line 195)。
+- 后端唯一验证方式:`cd cfc-backend && mvn clean compile`(无运行时验证)。
+
+**规格文档:** `docs/superpowers/specs/2026-09-01-cf-commission-redesign.md`
+
+---
+
+## 文件结构
+
+**新建后端:**
+- `cfc-backend/src/main/java/com/etotem/cfc/entity/CfRateTier.java` — 阶梯配置实体
+- `cfc-backend/src/main/java/com/etotem/cfc/mapper/CfRateTierMapper.java` — 阶梯配置 Mapper
+- `cfc-backend/src/main/java/com/etotem/cfc/entity/CfTransferRecord.java` — CF 流转记录实体
+- `cfc-backend/src/main/java/com/etotem/cfc/mapper/CfTransferRecordMapper.java` — 流转记录 Mapper
+- `cfc-backend/src/main/java/com/etotem/cfc/service/CfCommissionService.java` — 统一分佣核心
+- `cfc-backend/src/main/java/com/etotem/cfc/service/CfReferralService.java` — 推荐树物化 + 团队规模统计 + 转让
+- `cfc-backend/src/main/java/com/etotem/cfc/controller/CfCommissionController.java` — 用户查询接口
+- `cfc-backend/src/main/java/com/etotem/cfc/controller/CfTransferController.java` — CF 转让接口
+- `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCfRateTierController.java` — 管理端阶梯配置
+
+**修改后端:**
+- `service/CommissionService.java` — `bindReferral` 物化推荐树;`settle/settleTwoLevel` 标 `@Deprecated`(调用点摘除后)
+- `service/PlatformPointsService.java` — `earn` 补幂等
+- `service/PromotionTierService.java` — 删除 3 层字段逻辑,加 `refreshRate`
+- `entity/PromotionTier.java` / `entity/PromotionTierConfig.java` — 精简/废弃
+- `service/PromotionTierEvalService.java` — 改读 `cf_rate_tier`
+- `task/PromotionTierCheckScheduledTask.java` — 改调 `refreshRate`
+- `service/AssessmentOrderService.java` / `PackagePaymentService.java` / `PaymentService.java` / `MembershipService.java` / `MemberSubscriptionService.java` / `ProductOrderService.java` — 迁移结算调用点
+- `service/CouponService.java` / `service/FamilyPlatformPointsService.java` — 券挂 `family_id`
+- `config/DatabaseInitializer.java` — 迁移脚本
+- `resources/schema.sql` — 同步 DDL
+
+**新建前端(小程序 `cfc-frontend/`):**
+- `pages/promotion/team.vue`(修改)— 展示全层级团队规模 + 返佣比例
+- `pages/promotion/commission.vue`(修改)— CF 分润流水
+- `pages/promotion/index.vue`(修改)— 个人 CF 钱包汇总
+- `pages/profile-extra/*`(修改)— 家庭券库 + CF 转让入口
+- `utils/api.js`(修改)— 新增接口封装
+
+---
+
+### 任务 1:数据库迁移(DatabaseInitializer + schema.sql)
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`
+- 修改:`cfc-backend/src/main/resources/schema.sql`
+
+- [ ] **步骤 1:确认最新迁移编号**
+
+运行:`grep -n "// 迁移" cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java | tail -5`
+预期:记录最大编号 `迁移N`(当前应 ≥113)。后续迁移编号从 N+1 递增。
+
+- [ ] **步骤 2:在 `runMigrations()` 末尾追加 cf_rate_tier 建表+种子**
+
+```java
+// 迁移N+1: 创建 cf_rate_tier 表(CF值返佣阶梯配置,替代 promotion_tier_config)
+try {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS cf_rate_tier (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "tier_name VARCHAR(50) NOT NULL COMMENT '档位名称', " +
+            "min_team_size INT NOT NULL DEFAULT 0 COMMENT '团队规模下限(含)', " +
+            "rate_percent INT NOT NULL DEFAULT 0 COMMENT '返佣比例(%)', " +
+            "sort_order INT NOT NULL DEFAULT 0 COMMENT '排序,越大越高', " +
+            "enabled TINYINT DEFAULT 1 COMMENT '1启用/0停用', " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+            "INDEX idx_min_size (min_team_size)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值返佣阶梯配置'");
+    log.info("已创建cf_rate_tier表");
+    Integer cnt = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM cf_rate_tier", Integer.class);
+    if (cnt == null || cnt == 0) {
+        jdbcTemplate.execute("INSERT INTO cf_rate_tier (tier_name, min_team_size, rate_percent, sort_order) VALUES " +
+                "('铜牌', 0, 5, 1), ('银牌', 3, 10, 2), ('金牌', 10, 15, 3), ('铂金', 30, 20, 4), ('钻石', 100, 25, 5)");
+        log.info("cf_rate_tier 种子数据已初始化");
+    }
+} catch (Exception e) {
+    log.warn("创建cf_rate_tier表失败: {}", e.getMessage());
+}
+```
+
+- [ ] **步骤 3:追加 cf_transfer_record 建表**
+
+```java
+// 迁移N+2: 创建 cf_transfer_record 表(CF值流转记录)
+try {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS cf_transfer_record (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "from_user_id BIGINT COMMENT '转出用户ID(null=平台)', " +
+            "to_user_id BIGINT COMMENT '转入用户ID', " +
+            "family_id BIGINT COMMENT '所属家庭ID', " +
+            "amount INT NOT NULL COMMENT 'CF值数量', " +
+            "type VARCHAR(16) NOT NULL COMMENT 'transfer/allocate/refund', " +
+            "ref_type VARCHAR(50) COMMENT '关联业务类型', " +
+            "ref_id BIGINT COMMENT '关联业务ID', " +
+            "remark VARCHAR(255), " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "INDEX idx_from (from_user_id), " +
+            "INDEX idx_to (to_user_id), " +
+            "INDEX idx_family (family_id)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值流转记录'");
+    log.info("已创建cf_transfer_record表");
+} catch (Exception e) {
+    log.warn("创建cf_transfer_record表失败: {}", e.getMessage());
+}
+```
+
+- [ ] **步骤 4:promotion_tier 表精简 + coupon 加 family_id**
+
+```java
+// 迁移N+3: promotion_tier 精简列(删三层,加 rate_percent)
+try {
+    jdbcTemplate.execute("ALTER TABLE promotion_tier DROP COLUMN team_size_1st, DROP COLUMN team_size_2nd, DROP COLUMN team_size_3rd");
+    log.info("promotion_tier 已删除三层团队列");
+} catch (Exception e) {
+    log.warn("promotion_tier 三层列删除失败(可能不存在): {}", e.getMessage());
+}
+try {
+    jdbcTemplate.execute("ALTER TABLE promotion_tier ADD COLUMN rate_percent INT DEFAULT 0 COMMENT '当前返佣比例(%)'");
+    log.info("promotion_tier 已添加 rate_percent");
+} catch (Exception e) {
+    log.warn("promotion_tier.rate_percent 已存在: {}", e.getMessage());
+}
+
+// 迁移N+4: coupon 表添加 family_id
+try {
+    jdbcTemplate.execute("ALTER TABLE coupon ADD COLUMN family_id BIGINT DEFAULT NULL COMMENT '绑定家庭ID(NULL=不绑定)'");
+    log.info("coupon 已添加 family_id");
+} catch (Exception e) {
+    log.warn("coupon.family_id 已存在: {}", e.getMessage());
+}
+```
+
+- [ ] **步骤 5:同步 schema.sql**
+
+在 `schema.sql` 末尾追加三张新表的 `CREATE TABLE IF NOT EXISTS`(与任务 1 步骤 2/3 SQL 一致);修改 `promotion_tier` 的 CREATE TABLE 去掉 `team_size_1st/2nd/3rd` 并加 `rate_percent`;`coupon` 的 CREATE TABLE 加 `family_id` 列。保持 schema.sql 为完整快照。
+
+- [ ] **步骤 6:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 7:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java cfc-backend/src/main/resources/schema.sql
+git commit -m "feat(cf): 数据库迁移:cf_rate_tier/cf_transfer_record 建表 + promotion_tier 精简 + coupon 加 family_id"
+```
+
+---
+
+### 任务 2:`CfRateTier` / `CfTransferRecord` 实体与 Mapper
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/CfRateTier.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/CfRateTierMapper.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/CfTransferRecord.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/CfTransferRecordMapper.java`
+
+- [ ] **步骤 1:创建 `CfRateTier` 实体**
+
+```java
+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.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("cf_rate_tier")
+public class CfRateTier implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String tierName;
+    private Integer minTeamSize;
+    private Integer ratePercent;
+    private Integer sortOrder;
+    private Integer enabled;
+    private Date createdAt;
+    private Date updatedAt;
+}
+```
+
+- [ ] **步骤 2:创建 `CfRateTierMapper`**
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.CfRateTier;
+
+public interface CfRateTierMapper extends BaseMapper<CfRateTier> {
+}
+```
+
+- [ ] **步骤 3:创建 `CfTransferRecord` 实体**
+
+```java
+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.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("cf_transfer_record")
+public class CfTransferRecord implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long fromUserId;
+    private Long toUserId;
+    private Long familyId;
+    private Integer amount;
+    private String type;      // transfer/allocate/refund
+    private String refType;
+    private Long refId;
+    private String remark;
+    private Date createdAt;
+}
+```
+
+- [ ] **步骤 4:创建 `CfTransferRecordMapper`**
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.CfTransferRecord;
+
+public interface CfTransferRecordMapper extends BaseMapper<CfTransferRecord> {
+}
+```
+
+- [ ] **步骤 5:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 6:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/CfRateTier.java cfc-backend/src/main/java/com/etotem/cfc/mapper/CfRateTierMapper.java cfc-backend/src/main/java/com/etotem/cfc/entity/CfTransferRecord.java cfc-backend/src/main/java/com/etotem/cfc/mapper/CfTransferRecordMapper.java
+git commit -m "feat(cf): 新增 cf_rate_tier 与 cf_transfer_record 实体/Mapper"
+```
+
+---
+
+### 任务 3:`PlatformPointsService.earn` 补幂等
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/PlatformPointsService.java`
+
+- [ ] **步骤 1:为 `earn` 增加幂等检查**
+
+在 `PlatformPointsService.earn`(当前 line 73-84)开头,`amount <= 0` 校验之后、余额变更之前插入:
+
+```java
+// 幂等:按 (userId, ref_type, ref_id) 去重
+if (refId != null) {
+    Long existing = logMapper.selectCount(
+            new LambdaQueryWrapper<PlatformBalanceLog>()
+                    .eq(PlatformBalanceLog::getUserId, userId)
+                    .eq(PlatformBalanceLog::getRefType, refType)
+                    .eq(PlatformBalanceLog::getRefId, refId));
+    if (existing != null && existing > 0) {
+        return;
+    }
+}
+```
+
+确认文件已 import `PlatformBalanceLog` 与 `LambdaQueryWrapper`(`PlatformBalanceLog` 当前未 import,需补 `import com.etotem.cfc.entity.PlatformBalanceLog;`;`LambdaQueryWrapper` 已在 line 3 存在)。
+
+- [ ] **步骤 2:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/PlatformPointsService.java
+git commit -m "feat(cf): PlatformPointsService.earn 增加 (ref_type, ref_id) 幂等去重"
+```
+
+---
+
+### 任务 4:`CfReferralService` — 推荐树物化 + 团队规模统计
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/service/CfReferralService.java`
+
+**背景:** `referral_tree` 表当前无写入。本任务实现:绑定推荐时物化整条祖先链;全层级团队规模统计;按团队规模匹配阶梯比例。
+
+- [ ] **步骤 1:创建 `CfReferralService`**
+
+```java
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.CfRateTier;
+import com.etotem.cfc.entity.ReferralTree;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.CfRateTierMapper;
+import com.etotem.cfc.mapper.ReferralTreeMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 推荐关系物化与团队规模统计(全层级)
+ */
+@Service
+public class CfReferralService {
+
+    @Resource
+    private ReferralTreeMapper referralTreeMapper;
+    @Resource
+    private UserMapper userMapper;
+    @Resource
+    private CfRateTierMapper cfRateTierMapper;
+
+    /**
+     * 绑定推荐关系时物化整条祖先链(含本人 L1)到 referral_tree。
+     * 调用方:CommissionService.bindReferral() 设置 user.referrerId 之后。
+     */
+    @Transactional
+    public void materializeReferralTree(Long childId) {
+        // 防止重复物化
+        Long cnt = referralTreeMapper.selectCount(
+                new LambdaQueryWrapper<ReferralTree>().eq(ReferralTree::getChildId, childId));
+        if (cnt != null && cnt > 0) {
+            return;
+        }
+        // 沿 referrerId 链收集祖先(child 本人算 level 0)
+        List<Long> chain = new ArrayList<>();
+        User cur = userMapper.selectById(childId);
+        while (cur != null && cur.getReferrerId() != null && !chain.contains(cur.getReferrerId())) {
+            chain.add(cur.getReferrerId());
+            cur = userMapper.selectById(cur.getReferrerId());
+        }
+        // 写 referral_tree:level = 1..n,path = 从根到该祖先
+        // path 格式: /ancestorId/.../directReferrerId/
+        StringBuilder path = new StringBuilder("/");
+        for (int i = chain.size() - 1; i >= 0; i--) {
+            Long parentId = chain.get(i);
+            path.append(parentId).append("/");
+            ReferralTree node = new ReferralTree();
+            node.setParentId(parentId);
+            node.setChildId(childId);
+            node.setLevel(chain.size() - i);
+            node.setPath(path.toString());
+            node.setCreatedAt(new Date());
+            referralTreeMapper.insert(node);
+        }
+    }
+
+    /**
+     * 全层级团队总人数(所有下线,含间接)。
+     * 统计 referral_tree 中 parent_id = userId 的所有记录数。
+     */
+    public int getTotalTeamSize(Long userId) {
+        Long cnt = referralTreeMapper.selectCount(
+                new LambdaQueryWrapper<ReferralTree>().eq(ReferralTree::getParentId, userId));
+        return cnt == null ? 0 : cnt.intValue();
+    }
+
+    /**
+     * 按团队规模匹配阶梯比例:cf_rate_tier WHERE enabled=1 AND min_team_size <= size ORDER BY min_team_size DESC LIMIT 1
+     * 无匹配返回 0。
+     */
+    public CfRateTier matchRateTier(int teamSize) {
+        List<CfRateTier> tiers = cfRateTierMapper.selectList(
+                new LambdaQueryWrapper<CfRateTier>()
+                        .eq(CfRateTier::getEnabled, 1)
+                        .le(CfRateTier::getMinTeamSize, teamSize)
+                        .orderByDesc(CfRateTier::getMinTeamSize)
+                        .last("LIMIT 1"));
+        return tiers.isEmpty() ? null : tiers.get(0);
+    }
+}
+```
+
+- [ ] **步骤 2:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/CfReferralService.java
+git commit -m "feat(cf): CfReferralService 推荐树物化 + 全层级团队规模 + 阶梯比例匹配"
+```
+
+---
+
+### 任务 5:`CfCommissionService` — 统一分佣核心
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/service/CfCommissionService.java`
+
+- [ ] **步骤 1:创建 `CfCommissionService`**
+
+```java
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.CfRateTier;
+import com.etotem.cfc.entity.CfTransferRecord;
+import com.etotem.cfc.entity.ReferralTree;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.CfTransferRecordMapper;
+import com.etotem.cfc.mapper.ReferralTreeMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 统一 CF 值分佣服务。
+ * 规则:
+ *  - 普通订单(商品/套餐/测评):当前消费人返 CF + 推荐人按团队规模阶梯比例分润
+ *  - 会员/订阅订单:只返推荐人,不返当前人
+ *  - 同家庭互推:跳过本人,上溯到第一个非同家庭引荐人
+ */
+@Service
+public class CfCommissionService {
+
+    private static final Logger log = LoggerFactory.getLogger(CfCommissionService.class);
+
+    @Resource
+    private ReferralTreeMapper referralTreeMapper;
+    @Resource
+    private UserMapper userMapper;
+    @Resource
+    private CfReferralService cfReferralService;
+    @Resource
+    private PlatformPointsService platformPointsService;
+    @Resource
+    private CfTransferRecordMapper cfTransferRecordMapper;
+    @Resource
+    private PpointConfigService ppointConfigService;
+    @Resource
+    private SysConfigService sysConfigService;
+
+    /**
+     * 通用分佣(双返):当前人 + 推荐人
+     */
+    @Transactional
+    public void settle(Long orderId, String orderType, Long buyerUserId, Long buyerFamilyId,
+                       Integer orderAmountCent, Long productId) {
+        // 当前消费人返 CF(个人钱包)
+        int buyerReturn = calcBuyerReturn(orderType, orderAmountCent, productId, buyerUserId);
+        if (buyerReturn > 0) {
+            try {
+                platformPointsService.earn(buyerUserId, buyerReturn, "order_consume", orderId,
+                        "消费返CF:" + orderType);
+                record(buyerUserId, null, null, buyerReturn, "allocate", "order_consume", orderId, "消费返CF");
+            } catch (Exception e) {
+                log.error("当前人返CF失败: buyer={}, orderId={}, err={}", buyerUserId, orderId, e.getMessage());
+            }
+        }
+        // 推荐人分润
+        distributeToReferrers(orderId, orderType, buyerUserId, buyerFamilyId, orderAmountCent, productId);
+    }
+
+    /**
+     * 会员/订阅专用:只返推荐人
+     */
+    @Transactional
+    public void settleReferrerOnly(Long orderId, String orderType, Long buyerUserId, Long buyerFamilyId,
+                                   Integer orderAmountCent) {
+        distributeToReferrers(orderId, orderType, buyerUserId, buyerFamilyId, orderAmountCent, null);
+    }
+
+    /**
+     * 计算当前消费人返 CF。
+     * 商品:floor(orderAmount/100) × P点 × shareBps/10000
+     * 非商品:floor(orderAmount/100) × serviceRateBps/10000
+     */
+    private int calcBuyerReturn(String orderType, Integer orderAmountCent, Long productId, Long buyerUserId) {
+        if (orderAmountCent == null || orderAmountCent <= 0) return 0;
+        int amountYuan = orderAmountCent / 100;
+        if ("product".equals(orderType) && productId != null) {
+            try {
+                int effectivePpoint = ppointConfigService.getEffectivePpoint(productId,
+                        /* categoryId */ null == null ? null : null);
+                if (effectivePpoint <= 0) return 0;
+                int shareBps = getSysBps("product_platform_points_share", 1000);
+                return amountYuan * effectivePpoint / 100 * shareBps / 10000;
+            } catch (Exception e) {
+                return 0;
+            }
+        }
+        int serviceRateBps = getSysBps("commission_service_rate", 1000);
+        return amountYuan * serviceRateBps / 10000;
+    }
+
+    /**
+     * 推荐人分润:查全链路 → 同家庭跳过上溯 → 按阶梯比例
+     */
+    private void distributeToReferrers(Long orderId, String orderType, Long buyerUserId, Long buyerFamilyId,
+                                       Integer orderAmountCent, Long productId) {
+        if (orderAmountCent == null || orderAmountCent <= 0) return;
+        int amountYuan = orderAmountCent / 100;
+
+        // 取买家全链路推荐人(referral_tree,含全部层级)
+        List<ReferralTree> referrals = referralTreeMapper.selectList(
+                new LambdaQueryWrapper<ReferralTree>()
+                        .eq(ReferralTree::getChildId, buyerUserId)
+                        .orderByAsc(ReferralTree::getLevel));
+        if (referrals == null || referrals.isEmpty()) return;
+
+        List<Long> processed = new ArrayList<>();
+        for (ReferralTree ref : referrals) {
+            Long referrerId = ref.getParentId();
+            if (processed.contains(referrerId)) continue;
+            processed.add(referrerId);
+
+            User referrer = userMapper.selectById(referrerId);
+            if (referrer == null) continue;
+            // 同家庭跳过(D5):不返,继续上溯(循环继续)
+            if (buyerFamilyId != null && buyerFamilyId.equals(referrer.getFamilyId())) {
+                continue;
+            }
+            // 按团队规模匹配阶梯比例
+            int teamSize = cfReferralService.getTotalTeamSize(referrerId);
+            CfRateTier tier = cfReferralService.matchRateTier(teamSize);
+            int ratePercent = tier == null ? 0 : tier.getRatePercent();
+            if (ratePercent <= 0) continue;
+
+            // 分润基数:商品按 P点,非商品按服务费率
+            int base = calcReferrerBase(orderType, amountYuan, productId);
+            if (base <= 0) continue;
+            int share = base * ratePercent / 100;
+            if (share <= 0) continue;
+
+            try {
+                platformPointsService.earn(referrerId, share, "referral_dist", orderId,
+                        "推荐分润:" + orderType);
+                record(referrerId, null, referrer.getFamilyId(), share, "allocate", "referral_dist", orderId, "推荐分润");
+            } catch (Exception e) {
+                log.error("推荐人分润失败: referrer={}, orderId={}, err={}", referrerId, orderId, e.getMessage());
+            }
+        }
+    }
+
+    private int calcReferrerBase(String orderType, int amountYuan, Long productId) {
+        if ("product".equals(orderType) && productId != null) {
+            try {
+                int effectivePpoint = ppointConfigService.getEffectivePpoint(productId, null);
+                return effectivePpoint;
+            } catch (Exception e) {
+                return 0;
+            }
+        }
+        int serviceRateBps = getSysBps("commission_service_rate", 1000);
+        return amountYuan * serviceRateBps / 10000;
+    }
+
+    private int getSysBps(String key, int def) {
+        try {
+            String v = sysConfigService.getValue(key);
+            if (v != null && !v.isEmpty()) return Integer.parseInt(v);
+        } catch (Exception ignored) {}
+        return def;
+    }
+
+    private void record(Long fromUserId, Long toUserId, Long familyId, int amount, String type,
+                        String refType, Long refId, String remark) {
+        CfTransferRecord r = new CfTransferRecord();
+        r.setFromUserId(fromUserId);
+        r.setToUserId(toUserId);
+        r.setFamilyId(familyId);
+        r.setAmount(amount);
+        r.setType(type);
+        r.setRefType(refType);
+        r.setRefId(refId);
+        r.setRemark(remark);
+        r.setCreatedAt(new Date());
+        try { cfTransferRecordMapper.insert(r); } catch (Exception e) { log.warn("写流转记录失败: {}", e.getMessage()); }
+    }
+}
+```
+
+> **注意:** 步骤 1 中 `calcBuyerReturn` 内有一处明显的占位式写法(`getEffectivePpoint(productId, null)` 参数猜测),实现时需核对 `PpointConfigService.getEffectivePpoint` 的真实签名(参考 `CommissionDistService` 现有调用 `ppointConfigService.getEffectivePpoint(productId, product.getCategoryId())`),修正为实际签名。这是实现者必须确认的点。
+
+- [ ] **步骤 2:核对 `PpointConfigService.getEffectivePpoint` 签名**
+
+运行:`grep -n "getEffectivePpoint" cfc-backend/src/main/java/com/etotem/cfc/service/PpointConfigService.java`
+预期:确认参数签名,修正 `CfCommissionService` 中所有调用点为真实签名(参考 `CommissionDistService:72` 与 `ProductOrderService:626` 的现有用法)。
+
+- [ ] **步骤 3:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 4:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/CfCommissionService.java
+git commit -m "feat(cf): CfCommissionService 统一分佣核心(双返/只返推荐人/同家庭上溯/阶梯比例)"
+```
+
+---
+
+### 任务 6:`CommissionService.bindReferral` 物化推荐树 + 团队规模更新
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java`(line 108-141 `bindReferral`)
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/PromotionTierService.java`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/entity/PromotionTier.java`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/entity/PromotionTierConfig.java`
+
+- [ ] **步骤 1:`bindReferral` 末尾调用物化**
+
+在 `CommissionService.bindReferral()`(当前 `userMapper.updateById(user)` 与 `onboardingService.completeTask` 之间)插入:
+
+```java
+// 物化推荐树(全层级)
+try { cfReferralService.materializeReferralTree(userId); } catch (Exception e) { log.warn("推荐树物化失败 userId={}", userId, e); }
+```
+
+在类中新增注入:
+
+```java
+@Resource
+private CfReferralService cfReferralService;
+```
+
+- [ ] **步骤 2:`PromotionTier` 实体精简**
+
+删除 `teamSize1st/teamSize2nd/teamSize3rd` 三个字段(含 `@TableField` 注解),新增:
+
+```java
+private Integer ratePercent;
+```
+
+- [ ] **步骤 3:`PromotionTierService` 重写 `updateTeamSize` + 新增 `refreshRate`**
+
+将 `updateTeamSize(Long userId, int level, int delta)` 替换为:
+
+```java
+/**
+ * 团队规模 +delta(新推荐绑定后对所有祖先增量)。level 参数已废弃(改为全层级)。
+ */
+public void updateTeamSize(Long userId, int delta) {
+    PromotionTier tier = getCurrentTier(userId);
+    if (tier == null) {
+        tier = new PromotionTier();
+        tier.setUserId(userId);
+        tier.setTier("R0");
+        tier.setTotalTeamSize(0);
+        tier.setTotalReferralEarnings(0);
+        tier.setTotalShareEarnings(0);
+        tier.setCreatedAt(new Date());
+        tier.setUpdatedAt(new Date());
+        tierMapper.insert(tier);
+    }
+    tier.setTotalTeamSize((tier.getTotalTeamSize() == null ? 0 : tier.getTotalTeamSize()) + delta);
+    tier.setLastChangeAt(new Date());
+    tier.setUpdatedAt(new Date());
+    tierMapper.updateById(tier);
+    refreshRate(userId);
+}
+
+/**
+ * 按全层级团队规模刷新等级与返佣比例(读 cf_rate_tier)
+ */
+public void refreshRate(Long userId) {
+    try {
+        int teamSize = cfReferralService.getTotalTeamSize(userId);
+        CfRateTier tier = cfReferralService.matchRateTier(teamSize);
+        PromotionTier pt = getCurrentTier(userId);
+        if (pt == null) return;
+        if (tier != null) {
+            pt.setTier(tier.getTierName());
+            pt.setRatePercent(tier.getRatePercent());
+        } else {
+            pt.setRatePercent(0);
+        }
+        pt.setUpdatedAt(new Date());
+        tierMapper.updateById(pt);
+    } catch (Exception e) {
+        log.warn("refreshRate失败 userId={}", userId, e);
+    }
+}
+```
+
+需要注入 `CfReferralService cfReferralService` 与 `CfRateTier` import。同时删除 `addReferralEarnings/addShareEarnings` 中对 `teamSize1st/2nd/3rd` 的依赖(现有实现未用它们,可直接保留,但确认无编译错误)。
+
+- [ ] **步骤 4:`PromotionTierEvalService` 改读 cf_rate_tier**
+
+将 `evaluateTier` 方法体替换为:
+
+```java
+public String evaluateTier(Long userId) {
+    try {
+        int teamSize = cfReferralService.getTotalTeamSize(userId);
+        CfRateTier tier = cfReferralService.matchRateTier(teamSize);
+        return tier == null ? "R0" : tier.getTierName();
+    } catch (Exception e) {
+        return "R0";
+    }
+}
+```
+
+删除对 `promotion_tier_config` / `PromotionTierConfig` / `configMapper` 的依赖(或保留但不再查询)。`PromotionTierConfig` 实体标记 `@Deprecated` 并在类上加注释"已废弃,改用 cf_rate_tier"。
+
+- [ ] **步骤 5:`PromotionTierCheckScheduledTask` 改调 `refreshRate`**
+
+将定时任务改为批量 `refreshRate`(替代 `evaluateAllUsers`):
+
+```java
+@Scheduled(cron = "0 0 1 * * ?")
+public void checkAndUpdateAllTiers() {
+    log.info("开始执行推广等级定时刷新任务");
+    try {
+        List<PromotionTier> allTiers = promotionTierMapper.selectList(null);
+        for (PromotionTier pt : allTiers) {
+            try { promotionTierService.refreshRate(pt.getUserId()); } catch (Exception e) { /* 单用户失败不影响 */ }
+        }
+        log.info("推广等级定时刷新完成,共{}个用户", allTiers.size());
+    } catch (Exception e) {
+        log.error("推广等级定时刷新失败", e);
+    }
+}
+```
+
+需要注入 `PromotionTierMapper`。
+
+- [ ] **步骤 6:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS(若 `CommissionDistService` 或 `EnergyService` 引用了已删除字段需一并修正)
+
+- [ ] **步骤 7:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java cfc-backend/src/main/java/com/etotem/cfc/service/PromotionTierService.java cfc-backend/src/main/java/com/etotem/cfc/entity/PromotionTier.java cfc-backend/src/main/java/com/etotem/cfc/entity/PromotionTierConfig.java cfc-backend/src/main/java/com/etotem/cfc/service/PromotionTierEvalService.java cfc-backend/src/main/java/com/etotem/cfc/task/PromotionTierCheckScheduledTask.java
+git commit -m "feat(cf): 推荐绑定物化推荐树 + 团队规模全层级统计 + 阶梯比例刷新(替代 promotion_tier_config)"
+```
+
+---
+
+### 任务 7:订单结算迁移点切换(7 处)
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentOrderService.java:76`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/PackagePaymentService.java:220`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/PaymentService.java:240`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java:784`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/MemberSubscriptionService.java:174,255`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java:633-648`
+
+- [ ] **步骤 1:测评订单迁移(AssessmentOrderService:76)**
+
+将 `commissionService.settle(order.getId(), "assessment", order.getUserId(), ..., null)` 替换为:
+
+```java
+cfCommissionService.settle(order.getId(), "assessment", order.getUserId(),
+        userMapper.selectById(order.getUserId()).getFamilyId(),
+        order.getActualPrice() != null ? order.getActualPrice().intValue() : 0, null);
+```
+
+注入 `CfCommissionService cfCommissionService` 与 `UserMapper userMapper`(如已存在则复用)。注意 `orderType` 传 `"assessment"`。
+
+- [ ] **步骤 2:套餐订单迁移(PackagePaymentService:220 + PaymentService:240)**
+
+将 `commissionService.settle(..., "package", ...)` 替换为:
+
+```java
+cfCommissionService.settle(order.getId(), "package", order.getUserId(),
+        order.getFamilyId(), order.getPrice(), null);
+```
+
+若 `order` 无 `getFamilyId()`,改用 `userMapper.selectById(order.getUserId()).getFamilyId()`。`orderType` 传 `"package"`。
+
+> **注意:** `PackagePaymentService:220` 与 `PaymentService:240` 都处理 package 结算。因 `CfCommissionService` 依赖 `platformPointsService.earn` 的 `(ref_type, ref_id)` 幂等(任务 3 已实现),重复调用会自动跳过,无需额外去重逻辑。
+
+- [ ] **步骤 3:会员订单迁移(MembershipService:784)**
+
+将 `commissionService.settleTwoLevel(..., "membership", adminUserId, ...)` 替换为:
+
+```java
+cfCommissionService.settleReferrerOnly(order.getId(), "membership", adminUserId,
+        userMapper.selectById(adminUserId).getFamilyId(), order.getAmount());
+```
+
+注入 `CfCommissionService`。`orderType` 传 `"membership"`。
+
+- [ ] **步骤 4:订阅订单迁移(MemberSubscriptionService:174,255)**
+
+两处 `commissionService.settleTwoLevel(..., "subscription", family.getCreatorId(), amount, null)` 替换为:
+
+```java
+cfCommissionService.settleReferrerOnly(order.getId(), "subscription", family.getCreatorId(),
+        family.getId(), amount);
+```
+
+注入 `CfCommissionService`。`orderType` 传 `"subscription"`。
+
+- [ ] **步骤 5:商品订单统一入口(ProductOrderService:633-648)**
+
+将 `:633` 个人 CF 返现块与 `:645` 推荐人分润块**整体替换**为单次 `cfCommissionService.settle(...)` 调用:
+
+```java
+// CF值:统一分佣(当前人 + 推荐人)
+try {
+    cfCommissionService.settle(order.getId(), "product", order.getBuyerId(),
+            userMapper.selectById(order.getBuyerId()).getFamilyId(),
+            order.getTotalAmount(), order.getProductId());
+} catch (Exception e) {
+    log.error("CF值分佣失败: orderId={}, error={}", order.getId(), e.getMessage());
+}
+```
+
+删除原 `:622-641` 的 `platformPointsService.earn(...)` 块与 `:643-648` 的 `commissionDistService.distribute(...)` 块。保留 `:827 confirmReceive` 家庭池返 CF(任务外,不改)。
+
+- [ ] **步骤 6:活动订单确认**
+
+确认 `ActivityOrderService` 无结算调用(现有即无),无需改动。
+
+- [ ] **步骤 7:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS(需处理旧 `commissionService` 字段可能变为未使用——若不再被引用可保留注入或移除)
+
+- [ ] **步骤 8:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentOrderService.java cfc-backend/src/main/java/com/etotem/cfc/service/PackagePaymentService.java cfc-backend/src/main/java/com/etotem/cfc/service/PaymentService.java cfc-backend/src/main/java/com/etotem/cfc/service/MembershipService.java cfc-backend/src/main/java/com/etotem/cfc/service/MemberSubscriptionService.java cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java
+git commit -m "feat(cf): 全部订单结算迁移到 CfCommissionService(统一 CF 值分佣),废弃旧佣金调用"
+```
+
+---
+
+### 任务 8:旧佣金体系废弃标记 + 清理
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java`
+
+- [ ] **步骤 1:确认无残留调用点**
+
+运行:`grep -rn "commissionService.settle\|commissionService.settleTwoLevel" cfc-backend/src/main/java/com/etotem/cfc`
+预期:仅 `CommissionService.java` 内部定义(`settle` → `settleTwoLevel` 委托),无其他业务调用。
+
+- [ ] **步骤 2:`settle`/`settleTwoLevel` 加废弃注释**
+
+确认 `settle`(line 180)与 `settleTwoLevel`(line 194)已有 `@Deprecated` 注解。在方法 javadoc 补充:
+
+```java
+/**
+ * 已废弃(CF值分佣替代)。保留历史数据读取与兼容旧调用方,不再被业务订单调用。
+ */
+```
+
+- [ ] **步骤 3:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 4:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/CommissionService.java
+git commit -m "chore(cf): 旧佣金 settle/settleTwoLevel 确认废弃(无业务调用方)"
+```
+
+---
+
+### 任务 9:用户查询接口(团队规模 + 返佣比例 + CF 流水)
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/controller/CfCommissionController.java`
+
+- [ ] **步骤 1:创建 `CfCommissionController`**
+
+```java
+package com.etotem.cfc.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.CfRateTier;
+import com.etotem.cfc.entity.PlatformBalanceLog;
+import com.etotem.cfc.service.CfReferralService;
+import com.etotem.cfc.service.PlatformPointsService;
+import com.etotem.cfc.service.PromotionTierService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/commission/cf")
+public class CfCommissionController {
+
+    @Resource
+    private CfReferralService cfReferralService;
+    @Resource
+    private PlatformPointsService platformPointsService;
+    @Resource
+    private PromotionTierService promotionTierService;
+
+    /** 我的团队规模 + 当前返佣比例 + 档位名 */
+    @PostMapping("/rate")
+    public Result<Map<String, Object>> rate(@RequestAttribute("userId") Long userId) {
+        int teamSize = cfReferralService.getTotalTeamSize(userId);
+        CfRateTier tier = cfReferralService.matchRateTier(teamSize);
+        Map<String, Object> data = new HashMap<>();
+        data.put("totalTeamSize", teamSize);
+        data.put("ratePercent", tier == null ? 0 : tier.getRatePercent());
+        data.put("tierName", tier == null ? "未入档" : tier.getTierName());
+        return Result.success(data);
+    }
+
+    /** 我的 CF 钱包汇总 */
+    @PostMapping("/summary")
+    public Result<Map<String, Object>> summary(@RequestAttribute("userId") Long userId) {
+        return Result.success(platformPointsService.getBalance(userId));
+    }
+
+    /** CF 流水(分页) */
+    @PostMapping("/list")
+    public Result<Page<PlatformBalanceLog>> list(@RequestAttribute("userId") Long userId,
+                                                 @RequestBody Map<String, Integer> params) {
+        int page = params.getOrDefault("page", 1);
+        int size = params.getOrDefault("size", 20);
+        return Result.success(platformPointsService.getLogs(userId, page, size));
+    }
+}
+```
+
+- [ ] **步骤 2:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/CfCommissionController.java
+git commit -m "feat(cf): 用户查询接口(团队规模/返佣比例/CF汇总/流水)"
+```
+
+---
+
+### 任务 10:CF 转让接口
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/controller/CfTransferController.java`
+
+- [ ] **步骤 1:创建 `CfTransferController`**
+
+```java
+package com.etotem.cfc.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.CfTransferRecord;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.CfTransferRecordMapper;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.PlatformPointsService;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/cf/transfer")
+public class CfTransferController {
+
+    @Resource
+    private PlatformPointsService platformPointsService;
+    @Resource
+    private CfTransferRecordMapper cfTransferRecordMapper;
+    @Resource
+    private UserMapper userMapper;
+
+    /** 成员间转让 CF(需同家庭) */
+    @PostMapping("/send")
+    @Transactional
+    public Result<String> send(@RequestAttribute("userId") Long fromUserId,
+                               @RequestBody Map<String, Object> params) {
+        Long toUserId = ((Number) params.get("toUserId")).longValue();
+        int amount = ((Number) params.get("amount")).intValue();
+        if (amount <= 0) {
+            return Result.error("CF值必须为正数");
+        }
+        User from = userMapper.selectById(fromUserId);
+        User to = userMapper.selectById(toUserId);
+        if (from == null || to == null) {
+            return Result.error("用户不存在");
+        }
+        if (from.getFamilyId() == null || !from.getFamilyId().equals(to.getFamilyId())) {
+            return Result.error("仅限同一家庭成员间转让");
+        }
+        // 转出扣减 + 转入增加(同一事务)
+        platformPointsService.spend(fromUserId, amount, "cf_transfer_out", fromUserId,
+                "转给成员: " + (to.getNickname() == null ? toUserId : to.getNickname()));
+        platformPointsService.earn(toUserId, amount, "cf_transfer_in", fromUserId,
+                "收到成员转让: " + (from.getNickname() == null ? fromUserId : from.getNickname()));
+
+        CfTransferRecord record = new CfTransferRecord();
+        record.setFromUserId(fromUserId);
+        record.setToUserId(toUserId);
+        record.setFamilyId(from.getFamilyId());
+        record.setAmount(amount);
+        record.setType("transfer");
+        record.setRemark("成员间转让");
+        record.setCreatedAt(new Date());
+        cfTransferRecordMapper.insert(record);
+        return Result.success("转让成功");
+    }
+
+    /** 转让记录(分页,按当前用户家庭) */
+    @PostMapping("/list")
+    public Result<Page<CfTransferRecord>> list(@RequestAttribute("userId") Long userId,
+                                               @RequestBody Map<String, Integer> params) {
+        int page = params.getOrDefault("page", 1);
+        int size = params.getOrDefault("size", 20);
+        User user = userMapper.selectById(userId);
+        Long familyId = user == null ? null : user.getFamilyId();
+        LambdaQueryWrapper<CfTransferRecord> wrapper = new LambdaQueryWrapper<>();
+        if (familyId != null) {
+            wrapper.eq(CfTransferRecord::getFamilyId, familyId);
+        }
+        wrapper.orderByDesc(CfTransferRecord::getCreatedAt);
+        return Result.success(cfTransferRecordMapper.selectPage(new Page<>(page, size), wrapper));
+    }
+}
+```
+
+> **注意:** `PlatformPointsService.spend/earn` 的幂等键由 `refId` 控制。上述代码用 `fromUserId` 作 refId 会导致**同一对用户多次转让互相覆盖**——实现时必须改为每次唯一 refId(如用 `System.nanoTime()` 或生成唯一 id),或在 `spend` 增加 `cf_transfer_*` 专用幂等处理。这是实现者必须修正的点(推荐:转让场景不走幂等检查,或 earn/spend 传入自增业务唯一键)。
+
+- [ ] **步骤 2:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/CfTransferController.java
+git commit -m "feat(cf): CF 成员间转让接口(同家庭校验 + 双账本 + 流水)"
+```
+
+---
+
+### 任务 11:优惠券绑定家庭(家庭券库)
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/CouponService.java`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/FamilyPlatformPointsService.java`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/CfCommissionController.java`(新增家庭券库接口)
+
+- [ ] **步骤 1:`UserCoupon` 实体加 `familyId`**
+
+`cfc-backend/src/main/java/com/etotem/cfc/entity/UserCoupon.java` 新增字段:
+
+```java
+private Long familyId;
+```
+
+- [ ] **步骤 2:`grant` 方法支持传 familyId**
+
+`CouponService.grant`(line 195)增加重载:
+
+```java
+public boolean grant(Long userId, Long couponId, String grantType, String period, String source, Long familyId) {
+    UserCoupon uc = new UserCoupon();
+    uc.setUserId(userId);
+    uc.setCouponId(couponId);
+    uc.setStatus("AVAILABLE");
+    uc.setReceivedAt(new Date());
+    uc.setFamilyId(familyId);
+    userCouponMapper.insert(uc);
+    return true;
+}
+```
+
+原 5 参 `grant` 保留并委托新方法(传 null)。核对原 `grant` 现有实现(line 195-231),保持 `CouponGrantLog` 写入等原有逻辑不丢失。
+
+- [ ] **步骤 3:`exchangeCouponByCf` 兑换后券挂家庭**
+
+`FamilyPlatformPointsService.exchangeCouponByCf`(line 271)调用改为:
+
+```java
+couponService.grant(refUserId, couponId, "CF_EXCHANGE", null, "family:" + familyId, familyId);
+```
+
+- [ ] **步骤 4:家庭券库查询接口**
+
+`CfCommissionController` 新增:
+
+```java
+/** 家庭共享券列表 */
+@PostMapping("/family-coupons")
+public Result<com.baomidou.mybatisplus.extension.plugins.pagination.Page<com.etotem.cfc.entity.UserCoupon>>
+familyCoupons(@RequestAttribute("userId") Long userId,
+              @RequestBody Map<String, Integer> params) {
+    int page = params.getOrDefault("page", 1);
+    int size = params.getOrDefault("size", 20);
+    com.etotem.cfc.entity.User user = com.etotem.cfc.entity.User.class.cast(null);
+    // 需注入 UserMapper 获取 familyId
+    ...
+}
+```
+
+> **注意:** 该接口需要 `UserMapper` 注入 `CfCommissionController` 以获取 `familyId`,上述为示意,实现时用 `@Resource UserMapper userMapper` 正常注入,查询 `userCoupon WHERE family_id=familyId AND status='AVAILABLE'`。完成后再补完整实现。
+
+- [ ] **步骤 5:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 6:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/UserCoupon.java cfc-backend/src/main/java/com/etotem/cfc/service/CouponService.java cfc-backend/src/main/java/com/etotem/cfc/service/FamilyPlatformPointsService.java cfc-backend/src/main/java/com/etotem/cfc/controller/CfCommissionController.java
+git commit -m "feat(cf): 优惠券绑定家庭 + 家庭券库查询(CF兑换券挂family_id)"
+```
+
+---
+
+### 任务 12:管理端阶梯配置接口
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCfRateTierController.java`
+
+- [ ] **步骤 1:创建 `AdminCfRateTierController`**
+
+```java
+package com.etotem.cfc.controller.admin;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.CfRateTier;
+import com.etotem.cfc.mapper.CfRateTierMapper;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/admin/cf-rate-tier")
+public class AdminCfRateTierController {
+
+    @Resource
+    private CfRateTierMapper cfRateTierMapper;
+
+    @PostMapping("/list")
+    public Result<List<CfRateTier>> list() {
+        return Result.success(cfRateTierMapper.selectList(
+                new LambdaQueryWrapper<CfRateTier>().orderByAsc(CfRateTier::getSortOrder)));
+    }
+
+    @PostMapping("/save")
+    public Result<String> save(@RequestBody CfRateTier tier) {
+        if (tier.getId() != null) {
+            tier.setUpdatedAt(new Date());
+            cfRateTierMapper.updateById(tier);
+        } else {
+            tier.setCreatedAt(new Date());
+            tier.setUpdatedAt(new Date());
+            if (tier.getEnabled() == null) tier.setEnabled(1);
+            cfRateTierMapper.insert(tier);
+        }
+        return Result.success("已保存");
+    }
+
+    @PostMapping("/delete")
+    public Result<String> delete(@RequestBody Map<String, Object> params) {
+        Long id = ((Number) params.get("id")).longValue();
+        cfRateTierMapper.deleteById(id);
+        return Result.success("已删除");
+    }
+}
+```
+
+- [ ] **步骤 2:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminCfRateTierController.java
+git commit -m "feat(cf): 管理端 CF 返佣阶梯配置接口(list/save/delete)"
+```
+
+---
+
+### 任务 13:前端改造(小程序)
+
+**文件:**
+- 修改:`cfc-frontend/utils/api.js`
+- 修改:`cfc-frontend/pages/promotion/team.vue`
+- 修改:`cfc-frontend/pages/promotion/commission.vue`
+- 修改:`cfc-frontend/pages/promotion/index.vue`
+
+- [ ] **步骤 1:`utils/api.js` 新增接口封装**
+
+```js
+// CF 分佣
+export const getCfRate = () => request('/api/commission/cf/rate', 'POST')
+export const getCfSummary = () => request('/api/commission/cf/summary', 'POST')
+export const getCfList = (page, size) => request('/api/commission/cf/list', 'POST', { page, size })
+export const getFamilyCoupons = (page, size) => request('/api/commission/cf/family-coupons', 'POST', { page, size })
+// CF 转让
+export const sendCfTransfer = (toUserId, amount) => request('/api/cf/transfer/send', 'POST', { toUserId, amount })
+export const getCfTransferList = (page, size) => request('/api/cf/transfer/list', 'POST', { page, size })
+```
+
+- [ ] **步骤 2:`pages/promotion/team.vue` 改造**
+
+将 stats-card 改为展示 `getCfRate()` 返回的 `totalTeamSize` / `ratePercent` / `tierName`(替换原 L1/L2/团队佣金),模板与 methods 同步替换。删除 `getCommissionTeam` 依赖(或保留兼容)。
+
+- [ ] **步骤 3:`pages/promotion/commission.vue` 改造**
+
+数据源从旧 `/api/commission/list` 切换为 `getCfList()`,展示 CF 流水(金额/类型/时间),注意时间用 `parseDate()` 格式化。
+
+- [ ] **步骤 4:`pages/promotion/index.vue` 改造**
+
+头部汇总改用 `getCfSummary()`(available/frozen/totalEarned),保留推广二维码/邀请等原有功能。
+
+- [ ] **步骤 5:前端语法校验**
+
+运行:`node -e "require('@babel/parser')"` 不可用则用 `node --check` 提取 script 块逐文件校验。小程序由 HBuilderX 打包(Agent 不执行 build)。
+
+- [ ] **步骤 6:Commit**
+
+```bash
+git add cfc-frontend/utils/api.js cfc-frontend/pages/promotion/team.vue cfc-frontend/pages/promotion/commission.vue cfc-frontend/pages/promotion/index.vue
+git commit -m "feat(cf): 小程序推广中心切换到 CF 分佣接口(团队规模/比例/流水/钱包)"
+```
+
+---
+
+### 任务 14:测试与收尾
+
+**文件:**
+- 创建:`cfc-backend/src/test/java/com/etotem/cfc/service/CfCommissionServiceTest.java`(可选,若测试基建可用)
+- 修改:`docs/superpowers/api/API_REFERENCE.md`
+
+- [ ] **步骤 1:单元测试(阶梯匹配边界)**
+
+若后端测试基建可用,创建测试:
+
+```java
+// 验证 matchRateTier 边界:0/2/3/9/10/29/30/99/100
+// 0 → 铜牌5;3 → 银牌10;10 → 金牌15;30 → 铂金20;100 → 钻石25
+```
+
+- [ ] **步骤 2:全量编译 + 测试**
+
+运行:`cd cfc-backend && mvn clean compile && mvn test`
+预期:BUILD SUCCESS;测试通过(或记录预存失败)
+
+- [ ] **步骤 3:更新 API_REFERENCE.md**
+
+在 `docs/superpowers/api/API_REFERENCE.md` 新增章节记录:`/api/commission/cf/*`(rate/summary/list/family-coupons)、`/api/cf/transfer/*`(send/list)、`/api/admin/cf-rate-tier/*`(list/save/delete)。
+
+- [ ] **步骤 4:Commit**
+
+```bash
+git add cfc-backend/src/test/java/com/etotem/cfc/service/CfCommissionServiceTest.java docs/superpowers/api/API_REFERENCE.md
+git commit -m "docs(cf): API 文档补充 CF 分佣/转让/阶梯配置接口 + 单元测试"
+```
+
+---
+
+## 自检记录
+
+**1. 规格覆盖度:**
+- 数据模型(cf_rate_tier / cf_transfer_record / 复用 user_platform_balance / coupon+family_id)→ 任务 1、2、11 ✓
+- 统一分佣服务 + 双返/只返推荐人 + 同家庭上溯 + 阶梯比例 → 任务 5 ✓
+- 推荐树物化 + 团队规模全层级 → 任务 4、6 ✓
+- 等级评估链路修复(updateTeamSize 调用 + refreshRate + 定时任务)→ 任务 6 ✓
+- 订单迁移点 7 处 → 任务 7 ✓
+- 旧佣金废弃 → 任务 8 ✓
+- 用户查询(团队规模/比例/流水)→ 任务 9 ✓
+- CF 转让 → 任务 10 ✓
+- 家庭券库 → 任务 11 ✓
+- 管理端阶梯配置 → 任务 12 ✓
+- 前端 → 任务 13 ✓
+- 测试/文档 → 任务 14 ✓
+
+**2. 占位符扫描:** 无"待定/TODO";任务 5 步骤 1 中 `getEffectivePpoint` 签名猜测已显式标注为实现者必须核对点;任务 10 转让幂等键问题已显式标注;任务 11 family-coupons 示意代码已标注需补全。
+
+**3. 类型一致性:** `CfRateTier`/`CfTransferRecord`/`CfReferralService`/`CfCommissionService` 名称在后续任务中一致;`refreshRate` 在任务 6 定义、任务 6 定时任务引用一致;`updateTeamSize(userId, delta)` 新签名在任务 6 统一。
+
+---
+
+## 执行交接
+
+计划已完成并保存到 `docs/superpowers/plans/2026-09-01-cf-commission-redesign.md`。两种执行方式:
+
+**1. 子代理驱动(推荐)** — 每个任务调度一个新的子代理,任务间进行审查,快速迭代
+
+**2. 内联执行** — 在当前会话中使用 executing-plans 执行任务,批量执行并设有检查点

+ 385 - 0
docs/superpowers/specs/2026-08-31-self-check-ai-integration-design.md

@@ -0,0 +1,385 @@
+# 五维家庭自检 AI 结合(P0 + P1)设计
+
+> 日期:2026-08-31
+> 状态:已获用户方案确认(P0+P1 第一批,P2 后续)
+> 关联:`FiveDimensionSelfCheckService` / `AiGateway` / `cfc-langgraph`
+
+---
+
+## 1. 背景与目标
+
+现有五维自检使用**静态五行寻源表**(`WuxingSourcingService` 硬编码 5 条建议)生成结果,所有用户看到相同文案,无个性化、无趋势感知。
+
+本批次实现 P0+P1 共 4 个子系统,按优先级排序:
+
+| 优先级 | 子系统 | 目标 |
+|:------:|--------|------|
+| **P0-1** | 静态建议 → AI 个性化建议 | 每次自检生成有温度、有针对性的解读 |
+| **P0-2** | 用户点击生成健康计划 | 自检低分可一键生成家庭健康计划草稿 |
+| **P1-1** | 历史趋势 + AI 解读 | 3 次自检趋势可视化 + AI 趋势分析 |
+| **P1-2** | 自检结果注入 AI Chat | 聊天页自动携带自检上下文 |
+
+P2(家庭成员差异化建议)延后单独实现。
+
+已确认决策:
+- P0-1:**纯 AI 替换**,不保留静态五行寻源建议(`WuxingSourcingService` 仅保留 `levelOf/levelName/dimensionName`)
+- P0-2:**用户点击才生成**,不自动创建 draft plan
+- P0-1 字段设计:**直接改造 `advices` 字段**,新结构 `[{dimension, interpretation, microActions, aiInsight}]`
+
+---
+
+## 2. 现状盘点
+
+### 后端(自检)
+
+- `FiveDimensionSelfCheckController`:`/questions` `/submit` `/latest` `/history`
+- `FiveDimensionSelfCheckService`:
+  - `QUESTION_BANK`(15题)、`SUB_DIMENSION_QUESTION_BANK`(19题)
+  - `submitSelfCheck`:计分 → `wuxingSourcingService.getAdvicesForLowScores()` → 落库 `adviceJson`
+  - `getQuestions()` 已改为支持 `retake` 参数(上个计划完成)
+- `WuxingSourcingService`:静态 `SOURCING_TABLE`(5行×5列)+ `levelOf/levelName/dimensionName` 纯工具方法
+- 表 `five_dimension_self_checks`:`adviceJson TEXT`
+
+### 后端(AI 基础设施)
+
+- `AiGateway.java`:统一网关(熔断+降级+PII脱敏)
+  - `chat(query, userId, conversationId, inputs)` → `POST /api/v1/chat`
+  - `generateInnateReading(portrait)` → `POST /api/v1/innate/reading`
+  - `generateHealthPlan(inputs)` → `POST /api/v1/analysis/run`
+- LangGraph Python(`cfc-langgraph/app/`,端口 9000):
+  - `app/graphs/chat_graph.py`(78行,Dify 兼容)
+  - `app/graphs/health_plan_graph.py`(297行,Java 客户端 JavaClient 读取家庭数据)
+  - `app/graphs/innate_portrait_graph.py`(65行,参考实现)
+  - `app/api/adapter.py`(Dify 兼容路由)
+  - `app/api/innate_portrait.py`(`/api/v1/innate/reading`)
+  - `app/main.py` 注册各 router
+- 参考链路:`InnatePortraitController` → `InnatePortraitService.generateAiReading()` → `AiGateway.generateInnateReading()` → LangGraph `/api/v1/innate/reading` → `innate_portrait_graph.py` → 成功返回 reading,失败 null(调用方降级模板)
+
+### 前端
+
+- `pages/family/self-check-result.vue`:结果页,advices 展示在第 71-123 行(五行相生寻源建议区块)
+- `pages/family/self-check-entry.vue`:中间页(刚实现)
+- `pages/ai/chat.vue`:AI 聊天页(需确认路径)
+- `utils/api.js`:自检相关方法(`getSelfCheckStatus/ignoreSelfCheck/getSelfCheckQuestions/submitSelfCheck/getSelfCheckLatest/getSelfCheckHistory`)
+
+---
+
+## 3. 数据层设计
+
+### 3.1 `adviceJson` 结构变更
+
+**旧结构**(静态五行寻源):
+```json
+[
+  {
+    "dimension": "mind",
+    "dimensionName": "心",
+    "element": "火",
+    "color": "#FF6B9D",
+    "score": 2,
+    "level": "tense",
+    "levelName": "紧绷",
+    "upstreamDimension": "action",
+    "upstreamName": "行",
+    "upstreamElement": "木",
+    "upstreamColor": "#10B981",
+    "upstreamReason": "关系顺畅了,内心才安定",
+    "restrainerDimension": "wealth",
+    "restrainerName": "富",
+    "restrainerElement": "水",
+    "restrainerColor": "#F59E0B",
+    "restrainerReason": "钱多了,情薄了",
+    "action": "关系周记;家庭夜谈"
+  }
+]
+```
+
+**新结构**(AI 生成):
+```json
+[
+  {
+    "dimension": "mind",
+    "dimensionName": "心",
+    "element": "火",
+    "color": "#FF6B9D",
+    "score": 2,
+    "level": "tense",
+    "levelName": "紧绷",
+    "interpretation": "你的心能量偏低,可能最近情绪压力较大...",
+    "microActions": ["今晚睡前做10分钟深呼吸", "和伴侣约定每周一次夜谈"],
+    "aiInsight": "建议从'行'维度入手...",
+    "fallbackUsed": false
+  }
+]
+```
+
+**注意**:`dimension`/`dimensionName`/`element`/`color`/`score`/`level`/`levelName` 字段保留,与现有 VO 兼容。`upstreamDimension`/`upstreamReason`/`restrainerDimension` 等静态字段移除,新增 `interpretation`/`microActions`/`aiInsight`/`fallbackUsed`。
+
+### 3.2 新增 LangGraph Graph 输入/输出
+
+**self_check_analysis_graph**(P0-1):
+- 输入 `dict`:`scores`(Map<dimension, score>)、`questionIds`(List[int])、`userId`(int)、`recentHistory`(List[dict] 最近3次自检快照)
+- 输出 `dict`:`advice_json`(新结构 JSON string)、`fallback_used`(bool)
+
+**self_check_trend_graph**(P1-1):
+- 输入 `dict`:`history`(List[dict] 最近3次含 totalScore/dimensions/createdAt)、`userId`(int)
+- 输出 `dict`:`aiInsight`(str)、`trendSummary`(str)
+
+---
+
+## 4. 后端接口设计
+
+### 4.1 修改 `POST /api/family/self-check/submit`(P0-1)
+
+AI 建议在 `submitSelfCheck` 时**同步生成**并落库 `adviceJson`(不单独暴露 analysis 接口,避免 YAGNI 冗余)。
+
+```
+POST /api/family/self-check/submit
+  ↓
+submitSelfCheck():计分 → selfCheckAnalysisService.generateAdvice(...) → 落库
+```
+
+### 4.2 新增 `POST /api/family/self-check/generate-plan`(P0-2)
+
+请求:`{ "checkId": 12 }`(可空,默认取最近一次)。
+
+响应 `Result<Map>`:
+```json
+{ "planId": 45, "status": "draft" }
+```
+
+### 4.3 新增 `POST /api/family/self-check/trend-analysis`(P1-1)
+
+请求:无 body(JWT 取 `userId`)。
+
+响应 `Result<TrendAnalysisVO>`:
+```json
+{
+  "history": [{ "createdAt": "...", "totalScore": 32, "dimensions": [...] }],
+  "aiInsight": "近三次身维度持续下降,可能...",
+  "trendSummary": "身-2 智+1 富0 行-1 心+2"
+}
+```
+
+### 4.4 修改 `POST /api/family/self-check/submit`(P0-1)
+
+删除 `WuxingSourcingService.getAdvicesForLowScores()` 调用,改为:
+```java
+SelfCheckAdviceResult adviceResult = selfCheckAnalysisService.generateAdvice(userId, scoreMap, questionIds);
+// adviceResult 含 adviceJson + fallbackUsed
+// 直接落库
+```
+
+### 4.5 `SelfCheckResultVO` 字段
+
+`SelfCheckResultVO.advices` 类型保持 `List<WuxingSourcingAdviceVO>` 不变,字段结构按 5.4 改造后的 VO 承载 AI 建议内容。
+
+---
+
+## 5. Java 服务层设计
+
+### 5.1 新增 `SelfCheckAnalysisService`
+
+```java
+@Service
+public class SelfCheckAnalysisService {
+    @Resource private AiGateway aiGateway;
+    @Resource private FiveDimensionSelfCheckMapper selfCheckMapper;
+    
+    /** 生成自检建议(AI 优先,LangGraph 失败返回 null) */
+    public SelfCheckAdviceResult generateAdvice(Long userId, Map<String, Integer> scoreMap, List<Integer> questionIds) {
+        // 1. 组装 inputs
+        // 2. 调 aiGateway.generateSelfCheckAdvice(inputs)
+        // 3. 返回 SelfCheckAdviceResult(含 adviceJson + fallbackUsed)
+    }
+    
+    /** 生成趋势分析 */
+    public TrendAnalysisVO getTrendAnalysis(Long userId) { ... }
+}
+```
+
+### 5.2 AiGateway 新增方法
+
+```java
+public Map<String, Object> generateSelfCheckAdvice(Map<String, Object> inputs) {
+    // POST /api/v1/self-check/analysis
+    // 失败返回 null(调用方设置 fallbackUsed=true)
+}
+
+public Map<String, Object> generateSelfCheckTrend(Map<String, Object> inputs) {
+    // POST /api/v1/self-check/trend
+}
+```
+
+### 5.3 WuxingSourcingService 变更
+
+- **保留**:`DIMENSION_META`、`levelOf()`、`levelName()`、`dimensionName()`(被 `buildDimensionScoreVO` 使用)
+- **删除**:`SOURCING_TABLE`、`getAdvice()`、`getAdvicesForLowScores()`
+
+### 5.4 `WuxingSourcingAdviceVO` 改造(P0-1)
+
+不新建 VO,直接改造现有 `dto/WuxingSourcingAdviceVO.java`(保持 `SelfCheckResultVO.advices` 类型不变,最小改动):
+
+- **保留字段**:`dimension` / `dimensionName` / `element` / `color` / `score` / `level` / `levelName`(与现有 VO 兼容)
+- **移除字段**:`upstreamDimension` / `upstreamName` / `upstreamElement` / `upstreamColor` / `upstreamReason` / `restrainerDimension` / `restrainerName` / `restrainerElement` / `restrainerColor` / `restrainerReason` / `action`
+- **新增字段**:
+  - `interpretation`(String,AI 对低分维度的解读)
+  - `microActions`(List&lt;String&gt;,2-3 个本周微行动)
+  - `aiInsight`(String,AI 补充洞察,可空)
+  - `fallbackUsed`(Boolean,AI 不可用降级标记)
+
+---
+
+## 6. LangGraph Python 设计
+
+### 6.1 新增 `self_check_analysis_graph.py`
+
+```python
+# cfc-langgraph/app/graphs/self_check_analysis_graph.py
+SYSTEM_PROMPT = """你是一位家庭健康顾问,基于五维自检结果(身·智·富·行·心,每维0-9分,满分45)给出个性化建议。
+
+要求:
+1. 用第二人称"你"称呼
+2. 对每个低分维度(≤6分)给出1-2句解读和2-3个具体可执行的微行动
+3. 如有历史数据,简要对比趋势
+4. 语气温暖口语化,不超过300字/维度
+5. 最后给出1句家庭整体洞察
+
+返回 JSON:
+{
+  "advice": [
+    {"dimension": "mind", "dimensionName": "心", "interpretation": "...", "microActions": ["...", "..."]},
+    ...
+  ],
+  "familyInsight": "..."
+}
+"""
+
+class SelfCheckAnalysisAgent:
+    async def run(self, scores: dict, questionIds: list, recentHistory: list, userId: int) -> dict:
+        # 格式化输入 → LLM → JSON 解析 → 返回
+```
+
+### 6.2 新增 `self_check_trend_graph.py`
+
+```python
+SYSTEM_PROMPT = """分析用户近3次五维自检趋势,输出:
+- aiInsight: 趋势解读(2-3句,指出最大变化维度和可能原因)
+- trendSummary: 各维度 delta(如"身-2 智+1 富0 行-1 心+2")
+返回 JSON:{"aiInsight": "...", "trendSummary": "..."}
+"""
+```
+
+### 6.3 注册路由
+
+`cfc-langgraph/app/api/self_check.py`:
+- `POST /api/v1/self-check/analysis` → `self_check_analysis_graph`
+- `POST /api/v1/self-check/trend` → `self_check_trend_graph`
+
+`cfc-langgraph/app/main.py`:追加 `app.include_router(self_check.router)`
+
+---
+
+## 7. 前端设计
+
+### 7.1 P0-1:`self-check-result.vue` 建议区块重写
+
+删除现有 `scr-card` 寻源建议区块(第 71-123 行),替换为:
+
+```html
+<view class="ai-advice-card" v-if="result.adviceJson">
+  <view class="ai-advice-header">
+    <text class="ai-advice-title">AI 健康解读</text>
+    <text class="ai-advice-sub">基于你的五维自检结果</text>
+  </view>
+  <view v-for="item in result.adviceJson" :key="item.dimension" class="ai-advice-item">
+    <view class="ai-advice-dim-badge" :style="{background: item.color}">
+      <text>{{ item.dimensionName }}</text>
+      <text>{{ item.score }}分</text>
+    </view>
+    <text class="ai-advice-interpretation">{{ item.interpretation }}</text>
+    <view class="ai-advice-actions">
+      <text class="ai-advice-action-label">本周行动:</text>
+      <text v-for="(action, i) in item.microActions" :key="'a'+i" class="ai-advice-action">{{ i+1 }}. {{ action }}</text>
+    </view>
+  </view>
+  <view class="ai-advice-family" v-if="result.adviceJson[0] && result.adviceJson[0].familyInsight">
+    <text class="ai-advice-family-label">💡 家庭整体洞察</text>
+    <text class="ai-advice-family-text">{{ result.adviceJson[0].familyInsight }}</text>
+  </view>
+</view>
+<view class="ai-advice-fallback" v-else-if="result.fallbackUsed">
+  <text>AI 分析暂时不可用,请稍后再试</text>
+</view>
+```
+
+### 7.2 P0-2:结果页加「生成健康计划」按钮
+
+在结果页底部按钮区追加(有低分维度时显示):
+```html
+<button v-if="hasLowScore && !planGenerating" class="plan-btn" @click="generatePlan">
+  生成健康计划
+</button>
+```
+
+### 7.3 P1-1:趋势展示
+
+新增 `POST /api/family/self-check/trend-analysis`,结果页中间页可展示(或结果页底部加「查看趋势」链接)。
+
+### 7.4 P1-2:Chat 注入 context
+
+在 `self-check-result.vue` 加「问问 AI」按钮:
+```html
+<button @click="askAI">问问 AI</button>
+```
+跳转:`uni.navigateTo({ url: '/pages/ai/chat?selfCheckId=' + result.id })`
+
+`AIChatController` 读取 `selfCheckId`,调 `getSelfCheckLatest`,将结果放入 context 传给 AiGateway。
+
+---
+
+## 8. 边界行为与错误处理
+
+1. **LangGraph 不可用**:`AiGateway` 熔断/open 时,`generateAdvice` 返回 `fallbackUsed=true`,`adviceJson=null`,前端显示"AI 分析暂时不可用"
+2. **LLM 返回非 JSON**:Python 侧 `validate` 节点捕获,返回 error,Java 侧 fallbackUsed=true
+3. **P0-2 计划生成失败**:toast "计划生成失败,请稍后重试",不阻断结果页
+4. **P1-1 历史不足 3 次**:用现有次数展示(1-2 次),LLM 做有限对比
+5. **P1-2 chat 上下文**:selfCheckId 无效时静默跳过,不影响 chat 正常流程
+
+---
+
+## 9. 测试与验证
+
+- 后端:`mvn clean compile` BUILD SUCCESS
+- 前端:`node --check` api.js 语法校验
+- 手动验证路径:
+  1. 提交自检 → 查看 adviceJson 含 AI 生成内容(非静态表)
+  2. 点「生成健康计划」→ 跳转 plan 详情(draft 状态)
+  3. 点「查看趋势」→ 展示趋势 + AI 解读
+  4. 点「问问 AI」→ chat 页,LLM 回答知晓自检背景
+
+---
+
+## 10. 文件变更清单
+
+**LangGraph Python(新增 2 graph + 1 api + 改 main)**:
+- `cfc-langgraph/app/graphs/self_check_analysis_graph.py`(P0-1)
+- `cfc-langgraph/app/graphs/self_check_trend_graph.py`(P1-1)
+- `cfc-langgraph/app/api/self_check.py`(路由注册)
+- `cfc-langgraph/app/main.py`(追加 router include)
+
+**Java 后端**:
+- `dto/WuxingSourcingAdviceVO.java`(改造:移除 upstream/restrainer/action,新增 interpretation/microActions/aiInsight/fallbackUsed)
+- `service/SelfCheckAnalysisService.java`(新建)
+- `service/WuxingSourcingService.java`(删除 SOURCING_TABLE/getAdvice/getAdvicesForLowScores)
+- `service/FiveDimensionSelfCheckService.java`(修改 submitSelfCheck 调新 Service,新增 trend 方法)
+- `service/AiGateway.java`(新增 2 方法)
+- `controller/family/FiveDimensionSelfCheckController.java`(新增 3 接口,修改 submit)
+- `service/HealthPlanService.java`(新增 generateFromSelfCheck)
+- `service/impl/HealthPlanServiceImpl.java`(实现 generateFromSelfCheck)
+
+**前端**:
+- `utils/api.js`(新增 3 方法)
+- `pages/family/self-check-result.vue`(P0-1/P0-2/P1-2 改造)
+- `pages/family/self-check-entry.vue`(P1-1 趋势展示)
+- `pages/ai/chat.vue`(P1-2 context 注入,需确认是否存在)

+ 322 - 0
docs/superpowers/specs/2026-09-01-cf-commission-redesign.md

@@ -0,0 +1,322 @@
+# CF 值分佣体系重构设计文档
+
+> 日期:2026-09-01
+> 状态:待评审
+> 分支:cfc 主分支
+
+## 1. 背景与目标
+
+### 1.1 现状问题
+
+当前系统存在两套并行的返利/分佣体系:
+
+| 体系 | 服务 | 表 | 作用域 | 用途 | 状态 |
+|------|------|-----|--------|------|------|
+| 新 CF 值分佣 | `CommissionDistService` | `commission_dist_log` / `user_platform_balance` | 个人 | 商品订单 P点分润 | 仅商品订单 |
+| 旧佣金体系 | `CommissionService.settle/settleTwoLevel` | `commission_records` / `family_earnings` | 个人+家庭 | 套餐/测评/会员/订阅订单 L1/L2 佣金 | `@Deprecated`,仍被调用 |
+
+问题:
+1. 旧佣金体系 `settleTwoLevel` 仍被 5 个订单服务调用(套餐/测评/会员/订阅),与需求"全部走 CF 值体系"冲突。
+2. 团队规模统计 `PromotionTierService.updateTeamSize()` 全库无调用者,等级评估链路断裂。
+3. `promotion_tier_config` 表列(`min_team_size_1st/2nd + commission_rate_l1/l2`)与实体 `PromotionTierConfig`(`min_team_size + profit_share_percent + enabled`)不一致,等级配置实际未接通。
+4. 用户无法查询自己的团队规模和返佣比例。
+5. CF 值不能成员间转让;优惠券不绑定家庭。
+
+### 1.2 目标
+
+1. 全部订单统一走 CF 值分佣,废弃旧佣金体系写入。
+2. 返佣比例由团队规模(不限层级)决定,规模越大比例越高。
+3. 用户可查自己的团队规模与返佣比例。
+4. 家庭成员间可转让 CF 值;CF 值兑换的优惠券绑定家庭共享。
+
+### 1.3 关键决策(已与需求方确认)
+
+| # | 决策点 | 结论 |
+|---|--------|------|
+| D1 | 返佣比例模型 | **纯团队规模阶梯**(`cf_rate_tier` 动态可配置) |
+| D2 | CF 值归属维度 | **返给个人钱包**(`user_platform_balance`),不进家庭池 |
+| D3 | 会员升级特例 | 仅 membership/subscription 订单不返当前人,只返推荐人 |
+| D4 | 优惠券绑定 | 用 CF 兑换的券**放进家庭共享池**(券绑 `family_id`,全家可用) |
+| D5 | 同家庭互推 | **跳过本人,上溯到第一个非同家庭的引荐人** |
+| D6 | 分润结算主体 | **个人维度**(A/B 引荐人不同则各自独立结算) |
+| D7 | 团队规模口径 | 推荐人全层级下线总人数(`promotion_tier.total_team_size`) |
+
+## 2. 数据模型设计
+
+### 2.1 新建表
+
+#### `cf_rate_tier` — 分佣阶梯配置
+
+```sql
+CREATE TABLE IF NOT EXISTS cf_rate_tier (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    tier_name VARCHAR(50) NOT NULL COMMENT '档位名称: 铜牌/银牌/金牌/铂金/钻石',
+    min_team_size INT NOT NULL DEFAULT 0 COMMENT '团队规模下限(含)',
+    rate_percent INT NOT NULL DEFAULT 0 COMMENT '返佣比例(%),如10表示10%',
+    sort_order INT NOT NULL DEFAULT 0 COMMENT '排序,越大越高',
+    enabled TINYINT DEFAULT 1 COMMENT '1启用/0停用',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_min_size (min_team_size)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值返佣阶梯配置';
+```
+
+种子数据(默认 5 档,管理端可动态增删改):
+
+| tier_name | min_team_size | rate_percent |
+|-----------|--------------|--------------|
+| 铜牌 | 0 | 5 |
+| 银牌 | 3 | 10 |
+| 金牌 | 10 | 15 |
+| 铂金 | 30 | 20 |
+| 钻石 | 100 | 25 |
+
+#### 个人 CF 钱包 — **复用 `user_platform_balance`(不新建)**
+
+经核对,`user_platform_balance` 表已具备 `family_id` 列及完整字段(`available/frozen/withdrawn/exchanged/total_earned`),`PlatformPointsService` 即为个人 CF 钱包服务(`earn/spend/freeze/unfreeze/withdraw`)。**直接复用,无需新建 `user_cf_wallet` 表。**
+
+转让时个人钱包变动复用 `PlatformPointsService.spend/earn`,通过 `cf_transfer_record` 记录流转明细。
+
+#### `cf_transfer_record` — CF 流转记录
+
+```sql
+CREATE TABLE IF NOT EXISTS cf_transfer_record (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    from_user_id BIGINT COMMENT '转出用户ID(null=平台/家庭池)',
+    to_user_id BIGINT COMMENT '转入用户ID',
+    family_id BIGINT COMMENT '所属家庭ID',
+    amount INT NOT NULL COMMENT 'CF值数量',
+    type VARCHAR(16) NOT NULL COMMENT 'transfer成员转让/allocate管理员分配/refund退款回补',
+    ref_type VARCHAR(50) COMMENT '关联业务类型',
+    ref_id BIGINT COMMENT '关联业务ID',
+    remark VARCHAR(255),
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_from (from_user_id),
+    INDEX idx_to (to_user_id),
+    INDEX idx_family (family_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值流转记录';
+```
+
+### 2.2 迁移现有表
+
+| 表 | 操作 | 说明 |
+|----|------|------|
+| `promotion_tier` | 删除 `team_size_1st/2nd/3rd`;新增 `rate_percent INT DEFAULT 0` | 团队规模只留 `total_team_size` 一个口径 |
+| `promotion_tier_config` | 废弃(保留历史数据,业务不再读取) | 替代者为 `cf_rate_tier` |
+| `coupon` | 新增 `family_id BIGINT NULL` | 券绑定家庭(NULL=不绑定) |
+| `user_platform_balance` | **复用,不改表** | 已有 `family_id` 列及完整字段,直接作为个人 CF 钱包 |
+
+### 2.3 保留不变
+
+- `referral_tree`:已支持全层级(`path` 物化路径)。
+- `user_platform_balance` / `platform_balance_log`:个人 CF 值余额与流水(复用为个人钱包)。
+- `commission_dist_log`:商品订单分润流水(迁移后并入通用分润流水体系)。
+- `family_platform_balance` / `family_platform_exchange_record`:**仅作优惠券兑换用途**(用个人 CF 兑换后券挂家庭),不再承接返佣。
+
+## 3. 分佣核心逻辑
+
+### 3.1 统一服务 `CfCommissionService`(新建)
+
+```java
+@Service
+public class CfCommissionService {
+    /** 通用分佣入口(商品/套餐/测评订单:当前人+推荐人双返) */
+    void settle(Long orderId, String orderType, Long buyerUserId, Long buyerFamilyId,
+                Integer orderAmountCent, Long productId);
+
+    /** 会员/订阅专用(只返推荐人,不返当前人) */
+    void settleReferrerOnly(Long orderId, String orderType, Long buyerUserId, Long buyerFamilyId,
+                Integer orderAmountCent);
+}
+```
+
+### 3.2 推荐链查询与同家庭上溯
+
+```java
+List<ReferralNode> getEffectiveReferrers(Long buyerUserId, Long buyerFamilyId) {
+    // 1. 查 referral_tree WHERE child_id=buyerUserId ORDER BY level ASC
+    // 2. 对每个 referrer R:
+    //    a. R.familyId == buyerFamilyId → 跳过(同家庭不返),查 R 的引荐人继续上溯
+    //    b. R.familyId != buyerFamilyId → 加入结果集
+    // 3. 上溯终止:无更上级 / 已找到非同家庭引荐人
+}
+```
+
+> 决策 D5:同家庭互推时跳过本人,上溯到第一个非同家庭的引荐人。避免"自家消费返自己家人"的无效内耗与刷单。
+
+### 3.3 返佣比例计算(纯团队规模阶梯)
+
+```java
+int getRatePercent(Long referrerUserId) {
+    // 1. 查 promotion_tier.total_team_size(全层级下线人数)
+    // 2. SELECT rate_percent FROM cf_rate_tier
+    //    WHERE enabled=1 AND min_team_size <= total_team_size
+    //    ORDER BY min_team_size DESC LIMIT 1
+    // 3. 无匹配 → 0
+}
+```
+
+### 3.4 分润公式
+
+```
+分润CF = floor(orderAmountCent / 100) × 分润基数比例 × rate_percent / 100
+
+其中:
+- 商品订单:分润基数比例 = 商品 P点 ppointConfig(如 sourcePpoint 已含此义)
+- 非商品订单(套餐/测评/会员/订阅):分润基数比例 = sys_config.commission_service_rate(默认 10%)
+```
+
+简化统一为:
+
+```
+商品:cf = floor(sourcePpoint) × rate_percent / 100   (sourcePpoint 来自 PpointConfigService)
+非商品:cf = floor(orderAmountCent / 100) × serviceRateBps / 10000 × rate_percent / 100
+```
+
+### 3.5 返现路径
+
+- **当前人返 CF**(商品/套餐/测评):`platformPointsService.earn(buyerUserId, cf, "order_consume", orderId, ...)` → 个人钱包
+- **推荐人分润**:`platformPointsService.earn(referrerUserId, cf, "referral_dist", orderId, ...)` → 个人钱包
+- **流水**:`cf_transfer_record` 记录 from=平台 to=受益人(type=allocate)
+
+### 3.6 幂等
+
+以 `(ref_type='referral_dist'|'order_consume', ref_id=orderId)` 为幂等键,`platform_balance_log` 存在即跳过。`platformPointsService.earn` 当前实现无幂等检查,实现时需补(仿 `FamilyPlatformPointsService.earn` 的 `(ref_type, ref_id)` 去重)。
+
+## 4. 订单结算迁移点
+
+| 订单类型 | 文件:行 | 原调用 | 新调用 | 当前人返 CF | 推荐人分润 |
+|---|---|---|---|---|---|
+| 测评 | `AssessmentOrderService.java:76` | `commissionService.settle(...)` | `cfCommissionService.settle(...)` | ✅ | ✅ |
+| 套餐 | `PackagePaymentService.java:220` | `commissionService.settle(...)` | `cfCommissionService.settle(...)` | ✅ | ✅ |
+| 套餐 | `PaymentService.java:240` | `commissionService.settle(...)` | `cfCommissionService.settle(...)` | ✅ | ✅ |
+| 商品 | `ProductOrderService.java:645` | `commissionDistService.distribute(...)` | `cfCommissionService.settle(...)` | ✅ | ✅ |
+| 会员 | `MembershipService.java:784` | `commissionService.settleTwoLevel(...)` | `cfCommissionService.settleReferrerOnly(...)` | ❌ | ✅ |
+| 订阅 | `MemberSubscriptionService.java:174` | `commissionService.settleTwoLevel(...)` | `cfCommissionService.settleReferrerOnly(...)` | ❌ | ✅ |
+| 订阅 | `MemberSubscriptionService.java:255` | `commissionService.settleTwoLevel(...)` | `cfCommissionService.settleReferrerOnly(...)` | ❌ | ✅ |
+| 活动 | `ActivityOrderService` | 无 | 无 | ❌ | ❌ |
+
+### 4.1 商品订单处理
+
+商品订单当前 `ProductOrderService:633` 用 `platformPointsService.earn(buyerId, cfValue, "product_order", ...)` 返个人钱包,`:827 confirmReceive` 用 `familyPlatformPointsService.earn(familyId, ...)` 返家庭池。
+
+**本次设计决定:**
+- `:633` 个人 CF 返现:**保留**(属于分佣双返中的"当前人返")。
+- `:827 confirmReceive` 家庭池返 CF:**保留不动**(属于"确认收货额外积分",非本次推荐分佣范畴)。若后续需统一口径,另行提任务。
+
+> 待产品确认项:确认收货返家庭池 CF 是否仍需保留(与"返佣只进个人钱包"是否冲突)。
+
+## 5. 等级评估触发链路修复
+
+### 5.1 团队规模写入
+
+```
+推荐绑定:CommissionService.bindReferral(userId, referralCode)
+  → 建立 referral_tree 关系(path 物化路径)
+  → promotionTierService.updateTeamSize(referrerId, 全链路上溯, +1)
+     (对 referrer 及其所有祖先 promotion_tier.total_team_size +1)
+```
+
+### 5.2 等级/比例刷新
+
+```
+PromotionTierService.refreshRate(referrerUserId)
+  → 查 total_team_size → JOIN cf_rate_tier → rate_percent
+  → 写回 promotion_tier.rate_percent + tier(档位名)
+```
+
+触发时机:
+1. `bindReferral` 后(实时)
+2. 每日定时任务 `PromotionTierCheckScheduledTask`(现有,改调 refreshRate 批量刷新)
+
+### 5.3 废弃旧逻辑
+
+- `CommissionService.settle/settleTwoLevel` → 标 `@Deprecated` + 保留(历史读取),删除全部调用点。
+- `PromotionTierEvalService.evaluateTier`(基于 promotion_tier_config)→ 改读 `cf_rate_tier`。
+- `PromotionTierConfig` 实体 → 不再使用。
+
+## 6. 查询接口
+
+### 6.1 用户查询
+
+| 接口 | 说明 | 返回 |
+|------|------|------|
+| `POST /api/commission/cf/rate` | 我的团队规模 + 当前返佣比例 | `{ totalTeamSize, ratePercent, tierName }` |
+| `POST /api/commission/cf/summary` | 我的 CF 钱包汇总 | `{ available, frozen, totalEarned, totalSpent }` |
+| `POST /api/commission/cf/list` | CF 分润/消费流水(分页) | `Page<PlatformBalanceLog>` |
+| `POST /api/commission/cf/wallet` | 个人 CF 余额 | `PlatformPointsService.getBalance` |
+
+### 6.2 CF 转让
+
+| 接口 | 说明 | 约束 |
+|------|------|------|
+| `POST /api/cf/transfer/send` | 成员间转让 CF | 双方同家庭;转出余额足;type=transfer |
+| `POST /api/cf/transfer/list` | 转让记录(分页) | 按 family_id 或 userId 查询 |
+
+### 6.3 家庭券库
+
+| 接口 | 说明 |
+|------|------|
+| `POST /api/coupon/family/list` | 查询家庭共享券(`user_coupon.family_id=familyId`) |
+| 复用 | `POST /api/family-platform/exchange-coupon`(兑换后券挂 family_id) |
+
+### 6.4 管理端
+
+| 接口 | 说明 |
+|------|------|
+| `POST /api/admin/cf-rate-tier/list` | 阶梯配置列表 |
+| `POST /api/admin/cf-rate-tier/save` | 新增/更新档位 |
+| `POST /api/admin/cf-rate-tier/delete` | 删除档位 |
+
+## 7. 前端改造
+
+### 7.1 推广中心(`pages/promotion/*`)
+
+- `team.vue`:展示全层级团队总人数 + 当前返佣比例 + 档位名(调 `/api/commission/cf/rate`)
+- `commission.vue`:改用 CF 分润流水(调 `/api/commission/cf/list`)
+- `index.vue`:头部汇总改用个人 CF 钱包(调 `/api/commission/cf/summary`)
+
+### 7.2 家庭积分页
+
+- 新增"成员间转让"入口:调 `/api/cf/transfer/send`
+- 显示个人 CF 余额与转让记录
+
+### 7.3 优惠券
+
+- 兑换后的券进入家庭共享券库;下单可选择家庭券
+
+## 8. 错误处理与边界
+
+| 场景 | 处理 |
+|------|------|
+| 同家庭互推 | 跳过本人,上溯到非同家庭引荐人(D5) |
+| 无有效推荐人 | 不产生分润,结束 |
+| 余额不足 | `spend` 抛异常,前端提示"CF值不足" |
+| 重复结算 | `earn` 补幂等(`ref_type+ref_id` 去重) |
+| 团队规模未更新 | 定时任务兜底刷新 |
+| 孤儿用户(无 family) | 分润照常走个人钱包,family_id 冗余为空 |
+
+## 9. 测试计划
+
+- 单元:`getRatePercent` 阶梯匹配(边界 0/2/3/9/10);同家庭上溯逻辑;幂等防重。
+- 集成:各订单支付成功 → 分润到账;会员订单只返推荐人;转让同家庭校验。
+- E2E:推广中心展示团队规模与比例;成员间转让;家庭券库兑换与使用。
+
+## 10. 实施顺序(阶段)
+
+1. 数据库迁移:`cf_rate_tier` 建表 + 种子;`promotion_tier` 精简加列;`coupon` 加 `family_id`;`user_platform_balance` 补 `family_id`。
+2. `CfCommissionService` 核心 + `PlatformPointsService.earn` 幂等补丁。
+3. 等级评估:`updateTeamSize` 调用链 + `refreshRate` + 定时任务改造。
+4. 各订单服务迁移点切换(7 处)。
+5. 旧 `settle/settleTwoLevel` 调用点摘除 + `@Deprecated`。
+6. 查询接口 + CF 转让接口 + 家庭券库接口。
+7. 管理端阶梯配置接口。
+8. 前端改造。
+9. 测试 + 部署。
+
+## 11. 明确不做(YAGNI)
+
+- 不重建家庭级分润系统(家庭公共账户 `family_earnings` 不在本次范围)。
+- 不改 `invite_milestone` / `referral_leaderboard` / `onboarding` 现有逻辑。
+- 不迁移历史 `commission_records` 数据到 CF 体系(历史只读)。
+- 不做积分商城实物兑换(现有 `PointsExchangeProduct` 不动)。