Просмотр исходного кода

feat(dimension): P1-T4 其他消费方去直读统一经注册中心

- DimensionRegistryService 新增桥接方法 getDifyDatasetIdByCode/
  getDimensionCodesByIds/getDimensionIdByCode,消费方不再直读
  product_dimension_config(实体/表仅注册中心层可见)。
- DanKnowledgeBaseService: 4 处 mapper 直读(selectBatchIds×2/selectById
  循环/selectOne by code)全部改走 DimensionRegistryService 桥接,
  行为等价(同 id→code、code→id 语义)。
- LanggraphSyncService: selectOne by code 改 getDifyDatasetIdByCode。
- HealthDimensionController: /api/dimension/overview 与 /api/dimension/radar-data
  标 @Deprecated(设计 §5.3,不删除,保留至零调用确认后下线)。
- cfc-web: ContentDimensionThresholdEditor / 知识库维度树改走只读
  /api/dimension-registry/tree(映射 code/name,跳过 indicator 节点);
  DimensionSelector 排除 indicator 节点保持选择项与旧数据源一致。
- 保留直读点(注册中心管理层/一次性迁移,说明见 commit 后汇报):
  DimensionConfigController 与管理页(/api/admin/dimension-config/* 写接口)、
  MigrationController 一次性历史迁移(enabled=1 语义不可经树等价表达)。
- 验收: mvn -q compile EXIT=0;web 脚本 node --check 通过;无新增循环依赖。
Agnes Agent 1 неделя назад
Родитель
Сommit
f5c7445f3e

+ 14 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthDimensionController.java

@@ -28,6 +28,13 @@ public class HealthDimensionController {
     @Resource
     private FamilyMemberMapper familyMemberMapper;
 
+    /**
+     * 获取健康维度概览。
+     *
+     * @deprecated 统一维度体系下前端改走 /api/dimension-registry/*(注册中心只读接口),
+     *             本接口保留至零调用确认后下线(设计文档 §5.3),不删除。
+     */
+    @Deprecated
     @Operation(summary = "获取健康维度概览")
     @PostMapping("/overview")
     public Result<HealthDimensionVO> overview(
@@ -42,6 +49,13 @@ public class HealthDimensionController {
         return Result.success(vo);
     }
 
+    /**
+     * 获取雷达图三层数据:实际值/人群位置/家庭占比。
+     *
+     * @deprecated 统一维度体系下前端改走 /api/dimension-registry/*(注册中心只读接口),
+     *             本接口保留至零调用确认后下线(设计文档 §5.3),不删除。
+     */
+    @Deprecated
     @Operation(summary = "获取雷达图三层数据:实际值/人群位置/家庭占比")
     @PostMapping("/radar-data")
     public Result<Map<String, Object>> radarData(

+ 11 - 40
cfc-backend/src/main/java/com/etotem/cfc/service/DanKnowledgeBaseService.java

@@ -7,12 +7,10 @@ import com.etotem.cfc.entity.DanKnowledgeBase;
 import com.etotem.cfc.entity.DanKnowledgeBaseDimension;
 import com.etotem.cfc.entity.DanKnowledgeBaseTag;
 import com.etotem.cfc.entity.DanKnowledgeTag;
-import com.etotem.cfc.entity.ProductDimensionConfig;
 import com.etotem.cfc.mapper.DanKnowledgeBaseDimensionMapper;
 import com.etotem.cfc.mapper.DanKnowledgeBaseMapper;
 import com.etotem.cfc.mapper.DanKnowledgeBaseTagMapper;
 import com.etotem.cfc.mapper.DanKnowledgeTagMapper;
-import com.etotem.cfc.mapper.ProductDimensionConfigMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -46,7 +44,7 @@ public class DanKnowledgeBaseService extends ServiceImpl<DanKnowledgeBaseMapper,
     private DanKnowledgeTagMapper danKnowledgeTagMapper;
 
     @Resource
-    private ProductDimensionConfigMapper productDimensionConfigMapper;
+    private DimensionRegistryService dimensionRegistryService;
 
     @Resource
     private LanggraphSyncService langgraphSyncService;
@@ -276,17 +274,7 @@ public class DanKnowledgeBaseService extends ServiceImpl<DanKnowledgeBaseMapper,
                 }
             }
         }
