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

docs: add 7-dim health model refactor plan + design spec

- Plan: align existing health dimensions (body/fitness/cardio/blood/sleep/psyche/diet)
  to design doc spec (growth/sleep/vision/immunity/nutrition/gut/exercise)
- Includes: DB migration (3 tables), entity remodel, percentile engine,
  data source tier priority, gut flora report mapping
- Design spec: 7-dim model definition, mapping matrix, architecture notes
Sisyphus 4 месяцев назад
Родитель
Сommit
fc4a0664b6

+ 1466 - 0
docs/superpowers/plans/2026-06-26-health-dimensions-refactor.md

@@ -0,0 +1,1466 @@
+# 身体健康7维模型 — 重构对齐计划(Phase 1)
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 将现有7维健康实现(body/fitness/cardio/blood/sleep/psyche/diet)重构对齐到设计文档规范的7维模型(growth/sleep/vision/immunity/nutrition/gut/exercise),包括数据库表结构、评分引擎、常模百分位系统、菌群报告映射。
+
+**核心架构变更:**
+- 表 `health_dimension_score` → `health_dimension_scores`,增加 percentile/tier/raw_data/expire_date 字段
+- 表 `health_norm_reference` 改为百分位结构(percentile_5~95),按性别+年龄月查询
+- 表 `health_data_source_record` 增加 sourceType/dimensionCodes/parsedResult/confidence
+- `DimensionScoreServiceImpl` 核心聚合器:替换7维列表 + 实现百分位计算 + 数据源Tier优先级 + 菌群映射集成
+
+**Tech Stack:** Java 8, Spring Boot 2.7.18, MyBatis-Plus, MySQL 8.0, uni-app Vue 2
+
+---
+
+## Context
+
+### 设计文档中定义的7维
+```java
+// 枚举值(维度code → 中文名)
+growth    → "生长发育"
+sleep     → "睡眠质量"
+vision    → "视力健康"
+immunity  → "免疫力"
+nutrition → "营养均衡"
+gut       → "肠胃健康"
+exercise  → "运动活力"
+```
+
+### 设计文档中的Phase 1范围
+1. 核心框架:`health_dimension_scores` / `health_data_sources` / `health_norm_reference` 3张表
+2. `DimensionScoreService` 聚合逻辑(Tier 3 问卷兜底 + Tier 1 报告覆盖)
+3. `HealthDimensionController` — 获取7维 / 手动输入问卷
+4. 前端:7维雷达图 + 每个维度简单问卷
+5. **已有菌群报告数据直接映射**(PdfParseService 解析结果 → 维度评分)
+6. 部署常模种子数据(BMI/睡眠/体测等国家标准)
+
+### 数据源优先级
+- Tier 1 (REPORT): 菌群报告/体检报告 → 覆盖低层级
+- Tier 2 (VOICE/PHOTO): 语音解析/拍照 → 置信度<0.6降级
+- Tier 3 (MANUAL): 问卷/手动 → 兜底
+
+### 常模百分位颜色
+- ≥85 → 优秀 (深绿)
+- 60-84 → 良好 (浅绿)
+- 40-59 → 一般 (黄)
+- <40 → 关注 (橙/红)
+
+### 菌群报告 → 7维映射矩阵(关键)
+
+| 菌群指标 | 生长 | 睡眠 | 视力 | 免疫 | 营养 | 肠胃 | 运动 |
+|---------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| gutHealthScore | | | | ✅ | | ✅ | |
+| balanceScore | | | | | | ✅ | |
+| diversityScore | | | | ✅ | | ✅ | |
+| nutritionScore | | | | | ✅ | | |
+| chronicDiseaseScore | ✅ | | | | ✅ | | |
+| beneficialBacteriaScore | | | | ✅ | | ✅ | |
+| harmfulBacteriaScore | | | | ✅ | | ✅ | |
+| coreGenusScore | | | | | | ✅ | |
+| gutAge | | ✅ | | | | ✅ | |
+| 神经递质(indicators) | | ✅ | | | | | |
+| 肠道屏障(indicators) | | | | ✅ | | ✅ | |
+| 短链脂肪酸(indicators) | | | | | ✅ | ✅ | |
+| 疾病风险(diseaseRisks) | ✅ | | | ✅ | | | |
+| 菌种丰度(populationLevel) | ✅ | | | ✅ | ✅ | ✅ | |
+| 微量指标(维生素/矿物质) | | | | | ✅ | | |
+
+### 已存在的文件(重构对象)
+```
+后端:
+  entity/    HealthDimensionScore.java   HealthNormReference.java   HealthDataSourceRecord.java
+  mapper/    HealthDimensionScoreMapper.java   HealthNormReferenceMapper.java   HealthDataSourceRecordMapper.java
+  service/   HealthDimensionScoreService.java + impl/
+             DimensionScoreService.java + impl/DimensionScoreServiceImpl.java
+             HealthDataSourceRecordService.java (interface)
+             PdfParseService.java  ← 菌群报告解析器(已有)
+  controller/HealthDimensionController.java
+  dto/       HealthDimensionVO.java   DimensionUploadVO.java   HealthDimensionQuestionnaireVO.java   DimensionScoreVO.java
+  config/    DatabaseInitializer.java
+
+前端:
+  pages/body/health-dimensions.vue
+  pages.json (路由已注册,无需修改)
+```
+
+---
+
+## File Structure
+
+### Modified Backend Files
+| # | File | Change |
+|:-:|------|--------|
+| 1 | `entity/HealthDimensionScore.java` | 表名→`health_dimension_scores`,增 `percentile`/`tier`/`rawData`/`assessDate`/`expireDate`/`updatedAt` |
+| 2 | `entity/HealthNormReference.java` | 改为百分位字段 `percentile5-95` + `gender`/`ageMin`/`ageMax`(月) |
+| 3 | `entity/HealthDataSourceRecord.java` | 改为 `sourceType`/`dimensionCodes`/`parsedResult`/`fileUrl`/`confidence` |
+| 4 | `mapper/HealthDimensionScoreMapper.java` | SQL适配新表名和字段,新增百分位查询 |
+| 5 | `mapper/HealthNormReferenceMapper.java` | 新查询方法 `selectPercentile(dimension, gender, ageMonths, score)` |
+| 6 | `mapper/HealthDataSourceRecordMapper.java` | SQL适配新表结构 |
+| 7 | `service/HealthDimensionScoreService.java` | 接口更新 |
+| 8 | `service/impl/HealthDimensionScoreServiceImpl.java` | 实现更新 |
+| 9 | `service/DimensionScoreService.java` | 接口新增 `refreshFromReport()` 等 |
+| 10 | `service/impl/DimensionScoreServiceImpl.java` | ⚠️ 核心重写:新7维、百分位引擎、Tier优先级、菌群映射 |
+| 11 | `controller/HealthDimensionController.java` | 适配新DTO,新增菌群刷新端点 |
+| 12 | `dto/HealthDimensionVO.java` | 增 `percentile`/`dataSource`/`tier`/`expireDate` |
+| 13 | `dto/DimensionUploadVO.java` | 增 `sourceType`/`confidence` |
+| 14 | `dto/HealthDimensionQuestionnaireVO.java` | 不变(结构通用) |
+| 15 | `dto/DimensionScoreVO.java` | 更新默认7维列表 |
+| 16 | `config/DatabaseInitializer.java` | ⚠️ 更新DDL(3表)+ 新增7维常模种子数据 |
+
+### Modified Frontend Files
+| # | File | Change |
+|:-:|------|--------|
+| 17 | `pages/body/health-dimensions.vue` | 7维标签更换 + 数据源图标 + 问卷入口 |
+
+---
+
+## Parallel Execution Waves
+
+```
+Wave 1 (DB + Entity — 3 parallel tasks, depends on nothing):
+├── Task 1: Database migration (更新3张表DDL + 常模种子数据)
+├── Task 2: 更新3个实体类
+└── Task 3: 更新3个Mapper
+
+Wave 2 (Service Core — 3 parallel tasks, depends on Wave 1):
+├── Task 4: HealthDimensionScoreService CRUD + HealthDataSourceRecordService 更新
+├── Task 5: DimensionScoreService 核心聚合器重构 (新7维 + 百分位 + Tier优先级)
+└── Task 6: 菌群报告→维度映射 (PdfParseService 集成)
+
+Wave 3 (API + DTO — 2 parallel tasks, depends on Wave 2):
+├── Task 7: 更新DTO对象
+└── Task 8: 更新Controller (适配新DTO + 新增refresh端点)
+
+Wave 4 (Frontend — depends on Wave 3):
+└── Task 9: 更新前端页面
+
+Wave FINAL (Verification):
+├── F1: mvn clean compile
+├── F2: 启动验证 + API curl测试
+├── F3: 前端构建验证
+└── F4: 设计文档合规性检查
+```
+
+---
+
+## TODOs
+
+### Task 1: 数据库迁移(更新3张表 + 常模种子数据)
+
+**Files:** `config/DatabaseInitializer.java`
+
+**Note:** 现有表 `health_dimension_score` 和 `health_data_source_record` 已有生产数据。迁移策略:
+1. 创建新表(新名字/新结构)
+2. 迁移旧数据(将 `is_latest=1` 的记录映射到新表)
+3. 保留旧表不动(防止数据丢失),仅在新表上工作
+
+**注意:** `DatabaseInitializer.java` 使用 `jdbcTemplate.execute()` 和 `migrate("V...", () -> { ... })` 模式。新迁移加在 `migrateHealthDimensionsTable()` 方法中。
+
+- [ ] **Step 1: 创建 `health_dimension_scores` 新表(在 `migrateHealthDimensionsTable()` 前添加新迁移)**
+
+```java
+// 在 DatabaseInitializer.java 的 migrateHealthDimensionsTable() 调用之前,添加一个新的 migrate 调用:
+
+migrate("V20260626_01__create_health_dimension_scores", () -> {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `health_dimension_scores` (\n" +
+        "  `id` bigint NOT NULL AUTO_INCREMENT,\n" +
+        "  `member_id` bigint NOT NULL COMMENT '家庭成员ID',\n" +
+        "  `dimension` varchar(20) NOT NULL COMMENT '维度: growth/sleep/vision/immunity/nutrition/gut/exercise',\n" +
+        "  `score` int NOT NULL COMMENT '0-100分',\n" +
+        "  `percentile` int DEFAULT NULL COMMENT '人群百分位 0-100',\n" +
+        "  `data_source` varchar(20) NOT NULL COMMENT '数据源: REPORT/PHOTO/VOICE/MANUAL',\n" +
+        "  `tier` tinyint NOT NULL DEFAULT 3 COMMENT '数据层级: 1自动/2语音/3手动',\n" +
+        "  `raw_data` json DEFAULT NULL COMMENT '原始数据快照',\n" +
+        "  `assess_date` date NOT NULL COMMENT '评估日期',\n" +
+        "  `expire_date` date DEFAULT NULL COMMENT '数据过期日',\n" +
+        "  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n" +
+        "  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n" +
+        "  PRIMARY KEY (`id`),\n" +
+        "  UNIQUE KEY `uk_member_dim_date` (`member_id`, `dimension`, `assess_date`)\n" +
+        ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康维度评分表'");
+    log.info("health_dimension_scores 表创建完成");
+    return true;
+});
+```
+
+- [ ] **Step 2: 创建 `health_data_sources` 新表**
+
+```java
+migrate("V20260626_02__create_health_data_sources", () -> {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `health_data_sources` (\n" +
+        "  `id` bigint NOT NULL AUTO_INCREMENT,\n" +
+        "  `member_id` bigint NOT NULL,\n" +
+        "  `source_type` varchar(20) NOT NULL COMMENT 'PDF/PHOTO/VOICE/MANUAL/WEIXIN_STEP',\n" +
+        "  `dimension_codes` json NOT NULL COMMENT '影响的维度列表',\n" +
+        "  `parsed_result` json DEFAULT NULL COMMENT '解析结果全量',\n" +
+        "  `file_url` varchar(500) DEFAULT NULL COMMENT '原始文件URL',\n" +
+        "  `confidence` decimal(3,2) DEFAULT NULL COMMENT 'AI解析置信度',\n" +
+        "  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n" +
+        "  PRIMARY KEY (`id`),\n" +
+        "  KEY `idx_member_source` (`member_id`, `source_type`)\n" +
+        ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康数据源记录表'");
+    log.info("health_data_sources 表创建完成");
+    return true;
+});
+```
+
+- [ ] **Step 3: 重构 `health_norm_reference` 表(修改表结构为百分位模式)**
+
+```java
+migrate("V20260626_03__recreate_health_norm_reference", () -> {
+    jdbcTemplate.execute("DROP TABLE IF EXISTS `health_norm_reference_old`");
+    jdbcTemplate.execute("RENAME TABLE `health_norm_reference` TO `health_norm_reference_old`");
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `health_norm_reference` (\n" +
+        "  `id` bigint NOT NULL AUTO_INCREMENT,\n" +
+        "  `dimension` varchar(20) NOT NULL COMMENT '维度',\n" +
+        "  `gender` varchar(10) NOT NULL COMMENT 'male/female/all',\n" +
+        "  `age_min` int NOT NULL COMMENT '年龄下限(月)',\n" +
+        "  `age_max` int NOT NULL COMMENT '年龄上限(月)',\n" +
+        // 所有维度的 percentile_X 统一为 0-100 评分(与 health_dimension_scores.score 同尺度)
+        "  `percentile_5` int DEFAULT NULL,\n" +
+        "  `percentile_15` int DEFAULT NULL,\n" +
+        "  `percentile_25` int DEFAULT NULL,\n" +
+        "  `percentile_50` int DEFAULT NULL COMMENT '中位值',\n" +
+        "  `percentile_75` int DEFAULT NULL,\n" +
+        "  `percentile_85` int DEFAULT NULL,\n" +
+        "  `percentile_95` int DEFAULT NULL,\n" +
+        "  `source` varchar(50) DEFAULT NULL COMMENT '常模来源',\n" +
+        "  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n" +
+        "  PRIMARY KEY (`id`),\n" +
+        "  UNIQUE KEY `uk_dim_gender_age` (`dimension`, `gender`, `age_min`, `age_max`)\n" +
+        ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康常模对照表'");
+    log.info("health_norm_reference 表重建完成");
+    return true;
+});
+```
+
+- [ ] **Step 4: 插入7维常模种子数据(修改 `seedHealthNorms()` 方法)**
+
+```java
+    // 替换现有的 seedHealthNorms() 方法体
+    // 注意:所有 percentile_X 字段的单位统一为 0-100 评分(与 health_dimension_scores.score 同尺度),
+    // 这样 calcPercentile() 可以直接用 score 与常模比较,无需单位换算。
+    // 以下数据基于国家标准/行业参考的人群分布,转换为百分制评分。
+    private void seedHealthNorms() {
+        try {
+            int count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM health_norm_reference", Integer.class);
+            if (count == 0) {
+                // 生长发育 (growth) — 基于BMI-for-age分布映射到0-100评分
+                jdbcTemplate.execute("INSERT INTO health_norm_reference (dimension, gender, age_min, age_max, percentile_5, percentile_15, percentile_25, percentile_50, percentile_75, percentile_85, percentile_95, source) VALUES\n" +
+                    "('growth', 'male', 72, 84, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
+                    "('growth', 'female', 72, 84, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
+                    "('growth', 'male', 84, 96, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
+                    "('growth', 'female', 84, 96, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
+                    "('growth', 'male', 96, 120, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
+                    "('growth', 'female', 96, 120, 25, 35, 45, 55, 70, 80, 90, 'BMI-for-age→百分制'),\n" +
+                    // 睡眠质量 (sleep) — 时长分布→百分制评分
+                    "('sleep', 'all', 0, 12, 40, 50, 60, 70, 80, 88, 95, 'NSF婴幼儿睡眠→百分制'),\n" +
+                    "('sleep', 'all', 12, 36, 40, 50, 60, 70, 80, 88, 95, 'NSF幼儿睡眠→百分制'),\n" +
+                    "('sleep', 'all', 36, 72, 40, 50, 60, 70, 80, 88, 95, 'NSF学龄前睡眠→百分制'),\n" +
+                    "('sleep', 'all', 72, 156, 40, 50, 60, 70, 80, 88, 95, 'NSF学龄儿童睡眠→百分制'),\n" +
+                    // 视力健康 (vision) — 对数视力→百分制评分
+                    "('vision', 'male', 72, 144, 45, 55, 65, 75, 85, 90, 95, '视力发育标准→百分制'),\n" +
+                    "('vision', 'female', 72, 144, 45, 55, 65, 75, 85, 90, 95, '视力发育标准→百分制'),\n" +
+                    // 免疫力 (immunity) — 年感冒次数→百分制评分(次数越少分越高)
+                    "('immunity', 'all', 0, 72, 15, 30, 45, 65, 80, 90, 95, '年患病次数→百分制'),\n" +
+                    "('immunity', 'all', 72, 192, 15, 35, 50, 70, 85, 92, 98, '年患病次数→百分制'),\n" +
+                    // 营养均衡 (nutrition) — 膳食多样性评分→百分制
+                    "('nutrition', 'all', 36, 192, 20, 30, 40, 50, 60, 70, 80, '膳食多样性→百分制'),\n" +
+                    // 肠胃健康 (gut) — 菌群健康度评分(已是0-100)
+                    "('gut', 'all', 0, 192, 40, 50, 60, 70, 80, 85, 95, '菌群人群分布(平台自建)'),\n" +
+                    // 运动活力 (exercise) — 体质健康标准(已是0-100)
+                    "('exercise', 'male', 72, 144, 30, 40, 50, 60, 70, 80, 90, '国家学生体质健康标准'),\n" +
+                    "('exercise', 'female', 72, 144, 25, 35, 45, 55, 65, 75, 85, '国家学生体质健康标准')");
+            log.info("health_norm_reference 常模种子数据已加载 (18条, 统一0-100百分制)");
+        } else {
+            log.info("health_norm_reference 常模数据已存在 ({}条), 跳过", count);
+        }
+    } catch (Exception e) {
+        log.warn("health_norm_reference 常模种子数据初始化失败: {}", e.getMessage());
+    }
+}
+```
+
+- [ ] **Step 5: 数据迁移 — 将旧 `health_dimension_score` 数据迁移到新表**
+
+```java
+migrate("V20260626_04__migrate_old_dimension_scores", () -> {
+    // 检查旧表是否有数据
+    Integer oldCount = jdbcTemplate.queryForObject(
+        "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'health_dimension_score'",
+        Integer.class);
+    if (oldCount != null && oldCount > 0) {
+        // 将旧表数据映射到新表(维度code映射)
+        jdbcTemplate.execute("INSERT IGNORE INTO health_dimension_scores (member_id, dimension, score, data_source, tier, assess_date, created_at)\n" +
+            " SELECT member_id,\n" +
+            "  CASE dimension\n" +
+            "    WHEN 'body' THEN 'growth'\n" +
+            "    WHEN 'fitness' THEN 'exercise'\n" +
+            "    WHEN 'cardio' THEN 'exercise'\n" +
+            "    WHEN 'blood' THEN 'immunity'\n" +
+            "    WHEN 'sleep' THEN 'sleep'\n" +
+            "    WHEN 'psyche' THEN 'growth'\n" +
+            "    WHEN 'diet' THEN 'nutrition'\n" +
+            "    ELSE dimension\n" +
+            "  END,\n" +
+            "  score,\n" +
+            "  UPPER(data_source),\n" +
+            "  3,\n" +
+            "  record_date,\n" +
+            "  NOW()\n" +
+            " FROM health_dimension_score WHERE is_latest = 1");
+        log.info("旧维度评分数据迁移完成");
+    }
+    return true;
+});
+```
+
+- [ ] **Step 6: 编译验证**
+
+```bash
+export PATH=$HOME/.local/bin:$PATH
+cd /sc-data/cfc/cfc-backend && rtk mvn clean compile -q 2>&1 | tail -20
+```
+
+- [ ] **Step 7: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
+git commit -m "feat(db): migrate 7-dim health tables to design spec schema + norm seed data"
+```
+
+---
+
+### Task 2: 更新3个实体类
+
+**Files:**
+- Modify: `entity/HealthDimensionScore.java`
+- Modify: `entity/HealthNormReference.java`
+- Modify: `entity/HealthDataSourceRecord.java`
+
+- [ ] **Step 1: 重写 `HealthDimensionScore.java`**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("health_dimension_scores")
+public class HealthDimensionScore implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long memberId;
+    private String dimension;     // growth/sleep/vision/immunity/nutrition/gut/exercise
+    private Integer score;        // 0-100
+    private Integer percentile;   // 人群百分位 0-100
+    private String dataSource;    // REPORT/PHOTO/VOICE/MANUAL
+    private Integer tier;         // 1/2/3
+    private String rawData;       // JSON
+    private Date assessDate;
+    private Date expireDate;
+    private Date createdAt;
+    private Date updatedAt;
+}
+```
+
+- [ ] **Step 2: 重写 `HealthNormReference.java`**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("health_norm_reference")
+public class HealthNormReference implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String dimension;
+    private String gender;       // male/female/all
+    private Integer ageMin;      // 月
+    private Integer ageMax;      // 月
+    private Integer percentile5;
+    private Integer percentile15;
+    private Integer percentile25;
+    private Integer percentile50;
+    private Integer percentile75;
+    private Integer percentile85;
+    private Integer percentile95;
+    private String source;
+    private Date createdAt;
+}
+```
+
+- [ ] **Step 3: 重写 `HealthDataSourceRecord.java`**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("health_data_sources")
+public class HealthDataSourceRecord implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long memberId;
+    private String sourceType;       // PDF/PHOTO/VOICE/MANUAL/WEIXIN_STEP
+    private String dimensionCodes;   // JSON array
+    private String parsedResult;     // JSON
+    private String fileUrl;
+    private BigDecimal confidence;   // 0.00-1.00
+    private Date createdAt;
+}
+```
+
+- [ ] **Step 4: 编译验证**
+
+```bash
+cd /sc-data/cfc/cfc-backend && rtk mvn clean compile -q 2>&1 | tail -20
+```
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/
+git commit -m "feat(entity): remodel 3 health dimension entities to match design spec"
+```
+
+---
+
+### Task 3: 更新3个Mapper
+
+**Files:**
+- Modify: `mapper/HealthDimensionScoreMapper.java`
+- Modify: `mapper/HealthNormReferenceMapper.java`
+- Modify: `mapper/HealthDataSourceRecordMapper.java`
+
+- [ ] **Step 1: 重写 `HealthDimensionScoreMapper.java`**
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.HealthDimensionScore;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import java.util.List;
+
+@Mapper
+public interface HealthDimensionScoreMapper extends BaseMapper<HealthDimensionScore> {
+
+    // 取某个member最新的7维评分(每个维度最新assess_date的一条)
+    @Select("SELECT h.* FROM health_dimension_scores h " +
+        "INNER JOIN (SELECT dimension, MAX(assess_date) max_date FROM health_dimension_scores " +
+        "WHERE member_id = #{memberId} GROUP BY dimension) latest " +
+        "ON h.dimension = latest.dimension AND h.assess_date = latest.max_date " +
+        "WHERE h.member_id = #{memberId}")
+    List<HealthDimensionScore> selectLatestByMember(@Param("memberId") Long memberId);
+
+    // 取某member某维度的最新评分
+    @Select("SELECT * FROM health_dimension_scores WHERE member_id = #{memberId} " +
+        "AND dimension = #{dimension} ORDER BY assess_date DESC LIMIT 1")
+    HealthDimensionScore selectLatest(@Param("memberId") Long memberId, @Param("dimension") String dimension);
+
+    // 取某member某维度的历史评分
+    @Select("SELECT * FROM health_dimension_scores WHERE member_id = #{memberId} " +
+        "AND dimension = #{dimension} ORDER BY assess_date DESC LIMIT #{limit}")
+    List<HealthDimensionScore> selectHistory(@Param("memberId") Long memberId,
+        @Param("dimension") String dimension, @Param("limit") int limit);
+
+    // 删除现有迁移方法中用不到 `updateExpired`,新表用 UK 机制保证唯一性
+}
+```
+
+- [ ] **Step 2: 重写 `HealthNormReferenceMapper.java`**
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.HealthNormReference;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+@Mapper
+public interface HealthNormReferenceMapper extends BaseMapper<HealthNormReference> {
+
+    // 根据维度+性别+年龄查到对应的常模行(按年龄范围匹配)
+    @Select("SELECT * FROM health_norm_reference WHERE dimension = #{dimension} " +
+        "AND (gender = #{gender} OR gender = 'all') " +
+        "AND age_min <= #{ageMonths} AND age_max > #{ageMonths} " +
+        "ORDER BY gender DESC LIMIT 1")
+    HealthNormReference selectNorm(@Param("dimension") String dimension,
+        @Param("gender") String gender, @Param("ageMonths") Integer ageMonths);
+}
+```
+
+- [ ] **Step 3: 重写 `HealthDataSourceRecordMapper.java`**
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.HealthDataSourceRecord;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import java.util.List;
+
+@Mapper
+public interface HealthDataSourceRecordMapper extends BaseMapper<HealthDataSourceRecord> {
+
+    @Select("SELECT * FROM health_data_sources WHERE member_id = #{memberId} " +
+        "AND source_type = #{sourceType} ORDER BY created_at DESC LIMIT 1")
+    HealthDataSourceRecord selectLatestBySource(@Param("memberId") Long memberId,
+        @Param("sourceType") String sourceType);
+
+    @Select("SELECT * FROM health_data_sources WHERE member_id = #{memberId} " +
+        "ORDER BY created_at DESC")
+    List<HealthDataSourceRecord> selectByMember(@Param("memberId") Long memberId);
+
+    @Select("SELECT * FROM health_data_sources WHERE member_id = #{memberId} " +
+        "AND JSON_CONTAINS(dimension_codes, #{dimensionCode}) " +
+        "ORDER BY created_at DESC LIMIT #{limit}")
+    List<HealthDataSourceRecord> selectByDimension(@Param("memberId") Long memberId,
+        @Param("dimensionCode") String dimensionCode, @Param("limit") int limit);
+}
+```
+
+- [ ] **Step 4: 编译验证**
+
+```bash
+cd /sc-data/cfc/cfc-backend && rtk mvn clean compile -q 2>&1 | tail -20
+```
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/mapper/
+git commit -m "feat(mapper): update 3 health dimension mappers for new schema"
+```
+
+---
+
+### Task 4: 更新 HealthDimensionScoreService + HealthDataSourceRecordService
+
+**Files:**
+- Modify: `service/HealthDimensionScoreService.java`
+- Modify: `service/impl/HealthDimensionScoreServiceImpl.java`
+
+- [ ] **Step 1: 更新 `HealthDimensionScoreService.java` 接口**
+
+```java
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.HealthDimensionScore;
+import java.util.List;
+
+public interface HealthDimensionScoreService {
+    void saveScore(HealthDimensionScore score);
+    List<HealthDimensionScore> getLatestScores(Long memberId);
+    HealthDimensionScore getLatestScore(Long memberId, String dimension);
+    List<HealthDimensionScore> getHistoryScores(Long memberId, String dimension, int limit);
+}
+```
+
+- [ ] **Step 2: 更新 `HealthDimensionScoreServiceImpl.java`**
+
+```java
+package com.etotem.cfc.service.impl;
+
+import com.etotem.cfc.entity.HealthDimensionScore;
+import com.etotem.cfc.mapper.HealthDimensionScoreMapper;
+import com.etotem.cfc.service.HealthDimensionScoreService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import javax.annotation.Resource;
+import java.util.List;
+
+@Service
+public class HealthDimensionScoreServiceImpl implements HealthDimensionScoreService {
+
+    @Resource
+    private HealthDimensionScoreMapper scoreMapper;
+
+    @Override
+    @Transactional
+    public void saveScore(HealthDimensionScore score) {
+        scoreMapper.insert(score);
+    }
+
+    @Override
+    public List<HealthDimensionScore> getLatestScores(Long memberId) {
+        return scoreMapper.selectLatestByMember(memberId);
+    }
+
+    @Override
+    public HealthDimensionScore getLatestScore(Long memberId, String dimension) {
+        return scoreMapper.selectLatest(memberId, dimension);
+    }
+
+    @Override
+    public List<HealthDimensionScore> getHistoryScores(Long memberId, String dimension, int limit) {
+        return scoreMapper.selectHistory(memberId, dimension, limit);
+    }
+}
+```
+
+- [ ] **Step 3: 编译验证**
+
+```bash
+cd /sc-data/cfc/cfc-backend && rtk mvn clean compile -q 2>&1 | tail -20
+```
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/HealthDimensionScoreService.java
+git add cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthDimensionScoreServiceImpl.java
+git commit -m "feat(service): update HealthDimensionScoreService for new entity"
+```
+
+---
+
+### Task 5: DimensionScoreService 核心聚合器重构
+
+**Files:**
+- Modify: `service/DimensionScoreService.java`
+- Modify: `service/impl/DimensionScoreServiceImpl.java`
+
+这是整个重构的核心。需要实现:
+1. 7维列表从旧 → 新
+2. 百分位计算引擎(原始分 → 查常模 → 百分位)
+3. 数据源Tier优先级合并
+4. 健康指数计算
+
+- [ ] **Step 1: 更新 `DimensionScoreService.java` 接口**
+
+```java
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.dto.HealthDimensionVO;
+import com.etotem.cfc.dto.DimensionUploadVO;
+import com.etotem.cfc.dto.HealthDimensionQuestionnaireVO;
+import java.util.List;
+
+public interface DimensionScoreService {
+    /** 获取某member的7维概览(含百分位) */
+    HealthDimensionVO getHealthDimension(Long memberId);
+
+    /** 手动上传单维度评分 */
+    void uploadScore(DimensionUploadVO vo);
+
+    /** 问卷提交(答案→均分→维度评分) */
+    void submitQuestionnaire(HealthDimensionQuestionnaireVO vo);
+
+    /** 获取维度历史 */
+    List<HealthDimensionVO.DimensionItem> getDimensionHistory(Long memberId, String dimension, int limit);
+
+    /** 菌群报告解析后刷新维度(核心映射逻辑) */
+    void refreshFromGutReport(Long memberId, Long reportId);
+}
+```
+
+- [ ] **Step 2: 重写 `DimensionScoreServiceImpl.java`**
+
+```java
+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.entity.HealthDimensionScore;
+import com.etotem.cfc.entity.HealthDataSourceRecord;
+import com.etotem.cfc.entity.HealthGutFlora;
+import com.etotem.cfc.entity.HealthIndicator;
+import com.etotem.cfc.entity.HealthDiseaseRisk;
+import com.etotem.cfc.entity.HealthNormReference;
+import com.etotem.cfc.entity.HealthReport;
+import com.etotem.cfc.mapper.HealthNormReferenceMapper;
+import com.etotem.cfc.service.DimensionScoreService;
+import com.etotem.cfc.service.HealthDimensionScoreService;
+import com.etotem.cfc.service.HealthReportService;
+import com.etotem.cfc.service.HealthDataSourceRecordService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+public class DimensionScoreServiceImpl implements DimensionScoreService {
+
+    // === 设计文档规范的7维 ===
+    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",  "运动活力"}
+    );
+
+    private static final Map<String, Integer> DIMENSION_TIER = new HashMap<>();
+    static {
+        // Tier 1: 报告/自动数据
+        DIMENSION_TIER.put("growth", 1);
+        DIMENSION_TIER.put("sleep", 1);
+        DIMENSION_TIER.put("vision", 1);
+        DIMENSION_TIER.put("immunity", 1);
+        DIMENSION_TIER.put("nutrition", 1);
+        DIMENSION_TIER.put("gut", 1);
+        DIMENSION_TIER.put("exercise", 1);
+    }
+
+    @Resource
+    private HealthDimensionScoreService scoreService;
+
+    @Resource
+    private HealthNormReferenceMapper normMapper;
+
+    @Resource
+    private HealthReportService healthReportService;
+
+    @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));
+
+        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);
+
+            HealthDimensionVO.DimensionItem item = new HealthDimensionVO.DimensionItem();
+            item.setDimension(code);
+            item.setLabel(label);
+
+            if (found != null) {
+                item.setScore(BigDecimal.valueOf(found.getScore()));
+                item.setPercentile(found.getPercentile());
+                item.setDataSource(found.getDataSource());
+                item.setTier(found.getTier());
+                item.setLevel(calcLevel(found.getScore()));
+                item.setTrend("stable");
+                if (found.getExpireDate() != null) {
+                    item.setExpireDate(new SimpleDateFormat("yyyy-MM-dd").format(found.getExpireDate()));
+                }
+                totalScore = totalScore.add(BigDecimal.valueOf(found.getScore()));
+                validCount++;
+            } else {
+                item.setScore(null);
+                item.setLevel("none");
+                item.setTrend(null);
+            }
+            items.add(item);
+        }
+
+        HealthDimensionVO vo = new HealthDimensionVO();
+        vo.setMemberId(memberId);
+        vo.setHealthIndex(validCount > 0
+            ? totalScore.divide(BigDecimal.valueOf(validCount), 2, RoundingMode.HALF_UP) : null);
+        vo.setDimensions(items);
+        vo.setUpdateTime(new Date());
+        return vo;
+    }
+
+    @Override
+    @Transactional
+    public void uploadScore(DimensionUploadVO vo) {
+        HealthDimensionScore score = new HealthDimensionScore();
+        score.setMemberId(vo.getMemberId());
+        score.setDimension(vo.getDimension());
+        score.setScore(vo.getScore() != null ? vo.getScore().intValue() : 0);
+        score.setDataSource(vo.getSourceType() != null ? vo.getSourceType() : "MANUAL");
+        score.setTier(3); // 手动上传默认 Tier 3
+        if (vo.getRecordDate() != null) {
+            score.setAssessDate(java.sql.Date.valueOf(vo.getRecordDate()));
+        } else {
+            score.setAssessDate(new java.sql.Date(System.currentTimeMillis()));
+        }
+        // 计算百分位
+        score.setPercentile(calcPercentile(vo.getMemberId(), vo.getDimension(),
+            vo.getScore() != null ? vo.getScore().intValue() : 0));
+        scoreService.saveScore(score);
+    }
+
+    @Override
+    @Transactional
+    public void submitQuestionnaire(HealthDimensionQuestionnaireVO vo) {
+        BigDecimal total = BigDecimal.ZERO;
+        int count = 0;
+        for (HealthDimensionQuestionnaireVO.QuestionAnswer qa : vo.getAnswers()) {
+            if (qa.getScore() != null) {
+                total = total.add(qa.getScore());
+                count++;
+            }
+        }
+        if (count == 0) return;
+        int avgScore = total.divide(BigDecimal.valueOf(count), 0, RoundingMode.HALF_UP).intValue();
+
+        HealthDimensionScore score = new HealthDimensionScore();
+        score.setMemberId(vo.getMemberId());
+        score.setDimension(vo.getDimension());
+        score.setScore(avgScore);
+        score.setDataSource("MANUAL");
+        score.setTier(3);
+        score.setAssessDate(new java.sql.Date(System.currentTimeMillis()));
+        score.setPercentile(calcPercentile(vo.getMemberId(), vo.getDimension(), avgScore));
+        scoreService.saveScore(score);
+    }
+
+    @Override
+    public List<HealthDimensionVO.DimensionItem> getDimensionHistory(Long memberId, String dimension, int limit) {
+        List<HealthDimensionScore> history = scoreService.getHistoryScores(memberId, dimension, limit);
+        List<HealthDimensionVO.DimensionItem> items = new ArrayList<>();
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        for (HealthDimensionScore s : history) {
+            HealthDimensionVO.DimensionItem item = new HealthDimensionVO.DimensionItem();
+            item.setDimension(s.getDimension());
+            item.setScore(BigDecimal.valueOf(s.getScore()));
+            item.setPercentile(s.getPercentile());
+            item.setDataSource(s.getDataSource());
+            item.setTier(s.getTier());
+            item.setLevel(calcLevel(s.getScore()));
+            item.setRecordDate(sdf.format(s.getAssessDate()));
+            items.add(item);
+        }
+        return items;
+    }
+
+    @Override
+    @Transactional
+    public void refreshFromGutReport(Long memberId, Long reportId) {
+        HealthReport report = healthReportService.getReportById(reportId);
+        if (report == null) return;
+
+        List<HealthGutFlora> gutFlora = healthReportService.getGutFloraByReportId(reportId);
+        List<HealthIndicator> indicators = healthReportService.getReportIndicators(reportId);
+        List<HealthDiseaseRisk> diseaseRisks = healthReportService.getDiseaseRisksByReportId(reportId);
+
+        Map<String, Integer> dimScores = mapGutFloraToDimensions(report, gutFlora, indicators, diseaseRisks);
+
+        // 写入维度评分(Tier 1: 报告数据)
+        Date assessDate = report.getReportDate() != null
+            ? report.getReportDate() : new java.sql.Date(System.currentTimeMillis());
+        for (Map.Entry<String, Integer> entry : dimScores.entrySet()) {
+            HealthDimensionScore score = new HealthDimensionScore();
+            score.setMemberId(memberId);
+            score.setDimension(entry.getKey());
+            score.setScore(entry.getValue());
+            score.setPercentile(calcPercentile(memberId, entry.getKey(), entry.getValue()));
+            score.setDataSource("REPORT");
+            score.setTier(1);
+            score.setAssessDate(assessDate);
+            // 菌群报告有效期6个月
+            Calendar cal = Calendar.getInstance();
+            cal.setTime(assessDate);
+            cal.add(Calendar.MONTH, 6);
+            score.setExpireDate(cal.getTime());
+            score.setRawData("{\"reportId\":" + reportId + "}");
+            scoreService.saveScore(score);
+        }
+    }
+
+    /**
+     * 根据设计文档的映射矩阵,将已有健康报告数据映射到7维评分。
+     *
+     * 数据来源:
+     * - HealthReport 本身的评分子段: gutHealthScore, chronicDiseaseScore, nutritionScore,
+     *   gutBalanceScore, gutDiversityScore, beneficialBacteriaScore, harmfulBacteriaScore, coreSpeciesScore
+     * - HealthIndicator(indicators): category + indicatorValue(需解析String→数值)
+     * - HealthDiseaseRisk(diseaseRisks): riskValue(String)/riskLevel → 反向分
+     * - HealthGutFlora(gutFlora): populationLevel(String, e.g."35%") 作为人群百分位参考
+     *
+     * 映射矩阵参照设计文档 3.2 节。
+     */
+    private Map<String, Integer> mapGutFloraToDimensions(
+            HealthReport report,
+            List<HealthGutFlora> gutFlora,
+            List<HealthIndicator> indicators,
+            List<HealthDiseaseRisk> diseaseRisks) {
+
+        Map<String, List<Integer>> dimValues = new HashMap<>();
+        for (String[] dim : DIMENSIONS) {
+            dimValues.put(dim[0], new ArrayList<>());
+        }
+
+        // === 1. 从 HealthReport 直接取评分字段 ===
+        // gutHealthScore → immunity, gut
+        if (report.getGutHealthScore() != null) {
+            dimValues.get("immunity").add(report.getGutHealthScore());
+            dimValues.get("gut").add(report.getGutHealthScore());
+        }
+        // chronicDiseaseScore → growth, nutrition(反转:分越高越健康)
+        if (report.getChronicDiseaseScore() != null) {
+            int inverted = 100 - report.getChronicDiseaseScore();
+            dimValues.get("growth").add(inverted);
+            dimValues.get("nutrition").add(inverted);
+        }
+        // nutritionScore → nutrition
+        if (report.getNutritionScore() != null) {
+            dimValues.get("nutrition").add(report.getNutritionScore());
+        }
+        // gutBalanceScore → gut
+        if (report.getGutBalanceScore() != null) {
+            dimValues.get("gut").add(report.getGutBalanceScore());
+        }
+        // gutDiversityScore → immunity, gut
+        if (report.getGutDiversityScore() != null) {
+            dimValues.get("immunity").add(report.getGutDiversityScore());
+            dimValues.get("gut").add(report.getGutDiversityScore());
+        }
+        // beneficialBacteriaScore → immunity, gut
+        if (report.getBeneficialBacteriaScore() != null) {
+            dimValues.get("immunity").add(report.getBeneficialBacteriaScore());
+            dimValues.get("gut").add(report.getBeneficialBacteriaScore());
+        }
+        // harmfulBacteriaScore → immunity, gut(反转)
+        if (report.getHarmfulBacteriaScore() != null) {
+            int inverted = 100 - report.getHarmfulBacteriaScore();
+            dimValues.get("immunity").add(inverted);
+            dimValues.get("gut").add(inverted);
+        }
+        // coreSpeciesScore → gut
+        if (report.getCoreSpeciesScore() != null) {
+            dimValues.get("gut").add(report.getCoreSpeciesScore());
+        }
+        // gutAge → sleep, gut(解析字符串,如 "35岁" → 数值映射)
+        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);
+            } catch (NumberFormatException ignored) {}
+        }
+
+        // === 2. 从 HealthIndicator 取(category分类)===
+        for (HealthIndicator ind : indicators) {
+            String cat = ind.getCategory();
+            String rawVal = ind.getIndicatorValue();
+            if (cat == null || rawVal == null) continue;
+
+            int val = tryParseNumericIndicator(rawVal);
+            if (val < 0) continue; // 无法解析为数值
+
+            if ("神经递质与激素".contains(cat != null ? cat : "")) {
+                dimValues.get("sleep").add(val);
+            } else if ("短链脂肪酸".equals(cat)) {
+                dimValues.get("nutrition").add(val);
+                dimValues.get("gut").add(val);
+            } else if ("肠道屏障".equals(cat)) {
+                dimValues.get("immunity").add(val);
+                dimValues.get("gut").add(val);
+            } else if ("抗生素耐药".equals(cat)) {
+                // 耐药分越低越好 → 反转
+                dimValues.get("immunity").add(100 - Math.min(val, 100));
+            } else if (cat != null && (cat.contains("维生素") || cat.contains("矿物质") || cat.contains("微量"))) {
+                dimValues.get("nutrition").add(val);
+            }
+        }
+
+        // === 3. 从 HealthDiseaseRisk 取 ===
+        for (HealthDiseaseRisk risk : diseaseRisks) {
+            if (risk.getRiskValue() == null) continue;
+            int riskVal = tryParseNumericIndicator(risk.getRiskValue());
+            if (riskVal < 0) {
+                // 用 riskLevel 映射
+                if ("低风险".equals(risk.getRiskLevel())) riskVal = 20;
+                else if ("需注意".equals(risk.getRiskLevel())) riskVal = 50;
+                else if ("高风险".equals(risk.getRiskLevel())) riskVal = 80;
+                else continue;
+            }
+            int invertedScore = 100 - Math.min(riskVal, 100);
+            // 慢性病/代谢类风险 → growth, nutrition
+            String name = risk.getDiseaseName();
+            if (name != null && (name.contains("代谢") || name.contains("肥胖") || name.contains("超重"))) {
+                dimValues.get("growth").add(invertedScore);
+                dimValues.get("nutrition").add(invertedScore);
+            }
+            // 感染类风险 → immunity
+            if (name != null && (name.contains("感染") || name.contains("炎症"))) {
+                dimValues.get("immunity").add(invertedScore);
+            }
+            // 其他风险都影响肠胃
+            dimValues.get("gut").add(invertedScore);
+        }
+
+        // === 4. 从 HealthGutFlora 取 populationLevel(作为百分位参考,不直接用于打分)===
+        // populationLevel 如 "35%" 表示该菌种处于人群35%水平
+        // 这里暂不直接用,留给后续百分位校准
+
+        // 聚合:每个维度取平均值
+        Map<String, Integer> result = new HashMap<>();
+        for (String[] dim : DIMENSIONS) {
+            List<Integer> vals = dimValues.get(dim[0]);
+            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)));
+            }
+        }
+        return result;
+    }
+
+    /** 尝试从字符串解析数值,失败返回 -1 */
+    private int tryParseNumericIndicator(String val) {
+        if (val == null) return -1;
+        try {
+            String cleaned = val.replaceAll("[^0-9.]", "").trim();
+            if (cleaned.isEmpty()) return -1;
+            return (int) Double.parseDouble(cleaned);
+        } catch (NumberFormatException e) {
+            return -1;
+        }
+    }
+
+    /**
+     * 百分位计算:根据原始分(0-100) + 性别年龄 → 查常模 → 返回百分位
+     *
+     * 常模表 health_norm_reference 的 percentile_X 字段统一为 0-100 评分,
+     * 与 health_dimension_scores.score 同尺度,可直接整数比较。
+     */
+    private Integer calcPercentile(Long memberId, String dimension, int score) {
+        // TODO: 需要从用户资料读取性别和年龄(月)
+        // 这里简化实现,使用默认常模
+        HealthNormReference norm = normMapper.selectNorm(dimension, "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;
+        if (norm.getPercentile75() != null && score >= norm.getPercentile75()) return 75;
+        if (norm.getPercentile50() != null && score >= norm.getPercentile50()) return 50;
+        if (norm.getPercentile25() != null && score >= norm.getPercentile25()) return 25;
+        if (norm.getPercentile15() != null && score >= norm.getPercentile15()) return 15;
+        if (norm.getPercentile5() != null && score >= norm.getPercentile5()) return 5;
+        return 1;
+    }
+
+    private String calcLevel(int score) {
+        if (score >= 85) return "excellent";
+        if (score >= 60) return "good";
+        if (score >= 40) return "fair";
+        return "poor";
+    }
+}
+```
+
+- [ ] **Step 3: 编译验证**
+
+```bash
+cd /sc-data/cfc/cfc-backend && rtk mvn clean compile -q 2>&1 | tail -20
+```
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/DimensionScoreService.java
+git add cfc-backend/src/main/java/com/etotem/cfc/service/impl/DimensionScoreServiceImpl.java
+git commit -m "feat(service): refactor DimensionScoreService - new 7-dim, percentile engine, gut mapping"
+```
+
+---
+
+### Task 6: 菌群报告映射集成完善
+
+**Files:**
+- Modify: `service/PdfParseService.java` 或新增加方法
+- Modify: 在 `HealthReportController.java` 中添加报告上传后的触发调用
+
+**注意**: PdfParseService 的 `ParsedReportResult` 对象包含 `getGutHealthScore()`、`getChronicDiseaseScore()`、`getNutritionScore()`、`getBalanceScore()`、`getDiversityScore()`、`getBeneficialScore()`、`getHarmfulScore()`、`getCoreGenusScore()` 等字段。已在 `HealthReportController` 的报告上传流程中使用。
+
+需要在报告上传成功创建后,调用 `DimensionScoreServiceImpl.refreshFromGutReport()`。
+
+- [ ] **Step 1: 在 `HealthReportController` 的报告创建端点中,创建完后调用菌群映射**
+
+定位到 `HealthReportController` 中的报告创建端点(`/api/health/report/upload` 或类似),在 `healthReportService.createReport()` 调用之后添加:
+
+```java
+// 触发菌群报告→7维映射
+try {
+    dimensionScoreService.refreshFromGutReport(matchedChildId, created.getId());
+    log.info("已触发菌群报告维度映射: reportId={}, childId={}", created.getId(), matchedChildId);
+} catch (Exception e) {
+    log.warn("菌群报告维度映射失败: {}", e.getMessage());
+}
+```
+
+需要在 Controller 中注入 `DimensionScoreService`:`@Resource private DimensionScoreService dimensionScoreService;`
+
+- [ ] **Step 2: 编译验证**
+
+```bash
+cd /sc-data/cfc/cfc-backend && rtk mvn clean compile -q 2>&1 | tail -20
+```
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java
+git commit -m "feat(integration): trigger gut flora dimension mapping after report upload"
+```
+
+---
+
+### Task 7: 更新DTO对象
+
+**Files:**
+- Modify: `dto/HealthDimensionVO.java`
+- Modify: `dto/DimensionUploadVO.java`
+- Modify: `dto/DimensionScoreVO.java`
+
+- [ ] **Step 1: 更新 `HealthDimensionVO.java`**
+
+```java
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+@Data
+public class HealthDimensionVO {
+    private Long memberId;
+    private BigDecimal healthIndex;      // 综合健康指数
+    private List<DimensionItem> dimensions;
+    private Date updateTime;
+
+    @Data
+    public static class DimensionItem {
+        private String dimension;        // growth/sleep/vision/immunity/nutrition/gut/exercise
+        private String label;            // 中文名
+        private BigDecimal score;        // 0-100
+        private Integer percentile;      // 人群百分位
+        private String dataSource;       // REPORT/PHOTO/VOICE/MANUAL
+        private Integer tier;            // 1/2/3
+        private String level;            // excellent/good/fair/poor/none
+        private String trend;            // up/down/stable
+        private String recordDate;       // 评估日期
+        private String expireDate;       // 过期日期
+    }
+}
+```
+
+- [ ] **Step 2: 更新 `DimensionUploadVO.java`**
+
+```java
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+import java.math.BigDecimal;
+
+@Data
+public class DimensionUploadVO {
+    private Long memberId;
+    private String dimension;
+    private BigDecimal score;
+    private String sourceType;       // REPORT/PHOTO/VOICE/MANUAL
+    private BigDecimal confidence;   // AI置信度
+    private String recordDate;
+}
+```
+
+- [ ] **Step 3: 更新 `DimensionScoreVO.java`**
+
+```java
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+public class DimensionScoreVO {
+    private String dimension;
+    private BigDecimal score;
+    private String label;
+    private String dataSource;
+    private String recordDate;
+
+    public DimensionScoreVO() {}
+
+    public DimensionScoreVO(String dimension, String label, BigDecimal score, String dataSource, String recordDate) {
+        this.dimension = dimension;
+        this.label = label;
+        this.score = score;
+        this.dataSource = dataSource;
+        this.recordDate = recordDate;
+    }
+
+    public static List<DimensionScoreVO> DEFAULT_DIMENSIONS = java.util.Arrays.asList(
+        new DimensionScoreVO("growth", "生长发育", null, null, null),
+        new DimensionScoreVO("sleep", "睡眠质量", null, null, null),
+        new DimensionScoreVO("vision", "视力健康", null, null, null),
+        new DimensionScoreVO("immunity", "免疫力", null, null, null),
+        new DimensionScoreVO("nutrition", "营养均衡", null, null, null),
+        new DimensionScoreVO("gut", "肠胃健康", null, null, null),
+        new DimensionScoreVO("exercise", "运动活力", null, null, null)
+    );
+}
+```
+
+- [ ] **Step 4: 编译验证**
+
+```bash
+cd /sc-data/cfc/cfc-backend && rtk mvn clean compile -q 2>&1 | tail -20
+```
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/
+git commit -m "feat(dto): update DTOs for new 7-dim model and percentile"
+```
+
+---
+
+### Task 8: 更新 Controller
+
+**Files:**
+- Modify: `controller/HealthDimensionController.java`
+
+- [ ] **Step 1: 更新 Controller**
+
+保留现有4个端点,`@RequestMapping("/api/dimension")` 也保持不动(API对外稳定),仅更新 DTO 引用即可。
+
+主要变更:
+1. `upload` 端点:参数验证适配新字段名
+2. 可选:新增统计端点
+
+```java
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.DimensionUploadVO;
+import com.etotem.cfc.dto.HealthDimensionQuestionnaireVO;
+import com.etotem.cfc.dto.HealthDimensionVO;
+import com.etotem.cfc.service.DimensionScoreService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "健康维度", description = "七维健康指标查询与录入(设计文档规范版)")
+@RestController
+@RequestMapping("/api/dimension")
+public class HealthDimensionController {
+
+    @Resource
+    private DimensionScoreService dimensionScoreService;
+
+    @Operation(summary = "获取健康维度概览(7维+百分位)")
+    @PostMapping("/overview")
+    public Result<HealthDimensionVO> overview(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId) {
+        Long memberId = params.get("memberId") != null
+                ? Long.valueOf(params.get("memberId").toString()) : null;
+        if (memberId == null) return Result.error("memberId不能为空");
+        HealthDimensionVO vo = dimensionScoreService.getHealthDimension(memberId);
+        return Result.success(vo);
+    }
+
+    @Operation(summary = "手动录入维度评分")
+    @PostMapping("/upload")
+    public Result<Void> upload(
+            @RequestBody DimensionUploadVO vo,
+            @RequestAttribute("userId") Long userId) {
+        if (vo.getMemberId() == null || vo.getDimension() == null || vo.getScore() == null) {
+            return Result.error("memberId, dimension, score不能为空");
+        }
+        dimensionScoreService.uploadScore(vo);
+        return Result.success(null);
+    }
+
+    @Operation(summary = "问卷提交维度评分")
+    @PostMapping("/questionnaire")
+    public Result<Void> questionnaire(
+            @RequestBody HealthDimensionQuestionnaireVO vo,
+            @RequestAttribute("userId") Long userId) {
+        if (vo.getMemberId() == null || vo.getDimension() == null || vo.getAnswers() == null) {
+            return Result.error("memberId, dimension, answers不能为空");
+        }
+        dimensionScoreService.submitQuestionnaire(vo);
+        return Result.success(null);
+    }
+
+    @Operation(summary = "获取维度历史记录")
+    @PostMapping("/history")
+    public Result<List<HealthDimensionVO.DimensionItem>> history(
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute("userId") Long userId) {
+        Long memberId = params.get("memberId") != null
+                ? Long.valueOf(params.get("memberId").toString()) : null;
+        String dimension = (String) params.get("dimension");
+        int limit = params.get("limit") != null ? Integer.parseInt(params.get("limit").toString()) : 10;
+        if (memberId == null || dimension == null) {
+            return Result.error("memberId和dimension不能为空");
+        }
+        List<HealthDimensionVO.DimensionItem> items =
+            dimensionScoreService.getDimensionHistory(memberId, dimension, limit);
+        return Result.success(items);
+    }
+}
+```
+
+- [ ] **Step 2: 编译验证**
+
+```bash
+cd /sc-data/cfc/cfc-backend && rtk mvn clean compile -q 2>&1 | tail -20
+```
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/HealthDimensionController.java
+git commit -m "feat(controller): update to use new DTOs and consistent 7-dim API"
+```
+
+---
+
+### Task 9: 更新前端页面
+
+**Files:**
+- Modify: `cfc-frontend/pages/body/health-dimensions.vue`
+
+- [ ] **Step 1: 更新维度选项和数据源选项**
+
+```javascript
+// 将 dimensionOptions 从旧的7维替换为新的7维:
+dimensionOptions: [
+  { code: 'growth', label: '生长发育' },
+  { code: 'sleep', label: '睡眠质量' },
+  { code: 'vision', label: '视力健康' },
+  { code: 'immunity', label: '免疫力' },
+  { code: 'nutrition', label: '营养均衡' },
+  { code: 'gut', label: '肠胃健康' },
+  { code: 'exercise', label: '运动活力' }
+],
+sourceOptions: [
+  { code: 'REPORT', label: '健康报告' },
+  { code: 'PHOTO', label: '拍照识别' },
+  { code: 'VOICE', label: '语音输入' },
+  { code: 'MANUAL', label: '手动录入' }
+]
+```
+
+- [ ] **Step 2: 更新模板中的数据显示**
+
+在维度卡片的 `dim-header` 区域旁边新增百分位显示:
+```html
+<text class="dim-percentile" v-if="dim.percentile">
+  优于 {{ dim.percentile }}% 同龄人
+</text>
+```
+
+在 `dim-footer` 区域新增数据源图标显示:
+```html
+<text class="dim-source-icon" v-if="dim.dataSource">
+  {{ dim.dataSource === 'REPORT' ? '📄' : dim.dataSource === 'PHOTO' ? '📷' : dim.dataSource === 'VOICE' ? '🎤' : '✏️' }}
+  {{ dim.dataSource }}
+</text>
+```
+
+在级别标签旁新增Tier显示:
+```html
+<text class="dim-tier" v-if="dim.tier">
+  Tier{{ dim.tier }}
+</text>
+```
+
+- [ ] **Step 3: 编译验证**
+
+```bash
+cd /sc-data/cfc/cfc-frontend && rtk npx tsc --noEmit 2>&1 | tail -10 || echo "no TS check; verifying via build"
+```
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-frontend/pages/body/health-dimensions.vue
+git commit -m "feat(frontend): update 7-dim labels and add percentile+source display"
+```
+
+---
+
+## Final Verification Wave
+
+- [ ] **F1: 编译验证**
+
+```bash
+cd /sc-data/cfc/cfc-backend && rtk mvn clean compile -q 2>&1
+cd /sc-data/cfc/cfc-frontend && rtk npx vue-cli-service build --mode production 2>&1 | tail -10
+```
+
+- [ ] **F2: API端点测试(curl)**
+
+```bash
+# 启动应用(后台)
+cd /sc-data/cfc/cfc-backend && mvn spring-boot:run > /tmp/app.log 2>&1 &
+sleep 30
+# 测试overview端点
+curl -s -X POST http://localhost:9082/api/dimension/overview \
+  -H "Content-Type: application/json" \
+  -d '{"memberId": 1}' | python3 -m json.tool
+# 测试upload端点
+curl -s -X POST http://localhost:9082/api/dimension/upload \
+  -H "Content-Type: application/json" \
+  -d '{"memberId":1,"dimension":"sleep","score":85,"sourceType":"MANUAL"}' | python3 -m json.tool
+# 测试questionnaire端点
+curl -s -X POST http://localhost:9082/api/dimension/questionnaire \
+  -H "Content-Type: application/json" \
+  -d '{"memberId":1,"dimension":"nutrition","answers":[{"questionId":"q1","answer":"A","score":80},{"questionId":"q2","answer":"B","score":70}]}' | python3 -m json.tool
+# 停应用
+kill %1
+```
+
+- [ ] **F3: 设计文档合规性检查**
+
+逐项检查:
+- [ ] 7维code是否为 `growth/sleep/vision/immunity/nutrition/gut/exercise`
+- [ ] 表结构是否有 `percentile`、`tier`、`raw_data`、`assess_date`、`expire_date`
+- [ ] `health_norm_reference` 是否为百分位结构(percentile_5~95)
+- [ ] 菌群报告上传后是否触发了 `refreshFromGutReport()`
+- [ ] 常模种子数据是否包含了所有7维
+
+- [ ] **F4: 最终提交**
+
+```bash
+git push origin cfclub
+```

+ 412 - 0
docs/superpowers/specs/2026-06-26-health-dimensions-design-spec.md

@@ -0,0 +1,412 @@
+# 身体健康7维模型设计规范
+
+> 建立「身」维度下可量化的7维健康子体系,面向家长用户,以最少的用户输入获取最精准的健康画像。
+
+**版本:** v1.0  
+**日期:** 2026-06-25  
+**状态:** Draft
+
+---
+
+## 1. 背景与目标
+
+### 1.1 现有维度体系
+
+| 体系 | 维度 | 说明 |
+|------|------|------|
+| 五维能量 | 身·心·智·行·富 | 平台顶层框架,五行相生相克 |
+| 认知(DAN评估) | 注意力/专注力/记忆力/逻辑/感知/空间/加工速度 | 6+1维 |
+| 心理(EMI) | 情绪商数/心理韧性/压力应对/自我认知 | 4维 |
+| 大五人格 | 开放性/尽责性/外向性/宜人性/神经质 | 5维 |
+| 关系 | 亲密/沟通/信任 | 3维 |
+| **「身」子维度** | **生长发育/睡眠质量/视力健康/免疫力/营养均衡/肠胃健康/运动活力** | **7维(本文档)** |
+
+### 1.2 核心设计原则
+
+1. **最少用户输入**:拍照/上传 > 语音 > 点选 > 手动输入
+2. **渐进式精度**:问卷兜底(Tier 3)→ 拍照AI解析(Tier 2)→ 自动报告(Tier 1),有高精度数据自动替代低精度
+3. **常模对标**:每个维度的评分映射到同龄同性别人群百分位
+4. **菌群报告优先复用**:已有PDF解析的指标直接映射到对应维度
+
+---
+
+## 2. 7维定义与数据源
+
+### 📈 生长发育
+
+| 项目 | 内容 |
+|------|------|
+| **用户理解** | 身高体重达标吗,胖瘦合适吗 |
+| **Tier 1(拍照/自动)** | ①体脂称照片OCR → 体重/体脂率/肌肉量/骨量 ②菌群报告 → 疾病风险(易胖/超重指标)+ Firmicutes/Bacteroidetes比值 ③体检报告OCR → 身高/体重 |
+| **Tier 2(语音)** | "8岁,身高130,体重26" → NLP提取 |
+| **Tier 3(手动)** | 输入身高、体重(年龄+性别从用户资料读取) |
+| **评分算法** | BMI-for-age百分位(中国WS/T 586-2018)+ 体脂率校正 + 历史趋势(持续增长加分) |
+| **常模来源** | 中国儿童生长发育标准曲线(卫健委) |
+
+### 🛌 睡眠质量
+
+| 项目 | 内容 |
+|------|------|
+| **用户理解** | 睡够了没,睡得好不好 |
+| **Tier 1** | ①菌群报告 → 神经递质指标(血清素/GABA水平,category="神经递质") ②手环/手表睡眠截图OCR → 深睡时长/总时长/醒夜次数 |
+| **Tier 2** | "晚上十点睡早上七点起,中间醒一次" |
+| **Tier 3** | 点选睡眠时长区间(<6h / 6-8h / 8-10h / >10h)+ 点选质量(好/一般/差) |
+| **评分算法** | 时长分(年龄推荐时长±1h内满分)+ 质量调整因子 |
+| **常模来源** | 同年龄睡眠需求推荐值(美国国家睡眠基金会/AASM) |
+
+### 👁️ 视力健康
+
+| 项目 | 内容 |
+|------|------|
+| **用户理解** | 近视了没,度数涨了没 |
+| **Tier 1** | ①视力检查单/验光单拍照OCR → 左右眼裸眼视力/矫正视力/屈光度 |
+| **Tier 2** | "左眼5.0,右眼4.8" 或 "左眼100度,右眼150度" |
+| **Tier 3** | 点选是否戴镜 + 输入视力值 |
+| **评分算法** | 裸眼视力值映射分 + 屈光度变化趋势(稳定加分/加深扣分)+ 每日屏幕时间风险因子 |
+| **常模来源** | 儿童视力发育标准(各年龄正常视力下限) |
+
+### 🛡️ 免疫力
+
+| 项目 | 内容 |
+|------|------|
+| **用户理解** | 老不老实生病 |
+| **Tier 1** | ①菌群报告 → 肠道屏障指标 + 抗生素耐药指标 + 有益菌水平 ②血常规照片OCR → 白细胞/中性粒细胞/淋巴细胞/免疫球蛋白 |
+| **Tier 2** | "今年感冒了3次,没得过肺炎" |
+| **Tier 3** | 点选半年生病次数(0次/1-2次/3-5次/6次以上)+ 点选严重程度(从未住院/住院1次/住院多次) |
+| **评分算法** | 基础分=年龄常模基线 → 生病频率调整(越多越低)→ 菌群屏障加成 → 血常规校正 |
+| **常模来源** | 各年龄段年患病次数参考值 |
+
+### 🥗 营养均衡
+
+| 项目 | 内容 |
+|------|------|
+| **用户理解** | 偏不偏食,缺不缺营养 |
+| **Tier 1** | ①菌群报告 → 营养均衡分 + 微量指标(维生素D/铁/锌/钙等)+ 营养缺乏记录 ②饭菜拍照 → AI识别菜品 → 膳食多样性评分 |
+| **Tier 2** | "不爱吃青菜,肉还行,牛奶每天喝" |
+| **Tier 3** | 3题快速选择:挑食程度(不挑/挑几样/严重挑食)+ 每周食材种类数 + 补充剂使用 |
+| **评分算法** | 菌群微营养指标得分 × 0.6 + 膳食多样性得分 × 0.4(仅有问卷时=问卷分×0.8+基线×0.2) |
+| **常模来源** | 各年龄段推荐营养素摄入量(DRIs) |
+
+### 🌿 肠胃健康
+
+| 项目 | 内容 |
+|------|------|
+| **用户理解** | 肠胃舒服吗,拉得好吗 |
+| **Tier 1** | ①菌群报告 → **最完整覆盖**:健康度/平衡分/多样性/有益菌/有害菌/核心菌属/SCFA/肠型/肠道年龄 ②舌诊照片 → AI分析舌苔厚薄/舌色/齿痕 → 消化功能状态 |
+| **Tier 2** | "最近大便偏稀,一周拉4次,偶尔肚子疼" |
+| **Tier 3** | 点选消化舒适度(好/一般/差,含解释emoji辅助) |
+| **评分算法** | 菌群综合分(多指标加权)+ 舌诊校正因子 |
+| **常模来源** | 菌群报告自带 populationLevel + detectionRate(人群百分位) |
+
+### 🏃 运动活力
+
+| 项目 | 内容 |
+|------|------|
+| **用户理解** | 运动够不够,体能怎么样 |
+| **Tier 1** | ①微信运动授权 → 日均步数 ②学校体测报告拍照OCR → 跳绳/50米跑/坐位体前屈等成绩(对照国家学生体质健康标准) |
+| **Tier 2** | "每周两节体育课,周末踢一小时球" |
+| **Tier 3** | 点选运动频率(几乎不/每周1-2次/3-5次/每天)+ 点选运动强度(轻度/中等/剧烈) |
+| **评分算法** | 运动量(MET-min/周)映射分 + 国家体测标准分(如有) |
+| **常模来源** | 国家学生体质健康标准(2023版)+ 各年龄推荐运动量(WHO) |
+
+---
+
+## 3. 数据源优先级与映射规则
+
+### 3.1 全局规则
+
+```
+IF 存在 Tier 1 数据 THEN 使用 Tier 1 替代所有低层数据
+IF 仅有 Tier 2 数据 THEN 使用 NLP 提取值,置信度低于0.6时降级为 Tier 3
+IF 无 Tier 1/2 数据 THEN 使用 Tier 3(问卷/手动),标注"快速评估"标识
+```
+
+### 3.2 菌群报告 → 7维映射矩阵
+
+| 菌群报告字段 | 生长发育 | 睡眠质量 | 视力健康 | 免疫力 | 营养均衡 | 肠胃健康 | 运动活力 |
+|-------------|:-------:|:-------:|:-------:|:-----:|:-------:|:-------:|:-------:|
+| gutHealthScore | | | | ✅ | | ✅ | |
+| balanceScore | | | | | | ✅ | |
+| diversityScore | | | | ✅ | | ✅ | |
+| nutritionScore | | | | | ✅ | | |
+| chronicDiseaseScore | ✅ | | | | ✅ | | |
+| beneficialBacteriaScore | | | | ✅ | | ✅ | |
+| harmfulBacteriaScore | | | | ✅ | | ✅ | |
+| coreGenusScore | | | | | | ✅ | |
+| gutAge | | ✅ | | | | ✅ | |
+| gutBalanceScore | | | | | | ✅ | |
+| gutDiversityScore | | | | ✅ | | ✅ | |
+| 神经递质(indicators) | | ✅ | | | | | |
+| 肠道屏障(indicators) | | | | ✅ | | ✅ | |
+| 短链脂肪酸(indicators) | | | | | ✅ | ✅ | |
+| 疾病风险(diseaseRisks) | ✅ | | | ✅ | | | |
+| 菌种丰度(populationLevel) | ✅ | | | ✅ | ✅ | ✅ | |
+| 微量指标(维生素/矿物质) | | | | | ✅ | | |
+| 抗生素耐药 | | | | ✅ | | | |
+
+### 3.3 维度分数聚合
+
+```
+纬度最终分 = Σ(各数据源分 × 权重) × 年龄段常模校正因子
+
+权重说明:
+- 有菌群报告时:菌群指标权重占 0.4-0.7,其他数据源占 0.3-0.6
+- 无菌群报告时:问卷/语音权重占 0.7-1.0,基线常模占 0-0.3
+
+常模校正:
+- 原始分 → 查同性别同年龄百分位 → 输出 1-100 分(50 = 人群中位)
+- 百分位≥85 → 优秀(绿色)
+- 百分位50-84 → 良好(蓝色)
+- 百分位15-49 → 一般(黄色)
+- 百分位<15 → 关注(橙色/红色)
+```
+
+---
+
+## 4. 架构设计
+
+### 4.1 模块划分
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│                    Controller 层                              │
+│  HealthDimensionController                                    │
+│  POST /api/health/dimensions/{memberId} — 获取7维评分        │
+│  POST /api/health/dimensions/{memberId}/upload — 上传数据源  │
+│     body: { type: "report"|"photo"|"voice", file/data }     │
+└──────────────────────┬──────────────────────────────────────┘
+                       │
+┌──────────────────────▼──────────────────────────────────────┐
+│                   Service 层                                 │
+│                                                              │
+│  DimensionScoreService     ← 核心聚合器                       │
+│  ├─ getDimensions(memberId) → 7维评分 + 常模百分位            │
+│  ├─ refresh(memberId, dataSource) → 监听到新数据,重算        │
+│  └─ compareTrend(memberId, months) → 历史趋势                 │
+│                                                              │
+│  ┌────────────┬───────────┬───────────┬──────────────────┐   │
+│  │ GrowScore  │ SleepScore│ VisionScore│ ImmunityScore   │   │
+│  │ Service    │ Service   │ Service   │ Service          │   │
+│  ├────────────┼───────────┼───────────┼──────────────────┤   │
+│  │ Nutrition  │ GutScore  │ Exercise  │                  │   │
+│  │ ScoreSvc   │ Service   │ ScoreSvc  │                  │   │
+│  └────────────┴───────────┴───────────┴──────────────────┘   │
+│                                                              │
+│  外部依赖 Service:                                            │
+│  ├─ HealthReportService  (菌群报告)                          │
+│  ├─ OcrService           (照片OCR)                           │
+│  ├─ VoiceInputService    (语音→NLP)                          │
+│  ├─ TongueDiagnosisService (舌诊AI)                          │
+│  └─ WechatStepService    (微信运动)                          │
+└──────────────────────┬──────────────────────────────────────┘
+                       │
+┌──────────────────────▼──────────────────────────────────────┐
+│                  Entity / Mapper 层                           │
+│                                                              │
+│  HealthDimensionScore ← 核心实体                              │
+│  ├─ id, memberId, dimension (code), score (0-100)            │
+│  ├─ percentile (0-100), dataSource (枚举: REPORT/PHOTO/...)  │
+│  ├─ tier (1/2/3), assessDate, expireDate                     │
+│  └─ rawData (JSON) — 保留原始输入用于追溯                     │
+│                                                              │
+│  DataSourceRecord ← 数据源记录                                │
+│  ├─ id, memberId, sourceType (PDF/PHOTO/VOICE/MANUAL)        │
+│  ├─ dimensionCodes (影响的维度列表)                           │
+│  └─ parsedResult (JSON) — OCR/解析后的结构化结果              │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### 4.2 核心实体设计
+
+#### HealthDimensionScore
+
+```sql
+CREATE TABLE health_dimension_scores (
+    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
+    member_id       BIGINT       NOT NULL COMMENT '家庭成员ID',
+    dimension       VARCHAR(20)  NOT NULL COMMENT '维度: growth/sleep/vision/immunity/nutrition/gut/exercise',
+    score           INT          NOT NULL COMMENT '0-100分',
+    percentile      INT          COMMENT '人群百分位 0-100',
+    data_source     VARCHAR(20)  NOT NULL COMMENT '数据源: REPORT/PHOTO/VOICE/MANUAL',
+    tier            TINYINT      NOT NULL DEFAULT 3 COMMENT '数据层级: 1自动/2语音/3手动',
+    raw_data        JSON         COMMENT '原始数据快照',
+    assess_date     DATE         NOT NULL COMMENT '评估日期',
+    expire_date     DATE         COMMENT '数据过期日(Tier1长/Tier3短)',
+    created_at      DATETIME,
+    updated_at      DATETIME,
+    UNIQUE KEY uk_member_dim_date (member_id, dimension, assess_date)
+);
+```
+
+#### DataSourceRecord
+
+```sql
+CREATE TABLE health_data_sources (
+    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
+    member_id       BIGINT       NOT NULL,
+    source_type     VARCHAR(20)  NOT NULL COMMENT 'PDF/PHOTO/VOICE/MANUAL/WEIXIN_STEP',
+    dimension_codes JSON         NOT NULL COMMENT '影响的维度列表',
+    parsed_result   JSON         COMMENT '解析结果全量',
+    file_url        VARCHAR(500) COMMENT '原始文件URL',
+    confidence      DECIMAL(3,2) COMMENT 'AI解析置信度 0-1',
+    created_at      DATETIME,
+    INDEX idx_member_source (member_id, source_type)
+);
+```
+
+### 4.3 维度分数生命周期
+
+```
+用户上传数据源
+     │
+     ▼
+解析层(OcrService / PdfParseService / VoiceInputService / TongueDiagnosisService)
+     │
+     ▼ 返回结构化原始数据
+DimensionScoreService.refresh(memberId, dataSource)
+     │
+     ├─ 读取该dataSource影响的所有维度
+     ├─ 对每个维度:读取所有活跃数据源
+     ├─ 按Tier优先级合并(Tier1覆盖Tier2/3)
+     ├─ 计算加权得分
+     ├─ 查常模表得到百分位
+     └─ 写入/更新 health_dimension_scores
+
+用户查看7维面板
+     │
+     ▼
+POST /api/health/dimensions/{memberId}
+     │
+     ├─ 取各维度最新 assess_date 的记录
+     ├─ 有expire_date且未过期 → 直接返回
+     ├─ 有过期 → 提示用户补充数据,同时返回过期数据(标注)
+     └─ 无数据 → 进入Tier 3(问卷流程)
+```
+
+---
+
+## 5. 常模系统
+
+### 5.1 常模数据结构
+
+```sql
+CREATE TABLE health_norm_reference (
+    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
+    dimension       VARCHAR(20)  NOT NULL COMMENT '维度',
+    gender          VARCHAR(10)  NOT NULL COMMENT 'male/female/all',
+    age_min         INT          NOT NULL COMMENT '年龄下限(月)',
+    age_max         INT          NOT NULL COMMENT '年龄上限(月)',
+    percentile_5    INT,
+    percentile_15   INT,
+    percentile_25   INT,
+    percentile_50   INT  COMMENT '中位值',
+    percentile_75   INT,
+    percentile_85   INT,
+    percentile_95   INT,
+    source          VARCHAR(50)  COMMENT '常模来源: WHO/卫健委/平台自建',
+    UNIQUE KEY uk_dim_gender_age (dimension, gender, age_min, age_max)
+);
+```
+
+### 5.2 常模优先级
+
+1. **中国官方标准**(BMI-for-age WS/T 586、学生体质健康标准、血压百分位)
+2. **菌群报告人群数据**(已有 populationLevel → 平台积累后直接复用)
+3. **国际权威标准**(WHO生长曲线、睡眠时长推荐)
+4. **平台自建常模**(运营一段后基于用户数据建立自有百分位)
+
+### 5.3 菌群报告已有常模数据的利用
+
+`HealthGutFlora.populationLevel` 字段(如 "35%")和 `detectionRate`(如 "98.56%")已经是现成的常模数据。可直接用于:
+- 肠胃健康维度的评分校准
+- 免疫力维度的部分指标校准
+- 营养均衡维度的微营养指标参考
+
+策略:菌群报告解析时,populationLevel 直接作为该指标在人群中的百分位使用,无需二次查表。
+
+---
+
+## 6. 分期实现计划
+
+### Phase 1 — 核心框架 + 问卷兜底(MVP)
+
+**目标:7维全部可评估,所有用户可使用**
+
+- 创建 `HealthDimensionScore` / `DataSourceRecord` / `health_norm_reference` 表
+- `DimensionScoreService` 聚合逻辑(Tier 3 问卷兜底)
+- `HealthDimensionController` — 获取7维 / 手动输入问卷
+- 前端:7维雷达图页面 + 每个维度的简单问卷(3-5题点选)
+- 已有菌群报告数据直接映射(`PdfParseService` 解析结果 → 对应维度)
+- 部署常模种子数据(BMI/体测等国家标准)
+
+### Phase 2 — 拍照OCR
+
+**目标:替代手动输入,减少用户操作**
+
+- `OcrService` 通用引擎(支持体检报告/体测单/血常规拍照)
+- 体脂称照片OCR(显示屏幕 → 数字识别)
+- 舌诊照片AI分析(舌色/舌苔/齿痕 → 肠胃/免疫/睡眠推论)
+- `VoiceInputService`(语音→ASR→NLP指标提取)
+- 后端整合Tier1/2→覆盖Tier3
+
+### Phase 3 — 自动接入
+
+**目标:最低用户操作**
+
+- 微信运动授权接入(步数→运动活力)
+- 手环截图自动识别
+- 历史数据趋势图(各维度3/6/12个月变化)
+- 健康建议引擎(各维度低分→个性化改善建议,关联商城营养品)
+
+---
+
+## 7. 用户界面示意(文字描述)
+
+### 7.1 7维健康雷达图
+
+主页面显示一个7边形雷达图,每个顶点标注维度名称和分数(0-100),中心为常模中位线(50分)。
+
+颜色编码:
+- ≥85 分深绿 — 🟢 优秀
+- 60-84 分浅绿 — 良好
+- 40-59 分黄色 — ⚠️ 一般
+- <40 分橙色/红 — 🔴 关注
+
+### 7.2 数据源提示
+
+每个维度下方显示当前数据源等级图标:
+- 📄 菌群报告 / 🏥 体检报告(Tier 1 自动)
+- 📷 照片识别(Tier 2 拍照)
+- 🎤 语音输入(Tier 2 语音)
+- ✏️ 手动填写(Tier 3 手动)
+- 点击可查看原始数据和到期时间
+
+### 7.3 数据补充引导
+
+当某维度数据过期或从未采集时:
+- 入口文案:"拍张体检单,自动获取{维度名}评估"
+- 或语音按钮:"说句话,AI帮你记录"
+- 或点选入口:3-5题快速问卷
+
+---
+
+## 8. 待确认问题
+
+- [ ] 舌诊AI方案:自研基于颜色/纹理分类 vs 接入第三方API(如百度AI舌诊)
+- [ ] 体脂称:支持哪些品牌的屏幕布局,是否需要通用OCR适配层
+- [ ] 语音模型:用微信内置语音识别还是自建ASR
+- [ ] 常模数据:BMI百分位表是硬编码在代码中还是做成可配置表
+
+---
+
+## 9. 附录:7维与五维能量「身」的衔接
+
+```
+五维「身」能量值 = Σ(7维子维度分 × 权重)
+                    其中权重可配置(默认等权 1/7)
+
+衔接逻辑:
+- 用户首次进入 → 引导上传菌群报告/拍照 → 自动生成7维+「身」能量
+- 已有旧数据 → 历史迁移:菌群报告现有字段直接映射,缺的维度走问卷
+- 实时联动 → 任一子维度更新 → 自动重算「身」能量(已有 EnergyService)
+```