Ver Fonte

feat(self-check): 五维家庭自检问卷(P0-1) + 五行相生寻源建议引擎(P0-2)

E2E Test Bot há 3 meses atrás
pai
commit
6aea64b750

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

@@ -7328,5 +7328,23 @@ private void runMigration100() {
 		} catch (Exception e) {
 		} catch (Exception e) {
 			log.warn("创建family_fortune_report表失败: {}", e.getMessage());
 			log.warn("创建family_fortune_report表失败: {}", e.getMessage());
 		}
 		}
+
+		// 迁移146: 创建五维家庭自检记录表(P0-1 五维自检问卷,书稿终章15题)
+		try {
+			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS five_dimension_self_checks (" +
+					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+					"user_id BIGINT NOT NULL COMMENT '答卷人用户ID', " +
+					"family_member_id BIGINT COMMENT '关联家庭成员ID(可选)', " +
+					"answers_json TEXT COMMENT '用户答案JSON', " +
+					"scores_json VARCHAR(512) COMMENT '各维度得分JSON', " +
+					"total_score INT COMMENT '五维总分(满分45)', " +
+					"advice_json TEXT COMMENT '五行相生寻源建议JSON', " +
+					"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+					"INDEX idx_user_id (user_id)" +
+					") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='五维家庭自检记录'");
+			log.info("已创建five_dimension_self_checks表");
+		} catch (Exception e) {
+			log.warn("创建five_dimension_self_checks表失败: {}", e.getMessage());
+		}
 	}
 	}
 }
 }

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