-        Map<Long, String> dimCodesById = new HashMap<>();
-        if (!allDimIds.isEmpty()) {
-            List<ProductDimensionConfig> configs = productDimensionConfigMapper.selectBatchIds(allDimIds);
-            if (configs != null) {
-                for (ProductDimensionConfig config : configs) {
-                    if (config.getId() != null && config.getDimensionCode() != null) {
-                        dimCodesById.put(config.getId(), config.getDimensionCode());
-                    }
-                }
-            }
-        }
+        Map<Long, String> dimCodesById = dimensionRegistryService.getDimensionCodesByIds(allDimIds);
 
         for (DanKnowledgeBase record : records) {
             List<Long> tids = tagIdsByKnowledge.getOrDefault(record.getId(), Collections.emptyList());
@@ -322,15 +310,7 @@ public class DanKnowledgeBaseService extends ServiceImpl<DanKnowledgeBaseMapper,
         }
         List<Long> dimIds = knowledge.getDimensionIds();
         if (dimIds != null && !dimIds.isEmpty()) {
-            List<ProductDimensionConfig> configs = productDimensionConfigMapper.selectBatchIds(dimIds);
-            Map<Long, String> codes = new HashMap<>();
-            if (configs != null) {
-                for (ProductDimensionConfig config : configs) {
-                    if (config.getId() != null && config.getDimensionCode() != null) {
-                        codes.put(config.getId(), config.getDimensionCode());
-                    }
-                }
-            }
+            Map<Long, String> codes = dimensionRegistryService.getDimensionCodesByIds(dimIds);
             knowledge.setDimensionCodes(dimIds.stream().map(codes::get)
                 .filter(Objects::nonNull).collect(Collectors.toList()));
         } else {
@@ -390,21 +370,15 @@ public class DanKnowledgeBaseService extends ServiceImpl<DanKnowledgeBaseMapper,
                 .eq(DanKnowledgeBaseDimension::getKnowledgeId, knowledgeId));
         if (dims == null || dims.isEmpty()) return new java.util.ArrayList<>();
         List<Long> dimIds = dims.stream().map(DanKnowledgeBaseDimension::getDimensionId).collect(Collectors.toList());
-        ProductDimensionConfig cfg = new ProductDimensionConfig();
-        cfg.setId(dimIds.get(0));
-        List<ProductDimensionConfig> configs = new java.util.ArrayList<>();
+        Map<Long, String> codeById = dimensionRegistryService.getDimensionCodesByIds(dimIds);
+        List<String> codes = new java.util.ArrayList<>();
         for (Long dimId : dimIds) {
-            ProductDimensionConfig c = new ProductDimensionConfig();
-            c.setId(dimId);
-            configs.add(c);
+            String code = codeById.get(dimId);
+            if (code != null) {
+                codes.add(code);
+            }
         }
-        return configs.stream()
-            .map(c -> {
-                ProductDimensionConfig full = productDimensionConfigMapper.selectById(c.getId());
-                return full != null ? full.getDimensionCode() : null;
-            })
-            .filter(code -> code != null)
-            .collect(Collectors.toList());
+        return codes;
     }
 
     // ==================== 标签/维度自动回填 ====================
@@ -525,9 +499,6 @@ public class DanKnowledgeBaseService extends ServiceImpl<DanKnowledgeBaseMapper,
     }
 
     private Long dimensionIdByCode(String code) {
-        ProductDimensionConfig cfg = productDimensionConfigMapper.selectOne(
-            new LambdaQueryWrapper<ProductDimensionConfig>()
-                .eq(ProductDimensionConfig::getDimensionCode, code));
-        return cfg != null ? cfg.getId() : null;
+        return dimensionRegistryService.getDimensionIdByCode(code);
     }
 }

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

@@ -10,6 +10,7 @@ import org.springframework.util.StringUtils;
 
 import javax.annotation.Resource;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -146,6 +147,67 @@ public class DimensionRegistryService {
         return dimensionCodeMapper.resolveToUnified(legacy);
     }
 
