# 先天画像体系实现计划 > **面向 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 { } ``` ```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 { } ``` ```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 { } ``` - [ ] **步骤 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 eightCharacters; /** 五行 {"wood":30,"fire":45,"earth":60,"metal":25,"water":40} */ private Map wuxingElements; /** 生肖: rat/ox/... */ private String zodiac; /** 血型: A/B/AB/O */ private String bloodType; /** 心先天基础分 */ private Integer mindBaseScore; /** 智先天基础分 */ private Integer wisdomBaseScore; /** 数字能量详情 */ private NumSoulDetailVO numSoul; /** 各来源贡献明细 */ private List> 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 innateDimensionScores; /** 后天能量 {"mindBalance":120,"mindTotalEarned":350} */ private Map currentEnergy; /** 先天vs后天 {"mindInnate":72,"mindCurrent":81,"growth":"+9"} */ private Map 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 wrapper = new LambdaQueryWrapper<>(); wrapper.eq(NumSoulDetailConfig::getNumberType, type) .eq(NumSoulDetailConfig::getNumberValue, value); return numSoulDetailConfigMapper.selectOne(wrapper); } /** * 来源贡献明细(用于管理端可配置权重展示) */ private List> listSourceContributions(Long memberId, String memberType, FamilyMemberAttributes attrs) { List> list = new ArrayList<>(); List configs = innatePortraitConfigMapper.selectList( new LambdaQueryWrapper().orderByAsc(InnatePortraitConfig::getSortOrder)); for (InnatePortraitConfig cfg : configs) { Map 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() .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 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 innateScores = new LinkedHashMap<>(); innateScores.put("mind", portrait.getMindBaseScore()); innateScores.put("wisdom", portrait.getWisdomBaseScore()); vo.setInnateDimensionScores(innateScores); // 后天能量:从 energy_balance 读取(按 dimension code 匹配) Map currentEnergy = new LinkedHashMap<>(); Map innateVsCurrent = new LinkedHashMap<>(); List 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() .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 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 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 parseJsonInt(String json) { Map raw = parseJson(json); Map result = new LinkedHashMap<>(); for (Map.Entry 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 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 portrait(@RequestBody Map 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 reading(@RequestBody Map 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 numsoul(@RequestBody Map 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 trajectory(@RequestBody Map 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 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 modifier = fromJson(attrs.getBehaviorModifier()); Object lifeNum = modifier.get("lifeNumber"); if (lifeNum instanceof Number) { LambdaQueryWrapper 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, * 可直接使用 this.list() / this.updateById() */ public void invalidateAllInnateScores() { List all = this.list(); for (FamilyMemberAttributes a : all) { a.setMindBaseScore(0); a.setWisdomBaseScore(0); this.updateById(a); } log.info("已清除 {} 条成员先天分缓存", all.size()); } ``` (在 `FamilyMemberAttributeService` 中新增该方法。因该类继承 `ServiceImpl`,直接用 `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 generateInnateReading(Object portrait) { if (!enabled || isCircuitOpen()) return null; try { ObjectNode body = objectMapper.createObjectNode(); body.set("portrait", objectMapper.valueToTree(portrait)); HttpEntity entity = new HttpEntity<>(body.toString(), createJsonHeaders()); String url = baseUrl + "/api/v1/innate/reading"; ResponseEntity response = restTemplate.postForEntity(url, entity, String.class); if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { JsonNode root = objectMapper.readTree(response.getBody()); Map 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() { return Result.success(innatePortraitConfigMapper.selectList( new LambdaQueryWrapper().orderByAsc(InnatePortraitConfig::getSortOrder))); } @PostMapping("/update") public Result 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 create(@RequestBody InnatePortraitConfig config) { innatePortraitConfigMapper.insert(config); return Result.success(config); } @PostMapping("/delete") public Result 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(@RequestBody(required = false) java.util.Map params) { LambdaQueryWrapper 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 create(@RequestBody NumSoulDetailConfig config) { numSoulDetailConfigMapper.insert(config); return Result.success(config); } @PostMapping("/update") public Result update(@RequestBody NumSoulDetailConfig config) { if (config.getId() == null) { return Result.error("缺少id"); } numSoulDetailConfigMapper.updateById(config); return Result.success(config); } @PostMapping("/delete") public Result 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 ``` - [ ] **步骤 2:创建 NumSoulDetailConfig.vue** ```vue ``` - [ ] **步骤 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 ``` - [ ] **步骤 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 ``` - [ ] **步骤 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`