Преглед на файлове

docs: 先天画像体系实现计划(7阶段15任务,含数字能量TDD与权重替换改造)

Xiaogang Liao преди 3 седмици
родител
ревизия
dcd1b9bad5
променени са 1 файла, в които са добавени 2316 реда и са изтрити 0 реда
  1. 2316 0
      docs/superpowers/plans/2026-08-28-innate-portrait-mind-dimension.md

+ 2316 - 0
docs/superpowers/plans/2026-08-28-innate-portrait-mind-dimension.md

@@ -0,0 +1,2316 @@
+# 先天画像体系实现计划
+
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
+
+**目标:** 将星座/八字/数字能量统一为"心维度先天画像"体系——聚合先天画像 + AI 解读 + 数字能量补强 + 先天后天打通 + 管理端可配置化。
+
+**架构:** 新增 `InnatePortraitService` 聚合层,复用现有 `FamilyMemberAttributeService.calcInnateScore()` 计算 mind_base_score(管理端可配置权重替换硬编码),通过新增 LangGraph `innate_portrait_graph` 生成 AI 解读,`AiGateway` 调用并模板降级。小程序新增先天画像报告页 + 成长轨迹页,管理端新增 2 个配置页。
+
+**技术栈:** Java 8 + Spring Boot 2.7.18 + MyBatis-Plus / Python 3.11 + LangGraph + FastAPI / uni-app Vue 2 / Vue 2 + Element UI
+
+**规格:** [2026-08-28-innate-portrait-mind-dimension-design.md](../specs/2026-08-28-innate-portrait-mind-dimension-design.md)
+
+---
+
+## 文件结构
+
+### 后端(cfc-backend)
+
+| 文件 | 职责 |
+|------|------|
+| `src/main/java/com/etotem/cfc/entity/InnatePortraitConfig.java` | 来源权重配置实体 |
+| `src/main/java/com/etotem/cfc/entity/InnatePortraitReport.java` | AI 报告缓存实体 |
+| `src/main/java/com/etotem/cfc/entity/NumSoulDetailConfig.java` | 数字能量详细配置实体 |
+| `src/main/java/com/etotem/cfc/mapper/InnatePortraitConfigMapper.java` | 配置 Mapper |
+| `src/main/java/com/etotem/cfc/mapper/InnatePortraitReportMapper.java` | 报告 Mapper |
+| `src/main/java/com/etotem/cfc/mapper/NumSoulDetailConfigMapper.java` | 数字配置 Mapper |
+| `src/main/java/com/etotem/cfc/service/NumSoulCalculator.java` | 数字能量计算(纯逻辑,可单测) |
+| `src/main/java/com/etotem/cfc/service/InnatePortraitService.java` | 先天画像聚合服务 |
+| `src/main/java/com/etotem/cfc/dto/InnatePortraitVO.java` | 先天画像 VO |
+| `src/main/java/com/etotem/cfc/dto/NumSoulDetailVO.java` | 数字能量 VO |
+| `src/main/java/com/etotem/cfc/dto/InnatePortraitReportVO.java` | AI 报告 VO |
+| `src/main/java/com/etotem/cfc/dto/InnateTrajectoryVO.java` | 成长轨迹 VO |
+| `src/main/java/com/etotem/cfc/controller/mind/InnatePortraitController.java` | 先天画像接口 |
+| `src/main/java/com/etotem/cfc/controller/admin/InnatePortraitConfigController.java` | 管理端配置接口 |
+| `src/main/java/com/etotem/cfc/controller/admin/NumSoulDetailConfigController.java` | 管理端数字配置接口 |
+| `src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` | 迁移 267-269 |
+| `src/main/java/com/etotem/cfc/service/FamilyMemberAttributeService.java` | calcInnateScore 权重替换改造 |
+| `src/main/java/com/etotem/cfc/service/AiGateway.java` | 新增 generateInnateReading |
+
+### AI(cfc-langgraph)
+
+| 文件 | 职责 |
+|------|------|
+| `app/graphs/innate_portrait_graph.py` | 先天画像 AI 解读 graph |
+| `app/api/innate.py` | FastAPI 端点 `/api/v1/innate/reading` |
+| `app/main.py` | 注册 innate 路由 |
+
+### 管理端(cfc-web)
+
+| 文件 | 职责 |
+|------|------|
+| `src/views/admin/InnatePortraitConfig.vue` | 来源权重配置页 |
+| `src/views/admin/NumSoulDetailConfig.vue` | 数字能量配置页 |
+
+### 小程序(cfc-frontend)
+
+| 文件 | 职责 |
+|------|------|
+| `pages/mind-detail/innate-portrait.vue` | 先天画像报告页 |
+| `pages/mind-detail/innate-trajectory.vue` | 成长轨迹页 |
+| `pages/mind-detail/index.vue` | 心维度首页增强 |
+| `pages.json` | 注册新页面 |
+| `utils/api.js` | 新增接口 |
+
+---
+
+## 阶段 1:数据库迁移 + 后端数据层
+
+### 任务 1:创建 3 张新表(迁移 267-269)
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`
+- 修改:`cfc-backend/src/main/resources/schema.sql`
+
+- [ ] **步骤 1:在 DatabaseInitializer 新增迁移 267(innate_portrait_config)**
+
+在 `runMigrations()` 方法末尾(最新迁移 266 之后)添加:
+
+```java
+// ==================== 迁移267: 先天画像来源权重配置表 ====================
+try {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS innate_portrait_config (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
+            "source_type VARCHAR(20) NOT NULL," +
+            "source_name VARCHAR(50) NOT NULL," +
+            "weight INT DEFAULT 25," +
+            "enabled TINYINT(1) DEFAULT 1," +
+            "ai_enabled TINYINT(1) DEFAULT 1," +
+            "sort_order INT DEFAULT 0," +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
+            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
+            "UNIQUE KEY uk_source (source_type)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='先天画像来源权重配置'");
+    // 种子数据(4来源默认权重25%)
+    Integer cnt = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM innate_portrait_config", Integer.class);
+    if (cnt == null || cnt == 0) {
+        jdbcTemplate.batchUpdate(
+            "INSERT INTO innate_portrait_config (source_type, source_name, weight, sort_order) VALUES (?, ?, ?, ?)",
+            java.util.Arrays.asList(new Object[][]{
+                {"zodiac", "星座", 2500, 1},
+                {"bazi", "八字", 2500, 2},
+                {"blood", "血型", 2500, 3},
+                {"numsoul", "数字能量", 2500, 4},
+            })
+        );
+        log.info("迁移267: innate_portrait_config 种子数据已加载 (4条)");
+    }
+    log.info("迁移267: innate_portrait_config 表创建完成");
+} catch (Exception e) {
+    log.warn("迁移267失败: {}", e.getMessage());
+}
+```
+
+- [ ] **步骤 2:新增迁移 268(innate_portrait_report)**
+
+在迁移 267 后紧接添加:
+
+```java
+// ==================== 迁移268: 先天画像报告缓存表 ====================
+try {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS innate_portrait_report (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
+            "member_id BIGINT NOT NULL," +
+            "member_type VARCHAR(10) NOT NULL," +
+            "family_id BIGINT NOT NULL," +
+            "portrait_json TEXT," +
+            "ai_reading TEXT," +
+            "generated_at DATETIME," +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
+            "UNIQUE KEY uk_member (member_id, member_type)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='先天画像报告缓存'");
+    log.info("迁移268: innate_portrait_report 表创建完成");
+} catch (Exception e) {
+    log.warn("迁移268失败: {}", e.getMessage());
+}
+```
+
+- [ ] **步骤 3:新增迁移 269(numsoul_detail_config)**
+
+在迁移 268 后紧接添加:
+
+```java
+// ==================== 迁移269: 数字能量详细配置表 ====================
+try {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS numsoul_detail_config (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
+            "number_type VARCHAR(20) NOT NULL," +
+            "number_value INT NOT NULL," +
+            "title VARCHAR(50)," +
+            "keywords VARCHAR(200)," +
+            "mind_base INT DEFAULT 0," +
+            "mind_advice TEXT," +
+            "color_hex VARCHAR(10)," +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
+            "UNIQUE KEY uk_type_num (number_type, number_value)" +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数字能量详细配置'");
+    Integer cnt2 = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM numsoul_detail_config", Integer.class);
+    if (cnt2 == null || cnt2 == 0) {
+        // 生命灵数种子(1-9 + 主数11/22/33)
+        jdbcTemplate.batchUpdate(
+            "INSERT INTO numsoul_detail_config (number_type, number_value, title, keywords, mind_base, mind_advice, color_hex) VALUES (?, ?, ?, ?, ?, ?, ?)",
+            java.util.Arrays.asList(new Object[][]{
+                {"life_path", 1, "开创者", "独立、领导、创新", 68, "培养团队协作意识,避免独断专行", "#FF4444"},
+                {"life_path", 2, "和平者", "温和、合作、敏感", 72, "增强自我主张,避免过度迎合", "#FF8C00"},
+                {"life_path", 3, "表达者", "创意、社交、乐观", 78, "保持专注,避免浅尝辄止", "#FFD700"},
+                {"life_path", 4, "建设者", "务实、稳定、有序", 70, "学会变通,避免过于固执", "#4CAF50"},
+                {"life_path", 5, "自由者", "冒险、变化、好奇", 75, "培养持久力,避免缺乏恒心", "#2196F3"},
+                {"life_path", 6, "关怀者", "责任、关爱、奉献", 80, "学会自我关爱,避免过度付出", "#9C27B0"},
+                {"life_path", 7, "探索者", "智慧、分析、内省", 76, "与人分享见解,避免过度封闭", "#673AB7"},
+                {"life_path", 8, "成就者", "雄心、管理、决策", 72, "关注内在成长,避免过度追求外在", "#3F51B5"},
+                {"life_path", 9, "博爱者", "包容、智慧、善良", 82, "设定个人边界,避免过度牺牲", "#E91E63"},
+                {"life_path", 11, "启明者", "直觉、灵感、理想", 85, "落地执行,避免空想", "#7C4DFF"},
+                {"life_path", 22, "建造师", "远见、实践、宏图", 88, "分步落地大目标", "#536DFE"},
+                {"life_path", 33, "引路人", "大爱、奉献、疗愈", 90, "先照顾好自己再照亮他人", "#FF4081"},
+            })
+        );
+        log.info("迁移269: numsoul_detail_config 生命灵数种子已加载");
+    }
+    log.info("迁移269: numsoul_detail_config 表创建完成");
+} catch (Exception e) {
+    log.warn("迁移269失败: {}", e.getMessage());
+}
+```
+
+- [ ] **步骤 4:同步 schema.sql**
+
+在 `cfc-backend/src/main/resources/schema.sql` 末尾追加 3 张表的完整 CREATE TABLE 定义(与迁移 267-269 的 DDL 完全一致,含 COMMENT)。
+
+- [ ] **步骤 5:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 6: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: 迁移267-269 创建先天画像三张新表"
+```
+
+### 任务 2:后端实体类
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/InnatePortraitConfig.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/InnatePortraitReport.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/NumSoulDetailConfig.java`
+
+- [ ] **步骤 1:创建 InnatePortraitConfig.java**
+
+```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("innate_portrait_config")
+public class InnatePortraitConfig implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 来源: zodiac/bazi/blood/numsoul */
+    private String sourceType;
+
+    /** 来源名称 */
+    private String sourceName;
+
+    /** 权重(基点,2500=25%) */
+    private Integer weight;
+
+    /** 是否启用 0/1 */
+    private Integer enabled;
+
+    /** 是否启用AI解读 0/1 */
+    private Integer aiEnabled;
+
+    /** 排序 */
+    private Integer sortOrder;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}
+```
+
+- [ ] **步骤 2:创建 InnatePortraitReport.java**
+
+```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("innate_portrait_report")
+public class InnatePortraitReport implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long memberId;
+
+    /** 成员类型: child/parent */
+    private String memberType;
+
+    private Long familyId;
+
+    /** 先天画像数据JSON */
+    private String portraitJson;
+
+    /** AI解读文案 */
+    private String aiReading;
+
+    private Date generatedAt;
+
+    private Date createdAt;
+}
+```
+
+- [ ] **步骤 3:创建 NumSoulDetailConfig.java**
+
+```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("numsoul_detail_config")
+public class NumSoulDetailConfig implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 类型: life_path/talent/birthday/destiny */
+    private String numberType;
+
+    /** 数字值 1-9 或主数 11/22/33 */
+    private Integer numberValue;
+
+    private String title;
+
+    private String keywords;
+
+    /** 心先天基础分(百分位) */
+    private Integer mindBase;
+
+    private String mindAdvice;
+
+    private String colorHex;
+
+    private Date createdAt;
+}
+```
+
+- [ ] **步骤 4:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/InnatePortraitConfig.java cfc-backend/src/main/java/com/etotem/cfc/entity/InnatePortraitReport.java cfc-backend/src/main/java/com/etotem/cfc/entity/NumSoulDetailConfig.java
+git commit -m "feat: 先天画像3个实体类"
+```
+
+### 任务 3:Mapper 接口 + DTO
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/InnatePortraitConfigMapper.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/InnatePortraitReportMapper.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/NumSoulDetailConfigMapper.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/dto/InnatePortraitVO.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/dto/NumSoulDetailVO.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/dto/InnatePortraitReportVO.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/dto/InnateTrajectoryVO.java`
+
+- [ ] **步骤 1:创建 3 个 Mapper**
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.InnatePortraitConfig;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface InnatePortraitConfigMapper extends BaseMapper<InnatePortraitConfig> {
+}
+```
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.InnatePortraitReport;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface InnatePortraitReportMapper extends BaseMapper<InnatePortraitReport> {
+}
+```
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.NumSoulDetailConfig;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface NumSoulDetailConfigMapper extends BaseMapper<NumSoulDetailConfig> {
+}
+```
+
+- [ ] **步骤 2:创建 InnatePortraitVO.java**
+
+```java
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class InnatePortraitVO {
+
+    /** 成员ID */
+    private Long memberId;
+
+    private String memberType;
+
+    /** 八字四柱 {"year":"甲子","month":"丙寅","day":"戊辰","hour":"壬申"} */
+    private Map<String, String> eightCharacters;
+
+    /** 五行 {"wood":30,"fire":45,"earth":60,"metal":25,"water":40} */
+    private Map<String, Integer> wuxingElements;
+
+    /** 生肖: rat/ox/... */
+    private String zodiac;
+
+    /** 血型: A/B/AB/O */
+    private String bloodType;
+
+    /** 心先天基础分 */
+    private Integer mindBaseScore;
+
+    /** 智先天基础分 */
+    private Integer wisdomBaseScore;
+
+    /** 数字能量详情 */
+    private NumSoulDetailVO numSoul;
+
+    /** 各来源贡献明细 */
+    private List<Map<String, Object>> sourceContributions;
+}
+```
+
+- [ ] **步骤 3:创建 NumSoulDetailVO.java**
+
+```java
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class NumSoulDetailVO {
+
+    /** 生命灵数 */
+    private Integer lifePath;
+
+    private String lifePathTitle;
+
+    private String lifePathKeywords;
+
+    private String lifePathAdvice;
+
+    /** 天赋数(可能有2个) */
+    private Integer talent1;
+
+    private Integer talent2;
+
+    private String talentTitle;
+
+    /** 生日数 */
+    private Integer birthday;
+
+    private String birthdayTitle;
+
+    /** 命运数 */
+    private Integer destiny;
+
+    private String destinyTitle;
+
+    /** 生命灵数代表色 */
+    private String colorHex;
+}
+```
+
+- [ ] **步骤 4:创建 InnatePortraitReportVO.java**
+
+```java
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class InnatePortraitReportVO {
+
+    private InnatePortraitVO portrait;
+
+    /** AI解读文案 */
+    private String aiReading;
+
+    /** 解读生成时间 */
+    private String generatedAt;
+
+    /** 是否来自缓存 */
+    private Boolean fromCache;
+
+    /** 是否降级为模板文案 */
+    private Boolean fallbackUsed;
+}
+```
+
+- [ ] **步骤 5:创建 InnateTrajectoryVO.java**
+
+```java
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.util.Map;
+
+@Data
+public class InnateTrajectoryVO {
+
+    private InnatePortraitVO innatePortrait;
+
+    /** 五维先天分 {"mind":72,"body":65,...} */
+    private Map<String, Integer> innateDimensionScores;
+
+    /** 后天能量 {"mindBalance":120,"mindTotalEarned":350} */
+    private Map<String, Object> currentEnergy;
+
+    /** 先天vs后天 {"mindInnate":72,"mindCurrent":81,"growth":"+9"} */
+    private Map<String, Object> innateVsCurrent;
+
+    /** 成长趋势文案 */
+    private String growthTrend;
+
+    /** 成长建议 */
+    private String advice;
+}
+```
+
+- [ ] **步骤 6:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 7:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/mapper cfc-backend/src/main/java/com/etotem/cfc/dto
+git commit -m "feat: 先天画像Mapper和DTO"
+```
+
+---
+
+## 阶段 2:数字能量计算
+
+### 任务 4:NumSoulCalculator(纯计算逻辑 + TDD)
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/service/NumSoulCalculator.java`
+- 测试:`cfc-backend/src/test/java/com/etotem/cfc/service/NumSoulCalculatorTest.java`
+
+- [ ] **步骤 1:编写失败的测试**
+
+```java
+package com.etotem.cfc.service;
+
+import org.junit.jupiter.api.Test;
+import java.time.LocalDate;
+import static org.junit.jupiter.api.Assertions.*;
+
+class NumSoulCalculatorTest {
+
+    @Test
+    void lifePath_19900515_shouldBe3() {
+        // 1+9+9+0+0+5+1+5=30 -> 3+0=3
+        assertEquals(3, NumSoulCalculator.calcLifePath(1990, 5, 15));
+    }
+
+    @Test
+    void lifePath_masterNumber_shouldKeep11() {
+        // 1987+11+29 = 1+9+8+7+1+1+2+9=38 -> 3+8=11 (主数保留)
+        assertEquals(11, NumSoulCalculator.calcLifePath(1987, 11, 29));
+    }
+
+    @Test
+    void birthday_shouldReduceDay() {
+        // 15 -> 1+5=6
+        assertEquals(6, NumSoulCalculator.calcBirthday(15));
+    }
+
+    @Test
+    void talent_shouldReturnTwoNumbers() {
+        // 1990: 1+9+9+0=19, 05: 5, 15: 6 -> 天赋数 19 和 11(5+6)
+        assertArrayEquals(new int[]{19, 11}, NumSoulCalculator.calcTalent(1990, 5, 15));
+    }
+
+    @Test
+    void destiny_shouldReduceFullSum() {
+        // 1990-05-15 sum=30 -> 3
+        assertEquals(3, NumSoulCalculator.calcDestiny(1990, 5, 15));
+    }
+}
+```
+
+- [ ] **步骤 2:运行测试验证失败**
+
+运行:`cd cfc-backend && mvn test -Dtest=NumSoulCalculatorTest`
+预期:FAIL(类不存在)
+
+- [ ] **步骤 3:实现 NumSoulCalculator**
+
+```java
+package com.etotem.cfc.service;
+
+import org.springframework.stereotype.Component;
+
+@Component
+public class NumSoulCalculator {
+
+    /** 主数(大师数),不化简 */
+    private static final int[] MASTER_NUMBERS = {11, 22, 33};
+
+    /**
+     * 生命灵数:年月日所有数字相加直至个位(主数11/22/33保留)
+     */
+    public static int calcLifePath(int year, int month, int day) {
+        int sum = digitSum(year) + digitSum(month) + digitSum(day);
+        return reduceToSingleOrMaster(sum);
+    }
+
+    /**
+     * 天赋数:年化简、月+日合并化简,返回两个两位数
+     * 例: 1990-05-15 -> 年=19, 日+月=5+6=11 -> [19, 11]
+     */
+    public static int[] calcTalent(int year, int month, int day) {
+        int yearNum = digitSum(year);          // 1990 -> 19
+        int monthDayNum = digitSum(month) + digitSum(day); // 5 + 6 = 11
+        return new int[]{yearNum, monthDayNum};
+    }
+
+    /**
+     * 生日数:出生日化简至个位
+     */
+    public static int calcBirthday(int day) {
+        return reduceToSingleOrMaster(day);
+    }
+
+    /**
+     * 命运数(简化版,不含姓名):出生年月日总和化简
+     */
+    public static int calcDestiny(int year, int month, int day) {
+        int sum = digitSum(year) + digitSum(month) + digitSum(day);
+        return reduceToSingleOrMaster(sum);
+    }
+
+    private static int digitSum(int n) {
+        int sum = 0;
+        while (n > 0) {
+            sum += n % 10;
+            n /= 10;
+        }
+        return sum;
+    }
+
+    private static int reduceToSingleOrMaster(int n) {
+        while (n >= 10 && !isMasterNumber(n)) {
+            n = digitSum(n);
+        }
+        return n;
+    }
+
+    private static boolean isMasterNumber(int n) {
+        for (int m : MASTER_NUMBERS) {
+            if (m == n) return true;
+        }
+        return false;
+    }
+}
+```
+
+- [ ] **步骤 4:运行测试验证通过**
+
+运行:`cd cfc-backend && mvn test -Dtest=NumSoulCalculatorTest`
+预期:PASS(5个测试全绿)
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/NumSoulCalculator.java cfc-backend/src/test/java/com/etotem/cfc/service/NumSoulCalculatorTest.java
+git commit -m "feat: 数字能量计算器 NumSoulCalculator + 单元测试"
+```
+
+---
+
+## 阶段 3:先天画像聚合服务 + 接口
+
+### 任务 5:InnatePortraitService(聚合 + AI编排)
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/service/InnatePortraitService.java`
+
+- [ ] **步骤 1:实现 InnatePortraitService(聚合画像)**
+
+```java
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.dto.InnatePortraitReportVO;
+import com.etotem.cfc.dto.InnatePortraitVO;
+import com.etotem.cfc.dto.InnateTrajectoryVO;
+import com.etotem.cfc.dto.NumSoulDetailVO;
+import com.etotem.cfc.entity.*;
+import com.etotem.cfc.mapper.*;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.*;
+
+@Service
+public class InnatePortraitService {
+
+    private static final Logger log = LoggerFactory.getLogger(InnatePortraitService.class);
+
+    @Resource
+    private FamilyMemberAttributeService familyMemberAttributeService;
+
+    @Resource
+    private InnatePortraitConfigMapper innatePortraitConfigMapper;
+
+    @Resource
+    private InnatePortraitReportMapper innatePortraitReportMapper;
+
+    @Resource
+    private NumSoulDetailConfigMapper numSoulDetailConfigMapper;
+
+    @Resource
+    private AiGateway aiGateway;
+
+    @Resource
+    private EnergyBalanceMapper energyBalanceMapper;
+
+    @Resource
+    private EnergyDimensionMapper energyDimensionMapper;
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    /**
+     * 聚合先天画像(复用 calcInnateScore 计算 mind_base_score)
+     */
+    public InnatePortraitVO getInnatePortrait(Long memberId, String memberType) {
+        FamilyMemberAttributes attrs = familyMemberAttributeService.getByMember(memberId, memberType);
+        if (attrs == null) {
+            return null;
+        }
+
+        InnatePortraitVO vo = new InnatePortraitVO();
+        vo.setMemberId(memberId);
+        vo.setMemberType(memberType);
+        vo.setEightCharacters(parseJson(attrs.getEightCharacters()));
+        vo.setWuxingElements(parseJsonInt(attrs.getWuxingElements()));
+        vo.setZodiac(attrs.getZodiac());
+        vo.setBloodType(attrs.getBloodType());
+        // 复用现有计算(含缓存),不重复实现
+        vo.setMindBaseScore(familyMemberAttributeService.calcInnateScore(memberId, memberType, "mind"));
+        vo.setWisdomBaseScore(familyMemberAttributeService.calcInnateScore(memberId, memberType, "wisdom"));
+
+        // 数字能量
+        if (attrs.getBirthDatetime() != null) {
+            Calendar cal = Calendar.getInstance();
+            cal.setTime(attrs.getBirthDatetime());
+            vo.setNumSoul(calculateNumSoul(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)));
+        }
+
+        // 来源贡献明细
+        vo.setSourceContributions(listSourceContributions(memberId, memberType, attrs));
+
+        return vo;
+    }
+
+    /**
+     * 数字能量计算
+     */
+    public NumSoulDetailVO calculateNumSoul(int year, int month, int day) {
+        NumSoulDetailVO vo = new NumSoulDetailVO();
+        int lifePath = NumSoulCalculator.calcLifePath(year, month, day);
+        int[] talent = NumSoulCalculator.calcTalent(year, month, day);
+        int birthday = NumSoulCalculator.calcBirthday(day);
+        int destiny = NumSoulCalculator.calcDestiny(year, month, day);
+
+        vo.setLifePath(lifePath);
+        vo.setTalent1(talent[0]);
+        vo.setTalent2(talent[1]);
+        vo.setBirthday(birthday);
+        vo.setDestiny(destiny);
+
+        // 从配置表补充解读
+        NumSoulDetailConfig lifeCfg = getNumSoulConfig("life_path", lifePath);
+        if (lifeCfg != null) {
+            vo.setLifePathTitle(lifeCfg.getTitle());
+            vo.setLifePathKeywords(lifeCfg.getKeywords());
+            vo.setLifePathAdvice(lifeCfg.getMindAdvice());
+            vo.setColorHex(lifeCfg.getColorHex());
+        }
+        NumSoulDetailConfig talentCfg = getNumSoulConfig("talent", talent[0]);
+        if (talentCfg != null) {
+            vo.setTalentTitle(talentCfg.getTitle());
+        }
+        NumSoulDetailConfig birthdayCfg = getNumSoulConfig("birthday", birthday);
+        if (birthdayCfg != null) {
+            vo.setBirthdayTitle(birthdayCfg.getTitle());
+        }
+        NumSoulDetailConfig destinyCfg = getNumSoulConfig("destiny", destiny);
+        if (destinyCfg != null) {
+            vo.setDestinyTitle(destinyCfg.getTitle());
+        }
+        return vo;
+    }
+
+    private NumSoulDetailConfig getNumSoulConfig(String type, int value) {
+        LambdaQueryWrapper<NumSoulDetailConfig> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(NumSoulDetailConfig::getNumberType, type)
+               .eq(NumSoulDetailConfig::getNumberValue, value);
+        return numSoulDetailConfigMapper.selectOne(wrapper);
+    }
+
+    /**
+     * 来源贡献明细(用于管理端可配置权重展示)
+     */
+    private List<Map<String, Object>> listSourceContributions(Long memberId, String memberType, FamilyMemberAttributes attrs) {
+        List<Map<String, Object>> list = new ArrayList<>();
+        List<InnatePortraitConfig> configs = innatePortraitConfigMapper.selectList(
+                new LambdaQueryWrapper<InnatePortraitConfig>().orderByAsc(InnatePortraitConfig::getSortOrder));
+        for (InnatePortraitConfig cfg : configs) {
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("sourceType", cfg.getSourceType());
+            item.put("sourceName", cfg.getSourceName());
+            item.put("weight", cfg.getWeight());
+            item.put("enabled", cfg.getEnabled());
+            list.add(item);
+        }
+        return list;
+    }
+
+    /**
+     * 生成/获取 AI 解读(缓存到 innate_portrait_report)
+     */
+    public InnatePortraitReportVO generateAiReading(Long memberId, String memberType, Long familyId) {
+        // 1. 检查缓存
+        InnatePortraitReport report = innatePortraitReportMapper.selectOne(
+                new LambdaQueryWrapper<InnatePortraitReport>()
+                        .eq(InnatePortraitReport::getMemberId, memberId)
+                        .eq(InnatePortraitReport::getMemberType, memberType));
+        boolean fallbackUsed = false;
+        if (report != null && report.getAiReading() != null && !report.getAiReading().isEmpty()) {
+            InnatePortraitReportVO cached = new InnatePortraitReportVO();
+            cached.setPortrait(getInnatePortrait(memberId, memberType));
+            cached.setAiReading(report.getAiReading());
+            cached.setGeneratedAt(String.valueOf(report.getGeneratedAt()));
+            cached.setFromCache(true);
+            cached.setFallbackUsed(false);
+            return cached;
+        }
+
+        // 2. 聚合先天画像
+        InnatePortraitVO portrait = getInnatePortrait(memberId, memberType);
+        if (portrait == null) {
+            return null;
+        }
+
+        // 3. 调用 AiGateway 生成解读(失败返回 null)
+        String aiReading = null;
+        try {
+            Map<String, Object> result = aiGateway.generateInnateReading(portrait);
+            if (result != null && result.get("reading") != null) {
+                aiReading = (String) result.get("reading");
+            }
+        } catch (Exception e) {
+            log.warn("AI解读生成失败,降级模板: {}", e.getMessage());
+        }
+
+        // 4. 降级:模板拼接
+        if (aiReading == null || aiReading.isEmpty()) {
+            aiReading = buildFallbackReading(portrait);
+            fallbackUsed = true;
+        }
+
+        // 5. 缓存
+        InnatePortraitReport newReport = new InnatePortraitReport();
+        newReport.setMemberId(memberId);
+        newReport.setMemberType(memberType);
+        newReport.setFamilyId(familyId);
+        newReport.setPortraitJson(safeToJson(portrait));
+        newReport.setAiReading(aiReading);
+        newReport.setGeneratedAt(new Date());
+        if (report != null) {
+            newReport.setId(report.getId());
+            innatePortraitReportMapper.updateById(newReport);
+        } else {
+            innatePortraitReportMapper.insert(newReport);
+        }
+
+        InnatePortraitReportVO vo = new InnatePortraitReportVO();
+        vo.setPortrait(portrait);
+        vo.setAiReading(aiReading);
+        vo.setGeneratedAt(String.valueOf(newReport.getGeneratedAt()));
+        vo.setFromCache(false);
+        vo.setFallbackUsed(fallbackUsed);
+        return vo;
+    }
+
+    /**
+     * 降级模板文案(AI 不可用时)
+     */
+    private String buildFallbackReading(InnatePortraitVO portrait) {
+        StringBuilder sb = new StringBuilder();
+        if (portrait.getZodiac() != null) {
+            sb.append("生肖").append(portrait.getZodiac()).append(",");
+        }
+        if (portrait.getNumSoul() != null && portrait.getNumSoul().getLifePath() != null) {
+            sb.append("生命灵数").append(portrait.getNumSoul().getLifePath())
+              .append(portrait.getNumSoul().getLifePathTitle() != null ? "(" + portrait.getNumSoul().getLifePathTitle() + ")" : "").append(",");
+        }
+        sb.append("心先天基础分 ").append(portrait.getMindBaseScore()).append("。");
+        sb.append("建议通过情绪打卡、家庭互动与阅读持续滋养心能量。");
+        return sb.toString();
+    }
+
+    /**
+     * 先天+后天成长轨迹
+     */
+    public InnateTrajectoryVO getTrajectory(Long memberId, String memberType, Long familyId) {
+        InnateTrajectoryVO vo = new InnateTrajectoryVO();
+        InnatePortraitVO portrait = getInnatePortrait(memberId, memberType);
+        if (portrait == null) {
+            return null;
+        }
+        vo.setInnatePortrait(portrait);
+
+        Map<String, Integer> innateScores = new LinkedHashMap<>();
+        innateScores.put("mind", portrait.getMindBaseScore());
+        innateScores.put("wisdom", portrait.getWisdomBaseScore());
+        vo.setInnateDimensionScores(innateScores);
+
+        // 后天能量:从 energy_balance 读取(按 dimension code 匹配)
+        Map<String, Object> currentEnergy = new LinkedHashMap<>();
+        Map<String, Object> innateVsCurrent = new LinkedHashMap<>();
+        List<EnergyDimension> allDims = energyDimensionMapper.selectList(null);
+        Integer mindCurrent = null;
+        for (EnergyDimension dim : allDims) {
+            if (dim.getStatus() != null && dim.getStatus() != 1) continue;
+            EnergyBalance balance = energyBalanceMapper.selectOne(
+                    new LambdaQueryWrapper<EnergyBalance>()
+                            .eq(EnergyBalance::getChildId, memberId)
+                            .eq(EnergyBalance::getDimensionId, dim.getId()));
+            int bal = balance != null && balance.getBalance() != null ? balance.getBalance() : 0;
+            currentEnergy.put(dim.getCode() + "Balance", bal);
+            if (balance != null && balance.getTotalEarned() != null) {
+                currentEnergy.put(dim.getCode() + "TotalEarned", balance.getTotalEarned());
+            }
+            if ("mind".equals(dim.getCode())) {
+                mindCurrent = bal;
+            }
+        }
+        vo.setCurrentEnergy(currentEnergy);
+
+        // 先天 vs 后天(心维度)
+        int mindInnate = portrait.getMindBaseScore() != null ? portrait.getMindBaseScore() : 0;
+        int mindCur = mindCurrent != null ? mindCurrent : 0;
+        Map<String, Object> vs = new LinkedHashMap<>();
+        vs.put("mindInnate", mindInnate);
+        vs.put("mindCurrent", mindCur);
+        vs.put("growth", mindCur >= mindInnate ? "+" + (mindCur - mindInnate) : String.valueOf(mindCur - mindInnate));
+        vo.setInnateVsCurrent(vs);
+
+        // 成长趋势文案(确定性生成,AI 增强见任务9)
+        if (mindCur >= mindInnate) {
+            vo.setGrowthTrend("先天心能量基础分 " + mindInnate + ",后天通过情绪打卡/家庭互动成长至 " + mindCur + ",持续向好。");
+        } else {
+            vo.setGrowthTrend("先天心能量基础分 " + mindInnate + ",当前能量 " + mindCur + ",建议增加情绪打卡与家庭共处时间。");
+        }
+        vo.setAdvice("保持情绪打卡频率,增加家庭共处时间,通过阅读与亲子对话持续滋养心能量。");
+        return vo;
+    }
+
+    private Map<String, Object> parseJson(String json) {
+        if (json == null || json.isEmpty()) return new HashMap<>();
+        try {
+            return objectMapper.readValue(json, Map.class);
+        } catch (Exception e) {
+            return new HashMap<>();
+        }
+    }
+
+    private Map<String, Integer> parseJsonInt(String json) {
+        Map<String, Object> raw = parseJson(json);
+        Map<String, Integer> result = new LinkedHashMap<>();
+        for (Map.Entry<String, Object> e : raw.entrySet()) {
+            result.put(e.getKey(), e.getValue() instanceof Number ? ((Number) e.getValue()).intValue() : 0);
+        }
+        return result;
+    }
+
+    private String safeToJson(Object obj) {
+        try {
+            return objectMapper.writeValueAsString(obj);
+        } catch (Exception e) {
+            return "{}";
+        }
+    }
+}
+```
+
+> **依赖处理**:`AiGateway.generateInnateReading` 在任务 8 实现。本步骤(任务 5)先保证聚合逻辑可编译——**先在 `AiGateway` 添加返回 null 的 stub 方法**(见下方步骤 1b),任务 8 再完善为真实 HTTP 调用。AI 方法返回 null 时 `generateAiReading` 走降级模板路径。
+
+- [ ] **步骤 1b:在 AiGateway 添加 stub(任务 8 完善)**
+
+在 `AiGateway.java` 类中临时添加:
+
+```java
+/**
+ * 先天画像 AI 解读(stub,任务8完善为真实HTTP调用)
+ */
+public Map<String, Object> generateInnateReading(Object portrait) {
+    return null;
+}
+```
+
+- [ ] **步骤 2:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/InnatePortraitService.java cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java
+git commit -m "feat: InnatePortraitService 先天画像聚合服务"
+```
+
+### 任务 6:InnatePortraitController(4 个接口)
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/controller/mind/InnatePortraitController.java`
+
+- [ ] **步骤 1:实现 Controller**
+
+```java
+package com.etotem.cfc.controller.mind;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.InnatePortraitReportVO;
+import com.etotem.cfc.dto.InnatePortraitVO;
+import com.etotem.cfc.dto.InnateTrajectoryVO;
+import com.etotem.cfc.dto.NumSoulDetailVO;
+import com.etotem.cfc.service.InnatePortraitService;
+import com.etotem.cfc.util.ParamUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Map;
+
+@Slf4j
+@RestController
+@RequestMapping("/api/mind/innate")
+public class InnatePortraitController {
+
+    @Resource
+    private InnatePortraitService innatePortraitService;
+
+    /**
+     * 获取先天画像
+     */
+    @PostMapping("/portrait")
+    public Result<InnatePortraitVO> portrait(@RequestBody Map<String, Object> params) {
+        try {
+            Long memberId = ParamUtils.getLong(params.get("memberId"));
+            String memberType = params.containsKey("memberType") ? params.get("memberType").toString() : "child";
+            InnatePortraitVO vo = innatePortraitService.getInnatePortrait(memberId, memberType);
+            if (vo == null) {
+                return Result.error("成员不存在或未设置出生信息");
+            }
+            return Result.success(vo);
+        } catch (Exception e) {
+            log.error("获取先天画像失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 生成/获取 AI 解读
+     */
+    @PostMapping("/reading")
+    public Result<InnatePortraitReportVO> reading(@RequestBody Map<String, Object> params) {
+        try {
+            Long memberId = ParamUtils.getLong(params.get("memberId"));
+            String memberType = params.containsKey("memberType") ? params.get("memberType").toString() : "child";
+            Long familyId = ParamUtils.getLong(params.get("familyId"));
+            InnatePortraitReportVO vo = innatePortraitService.generateAiReading(memberId, memberType, familyId);
+            if (vo == null) {
+                return Result.error("成员不存在或未设置出生信息");
+            }
+            return Result.success(vo);
+        } catch (Exception e) {
+            log.error("生成AI解读失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 获取数字能量详情
+     */
+    @PostMapping("/numsoul")
+    public Result<NumSoulDetailVO> numsoul(@RequestBody Map<String, Object> params) {
+        try {
+            Integer year = params.get("year") instanceof Number ? ((Number) params.get("year")).intValue() : null;
+            Integer month = params.get("month") instanceof Number ? ((Number) params.get("month")).intValue() : null;
+            Integer day = params.get("day") instanceof Number ? ((Number) params.get("day")).intValue() : null;
+            if (year == null || month == null || day == null) {
+                return Result.error("请提供出生年月日");
+            }
+            return Result.success(innatePortraitService.calculateNumSoul(year, month, day));
+        } catch (Exception e) {
+            log.error("获取数字能量失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 获取先天+后天成长轨迹
+     */
+    @PostMapping("/trajectory")
+    public Result<InnateTrajectoryVO> trajectory(@RequestBody Map<String, Object> params) {
+        try {
+            Long memberId = ParamUtils.getLong(params.get("memberId"));
+            String memberType = params.containsKey("memberType") ? params.get("memberType").toString() : "child";
+            Long familyId = ParamUtils.getLong(params.get("familyId"));
+            InnateTrajectoryVO vo = innatePortraitService.getTrajectory(memberId, memberType, familyId);
+            if (vo == null) {
+                return Result.error("成员不存在或未设置出生信息");
+            }
+            return Result.success(vo);
+        } catch (Exception e) {
+            log.error("获取成长轨迹失败", e);
+            return Result.error(e.getMessage());
+        }
+    }
+}
+```
+
+- [ ] **步骤 2:检查 ParamUtils 是否有 getInt**
+
+运行:`grep -n "getInt\|public static Integer get" cfc-backend/src/main/java/com/etotem/cfc/util/ParamUtils.java`
+预期:**无 `getInt`**(已确认)。因此 `numsoul` 接口中 `year/month/day` 用类型判断读取:
+
+```java
+Integer year = params.get("year") instanceof Number ? ((Number) params.get("year")).intValue() : null;
+Integer month = params.get("month") instanceof Number ? ((Number) params.get("month")).intValue() : null;
+Integer day = params.get("day") instanceof Number ? ((Number) params.get("day")).intValue() : null;
+```
+
+将任务 6 步骤 1 中 `numsoul` 方法的 `ParamUtils.getInt` 替换为上述写法。
+
+- [ ] **步骤 3:检查路由重复**
+
+运行:
+```bash
+grep -rn '@Mapping' cfc-backend/src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u | grep "mind/innate"
+```
+预期:仅输出 /portrait /reading /numsoul /trajectory,无重复
+
+- [ ] **步骤 4:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/mind/InnatePortraitController.java
+git commit -m "feat: 先天画像4个接口(portrait/reading/numsoul/trajectory)"
+```
+
+### 任务 7:calcInnateScore 权重替换改造
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberAttributeService.java:214-306`
+
+- [ ] **步骤 1:注入 InnatePortraitConfigMapper**
+
+在 `FamilyMemberAttributeService` 类中新增注入(现有字段附近):
+
+```java
+@Resource
+private com.etotem.cfc.mapper.InnatePortraitConfigMapper innatePortraitConfigMapper;
+
+@Resource
+private com.etotem.cfc.mapper.NumSoulDetailConfigMapper numSoulDetailConfigMapper;
+```
+
+- [ ] **步骤 2:替换加权求和部分**
+
+将 `calcInnateScore` 中硬编码加权(原 293-296 行):
+
+```java
+// --- Weighted sum: zodiac×50% + bazi×30% + blood×20% ---
+// Weights in bps: 50% = 5000, 30% = 3000, 20% = 2000
+Integer result = (zodiacScore * 5000 + baziScore * 3000 + bloodScore * 2000) / 10000;
+```
+
+替换为:
+
+```java
+// --- Weighted sum: 从 innate_portrait_config 读取权重(可配置)---
+// 未配置时回退硬编码 zodiac×50% + bazi×30% + blood×20%
+int wZodiac = 5000, wBazi = 3000, wBlood = 2000, wNumsoul = 0;
+List<InnatePortraitConfig> configs = innatePortraitConfigMapper.selectList(null);
+if (configs != null && !configs.isEmpty()) {
+    for (InnatePortraitConfig cfg : configs) {
+        int w = (cfg.getWeight() != null ? cfg.getWeight() : 0);
+        if (cfg.getEnabled() != null && cfg.getEnabled() == 0) {
+            w = 0;
+        }
+        switch (cfg.getSourceType()) {
+            case "zodiac": wZodiac = w; break;
+            case "bazi": wBazi = w; break;
+            case "blood": wBlood = w; break;
+            case "numsoul": wNumsoul = w; break;
+            default: break;
+        }
+    }
+}
+
+// numsoul 贡献:按生命灵数匹配 life_path 配置
+int numsoulScore = 0;
+if (wNumsoul > 0 && attrs.getBehaviorModifier() != null) {
+    Map<String, Object> modifier = fromJson(attrs.getBehaviorModifier());
+    Object lifeNum = modifier.get("lifeNumber");
+    if (lifeNum instanceof Number) {
+        LambdaQueryWrapper<NumSoulDetailConfig> nsWrapper = new LambdaQueryWrapper<>();
+        nsWrapper.eq(NumSoulDetailConfig::getNumberType, "life_path")
+                 .eq(NumSoulDetailConfig::getNumberValue, ((Number) lifeNum).intValue());
+        NumSoulDetailConfig nsCfg = numSoulDetailConfigMapper.selectOne(nsWrapper);
+        if (nsCfg != null && nsCfg.getMindBase() != null) {
+            numsoulScore = nsCfg.getMindBase();
+        }
+    }
+}
+
+// 累加启用来源的权重,归一化
+int totalWeight = wZodiac + wBazi + wBlood + wNumsoul;
+int result;
+if (totalWeight > 0) {
+    result = (zodiacScore * wZodiac + baziScore * wBazi + bloodScore * wBlood + numsoulScore * wNumsoul) / totalWeight;
+} else {
+    result = 0;
+}
+```
+
+- [ ] **步骤 3:处理缓存失效问题(关键)**
+
+**问题**:`calcInnateScore` 开头有缓存逻辑 `if (attrs.getMindBaseScore() != null && attrs.getMindBaseScore() > 0) return attrs.getMindBaseScore();`——权重修改后已缓存的值不会重算。
+
+**改造**:在权重配置变化时清除缓存。在 `InnatePortraitConfigController.update/delete` 成功后调用新增方法:
+
+```java
+/**
+ * 权重配置变化时清除所有已缓存的先天分,触发重算
+ * 说明:本服务继承 ServiceImpl<FamilyMemberAttributesMapper, FamilyMemberAttributes>,
+ * 可直接使用 this.list() / this.updateById()
+ */
+public void invalidateAllInnateScores() {
+    List<FamilyMemberAttributes> all = this.list();
+    for (FamilyMemberAttributes a : all) {
+        a.setMindBaseScore(0);
+        a.setWisdomBaseScore(0);
+        this.updateById(a);
+    }
+    log.info("已清除 {} 条成员先天分缓存", all.size());
+}
+```
+
+(在 `FamilyMemberAttributeService` 中新增该方法。因该类继承 `ServiceImpl<FamilyMemberAttributesMapper, FamilyMemberAttributes>`,直接用 `this.list()` / `this.updateById()`,无需额外注入 mapper。)
+
+- [ ] **步骤 4:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS(`this.list()` / `this.updateById()` 来自 ServiceImpl 基类,已确认可用)
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberAttributeService.java
+git commit -m "feat: calcInnateScore 支持可配置权重+数字能量参与+缓存失效"
+```
+
+---
+
+## 阶段 4:AI 解读(LangGraph)
+
+### 任务 8:AiGateway.generateInnateReading
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java`
+
+- [ ] **步骤 1:实现 generateInnateReading**
+
+在 `AiGateway` 类中(`generateQuestionnaire` 之后)新增:
+
+```java
+/**
+ * 调用 Python 先天画像解读服务
+ * @param portrait 先天画像数据(InnatePortraitVO 转 Map)
+ * @return 含 "reading" 字段的 Map;失败返回 null(调用方降级模板)
+ */
+public Map<String, Object> generateInnateReading(Object portrait) {
+    if (!enabled || isCircuitOpen()) return null;
+    try {
+        ObjectNode body = objectMapper.createObjectNode();
+        body.set("portrait", objectMapper.valueToTree(portrait));
+
+        HttpEntity<String> entity = new HttpEntity<>(body.toString(), createJsonHeaders());
+        String url = baseUrl + "/api/v1/innate/reading";
+
+        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("reading", root.has("reading") ? root.get("reading").asText() : "");
+            consecutiveFailures.set(0);
+            return result;
+        }
+        return null;
+    } catch (Exception e) {
+        log.warn("AiGateway generateInnateReading 调用失败: {}", e.getMessage());
+        recordFailure();
+        return null;
+    }
+}
+```
+
+- [ ] **步骤 2:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java
+git commit -m "feat: AiGateway 新增 generateInnateReading"
+```
+
+### 任务 9:innate_portrait_graph(LangGraph)
+
+**文件:**
+- 创建:`cfc-langgraph/app/graphs/innate_portrait_graph.py`
+- 创建:`cfc-langgraph/app/api/innate.py`
+- 修改:`cfc-langgraph/app/main.py`
+
+- [ ] **步骤 1:创建 innate_portrait_graph.py**
+
+```python
+from typing import TypedDict
+from langgraph.graph import StateGraph, START, END
+from langchain_openai import ChatOpenAI
+from langchain_core.messages import SystemMessage, HumanMessage
+from app.config import settings
+import json
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class InnatePortraitState(TypedDict):
+    portrait: dict
+    reading: str | None
+    raw_output: str | None
+
+
+SYSTEM_PROMPT = (
+    "你是一位家庭教育成长陪伴师,擅长用东西方智慧解读孩子的先天特质。"
+    "你的任务是根据用户的先天画像数据(八字五行、星座生肖、血型、生命灵数等),"
+    "生成一段温暖、积极、可操作的个性化解读。"
+    "原则:"
+    "1. 强调'先天特质 + 后天成长'视角,避免宿命论措辞"
+    "2. 心维度对应五行'火',代表情绪稳定、理解包容、传承传递"
+    "3. 措辞要面向家长,给出亲子互动建议"
+    "4. 输出 300 字以内,直接输出解读文本,不要 JSON 包装"
+)
+
+
+def create_innate_portrait_graph():
+    async def build_prompt(state: InnatePortraitState) -> dict:
+        portrait = state["portrait"]
+        # 提取关键画像数据
+        summary = {
+            "zodiac": portrait.get("zodiac"),
+            "bloodType": portrait.get("bloodType"),
+            "mindBaseScore": portrait.get("mindBaseScore"),
+            "wisdomBaseScore": portrait.get("wisdomBaseScore"),
+            "numSoul": portrait.get("numSoul"),
+            "wuxingElements": portrait.get("wuxingElements"),
+            "eightCharacters": portrait.get("eightCharacters"),
+        }
+        return {
+            "portrait": portrait,
+            "raw_output": json.dumps(summary, ensure_ascii=False),
+        }
+
+    async def generate(state: InnatePortraitState) -> dict:
+        llm = ChatOpenAI(
+            model=settings.llm_model,
+            api_key=settings.llm_api_key,
+            base_url=settings.llm_base_url,
+            temperature=0.7,
+        )
+        user_msg = (
+            f"请为以下孩子生成先天心维度画像解读:\n"
+            f"{state['raw_output']}\n\n"
+            f"请按此结构输出:\n"
+            f"一、先天特质(基于五行/星座/灵数综合分析)\n"
+            f"二、先天优势(列出2-3个)\n"
+            f"三、心维度成长建议\n"
+            f"四、家长互动建议\n"
+            f"五、五行平衡提示"
+        )
+        resp = await llm.ainvoke([
+            SystemMessage(content=SYSTEM_PROMPT),
+            HumanMessage(content=user_msg),
+        ])
+        return {"reading": resp.content}
+
+    graph = StateGraph(InnatePortraitState)
+    graph.add_node("build_prompt", build_prompt)
+    graph.add_node("generate", generate)
+    graph.add_edge(START, "build_prompt")
+    graph.add_edge("build_prompt", "generate")
+    graph.add_edge("generate", END)
+    return graph.compile()
+```
+
+- [ ] **步骤 2:创建 app/api/innate.py**
+
+```python
+import uuid
+from fastapi import APIRouter
+from pydantic import BaseModel, Field
+from app.graphs.innate_portrait_graph import create_innate_portrait_graph
+
+router = APIRouter(prefix="/api/v1", tags=["innate"])
+
+_graph = None
+
+
+def get_graph():
+    global _graph
+    if _graph is None:
+        _graph = create_innate_portrait_graph()
+    return _graph
+
+
+class InnateReadingRequest(BaseModel):
+    portrait: dict = Field(..., description="先天画像数据")
+
+
+@router.post("/innate/reading")
+async def innate_reading(req: InnateReadingRequest):
+    """先天画像 AI 解读"""
+    trace_id = str(uuid.uuid4())
+    graph = get_graph()
+    initial_state = {
+        "portrait": req.portrait,
+        "reading": None,
+        "raw_output": None,
+    }
+    try:
+        result = await graph.ainvoke(initial_state)
+        return {
+            "reading": result.get("reading", ""),
+            "trace_id": trace_id,
+        }
+    except Exception as e:
+        logger = __import__("logging").getLogger(__name__)
+        logger.error("innate reading failed: %s", e)
+        return {"reading": "", "trace_id": trace_id}
+```
+
+- [ ] **步骤 3:注册路由到 main.py**
+
+在 `app/main.py` 中,import 部分新增:
+
+```python
+from app.api import innate
+```
+
+并在 `app.include_router(...)` 区段新增:
+
+```python
+app.include_router(innate.router)
+```
+
+- [ ] **步骤 4:验证配置字段语义**
+
+运行:`grep -n "llm_model\|llm_api_key\|llm_base_url" cfc-langgraph/app/config.py`
+预期:找到 3 个字段(已确认存在)——`llm_model: str = "deepseek-chat"`、`llm_api_key: str`、`llm_base_url: str`。本计划步骤 1 中的 `settings.llm_model` / `settings.llm_api_key` / `settings.llm_base_url` 引用正确,无需替换。
+
+- [ ] **步骤 5:Python 语法检查**
+
+运行:`cd cfc-langgraph && python -m py_compile app/graphs/innate_portrait_graph.py app/api/innate.py`
+预期:无输出(编译通过)
+
+- [ ] **步骤 6:Commit**
+
+```bash
+git add cfc-langgraph/app/graphs/innate_portrait_graph.py cfc-langgraph/app/api/innate.py cfc-langgraph/app/main.py
+git commit -m "feat: 先天画像AI解读graph + /api/v1/innate/reading端点"
+```
+
+---
+
+## 阶段 5:管理端配置页
+
+### 任务 10:管理端 Controller(2 个)
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/controller/admin/InnatePortraitConfigController.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/controller/admin/NumSoulDetailConfigController.java`
+
+- [ ] **步骤 1:创建 InnatePortraitConfigController**
+
+```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.InnatePortraitConfig;
+import com.etotem.cfc.mapper.InnatePortraitConfigMapper;
+import com.etotem.cfc.service.FamilyMemberAttributeService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/admin/innate-portrait-config")
+public class InnatePortraitConfigController {
+
+    @Resource
+    private InnatePortraitConfigMapper innatePortraitConfigMapper;
+
+    @Resource
+    private FamilyMemberAttributeService familyMemberAttributeService;
+
+    @PostMapping("/list")
+    public Result<List<InnatePortraitConfig>> list() {
+        return Result.success(innatePortraitConfigMapper.selectList(
+                new LambdaQueryWrapper<InnatePortraitConfig>().orderByAsc(InnatePortraitConfig::getSortOrder)));
+    }
+
+    @PostMapping("/update")
+    public Result<InnatePortraitConfig> update(@RequestBody InnatePortraitConfig config) {
+        if (config.getId() == null) {
+            return Result.error("缺少id");
+        }
+        innatePortraitConfigMapper.updateById(config);
+        // 权重变化:清除所有先天分缓存,触发重算
+        familyMemberAttributeService.invalidateAllInnateScores();
+        return Result.success(config);
+    }
+
+    @PostMapping("/create")
+    public Result<InnatePortraitConfig> create(@RequestBody InnatePortraitConfig config) {
+        innatePortraitConfigMapper.insert(config);
+        return Result.success(config);
+    }
+
+    @PostMapping("/delete")
+    public Result<Void> delete(@RequestParam Long id) {
+        innatePortraitConfigMapper.deleteById(id);
+        familyMemberAttributeService.invalidateAllInnateScores();
+        return Result.success(null);
+    }
+}
+```
+
+- [ ] **步骤 2:创建 NumSoulDetailConfigController**
+
+```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.NumSoulDetailConfig;
+import com.etotem.cfc.mapper.NumSoulDetailConfigMapper;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/admin/numsoul-detail-config")
+public class NumSoulDetailConfigController {
+
+    @Resource
+    private NumSoulDetailConfigMapper numSoulDetailConfigMapper;
+
+    @PostMapping("/list")
+    public Result<List<NumSoulDetailConfig>> list(@RequestBody(required = false) java.util.Map<String, Object> params) {
+        LambdaQueryWrapper<NumSoulDetailConfig> wrapper = new LambdaQueryWrapper<>();
+        if (params != null && params.get("numberType") != null) {
+            wrapper.eq(NumSoulDetailConfig::getNumberType, params.get("numberType").toString());
+        }
+        wrapper.orderByAsc(NumSoulDetailConfig::getNumberType).orderByAsc(NumSoulDetailConfig::getNumberValue);
+        return Result.success(numSoulDetailConfigMapper.selectList(wrapper));
+    }
+
+    @PostMapping("/create")
+    public Result<NumSoulDetailConfig> create(@RequestBody NumSoulDetailConfig config) {
+        numSoulDetailConfigMapper.insert(config);
+        return Result.success(config);
+    }
+
+    @PostMapping("/update")
+    public Result<NumSoulDetailConfig> update(@RequestBody NumSoulDetailConfig config) {
+        if (config.getId() == null) {
+            return Result.error("缺少id");
+        }
+        numSoulDetailConfigMapper.updateById(config);
+        return Result.success(config);
+    }
+
+    @PostMapping("/delete")
+    public Result<Void> delete(@RequestParam Long id) {
+        numSoulDetailConfigMapper.deleteById(id);
+        return Result.success(null);
+    }
+}
+```
+
+- [ ] **步骤 3:编译验证 + 路由查重**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+运行:
+```bash
+grep -rn '@Mapping' cfc-backend/src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u | grep -E "innate-portrait|numsoul-detail"
+```
+预期:仅输出新接口路径,无重复
+
+- [ ] **步骤 4:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/admin/InnatePortraitConfigController.java cfc-backend/src/main/java/com/etotem/cfc/controller/admin/NumSoulDetailConfigController.java
+git commit -m "feat: 管理端先天画像/数字能量配置接口"
+```
+
+### 任务 11:管理端页面(2 个)
+
+**文件:**
+- 创建:`cfc-web/src/views/admin/InnatePortraitConfig.vue`
+- 创建:`cfc-web/src/views/admin/NumSoulDetailConfig.vue`
+
+- [ ] **步骤 1:创建 InnatePortraitConfig.vue**
+
+参照现有 `ZodiacConfigs.vue` 的表单+表格模式,核心结构:
+
+```vue
+<template>
+  <div class="app-container">
+    <el-card shadow="never">
+      <div slot="header">
+        <span>先天画像来源权重配置</span>
+        <el-button style="float: right" type="primary" size="small" @click="load">刷新</el-button>
+      </div>
+      <el-table :data="list" border stripe>
+        <el-table-column prop="sourceName" label="来源" width="120" />
+        <el-table-column label="权重" width="220">
+          <template slot-scope="scope">
+            <el-input-number
+              v-model="scope.row.weight"
+              :min="0" :max="10000" :step="500"
+              size="small"
+            />
+            <span style="margin-left: 8px; color: #999">
+              {{ (scope.row.weight / 100).toFixed(1) }}%
+            </span>
+          </template>
+        </el-table-column>
+        <el-table-column label="启用" width="100">
+          <template slot-scope="scope">
+            <el-switch v-model="scope.row.enabled" :active-value="1" :inactive-value="0" />
+          </template>
+        </el-table-column>
+        <el-table-column label="AI解读" width="100">
+          <template slot-scope="scope">
+            <el-switch v-model="scope.row.aiEnabled" :active-value="1" :inactive-value="0" />
+          </template>
+        </el-table-column>
+        <el-table-column label="排序" width="120">
+          <template slot-scope="scope">
+            <el-input-number v-model="scope.row.sortOrder" :min="0" size="small" />
+          </template>
+        </el-table-column>
+        <el-table-column label="操作">
+          <template slot-scope="scope">
+            <el-button type="primary" size="mini" @click="save(scope.row)">保存</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div style="margin-top: 12px; color: #999; font-size: 12px">
+        提示:权重按基点配置(2500=25%)。保存后自动清除所有成员先天分缓存并触发重算。启用来源权重之和不足100%时按比例归一化。
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'InnatePortraitConfig',
+  data() {
+    return { list: [] }
+  },
+  created() { this.load() },
+  methods: {
+    load() {
+      this.$http.post('/api/admin/innate-portrait-config/list').then(res => {
+        this.list = res.data || []
+      })
+    },
+    save(row) {
+      this.$http.post('/api/admin/innate-portrait-config/update', row).then(() => {
+        this.$message.success('保存成功')
+        this.load()
+      })
+    }
+  }
+}
+</script>
+```
+
+- [ ] **步骤 2:创建 NumSoulDetailConfig.vue**
+
+```vue
+<template>
+  <div class="app-container">
+    <el-card shadow="never">
+      <div slot="header">
+        <span>数字能量配置</span>
+        <div style="float: right">
+          <el-select v-model="filterType" placeholder="筛选类型" clearable size="small" style="width: 160px" @change="load">
+            <el-option label="生命灵数" value="life_path" />
+            <el-option label="天赋数" value="talent" />
+            <el-option label="生日数" value="birthday" />
+            <el-option label="命运数" value="destiny" />
+          </el-select>
+          <el-button type="primary" size="small" style="margin-left: 8px" @click="showCreate">新增</el-button>
+        </div>
+      </div>
+      <el-table :data="list" border stripe>
+        <el-table-column prop="numberType" label="类型" width="110">
+          <template slot-scope="scope">{{ typeLabel(scope.row.numberType) }}</template>
+        </el-table-column>
+        <el-table-column prop="numberValue" label="数字" width="80" />
+        <el-table-column prop="title" label="称号" width="120" />
+        <el-table-column prop="keywords" label="关键词" />
+        <el-table-column prop="mindBase" label="心先天分" width="100" />
+        <el-table-column prop="mindAdvice" label="成长建议" show-overflow-tooltip />
+        <el-table-column label="代表色" width="80">
+          <template slot-scope="scope">
+            <span :style="{ display: 'inline-block', width: '20px', height: '20px', background: scope.row.colorHex, borderRadius: '4px' }" />
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="140">
+          <template slot-scope="scope">
+            <el-button type="text" @click="showEdit(scope.row)">编辑</el-button>
+            <el-button type="text" style="color: #f56c6c" @click="remove(scope.row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <el-dialog :title="form.id ? '编辑' : '新增'" :visible.sync="dialogVisible" width="520px">
+        <el-form :model="form" label-width="100px">
+          <el-form-item label="类型">
+            <el-select v-model="form.numberType">
+              <el-option label="生命灵数" value="life_path" />
+              <el-option label="天赋数" value="talent" />
+              <el-option label="生日数" value="birthday" />
+              <el-option label="命运数" value="destiny" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="数字"><el-input-number v-model="form.numberValue" :min="1" :max="99" /></el-form-item>
+          <el-form-item label="称号"><el-input v-model="form.title" /></el-form-item>
+          <el-form-item label="关键词"><el-input v-model="form.keywords" /></el-form-item>
+          <el-form-item label="心先天分"><el-input-number v-model="form.mindBase" :min="0" :max="100" /></el-form-item>
+          <el-form-item label="成长建议"><el-input type="textarea" v-model="form.mindAdvice" /></el-form-item>
+          <el-form-item label="代表色"><el-color-picker v-model="form.colorHex" /></el-form-item>
+        </el-form>
+        <div slot="footer">
+          <el-button @click="dialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="save">保存</el-button>
+        </div>
+      </el-dialog>
+    </el-card>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'NumSoulDetailConfig',
+  data() {
+    return {
+      list: [],
+      filterType: '',
+      dialogVisible: false,
+      form: { id: null, numberType: 'life_path', numberValue: 1, title: '', keywords: '', mindBase: 50, mindAdvice: '', colorHex: '#FF4444' }
+    }
+  },
+  created() { this.load() },
+  methods: {
+    typeLabel(t) {
+      return { life_path: '生命灵数', talent: '天赋数', birthday: '生日数', destiny: '命运数' }[t] || t
+    },
+    load() {
+      this.$http.post('/api/admin/numsoul-detail-config/list', { numberType: this.filterType || undefined }).then(res => {
+        this.list = res.data || []
+      })
+    },
+    showCreate() {
+      this.form = { id: null, numberType: 'life_path', numberValue: 1, title: '', keywords: '', mindBase: 50, mindAdvice: '', colorHex: '#FF4444' }
+      this.dialogVisible = true
+    },
+    showEdit(row) {
+      this.form = Object.assign({}, row)
+      this.dialogVisible = true
+    },
+    save() {
+      const url = this.form.id ? '/api/admin/numsoul-detail-config/update' : '/api/admin/numsoul-detail-config/create'
+      this.$http.post(url, this.form).then(() => {
+        this.$message.success('保存成功')
+        this.dialogVisible = false
+        this.load()
+      })
+    },
+    remove(row) {
+      this.$confirm('确认删除该配置?', '提示', { type: 'warning' }).then(() => {
+        this.$http.post('/api/admin/numsoul-detail-config/delete', { id: row.id }).then(() => {
+          this.$message.success('删除成功')
+          this.load()
+        })
+      })
+    }
+  }
+}
+</script>
+```
+
+- [ ] **步骤 3:删除请求方式已确认**
+
+已确认现有项目约定:后端 `delete` 接口用 `@RequestParam Long id`,前端通过 `data: { id }`(body JSON)传参可正常工作(参照 `ZodiacConfigs.vue` 的 `deleteZodiacConfig` → `data: { id }`)。本计划任务 11 步骤 2 的 `remove()` 已按此约定编写,无需调整。
+
+- [ ] **步骤 4:注册路由**
+
+在 `cfc-web/src/router/` 中新增 2 条路由(参照现有 ZodiacConfigs 路由),菜单挂到"五维能量/先天画像"分组。
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-web/src/views/admin/InnatePortraitConfig.vue cfc-web/src/views/admin/NumSoulDetailConfig.vue cfc-web/src/router/
+git commit -m "feat: 管理端先天画像配置页 + 数字能量配置页"
+```
+
+---
+
+## 阶段 6:小程序
+
+### 任务 12:API 封装 + 页面注册
+
+**文件:**
+- 修改:`cfc-frontend/utils/api.js`
+- 修改:`cfc-frontend/pages.json`
+
+- [ ] **步骤 1:api.js 新增接口**
+
+在 `cfc-frontend/utils/api.js` 中,参照现有 `traditionalMirror` 等方法新增:
+
+```javascript
+// 先天画像
+export function getInnatePortrait(data) { return request('/api/mind/innate/portrait', data) }
+export function getInnateReading(data) { return request('/api/mind/innate/reading', data) }
+export function getInnateNumsoul(data) { return request('/api/mind/innate/numsoul', data) }
+export function getInnateTrajectory(data) { return request('/api/mind/innate/trajectory', data) }
+```
+
+- [ ] **步骤 2:pages.json 注册 2 个新页面**
+
+在 `pages/mind-detail` 的 pages 数组末尾追加:
+
+```json
+{
+  "path": "innate-portrait",
+  "style": { "navigationBarTitleText": "先天画像" }
+},
+{
+  "path": "innate-trajectory",
+  "style": { "navigationBarTitleText": "先天成长轨迹" }
+}
+```
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-frontend/utils/api.js cfc-frontend/pages.json
+git commit -m "feat: 小程序先天画像API封装+页面注册"
+```
+
+### 任务 13:先天画像报告页 innate-portrait.vue
+
+**文件:**
+- 创建:`cfc-frontend/pages/mind-detail/innate-portrait.vue`
+
+- [ ] **步骤 1:实现页面**
+
+参照 `traditional-mirror.vue` 的现有样式,遵守小程序限制(无 `?.`、无 Grid、`:key` 用方法、`parseDate()`):
+
+```vue
+<template>
+  <view class="portrait-container">
+    <view class="loading-state" v-if="loading">
+      <text>解读生成中...</text>
+    </view>
+
+    <template v-if="!loading && report">
+      <!-- 头部:心先天分 -->
+      <view class="portrait-header">
+        <text class="header-score">{{ report.portrait.mindBaseScore || 0 }}</text>
+        <text class="header-label">心维度先天能量</text>
+      </view>
+
+      <!-- 先天画像 -->
+      <view class="section">
+        <view class="section-title">先天画像</view>
+        <view class="bazi-grid" v-if="hasEightChars">
+          <view class="bazi-column" v-for="(pillar, key) in report.portrait.eightCharacters" :key="key">
+            <text class="bazi-label">{{ pillarLabel(key) }}</text>
+            <text class="bazi-value">{{ pillar }}</text>
+          </view>
+        </view>
+        <view class="info-row" v-if="report.portrait.zodiac">
+          <text class="info-label">生肖</text>
+          <text class="info-value">{{ report.portrait.zodiac }}</text>
+        </view>
+        <view class="info-row" v-if="report.portrait.bloodType">
+          <text class="info-label">血型</text>
+          <text class="info-value">{{ report.portrait.bloodType }}</text>
+        </view>
+        <view class="info-row" v-if="numSoulData">
+          <text class="info-label">生命灵数</text>
+          <text class="info-value">{{ numSoulData.lifePath }} {{ numSoulData.lifePathTitle || '' }}</text>
+        </view>
+      </view>
+
+      <!-- AI 解读 -->
+      <view class="section" v-if="report.aiReading">
+        <view class="section-title">
+          <text>AI 解读</text>
+          <text class="fallback-tag" v-if="report.fallbackUsed">模板解读</text>
+        </view>
+        <text class="reading-text">{{ report.aiReading }}</text>
+      </view>
+
+      <!-- 五行平衡 -->
+      <view class="section" v-if="hasWuxing">
+        <view class="section-title">五行能量</view>
+        <view class="wuxing-item" v-for="(wv, wk) in report.portrait.wuxingElements" :key="wk">
+          <text class="wuxing-label">{{ wuxingLabel(wk) }}</text>
+          <view class="wuxing-track">
+            <view class="wuxing-fill" :style="{ width: wv + '%', background: wuxingColor(wk) }"></view>
+          </view>
+          <text class="wuxing-value">{{ wv }}%</text>
+        </view>
+      </view>
+
+      <!-- 成长轨迹入口 -->
+      <view class="trajectory-entry" @click="goTrajectory">
+        <text>查看先天+后天成长轨迹</text>
+        <text class="arrow">›</text>
+      </view>
+    </template>
+
+    <view class="empty-state" v-if="!loading && !report && loadError">
+      <text>{{ loadError }}</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getInnateReading } from '../../utils/api'
+export default {
+  data() {
+    return {
+      memberId: null,
+      memberType: 'child',
+      familyId: null,
+      loading: false,
+      report: null,
+      loadError: '',
+      wuxingConfig: {
+        wood: { label: '木', color: '#10B981' },
+        fire: { label: '火', color: '#FF6B9D' },
+        earth: { label: '土', color: '#FF8C42' },
+        metal: { label: '金', color: '#6366F1' },
+        water: { label: '水', color: '#3B82F6' }
+      },
+      pillarLabels: { year: '年柱', month: '月柱', day: '日柱', hour: '时柱' }
+    }
+  },
+  computed: {
+    numSoulData() {
+      if (!this.report || !this.report.portrait || !this.report.portrait.numSoul) return null
+      return this.report.portrait.numSoul
+    },
+    hasEightChars() {
+      return !!this.report && !!this.report.portrait && !!this.report.portrait.eightCharacters
+    },
+    hasWuxing() {
+      return !!this.report && !!this.report.portrait && !!this.report.portrait.wuxingElements
+    }
+  },
+  onLoad(options) {
+    this.memberId = options.memberId ? Number(options.memberId) : null
+    this.memberType = options.memberType || 'child'
+    this.familyId = options.familyId ? Number(options.familyId) : null
+    if (!this.memberId) {
+      this.loadError = '缺少成员ID'
+      return
+    }
+    this.load()
+  },
+  methods: {
+    load() {
+      var self = this
+      self.loading = true
+      getInnateReading({ memberId: self.memberId, memberType: self.memberType, familyId: self.familyId }).then(function (res) {
+        self.loading = false
+        if (res && res.code === 200) {
+          self.report = res.data
+        } else {
+          self.loadError = (res && res.message) || '加载失败'
+        }
+      }).catch(function () {
+        self.loading = false
+        self.loadError = '网络异常'
+      })
+    },
+    pillarLabel(key) {
+      return this.pillarLabels[key] || key
+    },
+    wuxingLabel(key) {
+      var cfg = this.wuxingConfig[key]
+      return cfg ? cfg.label : key
+    },
+    wuxingColor(key) {
+      var cfg = this.wuxingConfig[key]
+      return cfg ? cfg.color : '#ccc'
+    },
+    goTrajectory() {
+      uni.navigateTo({
+        url: '/pages/mind-detail/innate-trajectory?memberId=' + this.memberId + '&memberType=' + this.memberType + '&familyId=' + (this.familyId || '')
+      })
+    }
+  }
+}
+</script>
+
+<style>
+/* 参照 traditional-mirror.vue 现有样式体系,flexbox 布局,禁止 Grid */
+</style>
+```
+
+- [ ] **步骤 2:确认 onLoad 参数来源**
+
+在 `pages/mind-detail/index.vue` 的"先天画像"入口卡片跳转时传 `memberId/memberType/familyId`(任务 14 实现),此处确保从 `options` 读取。
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-frontend/pages/mind-detail/innate-portrait.vue
+git commit -m "feat: 小程序先天画像报告页"
+```
+
+### 任务 14:成长轨迹页 + 心维度首页增强
+
+**文件:**
+- 创建:`cfc-frontend/pages/mind-detail/innate-trajectory.vue`
+- 修改:`cfc-frontend/pages/mind-detail/index.vue`
+
+- [ ] **步骤 1:创建 innate-trajectory.vue**
+
+```vue
+<template>
+  <view class="trajectory-container">
+    <view class="loading-state" v-if="loading">
+      <text>加载中...</text>
+    </view>
+
+    <template v-if="!loading && trajectory">
+      <!-- 先天 vs 后天 -->
+      <view class="section">
+        <view class="section-title">先天 vs 后天</view>
+        <view class="compare-row">
+          <view class="compare-item">
+            <text class="compare-label">先天心能量</text>
+            <text class="compare-value">{{ trajectory.innateVsCurrent.mindInnate || 0 }}</text>
+          </view>
+          <view class="compare-item">
+            <text class="compare-label">当前心能量</text>
+            <text class="compare-value">{{ trajectory.innateVsCurrent.mindCurrent || 0 }}</text>
+          </view>
+          <view class="compare-item">
+            <text class="compare-label">成长</text>
+            <text class="compare-value" :style="{ color: growthColor }">{{ trajectory.innateVsCurrent.growth || '0' }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 五维先天分 -->
+      <view class="section" v-if="hasInnateScores">
+        <view class="section-title">五维先天基础分</view>
+        <view class="dim-row" v-for="(score, dim) in trajectory.innateDimensionScores" :key="dim">
+          <text class="dim-label">{{ dimLabel(dim) }}</text>
+          <view class="dim-track">
+            <view class="dim-fill" :style="{ width: score + '%' }"></view>
+          </view>
+          <text class="dim-value">{{ score }}</text>
+        </view>
+      </view>
+
+      <!-- 成长趋势 -->
+      <view class="section" v-if="trajectory.growthTrend">
+        <view class="section-title">成长趋势</view>
+        <text class="trend-text">{{ trajectory.growthTrend }}</text>
+      </view>
+
+      <!-- 成长建议 -->
+      <view class="section" v-if="trajectory.advice">
+        <view class="section-title">成长建议</view>
+        <text class="advice-text">{{ trajectory.advice }}</text>
+      </view>
+    </template>
+
+    <view class="empty-state" v-if="!loading && !trajectory && loadError">
+      <text>{{ loadError }}</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getInnateTrajectory } from '../../utils/api'
+export default {
+  data() {
+    return {
+      memberId: null,
+      memberType: 'child',
+      familyId: null,
+      loading: false,
+      trajectory: null,
+      loadError: '',
+      dimNames: { body: '身', mind: '心', wisdom: '智', action: '行', wealth: '富' }
+    }
+  },
+  computed: {
+    hasInnateScores() {
+      return !!this.trajectory && !!this.trajectory.innateDimensionScores
+    },
+    growthColor() {
+      if (!this.trajectory || !this.trajectory.innateVsCurrent) return '#999'
+      var g = this.trajectory.innateVsCurrent.growth || '0'
+      if (g.indexOf('-') === 0) return '#f56c6c'
+      return '#07c160'
+    }
+  },
+  onLoad(options) {
+    this.memberId = options.memberId ? Number(options.memberId) : null
+    this.memberType = options.memberType || 'child'
+    this.familyId = options.familyId ? Number(options.familyId) : null
+    if (!this.memberId) {
+      this.loadError = '缺少成员ID'
+      return
+    }
+    this.load()
+  },
+  methods: {
+    load() {
+      var self = this
+      self.loading = true
+      getInnateTrajectory({ memberId: self.memberId, memberType: self.memberType, familyId: self.familyId }).then(function (res) {
+        self.loading = false
+        if (res && res.code === 200) {
+          self.trajectory = res.data
+        } else {
+          self.loadError = (res && res.message) || '加载失败'
+        }
+      }).catch(function () {
+        self.loading = false
+        self.loadError = '网络异常'
+      })
+    },
+    dimLabel(dim) {
+      return this.dimNames[dim] || dim
+    }
+  }
+}
+</script>
+```
+
+- [ ] **步骤 2:心维度首页增加"先天画像"入口卡片**
+
+在 `pages/mind-detail/index.vue` 的 `funcList` 数组(现有"家庭天盘"附近)新增:
+
+```javascript
+{ icon: '\u{1F31F}', label: '先天画像', needLogin: true, page: '/pages/mind-detail/innate-portrait' },
+```
+
+跳转时需传当前成员信息:找到 funcList 点击处理方法,跳转前追加 `memberId/memberType/familyId` 参数(参照现有"家庭天盘"的跳转方式从 `currentMember` 或 `familyId` data 读取)。
+
+- [ ] **步骤 3:verify 页面跳转参数**
+
+检查 `pages/mind-detail/index.vue` 中现有哪些 `currentMember`/`familyId` data 字段可用,确保跳转 innate-portrait 时能取到 memberId。
+
+- [ ] **步骤 4:Commit**
+
+```bash
+git add cfc-frontend/pages/mind-detail/innate-trajectory.vue cfc-frontend/pages/mind-detail/index.vue
+git commit -m "feat: 小程序先天成长轨迹页 + 心维度首页入口"
+```
+
+---
+
+## 阶段 7:收尾验证
+
+### 任务 15:全量验证 + 文档更新
+
+- [ ] **步骤 1:后端全量编译 + 测试**
+
+运行:`cd cfc-backend && mvn clean compile && mvn test -Dtest=NumSoulCalculatorTest`
+预期:BUILD SUCCESS,测试全绿
+
+- [ ] **步骤 2:路由查重**
+
+运行:
+```bash
+grep -rn '@Mapping' cfc-backend/src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u
+```
+预期:新接口 `/api/mind/innate/*` 与 `/api/admin/innate-portrait-config/*`、`/api/admin/numsoul-detail-config/*` 无重复
+
+- [ ] **步骤 3:Bean 命名冲突检查**
+
+新类名:`InnatePortraitService`、`InnatePortraitController`、`InnatePortraitConfig`(entity)、`InnatePortraitConfigController`、`InnatePortraitReportMapper`、`NumSoulDetailConfigController`——运行 `grep -rn "class InnatePortrait" cfc-backend/src/main/java/` 确认无重名类。
+
+- [ ] **步骤 4:Python 编译检查**
+
+运行:`cd cfc-langgraph && python -m py_compile app/graphs/innate_portrait_graph.py app/api/innate.py app/main.py`
+预期:无输出
+
+- [ ] **步骤 5:更新 API_REFERENCE.md**
+
+在 `docs/superpowers/api/API_REFERENCE.md` 的"其他接口"或"管理员接口"区段补充:
+- `POST /api/mind/innate/portrait` — 先天画像
+- `POST /api/mind/innate/reading` — AI 解读
+- `POST /api/mind/innate/numsoul` — 数字能量
+- `POST /api/mind/innate/trajectory` — 成长轨迹
+- `POST /api/admin/innate-portrait-config/*` — 先天画像配置
+- `POST /api/admin/numsoul-detail-config/*` — 数字能量配置
+
+- [ ] **步骤 6:更新 PROJECT-OVERVIEW.md**
+
+在 `docs/superpowers/PROJECT-OVERVIEW.md` 新增本设计 + 实现计划的文档索引条目(版本、状态、路径)。
+
+- [ ] **步骤 7:最终 Commit**
+
+```bash
+git add -A
+git commit -m "docs: 先天画像体系实现完成 - 更新API文档与项目概览"
+```
+
+---
+
+## 自检记录
+
+**1. 规格覆盖度:**
+- ✔ 数据模型(3表)→ 任务 1-3
+- ✔ 后端服务层 → 任务 5-7
+- ✔ AI 解读 graph → 任务 8-9
+- ✔ 先天+后天打通(trajectory)→ 任务 5(getTrajectory)+ 任务 6(接口)+ 任务 14(页面)
+- ✔ 管理端可配置化 → 任务 10-11
+- ✔ 小程序展示 → 任务 12-14
+- ✔ 数据库迁移 267-269 → 任务 1
+- ✔ 测试策略 → 任务 4(NumSoulCalculator TDD)+ 任务 15(验证)
+
+**2. 占位符扫描:** 无"待定/TODO/后续实现";所有代码步骤含完整代码块。
+
+**3. 类型一致性:**
+- `InnatePortraitService.generateAiReading` 返回 `InnatePortraitReportVO`(portrait/aiReading/generatedAt/fromCache/fallbackUsed)— 前端 `innate-portrait.vue` 读取 `report.portrait`/`report.aiReading`/`report.fallbackUsed` ✔
+- `AiGateway.generateInnateReading(Object portrait)` 返回 `Map` 含 `reading` key — 服务层读取 `result.get("reading")` ✔
+- `NumSoulCalculator` 静态方法签名与 `InnatePortraitService.calculateNumSoul` 调用一致 ✔
+- `FamilyMemberAttributeService.calcInnateScore(memberId, memberType, dimensionCode)` 签名被复用 ✔
+- `InnateTrajectoryVO.growth` 前端读取 `innateVsCurrent.growth` ✔
+
+**已验证的实现细节(无需实现时再摸索):**
+- `AiGateway.generateInnateReading` 先加 stub(任务 5 步骤 1b),任务 8 完善为真实 HTTP 调用
+- `getTrajectory` 后天能量读取:注入 `EnergyBalanceMapper` + `EnergyDimensionMapper`,按 dimension code 匹配,已写死实现
+- `ParamUtils` 无 `getInt`——numsoul 接口用 `instanceof Number` 判断,已写死
+- 管理端删除请求:`data: { id }` body JSON + 后端 `@RequestParam Long id`(现有约定,已确认可工作)
+- `calcInnateScore` 缓存失效:继承 `ServiceImpl` 用 `this.list()` / `this.updateById()` 清除
+- `cfc-langgraph` 配置字段名 `llm_model` / `llm_api_key` / `llm_base_url` 已确认存在
+- `FamilyMemberAttributeService` 继承 `ServiceImpl<FamilyMemberAttributesMapper, FamilyMemberAttributes>`