面向 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
| 文件 | 职责 |
|---|---|
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 |
| 文件 | 职责 |
|---|---|
app/graphs/innate_portrait_graph.py |
先天画像 AI 解读 graph |
app/api/innate.py |
FastAPI 端点 /api/v1/innate/reading |
app/main.py |
注册 innate 路由 |
| 文件 | 职责 |
|---|---|
src/views/admin/InnatePortraitConfig.vue |
来源权重配置页 |
src/views/admin/NumSoulDetailConfig.vue |
数字能量配置页 |
| 文件 | 职责 |
|---|---|
pages/mind-detail/innate-portrait.vue |
先天画像报告页 |
pages/mind-detail/innate-trajectory.vue |
成长轨迹页 |
pages/mind-detail/index.vue |
心维度首页增强 |
pages.json |
注册新页面 |
utils/api.js |
新增接口 |
文件:
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 之后)添加:
// ==================== 迁移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());
}
在迁移 267 后紧接添加:
// ==================== 迁移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());
}
在迁移 268 后紧接添加:
// ==================== 迁移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());
}
在 cfc-backend/src/main/resources/schema.sql 末尾追加 3 张表的完整 CREATE TABLE 定义(与迁移 267-269 的 DDL 完全一致,含 COMMENT)。
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 6:Commit
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 创建先天画像三张新表"
文件:
cfc-backend/src/main/java/com/etotem/cfc/entity/InnatePortraitConfig.javacfc-backend/src/main/java/com/etotem/cfc/entity/InnatePortraitReport.java创建:cfc-backend/src/main/java/com/etotem/cfc/entity/NumSoulDetailConfig.java
[ ] 步骤 1:创建 InnatePortraitConfig.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
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
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
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个实体类"
文件:
cfc-backend/src/main/java/com/etotem/cfc/mapper/InnatePortraitConfigMapper.javacfc-backend/src/main/java/com/etotem/cfc/mapper/InnatePortraitReportMapper.javacfc-backend/src/main/java/com/etotem/cfc/mapper/NumSoulDetailConfigMapper.javacfc-backend/src/main/java/com/etotem/cfc/dto/InnatePortraitVO.javacfc-backend/src/main/java/com/etotem/cfc/dto/NumSoulDetailVO.javacfc-backend/src/main/java/com/etotem/cfc/dto/InnatePortraitReportVO.java创建:cfc-backend/src/main/java/com/etotem/cfc/dto/InnateTrajectoryVO.java
[ ] 步骤 1:创建 3 个 Mapper
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> {
}
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> {
}
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
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
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
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
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
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"
文件:
cfc-backend/src/main/java/com/etotem/cfc/service/NumSoulCalculator.java测试:cfc-backend/src/test/java/com/etotem/cfc/service/NumSoulCalculatorTest.java
[ ] 步骤 1:编写失败的测试
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
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
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 + 单元测试"
文件:
创建:cfc-backend/src/main/java/com/etotem/cfc/service/InnatePortraitService.java
[ ] 步骤 1:实现 InnatePortraitService(聚合画像)
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走降级模板路径。
在 AiGateway.java 类中临时添加:
/**
* 先天画像 AI 解读(stub,任务8完善为真实HTTP调用)
*/
public Map<String, Object> generateInnateReading(Object portrait) {
return null;
}
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 3:Commit
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 先天画像聚合服务"
文件:
创建:cfc-backend/src/main/java/com/etotem/cfc/controller/mind/InnatePortraitController.java
[ ] 步骤 1:实现 Controller
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 用类型判断读取:
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 替换为上述写法。
运行:
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,无重复
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 5:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/controller/mind/InnatePortraitController.java
git commit -m "feat: 先天画像4个接口(portrait/reading/numsoul/trajectory)"
文件:
修改:cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberAttributeService.java:214-306
[ ] 步骤 1:注入 InnatePortraitConfigMapper
在 FamilyMemberAttributeService 类中新增注入(现有字段附近):
@Resource
private com.etotem.cfc.mapper.InnatePortraitConfigMapper innatePortraitConfigMapper;
@Resource
private com.etotem.cfc.mapper.NumSoulDetailConfigMapper numSoulDetailConfigMapper;
将 calcInnateScore 中硬编码加权(原 293-296 行):
// --- 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;
替换为:
// --- 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;
}
问题:calcInnateScore 开头有缓存逻辑 if (attrs.getMindBaseScore() != null && attrs.getMindBaseScore() > 0) return attrs.getMindBaseScore();——权重修改后已缓存的值不会重算。
改造:在权重配置变化时清除缓存。在 InnatePortraitConfigController.update/delete 成功后调用新增方法:
/**
* 权重配置变化时清除所有已缓存的先天分,触发重算
* 说明:本服务继承 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。)
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS(this.list() / this.updateById() 来自 ServiceImpl 基类,已确认可用)
[ ] 步骤 5:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberAttributeService.java
git commit -m "feat: calcInnateScore 支持可配置权重+数字能量参与+缓存失效"
文件:
修改:cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java
[ ] 步骤 1:实现 generateInnateReading
在 AiGateway 类中(generateQuestionnaire 之后)新增:
/**
* 调用 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;
}
}
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 3:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java
git commit -m "feat: AiGateway 新增 generateInnateReading"
文件:
cfc-langgraph/app/graphs/innate_portrait_graph.pycfc-langgraph/app/api/innate.py修改:cfc-langgraph/app/main.py
[ ] 步骤 1:创建 innate_portrait_graph.py
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
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 部分新增:
from app.api import innate
并在 app.include_router(...) 区段新增:
app.include_router(innate.router)
运行: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 引用正确,无需替换。
运行:cd cfc-langgraph && python -m py_compile app/graphs/innate_portrait_graph.py app/api/innate.py
预期:无输出(编译通过)
[ ] 步骤 6:Commit
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端点"
文件:
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
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
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
运行:
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
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: 管理端先天画像/数字能量配置接口"
文件:
cfc-web/src/views/admin/InnatePortraitConfig.vue创建:cfc-web/src/views/admin/NumSoulDetailConfig.vue
[ ] 步骤 1:创建 InnatePortraitConfig.vue
参照现有 ZodiacConfigs.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
<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() 已按此约定编写,无需调整。
在 cfc-web/src/router/ 中新增 2 条路由(参照现有 ZodiacConfigs 路由),菜单挂到"五维能量/先天画像"分组。
[ ] 步骤 5:Commit
git add cfc-web/src/views/admin/InnatePortraitConfig.vue cfc-web/src/views/admin/NumSoulDetailConfig.vue cfc-web/src/router/
git commit -m "feat: 管理端先天画像配置页 + 数字能量配置页"
文件:
cfc-frontend/utils/api.js修改:cfc-frontend/pages.json
[ ] 步骤 1:api.js 新增接口
在 cfc-frontend/utils/api.js 中,参照现有 traditionalMirror 等方法新增:
// 先天画像
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) }
在 pages/mind-detail 的 pages 数组末尾追加:
{
"path": "innate-portrait",
"style": { "navigationBarTitleText": "先天画像" }
},
{
"path": "innate-trajectory",
"style": { "navigationBarTitleText": "先天成长轨迹" }
}
[ ] 步骤 3:Commit
git add cfc-frontend/utils/api.js cfc-frontend/pages.json
git commit -m "feat: 小程序先天画像API封装+页面注册"
文件:
创建:cfc-frontend/pages/mind-detail/innate-portrait.vue
[ ] 步骤 1:实现页面
参照 traditional-mirror.vue 的现有样式,遵守小程序限制(无 ?.、无 Grid、:key 用方法、parseDate()):
<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>
在 pages/mind-detail/index.vue 的"先天画像"入口卡片跳转时传 memberId/memberType/familyId(任务 14 实现),此处确保从 options 读取。
[ ] 步骤 3:Commit
git add cfc-frontend/pages/mind-detail/innate-portrait.vue
git commit -m "feat: 小程序先天画像报告页"
文件:
cfc-frontend/pages/mind-detail/innate-trajectory.vue修改:cfc-frontend/pages/mind-detail/index.vue
[ ] 步骤 1:创建 innate-trajectory.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 数组(现有"家庭天盘"附近)新增:
{ icon: '\u{1F31F}', label: '先天画像', needLogin: true, page: '/pages/mind-detail/innate-portrait' },
跳转时需传当前成员信息:找到 funcList 点击处理方法,跳转前追加 memberId/memberType/familyId 参数(参照现有"家庭天盘"的跳转方式从 currentMember 或 familyId data 读取)。
检查 pages/mind-detail/index.vue 中现有哪些 currentMember/familyId data 字段可用,确保跳转 innate-portrait 时能取到 memberId。
[ ] 步骤 4:Commit
git add cfc-frontend/pages/mind-detail/innate-trajectory.vue cfc-frontend/pages/mind-detail/index.vue
git commit -m "feat: 小程序先天成长轨迹页 + 心维度首页入口"
运行:cd cfc-backend && mvn clean compile && mvn test -Dtest=NumSoulCalculatorTest
预期:BUILD SUCCESS,测试全绿
运行:
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/* 无重复
新类名:InnatePortraitService、InnatePortraitController、InnatePortraitConfig(entity)、InnatePortraitConfigController、InnatePortraitReportMapper、NumSoulDetailConfigController——运行 grep -rn "class InnatePortrait" cfc-backend/src/main/java/ 确认无重名类。
运行:cd cfc-langgraph && python -m py_compile app/graphs/innate_portrait_graph.py app/api/innate.py app/main.py
预期:无输出
在 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
git add -A
git commit -m "docs: 先天画像体系实现完成 - 更新API文档与项目概览"
1. 规格覆盖度:
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>