@@ -0,0 +1,84 @@
+package com.etotem.cfc.controller.family;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.SelfCheckQuestionVO;
+import com.etotem.cfc.dto.SelfCheckResultVO;
+import com.etotem.cfc.dto.SubmitSelfCheckDTO;
+import com.etotem.cfc.service.FiveDimensionSelfCheckService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+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.List;
+
+/**
+ * 五维家庭自检问卷(P0-1)
+ * 对应书稿《终章-绘制你家的幸福全景图》15题自检问卷 + 五行相生寻源建议(P0-2)
+ */
+@Tag(name = "五维家庭自检", description = "15题五维自检问卷、五行相生寻源建议")
+@RestController
+@RequestMapping("/api/family/self-check")
+public class FiveDimensionSelfCheckController {
+
+    @Resource
+    private FiveDimensionSelfCheckService selfCheckService;
+
+    @Operation(summary = "获取自检问卷题目(15题静态题库)")
+    @PostMapping("/questions")
+    public Result<List<SelfCheckQuestionVO>> getQuestions(@RequestAttribute("userId") Long userId) {
+        if (userId == null) {
+            return Result.error("请先登录");
+        }
+        return Result.success(selfCheckService.getQuestions());
+    }
+
+    @Operation(summary = "提交自检问卷并计算五维得分与寻源建议")
+    @PostMapping("/submit")
+    public Result<SelfCheckResultVO> submit(@RequestBody SubmitSelfCheckDTO dto,
+                                            @RequestAttribute("userId") Long userId) {
+        if (userId == null) {
+            return Result.error("请先登录");
+        }
+        try {
+            SelfCheckResultVO vo = selfCheckService.submitSelfCheck(dto, userId);
+            return Result.success(vo);
+        } catch (IllegalArgumentException e) {
+            return Result.error(e.getMessage());
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    @Operation(summary = "获取最新一次自检结果")
+    @PostMapping("/latest")
+    public Result<SelfCheckResultVO> getLatest(@RequestAttribute("userId") Long userId) {
+        if (userId == null) {
+            return Result.error("请先登录");
+        }
+        try {
+            SelfCheckResultVO vo = selfCheckService.getLatest(userId);
+            return Result.success(vo);
+        } catch (IllegalArgumentException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    @Operation(summary = "获取自检历史记录")
+    @PostMapping("/history")
+    public Result<List<SelfCheckResultVO>> getHistory(@RequestAttribute("userId") Long userId) {
+        if (userId == null) {
+            return Result.error("请先登录");
+        }
+        try {
+            List<SelfCheckResultVO> history = selfCheckService.getHistory(userId, 20);
+            return Result.success(history);
+        } catch (IllegalArgumentException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+}

+ 41 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/SelfCheckQuestionVO.java

@@ -0,0 +1,41 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 五维家庭自检题目 VO(P0-1)
+ */
+@Data
+public class SelfCheckQuestionVO {
+
+    /** 题号(1-15) */
+    private Integer id;
+
+    /** 维度 code(body/wisdom/wealth/action/mind) */
+    private String dimension;
+
+    /** 维度中文名 */
+    private String dimensionName;
+
+    /** 五行 */
+    private String element;
+
+    /** 维度主题色 */
+    private String color;
+
+    /** 题目文本 */
+    private String text;
+
+    /** 选项列表 */
+    private List<OptionVO> options;
+
+    @Data
+    public static class OptionVO {
+        /** 选项值(A/B/C/D,可含 E 跳过) */
+        private String value;
+        /** 选项文案 */
+        private String label;
+    }
+}

+ 50 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/SelfCheckResultVO.java

@@ -0,0 +1,50 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 五维家庭自检结果 VO(P0-1)
+ */
+@Data
+public class SelfCheckResultVO {
+
+    private Long id;
+
+    private Long userId;
+
+    private Long familyMemberId;
+
+    /** 各维度得分列表(5个) */
+    private List<DimensionScoreVO> dimensions;
+
+    /** 五维总分(满分45) */
+    private Integer totalScore;
+
+    /** 五行相生寻源建议(仅低分维度 ≤3 分) */
+    private List<WuxingSourcingAdviceVO> advices;
+
+    private Date createdAt;
+
+    @Data
+    public static class DimensionScoreVO {
+        /** 维度 code */
+        private String dimension;
+        /** 维度中文名 */
+        private String name;
+        /** 五行 */
+        private String element;
+        /** 主题色 */
+        private String color;
+        /** 得分(满分9) */
+        private Integer score;
+        /** 等级:healthy健康 / attention留意 / tense紧绷 */
+        private String level;
+        /** 等级中文 */
+        private String levelName;
+        /** 结果解读 */
+        private String summary;
+    }
+}

+ 25 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/SubmitSelfCheckDTO.java

@@ -0,0 +1,25 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 五维家庭自检提交 DTO(P0-1)
+ * 对应书稿《终章-绘制你家的幸福全景图》15题自检问卷
+ */
+@Data
+public class SubmitSelfCheckDTO {
+
+    /** 关联家庭成员ID(可选,默认当前用户本人) */
+    private Long familyMemberId;
+
+    /** 答案列表:[{questionId, answer}],answer 为 A/B/C/D */
+    private List<AnswerItem> answers;
+
+    @Data
+    public static class AnswerItem {
+        private Integer questionId;
+        private String answer;
+    }
+}

+ 65 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/WuxingSourcingAdviceVO.java

@@ -0,0 +1,65 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+/**
+ * 五行相生寻源建议(P0-2 五行相生寻源建议引擎输出)
+ * 对应书稿《附录-工具包汇总》附录六五行相生速查表
+ */
+@Data
+public class WuxingSourcingAdviceVO {
+
+    /** 低分维度 code(body/wisdom/wealth/action/mind) */
+    private String dimension;
+
+    /** 低分维度中文名(身/智/富/行/心) */
+    private String dimensionName;
+
+    /** 低分维度五行(土/金/水/木/火) */
+    private String element;
+
+    /** 低分维度主题色 */
+    private String color;
+
+    /** 自检得分(满分9) */
+    private Integer score;
+
+    /** 等级:healthy健康 / attention留意 / tense紧绷 */
+    private String level;
+
+    /** 等级中文 */
+    private String levelName;
+
+    /** 相生寻源:上游维度 code */
+    private String upstreamDimension;
+
+    /** 相生寻源:上游维度中文名 */
+    private String upstreamName;
+
+    /** 相生寻源:上游五行 */
+    private String upstreamElement;
+
+    /** 相生寻源:上游主题色 */
+    private String upstreamColor;
+
+    /** 相生寻源:为什么补上游更有效 */
+    private String upstreamReason;
+
+    /** 相克寻源:压制维度 code */
+    private String restrainerDimension;
+
+    /** 相克寻源:压制维度中文名 */
+    private String restrainerName;
+
+    /** 相克寻源:压制五行 */
+    private String restrainerElement;
+
+    /** 相克寻源:压制主题色 */
+    private String restrainerColor;
+
+    /** 相克寻源:谁在压制它 */
+    private String restrainerReason;
+
+    /** 简单应对建议 */
+    private String action;
+}

+ 41 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/FiveDimensionSelfCheck.java

@@ -0,0 +1,41 @@
+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;
+
+/**
+ * 五维家庭自检记录(P0-1 五维自检问卷)
+ * 对应书稿《终章-绘制你家的幸福全景图》15题自检问卷
+ */
+@Data
+@TableName("five_dimension_self_checks")
+public class FiveDimensionSelfCheck implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 答卷人用户ID */
+    private Long userId;
+
+    /** 关联家庭成员ID(可选,默认当前用户本人) */
+    private Long familyMemberId;
+
+    /** 用户答案JSON: [{"questionId":1,"answer":"A"},...] */
+    private String answersJson;
+
+    /** 各维度得分JSON: {"body":6,"wisdom":7,"wealth":5,"action":8,"mind":4} */
+    private String scoresJson;
+
+    /** 五维总分(满分45) */
+    private Integer totalScore;
+
+    /** 寻源建议JSON(五行相生寻源引擎输出) */
+    private String adviceJson;
+
+    private Date createdAt;
+}

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

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

+ 337 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FiveDimensionSelfCheckService.java

@@ -0,0 +1,337 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.dto.SelfCheckQuestionVO;
+import com.etotem.cfc.dto.SelfCheckResultVO;
+import com.etotem.cfc.dto.SubmitSelfCheckDTO;
+import com.etotem.cfc.dto.WuxingSourcingAdviceVO;
+import com.etotem.cfc.entity.FiveDimensionSelfCheck;
+import com.etotem.cfc.mapper.FiveDimensionSelfCheckMapper;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 五维家庭自检问卷服务(P0-1)
+ * 对应书稿《终章-绘制你家的幸福全景图》15题自检问卷
+ *
+ * 计分:A=3分,B=2分,C=1分,D=0分,每维度 3 题满分 9 分
+ * 结果解读:≥7 健康 / 4-6 留意 / ≤3 紧绷(紧绷维度触发五行相生寻源建议)
+ */
+@Service
+public class FiveDimensionSelfCheckService {
+
+    private static final Logger log = LoggerFactory.getLogger(FiveDimensionSelfCheckService.class);
+
+    @Resource
+    private FiveDimensionSelfCheckMapper selfCheckMapper;
+
+    @Resource
+    private WuxingSourcingService wuxingSourcingService;
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    /** 维度中文名与五行 */
+    private static final Map<String, String[]> DIMENSION_META = new LinkedHashMap<>();
+    static {
+        DIMENSION_META.put("body", new String[]{"身", "土", "#FF8C42"});
+        DIMENSION_META.put("wisdom", new String[]{"智", "金", "#6366F1"});
+        DIMENSION_META.put("wealth", new String[]{"富", "水", "#F59E0B"});
+        DIMENSION_META.put("action", new String[]{"行", "木", "#10B981"});
+        DIMENSION_META.put("mind", new String[]{"心", "火", "#FF6B9D"});
+    }
+
+    /** 15 题静态题库(书稿终章原文) */
+    private static final List<SelfCheckQuestionVO> QUESTION_BANK = buildQuestionBank();
+
+    private static List<SelfCheckQuestionVO> buildQuestionBank() {
+        List<SelfCheckQuestionVO> bank = new ArrayList<>();
+        // 身(1-3)
+        bank.add(q(1, "body",
+                "过去一周,你每天平均睡眠时间大约是多少?",
+                "A", "7小时以上", "B", "6-7小时", "C", "5-6小时", "D", "经常小于5小时"));
+        bank.add(q(2, "body",
+                "过去一个月,你每周有几次让身体活动起来的时刻(散步、跑步、做操都可以)?",
+                "A", "几乎每天", "B", "每周3-4次", "C", "每周1-2次", "D", "几乎没有"));
+        bank.add(q(3, "body",
+                "你对自己的身体状况的整体感觉是?",
+                "A", "精力充足,很少疲劳", "B", "偶尔疲劳但能恢复", "C", "经常觉得累", "D", "长期处于疲劳状态"));
+        // 智(4-6)
+        bank.add(q(4, "wisdom",
+                "过去一个月,你有没有做过一件\"学到新东西\"的事(读书、听课、查资料、跟人请教都算)?",
+                "A", "经常有", "B", "有过几次", "C", "很少", "D", "几乎没有"));
+        bank.add(q(5, "wisdom",
+                "当你遇到不懂的问题时,你的第一反应是?",
+                "A", "主动去查、去学", "B", "会查但有时拖着", "C", "等别人告诉我", "D", "算了不想管"));
+        bank.add(q(6, "wisdom",
+                "你家孩子的学习状态,你感觉是?(如果你没有孩子,请选E跳过)",
+                "A", "孩子主动且开心", "B", "主动但有点压力", "C", "需要催促", "D", "长期抗拒", "E", "没有孩子,跳过此题"));
+        // 富(7-9)
+        bank.add(q(7, "wealth",
+                "你知道你家上个月的总支出大概是多少吗?",
+                "A", "很清楚,有记录", "B", "大概知道,没有精确记录", "C", "不太清楚", "D", "完全不知道"));
+        bank.add(q(8, "wealth",
+                "除了工资收入之外,你家有没有其他的收入来源(哪怕很小)?",
+                "A", "有,稳定且有增长", "B", "有,但不稳定", "C", "正在尝试", "D", "没有,也没想过"));
+        bank.add(q(9, "wealth",
+                "你家目前的储蓄状态让你感觉?",
+                "A", "有安全感", "B", "一般,能应对小意外", "C", "不太够", "D", "几乎没有储蓄"));
+        // 行(10-12)
+        bank.add(q(10, "action",
+                "过去一周,你跟伴侣之间有过一次\"非事务性\"的对话吗(不聊家务、孩子、钱)?",
+                "A", "有,好几次", "B", "有,一次", "C", "好像没有", "D", "已经不记得了"));
+        bank.add(q(11, "action",
+                "当你遇到困难时,你觉得自己有可以开口求助的人吗(不包括家人)?",
+                "A", "有,好几个", "B", "有一两个", "C", "不太确定", "D", "没有"));
+        bank.add(q(12, "action",
+                "你家跟双方父母的关系,整体上让你感觉?",
+                "A", "和谐且有边界", "B", "和谐但有点累", "C", "有些紧张", "D", "长期有冲突"));
+        // 心(13-15)
+        bank.add(q(13, "mind",
+                "在这个家里,你能做真实的自己吗?",
+                "A", "完全可以", "B", "大部分时候可以", "C", "有时候需要掩饰", "D", "常常觉得不能说真话"));
+        bank.add(q(14, "mind",
+                "你家有没有一些固定的\"家庭仪式\"(哪怕很小,比如睡前说晚安、周末一起做早餐)?",
+                "A", "有,且一直在坚持", "B", "有,但不稳定", "C", "以前有,现在没了", "D", "从来没有"));
+        bank.add(q(15, "mind",
+                "你对自己家庭的未来感觉是?",
+                "A", "乐观,方向明确", "B", "比较乐观,但有些不确定", "C", "有点焦虑", "D", "很焦虑,不知道怎么办"));
+        return bank;
+    }
+
+    private static SelfCheckQuestionVO q(int id, String dimension, String text, String... opts) {
+        SelfCheckQuestionVO vo = new SelfCheckQuestionVO();
+        vo.setId(id);
+        vo.setDimension(dimension);
+        String[] meta = DIMENSION_META.get(dimension);
+        vo.setDimensionName(meta[0]);
+        vo.setElement(meta[1]);
+        vo.setColor(meta[2]);
+        vo.setText(text);
+        List<SelfCheckQuestionVO.OptionVO> options = new ArrayList<>();
+        for (int i = 0; i < opts.length; i += 2) {
+            SelfCheckQuestionVO.OptionVO o = new SelfCheckQuestionVO.OptionVO();
+            o.setValue(opts[i]);
+            o.setLabel(opts[i + 1]);
+            options.add(o);
+        }
+        vo.setOptions(options);
+        return vo;
+    }
+
+    /** 获取题库(15题) */
+    public List<SelfCheckQuestionVO> getQuestions() {
+        return QUESTION_BANK;
+    }
+
+    /**
+     * 提交自检问卷并计算评分
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public SelfCheckResultVO submitSelfCheck(SubmitSelfCheckDTO dto, Long userId) {
+        if (dto.getAnswers() == null || dto.getAnswers().isEmpty()) {
+            throw new IllegalArgumentException("答案不能为空");
+        }
+        if (dto.getAnswers().size() < 10) {
+            throw new IllegalArgumentException("请完成至少10道题目");
+        }
+
+        // 题目ID → 答案
+        Map<Integer, String> answerMap = new LinkedHashMap<>();
+        for (SubmitSelfCheckDTO.AnswerItem item : dto.getAnswers()) {
+            if (item.getQuestionId() == null || item.getAnswer() == null || item.getAnswer().isEmpty()) {
+                continue;
+            }
+            answerMap.put(item.getQuestionId(), item.getAnswer().trim().toUpperCase());
+        }
+
+        // 逐维度计分(A=3,B=2,C=1,D=0;E=跳过不计入)
+        List<SelfCheckResultVO.DimensionScoreVO> dimensions = new ArrayList<>();
+        int totalScore = 0;
+        for (Map.Entry<String, String[]> entry : DIMENSION_META.entrySet()) {
+            String dim = entry.getKey();
+            List<SelfCheckQuestionVO> dimQuestions = new ArrayList<>();
+            for (SelfCheckQuestionVO qv : QUESTION_BANK) {
+                if (dim.equals(qv.getDimension())) {
+                    dimQuestions.add(qv);
+                }
+            }
+            int sum = 0;
+            int answered = 0;
+            for (SelfCheckQuestionVO qv : dimQuestions) {
+                String ans = answerMap.get(qv.getId());
+                if (ans == null || "E".equals(ans)) {
+                    continue;
+                }
+                int score = scoreOf(ans);
+                sum += score;
+                answered++;
+            }
+            if (answered == 0) {
+                throw new IllegalArgumentException("维度[" + dim + "]至少回答1题");
+            }
+            // 按实际答题数折算到 9 分制(跳过1题时仍按比例折算)
+            int dimScore = new BigDecimal(sum * 3.0 / answered).setScale(0, RoundingMode.HALF_UP).intValue();
+            if (dimScore > 9) {
+                dimScore = 9;
+            }
+            totalScore += dimScore;
+            dimensions.add(buildDimensionScoreVO(dim, dimScore));
+        }
+
+        // 生成寻源建议(仅 ≤3 紧绷维度)
+        Map<String, Integer> scoreMap = new LinkedHashMap<>();
+        for (SelfCheckResultVO.DimensionScoreVO dvo : dimensions) {
+            scoreMap.put(dvo.getDimension(), dvo.getScore());
+        }
+        List<WuxingSourcingAdviceVO> advices = wuxingSourcingService.getAdvicesForLowScores(scoreMap);
+
+        // 保存记录
+        FiveDimensionSelfCheck record = new FiveDimensionSelfCheck();
+        record.setUserId(userId);
+        record.setFamilyMemberId(dto.getFamilyMemberId());
+        record.setAnswersJson(toJson(dto.getAnswers()));
+        record.setScoresJson(toJson(scoreMap));
+        record.setTotalScore(totalScore);
+        record.setAdviceJson(toJson(advices));
+        record.setCreatedAt(new Date());
+        selfCheckMapper.insert(record);
+
+        SelfCheckResultVO vo = new SelfCheckResultVO();
+        vo.setId(record.getId());
+        vo.setUserId(userId);
+        vo.setFamilyMemberId(record.getFamilyMemberId());
+        vo.setDimensions(dimensions);
+        vo.setTotalScore(totalScore);
+        vo.setAdvices(advices);
+        vo.setCreatedAt(record.getCreatedAt());
+        return vo;
+    }
+
+    /** 获取最新一次自检结果 */
+    public SelfCheckResultVO getLatest(Long userId) {
+        LambdaQueryWrapper<FiveDimensionSelfCheck> wrapper = new LambdaQueryWrapper<FiveDimensionSelfCheck>()
+                .eq(FiveDimensionSelfCheck::getUserId, userId)
+                .orderByDesc(FiveDimensionSelfCheck::getCreatedAt)
+                .last("LIMIT 1");
+        FiveDimensionSelfCheck record = selfCheckMapper.selectOne(wrapper);
+        return record != null ? toVO(record) : null;
+    }
+
+    /** 获取自检历史记录 */
+    public List<SelfCheckResultVO> getHistory(Long userId, int limit) {
+        if (limit <= 0 || limit > 50) {
+            limit = 20;
+        }
+        LambdaQueryWrapper<FiveDimensionSelfCheck> wrapper = new LambdaQueryWrapper<FiveDimensionSelfCheck>()
+                .eq(FiveDimensionSelfCheck::getUserId, userId)
+                .orderByDesc(FiveDimensionSelfCheck::getCreatedAt)
+                .last("LIMIT " + limit);
+        List<FiveDimensionSelfCheck> records = selfCheckMapper.selectList(wrapper);
+        List<SelfCheckResultVO> result = new ArrayList<>();
+        for (FiveDimensionSelfCheck record : records) {
+            result.add(toVO(record));
+        }
+        return result;
+    }
+
+    // === 内部方法 ===
+
+    private SelfCheckResultVO.DimensionScoreVO buildDimensionScoreVO(String dimension, int score) {
+        String[] meta = DIMENSION_META.get(dimension);
+        SelfCheckResultVO.DimensionScoreVO vo = new SelfCheckResultVO.DimensionScoreVO();
+        vo.setDimension(dimension);
+        vo.setName(meta[0]);
+        vo.setElement(meta[1]);
+        vo.setColor(meta[2]);
+        vo.setScore(score);
+        String level = wuxingSourcingService.levelOf(score);
+        vo.setLevel(level);
+        vo.setLevelName(wuxingSourcingService.levelName(level));
+        vo.setSummary(summaryOf(level));
+        return vo;
+    }
+
+    /** 结果解读(书稿终章) */
+    private String summaryOf(String level) {
+        if ("healthy".equals(level)) {
+            return "这个维度比较健康。继续保持就好,不用刻意加码。";
+        }
+        if ("attention".equals(level)) {
+            return "这个维度需要留意。找到最小输入,从这个维度开始。";
+        }
+        return "这个维度可能已经很紧绷了。不要急着补短板,看看它的上游维度(五行相生)——也许补上游比直接补它更有效。";
+    }
+
+    private int scoreOf(String answer) {
+        if ("A".equals(answer)) return 3;
+        if ("B".equals(answer)) return 2;
+        if ("C".equals(answer)) return 1;
+        return 0;
+    }
+
+    private String toJson(Object obj) {
+        try {
+            return objectMapper.writeValueAsString(obj);
+        } catch (JsonProcessingException e) {
+            log.warn("JSON序列化失败: {}", e.getMessage());
+            return "[]";
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private SelfCheckResultVO toVO(FiveDimensionSelfCheck record) {
+        SelfCheckResultVO vo = new SelfCheckResultVO();
+        vo.setId(record.getId());
+        vo.setUserId(record.getUserId());
+        vo.setFamilyMemberId(record.getFamilyMemberId());
+        vo.setTotalScore(record.getTotalScore());
+        vo.setCreatedAt(record.getCreatedAt());
+
+        // 解析各维度得分
+        Map<String, Integer> scoreMap = new LinkedHashMap<>();
+        if (record.getScoresJson() != null) {
+            try {
+                scoreMap = objectMapper.readValue(record.getScoresJson(), LinkedHashMap.class);
+            } catch (Exception e) {
+                log.warn("解析scoresJson失败: {}", e.getMessage());
+            }
+        }
+        List<SelfCheckResultVO.DimensionScoreVO> dimensions = new ArrayList<>();
+        for (Map.Entry<String, String[]> entry : DIMENSION_META.entrySet()) {
+            Integer score = scoreMap.get(entry.getKey());
+            if (score != null) {
+                dimensions.add(buildDimensionScoreVO(entry.getKey(), score));
+            }
+        }
+        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<>());
+        }
+        return vo;
+    }
+}

+ 169 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/WuxingSourcingService.java

@@ -0,0 +1,169 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.dto.WuxingSourcingAdviceVO;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 五行相生寻源建议引擎(P0-2)
+ * 对应书稿《附录-工具包汇总》附录六「五行相生速查表(主动健康寻源表)」
+ *
+ * 核心逻辑:
+ * 1. 当一个维度亮红灯(分数低)时,先别急着只补它,用两步寻源:
+ *    - 相生寻源:看它的上游(谁生它)——是不是上游滋养不足?
+ *    - 相克寻源:看谁在压制它——是不是哪个维度过度膨胀?
+ * 2. 依据速查表输出:上游维度、压制维度、简单应对
+ *
+ * 五行相生:身(土)→智(金)→富(水)→行(木)→心(火)→身
+ * 五行相克:身克富、智克行、富克心、行克身、心克智
+ */
+@Service
+public class WuxingSourcingService {
+
+    /** 维度元信息:code → 中文名/五行/颜色 */
+    private static final Map<String, String[]> DIMENSION_META = new LinkedHashMap<>();
+    static {
+        DIMENSION_META.put("body", new String[]{"身", "土", "#FF8C42"});
+        DIMENSION_META.put("wisdom", new String[]{"智", "金", "#6366F1"});
+        DIMENSION_META.put("wealth", new String[]{"富", "水", "#F59E0B"});
+        DIMENSION_META.put("action", new String[]{"行", "木", "#10B981"});
+        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 紧绷
+     */
+    public String levelOf(int score) {
+        if (score >= 7) {
+            return "healthy";
+        }
+        if (score >= 4) {
+            return "attention";
+        }
+        return "tense";
+    }
+
+    /** 等级中文名 */
+    public String levelName(String level) {
+        if (level == null) {
+            return "";
+        }
+        if ("healthy".equals(level)) {
+            return "健康";
+        }
+        if ("attention".equals(level)) {
+            return "留意";
+        }
+        if ("tense".equals(level)) {
+            return "紧绷";
+        }
+        return level;
+    }
+
+    /** 维度中文名 */
+    public String dimensionName(String dimension) {
+        String[] meta = DIMENSION_META.get(dimension);
+        return meta == null ? dimension : meta[0];
+    }
+}

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

@@ -3716,3 +3716,18 @@ CREATE TABLE IF NOT EXISTS family_fortune_report (
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     INDEX idx_family_id (family_id)
     INDEX idx_family_id (family_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭天盘周报导出记录';
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭天盘周报导出记录';
+
+-- ============================================================
+-- 五维家庭自检记录表(迁移146,P0-1 五维自检问卷,书稿终章15题)
+-- ============================================================
+CREATE TABLE IF NOT EXISTS five_dimension_self_checks (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL COMMENT '答卷人用户ID',
+    family_member_id BIGINT COMMENT '关联家庭成员ID(可选)',
+    answers_json TEXT COMMENT '用户答案JSON',
+    scores_json VARCHAR(512) COMMENT '各维度得分JSON',
+    total_score INT COMMENT '五维总分(满分45)',
+    advice_json TEXT COMMENT '五行相生寻源建议JSON',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_user_id (user_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='五维家庭自检记录';

+ 12 - 0
cfc-frontend/pages.json

@@ -1030,6 +1030,18 @@
           "style": {
           "style": {
             "navigationBarTitleText": "添加家庭成员"
             "navigationBarTitleText": "添加家庭成员"
           }
           }
+        },
+        {
+          "path": "self-check",
+          "style": {
+            "navigationBarTitleText": "五维家庭自检"
+          }
+        },
+        {
+          "path": "self-check-result",
+          "style": {
+            "navigationBarTitleText": "自检报告"
+          }
         }
         }
       ]
       ]
     },
     },

+ 670 - 0
cfc-frontend/pages/family/self-check-result.vue

@@ -0,0 +1,670 @@
+<template>
+  <view class="scr-page">
+    <!-- Loading -->
+    <view class="scr-loading" v-if="loading">
+      <text class="scr-loading-text">加载中...</text>
+    </view>
+
+    <template v-else>
+      <!-- 总分横幅 -->
+      <view class="scr-hero">
+        <view class="scr-hero-badge">
+          <text class="scr-hero-title">五维幸福总分</text>
+          <view class="scr-hero-score-row">
+            <text class="scr-hero-score">{{ result.totalScore }}</text>
+            <text class="scr-hero-total">/45</text>
+          </view>
+          <view class="scr-hero-level" :class="'level-' + overallLevel">
+            <text>{{ overallLevelName }}</text>
+          </view>
+        </view>
+        <text class="scr-hero-date" v-if="result.createdAt">自检时间:{{ formatDate(result.createdAt) }}</text>
+      </view>
+
+      <!-- 雷达图 -->
+      <view class="scr-card">
+        <view class="scr-card-title">
+          <text class="scr-card-title-text">五维能量雷达</text>
+          <text class="scr-card-sub">身·智·富·行·心</text>
+        </view>
+        <view class="scr-canvas-wrap">
+          <canvas
+            type="2d"
+            id="selfCheckRadar"
+            class="scr-canvas"
+            @ready="onCanvasReady"
+          ></canvas>
+        </view>
+      </view>
+
+      <!-- 维度得分 -->
+      <view class="scr-card">
+        <view class="scr-card-title">
+          <text class="scr-card-title-text">维度得分</text>
+          <text class="scr-card-sub">满分9分/维</text>
+        </view>
+        <view class="scr-dim-list">
+          <view class="scr-dim-item" v-for="dim in result.dimensions" :key="dim.dimension">
+            <view class="scr-dim-head">
+              <view class="scr-dim-label">
+                <view class="scr-dim-dot" :style="{ background: dim.color }"></view>
+                <text class="scr-dim-name">{{ dim.name }}</text>
+                <text class="scr-dim-element">{{ dim.element }}</text>
+              </view>
+              <view class="scr-dim-score-wrap">
+                <text class="scr-dim-score" :style="{ color: dim.color }">{{ dim.score }}</text>
+                <text class="scr-dim-score-max">/9</text>
+              </view>
+            </view>
+            <view class="scr-dim-bar">
+              <view class="scr-dim-bar-fill" :style="{ width: (dim.score / 9 * 100) + '%', background: dim.color }"></view>
+            </view>
+            <view class="scr-dim-foot">
+              <text class="scr-dim-level" :class="'lvl-' + dim.level">{{ dim.levelName }}</text>
+              <text class="scr-dim-summary">{{ dim.summary }}</text>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <!-- 五行相生寻源建议(P0-2) -->
+      <view class="scr-card" v-if="result.advices && result.advices.length > 0">
+        <view class="scr-card-title">
+          <text class="scr-card-title-text">五行相生 · 寻源建议</text>
+          <text class="scr-card-sub">低分维度 {{ result.advices.length }} 项</text>
+        </view>
+        <view class="scr-advice-intro">低于6分的维度,先别硬补——从"生"它的上游借力,同时避开"克"它的压制源:</view>
+        <view class="scr-advice-item" v-for="adv in result.advices" :key="adv.dimension">
+          <!-- 低分维度 -->
+          <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.element }}</text>
+            </view>
+            <text class="scr-advice-dim-score">{{ adv.score }}分</text>
+          </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>
+      </view>
+
+      <!-- 全部健康提示 -->
+      <view class="scr-card" v-else>
+        <view class="scr-all-healthy">
+          <text class="scr-all-healthy-icon">🌿</text>
+          <text class="scr-all-healthy-text">五维状态均健康,请继续保持这份平衡!</text>
+        </view>
+      </view>
+
+      <!-- 历史与重测 -->
+      <view class="scr-footer">
+        <button class="scr-btn scr-btn-primary" @click="goHistory">历史记录</button>
+        <button class="scr-btn scr-btn-outline" @click="retake">重新自检</button>
+      </view>
+    </template>
+  </view>
+</template>
+
+<script>
+import { getSelfCheckLatest, getSelfCheckHistory } from '@/utils/api'
+
+export default {
+  data() {
+    return {
+      loading: true,
+      result: null,
+      selfCheckId: '',
+      canvasContext: null,
+      canvasWidth: 340,
+      canvasHeight: 340,
+      // 雷达图轴序(对应 tianpan 木火土金水,顺时针)
+      axes: ['木', '火', '土', '金', '水'],
+      elementKeys: ['action', 'mind', 'body', 'wisdom', 'wealth'],
+      elementColors: ['#10B981', '#FF6B9D', '#FF8C42', '#6366F1', '#F59E0B'],
+      levelMap: {
+        healthy: '健康',
+        attention: '留意',
+        tense: '紧绷'
+      }
+    }
+  },
+  computed: {
+    overallLevel: function() {
+      var total = this.result ? (this.result.totalScore || 0) : 0
+      if (total >= 35) return 'healthy'
+      if (total >= 20) return 'attention'
+      return 'tense'
+    },
+    overallLevelName: function() {
+      return this.levelMap[this.overallLevel] || '留意'
+    }
+  },
+  onLoad: function(options) {
+    this.selfCheckId = options && options.id ? options.id : ''
+    this.loadResult()
+  },
+  methods: {
+    loadResult: function() {
+      var self = this
+      // 优先使用刚提交时缓存的结果,避免多一次请求
+      var cached = uni.getStorageSync('lastSelfCheckResult')
+      if (cached && cached.id) {
+        this.result = cached
+        this.loading = false
+        this.drawRadar()
+        return
+      }
+      getSelfCheckLatest().then(function(res) {
+        self.loading = false
+        if (res.code === 200 && res.data) {
+          self.result = res.data
+          self.drawRadar()
+        } else {
+          uni.showToast({ title: (res && res.message) || '暂无自检记录', icon: 'none' })
+        }
+      }).catch(function() {
+        self.loading = false
+        uni.showToast({ title: '加载失败', icon: 'none' })
+      })
+    },
+    onCanvasReady: function(res) {
+      if (!res || !res[0] || !res[0].node) {
+        return
+      }
+      var ctx = res[0].node.getContext('2d')
+      var dpr = uni.getSystemInfoSync().pixelRatio
+      this.canvasContext = ctx
+      this.canvasContext.scale(dpr, dpr)
+      this.drawRadar()
+    },
+    drawRadar: function() {
+      if (!this.canvasContext || !this.result) {
+        return
+      }
+      var ctx = this.canvasContext
+      var w = this.canvasWidth
+      var h = this.canvasHeight
+      var cx = w / 2
+      var cy = h / 2
+      var r = w * 0.34
+      var angleStep = (Math.PI * 2) / 5
+
+      // 清空画布
+      ctx.clearRect(0, 0, w, h)
+      ctx.fillStyle = '#FFF8F0'
+      ctx.fillRect(0, 0, w, h)
+
+      // 维度分数映射
+      var scores = {}
+      var dims = this.result.dimensions || []
+      for (var i = 0; i < dims.length; i++) {
+        scores[dims[i].dimension] = dims[i].score || 0
+      }
+
+      // 背景五边形(三层网格)
+      for (var layer = 1; layer <= 3; layer++) {
+        ctx.beginPath()
+        for (var g = 0; g < 5; g++) {
+          var gAngle = g * angleStep - Math.PI / 2
+          var gx = cx + r * (layer / 3) * Math.cos(gAngle)
+          var gy = cy + r * (layer / 3) * Math.sin(gAngle)
+          if (g === 0) {
+            ctx.moveTo(gx, gy)
+          } else {
+            ctx.lineTo(gx, gy)
+          }
+        }
+        ctx.closePath()
+        ctx.strokeStyle = 'rgba(249, 114, 22, 0.15)'
+        ctx.lineWidth = 1
+        ctx.stroke()
+      }
+
+      // 轴线 + 标签
+      for (var j = 0; j < 5; j++) {
+        var a2 = j * angleStep - Math.PI / 2
+        var lx = cx + r * Math.cos(a2)
+        var ly = cy + r * Math.sin(a2)
+        ctx.beginPath()
+        ctx.moveTo(cx, cy)
+        ctx.lineTo(lx, ly)
+        ctx.strokeStyle = this.elementColors[j] + '44'
+        ctx.lineWidth = 1
+        ctx.stroke()
+
+        var labelX = cx + (r + 18) * Math.cos(a2)
+        var labelY = cy + (r + 18) * Math.sin(a2)
+        ctx.fillStyle = this.elementColors[j]
+        ctx.font = 'bold 13px sans-serif'
+        ctx.textAlign = 'center'
+        ctx.textBaseline = 'middle'
+        ctx.fillText(this.axes[j], labelX, labelY)
+      }
+
+      // 数据驱动多边形
+      ctx.beginPath()
+      for (var m = 0; m < 5; m++) {
+        var ang = m * angleStep - Math.PI / 2
+        var key = this.elementKeys[m]
+        var ratio = (scores[key] || 0) / 9
+        var dataR = r * ratio
+        var dx = cx + dataR * Math.cos(ang)
+        var dy = cy + dataR * Math.sin(ang)
+        if (m === 0) {
+          ctx.moveTo(dx, dy)
+        } else {
+          ctx.lineTo(dx, dy)
+        }
+      }
+      ctx.closePath()
+      ctx.fillStyle = 'rgba(249, 114, 22, 0.2)'
+      ctx.fill()
+      ctx.strokeStyle = '#F97316'
+      ctx.lineWidth = 2
+      ctx.stroke()
+
+      // 数据节点圆点 + 分数标签
+      for (var n = 0; n < 5; n++) {
+        var ang2 = n * angleStep - Math.PI / 2
+        var key2 = this.elementKeys[n]
+        var ratio2 = (scores[key2] || 0) / 9
+        var dataR2 = r * ratio2
+        var px = cx + dataR2 * Math.cos(ang2)
+        var py = cy + dataR2 * Math.sin(ang2)
+
+        ctx.beginPath()
+        ctx.arc(px, py, 4, 0, Math.PI * 2)
+        ctx.fillStyle = this.elementColors[n]
+        ctx.fill()
+
+        var pctLabelR = dataR2 + (ratio2 > 0.35 ? -16 : 18)
+        var pLabelX = cx + pctLabelR * Math.cos(ang2)
+        var pLabelY = cy + pctLabelR * Math.sin(ang2)
+        ctx.fillStyle = this.elementColors[n]
+        ctx.font = 'bold 11px sans-serif'
+        ctx.textAlign = 'center'
+        ctx.textBaseline = 'middle'
+        ctx.fillText(String(scores[key2] || 0), pLabelX, pLabelY)
+      }
+    },
+    formatDate: function(dateStr) {
+      if (!dateStr) {
+        return ''
+      }
+      var d = new Date(dateStr)
+      var pad = function(n) { return n < 10 ? '0' + n : '' + n }
+      return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes())
+    },
+    goHistory: function() {
+      getSelfCheckHistory().then(function(res) {
+        if (res.code === 200 && res.data && res.data.length > 0) {
+          uni.showToast({ title: '已生成 ' + res.data.length + ' 条记录', icon: 'none' })
+        } else {
+          uni.showToast({ title: '暂无历史记录', icon: 'none' })
+        }
+      }).catch(function() {
+        uni.showToast({ title: '暂无历史记录', icon: 'none' })
+      })
+    },
+    retake: function() {
+      uni.redirectTo({ url: '/pages/family/self-check' })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.scr-page {
+  min-height: 100vh;
+  background: #F5FAFE;
+  padding: 24rpx 30rpx 120rpx;
+  box-sizing: border-box;
+}
+.scr-loading {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 60vh;
+}
+.scr-loading-text {
+  font-size: 28rpx;
+  color: #999;
+}
+
+/* 总分横幅 */
+.scr-hero {
+  background: linear-gradient(135deg, #F97316, #FF8C42);
+  border-radius: 24rpx;
+  padding: 40rpx 30rpx;
+  margin-bottom: 24rpx;
+  box-shadow: 0 6rpx 20rpx rgba(249, 114, 22, 0.25);
+}
+.scr-hero-badge {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.scr-hero-title {
+  font-size: 26rpx;
+  color: rgba(255, 255, 255, 0.9);
+}
+.scr-hero-score-row {
+  display: flex;
+  align-items: baseline;
+  margin-top: 10rpx;
+}
+.scr-hero-score {
+  font-size: 88rpx;
+  font-weight: 700;
+  color: #fff;
+}
+.scr-hero-total {
+  font-size: 30rpx;
+  color: rgba(255, 255, 255, 0.7);
+  margin-left: 6rpx;
+}
+.scr-hero-level {
+  margin-top: 14rpx;
+  padding: 8rpx 28rpx;
+  border-radius: 24rpx;
+  font-size: 26rpx;
+  font-weight: 600;
+  background: rgba(255, 255, 255, 0.92);
+}
+.scr-hero-level.level-healthy { color: #2E7D32; }
+.scr-hero-level.level-attention { color: #E65100; }
+.scr-hero-level.level-tense { color: #C62828; }
+.scr-hero-date {
+  display: block;
+  text-align: center;
+  font-size: 22rpx;
+  color: rgba(255, 255, 255, 0.8);
+  margin-top: 16rpx;
+}
+
+/* 通用卡片 */
+.scr-card {
+  background: #fff;
+  border-radius: 24rpx;
+  padding: 30rpx 26rpx;
+  margin-bottom: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+}
+.scr-card-title {
+  display: flex;
+  align-items: baseline;
+  justify-content: space-between;
+  margin-bottom: 24rpx;
+}
+.scr-card-title-text {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333;
+}
+.scr-card-sub {
+  font-size: 22rpx;
+  color: #999;
+}
+
+/* 雷达图 */
+.scr-canvas-wrap {
+  display: flex;
+  justify-content: center;
+}
+.scr-canvas {
+  width: 340px;
+  height: 340px;
+}
+
+/* 维度得分 */
+.scr-dim-list {
+  display: flex;
+  flex-direction: column;
+}
+.scr-dim-item {
+  margin-bottom: 28rpx;
+}
+.scr-dim-item:last-child {
+  margin-bottom: 0;
+}
+.scr-dim-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.scr-dim-label {
+  display: flex;
+  align-items: center;
+}
+.scr-dim-dot {
+  width: 28rpx;
+  height: 28rpx;
+  border-radius: 50%;
+  margin-right: 12rpx;
+}
+.scr-dim-name {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+}
+.scr-dim-element {
+  font-size: 22rpx;
+  color: #999;
+  margin-left: 10rpx;
+}
+.scr-dim-score-wrap {
+  display: flex;
+  align-items: baseline;
+}
+.scr-dim-score {
+  font-size: 40rpx;
+  font-weight: 700;
+}
+.scr-dim-score-max {
+  font-size: 22rpx;
+  color: #BBB;
+}
+.scr-dim-bar {
+  height: 12rpx;
+  background: #F0F0F0;
+  border-radius: 6rpx;
+  margin-top: 14rpx;
+  overflow: hidden;
+}
+.scr-dim-bar-fill {
+  height: 100%;
+  border-radius: 6rpx;
+  transition: width 0.5s ease;
+}
+.scr-dim-foot {
+  display: flex;
+  align-items: center;
+  margin-top: 12rpx;
+}
+.scr-dim-level {
+  font-size: 22rpx;
+  padding: 4rpx 16rpx;
+  border-radius: 16rpx;
+  margin-right: 14rpx;
+  flex-shrink: 0;
+}
+.scr-dim-level.lvl-healthy { background: #E8F5E9; color: #2E7D32; }
+.scr-dim-level.lvl-attention { background: #FFF8E1; color: #F57F17; }
+.scr-dim-level.lvl-tense { background: #FFEBEE; color: #C62828; }
+.scr-dim-summary {
+  flex: 1;
+  font-size: 24rpx;
+  color: #888;
+  line-height: 1.4;
+}
+
+/* 寻源建议 */
+.scr-advice-intro {
+  font-size: 24rpx;
+  color: #666;
+  line-height: 1.6;
+  background: #FFF7ED;
+  border-radius: 12rpx;
+  padding: 16rpx 20rpx;
+  margin-bottom: 24rpx;
+}
+.scr-advice-item {
+  border: 2rpx solid #F0E6DC;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+}
+.scr-advice-item:last-child {
+  margin-bottom: 0;
+}
+.scr-advice-dim-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 18rpx;
+}
+.scr-advice-dim-badge {
+  display: flex;
+  align-items: center;
+  padding: 8rpx 20rpx;
+  border-radius: 20rpx;
+}
+.scr-advice-dim-name {
+  font-size: 28rpx;
+  font-weight: 700;
+  color: #fff;
+  margin-right: 8rpx;
+}
+.scr-advice-dim-elem {
+  font-size: 22rpx;
+  color: rgba(255, 255, 255, 0.9);
+}
+.scr-advice-dim-score {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #C62828;
+}
+.scr-advice-flow {
+  display: flex;
+  align-items: flex-start;
+  margin-bottom: 12rpx;
+}
+.scr-flow-node {
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+  width: 200rpx;
+}
+.scr-flow-dot {
+  width: 20rpx;
+  height: 20rpx;
+  border-radius: 50%;
+  margin-right: 8rpx;
+  flex-shrink: 0;
+}
+.scr-flow-label {
+  font-size: 24rpx;
+  font-weight: 600;
+  color: #333;
+}
+.scr-flow-arrow {
+  flex-shrink: 0;
+  font-size: 24rpx;
+  color: #F97316;
+  margin: 0 10rpx;
+}
+.scr-flow-action {
+  flex: 1;
+  font-size: 24rpx;
+  color: #666;
+  line-height: 1.5;
+}
+.scr-advice-action {
+  display: flex;
+  align-items: flex-start;
+  background: #F5FAFE;
+  border-radius: 12rpx;
+  padding: 16rpx 20rpx;
+  margin-top: 10rpx;
+}
+.scr-advice-action-label {
+  flex-shrink: 0;
+  font-size: 22rpx;
+  font-weight: 600;
+  color: #F97316;
+  margin-right: 12rpx;
+  padding: 2rpx 0;
+}
+.scr-advice-action-text {
+  flex: 1;
+  font-size: 24rpx;
+  color: #333;
+  line-height: 1.5;
+}
+
+/* 全部健康 */
+.scr-all-healthy {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 30rpx 0;
+}
+.scr-all-healthy-icon {
+  font-size: 72rpx;
+  margin-bottom: 16rpx;
+}
+.scr-all-healthy-text {
+  font-size: 28rpx;
+  color: #666;
+}
+
+/* 底部按钮 */
+.scr-footer {
+  display: flex;
+  gap: 20rpx;
+  margin-top: 20rpx;
+}
+.scr-btn {
+  flex: 1;
+  height: 88rpx;
+  line-height: 88rpx;
+  font-size: 30rpx;
+  border-radius: 44rpx;
+  text-align: center;
+  border: none;
+}
+.scr-btn-primary {
+  background: linear-gradient(135deg, #F97316, #FF8C42);
+  color: #fff;
+  font-weight: 600;
+}
+.scr-btn-outline {
+  background: #fff;
+  color: #F97316;
+  border: 2rpx solid #F97316;
+}
+</style>

+ 418 - 0
cfc-frontend/pages/family/self-check.vue

@@ -0,0 +1,418 @@
+<template>
+  <view class="sc-page">
+    <!-- 顶部说明 -->
+    <view class="sc-hero">
+      <text class="sc-hero-title">五维家庭自检</text>
+      <text class="sc-hero-sub">15题 · 约5分钟 · 生成五行相生寻源建议</text>
+    </view>
+
+    <!-- 加载 -->
+    <view v-if="loading" class="sc-loading">
+      <text class="sc-loading-text">加载中...</text>
+    </view>
+
+    <template v-else>
+      <!-- 维度导航 -->
+      <view class="sc-dim-nav">
+        <view v-for="dim in dimensions" :key="dim.code" class="sc-dim-nav-item">
+          <view class="sc-dim-nav-dot" :style="{ background: dim.color }">
+            <text class="sc-dim-nav-char">{{ dim.name }}</text>
+          </view>
+          <view class="sc-dim-nav-dots">
+            <view
+              v-for="q in dim.questions"
+              :key="q.id"
+              class="sc-dim-nav-dot-mini"
+              :class="{ done: answers[q.id] }"
+              :style="answers[q.id] ? { background: dim.color } : {}"
+            ></view>
+          </view>
+        </view>
+      </view>
+
+      <!-- 进度条 -->
+      <view class="sc-progress-row">
+        <text class="sc-progress-text">已完成 {{ answeredCount }}/{{ questions.length }} 题</text>
+        <text class="sc-progress-pct">{{ progress }}%</text>
+      </view>
+      <view class="sc-progress-bar">
+        <view class="sc-progress-fill" :style="{ width: progress + '%' }"></view>
+      </view>
+
+      <!-- 题目分组(按维度) -->
+      <view v-for="dim in dimensions" :key="dim.code" class="sc-section">
+        <view class="sc-section-header" :style="{ borderLeftColor: dim.color }">
+          <text class="sc-section-name">{{ dim.name }}</text>
+          <text class="sc-section-element" :style="{ color: dim.color }">{{ dim.element }}</text>
+          <text class="sc-section-slogan">{{ dim.slogan }}</text>
+          <text class="sc-section-count">{{ dim.answeredCount }}/{{ dim.questions.length }}</text>
+        </view>
+
+        <view v-for="q in dim.questions" :key="q.id" class="sc-question">
+          <text class="sc-q-text">{{ q.text }}</text>
+          <view class="sc-opts">
+            <view
+              v-for="opt in q.options"
+              :key="opt.value"
+              class="sc-opt"
+              :class="{ 'sc-opt-sel': answers[q.id] === opt.value }"
+              @click="selectOption(q.id, opt.value)"
+            >
+              <text class="sc-opt-label">{{ opt.label }}</text>
+              <view class="sc-opt-radio" :class="{ 'sc-opt-radio-on': answers[q.id] === opt.value }"></view>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <!-- 提交 -->
+      <view class="sc-footer">
+        <button class="sc-submit-btn" :disabled="!canSubmit || submitLoading" @click="submit">
+          {{ submitLoading ? '提交中...' : '提交自检' }}
+        </button>
+        <text class="sc-submit-tip" v-if="!canSubmit && !submitLoading">至少完成10题,且每个维度至少回答1题</text>
+      </view>
+    </template>
+  </view>
+</template>
+
+<script>
+import { getSelfCheckQuestions, submitSelfCheck } from '@/utils/api'
+
+export default {
+  data() {
+    return {
+      loading: true,
+      submitLoading: false,
+      questions: [],
+      answers: {},
+      dimMeta: {
+        body: { name: '身', element: '土', color: '#FF8C42', slogan: '主动健康' },
+        wisdom: { name: '智', element: '金', color: '#6366F1', slogan: '赋能未来' },
+        wealth: { name: '富', element: '水', color: '#F59E0B', slogan: '利他创富' },
+        action: { name: '行', element: '木', color: '#10B981', slogan: '人际和谐' },
+        mind: { name: '心', element: '火', color: '#FF6B9D', slogan: '大爱相随' }
+      }
+    }
+  },
+  computed: {
+    dimensions: function() {
+      var order = ['body', 'wisdom', 'wealth', 'action', 'mind']
+      var meta = this.dimMeta
+      var result = []
+      for (var i = 0; i < order.length; i++) {
+        var code = order[i]
+        var dimQuestions = this.questions.filter(function(q) { return q.dimension === code })
+        if (dimQuestions.length === 0) {
+          continue
+        }
+        var self = this
+        var answered = dimQuestions.filter(function(q) { return self.answers[q.id] }).length
+        var m = meta[code] || {}
+        result.push({
+          code: code,
+          name: m.name || dimQuestions[0].dimensionName || code,
+          element: m.element || dimQuestions[0].element || '',
+          color: m.color || dimQuestions[0].color || '#F97316',
+          slogan: m.slogan || '',
+          questions: dimQuestions,
+          answeredCount: answered
+        })
+      }
+      return result
+    },
+    answeredCount: function() {
+      return Object.keys(this.answers).length
+    },
+    progress: function() {
+      if (!this.questions.length) {
+        return 0
+      }
+      return Math.round(this.answeredCount / this.questions.length * 100)
+    },
+    canSubmit: function() {
+      if (this.answeredCount < 10) {
+        return false
+      }
+      var dims = this.dimensions
+      for (var i = 0; i < dims.length; i++) {
+        if (dims[i].answeredCount < 1) {
+          return false
+        }
+      }
+      return true
+    }
+  },
+  methods: {
+    loadQuestions: function() {
+      var self = this
+      getSelfCheckQuestions().then(function(res) {
+        self.loading = false
+        if (res.code === 200 && res.data) {
+          self.questions = res.data || []
+        } else {
+          uni.showToast({ title: (res && res.message) || '加载失败', icon: 'none' })
+        }
+      }).catch(function() {
+        self.loading = false
+        uni.showToast({ title: '网络异常,请重试', icon: 'none' })
+      })
+    },
+    selectOption: function(questionId, value) {
+      var next = {}
+      var keys = Object.keys(this.answers)
+      for (var i = 0; i < keys.length; i++) {
+        next[keys[i]] = this.answers[keys[i]]
+      }
+      next[questionId] = value
+      this.answers = next
+    },
+    submit: function() {
+      var self = this
+      if (!this.canSubmit || this.submitLoading) {
+        return
+      }
+      this.submitLoading = true
+      var answersList = []
+      var ids = Object.keys(this.answers)
+      for (var i = 0; i < ids.length; i++) {
+        answersList.push({ questionId: Number(ids[i]), answer: this.answers[ids[i]] })
+      }
+      submitSelfCheck({ answers: answersList }).then(function(res) {
+        self.submitLoading = false
+        if (res.code === 200 && res.data) {
+          uni.setStorageSync('lastSelfCheckResult', res.data)
+          uni.navigateTo({ url: '/pages/family/self-check-result?id=' + res.data.id })
+        } else {
+          uni.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
+        }
+      }).catch(function() {
+        self.submitLoading = false
+        uni.showToast({ title: '提交失败,请重试', icon: 'none' })
+      })
+    }
+  },
+  onLoad: function() {
+    this.loadQuestions()
+  }
+}
+</script>
+
+<style scoped>
+.sc-page {
+  min-height: 100vh;
+  background: #F5FAFE;
+  padding: 24rpx 30rpx 120rpx;
+  box-sizing: border-box;
+}
+.sc-hero {
+  padding: 30rpx 10rpx 20rpx;
+}
+.sc-hero-title {
+  display: block;
+  font-size: 40rpx;
+  font-weight: 700;
+  color: #333;
+}
+.sc-hero-sub {
+  display: block;
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 8rpx;
+}
+.sc-loading {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 50vh;
+}
+.sc-loading-text {
+  font-size: 28rpx;
+  color: #999;
+}
+
+/* 维度导航 */
+.sc-dim-nav {
+  display: flex;
+  justify-content: space-between;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx 20rpx;
+  margin-bottom: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+}
+.sc-dim-nav-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.sc-dim-nav-dot {
+  width: 56rpx;
+  height: 56rpx;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.sc-dim-nav-char {
+  color: #fff;
+  font-size: 26rpx;
+  font-weight: 600;
+}
+.sc-dim-nav-dots {
+  display: flex;
+  margin-top: 10rpx;
+}
+.sc-dim-nav-dot-mini {
+  width: 12rpx;
+  height: 12rpx;
+  border-radius: 50%;
+  background: #E5E7EB;
+  margin: 0 4rpx;
+}
+.sc-dim-nav-dot-mini.done {
+  background: #F97316;
+}
+
+/* 进度 */
+.sc-progress-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 10rpx;
+}
+.sc-progress-text {
+  font-size: 24rpx;
+  color: #666;
+}
+.sc-progress-pct {
+  font-size: 24rpx;
+  color: #F97316;
+  font-weight: 600;
+}
+.sc-progress-bar {
+  height: 10rpx;
+  background: #E8E8E8;
+  border-radius: 5rpx;
+  margin-bottom: 30rpx;
+  overflow: hidden;
+}
+.sc-progress-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #F97316, #FF8C42);
+  border-radius: 5rpx;
+  transition: width 0.3s ease;
+}
+
+/* 维度分组 */
+.sc-section {
+  margin-bottom: 30rpx;
+}
+.sc-section-header {
+  display: flex;
+  align-items: center;
+  background: #fff;
+  border-left: 8rpx solid #F97316;
+  border-radius: 12rpx;
+  padding: 20rpx 24rpx;
+  margin-bottom: 16rpx;
+  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.04);
+}
+.sc-section-name {
+  font-size: 32rpx;
+  font-weight: 700;
+  color: #333;
+  margin-right: 12rpx;
+}
+.sc-section-element {
+  font-size: 26rpx;
+  font-weight: 600;
+  margin-right: 16rpx;
+}
+.sc-section-slogan {
+  flex: 1;
+  font-size: 24rpx;
+  color: #999;
+}
+.sc-section-count {
+  font-size: 24rpx;
+  color: #F97316;
+  font-weight: 600;
+}
+
+/* 题目卡片 */
+.sc-question {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx 26rpx 26rpx;
+  margin-bottom: 16rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+}
+.sc-q-text {
+  display: block;
+  font-size: 30rpx;
+  font-weight: 500;
+  color: #333;
+  line-height: 1.5;
+}
+.sc-opts {
+  margin-top: 24rpx;
+}
+.sc-opt {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 22rpx 24rpx;
+  border: 2rpx solid #E5E7EB;
+  border-radius: 14rpx;
+  margin-bottom: 14rpx;
+  transition: all 0.2s;
+}
+.sc-opt-sel {
+  border-color: #F97316;
+  background: #FFF7ED;
+}
+.sc-opt-label {
+  flex: 1;
+  font-size: 28rpx;
+  color: #333;
+  line-height: 1.4;
+}
+.sc-opt-radio {
+  width: 36rpx;
+  height: 36rpx;
+  border: 2rpx solid #D1D5DB;
+  border-radius: 50%;
+  margin-left: 20rpx;
+  box-sizing: border-box;
+}
+.sc-opt-radio-on {
+  border: 10rpx solid #F97316;
+}
+
+/* 提交 */
+.sc-footer {
+  margin-top: 20rpx;
+}
+.sc-submit-btn {
+  height: 92rpx;
+  line-height: 92rpx;
+  background: linear-gradient(135deg, #F97316, #FF8C42);
+  color: #fff;
+  font-size: 32rpx;
+  font-weight: 600;
+  border-radius: 46rpx;
+  border: none;
+  text-align: center;
+}
+.sc-submit-btn[disabled] {
+  opacity: 0.5;
+}
+.sc-submit-tip {
+  display: block;
+  text-align: center;
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 16rpx;
+}
+</style>

+ 94 - 0
cfc-frontend/pages/home-pages/parent-index.vue

@@ -79,6 +79,36 @@
       </PlayfulCard>
       </PlayfulCard>
     </view>
     </view>
 
 
+    <!-- ===== 五维家庭自检入口(P0-1/P0-2) ===== -->
+    <view class="selfcheck-entry-section">
+      <PlayfulCard variant="flat" shadow="sm" :clickable="true" @click="goToSelfCheck" class="selfcheck-card-wide">
+        <view class="selfcheck-card-content">
+          <view class="selfcheck-icon-wrap">
+            <view class="selfcheck-icon-circle" style="background: #FF8C42;">
+              <text class="selfcheck-icon-char">身</text>
+            </view>
+            <view class="selfcheck-icon-circle" style="background: #6366F1;">
+              <text class="selfcheck-icon-char">智</text>
+            </view>
+            <view class="selfcheck-icon-circle" style="background: #F59E0B;">
+              <text class="selfcheck-icon-char">富</text>
+            </view>
+            <view class="selfcheck-icon-circle" style="background: #10B981;">
+              <text class="selfcheck-icon-char">行</text>
+            </view>
+            <view class="selfcheck-icon-circle" style="background: #FF6B9D;">
+              <text class="selfcheck-icon-char">心</text>
+            </view>
+          </view>
+          <view class="selfcheck-body">
+            <text class="selfcheck-title">五维家庭自检</text>
+            <text class="selfcheck-subtitle">15题 · 5分钟 · 生成五行相生寻源建议</text>
+          </view>
+          <text class="selfcheck-arrow">→</text>
+        </view>
+      </PlayfulCard>
+    </view>
+
     <!-- ===== 家庭成员关系图谱 ===== -->
     <!-- ===== 家庭成员关系图谱 ===== -->
 <FamilyRelationGraph
 <FamilyRelationGraph
   v-if="familyMembersVisible.length > 0"
   v-if="familyMembersVisible.length > 0"
@@ -955,6 +985,9 @@ export default {
     goToHealthCircle: function() {
     goToHealthCircle: function() {
       uni.navigateTo({ url: '/pages/share/index' })
       uni.navigateTo({ url: '/pages/share/index' })
     },
     },
+    goToSelfCheck: function() {
+      uni.navigateTo({ url: '/pages/family/self-check' })
+    },
     goMemberDetail(member) {
     goMemberDetail(member) {
       if (!member || !member.memberId) return
       if (!member || !member.memberId) return
       uni.navigateTo({
       uni.navigateTo({
@@ -1806,6 +1839,67 @@ export default {
   margin-top: 4rpx;
   margin-top: 4rpx;
 }
 }
 
 
+/* ===== 五维家庭自检入口卡片(P0-1/P0-2) ===== */
+.selfcheck-entry-section {
+  margin: 0 20rpx 28rpx 20rpx;
+}
+.selfcheck-card-wide {
+  width: 100%;
+}
+.selfcheck-card-content {
+  padding: 28rpx 24rpx;
+  display: flex;
+  align-items: center;
+  gap: 20rpx;
+}
+.selfcheck-icon-wrap {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  flex-shrink: 0;
+}
+.selfcheck-icon-circle {
+  width: 52rpx;
+  height: 52rpx;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: -10rpx;
+  border: 4rpx solid #fff;
+  box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.12);
+}
+.selfcheck-icon-circle:last-child {
+  margin-right: 0;
+}
+.selfcheck-icon-char {
+  font-size: 24rpx;
+  font-weight: 700;
+  color: #fff;
+}
+.selfcheck-body {
+  flex: 1;
+}
+.selfcheck-title {
+  display: block;
+  font-size: 30rpx;
+  font-weight: 700;
+  color: #1E293B;
+  line-height: 1.4;
+}
+.selfcheck-subtitle {
+  display: block;
+  font-size: 22rpx;
+  color: #6B7280;
+  margin-top: 4rpx;
+}
+.selfcheck-arrow {
+  font-size: 30rpx;
+  color: #F97316;
+  font-weight: 600;
+  flex-shrink: 0;
+}
+
 /* ===== 邀请码二维码弹窗 ===== */
 /* ===== 邀请码二维码弹窗 ===== */
 .qr-code-modal {
 .qr-code-modal {
   position: fixed;
   position: fixed;

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

@@ -2075,6 +2075,23 @@ export const getQuestionnaireHistory = (memberId) => {
   return request('/api/family/questionnaire/history/' + memberId, 'GET', {})
   return request('/api/family/questionnaire/history/' + memberId, 'GET', {})
 }
 }
 
 
+// ===== 五维家庭自检(P0-1)=====
+export const getSelfCheckQuestions = () => {
+  return request('/api/family/self-check/questions', 'POST', {})
+}
+
+export const submitSelfCheck = (data) => {
+  return request('/api/family/self-check/submit', 'POST', data)
+}
+
+export const getSelfCheckLatest = () => {
+  return request('/api/family/self-check/latest', 'POST', {})
+}
+
+export const getSelfCheckHistory = () => {
+  return request('/api/family/self-check/history', '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)