+    /**
+     * 按统一 code 取维度配置的 Dify DatasetId。
+     *
+     * <p>P1-T4:知识库同步(LanggraphSyncService)等消费方不再直读 product_dimension_config,
+     * 统一经注册中心层取数,避免绕过注册中心。</p>
+     *
+     * @param code 统一维度 code(如 body / body.growth / mind.openness)
+     * @return 该维度的 Dify DatasetId;code 不存在或未配置返回 null
+     */
+    public String getDifyDatasetIdByCode(String code) {
+        if (code == null || code.isEmpty()) {
+            return null;
+        }
+        ProductDimensionConfig cfg = productDimensionConfigService.getByCode(code);
+        return cfg != null ? cfg.getDifyDatasetId() : null;
+    }
+
+    /**
+     * 批量取维度节点 id → dimensionCode 映射。
+     *
+     * <p>P1-T4:知识库关联回填(DanKnowledgeBaseService)不再直读 product_dimension_config,
+     * 统一经注册中心层取数。仅返回维度节点 id(与注册中心维度节点 id 同源,均为
+     * product_dimension_config.id),指标节点 id 不在映射内。</p>
+     *
+     * @param ids 维度节点 id 集合(可空)
+     * @return id → dimensionCode 映射(无匹配或空入参返回空 map)
+     */
+    public Map<Long, String> getDimensionCodesByIds(Collection<Long> ids) {
+        Map<Long, String> map = new HashMap<>();
+        if (ids == null || ids.isEmpty()) {
+            return map;
+        }
+        List<ProductDimensionConfig> configs = productDimensionConfigService.listByIds(ids);
+        if (configs == null) {
+            return map;
+        }
+        for (ProductDimensionConfig cfg : configs) {
+            if (cfg.getId() != null && cfg.getDimensionCode() != null) {
+                map.put(cfg.getId(), cfg.getDimensionCode());
+            }
+        }
+        return map;
+    }
+
+    /**
+     * 按统一 code 取维度节点 id。
+     *
+     * <p>P1-T4:知识库自动绑定(DanKnowledgeBaseService.dimensionIdByCode)不再直读
+     * product_dimension_config,统一经注册中心层取数。</p>
+     *
+     * @param code 统一维度 code(如 body / body.growth)
+     * @return 该维度节点 id;code 不存在返回 null
+     */
+    public Long getDimensionIdByCode(String code) {
+        if (code == null || code.isEmpty()) {
+            return null;
+        }
+        ProductDimensionConfig cfg = productDimensionConfigService.getByCode(code);
+        return cfg != null ? cfg.getId() : null;
+    }
+
     /** 注册中心写入入口:admin 配置写产品维度配置表 + 校验 code 唯一。 */
     public ProductDimensionConfig saveConfig(ProductDimensionConfig node) {
         if (node == null) {

+ 4 - 8
cfc-backend/src/main/java/com/etotem/cfc/service/LanggraphSyncService.java

@@ -1,9 +1,6 @@
 package com.etotem.cfc.service;
 
 import com.etotem.cfc.entity.DanKnowledgeBase;
-import com.etotem.cfc.entity.ProductDimensionConfig;
-import com.etotem.cfc.mapper.ProductDimensionConfigMapper;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Value;
@@ -38,7 +35,7 @@ public class LanggraphSyncService {
     private String difyApiKey;
 
     @Resource
-    private ProductDimensionConfigMapper productDimensionConfigMapper;
+    private DimensionRegistryService dimensionRegistryService;
 
     private final RestTemplate restTemplate = new RestTemplate();
 
@@ -47,13 +44,12 @@ public class LanggraphSyncService {
         if (dimensionCodes == null || dimensionCodes.isEmpty()) return;
         for (String code : dimensionCodes) {
             try {
-                ProductDimensionConfig config = productDimensionConfigMapper.selectOne(
-                    new LambdaQueryWrapper<ProductDimensionConfig>().eq(ProductDimensionConfig::getDimensionCode, code));
-                if (config == null || config.getDifyDatasetId() == null || config.getDifyDatasetId().isEmpty()) {
+                String datasetId = dimensionRegistryService.getDifyDatasetIdByCode(code);
+                if (datasetId == null || datasetId.isEmpty()) {
                     log.warn("维度[{}]未配置 Dify Dataset,跳过同步", code);
                     continue;
                 }
-                String url = difyBaseUrl + "/v1/datasets/" + config.getDifyDatasetId() + "/documents";
+                String url = difyBaseUrl + "/v1/datasets/" + datasetId + "/documents";
                 HttpHeaders headers = new HttpHeaders();
                 headers.setContentType(MediaType.APPLICATION_JSON);
                 headers.set("Authorization", "Bearer " + difyApiKey);

+ 13 - 5
cfc-web/src/components/ContentDimensionThresholdEditor.vue

@@ -66,7 +66,7 @@
 </template>
 
 <script>
-import { getDimensionTree } from '@/api/dimension'
+import { getDimensionRegistryTree } from '@/api/dimension'
 import { getContentDimensionThresholdList, batchSaveContentDimensionThreshold } from '@/api/content-dimension-threshold'
 
 export default {
@@ -117,7 +117,9 @@ export default {
   methods: {
     async loadDimensionTree() {
       try {
-        var res = await getDimensionTree()
+        // 统一维度体系(P1-T4):改走注册中心只读接口 /api/dimension-registry/tree,
+        // 不再直读 /api/admin/dimension-config/tree(旧管理端写接口仅保留给配置管理页)。
+        var res = await getDimensionRegistryTree()
         if (res.code === 200 && Array.isArray(res.data)) {
           this.flatOptions = this.flattenTree(res.data)
         }
@@ -129,9 +131,15 @@ export default {
       var result = []
       var walk = function(list, groupName) {
         ;(list || []).forEach(function(n) {
-          var g = groupName || (n.dimensionName || '其他')
-          if (n.dimensionCode) {
-            result.push({ dimensionCode: n.dimensionCode, dimensionName: n.dimensionName, group: g })
+          if (!n) return
+          // 指标节点(nodeKind=indicator)不参与维度阈值选择(阈值按维度 code 配置)
+          if (n.nodeKind === 'indicator') return
+          // 注册中心节点字段为 code/name,兼容旧接口的 dimensionCode/dimensionName
+          var code = n.dimensionCode || n.code
+          var name = n.dimensionName || n.name
+          var g = groupName || (name || '其他')
+          if (code) {
+            result.push({ dimensionCode: code, dimensionName: name, group: g })
           }
           if (n.children && n.children.length) {
             walk(n.children, g)

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

@@ -84,6 +84,9 @@ function flattenRegistryTree(nodes, result) {
   result = result || []
   if (!nodes || !nodes.length) return result
   nodes.forEach(n => {
+    // P1-T4:指标节点(nodeKind=indicator)不进入维度选择器——旧数据源
+    // (product_dimension_config) 仅含维度节点,混入指标会改变选择项语义。
+    if (n && n.nodeKind === 'indicator') return
     result.push({
       id: n.id,
       dimensionCode: n.code,

+ 32 - 4
cfc-web/src/views/admin/knowledge/index.vue

@@ -125,7 +125,7 @@
 import SortMixin from '@/mixins/SortMixin'
 import ListStateMixin from '@/mixins/ListStateMixin'
 import { mapState, mapActions } from 'vuex'
-import { getDimensionTree } from '@/api/dimension'
+import { getDimensionRegistryTree } from '@/api/dimension'
 import { getKnowledge, deleteKnowledge, bindKnowledgeDimensions, bindKnowledgeTags } from '@/api/dimension'
 import KnowledgeTable from '@/components/KnowledgeTable'
 import KnowledgeDialog from '@/components/KnowledgeDialog'
@@ -176,10 +176,38 @@ export default {
   methods: {
     ...mapActions('dimension', ['fetchKnowledgeList']),
     async fetchDimensionTree() {
-      const res = await getDimensionTree()
-      if (res.code === 200) {
-        this.dimensionTree = res.data || []
+      try {
+        // 统一维度体系(P1-T4):改走注册中心只读接口 /api/dimension-registry/tree,
+        // 不再直读 /api/admin/dimension-config/tree(旧写接口仅保留给维度配置管理页)。
+        const res = await getDimensionRegistryTree()
+        if (res.code === 200) {
+          this.dimensionTree = this.mapDimTree(res.data || [])
+        }
+      } catch (e) {
+        console.error('[knowledge] 加载维度树失败', e)
+      }
+    },
+    // 注册中心 DimensionNode({id, code, name, parentId, nodeKind, children}) →
+    // el-tree 所需 {id, dimensionCode, dimensionName, children};指标节点不参与知识维度绑定。
+    mapDimTree(nodes) {
+      var walk = function(list) {
+        var out = []
+        ;(list || []).forEach(function(n) {
+          if (!n || n.nodeKind === 'indicator') return
+          var child = {
+            id: n.id,
+            dimensionCode: n.code,
+            dimensionName: n.name,
+            children: []
+          }
+          if (n.children && n.children.length) {
+            child.children = walk(n.children)
+          }
+          out.push(child)
+        })
+        return out
       }
+      return walk(nodes)
     },
     fetchList() {
       const params = { page: this.page, size: this.size }