Ver código fonte

unified-dimension: T-B 后端去硬编码,DimensionScoreServiceImpl 维度清单改经注册中心取统一码 body.*

Agnes Agent 5 dias atrás
pai
commit
8b80dd3f0a

+ 97 - 48
cfc-backend/src/main/java/com/etotem/cfc/service/impl/DimensionScoreServiceImpl.java

@@ -3,6 +3,9 @@ package com.etotem.cfc.service.impl;
 import com.etotem.cfc.dto.HealthDimensionVO;
 import com.etotem.cfc.dto.DimensionUploadVO;
 import com.etotem.cfc.dto.HealthDimensionQuestionnaireVO;
+import com.etotem.cfc.dto.DimensionNode;
+import com.etotem.cfc.service.DimensionRegistryService;
+import com.etotem.cfc.service.DimensionCodeMapper;
 import com.etotem.cfc.entity.HealthDimensionScore;
 import com.etotem.cfc.entity.HealthDataSourceRecord;
 import com.etotem.cfc.entity.HealthGutFlora;
@@ -38,15 +41,11 @@ import java.util.stream.Collectors;
 @Service
 public class DimensionScoreServiceImpl implements DimensionScoreService {
 
-    private static final List<String[]> DIMENSIONS = Arrays.asList(
-        new String[]{"growth",    "生长发育"},
-        new String[]{"sleep",     "睡眠质量"},
-        new String[]{"vision",    "视力健康"},
-        new String[]{"immunity",  "免疫力"},
-        new String[]{"nutrition", "营养均衡"},
-        new String[]{"gut",       "肠胃健康"},
-        new String[]{"exercise",  "运动活力"}
-    );
+    @Resource
+    private DimensionRegistryService dimensionRegistryService;
+
+    @Resource
+    private DimensionCodeMapper dimensionCodeMapper;
 
     @Resource
     private HealthDimensionScoreService scoreService;
@@ -76,20 +75,57 @@ public class DimensionScoreServiceImpl implements DimensionScoreService {
     @Resource
     private DimensionEnergySyncService dimensionEnergySyncService;
 
+    /**
+     * 统一维度体系中 7 个身体健康子维度(code=body.growth...body.exercise),来源为注册中心
+     * body 根节点子维度(nodeKind=sub),替代原前端/后端硬编码裸名单(growth/sleep/...)。
+     *
+     * <p>顺序与注册中心 seed 一致(growth/sleep/vision/immunity/nutrition/gut/exercise),
+     * 指标节点(nodeKind=indicator,P1-T3 起混排挂载)不作为健康雷达轴,跳过。</p>
+     *
+     * @return 注册中心 body 根节点的子维度节点列表;未就绪时返回空列表(调用方按空处理)
+     */
+    private List<DimensionNode> bodyDimensions() {
+        DimensionNode body = dimensionRegistryService.getNodeByCode("body");
+        if (body == null || body.getChildren() == null) {
+            return new ArrayList<>();
+        }
+        List<DimensionNode> result = new ArrayList<>();
+        for (DimensionNode child : body.getChildren()) {
+            if (child == null) {
+                continue;
+            }
+            if ("indicator".equals(child.getNodeKind())) {
+                continue;
+            }
+            result.add(child);
+        }
+        return result;
+    }
+
     @Override
     public HealthDimensionVO getHealthDimension(Long memberId) {
         List<HealthDimensionScore> latest = scoreService.getLatestScores(memberId);
         Map<String, HealthDimensionScore> scoreMap = latest.stream()
             .collect(Collectors.toMap(HealthDimensionScore::getDimension, s -> s, (a, b) -> a));
 
+        // 统一维度体系整改:维度清单来源改为注册中心(body 根下子维度,统一 code=body.growth...),
+        // 兼容 DB 仍存旧裸名(growth/sleep/...)的过渡态:scoreMap 同时按 legacy 原文 + 统一码建索引。
+        Map<String, HealthDimensionScore> unifiedScoreMap = new HashMap<>(scoreMap);
+        for (Map.Entry<String, HealthDimensionScore> e : scoreMap.entrySet()) {
+            String unified = dimensionRegistryService.resolveLegacyToUnified(e.getKey());
+            if (unified != null) {
+                unifiedScoreMap.putIfAbsent(unified, e.getValue());
+            }
+        }
+
         List<HealthDimensionVO.DimensionItem> items = new ArrayList<>();
         BigDecimal totalScore = BigDecimal.ZERO;
         int validCount = 0;
 
-        for (String[] dim : DIMENSIONS) {
-            String code = dim[0];
-            String label = dim[1];
-            HealthDimensionScore found = scoreMap.get(code);
+        for (DimensionNode dim : bodyDimensions()) {
+            String code = dim.getCode();
+            String label = dim.getName();
+            HealthDimensionScore found = unifiedScoreMap.get(code);
 
             HealthDimensionVO.DimensionItem item = new HealthDimensionVO.DimensionItem();
             item.setDimension(code);
@@ -101,7 +137,7 @@ public class DimensionScoreServiceImpl implements DimensionScoreService {
                 item.setDataSource(found.getDataSource());
                 item.setTier(found.getTier());
                 item.setLevel(calcLevel(found.getScore()));
-                List<HealthDimensionVO.DimensionItem> history = getDimensionHistory(memberId, code, 2);
+                List<HealthDimensionVO.DimensionItem> history = getDimensionHistory(memberId, resolveNormLegacy(code), 2);
                 if (history.size() >= 2) {
                     BigDecimal earlierScore = history.get(1).getScore();
                     BigDecimal latestScore = history.get(0).getScore();
@@ -144,7 +180,7 @@ public class DimensionScoreServiceImpl implements DimensionScoreService {
                 String gender = (member.getGender() != null && !member.getGender().isEmpty())
                     ? member.getGender() : "all";
                 for (HealthDimensionVO.DimensionItem item : items) {
-                    String dimCode = item.getDimension();
+                    String dimCode = resolveNormLegacy(item.getDimension());
                     HealthNormReference norm = normMapper.selectNorm(dimCode, gender, (int) ageMonths);
                     if (norm != null && norm.getPercentile50() != null) {
                         item.setScore(BigDecimal.valueOf(norm.getPercentile50()));
@@ -400,47 +436,47 @@ public class DimensionScoreServiceImpl implements DimensionScoreService {
             List<HealthDiseaseRisk> diseaseRisks) {
 
         Map<String, List<Integer>> dimValues = new HashMap<>();
-        for (String[] dim : DIMENSIONS) {
-            dimValues.put(dim[0], new ArrayList<>());
+        for (DimensionNode dim : bodyDimensions()) {
+            dimValues.put(dim.getCode(), new ArrayList<>());
         }
 
         if (report.getGutHealthScore() != null) {
-            dimValues.get("immunity").add(report.getGutHealthScore());
-            dimValues.get("gut").add(report.getGutHealthScore());
+            dimValues.get("body.immunity").add(report.getGutHealthScore());
+            dimValues.get("body.gut").add(report.getGutHealthScore());
         }
         if (report.getChronicDiseaseScore() != null) {
             int inverted = 100 - report.getChronicDiseaseScore();
-            dimValues.get("growth").add(inverted);
-            dimValues.get("nutrition").add(inverted);
+            dimValues.get("body.growth").add(inverted);
+            dimValues.get("body.nutrition").add(inverted);
         }
         if (report.getNutritionScore() != null) {
-            dimValues.get("nutrition").add(report.getNutritionScore());
+            dimValues.get("body.nutrition").add(report.getNutritionScore());
         }
         if (report.getGutBalanceScore() != null) {
-            dimValues.get("gut").add(report.getGutBalanceScore());
+            dimValues.get("body.gut").add(report.getGutBalanceScore());
         }
         if (report.getGutDiversityScore() != null) {
-            dimValues.get("immunity").add(report.getGutDiversityScore());
-            dimValues.get("gut").add(report.getGutDiversityScore());
+            dimValues.get("body.immunity").add(report.getGutDiversityScore());
+            dimValues.get("body.gut").add(report.getGutDiversityScore());
         }
         if (report.getBeneficialBacteriaScore() != null) {
-            dimValues.get("immunity").add(report.getBeneficialBacteriaScore());
-            dimValues.get("gut").add(report.getBeneficialBacteriaScore());
+            dimValues.get("body.immunity").add(report.getBeneficialBacteriaScore());
+            dimValues.get("body.gut").add(report.getBeneficialBacteriaScore());
         }
         if (report.getHarmfulBacteriaScore() != null) {
             int inverted = 100 - report.getHarmfulBacteriaScore();
-            dimValues.get("immunity").add(inverted);
-            dimValues.get("gut").add(inverted);
+            dimValues.get("body.immunity").add(inverted);
+            dimValues.get("body.gut").add(inverted);
         }
         if (report.getCoreSpeciesScore() != null) {
-            dimValues.get("gut").add(report.getCoreSpeciesScore());
+            dimValues.get("body.gut").add(report.getCoreSpeciesScore());
         }
         if (report.getGutAge() != null) {
             try {
                 int gutAgeYears = Integer.parseInt(report.getGutAge().replaceAll("[^0-9]", ""));
                 int gutAgeScore = Math.max(0, Math.min(100, 100 - gutAgeYears * 2));
-                dimValues.get("sleep").add(gutAgeScore);
-                dimValues.get("gut").add(gutAgeScore);
+                dimValues.get("body.sleep").add(gutAgeScore);
+                dimValues.get("body.gut").add(gutAgeScore);
             } catch (NumberFormatException ignored) {}
         }
 
@@ -452,15 +488,15 @@ public class DimensionScoreServiceImpl implements DimensionScoreService {
             if (val < 0) continue;
 
             if (cat.contains("神经递质")) {
-                dimValues.get("sleep").add(val);
+                dimValues.get("body.sleep").add(val);
             } else if ("短链脂肪酸".equals(cat)) {
-                dimValues.get("nutrition").add(val);
-                dimValues.get("gut").add(val);
+                dimValues.get("body.nutrition").add(val);
+                dimValues.get("body.gut").add(val);
             } else if ("肠道屏障".equals(cat)) {
-                dimValues.get("immunity").add(val);
-                dimValues.get("gut").add(val);
+                dimValues.get("body.immunity").add(val);
+                dimValues.get("body.gut").add(val);
             } else if (cat.contains("维生素") || cat.contains("矿物质") || cat.contains("微量")) {
-                dimValues.get("nutrition").add(val);
+                dimValues.get("body.nutrition").add(val);
             }
         }
 
@@ -476,21 +512,21 @@ public class DimensionScoreServiceImpl implements DimensionScoreService {
             int invertedScore = 100 - Math.min(riskVal, 100);
             String name = risk.getDiseaseName();
             if (name != null && (name.contains("代谢") || name.contains("肥胖") || name.contains("超重"))) {
-                dimValues.get("growth").add(invertedScore);
-                dimValues.get("nutrition").add(invertedScore);
+                dimValues.get("body.growth").add(invertedScore);
+                dimValues.get("body.nutrition").add(invertedScore);
             }
             if (name != null && (name.contains("感染") || name.contains("炎症"))) {
-                dimValues.get("immunity").add(invertedScore);
+                dimValues.get("body.immunity").add(invertedScore);
             }
-            dimValues.get("gut").add(invertedScore);
+            dimValues.get("body.gut").add(invertedScore);
         }
 
         Map<String, Integer> result = new HashMap<>();
-        for (String[] dim : DIMENSIONS) {
-            List<Integer> vals = dimValues.get(dim[0]);
+        for (DimensionNode dim : bodyDimensions()) {
+            List<Integer> vals = dimValues.get(dim.getCode());
             if (vals != null && !vals.isEmpty()) {
                 int avg = (int) vals.stream().mapToInt(v -> v).average().orElse(0);
-                result.put(dim[0], Math.min(100, Math.max(0, avg)));
+                result.put(dim.getCode(), Math.min(100, Math.max(0, avg)));
             }
         }
         return result;
@@ -507,8 +543,21 @@ public class DimensionScoreServiceImpl implements DimensionScoreService {
         }
     }
 
+    private String resolveNormLegacy(String dimension) {
+        if (dimension == null || dimension.isEmpty()) {
+            return dimension;
+        }
+        List<String> legacyCodes = dimensionCodeMapper.resolveLegacy(dimension);
+        if (legacyCodes != null && !legacyCodes.isEmpty()) {
+            return legacyCodes.get(0);
+        }
+        return dimension;
+    }
+
     private Integer calcPercentile(String dimension, int score) {
-        HealthNormReference norm = normMapper.selectNorm(dimension, "all", 120);
+        // 统一维度体系:常模表按 legacy 维度码(growth/...)建索引,统一 code(body.growth)需先回解析
+        String normDimension = resolveNormLegacy(dimension);
+        HealthNormReference norm = normMapper.selectNorm(normDimension, "all", 120);
         if (norm == null) return null;
         if (norm.getPercentile95() != null && score >= norm.getPercentile95()) return 95;
         if (norm.getPercentile85() != null && score >= norm.getPercentile85()) return 85;
@@ -551,8 +600,8 @@ public class DimensionScoreServiceImpl implements DimensionScoreService {
                 return norm.getPercentile50();
             }
         } else {
-            // 身体/健康维度走 health_norm_reference 表
-            HealthNormReference norm = normMapper.selectNorm(dimension, gender, (int) ageMonths);
+            // 身体/健康维度走 health_norm_reference 表(常模表按 legacy 码建索引,统一 code 需回解析)
+            HealthNormReference norm = normMapper.selectNorm(resolveNormLegacy(dimension), gender, (int) ageMonths);
             if (norm != null) {
                 return norm.getPercentile50();
             }

+ 70 - 13
cfc-backend/src/test/java/com/etotem/cfc/integration/service/DimensionScoreServiceTest.java

@@ -3,8 +3,11 @@ package com.etotem.cfc.service;
 import com.etotem.cfc.dto.DimensionUploadVO;
 import com.etotem.cfc.dto.HealthDimensionQuestionnaireVO;
 import com.etotem.cfc.dto.HealthDimensionVO;
+import com.etotem.cfc.dto.DimensionNode;
 import com.etotem.cfc.entity.*;
 import com.etotem.cfc.mapper.HealthNormReferenceMapper;
+import com.etotem.cfc.service.DimensionRegistryService;
+import com.etotem.cfc.service.DimensionCodeMapper;
 import com.etotem.cfc.service.impl.DimensionScoreServiceImpl;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -14,6 +17,7 @@ import org.mockito.MockitoAnnotations;
 
 import java.math.BigDecimal;
 import java.sql.Date;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
@@ -57,6 +61,12 @@ public class DimensionScoreServiceTest {
     @Mock
     private DimensionEnergySyncService dimensionEnergySyncService;
 
+    @Mock
+    private DimensionRegistryService dimensionRegistryService;
+
+    @Mock
+    private DimensionCodeMapper dimensionCodeMapper;
+
     private DimensionScoreServiceImpl service;
 
     @BeforeEach
@@ -71,6 +81,52 @@ public class DimensionScoreServiceTest {
         setField(service, "familyMemberMapper", familyMemberMapper);
         setField(service, "danAssessmentResultMapper", danAssessmentResultMapper);
         setField(service, "dimensionEnergySyncService", dimensionEnergySyncService);
+        setField(service, "dimensionRegistryService", dimensionRegistryService);
+        setField(service, "dimensionCodeMapper", dimensionCodeMapper);
+        // 统一维度体系:注册中心 body 根节点 → 7 个健康子维度(统一码 body.growth...body.exercise)
+        mockBodyDimensions();
+    }
+
+    /** 构造注册中心 body 根下的一个子维度节点(nodeKind=sub)。 */
+    private DimensionNode dimNode(String code, String name) {
+        DimensionNode n = new DimensionNode();
+        n.setCode(code);
+        n.setName(name);
+        n.setNodeKind("sub");
+        return n;
+    }
+
+    /**
+     * Mock 注册中心:body 根节点挂载 7 个健康子维度(统一码),并 stub
+     * legacy→统一码 解析(兼容 DB 存旧裸名 growth/... 的过渡态)与
+     * 统一码→legacy 回解析(常模/历史表按 legacy 裸名建索引)。
+     */
+    private void mockBodyDimensions() {
+        DimensionNode body = new DimensionNode();
+        body.setCode("body");
+        body.setName("身体健康");
+        body.setNodeKind("root");
+        List<DimensionNode> children = new ArrayList<>();
+        children.add(dimNode("body.growth", "生长发育"));
+        children.add(dimNode("body.sleep", "睡眠质量"));
+        children.add(dimNode("body.vision", "视力健康"));
+        children.add(dimNode("body.immunity", "免疫力"));
+        children.add(dimNode("body.nutrition", "营养均衡"));
+        children.add(dimNode("body.gut", "肠胃健康"));
+        children.add(dimNode("body.exercise", "运动活力"));
+        body.setChildren(children);
+        when(dimensionRegistryService.getNodeByCode("body")).thenReturn(body);
+        // legacy 裸名 → 统一码(getHealthDimension 构建 unifiedScoreMap 时使用)
+        when(dimensionRegistryService.resolveLegacyToUnified("growth")).thenReturn("body.growth");
+        when(dimensionRegistryService.resolveLegacyToUnified("sleep")).thenReturn("body.sleep");
+        when(dimensionRegistryService.resolveLegacyToUnified("vision")).thenReturn("body.vision");
+        when(dimensionRegistryService.resolveLegacyToUnified("immunity")).thenReturn("body.immunity");
+        when(dimensionRegistryService.resolveLegacyToUnified("nutrition")).thenReturn("body.nutrition");
+        when(dimensionRegistryService.resolveLegacyToUnified("gut")).thenReturn("body.gut");
+        when(dimensionRegistryService.resolveLegacyToUnified("exercise")).thenReturn("body.exercise");
+        // 统一码 → legacy 回解析:默认返回空(resolveNormLegacy 原样返回入参),
+        // 使 calcPercentile/趋势历史仍按 legacy 裸名访问常模/历史表
+        when(dimensionCodeMapper.resolveLegacy(anyString())).thenReturn(Collections.emptyList());
     }
 
     private void setField(Object target, String fieldName, Object value) {
@@ -131,10 +187,10 @@ public class DimensionScoreServiceTest {
 
         HealthDimensionVO vo = service.getHealthDimension(1L);
 
-assertLevel(vo, "growth", "excellent");
-        assertLevel(vo, "sleep", "good");
-        assertLevel(vo, "vision", "fair");
-        assertLevel(vo, "immunity", "poor");
+        assertLevel(vo, "body.growth", "excellent");
+        assertLevel(vo, "body.sleep", "good");
+        assertLevel(vo, "body.vision", "fair");
+        assertLevel(vo, "body.immunity", "poor");
         assertEquals(new BigDecimal("63.75"), vo.getHealthIndex());
     }
 
@@ -147,7 +203,7 @@ assertLevel(vo, "growth", "excellent");
 
         HealthDimensionVO vo = service.getHealthDimension(1L);
 
-        HealthDimensionVO.DimensionItem item = findItem(vo, "growth");
+        HealthDimensionVO.DimensionItem item = findItem(vo, "body.growth");
         assertNotNull(item.getExpireDate());
         assertEquals(10, item.getExpireDate().length()); // yyyy-MM-dd = 10
     }
@@ -288,7 +344,7 @@ assertLevel(vo, "growth", "excellent");
 
         HealthDimensionVO vo = service.getHealthDimension(1L);
 
-        assertLevel(vo, "growth", "excellent");
+        assertLevel(vo, "body.growth", "excellent");
     }
 
     @Test
@@ -298,7 +354,7 @@ assertLevel(vo, "growth", "excellent");
 
         HealthDimensionVO vo = service.getHealthDimension(1L);
 
-        assertLevel(vo, "growth", "good");
+        assertLevel(vo, "body.growth", "good");
     }
 
     @Test
@@ -308,7 +364,7 @@ assertLevel(vo, "growth", "excellent");
 
         HealthDimensionVO vo = service.getHealthDimension(1L);
 
-        assertLevel(vo, "growth", "fair");
+        assertLevel(vo, "body.growth", "fair");
     }
 
     @Test
@@ -318,7 +374,7 @@ assertLevel(vo, "growth", "excellent");
 
         HealthDimensionVO vo = service.getHealthDimension(1L);
 
-        assertLevel(vo, "growth", "poor");
+        assertLevel(vo, "body.growth", "poor");
     }
 
     // ── calcPercentile (via uploadScore) ────────────────────────────────────
@@ -393,9 +449,9 @@ assertLevel(vo, "growth", "excellent");
         verify(scoreService, atLeast(1)).saveScore(cap.capture());
 
         boolean hasGut = cap.getAllValues().stream()
-            .anyMatch(s -> "gut".equals(s.getDimension()) && s.getScore() == 80);
+            .anyMatch(s -> "body.gut".equals(s.getDimension()) && s.getScore() == 80);
         boolean hasImmunity = cap.getAllValues().stream()
-            .anyMatch(s -> "immunity".equals(s.getDimension()) && s.getScore() == 80);
+            .anyMatch(s -> "body.immunity".equals(s.getDimension()) && s.getScore() == 80);
         assertTrue(hasGut, "gut dimension should be set from gutHealthScore");
         assertTrue(hasImmunity, "immunity dimension should be set from gutHealthScore");
     }
@@ -416,7 +472,8 @@ assertLevel(vo, "growth", "excellent");
         HealthDimensionVO vo = service.getHealthDimension(1L);
 
         List<String> expected = Arrays.asList(
-            "growth", "sleep", "vision", "immunity", "nutrition", "gut", "exercise");
+            "body.growth", "body.sleep", "body.vision", "body.immunity",
+            "body.nutrition", "body.gut", "body.exercise");
         List<String> expectedLabels = Arrays.asList(
             "生长发育", "睡眠质量", "视力健康", "免疫力", "营养均衡", "肠胃健康", "运动活力");
         List<String> actual = new java.util.ArrayList<>();
@@ -442,7 +499,7 @@ assertLevel(vo, "growth", "excellent");
 
         // 实现中即使过期也显示该分数(只按 found!=null 判断)
         // 这个测试验证实现行为:即使过期也有分
-        assertNotNull(findItem(vo, "growth").getScore());
+        assertNotNull(findItem(vo, "body.growth").getScore());
     }
 
     // ── Helper ─────────────────────────────────────────────────────────────

+ 33 - 3
cfc-backend/src/test/java/com/etotem/cfc/unit/ReportConfirmRefreshFiveDimContractTest.java

@@ -53,6 +53,36 @@ class ReportConfirmRefreshFiveDimContractTest {
         injectAll(service);
         healthReportService = (HealthReportService) readField(service, "healthReportService");
         scoreService = (HealthDimensionScoreService) readField(service, "scoreService");
+        mockBodyRegistry();
+    }
+
+    /**
+     * 统一维度体系:Mock 注册中心 body 根节点 → 7 个健康子维度(统一码 body.*),
+     * 使 mapGutFloraToDimensions 按注册中心维度清单取数。
+     */
+    private void mockBodyRegistry() throws Exception {
+        com.etotem.cfc.service.DimensionRegistryService registry =
+                (com.etotem.cfc.service.DimensionRegistryService) readField(service, "dimensionRegistryService");
+        String[][] dims = {
+                {"body.growth", "生长发育"}, {"body.sleep", "睡眠质量"},
+                {"body.vision", "视力健康"}, {"body.immunity", "免疫力"},
+                {"body.nutrition", "营养均衡"}, {"body.gut", "肠胃健康"},
+                {"body.exercise", "运动活力"}
+        };
+        List<com.etotem.cfc.dto.DimensionNode> children = new ArrayList<>();
+        for (String[] d : dims) {
+            com.etotem.cfc.dto.DimensionNode n = new com.etotem.cfc.dto.DimensionNode();
+            n.setCode(d[0]);
+            n.setName(d[1]);
+            n.setNodeKind("sub");
+            children.add(n);
+        }
+        com.etotem.cfc.dto.DimensionNode body = new com.etotem.cfc.dto.DimensionNode();
+        body.setCode("body");
+        body.setName("身体健康");
+        body.setNodeKind("root");
+        body.setChildren(children);
+        when(registry.getNodeByCode("body")).thenReturn(body);
     }
 
     private static Object readField(Object target, String name) throws Exception {
@@ -99,9 +129,9 @@ class ReportConfirmRefreshFiveDimContractTest {
             assertEquals(55L, s.getMemberId(), "五维分应写入指定成员");
             assertEquals("REPORT", s.getDataSource(), "报告确认来源应为 REPORT");
             assertEquals(Integer.valueOf(1), s.getTier(), "报告级数据 tier=1");
-            if ("gut".equals(s.getDimension())) hasGut = true;
-            if ("immunity".equals(s.getDimension())) hasImmunity = true;
-            if ("nutrition".equals(s.getDimension())) hasNutrition = true;
+            if ("body.gut".equals(s.getDimension())) hasGut = true;
+            if ("body.immunity".equals(s.getDimension())) hasImmunity = true;
+            if ("body.nutrition".equals(s.getDimension())) hasNutrition = true;
         }
         assertTrue(hasGut, "gut 肠胃健康应被派生");
         assertTrue(hasImmunity, "immunity 免疫力应被派生");