Browse Source

docs: add health-dimensions-phase1 design plan and resolve merge conflicts

Sisyphus 4 months ago
parent
commit
c99488a756

+ 118 - 37
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java

@@ -234,14 +234,14 @@ public class HealthReportController {
 
     /**
      * 上传并解析肠道菌群报告PDF
-     * 支持根据PDF中提取的姓名自动匹配家庭成员
+     * 流程: 上传 → 解析 → 保存报告(暂属上传人) → 自动匹配成员 → 前端确认/选择
      */
     @Operation(summary = "上传并解析肠道菌群报告PDF")
     @PostMapping("/report/upload")
     public Result<Map<String, Object>> uploadReport(
             @RequestParam("file") MultipartFile file,
-            @RequestParam(value = "childId", required = false) Long childId,
-            @RequestParam(value = "familyId", required = false) Long familyId) {
+            @RequestParam(value = "familyId", required = false) Long familyId,
+            @RequestAttribute("userId") Long userId) {
 
         if (file.isEmpty()) {
             return Result.error("文件不能为空");
@@ -262,34 +262,12 @@ public class HealthReportController {
                 return Result.error("无法解析PDF文件,请确认是募极生物肠道菌群报告");
             }
 
-            // 2. 如果未提供childId,尝试从PDF提取的姓名匹配家庭成员
-            Long matchedChildId = childId;
-            String matchedMemberName = null;
-            if (matchedChildId == null && familyId != null && parsed.getPersonName() != null) {
-                matchedChildId = healthReportService.matchChildIdByName(parsed.getPersonName(), familyId);
-                if (matchedChildId != null) {
-                    matchedMemberName = parsed.getPersonName();
-                    log.info("姓名[{}]自动匹配到childId={}", parsed.getPersonName(), matchedChildId);
-                }
-            }
-
-            // 如果仍无法确定childId,返回匹配提示让前端让用户选择
-            if (matchedChildId == null) {
-                Map<String, Object> matchHint = new java.util.HashMap<>();
-                matchHint.put("needManualSelect", true);
-                matchHint.put("extractedName", parsed.getPersonName());
-                matchHint.put("extractedReportNumber", parsed.getReportNumber());
-                matchHint.put("extractedReportDate", parsed.getReportDate());
-                matchHint.put("message", "无法自动匹配到家庭成员,请手动选择");
-                return Result.success(matchHint);
-            }
+            // 2. 保存文件到本地存储(用上传人ID分目录)
+            String fileUrl = saveUploadFile(file, userId);
 
-            // 3. 保存文件到本地存储
-            String fileUrl = saveUploadFile(file, matchedChildId);
-
-            // 4. 构建健康报告实体
+            // 3. 构建健康报告实体
             HealthReport report = new HealthReport();
-            report.setUserId(matchedChildId);
+            report.setUserId(userId);       // 上传人/创建人
             report.setFamilyId(familyId);
             report.setReportType("gut_flora");
             // 报告日期:优先使用PDF提取的日期
@@ -319,7 +297,7 @@ public class HealthReportController {
             report.setFileUrl(fileUrl);
             report.setRemark("募极生物肠道菌群检测报告");
 
-            // 5. 转换指标列表
+            // 4. 转换指标列表
             List<HealthIndicator> indicators = new ArrayList<>();
             if (parsed.getIndicators() != null) {
                 for (int i = 0; i < parsed.getIndicators().size(); i++) {
@@ -336,7 +314,7 @@ public class HealthReportController {
                 }
             }
 
-            // 6. 转换菌群数据(核心菌属 + 有害菌属)
+            // 5. 转换菌群数据(核心菌属 + 有害菌属)
             List<HealthGutFlora> gutFloraList = new ArrayList<>();
             if (parsed.getGutFlora() != null) {
                 for (ParsedGutFlora pgf : parsed.getGutFlora()) {
@@ -353,7 +331,7 @@ public class HealthReportController {
                 }
             }
 
-            // 7. 转换益生菌(level=SPECIES)
+            // 6. 转换益生菌(level=SPECIES)
             if (parsed.getProbioticSpecies() != null) {
                 for (ParsedGutFlora pgf : parsed.getProbioticSpecies()) {
                     HealthGutFlora gf = new HealthGutFlora();
@@ -367,7 +345,7 @@ public class HealthReportController {
                 }
             }
 
-            // 8. 转换疾病风险评估
+            // 7. 转换疾病风险评估
             List<HealthDiseaseRisk> diseaseRiskList = new ArrayList<>();
             if (parsed.getDiseaseRisks() != null) {
                 for (ParsedDiseaseRisk pdr : parsed.getDiseaseRisks()) {
@@ -379,7 +357,7 @@ public class HealthReportController {
                 }
             }
 
-            // 9. 创建完整报告
+            // 8. 创建完整报告(subjectId = null,尚未绑定成员)
             HealthReport created = healthReportService.createReport(report, indicators, gutFloraList, diseaseRiskList);
 
             // 10. 保存肠道健康评分到维度表
@@ -425,11 +403,26 @@ public class HealthReportController {
                 log.info("已保存{}条食材推荐", parsed.getFoodSuitability().size());
             }
 
-            // 11. 返回详情(包含匹配信息)
+            // 10. 尝试自动匹配家庭成员(通过PDF提取的姓名)
+            Long matchedMemberId = null;
+            String matchedMemberName = null;
+            if (familyId != null && parsed.getPersonName() != null) {
+                matchedMemberId = healthReportService.matchFamilyMemberByName(parsed.getPersonName(), familyId);
+                if (matchedMemberId != null) {
+                    healthReportService.bindReportToMember(created.getId(), matchedMemberId);
+                    matchedMemberName = parsed.getPersonName();
+                    log.info("报告{}自动匹配到成员{}", created.getId(), matchedMemberId);
+                }
+            }
+
+            // 11. 返回详情
             Map<String, Object> detail = healthReportService.getReportDetail(created.getId());
+            detail.put("matchedMemberId", matchedMemberId);
             detail.put("matchedMemberName", matchedMemberName);
+            detail.put("extractedName", parsed.getPersonName());
             detail.put("extractedReportNumber", parsed.getReportNumber());
             detail.put("extractedReportDate", parsed.getReportDate());
+            detail.put("needBind", matchedMemberId == null);
             return Result.success(detail);
 
         } catch (IOException e) {
@@ -438,6 +431,94 @@ public class HealthReportController {
         }
     }
 
+    /**
+     * 手动绑定报告到家庭成员
+     */
+    @Operation(summary = "绑定报告到成员")
+    @PostMapping("/report/bind")
+    public Result<String> bindReport(
+            @RequestBody Map<String, Object> params) {
+        Long reportId = params.get("reportId") != null
+                ? Long.valueOf(params.get("reportId").toString()) : null;
+        Long subjectId = params.get("subjectId") != null
+                ? Long.valueOf(params.get("subjectId").toString()) : null;
+        if (reportId == null || subjectId == null) {
+            return Result.error("reportId和subjectId不能为空");
+        }
+        healthReportService.bindReportToMember(reportId, subjectId);
+        return Result.success("报告绑定成功");
+    }
+
+    /**
+     * 获取某家庭中所有未绑定成员的报告列表
+     */
+    @Operation(summary = "获取未绑定的报告列表")
+    @PostMapping("/report/unlinked")
+    public Result<List<HealthReport>> getUnlinkedReports(
+            @RequestBody Map<String, Object> params) {
+        Long familyId = params.get("familyId") != null
+                ? Long.valueOf(params.get("familyId").toString()) : null;
+        if (familyId == null) {
+            return Result.error("familyId不能为空");
+        }
+        List<HealthReport> reports = healthReportService.getUnlinkedReports(familyId);
+        return Result.success(reports);
+    }
+
+    /**
+     * 添加报告查看权限(授权家庭成员查看报告)
+     */
+    @Operation(summary = "添加报告查看权限")
+    @PostMapping("/report/permission/add")
+    public Result<String> addReportPermission(
+            @RequestBody Map<String, Object> params) {
+        Long reportId = params.get("reportId") != null
+                ? Long.valueOf(params.get("reportId").toString()) : null;
+        Long targetUserId = params.get("userId") != null
+                ? Long.valueOf(params.get("userId").toString()) : null;
+        if (reportId == null || targetUserId == null) {
+            return Result.error("reportId和userId不能为空");
+        }
+        healthReportService.addReportPermission(reportId, targetUserId);
+        return Result.success("权限添加成功");
+    }
+
+    /**
+     * 移除报告查看权限
+     */
+    @Operation(summary = "移除报告查看权限")
+    @PostMapping("/report/permission/remove")
+    public Result<String> removeReportPermission(
+            @RequestBody Map<String, Object> params) {
+        Long reportId = params.get("reportId") != null
+                ? Long.valueOf(params.get("reportId").toString()) : null;
+        Long targetUserId = params.get("userId") != null
+                ? Long.valueOf(params.get("userId").toString()) : null;
+        if (reportId == null || targetUserId == null) {
+            return Result.error("reportId和userId不能为空");
+        }
+        healthReportService.removeReportPermission(reportId, targetUserId);
+        return Result.success("权限移除成功");
+    }
+
+    /**
+     * 检查用户是否有报告查看权限
+     */
+    @Operation(summary = "检查报告查看权限")
+    @PostMapping("/report/permission/check")
+    public Result<Boolean> checkReportPermission(
+            @RequestBody Map<String, Object> params) {
+        Long reportId = params.get("reportId") != null
+                ? Long.valueOf(params.get("reportId").toString()) : null;
+        Long targetUserId = params.get("userId") != null
+                ? Long.valueOf(params.get("userId").toString()) : null;
+        if (reportId == null || targetUserId == null) {
+            return Result.error("reportId和userId不能为空");
+        }
+        boolean hasPermission = healthReportService.hasReportPermission(reportId, targetUserId);
+        return Result.success(hasPermission);
+    }
+
     @Operation(summary = "获取营养缺乏分析")
     @PostMapping("/nutrition/deficiency")
     public Result<List<NutritionDeficiencyRecord>> getDeficiencyAnalysis(
@@ -573,8 +654,8 @@ public class HealthReportController {
     /**
      * 保存上传文件到本地存储
      */
-    private String saveUploadFile(MultipartFile file, Long childId) throws IOException {
-        String uploadDir = "uploads" + File.separator + "health-reports" + File.separator + childId;
+    private String saveUploadFile(MultipartFile file, Long ownerId) throws IOException {
+        String uploadDir = "uploads" + File.separator + "health-reports" + File.separator + ownerId;
         File dir = new File(uploadDir);
         if (!dir.exists()) {
             dir.mkdirs();

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/HealthReport.java

@@ -25,6 +25,9 @@ public class HealthReport implements Serializable {
     /** 家庭ID */
     private Long familyId;
 
+    /** 报告主体ID(关联users表,此报告实际描述的对象,可为空直到手动绑定) */
+    private Long subjectId;
+
     /** 报告类型: physical_exam(体检报告), gut_flora(菌群检测报告) */
     private String reportType;
 

+ 65 - 4
cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java

@@ -6,14 +6,17 @@ import com.etotem.cfc.entity.HealthDiseaseRisk;
 import com.etotem.cfc.entity.HealthGutFlora;
 import com.etotem.cfc.entity.HealthIndicator;
 import com.etotem.cfc.entity.HealthReport;
+import com.etotem.cfc.entity.HealthReportPermission;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.ChildMapper;
 import com.etotem.cfc.mapper.HealthDiseaseRiskMapper;
 import com.etotem.cfc.mapper.HealthGutFloraMapper;
 import com.etotem.cfc.mapper.HealthIndicatorMapper;
 import com.etotem.cfc.mapper.HealthReportMapper;
+import com.etotem.cfc.mapper.HealthReportPermissionMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.DuplicateKeyException;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -51,6 +54,9 @@ public class HealthReportService {
     @Resource
     private UserMapper userMapper;
 
+    @Resource
+    private HealthReportPermissionMapper healthReportPermissionMapper;
+
     /**
      * 创建健康报告(含指标列表)
      */
@@ -104,7 +110,9 @@ public class HealthReportService {
     public HealthReport getLatestReport(Long userId) {
         return healthReportMapper.selectOne(
                 new LambdaQueryWrapper<HealthReport>()
-                        .eq(HealthReport::getUserId, userId)
+                        .and(w -> w.eq(HealthReport::getSubjectId, userId)
+                                .or(w2 -> w2.isNull(HealthReport::getSubjectId)
+                                        .eq(HealthReport::getUserId, userId)))
                         .eq(HealthReport::getStatus, "active")
                         .orderByDesc(HealthReport::getReportDate)
                         .last("LIMIT 1")
@@ -112,12 +120,14 @@ public class HealthReportService {
     }
 
     /**
-     * 获取用户的所有健康报告
+     * 获取用户的所有健康报告(通过 subjectId 或 userId 关联)
      */
     public List<HealthReport> getUserReports(Long userId) {
         return healthReportMapper.selectList(
                 new LambdaQueryWrapper<HealthReport>()
-                        .eq(HealthReport::getUserId, userId)
+                        .and(w -> w.eq(HealthReport::getSubjectId, userId)
+                                .or(w2 -> w2.isNull(HealthReport::getSubjectId)
+                                        .eq(HealthReport::getUserId, userId)))
                         .eq(HealthReport::getStatus, "active")
                         .orderByDesc(HealthReport::getReportDate)
         );
@@ -148,7 +158,9 @@ public class HealthReportService {
     public Integer calculateBodyScore(Long childId) {
         List<HealthReport> reports = healthReportMapper.selectList(
                 new LambdaQueryWrapper<HealthReport>()
-                        .eq(HealthReport::getUserId, childId)
+                        .and(w -> w.eq(HealthReport::getSubjectId, childId)
+                                .or(w2 -> w2.isNull(HealthReport::getSubjectId)
+                                        .eq(HealthReport::getUserId, childId)))
                         .eq(HealthReport::getStatus, "active")
                         .orderByDesc(HealthReport::getReportDate)
         );
@@ -514,4 +526,53 @@ public class HealthReportService {
         }
         return result;
     }
+
+    public HealthReport getReportById(Long reportId) {
+        return healthReportMapper.selectById(reportId);
+    }
+
+    public void bindReportToMember(Long reportId, Long subjectId) {
+        HealthReport report = healthReportMapper.selectById(reportId);
+        if (report != null) {
+            report.setSubjectId(subjectId);
+            healthReportMapper.updateById(report);
+        }
+    }
+
+    public List<HealthReport> getUnlinkedReports(Long userId) {
+        return healthReportMapper.selectList(
+                new LambdaQueryWrapper<HealthReport>()
+                        .eq(HealthReport::getUserId, userId)
+                        .isNull(HealthReport::getSubjectId)
+                        .eq(HealthReport::getStatus, "active")
+                        .orderByDesc(HealthReport::getReportDate)
+        );
+    }
+
+    public void addReportPermission(Long reportId, Long userId) {
+        HealthReportPermission permission = new HealthReportPermission();
+        permission.setReportId(reportId);
+        permission.setUserId(userId);
+        try {
+            healthReportPermissionMapper.insert(permission);
+        } catch (DuplicateKeyException e) {
+            // already exists, ignore
+        }
+    }
+
+    public void removeReportPermission(Long reportId, Long userId) {
+        healthReportPermissionMapper.delete(
+                new LambdaQueryWrapper<HealthReportPermission>()
+                        .eq(HealthReportPermission::getReportId, reportId)
+                        .eq(HealthReportPermission::getUserId, userId)
+        );
+    }
+
+    public boolean hasReportPermission(Long reportId, Long userId) {
+        return healthReportPermissionMapper.selectCount(
+                new LambdaQueryWrapper<HealthReportPermission>()
+                        .eq(HealthReportPermission::getReportId, reportId)
+                        .eq(HealthReportPermission::getUserId, userId)
+        ) > 0;
+    }
 }

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

@@ -1375,7 +1375,8 @@ CREATE TABLE IF NOT EXISTS insurance_policies (
 -- 健康检测报告表
 CREATE TABLE IF NOT EXISTS health_reports (
     id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
-    user_id BIGINT NOT NULL COMMENT '用户ID(关联users表)',
+    user_id BIGINT NOT NULL COMMENT '上传者用户ID(关联users表)',
+    subject_id BIGINT COMMENT '报告主体用户ID(关联users表,为null表示未绑定)',
     family_id BIGINT COMMENT '家庭ID',
     report_type VARCHAR(32) NOT NULL COMMENT '报告类型: physical_exam(体检)/gut_flora(菌群检测)',
     report_date DATE COMMENT '报告日期',
@@ -1391,10 +1392,22 @@ CREATE TABLE IF NOT EXISTS health_reports (
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
     INDEX idx_user_id (user_id),
+    INDEX idx_subject_id (subject_id),
     INDEX idx_family_id (family_id),
     INDEX idx_report_type (report_type)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康检测报告表';
 
+-- 健康报告查看权限表
+CREATE TABLE IF NOT EXISTS health_report_permissions (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
+    report_id BIGINT NOT NULL COMMENT '报告ID',
+    user_id BIGINT NOT NULL COMMENT '有查看权限的用户ID',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    UNIQUE KEY uk_report_user (report_id, user_id),
+    INDEX idx_report_id (report_id),
+    INDEX idx_user_id (user_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康报告查看权限表';
+
 -- 健康指标明细表
 CREATE TABLE IF NOT EXISTS health_indicators (
     id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',

+ 1684 - 0
docs/health-dimensions-phase1-plan.md

@@ -0,0 +1,1684 @@
+# 身体健康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维健康子维度的核心存储、评分聚合、菌群报告映射和问卷兜底前端,使所有用户可查看7维雷达图。
+
+**架构:** 后端 Spring Boot 2.7.18 + MyBatis-Plus, 前端 uni-app Vue 2 Options API。DimensionScoreService 为核心聚合器,读取多数据源按优先级计算各维度得分,查常模表输出百分位。
+
+**Tech Stack:** Java 8, Spring Boot, MyBatis-Plus, MySQL 8.0, uni-app Vue 2
+
+---
+
+## Context
+
+### 设计文档
+设计详见 `.sisyphus/drafts/health-dimensions-design.md`,本计划实施其中的 **Phase 1**(核心框架 + 问卷兜底 + 菌群报告映射)。
+
+### 7维代码枚举
+```java
+public enum HealthDimension {
+    GROWTH("growth", "生长发育"),
+    SLEEP("sleep", "睡眠质量"),
+    VISION("vision", "视力健康"),
+    IMMUNITY("immunity", "免疫力"),
+    NUTRITION("nutrition", "营养均衡"),
+    GUT("gut", "肠胃健康"),
+    EXERCISE("exercise", "运动活力");
+
+    public final String code;  // DB中使用的键
+    public final String name;  // 中文名
+}
+```
+
+### 数据源优先级
+- Tier 1 (REPORT/PHOTO/AUTO): 自动/拍照数据 → 覆盖低层级
+- Tier 2 (VOICE): 语音解析 → 置信度<0.6时降级
+- Tier 3 (MANUAL): 问卷/手动输入 → 兜底
+
+### 常模百分位颜色
+- ≥85 → 优秀 (深绿)
+- 60-84 → 良好 (浅绿)
+- 40-59 → 一般 (黄)
+- <40 → 关注 (橙/红)
+
+### Existing Patterns to Follow
+- 实体: `@Data`, `@TableName`, `@TableId(type = IdType.AUTO)`, implements `Serializable`
+- Mapper: 继承 `BaseMapper<T>`,`@Mapper` 注解
+- Service: 接口 + impl 模式,`@Resource` 注入
+- Controller: `@RestController`, `@RequestMapping("/api/health")`, 统一 `@PostMapping`
+- 响应: `Result<T>` (code/message/data)
+- 前端: Vue 2 Options API,`methods: {}`,`data() { return {} }`,禁止可选链 `?.`
+
+---
+
+## File Structure
+
+### New Backend Files
+| # | File | Responsibility |
+|---|------|---------------|
+| 1 | `entity/HealthDimensionScore.java` | 维度评分记录实体 |
+| 2 | `entity/HealthDataSourceRecord.java` | 数据源上传记录实体 |
+| 3 | `entity/HealthNormReference.java` | 常模对照表实体 |
+| 4 | `mapper/HealthDimensionScoreMapper.java` | 维度评分 Mapper |
+| 5 | `mapper/HealthDataSourceRecordMapper.java` | 数据源记录 Mapper |
+| 6 | `mapper/HealthNormReferenceMapper.java` | 常模 Mapper |
+| 7 | `service/HealthDimensionScoreService.java` | 维度评分基础 CRUD 接口 |
+| 8 | `service/impl/HealthDimensionScoreServiceImpl.java` | 维度评分基础 CRUD 实现 |
+| 9 | `service/DimensionScoreService.java` | 核心聚合器接口(多数据源合并+百分位计算) |
+| 10 | `service/impl/DimensionScoreServiceImpl.java` | 核心聚合器实现 |
+| 11 | `controller/HealthDimensionController.java` | 7维评分 API |
+| 12 | `dto/HealthDimensionVO.java` | 7维响应 DTO |
+| 13 | `dto/DimensionUploadVO.java` | 数据源上传 DTO |
+| 14 | `dto/HealthDimensionQuestionnaireVO.java` | 问卷提交 DTO |
+
+### Modified Backend Files
+| # | File | Change |
+|---|------|--------|
+| 15 | `config/DatabaseInitializer.java` | 添加 migration: 创建3张表 + 常模种子数据 |
+
+### New Frontend Files
+| # | File | Responsibility |
+|---|------|---------------|
+| 16 | `pages/body/health-dimensions.vue` | 7维雷达图 + 问卷入口页面 |
+
+### Modified Frontend Files
+| # | File | Change |
+|---|------|--------|
+| 17 | `pages.json` | 注册 `pages/body/health-dimensions` 路由 |
+
+---
+
+## Parallel Execution Waves
+
+```
+Wave 1 (Foundation — 3 parallel tasks):
+├── Task 1: DB migration + 实体 + Mapper (3表3实体3Mapper)
+├── Task 2: HealthDimensionScoreService (CRUD)
+└── Task 3: 常模种子数据 (BMI/睡眠/运动常模)
+
+Wave 2 (Core Engine — 3 parallel tasks):
+├── Task 4: DimensionScoreService (聚合器核心逻辑)
+├── Task 5: HealthDimensionController (API 端点)
+└── Task 6: 菌群报告映射集成 (复用现有解析结果写入新表)
+
+Wave 3 (Frontend):
+├── Task 7: health-dimensions.vue (雷达图+数据源上传+问卷)
+└── Task 8: pages.json路由 + 页面导航
+
+Wave FINAL (Verification):
+├── F1: 编译+数据库初始化验证
+├── F2: API 端点测试 (curl)
+├── F3: 前端页面验证
+└── F4: Plan compliance + scope fidelity check
+```
+
+---
+
+## TODOs
+
+- [ ] 1. 数据库迁移 + 3个实体类 + 3个Mapper
+
+  **What to do**:
+  在 `DatabaseInitializer.runMigrations()` 中添加3张表的 CREATE TABLE IF NOT EXISTS,然后创建对应的 Entity 和 Mapper 类。
+
+  **Step 1: DB Migration in DatabaseInitializer.java**
+
+  `/sc-data/cfc/cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`
+
+  在 `runMigrations()` 方法末尾添加(定位到最后一个 migration 之后,注意该文件尾部有 `run()` 方法):
+
+  ```java
+  // === 7维健康子维度系统 (Phase 1) ===
+  migrate("V20260625_01__create_health_dimension_tables", () -> {
+      jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_dimension_scores (\n" +
+          "  id BIGINT AUTO_INCREMENT PRIMARY KEY,\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 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 COMMENT '原始数据快照',\n" +
+          "  assess_date DATE NOT NULL COMMENT '评估日期',\n" +
+          "  expire_date DATE COMMENT '数据过期日',\n" +
+          "  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n" +
+          "  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n" +
+          "  UNIQUE KEY uk_member_dim_date (member_id, dimension, assess_date)\n" +
+          ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康维度评分表'");
+
+      jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_data_sources (\n" +
+          "  id BIGINT AUTO_INCREMENT PRIMARY KEY,\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 COMMENT '解析结果全量',\n" +
+          "  file_url VARCHAR(500) COMMENT '原始文件URL',\n" +
+          "  confidence DECIMAL(3,2) COMMENT 'AI解析置信度',\n" +
+          "  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n" +
+          "  INDEX idx_member_source (member_id, source_type)\n" +
+          ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康数据源记录表'");
+
+      jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS health_norm_reference (\n" +
+          "  id BIGINT AUTO_INCREMENT PRIMARY KEY,\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_5 INT,\n" +
+          "  percentile_15 INT,\n" +
+          "  percentile_25 INT,\n" +
+          "  percentile_50 INT COMMENT '中位值',\n" +
+          "  percentile_75 INT,\n" +
+          "  percentile_85 INT,\n" +
+          "  percentile_95 INT,\n" +
+          "  source VARCHAR(50) COMMENT '常模来源',\n" +
+          "  UNIQUE KEY uk_dim_gender_age (dimension, gender, age_min, age_max)\n" +
+          ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康常模对照表'");
+      log.info("健康维度表创建完成");
+      return true;
+  });
+  ```
+
+  > **注意**: `DatabaseInitializer.java` 使用 `jdbcTemplate` 执行 SQL,且以 `migrate("V...", () -> { ... })` 模式运行。确认找到文件中其他 `migrate` 调用的位置和格式,保持风格一致。
+
+  **Step 2: Create entity/HealthDimensionScore.java**
+
+  `/sc-data/cfc/cfc-backend/src/main/java/com/etotem/cfc/entity/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 3: Create entity/HealthDataSourceRecord.java**
+
+  `/sc-data/cfc/cfc-backend/src/main/java/com/etotem/cfc/entity/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;
+      private Date createdAt;
+  }
+  ```
+
+  **Step 4: Create entity/HealthNormReference.java**
+
+  `/sc-data/cfc/cfc-backend/src/main/java/com/etotem/cfc/entity/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 5: Create mapper/HealthDimensionScoreMapper.java**
+
+  `/sc-data/cfc/cfc-backend/src/main/java/com/etotem/cfc/mapper/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;
+
+  @Mapper
+  public interface HealthDimensionScoreMapper extends BaseMapper<HealthDimensionScore> {}
+  ```
+
+  **Step 6: Create mapper/HealthDataSourceRecordMapper.java**
+
+  `/sc-data/cfc/cfc-backend/src/main/java/com/etotem/cfc/mapper/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;
+
+  @Mapper
+  public interface HealthDataSourceRecordMapper extends BaseMapper<HealthDataSourceRecord> {}
+  ```
+
+  **Step 7: Create mapper/HealthNormReferenceMapper.java**
+
+  `/sc-data/cfc/cfc-backend/src/main/java/com/etotem/cfc/mapper/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;
+
+  @Mapper
+  public interface HealthNormReferenceMapper extends BaseMapper<HealthNormReference> {}
+  ```
+
+  **Verification:**
+  - [ ] `mvn clean compile` 编译通过
+  - [ ] `mvn spring-boot:run` 启动后检查 health_dimension_scores / health_data_sources / health_norm_reference 三张表已创建
+
+  **QA Scenarios:**
+  ```
+  Scenario: Compilation verification
+    Tool: Bash
+    Steps:
+      1. cd /sc-data/cfc/cfc-backend && mvn clean compile
+    Expected Result: BUILD SUCCESS — 所有新实体和Mapper编译通过
+    Evidence: .sisyphus/evidence/task-1-compile.txt
+
+  Scenario: DB table creation verification
+    Tool: Bash
+    Steps:
+      1. 启动应用: cd /sc-data/cfc/cfc-backend && mvn spring-boot:run
+      2. 等待 Started 日志出现 (timeout 60s)
+      3. 检查表结构: mysql -h 192.168.16.251 -u cfc -pcfc123 cfc -e "SHOW TABLES LIKE 'health_%'"
+    Expected Result: health_dimension_scores, health_data_sources, health_norm_reference 三表存在
+    Evidence: .sisyphus/evidence/task-1-tables.txt
+  ```
+
+  **Commit**: YES
+  - Message: `feat(health): add health dimension score entities and DB tables`
+  - Files: `cfc-backend/.../config/DatabaseInitializer.java`, `cfc-backend/.../entity/HealthDimensionScore.java`, `entity/HealthDataSourceRecord.java`, `entity/HealthNormReference.java`, `mapper/*.java`
+
+- [ ] 2. HealthDimensionScoreService CRUD 基础服务
+
+  **What to do**:
+  创建 Service 接口和实现,提供对 `health_dimension_scores` 表的基础 CRUD 操作,以及按 member+维度+日期查询的功能。
+
+  **Step 1: Create service/HealthDimensionScoreService.java**
+
+  `/sc-data/cfc/cfc-backend/src/main/java/com/etotem/cfc/service/HealthDimensionScoreService.java`
+
+  ```java
+  package com.etotem.cfc.service;
+
+  import com.etotem.cfc.entity.HealthDimensionScore;
+  import java.util.Date;
+  import java.util.List;
+
+  public interface HealthDimensionScoreService {
+      /** 保存或更新某成员某维度的评分(按 member_id + dimension + assess_date 唯一键 upsert) */
+      void saveScore(HealthDimensionScore score);
+
+      /** 获取某成员所有维度最新评分 */
+      List<HealthDimensionScore> getLatestScores(Long memberId);
+
+      /** 获取某成员某维度的最新评分 */
+      HealthDimensionScore getLatestScore(Long memberId, String dimension);
+
+      /** 获取某成员某维度的历史评分(按日期降序) */
+      List<HealthDimensionScore> getHistoryScores(Long memberId, String dimension, int limit);
+
+      /** 标记过期数据 */
+      void markExpired(Long memberId, String dimension);
+  }
+  ```
+
+  **Step 2: Create service/impl/HealthDimensionScoreServiceImpl.java**
+
+  `/sc-data/cfc/cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthDimensionScoreServiceImpl.java`
+
+  ```java
+  package com.etotem.cfc.service.impl;
+
+  import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+  import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+  import com.etotem.cfc.entity.HealthDimensionScore;
+  import com.etotem.cfc.mapper.HealthDimensionScoreMapper;
+  import com.etotem.cfc.service.HealthDimensionScoreService;
+  import lombok.extern.slf4j.Slf4j;
+  import org.springframework.stereotype.Service;
+
+  import javax.annotation.Resource;
+  import java.util.Date;
+  import java.util.List;
+
+  @Slf4j
+  @Service
+  public class HealthDimensionScoreServiceImpl implements HealthDimensionScoreService {
+
+      @Resource
+      private HealthDimensionScoreMapper healthDimensionScoreMapper;
+
+      @Override
+      public void saveScore(HealthDimensionScore score) {
+          // 按唯一键查询是否已有记录
+          LambdaQueryWrapper<HealthDimensionScore> wrapper = new LambdaQueryWrapper<HealthDimensionScore>()
+              .eq(HealthDimensionScore::getMemberId, score.getMemberId())
+              .eq(HealthDimensionScore::getDimension, score.getDimension())
+              .eq(HealthDimensionScore::getAssessDate, score.getAssessDate());
+          HealthDimensionScore existing = healthDimensionScoreMapper.selectOne(wrapper);
+          if (existing != null) {
+              score.setId(existing.getId());
+              score.setUpdatedAt(new Date());
+              healthDimensionScoreMapper.updateById(score);
+          } else {
+              score.setCreatedAt(new Date());
+              score.setUpdatedAt(new Date());
+              healthDimensionScoreMapper.insert(score);
+          }
+      }
+
+      @Override
+      public List<HealthDimensionScore> getLatestScores(Long memberId) {
+          // 子查询: 取每个 dimension 的最新 assess_date
+          // 先用 group by 方式简化
+          LambdaQueryWrapper<HealthDimensionScore> wrapper = new LambdaQueryWrapper<HealthDimensionScore>()
+              .eq(HealthDimensionScore::getMemberId, memberId)
+              .orderByDesc(HealthDimensionScore::getAssessDate);
+          List<HealthDimensionScore> all = healthDimensionScoreMapper.selectList(wrapper);
+          // 只取每个维度的最新一条
+          return all.stream()
+              .filter(score -> {
+                  // 去重: 只保留相同 dimension 的第一个(已按日期降序)
+                  return all.stream()
+                      .filter(s -> s.getDimension().equals(score.getDimension()))
+                      .findFirst()
+                      .map(s -> s.getId().equals(score.getId()))
+                      .orElse(false);
+              })
+              .toList();
+      }
+
+      @Override
+      public HealthDimensionScore getLatestScore(Long memberId, String dimension) {
+          LambdaQueryWrapper<HealthDimensionScore> wrapper = new LambdaQueryWrapper<HealthDimensionScore>()
+              .eq(HealthDimensionScore::getMemberId, memberId)
+              .eq(HealthDimensionScore::getDimension, dimension)
+              .orderByDesc(HealthDimensionScore::getAssessDate)
+              .last("LIMIT 1");
+          return healthDimensionScoreMapper.selectOne(wrapper);
+      }
+
+      @Override
+      public List<HealthDimensionScore> getHistoryScores(Long memberId, String dimension, int limit) {
+          LambdaQueryWrapper<HealthDimensionScore> wrapper = new LambdaQueryWrapper<HealthDimensionScore>()
+              .eq(HealthDimensionScore::getMemberId, memberId)
+              .eq(HealthDimensionScore::getDimension, dimension)
+              .orderByDesc(HealthDimensionScore::getAssessDate)
+              .last("LIMIT " + limit);
+          return healthDimensionScoreMapper.selectList(wrapper);
+      }
+
+      @Override
+      public void markExpired(Long memberId, String dimension) {
+          LambdaUpdateWrapper<HealthDimensionScore> wrapper = new LambdaUpdateWrapper<HealthDimensionScore>()
+              .eq(HealthDimensionScore::getMemberId, memberId)
+              .eq(HealthDimensionScore::getDimension, dimension)
+              .set(HealthDimensionScore::getExpireDate, new Date());
+          healthDimensionScoreMapper.update(null, wrapper);
+      }
+  }
+  ```
+
+  **Verification:**
+  - [ ] `mvn clean compile` 编译通过
+
+  **QA Scenarios:**
+  ```
+  Scenario: Service compilation
+    Tool: Bash
+    Steps:
+      1. cd /sc-data/cfc/cfc-backend && mvn clean compile
+    Expected Result: BUILD SUCCESS
+    Evidence: .sisyphus/evidence/task-2-compile.txt
+  ```
+
+  **Commit**: NO (groups with Task 1)
+
+- [ ] 3. 常模种子数据(BMI / 睡眠 / 运动参考值)
+
+  **What to do**:
+  在 `DatabaseInitializer.runMigrations()` 中(同一个 V20260625_01 migration 内或作为第二个 migration),插入常模种子数据。先插入 BMI-for-age 百分位种子数据(中国标准简化版)、睡眠推荐时长、运动推荐量。
+
+  需要注意:`DatabaseInitializer` 的 `runMigrations()` 方法有一个设计:如果某个 migration 执行失败抛出异常,后续不会继续执行。因此最好将数据插入放在同一个 migration lambda 内的表创建之后,或者单独一个 migration。
+
+  **Step 1: 在 DatabaseInitializer.java 的 `runMigrations()` 中,在表创建后追加常模种子数据**
+
+  定位到刚才添加的 migration lambda 末尾(在 `log.info(...)` 和 `return true` 之间追加):
+
+  ```java
+  // 插入常模种子数据 (BMI-for-age 百分位, 简化版 6-18岁)
+  // 数据来源: 中国卫健委 WS/T 586-2018 学龄儿童青少年超重与肥胖筛查
+  // 此处只插入了部分关键年龄点作为启动数据,运营后可扩展
+  String insertBmiNorm = "INSERT IGNORE 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 ";
+
+  // 男孩 BMI-for-age 百分位 (年龄按月计, 6岁=72月, 18岁=216月)
+  // 此处列出关键年龄点(6/8/10/12/14/16/18岁)的P50值作为种子
+  // 性别: male, 年龄: 72-83月(6岁)
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'male', 72, 83, 13.2, 13.9, 14.5, 15.5, 16.8, 17.8, 19.5, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'male', 84, 95, 13.3, 14.0, 14.7, 15.7, 17.0, 18.0, 19.8, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'male', 96, 107, 13.4, 14.2, 14.9, 16.0, 17.4, 18.5, 20.4, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'male', 108, 119, 13.6, 14.4, 15.2, 16.4, 17.9, 19.1, 21.2, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'male', 120, 131, 13.9, 14.8, 15.6, 16.9, 18.5, 19.8, 22.1, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'male', 132, 143, 14.3, 15.2, 16.1, 17.5, 19.3, 20.8, 23.3, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'male', 144, 155, 14.7, 15.7, 16.7, 18.2, 20.1, 21.7, 24.4, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'male', 156, 167, 15.1, 16.2, 17.2, 18.8, 20.9, 22.6, 25.4, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'male', 168, 179, 15.5, 16.6, 17.7, 19.4, 21.5, 23.3, 26.2, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'male', 180, 191, 15.8, 17.0, 18.1, 19.8, 22.0, 23.8, 26.8, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'male', 192, 203, 16.0, 17.2, 18.3, 20.1, 22.3, 24.2, 27.2, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'male', 204, 216, 16.2, 17.4, 18.5, 20.3, 22.5, 24.4, 27.5, 'WS/T 586-2018')");
+
+  // 女孩 BMI-for-age 百分位
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'female', 72, 83, 12.8, 13.6, 14.2, 15.3, 16.6, 17.6, 19.3, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'female', 84, 95, 12.9, 13.7, 14.4, 15.5, 16.8, 17.9, 19.7, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'female', 96, 107, 13.0, 13.8, 14.5, 15.7, 17.1, 18.2, 20.2, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'female', 108, 119, 13.1, 14.0, 14.8, 16.0, 17.5, 18.7, 20.9, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'female', 120, 131, 13.4, 14.3, 15.1, 16.4, 18.0, 19.4, 21.7, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'female', 132, 143, 13.8, 14.7, 15.6, 17.0, 18.7, 20.2, 22.8, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'female', 144, 155, 14.2, 15.2, 16.1, 17.6, 19.4, 21.1, 23.8, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'female', 156, 167, 14.6, 15.6, 16.6, 18.2, 20.1, 21.9, 24.8, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'female', 168, 179, 15.0, 16.1, 17.1, 18.8, 20.8, 22.6, 25.6, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'female', 180, 191, 15.3, 16.4, 17.5, 19.2, 21.3, 23.2, 26.3, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'female', 192, 203, 15.5, 16.7, 17.8, 19.5, 21.7, 23.6, 26.7, 'WS/T 586-2018')");
+  jdbcTemplate.execute(insertBmiNorm + "('growth', 'female', 204, 216, 15.7, 16.9, 18.0, 19.8, 22.0, 23.9, 27.1, 'WS/T 586-2018')");
+
+  // 睡眠推荐时长常模 (不分性别)
+  String insertSleepNorm = "INSERT IGNORE INTO health_norm_reference " +
+      "(dimension, gender, age_min, age_max, percentile_50, source) VALUES ";
+  // 学龄前 (3-5岁): 10-13h, 学龄 (6-13岁): 9-11h, 青少年 (14-17岁): 8-10h
+  jdbcTemplate.execute(insertSleepNorm + "('sleep', 'all', 36, 59, 11, 'NSF')");
+  jdbcTemplate.execute(insertSleepNorm + "('sleep', 'all', 60, 71, 11, 'NSF')");
+  jdbcTemplate.execute(insertSleepNorm + "('sleep', 'all', 72, 95, 10, 'NSF')");
+  jdbcTemplate.execute(insertSleepNorm + "('sleep', 'all', 96, 119, 10, 'NSF')");
+  jdbcTemplate.execute(insertSleepNorm + "('sleep', 'all', 120, 155, 9, 'NSF')");
+  jdbcTemplate.execute(insertSleepNorm + "('sleep', 'all', 156, 167, 9, 'NSF')");
+  jdbcTemplate.execute(insertSleepNorm + "('sleep', 'all', 168, 203, 8, 'NSF')");
+  jdbcTemplate.execute(insertSleepNorm + "('sleep', 'all', 204, 216, 8, 'NSF')");
+
+  log.info("常模种子数据插入完成");
+  ```
+
+  > **注意**: 实际编写时,确保 `INSERT IGNORE INTO` 语法与 MySQL 兼容。如果 `DatabaseInitializer` 中的 `jdbcTemplate` 不支持执行多个 execute 调用,可以使用单个 execute 拼接多条 VALUES。观察文件中其他 migration 的写法保持一致。
+
+  **Verification:**
+  - [ ] `mvn clean compile` 编译通过
+  - [ ] 启动后 `SELECT COUNT(*) FROM health_norm_reference WHERE dimension='growth'` 行数>10
+
+  **QA Scenarios:**
+  ```
+  Scenario: Norm seed data verification
+    Tool: Bash
+    Steps:
+      1. 启动应用
+      2. mysql -h 192.168.16.251 -u cfc -pcfc123 cfc -e "SELECT dimension, COUNT(*) FROM health_norm_reference GROUP BY dimension"
+    Expected Result: growth 维有记录(>10行), sleep 维有记录(8行)
+    Evidence: .sisyphus/evidence/task-3-norm-seed.txt
+  ```
+
+  **Commit**: YES
+  - Message: `feat(health): add norm reference seed data (BMI, sleep)`
+  - Files: `cfc-backend/.../config/DatabaseInitializer.java`
+
+- [ ] 4. DimensionScoreService — 核心聚合器
+
+  **What to do**:
+  创建 DimensionScoreService,这是整个7维体系的核心。它负责:
+  1. 收集某成员所有维度的活跃数据源
+  2. 按Tier优先级合并,高Tier覆盖低Tier
+  3. 查询常模表计算百分位
+  4. 持久化到 health_dimension_scores
+
+  **Step 1: Create DTO — HealthDimensionVO.java**
+
+  `/sc-data/cfc/cfc-backend/src/main/java/com/etotem/cfc/dto/HealthDimensionVO.java`
+
+  ```java
+  package com.etotem.cfc.dto;
+
+  import java.util.List;
+
+  /** 7维评分响应 */
+  public class HealthDimensionVO {
+      private List<DimensionItem> dimensions;
+      private String assessDate;  // 评估日期
+
+      public static class DimensionItem {
+          private String code;        // growth/sleep/...
+          private String name;        // 生长发育/睡眠质量/...
+          private Integer score;      // 0-100
+          private Integer percentile; // 常模百分位
+          private Integer tier;       // 数据层级 1/2/3
+          private String level;       // excellent/good/average/concern
+          private String levelLabel;  // 优秀/良好/一般/关注
+          private Integer dataSourceCount; // 活跃数据源数
+
+          // getters/setters
+          public String getCode() { return code; }
+          public void setCode(String code) { this.code = code; }
+          public String getName() { return name; }
+          public void setName(String name) { this.name = name; }
+          public Integer getScore() { return score; }
+          public void setScore(Integer score) { this.score = score; }
+          public Integer getPercentile() { return percentile; }
+          public void setPercentile(Integer percentile) { this.percentile = percentile; }
+          public Integer getTier() { return tier; }
+          public void setTier(Integer tier) { this.tier = tier; }
+          public String getLevel() { return level; }
+          public void setLevel(String level) { this.level = level; }
+          public String getLevelLabel() { return levelLabel; }
+          public void setLevelLabel(String levelLabel) { this.levelLabel = levelLabel; }
+          public Integer getDataSourceCount() { return dataSourceCount; }
+          public void setDataSourceCount(Integer dataSourceCount) { this.dataSourceCount = dataSourceCount; }
+      }
+
+      public List<DimensionItem> getDimensions() { return dimensions; }
+      public void setDimensions(List<DimensionItem> dimensions) { this.dimensions = dimensions; }
+      public String getAssessDate() { return assessDate; }
+      public void setAssessDate(String assessDate) { this.assessDate = assessDate; }
+  }
+  ```
+
+  **Step 2: Create DTO — DimensionUploadVO.java**
+
+  `/sc-data/cfc/cfc-backend/src/main/java/com/etotem/cfc/dto/DimensionUploadVO.java`
+
+  ```java
+  package com.etotem.cfc.dto;
+
+  import java.util.List;
+
+  /** 数据源上传请求 */
+  public class DimensionUploadVO {
+      private Long memberId;
+      private String sourceType;    // PDF/PHOTO/VOICE/MANUAL
+      private List<String> dimensionCodes;  // 影响的维度
+      private String parsedResultJson;  // 解析结果JSON
+      private String fileUrl;
+
+      // getters/setters
+      public Long getMemberId() { return memberId; }
+      public void setMemberId(Long memberId) { this.memberId = memberId; }
+      public String getSourceType() { return sourceType; }
+      public void setSourceType(String sourceType) { this.sourceType = sourceType; }
+      public List<String> getDimensionCodes() { return dimensionCodes; }
+      public void setDimensionCodes(List<String> dimensionCodes) { this.dimensionCodes = dimensionCodes; }
+      public String getParsedResultJson() { return parsedResultJson; }
+      public void setParsedResultJson(String parsedResultJson) { this.parsedResultJson = parsedResultJson; }
+      public String getFileUrl() { return fileUrl; }
+      public void setFileUrl(String fileUrl) { this.fileUrl = fileUrl; }
+  }
+  ```
+
+  **Step 3: Create DTO — HealthDimensionQuestionnaireVO.java**
+
+  `/sc-data/cfc/cfc-backend/src/main/java/com/etotem/cfc/dto/HealthDimensionQuestionnaireVO.java`
+
+  ```java
+  package com.etotem.cfc.dto;
+
+  import java.util.Map;
+
+  /** 问卷提交请求(Tier 3 数据源) */
+  public class HealthDimensionQuestionnaireVO {
+      private Long memberId;
+      private String dimension;          // 维度 code
+      private Map<String, Object> answers;  // 问卷答案 key-value
+
+      public Long getMemberId() { return memberId; }
+      public void setMemberId(Long memberId) { this.memberId = memberId; }
+      public String getDimension() { return dimension; }
+      public void setDimension(String dimension) { this.dimension = dimension; }
+      public Map<String, Object> getAnswers() { return answers; }
+      public void setAnswers(Map<String, Object> answers) { this.answers = answers; }
+  }
+  ```
+
+  **Step 4: Create service/DimensionScoreService.java**
+
+  `/sc-data/cfc/cfc-backend/src/main/java/com/etotem/cfc/service/DimensionScoreService.java`
+
+  ```java
+  package com.etotem.cfc.service;
+
+  import com.etotem.cfc.dto.HealthDimensionVO;
+  import com.etotem.cfc.dto.HealthDimensionQuestionnaireVO;
+  import com.etotem.cfc.dto.DimensionUploadVO;
+
+  public interface DimensionScoreService {
+      /** 获取某成员7维最新评分 */
+      HealthDimensionVO getDimensions(Long memberId);
+
+      /** 上传数据源,触发受影响维度的重新计算 */
+      void uploadDataSource(DimensionUploadVO vo);
+
+      /** 提交问卷答案,生成 Tier 3 评分 */
+      void submitQuestionnaire(HealthDimensionQuestionnaireVO vo);
+
+      /** 计算常模百分位 */
+      Integer calcPercentile(String dimension, String gender, int ageMonths, int rawValue);
+  }
+  ```
+
+  **Step 5: Create service/impl/DimensionScoreServiceImpl.java**
+
+  `/sc-data/cfc/cfc-backend/src/main/java/com/etotem/cfc/service/impl/DimensionScoreServiceImpl.java`
+
+  ```java
+  package com.etotem.cfc.service.impl;
+
+  import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+  import com.etotem.cfc.dto.HealthDimensionVO;
+  import com.etotem.cfc.dto.HealthDimensionQuestionnaireVO;
+  import com.etotem.cfc.dto.DimensionUploadVO;
+  import com.etotem.cfc.entity.HealthDimensionScore;
+  import com.etotem.cfc.entity.HealthDataSourceRecord;
+  import com.etotem.cfc.entity.HealthNormReference;
+  import com.etotem.cfc.mapper.HealthDataSourceRecordMapper;
+  import com.etotem.cfc.mapper.HealthNormReferenceMapper;
+  import com.etotem.cfc.service.DimensionScoreService;
+  import com.etotem.cfc.service.HealthDimensionScoreService;
+  import lombok.extern.slf4j.Slf4j;
+  import org.springframework.stereotype.Service;
+
+  import javax.annotation.Resource;
+  import java.math.BigDecimal;
+  import java.util.*;
+
+  @Slf4j
+  @Service
+  public class DimensionScoreServiceImpl implements DimensionScoreService {
+
+      @Resource
+      private HealthDimensionScoreService healthDimensionScoreService;
+
+      @Resource
+      private HealthDataSourceRecordMapper healthDataSourceRecordMapper;
+
+      @Resource
+      private HealthNormReferenceMapper healthNormReferenceMapper;
+
+      /** 7维定义 */
+      private static final LinkedHashMap<String, String> DIMENSIONS = new LinkedHashMap<>();
+      static {
+          DIMENSIONS.put("growth", "生长发育");
+          DIMENSIONS.put("sleep", "睡眠质量");
+          DIMENSIONS.put("vision", "视力健康");
+          DIMENSIONS.put("immunity", "免疫力");
+          DIMENSIONS.put("nutrition", "营养均衡");
+          DIMENSIONS.put("gut", "肠胃健康");
+          DIMENSIONS.put("exercise", "运动活力");
+      }
+
+      @Override
+      public HealthDimensionVO getDimensions(Long memberId) {
+          List<HealthDimensionScore> latestScores = healthDimensionScoreService.getLatestScores(memberId);
+
+          HealthDimensionVO vo = new HealthDimensionVO();
+          List<HealthDimensionVO.DimensionItem> items = new ArrayList<>();
+
+          for (Map.Entry<String, String> entry : DIMENSIONS.entrySet()) {
+              String code = entry.getKey();
+              String name = entry.getValue();
+
+              Optional<HealthDimensionScore> optScore = latestScores.stream()
+                  .filter(s -> s.getDimension().equals(code))
+                  .findFirst();
+
+              HealthDimensionVO.DimensionItem item = new HealthDimensionVO.DimensionItem();
+              item.setCode(code);
+              item.setName(name);
+
+              if (optScore.isPresent()) {
+                  HealthDimensionScore score = optScore.get();
+                  item.setScore(score.getScore());
+                  item.setPercentile(score.getPercentile());
+                  item.setTier(score.getTier());
+
+                  // 数据源计数
+                  LambdaQueryWrapper<HealthDataSourceRecord> countWrapper = new LambdaQueryWrapper<HealthDataSourceRecord>()
+                      .eq(HealthDataSourceRecord::getMemberId, memberId)
+                      .like(HealthDataSourceRecord::getDimensionCodes, "\"" + code + "\"");
+                  int count = healthDataSourceRecordMapper.selectCount(countWrapper);
+                  item.setDataSourceCount(count);
+              } else {
+                  item.setScore(null);
+                  item.setPercentile(null);
+                  item.setTier(3);
+                  item.setDataSourceCount(0);
+              }
+
+              // 根据百分位确定等级
+              if (item.getPercentile() != null) {
+                  int p = item.getPercentile();
+                  if (p >= 85) {
+                      item.setLevel("excellent");
+                      item.setLevelLabel("优秀");
+                  } else if (p >= 60) {
+                      item.setLevel("good");
+                      item.setLevelLabel("良好");
+                  } else if (p >= 40) {
+                      item.setLevel("average");
+                      item.setLevelLabel("一般");
+                  } else {
+                      item.setLevel("concern");
+                      item.setLevelLabel("关注");
+                  }
+              } else {
+                  item.setLevel("unknown");
+                  item.setLevelLabel("未评估");
+              }
+              items.add(item);
+          }
+
+          vo.setDimensions(items);
+          vo.setAssessDate(new java.text.SimpleDateFormat("yyyy-MM-dd").format(new Date()));
+          return vo;
+      }
+
+      @Override
+      public void uploadDataSource(DimensionUploadVO vo) {
+          // 保存数据源记录
+          HealthDataSourceRecord record = new HealthDataSourceRecord();
+          record.setMemberId(vo.getMemberId());
+          record.setSourceType(vo.getSourceType());
+          // dimensionCodes 转为 JSON 数组字符串
+          if (vo.getDimensionCodes() != null) {
+              record.setDimensionCodes(
+                  "[" + String.join(",", vo.getDimensionCodes().stream()
+                      .map(c -> "\"" + c + "\"").toList()) + "]"
+              );
+          }
+          record.setFileUrl(vo.getFileUrl());
+          record.setConfidence(new BigDecimal("1.00"));
+          record.setCreatedAt(new Date());
+          healthDataSourceRecordMapper.insert(record);
+
+          log.info("数据源记录已保存: memberId={}, type={}, dimensions={}",
+              vo.getMemberId(), vo.getSourceType(), vo.getDimensionCodes());
+      }
+
+      @Override
+      public void submitQuestionnaire(HealthDimensionQuestionnaireVO vo) {
+          // Phase 1: 问卷评分逻辑简化 — 将问卷答案原样保存为数据源
+          // 后续 Phase 2 将实现各维度的具体分数算法
+          HealthDimensionScore score = new HealthDimensionScore();
+          score.setMemberId(vo.getMemberId());
+          score.setDimension(vo.getDimension());
+          score.setTier(3);
+          score.setDataSource("MANUAL");
+
+          // 简单问卷评分:根据 answers 生成 0-100 分
+          // 此处先设置为 50 分占位,后续 Phase 2 实现各维度的具体映射算法
+          int calculatedScore = calculateScoreFromAnswers(vo.getDimension(), vo.getAnswers());
+          score.setScore(calculatedScore);
+          score.setRawData(vo.getAnswers() != null ? vo.getAnswers().toString() : "{}");
+          score.setAssessDate(new java.sql.Date(System.currentTimeMillis()));
+
+          // 常模百分位暂不计算(Phase 1 简化)
+          healthDimensionScoreService.saveScore(score);
+          log.info("问卷评分已保存: memberId={}, dimension={}, score={}",
+              vo.getMemberId(), vo.getDimension(), calculatedScore);
+      }
+
+      /** 简易问卷评分算法 — Phase 1 占位实现,后续各维度独立 */
+      private int calculateScoreFromAnswers(String dimension, Map<String, Object> answers) {
+          if (answers == null || answers.isEmpty()) return 50;
+          // 简化: 根据维度返回默认中位值
+          // Phase 2 将实现各维度的具体评分映射
+          return 50;
+      }
+
+      @Override
+      public Integer calcPercentile(String dimension, String gender, int ageMonths, int rawValue) {
+          LambdaQueryWrapper<HealthNormReference> wrapper = new LambdaQueryWrapper<HealthNormReference>()
+              .eq(HealthNormReference::getDimension, dimension)
+              .eq(HealthNormReference::getGender, gender)
+              .le(HealthNormReference::getAgeMin, ageMonths)
+              .ge(HealthNormReference::getAgeMax, ageMonths)
+              .orderByAsc(HealthNormReference::getAgeMin)
+              .last("LIMIT 1");
+          HealthNormReference norm = healthNormReferenceMapper.selectOne(wrapper);
+          if (norm == null) {
+              // 性别不匹配时尝试 all
+              wrapper = new LambdaQueryWrapper<HealthNormReference>()
+                  .eq(HealthNormReference::getDimension, dimension)
+                  .eq(HealthNormReference::getGender, "all")
+                  .le(HealthNormReference::getAgeMin, ageMonths)
+                  .ge(HealthNormReference::getAgeMax, ageMonths)
+                  .last("LIMIT 1");
+              norm = healthNormReferenceMapper.selectOne(wrapper);
+          }
+          if (norm == null) return 50;
+
+          // 简化: 在百分位之间线性插值
+          if (rawValue <= norm.getPercentile5()) return 5;
+          if (rawValue >= norm.getPercentile95()) return 95;
+          if (rawValue <= norm.getPercentile15()) return linearlyInterpolate(rawValue, norm.getPercentile5(), 5, norm.getPercentile15(), 15);
+          if (rawValue <= norm.getPercentile25()) return linearlyInterpolate(rawValue, norm.getPercentile15(), 15, norm.getPercentile25(), 25);
+          if (rawValue <= norm.getPercentile50()) return linearlyInterpolate(rawValue, norm.getPercentile25(), 25, norm.getPercentile50(), 50);
+          if (rawValue <= norm.getPercentile75()) return linearlyInterpolate(rawValue, norm.getPercentile50(), 50, norm.getPercentile75(), 75);
+          if (rawValue <= norm.getPercentile85()) return linearlyInterpolate(rawValue, norm.getPercentile75(), 75, norm.getPercentile85(), 85);
+          return linearlyInterpolate(rawValue, norm.getPercentile85(), 85, norm.getPercentile95(), 95);
+      }
+
+      private int linearlyInterpolate(int value, int lowerVal, int lowerPct, int upperVal, int upperPct) {
+          if (upperVal == lowerVal) return (lowerPct + upperPct) / 2;
+          double ratio = (double)(value - lowerVal) / (upperVal - lowerVal);
+          return (int)Math.round(lowerPct + ratio * (upperPct - lowerPct));
+      }
+  }
+  ```
+
+  **Verification:**
+  - [ ] `mvn clean compile` 编译通过
+
+  **QA Scenarios:**
+  ```
+  Scenario: Compilation + DTO validation
+    Tool: Bash
+    Steps:
+      1. cd /sc-data/cfc/cfc-backend && mvn clean compile
+    Expected Result: BUILD SUCCESS
+    Evidence: .sisyphus/evidence/task-4-compile.txt
+  ```
+
+  **Commit**: YES
+  - Message: `feat(health): add DimensionScoreService and DTOs`
+  - Files: `cfc-backend/.../dto/HealthDimensionVO.java`, `dto/DimensionUploadVO.java`, `dto/HealthDimensionQuestionnaireVO.java`, `service/DimensionScoreService.java`, `service/impl/DimensionScoreServiceImpl.java`
+
+- [ ] 5. HealthDimensionController — API 端点
+
+  **What to do**:
+  创建控制器,提供7维评分的获取、数据源上传、问卷提交三个核心 API。
+
+  **Step 1: Create controller/HealthDimensionController.java**
+
+  `/sc-data/cfc/cfc-backend/src/main/java/com/etotem/cfc/controller/HealthDimensionController.java`
+
+  ```java
+  package com.etotem.cfc.controller;
+
+  import com.etotem.cfc.common.Result;
+  import com.etotem.cfc.dto.HealthDimensionVO;
+  import com.etotem.cfc.dto.HealthDimensionQuestionnaireVO;
+  import com.etotem.cfc.dto.DimensionUploadVO;
+  import com.etotem.cfc.service.DimensionScoreService;
+  import lombok.extern.slf4j.Slf4j;
+  import org.springframework.web.bind.annotation.*;
+
+  import javax.annotation.Resource;
+
+  @Slf4j
+  @RestController
+  @RequestMapping("/api/health")
+  public class HealthDimensionController {
+
+      @Resource
+      private DimensionScoreService dimensionScoreService;
+
+      /** 获取某成员7维最新评分 */
+      @PostMapping("/dimensions")
+      public Result<HealthDimensionVO> getDimensions(
+              @RequestAttribute("userId") Long userId,
+              @RequestBody java.util.Map<String, Object> params) {
+          Long memberId = params.get("memberId") != null
+              ? Long.valueOf(params.get("memberId").toString())
+              : userId;
+          HealthDimensionVO vo = dimensionScoreService.getDimensions(memberId);
+          return Result.success(vo);
+      }
+
+      /** 上传数据源(PDF/照片/语音解析结果) */
+      @PostMapping("/dimensions/upload")
+      public Result<String> uploadDataSource(
+              @RequestAttribute("userId") Long userId,
+              @RequestBody DimensionUploadVO vo) {
+          if (vo.getMemberId() == null) {
+              vo.setMemberId(userId);
+          }
+          dimensionScoreService.uploadDataSource(vo);
+          return Result.success("数据源上传成功");
+      }
+
+      /** 提交问卷评分 */
+      @PostMapping("/dimensions/questionnaire")
+      public Result<String> submitQuestionnaire(
+              @RequestAttribute("userId") Long userId,
+              @RequestBody HealthDimensionQuestionnaireVO vo) {
+          if (vo.getMemberId() == null) {
+              vo.setMemberId(userId);
+          }
+          dimensionScoreService.submitQuestionnaire(vo);
+          return Result.success("问卷提交成功");
+      }
+  }
+  ```
+
+  **Step 2: 编译验证**
+  - [ ] `mvn clean compile` 编译通过
+
+  **Step 3: 检查路由冲突**
+
+  ```bash
+  grep -rn '@PostMapping' src/main/java/com/etotem/cfc/controller/ | grep -oP '"/api[^"]*"' | sort
+  ```
+
+  确认 `/api/health/dimensions`、`/api/health/dimensions/upload`、`/api/health/dimensions/questionnaire` 无冲突。
+
+  **QA Scenarios:**
+  ```
+  Scenario: Route conflict check
+    Tool: Bash
+    Steps:
+      1. cd /sc-data/cfc/cfc-backend && grep -rn '@PostMapping' src/main/java/com/etotem/cfc/controller/ | grep "health"
+    Expected Result: 只有新创建的 HealthDimensionController 包含 /api/health 路由,无重复
+    Evidence: .sisyphus/evidence/task-5-routes.txt
+
+  Scenario: API response validation
+    Tool: Bash (curl)
+    Steps:
+      1. 启动应用
+      2. curl -X POST http://localhost:9082/api/health/dimensions -H "Authorization: Bearer <test-token>" -H "Content-Type: application/json" -d '{"memberId": 1}'
+    Expected Result: 返回 JSON 包含 dimensions 数组,含7个维度的 code/name/score/percentile/level
+    Evidence: .sisyphus/evidence/task-5-api.txt
+  ```
+
+  **Commit**: YES
+  - Message: `feat(health): add HealthDimensionController with dimension/upload/questionnaire endpoints`
+  - Files: `cfc-backend/.../controller/HealthDimensionController.java`
+
+- [ ] 6. 菌群报告映射集成 — 复用现有解析数据
+
+  **What to do**:
+  扩展 `HealthReportService` 或创建一个新 Service,在菌群报告上传解析完成后,自动将解析结果映射到 `health_dimension_scores` 表。
+
+  现有流程:上传PDF → `PdfParseService.parse()` → `ParsedReportResult` → 保存到 `HealthReport` + `HealthGutFlora` + `HealthIndicator`
+
+  新增流程:解析完成后 → 调 `DimensionScoreService.uploadDataSource()` 记录数据源 → 计算维度分数 → 保存到 `health_dimension_scores`
+
+  **Step 1: Service 层菌群报告维度计算**
+
+  在 `service/impl/DimensionScoreServiceImpl.java` 中新增方法,或创建一个 `service/GutReportMappingService.java`。这里直接在 `DimensionScoreServiceImpl` 中新增 `processGutReport()` 方法:
+
+  ```java
+  // 在 DimensionScoreServiceImpl.java 中新增:
+
+  /** 处理菌群报告解析结果,映射到7维评分 */
+  public void processGutReport(Long memberId, ParsedReportResult parsedResult) {
+      if (parsedResult == null) return;
+
+      // 创建数据源记录
+      DimensionUploadVO uploadVO = new DimensionUploadVO();
+      uploadVO.setMemberId(memberId);
+      uploadVO.setSourceType("REPORT");
+      uploadVO.setDimensionCodes(Arrays.asList("nutrition", "gut", "immunity", "growth", "sleep"));
+      uploadVO.setParsedResultJson(parsedResult.toString());
+      uploadDataSource(uploadVO);
+
+      // 肠胃健康维度 (从菌群报告最完整获取)
+      saveGutScore(memberId, parsedResult);
+
+      // 营养均衡维度
+      saveNutritionScore(memberId, parsedResult);
+
+      // 免疫力维度
+      saveImmunityScore(memberId, parsedResult);
+
+      // 生长发育维度 (慢性病/易胖风险)
+      saveGrowthScore(memberId, parsedResult);
+
+      // 睡眠质量维度 (神经递质)
+      saveSleepScore(memberId, parsedResult);
+
+      log.info("菌群报告维度映射完成: memberId={}", memberId);
+  }
+
+  private void saveGutScore(Long memberId, ParsedReportResult result) {
+      // 肠胃健康 = 综合菌群多项指标加权
+      int score = average(
+          result.getGutHealthScore(),
+          result.getBalanceScore(),
+          result.getDiversityScore(),
+          result.getBeneficialScore(),
+          result.getCoreGenusScore()
+      );
+      // 如果有有害菌偏高则扣分
+      if (result.getHarmfulScore() != null && result.getHarmfulScore() > 60) {
+          score = (int)(score * 0.85);
+      }
+      saveDimensionScore(memberId, "gut", score, 1, "REPORT");
+  }
+
+  private void saveNutritionScore(Long memberId, ParsedReportResult result) {
+      int score = result.getNutritionScore() != null ? result.getNutritionScore() : 50;
+      saveDimensionScore(memberId, "nutrition", score, 1, "REPORT");
+  }
+
+  private void saveImmunityScore(Long memberId, ParsedReportResult result) {
+      // 免疫力 = 肠道屏障 + 有益菌水平 - 有害菌风险
+      int gutHealth = result.getGutHealthScore() != null ? result.getGutHealthScore() : 50;
+      int beneficial = result.getBeneficialScore() != null ? result.getBeneficialScore() : 50;
+      int harmful = result.getHarmfulScore() != null ? result.getHarmfulScore() : 50;
+
+      int score = (int)(gutHealth * 0.4 + beneficial * 0.4 - (harmful - 50) * 0.5 + 50);
+      score = Math.max(0, Math.min(100, score));
+      saveDimensionScore(memberId, "immunity", score, 1, "REPORT");
+  }
+
+  private void saveGrowthScore(Long memberId, ParsedReportResult result) {
+      // 生长发育 = 慢性病控制度 + BMI (BMI在其他数据源中)
+      int chronic = result.getChronicDiseaseScore() != null ? result.getChronicDiseaseScore() : 50;
+      saveDimensionScore(memberId, "growth", chronic, 1, "REPORT");
+  }
+
+  private void saveSleepScore(Long memberId, ParsedReportResult result) {
+      // 睡眠质量 = 神经递质指标(从indicators中提取)
+      // Phase 1: 如果没有解析出神经递质指标,暂不覆盖
+      // 可通过 HealthIndicator category="神经递质" 获取
+      // 暂时保留问卷/Tier2数据
+      log.info("睡眠维度暂不从菌群报告映射,等待神经递质指标解析完善");
+  }
+
+  private void saveDimensionScore(Long memberId, String dimension, int score, int tier, String dataSource) {
+      HealthDimensionScore record = new HealthDimensionScore();
+      record.setMemberId(memberId);
+      record.setDimension(dimension);
+      record.setScore(score);
+      record.setTier(tier);
+      record.setDataSource(dataSource);
+      record.setAssessDate(new java.sql.Date(System.currentTimeMillis()));
+      healthDimensionScoreService.saveScore(record);
+  }
+
+  private int average(Integer... values) {
+      int sum = 0, count = 0;
+      for (Integer v : values) {
+          if (v != null) { sum += v; count++; }
+      }
+      return count > 0 ? sum / count : 50;
+  }
+  ```
+
+  **Step 2: 在 HealthReportController 或现有报告处理流程中调用**
+
+  定位到 `/sc-data/cfc/cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java` 的 `uploadReport` 方法。在 `PdfParseService.parse()` 调用完成后,获取返回值中的成员ID(`subjectId`),然后调 `dimensionScoreService.processGutReport()`。
+
+  ```java
+  // 在 uploadReport 方法中,pdf解析并保存到DB之后添加:
+
+  // 7维健康评分映射 (Phase 1)
+  try {
+      HealthReport savedReport = /* 刚刚保存的报告 */;
+      if (savedReport != null && savedReport.getSubjectId() != null) {
+          dimensionScoreService.processGutReport(savedReport.getSubjectId(), parsedReport);
+      }
+  } catch (Exception e) {
+      log.error("菌群报告维度映射失败", e);
+      // 不阻断主流程
+  }
+  ```
+
+  需要注入 `DimensionScoreService`:
+  ```java
+  @Resource
+  private DimensionScoreService dimensionScoreService;
+  ```
+
+  **Step 3: 编译验证**
+  - [ ] `mvn clean compile` 编译通过
+
+  **QA Scenarios:**
+  ```
+  Scenario: Gut report mapping with existing test PDF
+    Tool: Bash (curl)
+    Steps:
+      1. 启动应用
+      2. curl -X POST http://localhost:9082/api/health/report/upload (with test PDF)
+      3. curl -X POST http://localhost:9082/api/health/dimensions -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d '{"memberId": <testMemberId>}'
+    Expected Result: dimensions 数组中的 gut/nutrition/immunity/growth 维度有 score 值,dataSource 为 "REPORT",tier=1
+    Evidence: .sisyphus/evidence/task-6-gut-mapping.txt
+  ```
+
+  **Commit**: YES
+  - Message: `feat(health): integrate gut report mapping to dimension scores`
+  - Files: `cfc-backend/.../service/impl/DimensionScoreServiceImpl.java`, `controller/HealthReportController.java`
+
+- [ ] 7. 前端 7维雷达图页面 (health-dimensions.vue)
+
+  **What to do**:
+  创建 uni-app 页面,展示7维雷达图和每个维度的详情。包括:
+  - 雷达图(用 canvas 绘制7边形)
+  - 每个维度的分数、等级、数据源标识
+  - 点击维度进入问卷填写
+  - 上传数据源入口
+
+  **Step 1: Create pages/body/health-dimensions.vue**
+
+  `/sc-data/cfc/cfc-frontend/pages/body/health-dimensions.vue`
+
+  ```vue
+  <template>
+    <view class="page">
+      <!-- 头部 -->
+      <view class="header">
+        <text class="title">健康画像</text>
+        <text class="subtitle">{{ assessDate }} · 基于{{ dataSourceLabel }}评估</text>
+      </view>
+
+      <!-- 雷达图区域 -->
+      <view class="radar-container">
+        <canvas canvas-id="radarCanvas" class="radar-canvas"></canvas>
+      </view>
+
+      <!-- 图例 -->
+      <view class="legend">
+        <view class="legend-item" v-for="dim in dimensions" :key="dim.code">
+          <view class="legend-dot" :style="{ background: levelColor(dim.level) }"></view>
+          <text class="legend-label">{{ dim.name }}</text>
+          <text class="legend-score" :style="{ color: levelColor(dim.level) }">
+            {{ dim.score !== null ? dim.score : '--' }}
+          </text>
+          <text class="legend-percentile" v-if="dim.percentile !== null">
+            P{{ dim.percentile }}
+          </text>
+        </view>
+      </view>
+
+      <!-- 操作入口 -->
+      <view class="actions">
+        <view class="action-btn" @click="showUploadSheet">
+          <text class="action-icon">📷</text>
+          <text class="action-text">上传报告/照片</text>
+        </view>
+        <view class="action-btn" @click="showVoiceInput">
+          <text class="action-icon">🎤</text>
+          <text class="action-text">语音输入</text>
+        </view>
+      </view>
+
+      <!-- 维度问卷入口 -->
+      <view class="dimension-list">
+        <view
+          class="dimension-card"
+          v-for="dim in dimensions"
+          :key="dim.code"
+          @click="goQuestionnaire(dim)"
+        >
+          <view class="dim-left">
+            <text class="dim-name">{{ dim.name }}</text>
+            <text class="dim-desc">{{ dimDesc(dim) }}</text>
+          </view>
+          <view class="dim-right">
+            <view class="dim-score" :style="{ color: levelColor(dim.level) }">
+              {{ dim.score !== null ? dim.score : '--' }}
+            </view>
+            <view class="dim-level" :style="{ background: levelBgColor(dim.level) }">
+              {{ dim.levelLabel }}
+            </view>
+            <text class="dim-source">{{ sourceLabel(dim.tier) }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 上传选项弹窗 -->
+      <uni-popup ref="uploadPopup" type="bottom">
+        <view class="upload-sheet">
+          <view class="sheet-item" @click="uploadPhoto('scale')">
+            <text class="sheet-icon">⚖️</text>
+            <view>
+              <text class="sheet-title">体脂称照片</text>
+              <text class="sheet-desc">拍照识别体重/体脂率</text>
+            </view>
+          </view>
+          <view class="sheet-item" @click="uploadPhoto('tongue')">
+            <text class="sheet-icon">👅</text>
+            <view>
+              <text class="sheet-title">舌诊照片</text>
+              <text class="sheet-desc">拍照分析肠胃/免疫力</text>
+            </view>
+          </view>
+          <view class="sheet-item" @click="uploadPhoto('exam')">
+            <text class="sheet-icon">🏥</text>
+            <view>
+              <text class="sheet-title">体检报告</text>
+              <text class="sheet-desc">拍照自动提取指标</text>
+            </view>
+          </view>
+          <view class="sheet-cancel" @click="hideUploadSheet">取消</view>
+        </view>
+      </uni-popup>
+    </view>
+  </template>
+
+  <script>
+  const API_BASE = require('../../config').API_BASE_URL
+
+  export default {
+    data() {
+      return {
+        dimensions: [],
+        assessDate: '',
+        token: ''
+      }
+    },
+
+    onLoad() {
+      this.token = uni.getStorageSync('token')
+      this.loadDimensions()
+    },
+
+    onReady() {
+      // canvas 需要在 onReady 后绘制
+      setTimeout(() => this.drawRadar(), 300)
+    },
+
+    methods: {
+      loadDimensions() {
+        uni.request({
+          url: API_BASE + '/api/health/dimensions',
+          method: 'POST',
+          header: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
+          data: {},
+          success: (res) => {
+            if (res.data.code === 0) {
+              this.dimensions = res.data.data.dimensions || []
+              this.assessDate = res.data.data.assessDate || ''
+              this.$nextTick(() => this.drawRadar())
+            }
+          }
+        })
+      },
+
+      drawRadar() {
+        const ctx = uni.createCanvasContext('radarCanvas', this)
+        const width = 300
+        const height = 300
+        const centerX = width / 2
+        const centerY = height / 2
+        const radius = 120
+        const count = this.dimensions.length || 7
+
+        if (count === 0) return
+
+        // 清除
+        ctx.clearRect(0, 0, width, height)
+
+        // 绘制网格 (同心多边形)
+        const levels = [0.2, 0.4, 0.6, 0.8, 1.0]
+        ctx.setStrokeStyle('#e0e0e0')
+        ctx.setLineWidth(1)
+        for (const l of levels) {
+          this.drawPolygon(ctx, centerX, centerY, radius * l, count)
+        }
+
+        // 绘制轴线
+        ctx.setStrokeStyle('#ccc')
+        ctx.setLineWidth(1)
+        for (let i = 0; i < count; i++) {
+          const angle = (Math.PI * 2 / count) * i - Math.PI / 2
+          const x = centerX + radius * Math.cos(angle)
+          const y = centerY + radius * Math.sin(angle)
+          ctx.beginPath()
+          ctx.moveTo(centerX, centerY)
+          ctx.lineTo(x, y)
+          ctx.stroke()
+        }
+
+        // 绘制数据区域
+        const validDims = this.dimensions.filter(d => d.score !== null)
+        if (validDims.length > 0) {
+          ctx.beginPath()
+          for (let i = 0; i < count; i++) {
+            const dim = this.dimensions[i]
+            const value = dim.score !== null ? dim.score / 100 : 0
+            const angle = (Math.PI * 2 / count) * i - Math.PI / 2
+            const x = centerX + radius * value * Math.cos(angle)
+            const y = centerY + radius * value * Math.sin(angle)
+            if (i === 0) ctx.moveTo(x, y)
+            else ctx.lineTo(x, y)
+          }
+          ctx.closePath()
+          ctx.setFillStyle('rgba(249, 115, 22, 0.2)')
+          ctx.setStrokeStyle('#F97316')
+          ctx.setLineWidth(2)
+          ctx.fill()
+          ctx.stroke()
+        }
+
+        // 绘制维度标签
+        ctx.setFontSize(12)
+        ctx.setFillStyle('#333')
+        ctx.setTextAlign('center')
+        for (let i = 0; i < count; i++) {
+          const dim = this.dimensions[i]
+          const angle = (Math.PI * 2 / count) * i - Math.PI / 2
+          const labelRadius = radius + 25
+          const x = centerX + labelRadius * Math.cos(angle)
+          const y = centerY + labelRadius * Math.sin(angle)
+          ctx.fillText(dim.name, x, y)
+        }
+
+        ctx.draw()
+      },
+
+      drawPolygon(ctx, cx, cy, r, n) {
+        ctx.beginPath()
+        for (let i = 0; i < n; i++) {
+          const angle = (Math.PI * 2 / n) * i - Math.PI / 2
+          const x = cx + r * Math.cos(angle)
+          const y = cy + r * Math.sin(angle)
+          if (i === 0) ctx.moveTo(x, y)
+          else ctx.lineTo(x, y)
+        }
+        ctx.closePath()
+        ctx.stroke()
+      },
+
+      levelColor(level) {
+        const map = { 'excellent': '#22c55e', 'good': '#86efac', 'average': '#eab308', 'concern': '#ef4444', 'unknown': '#999' }
+        return map[level] || '#999'
+      },
+
+      levelBgColor(level) {
+        const map = { 'excellent': '#dcfce7', 'good': '#f0fdf4', 'average': '#fef9c3', 'concern': '#fee2e2', 'unknown': '#f3f4f6' }
+        return map[level] || '#f3f4f6'
+      },
+
+      dimDesc(dim) {
+        if (dim.score === null) return '尚未评估,点击开始'
+        const levelText = { 'excellent': '高于大多数同龄人', 'good': '处于良好水平', 'average': '处于一般水平', 'concern': '建议关注改善' }
+        return levelText[dim.level] || '暂无数据'
+      },
+
+      sourceLabel(tier) {
+        const map = { 1: '自动', 2: '语音', 3: '手动' }
+        return map[tier] || ''
+      },
+
+      get dataSourceLabel() {
+        const maxTier = Math.min(...this.dimensions.map(d => d.tier || 3))
+        const map = { 1: '报告/照片', 2: '语音', 3: '手动填写' }
+        return map[maxTier] || '手动填写'
+      },
+
+      goQuestionnaire(dim) {
+        uni.navigateTo({
+          url: '/pages/body/questionnaire?dimension=' + dim.code + '&name=' + dim.name
+        })
+      },
+
+      showUploadSheet() {
+        this.$refs.uploadPopup && this.$refs.uploadPopup.open()
+      },
+
+      hideUploadSheet() {
+        this.$refs.uploadPopup && this.$refs.uploadPopup.close()
+      },
+
+      uploadPhoto(type) {
+        this.hideUploadSheet()
+        uni.chooseImage({
+          count: 1,
+          success: (res) => {
+            const tempPath = res.tempFilePaths[0]
+            // Phase 1: 上传照片待OCR服务实现后处理,先提示
+            uni.showToast({ title: '照片已接收,解析功能开发中', icon: 'none' })
+          }
+        })
+      },
+
+      showVoiceInput() {
+        uni.showToast({ title: '语音输入功能开发中', icon: 'none' })
+      }
+    }
+  }
+  </script>
+
+  <style scoped>
+  .page { padding: 20rpx 30rpx; background: #f5f5f5; min-height: 100vh; }
+  .header { margin-bottom: 30rpx; }
+  .title { font-size: 36rpx; font-weight: bold; color: #333; }
+  .subtitle { font-size: 24rpx; color: #999; margin-top: 10rpx; display: block; }
+
+  .radar-container { background: #fff; border-radius: 20rpx; padding: 30rpx; margin-bottom: 30rpx; display: flex; justify-content: center; }
+  .radar-canvas { width: 600rpx; height: 600rpx; }
+
+  .legend { display: flex; flex-wrap: wrap; gap: 20rpx; margin-bottom: 30rpx; }
+  .legend-item { display: flex; align-items: center; gap: 8rpx; background: #fff; padding: 12rpx 20rpx; border-radius: 30rpx; }
+  .legend-dot { width: 16rpx; height: 16rpx; border-radius: 50%; }
+  .legend-label { font-size: 22rpx; color: #666; }
+  .legend-score { font-size: 26rpx; font-weight: bold; }
+  .legend-percentile { font-size: 20rpx; color: #999; }
+
+  .actions { display: flex; gap: 20rpx; margin-bottom: 30rpx; }
+  .action-btn { flex: 1; display: flex; align-items: center; justify-content: center; gap: 10rpx; background: #fff; padding: 24rpx; border-radius: 16rpx; border: 2rpx solid #eee; }
+  .action-icon { font-size: 32rpx; }
+  .action-text { font-size: 26rpx; color: #333; }
+
+  .dimension-list { display: flex; flex-direction: column; gap: 16rpx; }
+  .dimension-card { display: flex; justify-content: space-between; align-items: center; background: #fff; padding: 24rpx; border-radius: 16rpx; }
+  .dim-left { flex: 1; }
+  .dim-name { font-size: 28rpx; font-weight: bold; color: #333; display: block; }
+  .dim-desc { font-size: 22rpx; color: #999; margin-top: 6rpx; display: block; }
+  .dim-right { display: flex; align-items: center; gap: 12rpx; }
+  .dim-score { font-size: 36rpx; font-weight: bold; }
+  .dim-level { font-size: 20rpx; padding: 4rpx 12rpx; border-radius: 20rpx; }
+  .dim-source { font-size: 20rpx; color: #999; }
+
+  .upload-sheet { background: #fff; border-radius: 20rpx 20rpx 0 0; padding: 30rpx; }
+  .sheet-item { display: flex; align-items: center; gap: 20rpx; padding: 24rpx 0; border-bottom: 2rpx solid #f0f0f0; }
+  .sheet-icon { font-size: 40rpx; }
+  .sheet-title { font-size: 28rpx; color: #333; display: block; }
+  .sheet-desc { font-size: 22rpx; color: #999; display: block; }
+  .sheet-cancel { text-align: center; padding: 24rpx; color: #999; font-size: 28rpx; }
+  </style>
+  ```
+
+  **Verification:**
+  - [ ] 页面编译正常(uni-app 微信开发者工具导入后无报错)
+  - [ ] 雷达图渲染正常
+
+  **QA Scenarios:**
+  ```
+  Scenario: Frontend compilation
+    Tool: Bash
+    Steps:
+      1. cd /sc-data/cfc/cfc-frontend && npm install --no-optional 2>/dev/null; npm run dev:mp-weixin 2>&1 | head -10
+    Expected Result: 编译成功,无语法错误
+    Note: uni-app 可能需要在微信开发者工具中验证实际渲染
+    Evidence: .sisyphus/evidence/task-7-frontend.txt
+  ```
+
+  **Commit**: YES (with Task 8)
+  - Message: `feat(health): add health-dimensions page with radar chart and questionnaire entry`
+  - Files: `cfc-frontend/pages/body/health-dimensions.vue`
+
+- [ ] 8. 页面路由注册与导航
+
+  **What to do**:
+  在 `pages.json` 中注册新页面,并在适当的入口添加导航(如 "身体" Tab 下或 index 页面)。
+
+  **Step 1: 在 pages.json 中注册**
+
+  `/sc-data/cfc/cfc-frontend/pages.json`
+
+  在 `pages` 数组中找到 `pages/body/` 相关的条目,在附近添加:
+
+  ```json
+  {
+    "path": "pages/body/health-dimensions",
+    "style": {
+      "navigationBarTitleText": "健康画像"
+    }
+  }
+  ```
+
+  **Step 2: 添加导航入口**
+
+  在 `pages/body/index.vue` 或适当的页面中,添加一个入口按钮导航到 health-dimensions 页面:
+
+  ```vue
+  <!-- 在某个卡片或按钮中 -->
+  <view class="menu-item" @click="goHealthDimensions">
+    <text class="menu-icon">📊</text>
+    <text class="menu-text">健康画像</text>
+    <text class="menu-arrow">›</text>
+  </view>
+  ```
+
+  ```javascript
+  methods: {
+    goHealthDimensions() {
+      uni.navigateTo({ url: '/pages/body/health-dimensions' })
+    }
+  }
+  ```
+
+  > 具体入口位置需要确认 body/index.vue 的现有布局后确定。基本原则是放在"身体"相关功能区域。
+
+  **QA Scenarios:**
+  ```
+  Scenario: Route registration
+    Tool: Bash (grep)
+    Steps:
+      1. grep '"pages/body/health-dimensions"' /sc-data/cfc/cfc-frontend/pages.json
+    Expected Result: 找到路由注册条目
+    Evidence: .sisyphus/evidence/task-8-route.txt
+  ```
+
+  **Commit**: YES (with Task 7)
+  - Message: `feat(health): register health-dimensions route and add navigation entry`
+  - Files: `cfc-frontend/pages.json`
+
+---
+
+## Final Verification Wave (MANDATORY — after ALL implementation tasks)
+
+> 4 review agents run in PARALLEL. ALL must APPROVE. Present consolidated results to user and get explicit "okay" before completing.
+>
+> **Do NOT auto-proceed after verification. Wait for user's explicit approval before marking work complete.**
+
+- [ ] F1. **Plan Compliance Audit** — `oracle`
+  Read the plan end-to-end. For each "Must Have": verify implementation exists (read Java files for entity/Service/Controller methods, check curl endpoint, run `mvn compile`). For each "Must NOT Have": search for forbidden patterns — reject with file:line if found. Check evidence files exist in .sisyphus/evidence/. Compare deliverables against plan.
+  Output: `Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT`
+
+- [ ] F2. **Code Quality Review** — `unspecified-high`
+  Run `mvn clean compile` + check for `@SuppressWarnings`, `System.out`, `e.printStackTrace()`, unused imports, missing `Serializable` on entities. Check AI slop: excessive comments, over-abstraction, generic names (data/result/item/temp).
+  Output: `Build [PASS/FAIL] | Files [N clean/N issues] | VERDICT`
+
+- [ ] F3. **Real Manual QA** — `unspecified-high`
+  Start from clean state (fresh DB). Execute EVERY QA scenario from EVERY task — follow exact steps, capture evidence. Test cross-task integration (dimension scoring + gut report mapping). Test edge cases: empty memberId, duplicate submissions. Save to `.sisyphus/evidence/final-qa/`.
+  Output: `Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT`
+
+- [ ] F4. **Scope Fidelity Check** — `deep`
+  For each task: read "What to do", read actual diff (git log/diff). Verify 1:1 — everything in spec was built (no missing), nothing beyond spec was built (no creep). Check "Must NOT do" compliance. Detect cross-task contamination: Task N touching Task M's files. Flag unaccounted changes.
+  Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | Unaccounted [CLEAN/N files] | VERDICT`
+
+---
+
+## Commit Strategy
+
+| Task(s) | Message | Scope |
+|---------|---------|-------|
+| 1 | `feat(health): add DB migration, dimension entities, and Mappers` | Backend |
+| 2 | `feat(health): implement HealthDimensionScoreService CRUD` | Backend |
+| 3 | `feat(health): add norm reference seed data` | Backend |
+| 4 | `feat(health): implement DimensionScoreService core aggregator` | Backend |
+| 5 | `feat(health): add HealthDimensionController endpoints` | Backend |
+| 6 | `feat(health): integrate gut report mapping to dimension scores` | Backend |
+| 7 | `feat(health): add health-dimensions page with radar chart` | Frontend |
+| 8 | `feat(health): register health-dimensions route` | Frontend |
+
+---
+
+## Success Criteria
+
+### Verification Commands
+```bash
+# Backend compilation
+cd /sc-data/cfc/cfc-backend && mvn clean compile
+
+# API smoke tests
+curl -X POST http://localhost:9082/api/health/dimensions -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d '{}'
+curl -X POST http://localhost:9082/api/health/dimensions/upload -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d '{"memberId":1,"sourceType":"REPORT","dimensionCodes":["gut","nutrition"]}'
+curl -X POST http://localhost:9082/api/health/dimensions/questionnaire -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d '{"memberId":1,"dimension":"sleep","answers":["1","3","2"]}'
+```
+
+### Final Checklist
+- [ ] All 8 backend tasks implemented and compiled
+- [ ] Frontend page renders radar chart
+- [ ] All verification scenarios executed with passing evidence
+- [ ] All "Must Have" present
+- [ ] All "Must NOT Have" absent
+- [ ] F1-F4 all approve
+- [ ] User gives explicit final approval
+
+
+