Pārlūkot izejas kodu

feat(dimension): 统一维度体系差距补齐 P0-T1/P0-T2/注册中心/P1-T4(2026-11-03 设计文档)

按 docs/superpowers/specs/2026-11-03-unified-dimension-system-design.md 补齐实现差距:

P0-T1 Schema 迁移+实体字段:DatabaseInitializer 迁移 445/446/447(ensureColumn 幂等 + seed 5根7维);schema.sql 同步新列;ProductDimensionConfig 加 parent_id/legacy_alias/source_system/wuxing_rule_ref/node_kind;IndicatorDefinition 加 parent_id/legacy_alias

P0-T2 常量+映射层(NEW):DimensionCodeConstants(5根+7body code + legacy 别名);DimensionCodeMapper resolveToUnified/resolveLegacy 双向映射

注册中心核心(NEW):DimensionRegistryService getTree/getRoots/getNodeByCode;DimensionRegistryController /api/dimension-registry/* 只读;DimensionSyncService 双写骨架(flag 门控);DimensionNode DTO;DimensionConfigController /save 改走注册中心;application.yml dimension.registry.enabled/dualWrite=false

P1-T4 前端消费注册中心:DimensionSelector/index.vue 消费统一注册树(回落 store 旧源);api/dimension.js 新增 registry 接口

验证:cfc-backend mvn compile(最新 origin 基线复验)通过;cfc-web npm run build 本会话已通过
Agnes Agent 1 nedēļu atpakaļ
vecāks
revīzija
57e162242

+ 35 - 0
cfc-backend/src/main/java/com/etotem/cfc/common/constant/DimensionCodeConstants.java

@@ -0,0 +1,35 @@
+package com.etotem.cfc.common.constant;
+
+public class DimensionCodeConstants {
+    // 5 根维度
+    public static final String BODY = "body";
+    public static final String MIND = "mind";
+    public static final String WISDOM = "wisdom";
+    public static final String ACTION = "action";
+    public static final String WEALTH = "wealth";
+    
+    // 7 body 维度子节点(全小写点分,入库为 body.growth 等)
+    public static final String BODY_GROWTH = "body.growth";
+    public static final String BODY_SLEEP = "body.sleep";
+    public static final String BODY_VISION = "body.vision";
+    public static final String BODY_IMMUNITY = "body.immunity";
+    public static final String BODY_NUTRITION = "body.nutrition";
+    public static final String BODY_GUT = "body.gut";
+    public static final String BODY_EXERCISE = "body.exercise";
+    
+    // 7 维度子节点的 legacy_alias 映射(JSON数组字符串,如 ["growth"])
+    public static final String LEGACY_GROWTH = "growth";
+    public static final String LEGACY_SLEEP = "sleep";
+    public static final String LEGACY_VISION = "vision";
+    public static final String LEGACY_IMMUNITY = "immunity";
+    public static final String LEGACY_NUTRITION = "nutrition";
+    public static final String LEGACY_GUT = "gut";
+    public static final String LEGACY_EXERCISE = "exercise";
+    
+    // 五行规则映射(对应 EnergyDimension.element:木/火/金/水/土)
+    public static final String WUXING_WOOD = "木";
+    public static final String WUXING_FIRE = "火";
+    public static final String WUXING_METAL = "金";
+    public static final String WUXING_WATER = "水";
+    public static final String WUXING_EARTH = "土";
+}

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

@@ -3434,6 +3434,15 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
 
         // 迁移444: T38 团队邀请功能 —— supply_system_member 增加个人专属邀请码(幂等,永久有效+唯一)
         migrateSupplyMemberInviteCode();
+
+        // 迁移445: 统一维度体系(P0-T1)—— product_dimension_config 升级为唯一维度注册中心
+        // 新增 legacy_alias/source_system/wuxing_rule_ref/node_kind 4 列(设计文档 §3.1)
+        migrateUnifiedDimensionRegistryColumns();
+        // 迁移446: 统一维度体系(P0-T1)—— indicator_definitions 指标并入维度树混排
+        // 新增 parent_id/legacy_alias 2 列(设计文档 §3.2,红线 1:id 主键与外键目标不变)
+        migrateUnifiedIndicatorTreeColumns();
+        // 迁移447: 统一维度体系(P0-T3)—— 固化 5 根 + 7 body 子维度节点(幂等 seed)
+        seedUnifiedDimensionRegistry();
     }
 
     /**
@@ -15088,6 +15097,115 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
 
     }
 
+    // ==================== 迁移445-447:统一维度体系(P0) ====================
+
+    /**
+     * 迁移445: product_dimension_config 升级为唯一维度注册中心(设计文档 §3.1)。
+     * 新增 legacy_alias / source_system / wuxing_rule_ref / node_kind 4 列,均幂等。
+     * 现有 dimension_code VARCHAR(50) NOT NULL UNIQUE 复用为统一 code(body / body.growth 等)。
+     */
+    void migrateUnifiedDimensionRegistryColumns() {
+        ensureColumn("product_dimension_config", "legacy_alias",
+                "VARCHAR(512) DEFAULT NULL COMMENT '历史别名(JSON数组): 承载现存4套编码, 如 [\"growth\"]'");
+        ensureColumn("product_dimension_config", "source_system",
+                "VARCHAR(32) DEFAULT NULL COMMENT '归属子系统: health/energy/indicator/knowledge'");
+        ensureColumn("product_dimension_config", "wuxing_rule_ref",
+                "VARCHAR(30) DEFAULT NULL COMMENT '五行规则引用(木/火/金/水/土), 关联EnergyDimension.element'");
+        ensureColumn("product_dimension_config", "node_kind",
+                "VARCHAR(16) DEFAULT 'root' COMMENT '节点种类: root(五维根)/sub(子维度)'");
+        log.info("迁移445: product_dimension_config 注册中心 4 列就绪");
+    }
+
+    /**
+     * 迁移446: indicator_definitions 指标并入维度树混排(设计文档 §3.2,红线 1)。
+     * 新增 parent_id / legacy_alias 2 列;不改 id 主键、不改任何外键目标。
+     */
+    void migrateUnifiedIndicatorTreeColumns() {
+        ensureColumn("indicator_definitions", "parent_id",
+                "BIGINT DEFAULT NULL COMMENT '父节点ID(可指向product_dimension_config.id或本表id, 混排进维度树)'");
+        ensureColumn("indicator_definitions", "legacy_alias",
+                "VARCHAR(512) DEFAULT NULL COMMENT '历史别名(JSON数组)'");
+        log.info("迁移446: indicator_definitions 混排 2 列就绪");
+    }
+
+    /**
+     * 迁移447: 固化 5 根 + 7 body 子维度节点(设计文档 §2.3 / P0-T3,幂等 seed)。
+     * 5 根补 source_system='energy';7 维(body.growth..body.exercise)作为维度节点挂 body 下,
+     * legacy_alias 绑定旧名 growth/sleep/...,source_system='health'。
+     */
+    void seedUnifiedDimensionRegistry() {
+        // 5 根:dimension_type='five',node_kind='root'
+        upsertDimensionSeed("body", "身体", "five", null, 1, "energy", "root",
+                "[\"body\"]", null);
+        upsertDimensionSeed("mind", "心理", "five", null, 2, "energy", "root",
+                "[\"mind\"]", null);
+        upsertDimensionSeed("wisdom", "智能", "five", null, 3, "energy", "root",
+                "[\"wisdom\"]", null);
+        upsertDimensionSeed("action", "行动", "five", null, 4, "energy", "root",
+                "[\"action\"]", null);
+        upsertDimensionSeed("wealth", "财富", "five", null, 5, "energy", "root",
+                "[\"wealth\"]", null);
+
+        Long bodyId = queryDimensionIdByCode("body");
+        if (bodyId == null) {
+            log.warn("迁移447: 未找到 body 根节点 id,跳过 7 维子节点 seed");
+            return;
+        }
+        // 7 body 子维度:code=body.{sub},parent=body.id,legacy_alias=旧名
+        upsertDimensionSeed("body.growth", "生长发育", "health", bodyId, 1, "health", "sub",
+                "[\"growth\"]", null);
+        upsertDimensionSeed("body.sleep", "睡眠质量", "health", bodyId, 2, "health", "sub",
+                "[\"sleep\"]", null);
+        upsertDimensionSeed("body.vision", "视力健康", "health", bodyId, 3, "health", "sub",
+                "[\"vision\"]", null);
+        upsertDimensionSeed("body.immunity", "免疫力", "health", bodyId, 4, "health", "sub",
+                "[\"immunity\"]", null);
+        upsertDimensionSeed("body.nutrition", "营养均衡", "health", bodyId, 5, "health", "sub",
+                "[\"nutrition\"]", null);
+        upsertDimensionSeed("body.gut", "肠胃健康", "health", bodyId, 6, "health", "sub",
+                "[\"gut\"]", null);
+        upsertDimensionSeed("body.exercise", "运动活力", "health", bodyId, 7, "health", "sub",
+                "[\"exercise\"]", null);
+        log.info("迁移447: 统一维度注册中心 5 根 + 7 body 子维度 seed 完成");
+    }
+
+    /** 幂等 upsert 维度种子:按 dimension_code 存在则补 source_system/legacy_alias,不存在则插入。 */
+    private void upsertDimensionSeed(String code, String name, String dimensionType, Long parentId,
+                                     int sort, String sourceSystem, String nodeKind,
+                                     String legacyAlias, String wuxingRuleRef) {
+        try {
+            Long existingId = queryDimensionIdByCode(code);
+            if (existingId != null) {
+                jdbcTemplate.update(
+                        "UPDATE product_dimension_config SET source_system=?, node_kind=?, "
+                                + "legacy_alias=COALESCE(legacy_alias, ?), wuxing_rule_ref=COALESCE(wuxing_rule_ref, ?), "
+                                + "updated_at=NOW() WHERE id=?",
+                        sourceSystem, nodeKind, legacyAlias, wuxingRuleRef, existingId);
+                return;
+            }
+            jdbcTemplate.update(
+                    "INSERT INTO product_dimension_config "
+                            + "(dimension_code, dimension_name, dimension_type, parent_id, sort, enabled, "
+                            + "source_system, node_kind, legacy_alias, wuxing_rule_ref, created_at, updated_at) "
+                            + "VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?, NOW(), NOW())",
+                    code, name, dimensionType, parentId, sort, sourceSystem, nodeKind, legacyAlias, wuxingRuleRef);
+            log.info("迁移447: 插入维度种子 {} ({})", code, name);
+        } catch (Exception e) {
+            log.warn("迁移447: 维度种子 {} upsert 失败(忽略): {}", code, e.getMessage());
+        }
+    }
+
+    private Long queryDimensionIdByCode(String code) {
+        try {
+            List<Long> ids = jdbcTemplate.queryForList(
+                    "SELECT id FROM product_dimension_config WHERE dimension_code = ? LIMIT 1",
+                    Long.class, code);
+            return ids.isEmpty() ? null : ids.get(0);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
     private static String text(JsonNode row, String field) {
         if (row == null) return null;
         JsonNode n = row.get(field);

+ 7 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/DimensionConfigController.java

@@ -20,6 +20,9 @@ public class DimensionConfigController {
     @Resource
     private ProductDimensionConfigService productDimensionConfigService;
 
+    @Resource
+    private com.etotem.cfc.service.DimensionRegistryService dimensionRegistryService;
+
     @PostMapping("/list")
     public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params) {
         String dimensionType = (String) params.get("dimensionType");
@@ -40,7 +43,9 @@ public class DimensionConfigController {
 
     @PostMapping("/save")
     public Result<ProductDimensionConfig> save(@RequestBody ProductDimensionConfig config) {
-        return Result.success(productDimensionConfigService.saveConfig(config));
+        // 统一维度体系 P0-T4(设计文档 §5.2):admin 写入升级改调注册中心,
+        // 内部落 product_dimension_config(权威)+ dualWrite flag=true 时触发双写旧表。
+        return Result.success(dimensionRegistryService.saveConfig(config));
     }
 
     @PostMapping("/delete")
@@ -51,6 +56,7 @@ public class DimensionConfigController {
 
     @PostMapping("/tree")
     public Result<List<ProductDimensionConfig>> tree() {
+        // 7 body 子节点经 seed(迁移447)入库后,既有 tree() 按 parent_id 自然展开
         return Result.success(productDimensionConfigService.tree());
     }
 }

+ 50 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/DimensionRegistryController.java

@@ -0,0 +1,50 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.DimensionNode;
+import com.etotem.cfc.service.DimensionRegistryService;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+/**
+ * 统一维度树只读查询接口(设计文档 §5.1)——统一取数入口。
+ *
+ * <p>供小程序/管理端 DimensionProvider 读取;合并维度节点 + 指标节点的统一树。</p>
+ */
+@RestController
+@RequestMapping("/api/dimension-registry")
+public class DimensionRegistryController {
+
+    @Resource
+    private DimensionRegistryService dimensionRegistryService;
+
+    @GetMapping("/tree")
+    public Result<List<DimensionNode>> tree(@RequestParam(required = false) String sourceSystem) {
+        return Result.success(dimensionRegistryService.getTree(sourceSystem));
+    }
+
+    @GetMapping("/roots")
+    public Result<List<DimensionNode>> roots() {
+        return Result.success(dimensionRegistryService.getRoots());
+    }
+
+    @GetMapping("/code/{code}")
+    public Result<DimensionNode> getByCode(@PathVariable String code) {
+        DimensionNode node = dimensionRegistryService.getNodeByCode(code);
+        if (node == null) {
+            return Result.error(404, "维度不存在");
+        }
+        return Result.success(node);
+    }
+
+    @GetMapping("/map")
+    public Result<String> map(@RequestParam String legacy) {
+        return Result.success(dimensionRegistryService.resolveLegacyToUnified(legacy));
+    }
+}

+ 44 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/DimensionNode.java

@@ -0,0 +1,44 @@
+package com.etotem.cfc.dto;
+
+import java.util.List;
+
+/**
+ * 统一维度树节点(维度节点 ∪ 指标节点混排)。
+ *
+ * <p>结构见设计文档 §5.1:{id, code, name, parentId, nodeKind, sourceSystem,
+ * legacyAlias, wuxingRuleRef, children?}。</p>
+ */
+public class DimensionNode {
+
+    private Long id;
+    private String code;
+    private String name;
+    private Long parentId;
+    private String nodeKind;
+    private String sourceSystem;
+    private String legacyAlias;
+    private String wuxingRuleRef;
+    private List<DimensionNode> children;
+
+    public DimensionNode() {
+    }
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+    public String getCode() { return code; }
+    public void setCode(String code) { this.code = code; }
+    public String getName() { return name; }
+    public void setName(String name) { this.name = name; }
+    public Long getParentId() { return parentId; }
+    public void setParentId(Long parentId) { this.parentId = parentId; }
+    public String getNodeKind() { return nodeKind; }
+    public void setNodeKind(String nodeKind) { this.nodeKind = nodeKind; }
+    public String getSourceSystem() { return sourceSystem; }
+    public void setSourceSystem(String sourceSystem) { this.sourceSystem = sourceSystem; }
+    public String getLegacyAlias() { return legacyAlias; }
+    public void setLegacyAlias(String legacyAlias) { this.legacyAlias = legacyAlias; }
+    public String getWuxingRuleRef() { return wuxingRuleRef; }
+    public void setWuxingRuleRef(String wuxingRuleRef) { this.wuxingRuleRef = wuxingRuleRef; }
+    public List<DimensionNode> getChildren() { return children; }
+    public void setChildren(List<DimensionNode> children) { this.children = children; }
+}

+ 10 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/IndicatorDefinition.java

@@ -27,6 +27,12 @@ public class IndicatorDefinition {
     private Date createdAt;
     private Date updatedAt;
 
+    /** 父节点ID(可指向product_dimension_config.id或本表id, 混排进维度树) */
+    private Long parentId;
+
+    /** 历史别名(JSON数组字符串) */
+    private String legacyAlias;
+
     public Long getId() { return id; }
     public void setId(Long id) { this.id = id; }
     public String getDomain() { return domain; }
@@ -59,6 +65,10 @@ public class IndicatorDefinition {
     public void setGoodDirection(String goodDirection) { this.goodDirection = goodDirection; }
     public String getDimensionCode() { return dimensionCode; }
     public void setDimensionCode(String dimensionCode) { this.dimensionCode = dimensionCode; }
+    public Long getParentId() { return parentId; }
+    public void setParentId(Long parentId) { this.parentId = parentId; }
+    public String getLegacyAlias() { return legacyAlias; }
+    public void setLegacyAlias(String legacyAlias) { this.legacyAlias = legacyAlias; }
     public Date getCreatedAt() { return createdAt; }
     public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; }
     public Date getUpdatedAt() { return updatedAt; }

+ 12 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ProductDimensionConfig.java

@@ -31,6 +31,18 @@ public class ProductDimensionConfig implements Serializable {
 
     private String remark;
 
+    /** 历史别名(JSON数组字符串): 承载现存4套编码, 如 ["growth"] */
+    private String legacyAlias;
+
+    /** 归属子系统: health/energy/indicator/knowledge */
+    private String sourceSystem;
+
+    /** 五行规则引用(木/火/金/水/土), 关联EnergyDimension.element */
+    private String wuxingRuleRef;
+
+    /** 节点种类: root(五维根)/sub(子维度) */
+    private String nodeKind;
+
     private Date createdAt;
 
     private Date updatedAt;

+ 110 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DimensionCodeMapper.java

@@ -0,0 +1,110 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.ProductDimensionConfig;
+import com.etotem.cfc.mapper.ProductDimensionConfigMapper;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 映射层:legacy_alias ↔ 统一 code 双向翻译(设计文档 §4)。
+ *
+ * <p>注册中心加载后构建内存索引:
+ * <ul>
+ *   <li>Map&lt;legacyCode, unifiedCode&gt; — 旧编码 → 统一 code(resolveToUnified)</li>
+ *   <li>Map&lt;unifiedCode, List&lt;legacyCode&gt;&gt; — 统一 code → 旧编码列表(resolveLegacy)</li>
+ * </ul>
+ * 旧能量体系(energy_dimension.code)与统一 5 根同码,无需 alias 翻译,双写同步即 1:1。</p>
+ */
+@Service
+public class DimensionCodeMapper {
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    @Resource
+    private ProductDimensionConfigMapper configMapper;
+
+    private volatile Map<String, String> legacyToUnified = new HashMap<>();
+    private volatile Map<String, List<String>> unifiedToLegacy = new HashMap<>();
+    private volatile boolean loaded = false;
+
+    @PostConstruct
+    public synchronized void reload() {
+        Map<String, String> legacyMap = new HashMap<>();
+        Map<String, List<String>> unifiedMap = new HashMap<>();
+        for (ProductDimensionConfig cfg : configMapper.selectList(null)) {
+            String unified = cfg.getDimensionCode();
+            if (unified == null || unified.isEmpty()) {
+                continue;
+            }
+            // 统一 code 自身亦作为 alias 解析(自映射)
+            legacyMap.putIfAbsent(unified, unified);
+            List<String> aliases = parseLegacyAlias(cfg.getLegacyAlias());
+            if (aliases != null) {
+                for (String alias : aliases) {
+                    if (alias != null && !alias.isEmpty()) {
+                        legacyMap.putIfAbsent(alias, unified);
+                    }
+                }
+                unifiedMap.put(unified, aliases);
+            }
+        }
+        this.legacyToUnified = legacyMap;
+        this.unifiedToLegacy = unifiedMap;
+        this.loaded = true;
+    }
+
+    /** 旧编码 → 统一 code;已传入统一 code 则原样返回;未知则返回原值(容错)。 */
+    public String resolveToUnified(String legacy) {
+        if (legacy == null) {
+            return null;
+        }
+        if (!loaded) {
+            reload();
+        }
+        String unified = legacyToUnified.get(legacy.trim());
+        return unified != null ? unified : legacy.trim();
+    }
+
+    /** 统一 code → 旧编码列表;无别名返回空列表。 */
+    public List<String> resolveLegacy(String unified) {
+        if (unified == null) {
+            return Collections.emptyList();
+        }
+        if (!loaded) {
+            reload();
+        }
+        List<String> aliases = unifiedToLegacy.get(unified);
+        return aliases != null ? aliases : Collections.emptyList();
+    }
+
+    /** 校验传入 code 是否为注册中心已知统一 code(含 legacy_alias 匹配)。 */
+    public boolean isKnownCode(String code) {
+        if (code == null || code.isEmpty()) {
+            return false;
+        }
+        if (!loaded) {
+            reload();
+        }
+        return legacyToUnified.containsKey(code.trim());
+    }
+
+    private List<String> parseLegacyAlias(String json) {
+        if (json == null || json.isEmpty()) {
+            return Collections.emptyList();
+        }
+        try {
+            return MAPPER.readValue(json, new TypeReference<List<String>>() {});
+        } catch (Exception e) {
+            return new ArrayList<>();
+        }
+    }
+}

+ 183 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DimensionRegistryService.java

@@ -0,0 +1,183 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.common.constant.DimensionCodeConstants;
+import com.etotem.cfc.dto.DimensionNode;
+import com.etotem.cfc.entity.IndicatorDefinition;
+import com.etotem.cfc.entity.ProductDimensionConfig;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 统一维度体系注册中心服务(设计文档 §6)。
+ *
+ * <p>统一树 = product_dimension_config 维度节点 ∪ indicator_definitions 指标节点(混排)。
+ * 提供取数入口:getTree / getRoots / getNodeByCode / saveConfig。</p>
+ */
+@Service
+public class DimensionRegistryService {
+
+    @Resource
+    private ProductDimensionConfigService productDimensionConfigService;
+
+    @Resource
+    private IndicatorService indicatorService;
+
+    @Resource
+    private DimensionCodeMapper dimensionCodeMapper;
+
+    @Resource
+    private DimensionSyncService dimensionSyncService;
+
+    /** feature flag:注册中心读切换(默认关,分阶段开启) */
+    @Value("${dimension.registry.enabled:false}")
+    private boolean registryEnabled;
+
+    /** feature flag:双写过渡期开关(默认关) */
+    @Value("${dimension.registry.dualWrite:false}")
+    private boolean dualWrite;
+
+    /** 统一树(维度节点 + 指标节点混排)。sourceSystem 为空返回全部。 */
+    public List<DimensionNode> getTree(String sourceSystem) {
+        List<ProductDimensionConfig> dims = productDimensionConfigService.list();
+        List<IndicatorDefinition> indicators = indicatorService.listAll();
+
+        // 统一转成 DimensionNode(维度节点标识 root/sub;指标节点标识 indicator)
+        Map<Long, DimensionNode> idToNode = new HashMap<>();
+        List<DimensionNode> roots = new ArrayList<>();
+
+        for (ProductDimensionConfig d : dims) {
+            if (!StringUtils.isEmpty(sourceSystem) && !sourceSystem.equals(d.getSourceSystem())) {
+                continue;
+            }
+            DimensionNode n = toNode(d);
+            idToNode.put(n.getId(), n);
+        }
+
+        // 指标节点挂到维度节点(indicator_definitions.parent_id → product_dimension_config.id 或指标自引用)
+        for (IndicatorDefinition ind : indicators) {
+            if (ind.getParentId() == null) {
+                continue;
+            }
+            DimensionNode n = new DimensionNode();
+            n.setId(ind.getId());
+            n.setCode(ind.getCode());
+            n.setName(ind.getName());
+            n.setParentId(ind.getParentId());
+            n.setNodeKind("indicator");
+            n.setSourceSystem("indicator");
+            n.setLegacyAlias(ind.getLegacyAlias());
+            idToNode.putIfAbsent(n.getId(), n);
+        }
+
+        // 按 parentId 组装
+        for (DimensionNode n : idToNode.values()) {
+            Long parentId = n.getParentId();
+            if (parentId != null && idToNode.containsKey(parentId)) {
+                DimensionNode parent = idToNode.get(parentId);
+                if (parent.getChildren() == null) {
+                    parent.setChildren(new ArrayList<>());
+                }
+                parent.getChildren().add(n);
+            } else if (parentId == null) {
+                roots.add(n);
+            }
+        }
+        return roots;
+    }
+
+    /** 5 根。 */
+    public List<DimensionNode> getRoots() {
+        return getTree(null).stream()
+                .filter(n -> DimensionCodeConstants.BODY.equals(n.getCode())
+                        || DimensionCodeConstants.MIND.equals(n.getCode())
+                        || DimensionCodeConstants.WISDOM.equals(n.getCode())
+                        || DimensionCodeConstants.ACTION.equals(n.getCode())
+                        || DimensionCodeConstants.WEALTH.equals(n.getCode()))
+                .collect(java.util.stream.Collectors.toList());
+    }
+
+    /** 按统一 code 取节点(含 legacy_alias)。 */
+    public DimensionNode getNodeByCode(String code) {
+        if (code == null) {
+            return null;
+        }
+        String unified = dimensionCodeMapper.resolveToUnified(code);
+        for (DimensionNode n : flatten(getTree(null))) {
+            if (unified.equals(n.getCode())) {
+                return n;
+            }
+        }
+        return null;
+    }
+
+    /** legacy → 统一 code 解析(映射层)。 */
+    public String resolveLegacyToUnified(String legacy) {
+        return dimensionCodeMapper.resolveToUnified(legacy);
+    }
+
+    /** 注册中心写入入口:admin 配置写产品维度配置表 + 校验 code 唯一。 */
+    public ProductDimensionConfig saveConfig(ProductDimensionConfig node) {
+        if (node == null) {
+            return null;
+        }
+        ProductDimensionConfig saved = productDimensionConfigService.saveConfig(node);
+        // 双写过渡期:flag=true 时同步旧能量体系表
+        if (dualWrite) {
+            try {
+                dimensionSyncService.syncToLegacy(saved);
+            } catch (Exception e) {
+                // 双写失败不阻断主链路,由双写校验任务兜底
+            }
+        }
+        reloadMapper();
+        return saved;
+    }
+
+    /** 注册中心读取开关是否开启。 */
+    public boolean isRegistryEnabled() {
+        return registryEnabled;
+    }
+
+    /** 双写开关。 */
+    public boolean isDualWrite() {
+        return dualWrite;
+    }
+
+    private void reloadMapper() {
+        try {
+            dimensionCodeMapper.reload();
+        } catch (Exception ignored) {
+        }
+    }
+
+    private DimensionNode toNode(ProductDimensionConfig d) {
+        DimensionNode n = new DimensionNode();
+        n.setId(d.getId());
+        n.setCode(d.getDimensionCode());
+        n.setName(d.getDimensionName());
+        n.setParentId(d.getParentId());
+        n.setNodeKind(d.getNodeKind() != null ? d.getNodeKind() : "sub");
+        n.setSourceSystem(d.getSourceSystem());
+        n.setLegacyAlias(d.getLegacyAlias());
+        n.setWuxingRuleRef(d.getWuxingRuleRef());
+        return n;
+    }
+
+    private List<DimensionNode> flatten(List<DimensionNode> roots) {
+        List<DimensionNode> all = new ArrayList<>();
+        for (DimensionNode r : roots) {
+            all.add(r);
+            if (r.getChildren() != null) {
+                all.addAll(flatten(r.getChildren()));
+            }
+        }
+        return all;
+    }
+}

+ 48 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DimensionSyncService.java

@@ -0,0 +1,48 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.ProductDimensionConfig;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * 双写过渡期服务(设计文档 §5.2 / §8.2)。
+ *
+ * <p>注册中心上线后先双写:新写入同时落注册中心 + 旧表(energy_dimension /
+ * energy_dimension_config / dimension_weights),由 feature flag
+ * {@code dimension.registry.dualWrite} 控制。旧 EnergyService 仍读旧表,行为不变。</p>
+ *
+ * <p>当前为骨架:flag 门控 + 1:1 code 对齐映射(旧能量体系与统一 5 根同码)。
+ * 具体 upsert 到旧表由 P2-T3 前按调用日志确认零调用后退役,此处仅在 flag=true 时执行。</p>
+ */
+@Slf4j
+@Service
+public class DimensionSyncService {
+
+    /**
+     * 双写:注册中心节点 → 旧能量体系表(code 1:1 对齐,五行规则逻辑零改动)。
+     *
+     * @param node 注册中心节点(product_dimension_config)
+     */
+    public void syncToLegacy(ProductDimensionConfig node) {
+        if (node == null) {
+            return;
+        }
+        String code = node.getDimensionCode();
+        if (code == null || code.isEmpty()) {
+            return;
+        }
+        // 仅同步 5 根统一 code(body/mind/wisdom/action/wealth)到旧能量体系;
+        // 子维度(body.growth 等)暂不写旧表,避免与旧 7 维编码体系冲突。
+        switch (code) {
+            case "body":
+            case "mind":
+            case "wisdom":
+            case "action":
+            case "wealth":
+                log.debug("双写同步维度 {} 至旧能量体系表(骨架,待 P2-T3 接入实际 upsert)", code);
+                break;
+            default:
+                log.debug("双写跳过非五维码 {}(子维度由 legacy_alias 逻辑映射)", code);
+        }
+    }
+}

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/IndicatorService.java

@@ -19,6 +19,10 @@ public class IndicatorService {
     @Resource
     private IndicatorValueMapper indicatorValueMapper;
 
+    public List<IndicatorDefinition> listAll() {
+        return indicatorDefinitionMapper.selectList(null);
+    }
+
     public List<IndicatorDefinition> listDefinitions(String domain, String category, Integer status) {
         LambdaQueryWrapper<IndicatorDefinition> qw = new LambdaQueryWrapper<>();
         if (domain != null) qw.eq(IndicatorDefinition::getDomain, domain);

+ 7 - 0
cfc-backend/src/main/resources/application.yml

@@ -134,6 +134,13 @@ python:
     failure-threshold: 3
     reset-timeout-ms: 30000
 
+# 统一维度体系(2026-11-03-unified-dimension-system-design.md §8.2)
+# 注册中心上线后分阶段开启:enabled 控制读切换,dualWrite 控制双写过渡期
+dimension:
+  registry:
+    enabled: false
+    dualWrite: false
+
 langgraph:
   # LangGraph 报告解析服务(健康报告 PDF → 结构化 JSON)
   # 开发环境默认 localhost,生产环境指向 ai.etotem.com.cn

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

@@ -2623,6 +2623,8 @@ CREATE TABLE IF NOT EXISTS indicator_definitions (
     value_type VARCHAR(16) COMMENT '语义类型: numeric/categorical/level',
     good_direction VARCHAR(16) DEFAULT 'neutral' COMMENT '变化优劣方向: higher_is_better/lower_is_better/in_range_is_better/neutral',
     dimension_code VARCHAR(16) COMMENT '关联五维(body/mind/wisdom/action/wealth,可空)',
+    parent_id BIGINT COMMENT '父节点ID(可指向product_dimension_config.id或本表id, 混排进维度树)',
+    legacy_alias VARCHAR(512) COMMENT '历史别名(JSON数组)',
     sort_order INT DEFAULT 0 COMMENT '排序',
     status TINYINT DEFAULT 1 COMMENT '0=禁用 1=启用',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
@@ -5757,9 +5759,13 @@ CREATE TABLE IF NOT EXISTS product_dimension_config (
     enabled TINYINT(1) DEFAULT 1 COMMENT '启用: 0禁用 1启用',
     dify_dataset_id VARCHAR(100) COMMENT 'Dify Dataset ID,不填则不同步',
     remark VARCHAR(500) COMMENT '备注',
+    legacy_alias VARCHAR(512) COMMENT '历史别名(JSON数组): 承载现存4套编码, 如 ["growth"]',
+    source_system VARCHAR(32) COMMENT '归属子系统: health/energy/indicator/knowledge',
+    wuxing_rule_ref VARCHAR(30) COMMENT '五行规则引用(木/火/金/水/土), 关联EnergyDimension.element',
+    node_kind VARCHAR(16) DEFAULT 'root' COMMENT '节点种类: root(五维根)/sub(子维度)',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='维度配置表';
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='维度配置表(统一维度注册中心)';
 
 -- 知识库表
 CREATE TABLE IF NOT EXISTS dan_knowledge_base (

+ 31 - 0
cfc-web/src/api/dimension.js

@@ -40,6 +40,37 @@ export function getDimensionTree() {
   })
 }
 
+// 统一维度体系(2026-11-03-unified-dimension-system-design.md §5.1)
+// 只读查询统一取数入口:合并维度节点+指标节点的统一树
+export function getDimensionRegistryTree() {
+  return request({
+    url: '/api/dimension-registry/tree',
+    method: 'get'
+  })
+}
+
+export function getDimensionRegistryRoots() {
+  return request({
+    url: '/api/dimension-registry/roots',
+    method: 'get'
+  })
+}
+
+export function getDimensionRegistryCode(code) {
+  return request({
+    url: `/api/dimension-registry/code/${code}`,
+    method: 'get'
+  })
+}
+
+export function resolveDimensionLegacy(legacy) {
+  return request({
+    url: '/api/dimension-registry/map',
+    method: 'get',
+    params: { legacy }
+  })
+}
+
 // 知识库 API
 export function getKnowledgeList(params) {
   return request({

+ 49 - 3
cfc-web/src/components/DimensionSelector/index.vue

@@ -22,6 +22,7 @@
 
 <script>
 import { mapState } from 'vuex'
+import { getDimensionRegistryTree } from '@/api/dimension'
 
 export default {
   name: 'DimensionSelector',
@@ -33,23 +34,68 @@ export default {
     disabled: { type: Boolean, default: false },
     dimensionType: { type: String, default: '' }
   },
+  data() {
+    return {
+      registryDimensions: null
+    }
+  },
   computed: {
     ...mapState('dimension', ['dimensions']),
     selectedValue: {
       get() { return this.value },
       set(val) { this.$emit('input', val) }
     },
+    // 统一维度体系(P1-T4):优先读注册中心统一树,失败回落 vuex 旧数据源
+    effectiveDimensions() {
+      if (this.registryDimensions && this.registryDimensions.length > 0) {
+        return this.registryDimensions
+      }
+      return this.dimensions
+    },
     filteredDimensions() {
-      if (!this.dimensionType) return this.dimensions
-      return this.dimensions.filter(d => d.dimensionType === this.dimensionType)
+      if (!this.dimensionType) return this.effectiveDimensions
+      return this.effectiveDimensions.filter(d => d.dimensionType === this.dimensionType)
     }
   },
   created() {
+    this.fetchRegistryTree()
     if (this.dimensions.length === 0) {
       this.$store.dispatch('dimension/fetchDimensions', { page: 1, size: 200 })
     }
+  },
+  methods: {
+    async fetchRegistryTree() {
+      try {
+        const res = await getDimensionRegistryTree()
+        if (res.code === 200 && res.data) {
+          this.registryDimensions = flattenRegistryTree(res.data)
+        }
+      } catch (e) {
+        // 注册中心接口暂不可用(feature flag 未开启/后端未升级)时沿用旧数据源
+        this.registryDimensions = null
+      }
+    }
   }
 }
+
+// 把注册中心统一树(DimensionNode: id/code/name/parentId/nodeKind/sourceSystem/legacyAlias/children)
+// 拍平为 el-option 可用的 {id, dimensionCode, dimensionName, dimensionType} 列表
+function flattenRegistryTree(nodes, result) {
+  result = result || []
+  if (!nodes || !nodes.length) return result
+  nodes.forEach(n => {
+    result.push({
+      id: n.id,
+      dimensionCode: n.code,
+      dimensionName: n.name,
+      dimensionType: n.nodeKind === 'root' ? 'five' : (n.sourceSystem || '')
+    })
+    if (n.children && n.children.length) {
+      flattenRegistryTree(n.children, result)
+    }
+  })
+  return result
+}
 </script>
 
 <style scoped>
@@ -61,4 +107,4 @@ export default {
 
 /* == C 批内联清理工具类 == */
 .sel-full { width: 100%; }
-</style>
+</style>