Browse Source

docs: P0营养师核心功能实现计划(15 Tasks,5 Waves,档案视图+缺乏分析+可视化解读)

Sisyphus 4 tháng trước cách đây
mục cha
commit
bc78cfc999

+ 2798 - 0
docs/superpowers/plans/2026-06-23-P0营养师核心功能实现计划.md

@@ -0,0 +1,2798 @@
+# P0 营养师核心功能实现计划
+
+> **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:** 实现3个P0功能:孩子营养档案统一视图、结构化营养缺乏分析、家长端报告可视化解读
+
+**Architecture:** 后端聚合+前端展示。数据孤岛聚合在 ChildNutritionProfileService,缺乏分析基于指标→营养素映射表量化计算,结果供前端可视化+AI推荐消费。
+
+**Tech Stack:** Spring Boot 2.7.18 + MyBatis-Plus + MySQL + uni-app Vue 2 (小程序) + Vue 2 + Element UI (Web)
+
+---
+
+## 文件结构概览
+
+```
+cfc-backend/src/main/java/com/etotem/cfc/
+├── entity/
+│   ├── NutritionIndicatorMapping.java    # [NEW] 指标-营养素映射
+│   └── NutritionDeficiencyRecord.java    # [NEW] 缺乏分析结果
+├── mapper/
+│   ├── NutritionIndicatorMappingMapper.java  # [NEW]
+│   └── NutritionDeficiencyRecordMapper.java # [NEW]
+├── service/
+│   ├── ChildNutritionProfileService.java     # [NEW] 档案聚合
+│   └── NutritionDeficiencyService.java      # [NEW] 缺乏分析
+├── controller/
+│   └── HealthReportController.java           # [MODIFY] 新增4个端点
+├── dto/
+│   └── HealthAnalysisResult.java             # [MODIFY] 新增deficiencyRecords字段
+└── config/
+    └── DatabaseInitializer.java              # [MODIFY] 新增2张表DDL+种子数据
+
+cfc-frontend/
+├── pages/
+│   ├── health/
+│   │   └── nutrition-profile.vue      # [NEW] 营养档案统一视图
+│   └── body/
+│       ├── index.vue                   # [MODIFY] 导航入口
+│       └── health-report.vue           # [MODIFY] 可视化解读
+└── utils/
+    └── api.js                           # [MODIFY] 新增API函数
+
+cfc-web/src/
+├── views/nutritionist/
+│   ├── NutritionMappings.vue           # [NEW] 映射表CRUD
+│   ├── HealthReports.vue               # [MODIFY] 增加缺乏分析关联
+│   └── HealthIndicators.vue           # [MODIFY] 异常指标标签
+```
+
+---
+
+## 执行策略:5个并行Wave
+
+> 每个Wave内任务可并行执行,Wave间有依赖顺序。
+
+```
+Wave 1 (DB基础层 — 3 Tasks):
+├── T1: DatabaseInitializer 新增2张表DDL + 15条种子数据
+├── T2: NutritionIndicatorMapping Entity + Mapper
+└── T3: NutritionDeficiencyRecord Entity + Mapper
+
+Wave 2 (核心服务 — 2 Tasks, after Wave1):
+├── T4: NutritionDeficiencyService 实现
+└── T5: ChildNutritionProfileService 实现
+
+Wave 3 (API端点 — 3 Tasks, after Wave2):
+├── T6: HealthReportController 新增4个端点
+├── T7: HealthAnalysisService 链式调用缺乏分析
+└── T8: MealRecommendService.buildDifyInputs 增加deficiency_matrix
+
+Wave 4 (小程序前端 — 4 Tasks, after Wave3):
+├── T9:  pages/health/nutrition-profile.vue 新页面
+├── T10: pages/body/health-report.vue 改造(解读+趋势+雷达+矩阵)
+├── T11: pages/body/index.vue 导航入口
+└── T12: utils/api.js 新增4个API函数
+
+Wave 5 (Web管理端 — 3 Tasks, after Wave3):
+├── T13: nutritionist/NutritionMappings.vue 新页面
+├── T14: nutritionist/HealthReports.vue 改造
+└── T15: nutritionist/HealthIndicators.vue 改造
+```
+
+**关键路径**: T1 → T2/T3 → T4/T5 → T6/T7/T8 → T9/T10/T11/T12 + T13/T14/T15 (并行)
+**预计总Tasks**: 15个
+**并行执行**: Wave内并行,Wave间串行
+
+---
+
+## Wave 1: DB基础层
+
+### T1: DatabaseInitializer 新增2张表DDL + 种子数据
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` — 在 `initData()` 方法末尾添加DDL + 种子SQL
+
+**参考文件**: `DatabaseInitializer.java:initData()` 方法结构
+
+- [ ] **Step 1: 找到 DatabaseInitializer.java 并阅读 initData() 结构**
+
+文件路径:`cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`
+找到 `initData()` 方法,在方法末尾(return 之前)添加新增表的SQL
+
+- [ ] **Step 2: 添加 nutrition_indicator_mapping 表 DDL**
+
+在 `initData()` 方法 return 前插入以下SQL(使用 `jdbcTemplate.execute(sql)`):
+
+```java
+// 创建 nutrition_indicator_mapping 表
+jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS nutrition_indicator_mapping (" +
+    "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
+    "indicator_name_pattern VARCHAR(200) NOT NULL COMMENT '指标名匹配模式(SQL LIKE语法,%为通配符)'," +
+    "nutrient VARCHAR(50) NOT NULL COMMENT '关联营养素名'," +
+    "severity_weight DECIMAL(3,2) DEFAULT 1.00 COMMENT '权重(0-1)'," +
+    "deficiency_symptoms TEXT COMMENT '缺乏症状描述'," +
+    "excess_symptoms TEXT COMMENT '过量症状描述'," +
+    "food_suggestions TEXT COMMENT '推荐食物(逗号分隔)'," +
+    "sort_order INT DEFAULT 0 COMMENT '排序(大值优先匹配)'," +
+    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
+    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP," +
+    "INDEX idx_nutrient (nutrient)" +
+    ") COMMENT='指标-营养素映射表'");
+```
+
+- [ ] **Step 3: 添加 nutrition_deficiency_record 表 DDL**
+
+```java
+jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS nutrition_deficiency_record (" +
+    "id BIGINT AUTO_INCREMENT PRIMARY KEY," +
+    "child_id BIGINT NOT NULL COMMENT '孩子ID'," +
+    "report_id BIGINT NOT NULL COMMENT '关联报告ID'," +
+    "nutrient VARCHAR(50) NOT NULL COMMENT '营养素名'," +
+    "deficiency_type VARCHAR(20) NOT NULL COMMENT 'deficiency/excess/borderline/unclassified'," +
+    "severity DECIMAL(5,2) DEFAULT 0 COMMENT '严重程度(0-1)'," +
+    "indicator_name VARCHAR(100) COMMENT '关联指标名'," +
+    "indicator_value VARCHAR(50) COMMENT '指标检测值'," +
+    "ref_range VARCHAR(100) COMMENT '参考范围'," +
+    "symptoms TEXT COMMENT '症状描述'," +
+    "food_suggestions TEXT COMMENT '推荐食物'," +
+    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP," +
+    "INDEX idx_child (child_id)," +
+    "INDEX idx_report (report_id)," +
+    "INDEX idx_nutrient (nutrient)" +
+    ") COMMENT='营养缺乏分析记录'");
+```
+
+- [ ] **Step 4: 添加15条种子数据**
+
+```java
+// 插入15条指标-营养素映射种子数据(使用 batchUpdate 或逐条 execute)
+String[][] seedData = {
+    {"铁蛋白", "铁", "1.00", "乏力、面色苍白、注意力不集中", "铁过量可能损伤肝脏", "动物肝脏,红肉,菠菜,黑木耳", "10"},
+    {"25-羟基维生素D", "维生素D", "1.00", "影响钙吸收、骨骼发育", "维生素D过量可致高钙血症", "三文鱼,蛋黄,蘑菇,强化牛奶", "20"},
+    {"维生素D", "维生素D", "1.00", "影响钙吸收、骨骼发育、免疫力下降", "维生素D过量可致高钙血症", "三文鱼,蛋黄,蘑菇,强化牛奶", "15"},
+    {"锌", "锌", "0.80", "食欲减退、味觉异常、免疫力降低", "锌过量影响铜吸收", "牡蛎,牛肉,南瓜子,扁豆", "10"},
+    {"钙", "钙", "0.80", "骨骼发育迟缓、牙齿问题", "高钙血症、肾结石", "牛奶,豆腐,虾皮,芝麻酱", "10"},
+    {"维生素B12", "维生素B12", "1.00", "贫血、神经发育迟缓", "一般不易过量", "牛肉,鸡蛋,牛奶,鱼肉", "10"},
+    {"维生素C", "维生素C", "0.60", "免疫力下降、牙龈出血", "腹泻、肾结石风险", "猕猴桃,橙子,草莓,青椒", "10"},
+    {"白蛋白", "蛋白质", "0.90", "发育迟缓、肌肉萎缩", "肾脏负担", "鸡蛋,鱼肉,牛奶,豆制品", "10"},
+    {"总蛋白", "蛋白质", "0.90", "营养不良、免疫力下降", "血液粘稠", "鸡蛋,鱼肉,牛奶,豆制品", "9"},
+    {"膳食纤维", "膳食纤维", "0.50", "便秘、肠道菌群失衡", "腹胀、排便过多", "全谷物,蔬菜,豆类,水果", "10"},
+    {"叶酸", "叶酸", "0.70", "贫血、发育迟缓", "一般不易过量", "菠菜,动物肝脏,豆类,柑橘", "10"},
+    {"维生素A", "维生素A", "0.70", "视力问题、皮肤干燥、免疫力下降", "头痛、肝脏损伤", "胡萝卜,动物肝脏,南瓜,菠菜", "10"},
+    {"硒", "硒", "0.50", "免疫力下降、甲状腺功能异常", "脱发、指甲脆裂", "巴西坚果,海鲜,蘑菇", "10"},
+    {"镁", "镁", "0.40", "肌肉痉挛、睡眠问题", "腹泻、低血压", "坚果,全谷物,深绿蔬菜", "10"},
+    {"益生菌%", "益生菌", "0.60", "肠道菌群失衡、消化问题", "一般不易过量", "酸奶,泡菜,味噌,康普茶", "10"}
+};
+
+for (String[] row : seedData) {
+    jdbcTemplate.update("INSERT IGNORE INTO nutrition_indicator_mapping " +
+        "(indicator_name_pattern, nutrient, severity_weight, deficiency_symptoms, excess_symptoms, food_suggestions, sort_order) " +
+        "VALUES (?, ?, ?, ?, ?, ?, ?)",
+        row[0], row[1], new BigDecimal(row[2]), row[3], row[4], row[5], Integer.parseInt(row[6]));
+}
+```
+
+- [ ] **Step 5: 编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
+git commit -m "feat(health): add nutrition_indicator_mapping and nutrition_deficiency_record tables"
+```
+
+**QA验证**:
+- 启动后端,`jdbcTemplate` 会在应用启动时自动建表并插入种子数据
+- 验证: `mysql -h 192.168.16.251 -u cfc -pcfc@123 cfc -e "SELECT COUNT(*) FROM nutrition_indicator_mapping;"` → 预期15
+
+---
+
+### T2: NutritionIndicatorMapping Entity + Mapper
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/NutritionIndicatorMapping.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/NutritionIndicatorMappingMapper.java`
+
+**参考文件**: 参考现有 entity 如 `HealthIndicator.java` 的结构和 `@TableName` 用法
+
+- [ ] **Step 1: 创建 NutritionIndicatorMapping.java**
+
+文件路径:`cfc-backend/src/main/java/com/etotem/cfc/entity/NutritionIndicatorMapping.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.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("nutrition_indicator_mapping")
+public class NutritionIndicatorMapping {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 指标名匹配模式(SQL LIKE语法, %为通配符) */
+    private String indicatorNamePattern;
+
+    /** 关联营养素名 */
+    private String nutrient;
+
+    /** 权重(0-1) */
+    private BigDecimal severityWeight;
+
+    /** 缺乏症状描述 */
+    private String deficiencySymptoms;
+
+    /** 过量症状描述 */
+    private String excessSymptoms;
+
+    /** 推荐食物(逗号分隔) */
+    private String foodSuggestions;
+
+    /** 排序(大值优先匹配) */
+    private Integer sortOrder;
+
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+}
+```
+
+- [ ] **Step 2: 创建 NutritionIndicatorMappingMapper.java**
+
+文件路径:`cfc-backend/src/main/java/com/etotem/cfc/mapper/NutritionIndicatorMappingMapper.java`
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.NutritionIndicatorMapping;
+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 NutritionIndicatorMappingMapper extends BaseMapper<NutritionIndicatorMapping> {
+
+    /**
+     * 按指标名模糊匹配映射规则(按sort_order倒序,取第一条)
+     * 使用 LIKE 语法匹配 indicatorNamePattern
+     */
+    @Select("SELECT * FROM nutrition_indicator_mapping " +
+            "WHERE #{indicatorName} LIKE indicator_name_pattern " +
+            "ORDER BY sort_order DESC LIMIT 1")
+    NutritionIndicatorMapping findMappingByIndicatorName(@Param("indicatorName") String indicatorName);
+
+    /**
+     * 获取所有映射规则(用于Web管理端列表)
+     */
+    @Select("SELECT * FROM nutrition_indicator_mapping ORDER BY sort_order DESC, nutrient ASC")
+    List<NutritionIndicatorMapping> findAllOrderBySort();
+}
+```
+
+- [ ] **Step 3: 编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/NutritionIndicatorMapping.java
+git add cfc-backend/src/main/java/com/etotem/cfc/mapper/NutritionIndicatorMappingMapper.java
+git commit -m "feat(health): add NutritionIndicatorMapping entity and mapper"
+```
+
+---
+
+### T3: NutritionDeficiencyRecord Entity + Mapper
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/NutritionDeficiencyRecord.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/NutritionDeficiencyRecordMapper.java`
+
+**参考文件**: 参考现有 entity 如 `HealthIndicator.java`
+
+- [ ] **Step 1: 创建 NutritionDeficiencyRecord.java**
+
+文件路径:`cfc-backend/src/main/java/com/etotem/cfc/entity/NutritionDeficiencyRecord.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.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("nutrition_deficiency_record")
+public class NutritionDeficiencyRecord {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 孩子ID */
+    private Long childId;
+
+    /** 关联报告ID */
+    private Long reportId;
+
+    /** 营养素名 */
+    private String nutrient;
+
+    /** 缺乏类型: deficiency/excess/borderline/unclassified */
+    private String deficiencyType;
+
+    /** 严重程度(0-1) */
+    private BigDecimal severity;
+
+    /** 关联指标名 */
+    private String indicatorName;
+
+    /** 指标检测值 */
+    private String indicatorValue;
+
+    /** 参考范围 */
+    private String refRange;
+
+    /** 症状描述 */
+    private String symptoms;
+
+    /** 推荐食物 */
+    private String foodSuggestions;
+
+    private LocalDateTime createdAt;
+}
+```
+
+- [ ] **Step 2: 创建 NutritionDeficiencyRecordMapper.java**
+
+文件路径:`cfc-backend/src/main/java/com/etotem/cfc/mapper/NutritionDeficiencyRecordMapper.java`
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.NutritionDeficiencyRecord;
+import org.apache.ibatis.annotations.Delete;
+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 NutritionDeficiencyRecordMapper extends BaseMapper<NutritionDeficiencyRecord> {
+
+    /**
+     * 获取指定报告的缺乏分析结果
+     */
+    @Select("SELECT * FROM nutrition_deficiency_record WHERE report_id = #{reportId} ORDER BY severity DESC")
+    List<NutritionDeficiencyRecord> findByReportId(@Param("reportId") Long reportId);
+
+    /**
+     * 获取孩子的最新缺乏分析结果(取最新报告的)
+     */
+    @Select("SELECT r.* FROM nutrition_deficiency_record r " +
+            "INNER JOIN (SELECT report_id FROM nutrition_deficiency_record WHERE child_id = #{childId} ORDER BY created_at DESC LIMIT 1) latest " +
+            "ON r.report_id = latest.report_id " +
+            "ORDER BY r.severity DESC")
+    List<NutritionDeficiencyRecord> findLatestByChildId(@Param("childId") Long childId);
+
+    /**
+     * 获取孩子的缺乏趋势(多报告)
+     */
+    @Select("SELECT r.nutrient, r.severity, r.deficiency_type, " +
+            "FROM nutrition_deficiency_record r " +
+            "INNER JOIN health_reports hr ON r.report_id = hr.id " +
+            "WHERE r.child_id = #{childId} " +
+            "ORDER BY hr.report_date DESC, r.severity DESC")
+    List<NutritionDeficiencyRecord> findTrendByChildId(@Param("childId") Long childId);
+
+    /**
+     * 删除指定报告的分析结果(幂等重分析用)
+     */
+    @Delete("DELETE FROM nutrition_deficiency_record WHERE report_id = #{reportId}")
+    int deleteByReportId(@Param("reportId") Long reportId);
+
+    /**
+     * 获取孩子已有分析报告的日期列表(用于趋势)
+     */
+    @Select("SELECT DISTINCT DATE(r.created_at) as report_date FROM nutrition_deficiency_record r " +
+            "WHERE r.child_id = #{childId} ORDER BY report_date DESC")
+    List<String> findReportDatesByChildId(@Param("childId") Long childId);
+}
+```
+
+- [ ] **Step 3: 编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/NutritionDeficiencyRecord.java
+git add cfc-backend/src/main/java/com/etotem/cfc/mapper/NutritionDeficiencyRecordMapper.java
+git commit -m "feat(health): add NutritionDeficiencyRecord entity and mapper"
+```
+
+---
+
+## Wave 2: 核心服务层
+
+### T4: NutritionDeficiencyService 实现
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/NutritionDeficiencyService.java`
+
+**参考文件**: `HealthAnalysisService.java` (现有分析服务逻辑), `HealthIndicator.java`, `NutritionIndicatorMappingMapper.java`
+
+**依赖**: T2 + T3 (entity+mapper已就绪)
+
+- [ ] **Step 1: 创建基础服务骨架**
+
+文件路径:`cfc-backend/src/main/java/com/etotem/cfc/service/NutritionDeficiencyService.java`
+
+```java
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.HealthIndicator;
+import com.etotem.cfc.entity.NutritionDeficiencyRecord;
+import com.etotem.cfc.entity.NutritionIndicatorMapping;
+import com.etotem.cfc.mapper.NutritionDeficiencyRecordMapper;
+import com.etotem.cfc.mapper.NutritionIndicatorMappingMapper;
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+public class NutritionDeficiencyService {
+
+    @Resource
+    private NutritionIndicatorMappingMapper mappingMapper;
+
+    @Resource
+    private NutritionDeficiencyRecordMapper deficiencyRecordMapper;
+}
+```
+
+- [ ] **Step 2: 实现 analyzeDeficiencies(reportId, childId) 方法**
+
+在 `NutritionDeficiencyService.java` 中添加:
+
+```java
+/**
+ * 对指定报告执行营养缺乏分析,结果持久化
+ * @return 分析结果列表
+ */
+public List<NutritionDeficiencyRecord> analyzeDeficiencies(Long reportId, Long childId) {
+    // 1. 获取该报告的所有异常指标(status != "正常")
+    List<HealthIndicator> abnormalIndicators = getAbnormalIndicators(reportId);
+    if (abnormalIndicators.isEmpty()) {
+        return Collections.emptyList();
+    }
+
+    // 2. 幂等:删除该报告已有分析结果
+    deficiencyRecordMapper.deleteByReportId(reportId);
+
+    // 3. 获取所有映射规则(缓存优化)
+    List<NutritionIndicatorMapping> allMappings = mappingMapper.selectList(null);
+
+    // 4. 遍历异常指标,进行匹配和分析
+    List<NutritionDeficiencyRecord> results = new ArrayList<>();
+    for (HealthIndicator indicator : abnormalIndicators) {
+        // 使用指标名匹配映射表(按sort_order倒序取第一条)
+        NutritionIndicatorMapping mapping = findBestMapping(indicator.getIndicatorName(), allMappings);
+
+        if (mapping != null) {
+            // 匹配到映射规则 → 结构化缺乏记录
+            NutritionDeficiencyRecord record = buildRecord(childId, reportId, indicator, mapping);
+            results.add(record);
+        }
+        // 未匹配 → 不生成记录(渐进增强原则:有数据展示,无数据跳过)
+    }
+
+    // 5. 同一营养素多条记录 → 合并(取severity最高)
+    List<NutritionDeficiencyRecord> merged = mergeByNutrient(results);
+
+    // 6. 批量插入数据库
+    if (!merged.isEmpty()) {
+        for (NutritionDeficiencyRecord record : merged) {
+            deficiencyRecordMapper.insert(record);
+        }
+    }
+
+    return merged;
+}
+
+/**
+ * 获取报告的异常指标列表
+ * 这里假设HealthIndicatorService有getIndicatorsByReportId方法
+ */
+private List<HealthIndicator> getAbnormalIndicators(Long reportId) {
+    // TODO: 实际从HealthIndicatorService获取,暂用空实现占位
+    // 实现时应调用: healthIndicatorService.getIndicatorsByReportId(reportId)
+    // 并过滤 status != "正常"
+    return Collections.emptyList();
+}
+
+/**
+ * 查找最佳匹配映射(sort_order最大的)
+ */
+private NutritionIndicatorMapping findBestMapping(String indicatorName, List<NutritionIndicatorMapping> mappings) {
+    for (NutritionIndicatorMapping m : mappings) {
+        String pattern = m.getIndicatorNamePattern();
+        // SQL LIKE 语义: % 匹配任意字符
+        if (matchLike(indicatorName, pattern)) {
+            return m;
+        }
+    }
+    return null;
+}
+
+/**
+ * SQL LIKE 匹配实现
+ * pattern中的%匹配零个或多个任意字符
+ */
+private boolean matchLike(String text, String pattern) {
+    // 将SQL LIKE pattern转换为Java正则
+    String regex = pattern.replace("%", ".*");
+    return text.matches(regex);
+}
+
+/**
+ * 构建缺乏记录
+ */
+private NutritionDeficiencyRecord buildRecord(Long childId, Long reportId,
+        HealthIndicator indicator, NutritionIndicatorMapping mapping) {
+    NutritionDeficiencyRecord record = new NutritionDeficiencyRecord();
+    record.setChildId(childId);
+    record.setReportId(reportId);
+    record.setNutrient(mapping.getNutrient());
+    record.setIndicatorName(indicator.getIndicatorName());
+    record.setIndicatorValue(indicator.getIndicatorValue());
+    record.setRefRange(indicator.getRefRange());
+    record.setSymptoms(buildSymptoms(indicator, mapping));
+    record.setFoodSuggestions(mapping.getFoodSuggestions());
+
+    // 计算严重程度
+    BigDecimal severity = calculateSeverity(indicator, mapping);
+    record.setSeverity(severity);
+
+    // 确定缺乏类型
+    String deficiencyType = determineDeficiencyType(indicator.getStatus());
+    record.setDeficiencyType(deficiencyType);
+
+    return record;
+}
+
+/**
+ * 计算严重程度
+ * 偏低: severity = severity_weight * (1 - actual/ref_min), 截断到[0,1]
+ * 偏高: severity = severity_weight * (actual/ref_max - 1), 截断到[0,1]
+ * 无法数值化: severity = severity_weight * 0.5
+ */
+private BigDecimal calculateSeverity(HealthIndicator indicator, NutritionIndicatorMapping mapping) {
+    String status = indicator.getStatus();
+    String value = indicator.getIndicatorValue();
+    String refRange = indicator.getRefRange();
+
+    // 尝试解析参考范围
+    double refMin = 0, refMax = 0;
+    if (refRange != null && refRange.contains("-")) {
+        try {
+            String[] parts = refRange.split("-");
+            refMin = Double.parseDouble(parts[0].trim());
+            refMax = Double.parseDouble(parts[1].trim());
+        } catch (NumberFormatException e) {
+            // 无法解析,使用默认权重
+            return mapping.getSeverityWeight().multiply(new BigDecimal("0.5"));
+        }
+    }
+
+    // 尝试解析实际值
+    double actual;
+    try {
+        actual = Double.parseDouble(value);
+    } catch (NumberFormatException e) {
+        // 无法数值化,使用中等严重度
+        return mapping.getSeverityWeight().multiply(new BigDecimal("0.5"));
+    }
+
+    BigDecimal weight = mapping.getSeverityWeight();
+    double severity = 0;
+
+    if ("偏低".equals(status) || "缺乏".equals(status)) {
+        if (refMin > 0) {
+            severity = weight.doubleValue() * Math.min(1.0, Math.max(0, 1 - actual / refMin));
+        }
+    } else if ("偏高".equals(status) || "过量".equals(status)) {
+        if (refMax > 0) {
+            severity = weight.doubleValue() * Math.min(1.0, Math.max(0, actual / refMax - 1));
+        }
+    } else {
+        // 临界/边界
+        severity = weight.doubleValue() * 0.3;
+    }
+
+    return BigDecimal.valueOf(severity).setScale(4, RoundingMode.HALF_UP);
+}
+
+/**
+ * 确定缺乏类型
+ */
+private String determineDeficiencyType(String status) {
+    if ("偏低".equals(status) || "缺乏".equals(status)) {
+        return "deficiency";
+    } else if ("偏高".equals(status) || "过量".equals(status)) {
+        return "excess";
+    } else if ("注意".equals(status) || "临界".equals(status)) {
+        return "borderline";
+    }
+    return "deficiency";
+}
+
+/**
+ * 构建症状描述(指标症状+映射表症状合并)
+ */
+private String buildSymptoms(HealthIndicator indicator, NutritionIndicatorMapping mapping) {
+    String indicatorSymptoms = indicator.getSymptoms();
+    String mappingSymptoms = mapping.getDeficiencySymptoms();
+    StringBuilder sb = new StringBuilder();
+    if (mappingSymptoms != null && !mappingSymptoms.isEmpty()) {
+        sb.append(mappingSymptoms);
+    }
+    if (indicatorSymptoms != null && !indicatorSymptoms.isEmpty()) {
+        if (sb.length() > 0) sb.append(";");
+        sb.append(indicator.getIndicatorName()).append(": ").append(indicatorSymptoms);
+    }
+    return sb.toString();
+}
+
+/**
+ * 按营养素合并(取severity最高的)
+ */
+private List<NutritionDeficiencyRecord> mergeByNutrient(List<NutritionDeficiencyRecord> records) {
+    Map<String, NutritionDeficiencyRecord> map = new LinkedHashMap<>();
+    for (NutritionDeficiencyRecord r : records) {
+        String nutrient = r.getNutrient();
+        if (!map.containsKey(nutrient) || r.getSeverity().compareTo(map.get(nutrient).getSeverity()) > 0) {
+            map.put(nutrient, r);
+        }
+    }
+    return new ArrayList<>(map.values());
+}
+```
+
+- [ ] **Step 3: 实现 getLatestDeficiencies(childId) 方法**
+
+```java
+/**
+ * 获取孩子最新缺乏分析结果
+ */
+public List<NutritionDeficiencyRecord> getLatestDeficiencies(Long childId) {
+    List<NutritionDeficiencyRecord> records = deficiencyRecordMapper.findLatestByChildId(childId);
+    return records != null ? records : Collections.emptyList();
+}
+```
+
+- [ ] **Step 4: 实现 getDeficiencyTrend(childId) 方法**
+
+```java
+/**
+ * 获取孩子缺乏趋势
+ * 返回结构: [{ nutrient, records: [{ reportDate, severity, deficiencyType }] }]
+ */
+public List<Map<String, Object>> getDeficiencyTrend(Long childId) {
+    List<String> dates = deficiencyRecordMapper.findReportDatesByChildId(childId);
+    if (dates == null || dates.isEmpty()) {
+        return Collections.emptyList();
+    }
+
+    // 获取所有记录
+    List<NutritionDeficiencyRecord> allRecords = deficiencyRecordMapper.findTrendByChildId(childId);
+
+    // 按营养素分组
+    Map<String, List<Map<String, Object>>> nutrientGroups = new LinkedHashMap<>();
+    for (NutritionDeficiencyRecord r : allRecords) {
+        String nutrient = r.getNutrient();
+        Map<String, Object> item = new HashMap<>();
+        item.put("reportDate", r.getCreatedAt().toLocalDate().toString());
+        item.put("severity", r.getSeverity());
+        item.put("deficiencyType", r.getDeficiencyType());
+        nutrientGroups.computeIfAbsent(nutrient, k -> new ArrayList<>()).add(item);
+    }
+
+    // 转换为 [{nutrient, records}] 结构
+    List<Map<String, Object>> result = new ArrayList<>();
+    for (Map.Entry<String, List<Map<String, Object>>> entry : nutrientGroups.entrySet()) {
+        Map<String, Object> nutrientTrend = new HashMap<>();
+        nutrientTrend.put("nutrient", entry.getKey());
+        nutrientTrend.put("records", entry.getValue());
+        result.add(nutrientTrend);
+    }
+    return result;
+}
+```
+
+- [ ] **Step 5: 注入 HealthIndicatorService 依赖,并实现 getAbnormalIndicators**
+
+首先读取现有的 HealthIndicatorService 确认方法签名,然后在 NutritionDeficiencyService 中注入并使用:
+
+```java
+@Resource
+private com.etotem.cfc.service.HealthIndicatorService healthIndicatorService;
+
+private List<HealthIndicator> getAbnormalIndicators(Long reportId) {
+    // 获取该报告所有指标,过滤异常
+    List<HealthIndicator> all = healthIndicatorService.getIndicatorsByReportId(reportId);
+    if (all == null) {
+        return Collections.emptyList();
+    }
+    return all.stream()
+            .filter(ind -> ind.getStatus() != null && !"正常".equals(ind.getStatus()))
+            .collect(Collectors.toList());
+}
+```
+
+**注意**: 如果 `HealthIndicatorService` 没有 `getIndicatorsByReportId` 方法,需要先在 `HealthIndicatorService` 中添加此方法(参考 `HealthReportService` 中类似方法)
+
+- [ ] **Step 6: 编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+- [ ] **Step 7: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/NutritionDeficiencyService.java
+git commit -m "feat(health): implement NutritionDeficiencyService with analyze/getLatest/getTrend"
+```
+
+**QA验证**:
+- 启动后端,调用 `/api/health/deficiency/analyze` 端点(等T6实现后)
+- 或直接插入测试数据到 health_indicators 表验证分析逻辑
+
+---### T5: ChildNutritionProfileService 实现
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/ChildNutritionProfileService.java`
+
+**参考文件**: `HealthReportService.java` (报告聚合), `MealRecommendService.java` (aggregateContext)
+
+**依赖**: T1 (表已建) — 与T4独立并行
+
+- [ ] **Step 1: 创建服务骨架**
+
+文件路径:`cfc-backend/src/main/java/com/etotem/cfc/service/ChildNutritionProfileService.java`
+
+```java
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.*;
+import com.etotem.cfc.mapper.*;
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+public class ChildNutritionProfileService {
+
+    @Resource
+    private HealthReportService healthReportService;
+
+    @Resource
+    private HealthIndicatorService healthIndicatorService;
+
+    @Resource
+    private HealthCheckinService healthCheckinService;
+
+    @Resource
+    private MealLogService mealLogService;
+
+    @Resource
+    private GrowthRecordService growthRecordService;
+
+    @Resource
+    private UserNutritionProfileService userNutritionProfileService;
+
+    @Resource
+    private NutritionDeficiencyService nutritionDeficiencyService;
+
+    @Resource
+    private UserService userService;
+
+    public Map<String, Object> getNutritionProfile(Long childId) {
+        Map<String, Object> profile = new LinkedHashMap<>();
+        profile.put("childId", childId);
+
+        HealthReport latestReport = healthReportService.getLatestReport(childId);
+        if (latestReport != null) {
+            profile.put("scores", extractScores(latestReport));
+        } else {
+            profile.put("scores", null);
+        }
+
+        List<Map<String, Object>> abnormalIndicators = getAbnormalIndicators(childId, latestReport);
+        profile.put("abnormalIndicators", abnormalIndicators);
+
+        if (latestReport != null) {
+            List<HealthIndicator> allIndicators = healthIndicatorService.getIndicatorsByReportId(latestReport.getId());
+            profile.put("normalIndicatorCount", allIndicators.stream().filter(i -> "正常".equals(i.getStatus())).count());
+            profile.put("totalIndicatorCount", allIndicators.size());
+        }
+
+        profile.put("scoreTrend", getScoreTrend(childId));
+
+        if (latestReport != null) {
+            Map<String, Object> detail = healthReportService.getReportDetail(latestReport.getId());
+            profile.put("diseaseRisks", detail != null ? detail.get("diseaseRisks") : Collections.emptyList());
+        } else {
+            profile.put("diseaseRisks", Collections.emptyList());
+        }
+
+        profile.put("recentNutrition", getRecentNutrition(childId));
+        profile.put("checkinSummary", getCheckinSummary(childId));
+
+        List<HealthReport> reports = healthReportService.getUserReports(childId, 0, 3);
+        profile.put("reportCount", reports != null ? reports.size() : 0);
+        profile.put("latestReports", reports != null ?
+                reports.stream().limit(3).map(this::toReportSummary).collect(Collectors.toList()) :
+                Collections.emptyList());
+
+        Long userId = getUserIdByChildId(childId);
+        if (userId != null) {
+            profile.put("nutritionProfile", userNutritionProfileService.getByUserId(userId));
+        }
+
+        return profile;
+    }
+
+    private Map<String, Object> extractScores(HealthReport report) {
+        Map<String, Object> scores = new LinkedHashMap<>();
+        scores.put("overall", report.getOverallScore());
+        scores.put("gutHealth", report.getGutHealthScore());
+        scores.put("nutrition", report.getNutritionScore());
+        scores.put("chronicDisease", report.getChronicDiseaseScore());
+        scores.put("gutBalance", report.getGutBalanceScore());
+        scores.put("gutDiversity", report.getGutDiversityScore());
+        scores.put("beneficialBacteria", report.getBeneficialBacteriaScore());
+        scores.put("harmfulBacteria", report.getHarmfulBacteriaScore());
+        scores.put("coreSpecies", report.getCoreSpeciesScore());
+        return scores;
+    }
+
+    private List<Map<String, Object>> getAbnormalIndicators(Long childId, HealthReport report) {
+        if (report == null) return Collections.emptyList();
+        List<HealthIndicator> all = healthIndicatorService.getIndicatorsByReportId(report.getId());
+        if (all == null) return Collections.emptyList();
+
+        return all.stream()
+                .filter(i -> i.getStatus() != null && !"正常".equals(i.getStatus()))
+                .sorted((a, b) -> {
+                    int pa = priorityOf(a.getStatus());
+                    int pb = priorityOf(b.getStatus());
+                    if (pa != pb) return pb - pa;
+                    return a.getIndicatorName().compareTo(b.getIndicatorName());
+                })
+                .limit(5)
+                .map(this::toIndicatorMap)
+                .collect(Collectors.toList());
+    }
+
+    private int priorityOf(String status) {
+        if ("偏低".equals(status) || "偏高".equals(status)) return 2;
+        if ("注意".equals(status) || "临界".equals(status)) return 1;
+        return 0;
+    }
+
+    private Map<String, Object> toIndicatorMap(HealthIndicator i) {
+        Map<String, Object> m = new HashMap<>();
+        m.put("id", i.getId());
+        m.put("indicatorName", i.getIndicatorName());
+        m.put("indicatorValue", i.getIndicatorValue());
+        m.put("unit", i.getUnit());
+        m.put("refRange", i.getRefRange());
+        m.put("status", i.getStatus());
+        m.put("category", i.getCategory());
+        m.put("symptoms", i.getSymptoms());
+        m.put("priority", priorityOf(i.getStatus()) == 2 ? "high" : priorityOf(i.getStatus()) == 1 ? "medium" : "low");
+        return m;
+    }
+
+    private List<Map<String, Object>> getScoreTrend(Long childId) {
+        List<HealthReport> reports = healthReportService.getUserReports(childId, 0, 10);
+        if (reports == null || reports.isEmpty()) return Collections.emptyList();
+
+        return reports.stream()
+                .sorted(Comparator.comparing(HealthReport::getReportDate))
+                .limit(3)
+                .map(r -> {
+                    Map<String, Object> item = new HashMap<>();
+                    item.put("reportDate", r.getReportDate());
+                    item.put("overall", r.getOverallScore());
+                    item.put("gutHealth", r.getGutHealthScore());
+                    item.put("nutrition", r.getNutritionScore());
+                    return item;
+                })
+                .collect(Collectors.toList());
+    }
+
+    private Map<String, Object> getRecentNutrition(Long childId) {
+        Map<String, Object> nutrition = new HashMap<>();
+        nutrition.put("period", "7d");
+        nutrition.put("avgDailyCalories", 1600);
+        Map<String, Map<String, Object>> coverage = new LinkedHashMap<>();
+        coverage.put("protein", Map.of("ratio", 0.8, "label", "蛋白质"));
+        coverage.put("fiber", Map.of("ratio", 0.4, "label", "膳食纤维"));
+        coverage.put("vitaminC", Map.of("ratio", 0.6, "label", "维生素C"));
+        coverage.put("calcium", Map.of("ratio", 0.7, "label", "钙"));
+        nutrition.put("nutrientCoverage", coverage);
+        return nutrition;
+    }
+
+    private Map<String, Object> getCheckinSummary(Long childId) {
+        List<HealthCheckin> checkins = healthCheckinService.getRecentCheckins(childId, 30);
+        Map<String, Object> summary = new HashMap<>();
+        summary.put("monthTotal", checkins != null ? checkins.size() : 0);
+        List<Map<String, Object>> recent7 = new ArrayList<>();
+        if (checkins != null) {
+            checkins.stream().limit(7).forEach(c -> {
+                Map<String, Object> item = new HashMap<>();
+                item.put("date", c.getCheckinDate());
+                item.put("behaviors", parseBehaviors(c.getBehaviors()));
+                item.put("mood", c.getMood());
+                recent7.add(item);
+            });
+        }
+        summary.put("recent7Days", recent7);
+        return summary;
+    }
+
+    private List<String> parseBehaviors(String behaviorsJson) {
+        if (behaviorsJson == null || behaviorsJson.isEmpty()) return Collections.emptyList();
+        try {
+            return Arrays.asList(behaviorsJson.replaceAll("[\\[\\]\"]", "").split(","));
+        } catch (Exception e) {
+            return Collections.emptyList();
+        }
+    }
+
+    private Map<String, Object> toReportSummary(HealthReport r) {
+        Map<String, Object> m = new HashMap<>();
+        m.put("id", r.getId());
+        m.put("reportType", r.getReportType());
+        m.put("overallScore", r.getOverallScore());
+        m.put("reportDate", r.getReportDate());
+        return m;
+    }
+
+    private Long getUserIdByChildId(Long childId) {
+        return null;
+    }
+}
+```
+
+- [ ] **Step 2: 确认/补全依赖方法**
+
+在实现前需确认以下方法存在:
+1. `HealthIndicatorService.getIndicatorsByReportId(reportId)` — 如不存在需添加
+2. `HealthCheckinService.getRecentCheckins(childId, days)` — 如不存在需添加
+3. `UserNutritionProfileService` + `UserNutritionProfileMapper` — 如不存在需创建
+4. `MealLogService` 获取近7天营养摘要 — 暂无则保持TODO占位
+
+- [ ] **Step 3: 编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/ChildNutritionProfileService.java
+git commit -m "feat(health): add ChildNutritionProfileService for unified nutrition profile"
+```
+
+---
+
+## Wave 3: API端点
+
+### T6: HealthReportController 新增4个端点
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java`
+
+**参考文件**: `HealthReportController.java` — 查看现有端点结构(@PostMapping + Result包装 + @RequestAttribute)
+
+**依赖**: T4 (NutritionDeficiencyService) + T5 (ChildNutritionProfileService)
+
+- [ ] **Step 1: 阅读 HealthReportController.java 确认端点风格**
+
+文件路径:`cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java`
+
+找到现有端点的写法(注意:所有端点都是 `@PostMapping`,使用 `Result.success(data)` 返回)
+
+- [ ] **Step 2: 在 HealthReportController 末尾添加4个新端点**
+
+在类末尾添加以下方法(保持与现有风格一致,使用 `@Resource` 注入 Service):
+
+```java
+@Resource
+private ChildNutritionProfileService childNutritionProfileService;
+
+@Resource
+private NutritionDeficiencyService nutritionDeficiencyService;
+
+/**
+ * 孩子营养档案统一视图 — 聚合API
+ */
+@PostMapping("/nutrition-profile")
+public Result<Map<String, Object>> getNutritionProfile(
+        @RequestBody Map<String, Object> params,
+        @RequestAttribute("userId") Long userId) {
+    Long childId = params.get("childId") != null ?
+            Long.valueOf(params.get("childId").toString()) : null;
+    if (childId == null) {
+        return Result.error("childId不能为空");
+    }
+    // TODO: 校验用户是否有权查看该孩子
+    Map<String, Object> profile = childNutritionProfileService.getNutritionProfile(childId);
+    return Result.success(profile);
+}
+
+/**
+ * 触发营养缺乏分析
+ */
+@PostMapping("/deficiency/analyze")
+public Result<List<NutritionDeficiencyRecord>> analyzeDeficiency(
+        @RequestBody Map<String, Object> params,
+        @RequestAttribute("userId") Long userId) {
+    Long reportId = params.get("reportId") != null ?
+            Long.valueOf(params.get("reportId").toString()) : null;
+    Long childId = params.get("childId") != null ?
+            Long.valueOf(params.get("childId").toString()) : null;
+    if (reportId == null || childId == null) {
+        return Result.error("reportId和childId不能为空");
+    }
+    List<NutritionDeficiencyRecord> results = nutritionDeficiencyService.analyzeDeficiencies(reportId, childId);
+    return Result.success(results);
+}
+
+/**
+ * 获取孩子最新缺乏分析数据
+ */
+@PostMapping("/deficiency/latest")
+public Result<List<NutritionDeficiencyRecord>> getLatestDeficiencies(
+        @RequestBody Map<String, Object> params,
+        @RequestAttribute("userId") Long userId) {
+    Long childId = params.get("childId") != null ?
+            Long.valueOf(params.get("childId").toString()) : null;
+    if (childId == null) {
+        return Result.error("childId不能为空");
+    }
+    List<NutritionDeficiencyRecord> results = nutritionDeficiencyService.getLatestDeficiencies(childId);
+    return Result.success(results);
+}
+
+/**
+ * 获取孩子缺乏趋势
+ */
+@PostMapping("/deficiency/trend")
+public Result<List<Map<String, Object>>> getDeficiencyTrend(
+        @RequestBody Map<String, Object> params,
+        @RequestAttribute("userId") Long userId) {
+    Long childId = params.get("childId") != null ?
+            Long.valueOf(params.get("childId").toString()) : null;
+    if (childId == null) {
+        return Result.error("childId不能为空");
+    }
+    List<Map<String, Object>> trend = nutritionDeficiencyService.getDeficiencyTrend(childId);
+    return Result.success(trend);
+}
+```
+
+**注意**: 如果 HealthReportController 中没有导入 `NutritionDeficiencyRecord`,需要添加 import:
+```java
+import com.etotem.cfc.entity.NutritionDeficiencyRecord;
+import com.etotem.cfc.service.ChildNutritionProfileService;
+import com.etotem.cfc.service.NutritionDeficiencyService;
+```
+
+- [ ] **Step 3: 编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/HealthReportController.java
+git commit -m "feat(api): add 4 health deficiency and profile endpoints"
+```
+
+---
+
+### T7: HealthAnalysisService 链式调用缺乏分析
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/HealthAnalysisService.java`
+
+**参考文件**: `HealthAnalysisService.java` — 在 `analyze()` 方法末尾链式调用
+
+**依赖**: T4 (NutritionDeficiencyService 已就绪)
+
+- [ ] **Step 1: 阅读 HealthAnalysisService.java 找到 analyze() 方法末尾**
+
+文件路径:`cfc-backend/src/main/java/com/etotem/cfc/service/HealthAnalysisService.java`
+
+找到 `analyze()` 方法,在方法末尾(return result 之前)添加链式调用
+
+- [ ] **Step 2: 注入 NutritionDeficiencyService**
+
+在类顶部注入:
+```java
+@Resource
+private NutritionDeficiencyService nutritionDeficiencyService;
+```
+
+- [ ] **Step 3: 在 analyze() 末尾添加链式调用**
+
+在 `analyze()` 方法的 `return result;` 之前插入:
+
+```java
+// 链式触发营养缺乏分析(异步或同步均可)
+try {
+    nutritionDeficiencyService.analyzeDeficiencies(reportId, childId);
+} catch (Exception e) {
+    // 缺乏分析失败不影响主流程,降级处理
+    log.warn("营养缺乏分析失败, reportId={}, childId={}, error={}", reportId, childId, e.getMessage());
+}
+```
+
+- [ ] **Step 4: 修改 HealthAnalysisResult 增加 deficiencyRecords 字段**
+
+文件路径:`cfc-backend/src/main/java/com/etotem/cfc/dto/HealthAnalysisResult.java`
+
+添加字段:
+```java
+private List<NutritionDeficiencyRecord> deficiencyRecords;
+
+// getter/setter
+public List<NutritionDeficiencyRecord> getDeficiencyRecords() { return deficiencyRecords; }
+public void setDeficiencyRecords(List<NutritionDeficiencyRecord> deficiencyRecords) { this.deficiencyRecords = deficiencyRecords; }
+```
+
+并在 analyze() 返回前,将分析结果写入 result:
+```java
+result.setDeficiencyRecords(nutritionDeficiencyService.getLatestDeficiencies(childId));
+```
+
+- [ ] **Step 5: 编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+- [ ] **Step 6: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/HealthAnalysisService.java
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/HealthAnalysisResult.java
+git commit -m "feat(health): chain NutritionDeficiencyService in HealthAnalysisService"
+```
+
+---
+
+### T8: MealRecommendService.buildDifyInputs 增加 deficiency_matrix
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/MealRecommendService.java`
+
+**参考文件**: `MealRecommendService.java` — 找到 `buildDifyInputs()` 方法
+
+**依赖**: T4 (NutritionDeficiencyService) + T6 (API端点已注册)
+
+- [ ] **Step 1: 阅读 MealRecommendService.java 找到 buildDifyInputs() 方法**
+
+文件路径:`cfc-backend/src/main/java/com/etotem/cfc/service/MealRecommendService.java`
+
+找到 `buildDifyInputs()` 方法,确认当前返回的 JSON 结构
+
+- [ ] **Step 2: 注入 NutritionDeficiencyService**
+
+在类顶部注入:
+```java
+@Resource
+private NutritionDeficiencyService nutritionDeficiencyService;
+```
+
+- [ ] **Step 3: 在 buildDifyInputs() 的返回 Map 中增加 deficiency_matrix 字段**
+
+在方法返回的 Map 中添加(获取结构化缺乏数据):
+
+```java
+// 获取结构化营养缺乏数据
+List<NutritionDeficiencyRecord> deficiencyRecords = nutritionDeficiencyService.getLatestDeficiencies(childId);
+if (deficiencyRecords != null && !deficiencyRecords.isEmpty()) {
+    List<Map<String, Object>> deficiencyMatrix = new ArrayList<>();
+    for (NutritionDeficiencyRecord record : deficiencyRecords) {
+        Map<String, Object> item = new LinkedHashMap<>();
+        item.put("nutrient", record.getNutrient());
+        item.put("type", record.getDeficiencyType());
+        item.put("severity", record.getSeverity());
+        item.put("symptoms", record.getSymptoms());
+        // food_suggestions 是逗号分隔字符串,转为 List
+        if (record.getFoodSuggestions() != null && !record.getFoodSuggestions().isEmpty()) {
+            item.put("food_suggestions", Arrays.asList(record.getFoodSuggestions().split(",")));
+        } else {
+            item.put("food_suggestions", Collections.emptyList());
+        }
+        deficiencyMatrix.add(item);
+    }
+    result.put("deficiency_matrix", deficiencyMatrix);
+} else {
+    result.put("deficiency_matrix", Collections.emptyList());
+}
+```
+
+将此代码块插入到 `result.put("abnormal_indicators", ...)` 之后
+
+- [ ] **Step 4: 编译验证**
+
+```bash
+cd cfc-backend && mvn clean compile -q
+```
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/MealRecommendService.java
+git commit -m "feat(meal): add deficiency_matrix to Dify inputs for smarter recipe recommendations"
+```
+
+---
+
+## Wave 4: 小程序前端
+
+### T9: pages/health/nutrition-profile.vue 新页面
+
+**Files:**
+- Create: `cfc-frontend/pages/health/nutrition-profile.vue`
+- Modify: `cfc-frontend/pages.json` — 注册新页面
+
+**参考文件**: `cfc-frontend/pages/body/health-report.vue` (现有报告页结构), `cfc-frontend/pages.json` (页面注册格式)
+
+**依赖**: T6 (API端点已就绪) + T12 (api.js新增API)
+
+- [ ] **Step 1: 阅读 pages.json 确认页面注册格式**
+
+找到现有页面注册格式,如:
+```json
+{
+  "path": "pages/body/health-report",
+  "style": { ... }
+}
+```
+
+- [ ] **Step 2: 创建 pages/health 目录(如果不存在)并创建 nutrition-profile.vue**
+
+文件路径:`cfc-frontend/pages/health/nutrition-profile.vue`
+
+```vue
+<template>
+  <view class="nutrition-profile">
+    <!-- 头部:孩子信息 -->
+    <view class="profile-header">
+      <view class="child-info">
+        <text class="child-name">{{ childName || '加载中...' }}</text>
+        <text class="child-age">{{ childAge }}岁 · {{ childGender }}</text>
+      </view>
+    </view>
+
+    <!-- 综合评分圆环(空状态时隐藏) -->
+    <view class="score-section" v-if="scores">
+      <view class="main-score-ring">
+        <circle-progress :percent="scores.overall" :size="160" :strokeWidth="12" color="#F97316" />
+        <view class="main-score-text">
+          <text class="score-value">{{ scores.overall }}</text>
+          <text class="score-label">综合评分</text>
+        </view>
+      </view>
+      <view class="score-items">
+        <view class="score-item">
+          <text class="score-num">{{ scores.gutHealth }}</text>
+          <text class="score-name">肠道健康</text>
+        </view>
+        <view class="score-item">
+          <text class="score-num">{{ scores.nutrition }}</text>
+          <text class="score-name">营养状况</text>
+        </view>
+        <view class="score-item">
+          <text class="score-num">{{ scores.chronicDisease }}</text>
+          <text class="score-name">慢病控制</text>
+        </view>
+        <view class="score-item">
+          <text class="score-num">{{ scores.gutBalance }}</text>
+          <text class="score-name">菌群平衡</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="empty-state" v-if="!scores">
+      <text class="empty-icon">📋</text>
+      <text class="empty-title">暂无健康报告</text>
+      <text class="empty-desc">完成首次健康检测后,这里将展示孩子的营养档案</text>
+      <button class="empty-btn" @click="goToAssessment">预约健康检测</button>
+    </view>
+
+    <!-- 需关注指标 -->
+    <view class="section" v-if="abnormalIndicators && abnormalIndicators.length > 0">
+      <view class="section-title">⚠️ 需关注指标</view>
+      <view class="indicator-list">
+        <view class="indicator-item" v-for="item in abnormalIndicators" :key="item.id">
+          <view class="indicator-left">
+            <text class="indicator-status" :class="getPriorityClass(item.priority)">
+              {{ getStatusEmoji(item.status) }}
+            </text>
+            <view class="indicator-info">
+              <text class="indicator-name">{{ item.indicatorName }}</text>
+              <text class="indicator-value">{{ item.indicatorValue }}{{ item.unit }} · {{ item.status }}</text>
+            </view>
+          </view>
+        </view>
+      </view>
+      <view class="section-more" @click="goToIndicators">查看全部指标 →</view>
+    </view>
+
+    <!-- 变化趋势 -->
+    <view class="section" v-if="scoreTrend && scoreTrend.length > 1">
+      <view class="section-title">📊 变化趋势</view>
+      <view class="trend-chart">
+        <!-- 使用 uni-canvas 绘制折线图 -->
+        <canvas canvas-id="trendCanvas" id="trendCanvas" class="trend-canvas"></canvas>
+      </view>
+      <view class="trend-legend">
+        <view class="legend-item">
+          <text class="legend-color" style="background:#F97316"></text>
+          <text>综合评分</text>
+        </view>
+        <view class="legend-item">
+          <text class="legend-color" style="background:#10B981"></text>
+          <text>肠道健康</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 营养缺乏矩阵 -->
+    <view class="section" v-if="deficiencyRecords && deficiencyRecords.length > 0">
+      <view class="section-title">🔬 营养缺乏分析</view>
+      <view class="deficiency-list">
+        <view class="deficiency-item" v-for="record in deficiencyRecords" :key="record.nutrient">
+          <view class="deficiency-info">
+            <text class="deficiency-name">{{ record.nutrient }}</text>
+            <text class="deficiency-level">{{ getDeficiencyLabel(record) }}</text>
+          </view>
+          <view class="deficiency-bar">
+            <view class="deficiency-fill" :style="{ width: (record.severity * 100) + '%', background: getSeverityColor(record.severity) }"></view>
+          </view>
+          <text class="deficiency-foods">{{ record.foodSuggestions }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 近7天饮食 -->
+    <view class="section" v-if="recentNutrition">
+      <view class="section-title">🍽️ 近7天饮食</view>
+      <view class="nutrition-coverage">
+        <view class="coverage-item" v-for="(val, key) in recentNutrition.nutrientCoverage" :key="key">
+          <text class="coverage-label">{{ val.label }}</text>
+          <view class="coverage-bar-wrap">
+            <view class="coverage-bar">
+              <view class="coverage-fill" :style="{ width: (val.ratio * 100) + '%' }"></view>
+            </view>
+            <text class="coverage-percent">{{ Math.round(val.ratio * 100) }}%</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 健康打卡 -->
+    <view class="section" v-if="checkinSummary">
+      <view class="section-title">🏃 健康打卡</view>
+      <text class="checkin-total">本月打卡 {{ checkinSummary.monthTotal }} 天</text>
+      <view class="checkin-recent">
+        <view class="checkin-day" v-for="day in checkinSummary.recent7Days" :key="day.date">
+          <text class="day-date">{{ formatDate(day.date) }}</text>
+          <view class="day-behaviors">
+            <text v-for="b in day.behaviors" :key="b" class="behavior-tag">{{ b }}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 报告列表 -->
+    <view class="section" v-if="latestReports && latestReports.length > 0">
+      <view class="section-title">📋 报告列表</view>
+      <view class="report-list">
+        <view class="report-item" v-for="r in latestReports" :key="r.id" @click="goToReport(r.id)">
+          <view class="report-left">
+            <text class="report-type">{{ getReportTypeName(r.reportType) }}</text>
+            <text class="report-date">{{ r.reportDate }}</text>
+          </view>
+          <view class="report-right">
+            <text class="report-score">{{ r.overallScore }}分</text>
+            <text class="report-arrow">→</text>
+          </view>
+        </view>
+      </view>
+      <view class="section-more" @click="goToReportList">查看全部 →</view>
+    </view>
+
+    <!-- AI推荐入口 -->
+    <view class="section">
+      <button class="ai-btn" @click="goToAIRecommend">🥗 获取AI个性化推荐</button>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      childId: null,
+      childName: '',
+      childAge: 0,
+      childGender: '',
+      scores: null,
+      abnormalIndicators: [],
+      normalIndicatorCount: 0,
+      totalIndicatorCount: 0,
+      scoreTrend: [],
+      diseaseRisks: [],
+      recentNutrition: null,
+      checkinSummary: null,
+      reportCount: 0,
+      latestReports: [],
+      nutritionProfile: null,
+      deficiencyRecords: []
+    };
+  },
+  onLoad(options) {
+    this.childId = options.childId || 1;
+    this.loadProfile();
+  },
+  methods: {
+    loadProfile() {
+      this.$api.getNutritionProfile(this.childId).then(res => {
+        if (res.code === 200 && res.data) {
+          const d = res.data;
+          this.scores = d.scores;
+          this.abnormalIndicators = d.abnormalIndicators || [];
+          this.normalIndicatorCount = d.normalIndicatorCount || 0;
+          this.totalIndicatorCount = d.totalIndicatorCount || 0;
+          this.scoreTrend = d.scoreTrend || [];
+          this.diseaseRisks = d.diseaseRisks || [];
+          this.reportCount = d.reportCount || 0;
+          this.latestReports = d.latestReports || [];
+          this.nutritionProfile = d.nutritionProfile;
+          // 营养偏好
+          if (d.nutritionProfile) {
+            this.nutritionProfile = d.nutritionProfile;
+          }
+        }
+      });
+
+      // 分别加载缺乏分析数据
+      this.$api.getDeficiencyAnalysis(this.childId).then(res => {
+        if (res.code === 200 && res.data) {
+          this.deficiencyRecords = res.data;
+        }
+      });
+
+      // TODO: 加载饮食摘要数据(等 mealLogService 完善)
+    },
+    getPriorityClass(priority) {
+      if (priority === 'high') return 'priority-high';
+      if (priority === 'medium') return 'priority-medium';
+      return '';
+    },
+    getStatusEmoji(status) {
+      if (status === '偏低') return '🔴';
+      if (status === '偏高') return '🔴';
+      if (status === '注意') return '🟡';
+      return '🟢';
+    },
+    getDeficiencyLabel(record) {
+      if (record.deficiencyType === 'deficiency') {
+        if (record.severity >= 0.7) return '严重缺乏';
+        if (record.severity >= 0.3) return '缺乏';
+        return '轻度不足';
+      }
+      if (record.deficiencyType === 'excess') return '偏高';
+      if (record.deficiencyType === 'borderline') return '边界值';
+      return '未分类';
+    },
+    getSeverityColor(severity) {
+      if (severity >= 0.7) return '#EF4444';
+      if (severity >= 0.3) return '#F59E0B';
+      return '#10B981';
+    },
+    getReportTypeName(type) {
+      const map = { gut_flora: '肠道检测', physical_exam: '体检报告', blood: '血液检测' };
+      return map[type] || type || '健康报告';
+    },
+    formatDate(date) {
+      if (!date) return '';
+      return date.substring(5);
+    },
+    goToReport(id) {
+      uni.navigateTo({ url: `/pages/body/health-report?id=${id}&childId=${this.childId}` });
+    },
+    goToReportList() {
+      uni.navigateTo({ url: `/pages/health/report-list?childId=${this.childId}` });
+    },
+    goToIndicators() {
+      uni.navigateTo({ url: `/pages/body/health-report?childId=${this.childId}&tab=indicators` });
+    },
+    goToAssessment() {
+      uni.navigateTo({ url: `/pages/assessment/apply` });
+    },
+    goToAIRecommend() {
+      uni.navigateTo({ url: `/pages/meal/recommend?childId=${this.childId}` });
+    }
+  }
+};
+</script>
+
+<style scoped>
+.nutrition-profile {
+  min-height: 100vh;
+  background: #FFF7ED;
+  padding-bottom: 40rpx;
+}
+.profile-header {
+  padding: 40rpx 30rpx 20rpx;
+}
+.child-info {
+  display: flex;
+  flex-direction: column;
+}
+.child-name {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #1F2937;
+}
+.child-age {
+  font-size: 26rpx;
+  color: #6B7280;
+  margin-top: 8rpx;
+}
+.score-section {
+  padding: 0 30rpx;
+  background: white;
+  border-radius: 24rpx;
+  margin: 0 20rpx;
+  padding: 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.05);
+}
+.main-score-ring {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-bottom: 30rpx;
+}
+.main-score-text {
+  position: absolute;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.score-value {
+  font-size: 56rpx;
+  font-weight: bold;
+  color: #F97316;
+}
+.score-label {
+  font-size: 24rpx;
+  color: #9CA3AF;
+}
+.score-items {
+  display: grid;
+  grid-template-columns: 1fr 1fr 1fr 1fr;
+  gap: 20rpx;
+}
+.score-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.score-num {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #374151;
+}
+.score-name {
+  font-size: 22rpx;
+  color: #9CA3AF;
+  margin-top: 4rpx;
+}
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 80rpx 30rpx;
+}
+.empty-icon {
+  font-size: 80rpx;
+  margin-bottom: 20rpx;
+}
+.empty-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #374151;
+}
+.empty-desc {
+  font-size: 28rpx;
+  color: #9CA3AF;
+  text-align: center;
+  margin: 16rpx 0 30rpx;
+}
+.empty-btn {
+  background: #F97316;
+  color: white;
+  border-radius: 40rpx;
+  padding: 20rpx 60rpx;
+  font-size: 28rpx;
+}
+.section {
+  background: white;
+  border-radius: 24rpx;
+  margin: 20rpx;
+  padding: 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.05);
+}
+.section-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #374151;
+  margin-bottom: 20rpx;
+}
+.indicator-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+.indicator-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.indicator-left {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+}
+.indicator-status {
+  font-size: 32rpx;
+}
+.indicator-info {
+  display: flex;
+  flex-direction: column;
+}
+.indicator-name {
+  font-size: 28rpx;
+  color: #374151;
+}
+.indicator-value {
+  font-size: 24rpx;
+  color: #9CA3AF;
+  margin-top: 4rpx;
+}
+.section-more {
+  text-align: center;
+  color: #F97316;
+  font-size: 26rpx;
+  margin-top: 20rpx;
+}
+.trend-canvas {
+  width: 100%;
+  height: 300rpx;
+}
+.trend-legend {
+  display: flex;
+  gap: 30rpx;
+  justify-content: center;
+  margin-top: 16rpx;
+}
+.legend-item {
+  display: flex;
+  align-items: center;
+  gap: 8rpx;
+  font-size: 24rpx;
+  color: #6B7280;
+}
+.legend-color {
+  width: 20rpx;
+  height: 20rpx;
+  border-radius: 4rpx;
+}
+.deficiency-list {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+.deficiency-item {
+  display: flex;
+  flex-direction: column;
+  gap: 8rpx;
+}
+.deficiency-info {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.deficiency-name {
+  font-size: 28rpx;
+  color: #374151;
+  font-weight: 500;
+}
+.deficiency-level {
+  font-size: 24rpx;
+  color: #9CA3AF;
+}
+.deficiency-bar {
+  height: 12rpx;
+  background: #F3F4F6;
+  border-radius: 6rpx;
+  overflow: hidden;
+}
+.deficiency-fill {
+  height: 100%;
+  border-radius: 6rpx;
+}
+.deficiency-foods {
+  font-size: 22rpx;
+  color: #6B7280;
+}
+.nutrition-coverage {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+.coverage-item {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+}
+.coverage-label {
+  width: 100rpx;
+  font-size: 26rpx;
+  color: #374151;
+}
+.coverage-bar-wrap {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+}
+.coverage-bar {
+  flex: 1;
+  height: 16rpx;
+  background: #F3F4F6;
+  border-radius: 8rpx;
+  overflow: hidden;
+}
+.coverage-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #F97316, #FB923C);
+  border-radius: 8rpx;
+}
+.coverage-percent {
+  width: 80rpx;
+  font-size: 24rpx;
+  color: #6B7280;
+  text-align: right;
+}
+.checkin-total {
+  font-size: 28rpx;
+  color: #6B7280;
+  margin-bottom: 20rpx;
+}
+.checkin-recent {
+  display: flex;
+  flex-direction: column;
+  gap: 12rpx;
+}
+.checkin-day {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+}
+.day-date {
+  width: 100rpx;
+  font-size: 24rpx;
+  color: #9CA3AF;
+}
+.day-behaviors {
+  display: flex;
+  gap: 8rpx;
+  flex-wrap: wrap;
+}
+.behavior-tag {
+  background: #FFF7ED;
+  color: #F97316;
+  font-size: 22rpx;
+  padding: 4rpx 12rpx;
+  border-radius: 16rpx;
+}
+.report-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+.report-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16rpx;
+  background: #FFF7ED;
+  border-radius: 16rpx;
+}
+.report-type {
+  font-size: 28rpx;
+  color: #374151;
+}
+.report-date {
+  font-size: 24rpx;
+  color: #9CA3AF;
+  margin-top: 4rpx;
+}
+.report-right {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+}
+.report-score {
+  font-size: 28rpx;
+  color: #F97316;
+  font-weight: bold;
+}
+.report-arrow {
+  color: #9CA3AF;
+}
+.ai-btn {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  color: white;
+  border-radius: 40rpx;
+  padding: 24rpx;
+  font-size: 30rpx;
+  text-align: center;
+  border: none;
+}
+</style>
+```
+
+**注意**: 小程序禁止使用可选链 `?.` — 已全部用 `&&` 替代;使用 Vue 2 Options API
+
+- [ ] **Step 3: 在 pages.json 注册新页面**
+
+在 `cfc-frontend/pages.json` 中找到页面数组末尾,在 `pages/body/` 路由之后、`pages/assessment/` 路由之前添加:
+
+```json
+{
+  "path": "pages/health/nutrition-profile",
+  "style": {
+    "navigationBarTitleText": "孩子营养档案",
+    "navigationBarBackgroundColor": "#FFF7ED",
+    "navigationBarTextStyle": "black"
+  }
+}
+```
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-frontend/pages/health/nutrition-profile.vue
+git add cfc-frontend/pages.json
+git commit -m "feat(frontend): add nutrition-profile unified view page"
+```
+
+---
+
+### T10: pages/body/health-report.vue 改造(解读+趋势+雷达+矩阵)
+
+**Files:**
+- Modify: `cfc-frontend/pages/body/health-report.vue`
+
+**参考文件**: 现有 `health-report.vue` 结构,了解现有环形分数和指标表格的位置
+
+**依赖**: T6 (API端点) + T12 (api.js新增API)
+
+- [ ] **Step 1: 阅读现有 health-report.vue 确认结构**
+
+找到以下区域:1. 评分圆环区域 2. 指标表格区域 3. 页面底部
+
+- [ ] **Step 2: 在评分圆环区域下方增加通俗解读卡片**
+
+在指标表格区域上方(现有结构)添加:
+
+```vue
+<!-- 通俗解读卡片 -->
+<view class="interpretation-section" v-if="deficiencyRecords && deficiencyRecords.length > 0">
+  <view class="section-title">💡 这意味着什么?</view>
+  <view class="interpretation-list">
+    <view class="interpretation-item" v-for="record in deficiencyRecords" :key="record.nutrient">
+      <view class="interp-header">
+        <text class="interp-emoji">{{ getInterpretEmoji(record) }}</text>
+        <text class="interp-nutrient">{{ record.nutrient }}</text>
+        <text class="interp-status">{{ record.deficiencyType === 'deficiency' ? '偏低' : record.deficiencyType === 'excess' ? '偏高' : '注意' }}</text>
+      </view>
+      <text class="interp-symptoms">{{ record.symptoms || '暂无详细症状描述' }}</text>
+      <text class="interp-food" v-if="record.foodSuggestions">🍽️ {{ record.foodSuggestions }}</text>
+    </view>
+  </view>
+</view>
+```
+
+对应的样式(添加在 `<style scoped>` 中):
+
+```css
+.interpretation-section {
+  background: white;
+  border-radius: 24rpx;
+  margin: 20rpx;
+  padding: 30rpx;
+}
+.interpretation-list {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+  margin-top: 16rpx;
+}
+.interpretation-item {
+  display: flex;
+  flex-direction: column;
+  gap: 8rpx;
+  padding: 20rpx;
+  background: #FFF7ED;
+  border-radius: 16rpx;
+}
+.interp-header {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+}
+.interp-emoji {
+  font-size: 32rpx;
+}
+.interp-nutrient {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #374151;
+}
+.interp-status {
+  font-size: 24rpx;
+  color: #9CA3AF;
+}
+.interp-symptoms {
+  font-size: 26rpx;
+  color: #6B7280;
+  line-height: 1.5;
+}
+.interp-food {
+  font-size: 24rpx;
+  color: #F97316;
+  margin-top: 8rpx;
+}
+```
+
+- [ ] **Step 3: 在评分区域增加趋势图**
+
+在综合评分圆环下方(圆环 section 内)添加:
+
+```vue
+<!-- 趋势对比(单份报告时不显示) -->
+<view class="trend-section" v-if="scoreTrend && scoreTrend.length > 1">
+  <view class="trend-title">📈 健康评分变化</view>
+  <view class="trend-chart-wrap">
+    <canvas canvas-id="scoreTrendCanvas" id="scoreTrendCanvas" class="score-trend-canvas"></canvas>
+  </view>
+  <view class="trend-summary">
+    <text v-for="(item, idx) in scoreTrend" :key="idx" class="trend-point">
+      {{ formatTrendDate(item.reportDate) }}: {{ item.overall }}分
+      <text v-if="idx < scoreTrend.length - 1">{{ item.overall < scoreTrend[idx+1].overall ? '↑' : item.overall > scoreTrend[idx+1].overall ? '↓' : '→' }}</text>
+    </text>
+  </view>
+</view>
+```
+
+样式:
+
+```css
+.trend-section {
+  padding: 20rpx 0 0;
+  border-top: 1rpx solid #F3F4F6;
+  margin-top: 20rpx;
+}
+.trend-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #374151;
+  margin-bottom: 16rpx;
+}
+.score-trend-canvas {
+  width: 100%;
+  height: 250rpx;
+}
+.trend-summary {
+  display: flex;
+  justify-content: space-around;
+  margin-top: 16rpx;
+}
+.trend-point {
+  font-size: 24rpx;
+  color: #6B7280;
+}
+```
+
+- [ ] **Step 4: 在页面底部增加营养缺乏矩阵卡片**
+
+在页面底部(`</view>` 结束前)添加:
+
+```vue
+<!-- 营养缺乏矩阵 -->
+<view class="deficiency-matrix-section" v-if="deficiencyRecords && deficiencyRecords.length > 0">
+  <view class="section-title">🔬 营养缺乏分析</view>
+  <view class="matrix-table">
+    <view class="matrix-header">
+      <text class="matrix-col-nutrient">营养素</text>
+      <text class="matrix-col-level">状态</text>
+      <text class="matrix-col-bar">缺乏程度</text>
+    </view>
+    <view class="matrix-row" v-for="record in deficiencyRecords" :key="record.nutrient">
+      <text class="matrix-nutrient">{{ record.nutrient }}</text>
+      <text class="matrix-level">{{ getDeficiencyLevelLabel(record) }}</text>
+      <view class="matrix-bar-wrap">
+        <view class="matrix-bar">
+          <view class="matrix-fill" :style="{ width: (record.severity * 100) + '%', background: getSeverityBarColor(record.severity) }"></view>
+        </view>
+      </view>
+    </view>
+  </view>
+  <view class="matrix-priority" v-if="deficiencyRecords.length > 0">
+    <text class="priority-title">🎯 营养改善优先级:</text>
+    <view v-for="(record, idx) in deficiencyRecords.slice(0, 3)" :key="record.nutrient">
+      <text class="priority-item">{{ idx + 1 }}. {{ record.nutrient }} → {{ record.foodSuggestions }}</text>
+    </view>
+  </view>
+</view>
+```
+
+样式:
+
+```css
+.deficiency-matrix-section {
+  background: white;
+  border-radius: 24rpx;
+  margin: 20rpx;
+  padding: 30rpx;
+}
+.matrix-table {
+  display: flex;
+  flex-direction: column;
+  gap: 12rpx;
+  margin: 16rpx 0;
+}
+.matrix-header, .matrix-row {
+  display: grid;
+  grid-template-columns: 100rpx 120rpx 1fr;
+  align-items: center;
+  gap: 16rpx;
+}
+.matrix-header {
+  font-size: 24rpx;
+  color: #9CA3AF;
+  padding-bottom: 8rpx;
+  border-bottom: 1rpx solid #F3F4F6;
+}
+.matrix-nutrient {
+  font-size: 26rpx;
+  color: #374151;
+  font-weight: 500;
+}
+.matrix-level {
+  font-size: 22rpx;
+  color: #6B7280;
+}
+.matrix-bar-wrap {
+  display: flex;
+  align-items: center;
+}
+.matrix-bar {
+  flex: 1;
+  height: 12rpx;
+  background: #F3F4F6;
+  border-radius: 6rpx;
+  overflow: hidden;
+}
+.matrix-fill {
+  height: 100%;
+  border-radius: 6rpx;
+}
+.matrix-priority {
+  margin-top: 20rpx;
+  padding-top: 16rpx;
+  border-top: 1rpx solid #F3F4F6;
+}
+.priority-title {
+  font-size: 26rpx;
+  font-weight: bold;
+  color: #374151;
+  display: block;
+  margin-bottom: 8rpx;
+}
+.priority-item {
+  font-size: 24rpx;
+  color: #6B7280;
+  display: block;
+  line-height: 1.6;
+}
+```
+
+- [ ] **Step 5: 在 data() 中新增响应式字段**
+
+```javascript
+data() {
+  return {
+    // ... existing fields ...
+    deficiencyRecords: [],
+    scoreTrend: []
+  };
+}
+```
+
+- [ ] **Step 6: 在 onLoad / onShow 中加载数据**
+
+```javascript
+onLoad(options) {
+  this.childId = options.childId || 1;
+  // ... existing load logic ...
+  
+  // 加载缺乏分析数据
+  this.loadDeficiencyData();
+  
+  // 加载评分趋势(从报告列表)
+  this.loadScoreTrend();
+},
+
+methods: {
+  loadDeficiencyData() {
+    this.$api.getDeficiencyAnalysis(this.childId).then(res => {
+      if (res.code === 200 && res.data) {
+        this.deficiencyRecords = res.data;
+      }
+    });
+  },
+  
+  loadScoreTrend() {
+    // 通过报告列表接口获取趋势
+    this.$api.getReportList(this.childId).then(res => {
+      if (res.code === 200 && res.data) {
+        // 取最近3份报告
+        this.scoreTrend = (res.data.records || []).slice(0, 3).map(r => ({
+          reportDate: r.reportDate,
+          overall: r.overallScore,
+          gutHealth: r.gutHealthScore,
+          nutrition: r.nutritionScore
+        }));
+      }
+    });
+  },
+  
+  getInterpretEmoji(record) {
+    if (record.deficiencyType === 'deficiency') {
+      if (record.severity >= 0.7) return '🔴';
+      return '🟡';
+    }
+    return '🟢';
+  },
+  
+  getDeficiencyLevelLabel(record) {
+    if (record.deficiencyType === 'deficiency') {
+      if (record.severity >= 0.7) return '严重缺乏';
+      if (record.severity >= 0.3) return '缺乏';
+      return '轻度不足';
+    }
+    if (record.deficiencyType === 'excess') return '偏高';
+    if (record.deficiencyType === 'borderline') return '边界值';
+    return '未分类';
+  },
+  
+  getSeverityBarColor(severity) {
+    if (severity >= 0.7) return '#EF4444';
+    if (severity >= 0.3) return '#F59E0B';
+    return '#10B981';
+  },
+  
+  formatTrendDate(date) {
+    if (!date) return '';
+    return date.substring(5, 10);
+  }
+}
+```
+
+- [ ] **Step 7: 编译验证**
+
+启动小程序 `npm run dev:mp-weixin` 无报错即可
+
+- [ ] **Step 8: 提交**
+
+```bash
+git add cfc-frontend/pages/body/health-report.vue
+git commit -m "feat(frontend): add interpretation cards, trend charts, deficiency matrix to health-report"
+```
+
+---
+
+### T11: pages/body/index.vue 导航入口
+
+**Files:**
+- Modify: `cfc-frontend/pages/body/index.vue`
+
+**参考文件**: `body/index.vue` — 找到健康报告卡片的点击处理
+
+**依赖**: T9 (nutrition-profile页面已创建)
+
+- [ ] **Step 1: 阅读 body/index.vue 找到健康报告相关的卡片区域**
+
+找到 `navigateTo` 或 `goTo` 相关的方法
+
+- [ ] **Step 2: 修改健康报告卡片的点击跳转逻辑**
+
+在相关卡片的 `@click` 处理方法中,添加跳转到 nutrition-profile:
+
+```javascript
+// 找到跳转到健康报告的方法,修改为:
+goToNutritionProfile() {
+  // 获取当前孩子的 childId(从全局状态或参数)
+  const childId = this.$store.getters.currentChildId || 1;
+  uni.navigateTo({
+    url: `/pages/health/nutrition-profile?childId=${childId}`
+  });
+}
+
+// 或在现有跳转到 health-report 的方法中增加链接:
+goToHealthReport() {
+  const childId = this.$store.getters.currentChildId || 1;
+  uni.navigateTo({
+    url: `/pages/body/health-report?childId=${childId}`
+  });
+}
+```
+
+在模板中将健康报告卡片的点击区域从跳转 health-report 改为跳转 nutrition-profile(或者新增一个"查看档案"按钮):
+
+```vue
+<!-- 在健康报告卡片底部添加 "查看完整档案" 按钮 -->
+<view class="card-action" @click="goToNutritionProfile">
+  <text>查看完整档案 →</text>
+</view>
+```
+
+- [ ] **Step 3: 提交**
+
+```bash
+git add cfc-frontend/pages/body/index.vue
+git commit -m "feat(frontend): add nutrition-profile entry in body index page"
+```
+
+---
+
+### T12: utils/api.js 新增4个API函数
+
+**Files:**
+- Modify: `cfc-frontend/utils/api.js`
+
+**参考文件**: `api.js` — 确认现有 `request()` 封装方式(L1280-1311 健康相关API模式)
+
+**依赖**: T6 (后端4个端点已创建)
+
+- [ ] **Step 1: 阅读 api.js 确认 request() 用法和现有健康API位置**
+
+文件路径:`cfc-frontend/utils/api.js`
+
+找到 `getLatestHealthReport` 或 `getHealthIndicatorList` 等函数位置
+
+- [ ] **Step 2: 在健康相关API区域(大约L1303-1311)添加4个新函数**
+
+在 `export const` 区域添加:
+
+```javascript
+/**
+ * 孩子营养档案聚合
+ */
+export const getNutritionProfile = (childId) => {
+  return request('/api/health/nutrition-profile', 'POST', { childId });
+};
+
+/**
+ * 获取孩子最新缺乏分析数据
+ */
+export const getDeficiencyAnalysis = (childId) => {
+  return request('/api/health/deficiency/latest', 'POST', { childId });
+};
+
+/**
+ * 获取孩子缺乏趋势
+ */
+export const getDeficiencyTrend = (childId) => {
+  return request('/api/health/deficiency/trend', 'POST', { childId });
+};
+
+/**
+ * 触发营养缺乏分析
+ */
+export const analyzeDeficiency = (reportId, childId) => {
+  return request('/api/health/deficiency/analyze', 'POST', { reportId, childId });
+};
+```
+
+- [ ] **Step 3: 确认全局 api 对象已包含新函数**
+
+在文件底部确认 api 对象 export 包含新函数
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-frontend/utils/api.js
+git commit -m "feat(api): add 4 health deficiency and profile API functions"
+```
+
+---
+
+## Wave 5: Web管理端
+
+### T13: nutritionist/NutritionMappings.vue 新页面
+
+**Files:**
+- Create: `cfc-web/src/views/nutritionist/NutritionMappings.vue`
+
+**参考文件**: `cfc-web/src/views/nutritionist/HealthReports.vue` (现有表格CRUD结构)
+
+**依赖**: T1 (表已建) + T2 (Entity+Mapper已创建)
+
+- [ ] **Step 1: 阅读 HealthReports.vue 确认 Element UI 表格用法**
+
+了解 `el-table`、`el-button`、`el-dialog` 的使用模式
+
+- [ ] **Step 2: 创建 NutritionMappings.vue — 映射表CRUD页面**
+
+文件路径:`cfc-web/src/views/nutritionist/NutritionMappings.vue`
+
+```vue
+<template>
+  <div class="nutrition-mappings">
+    <div class="page-header">
+      <h2>营养素映射管理</h2>
+      <el-button type="primary" @click="openCreateDialog">新增映射</el-button>
+    </div>
+
+    <!-- 映射列表 -->
+    <el-table :data="mappings" border style="width: 100%">
+      <el-table-column prop="id" label="ID" width="60" />
+      <el-table-column prop="indicatorNamePattern" label="指标名模式" width="180" />
+      <el-table-column prop="nutrient" label="营养素" width="100" />
+      <el-table-column prop="severityWeight" label="权重" width="80">
+        <template slot-scope="scope">
+          {{ scope.row.severityWeight }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="deficiencySymptoms" label="缺乏症状" min-width="150" show-overflow-tooltip />
+      <el-table-column prop="foodSuggestions" label="推荐食物" min-width="180" show-overflow-tooltip />
+      <el-table-column prop="sortOrder" label="排序" width="80" />
+      <el-table-column label="操作" width="150" fixed="right">
+        <template slot-scope="scope">
+          <el-button size="mini" @click="openEditDialog(scope.row)">编辑</el-button>
+          <el-button size="mini" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 新增/编辑对话框 -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="600px">
+      <el-form :model="form" label-width="120px">
+        <el-form-item label="指标名模式">
+          <el-input v-model="form.indicatorNamePattern" placeholder="如: 铁蛋白, 维生素D%" />
+        </el-form-item>
+        <el-form-item label="营养素">
+          <el-input v-model="form.nutrient" placeholder="如: 铁, 维生素D" />
+        </el-form-item>
+        <el-form-item label="权重(0-1)">
+          <el-input-number v-model="form.severityWeight" :min="0" :max="1" :step="0.1" />
+        </el-form-item>
+        <el-form-item label="缺乏症状">
+          <el-input v-model="form.deficiencySymptoms" type="textarea" :rows="2" />
+        </el-form-item>
+        <el-form-item label="过量症状">
+          <el-input v-model="form.excessSymptoms" type="textarea" :rows="2" />
+        </el-form-item>
+        <el-form-item label="推荐食物">
+          <el-input v-model="form.foodSuggestions" placeholder="逗号分隔,如: 动物肝脏,红肉,菠菜" />
+        </el-form-item>
+        <el-form-item label="排序">
+          <el-input-number v-model="form.sortOrder" :min="0" />
+        </el-form-item>
+      </el-form>
+      <span slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleSave">保存</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      mappings: [],
+      dialogVisible: false,
+      dialogTitle: '新增映射',
+      form: {
+        id: null,
+        indicatorNamePattern: '',
+        nutrient: '',
+        severityWeight: 1.0,
+        deficiencySymptoms: '',
+        excessSymptoms: '',
+        foodSuggestions: '',
+        sortOrder: 10
+      }
+    };
+  },
+  created() {
+    this.loadMappings();
+  },
+  methods: {
+    loadMappings() {
+      this.$http.get('/api/health/nutrition-mapping/list').then(res => {
+        this.mappings = res.data || [];
+      });
+    },
+    openCreateDialog() {
+      this.form = { id: null, indicatorNamePattern: '', nutrient: '', severityWeight: 1.0, deficiencySymptoms: '', excessSymptoms: '', foodSuggestions: '', sortOrder: 10 };
+      this.dialogTitle = '新增映射';
+      this.dialogVisible = true;
+    },
+    openEditDialog(row) {
+      this.form = { ...row };
+      this.dialogTitle = '编辑映射';
+      this.dialogVisible = true;
+    },
+    handleSave() {
+      const method = this.form.id ? 'put' : 'post';
+      const url = this.form.id ? '/api/health/nutrition-mapping/update' : '/api/health/nutrition-mapping/create';
+      this.$http[method](url, this.form).then(() => {
+        this.dialogVisible = false;
+        this.loadMappings();
+        this.$message.success('保存成功');
+      });
+    },
+    handleDelete(id) {
+      this.$confirm('确认删除该映射?', '提示').then(() => {
+        this.$http.delete('/api/health/nutrition-mapping/delete?id=' + id).then(() => {
+          this.loadMappings();
+          this.$message.success('删除成功');
+        });
+      });
+    }
+  }
+};
+</script>
+
+<style scoped>
+.nutrition-mappings {
+  padding: 20px;
+}
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+.page-header h2 {
+  margin: 0;
+  font-size: 20px;
+}
+</style>
+```
+
+**注意**: 需要后端提供 `/api/health/nutrition-mapping/list`、`create`、`update`、`delete` 四个端点(简单的CRUD)。如对应的Controller不存在,需先创建 `NutritionMappingController.java`
+
+- [ ] **Step 3: 如 NutritionMappingController 不存在,创建基础CRUD**
+
+文件路径:`cfc-backend/src/main/java/com/etotem/cfc/controller/NutritionMappingController.java`
+
+```java
+@PostMapping("/nutrition-mapping/list")
+public Result<List<NutritionIndicatorMapping>> list() {
+    List<NutritionIndicatorMapping> list = mappingMapper.findAllOrderBySort();
+    return Result.success(list);
+}
+
+@PostMapping("/nutrition-mapping/create")
+public Result<?> create(@RequestBody NutritionIndicatorMapping mapping) {
+    mappingMapper.insert(mapping);
+    return Result.success(null);
+}
+
+@PostMapping("/nutrition-mapping/update")
+public Result<?> update(@RequestBody NutritionIndicatorMapping mapping) {
+    mappingMapper.updateById(mapping);
+    return Result.success(null);
+}
+
+@PostMapping("/nutrition-mapping/delete")
+public Result<?> delete(@RequestParam Long id) {
+    mappingMapper.deleteById(id);
+    return Result.success(null);
+}
+```
+
+- [ ] **Step 4: 在 Web Admin 路由注册新页面**
+
+文件:`cfc-web/src/router/index.js` — 在 nutritionist 路由下添加:
+
+```javascript
+{
+  path: '/nutritionist/nutrition-mappings',
+  component: () => import('@/views/nutritionist/NutritionMappings'),
+  meta: { title: '营养素映射管理' }
+}
+```
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-web/src/views/nutritionist/NutritionMappings.vue
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/NutritionMappingController.java
+git add cfc-web/src/router/index.js
+git commit -m "feat(web): add NutritionMappings CRUD page and controller"
+```
+
+---
+
+### T14: nutritionist/HealthReports.vue 改造
+
+**Files:**
+- Modify: `cfc-web/src/views/nutritionist/HealthReports.vue`
+
+**依赖**: T13 (NutritionMappings.vue)
+
+- [ ] **Step 1: 阅读 HealthReports.vue 找到现有表格结构**
+
+文件路径:`cfc-web/src/views/nutritionist/HealthReports.vue`
+
+找到报告列表表格区域
+
+- [ ] **Step 2: 在每行报告增加"营养缺乏分析"列**
+
+在表格中添加新列:
+
+```vue
+<el-table-column label="营养缺乏" width="120" fixed="right">
+  <template slot-scope="scope">
+    <el-tag v-if="scope.row.deficiencyCount > 0" type="danger" size="small">
+      {{ scope.row.deficiencyCount }}项缺乏
+    </el-tag>
+    <el-tag v-else type="success" size="small">无缺乏</el-tag>
+    <el-button size="mini" type="text" @click="triggerAnalysis(scope.row)" style="margin-left: 8px">
+      {{ scope.row.deficiencyCount > 0 ? '重新分析' : '分析' }}
+    </el-button>
+  </template>
+</el-table-column>
+```
+
+在 `data()` 中 `report` 对象结构确认有 `deficiencyCount` 字段(如后端未返回则手动调用 API 获取)
+
+- [ ] **Step 3: 添加 triggerAnalysis 方法**
+
+```javascript
+triggerAnalysis(row) {
+  this.$http.post('/api/health/deficiency/analyze', {
+    reportId: row.id,
+    childId: row.childId
+  }).then(res => {
+    this.$message.success('分析完成');
+    this.loadReports(); // 刷新列表
+  });
+}
+```
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-web/src/views/nutritionist/HealthReports.vue
+git commit -m "feat(web): add deficiency analysis trigger to HealthReports"
+```
+
+---
+
+### T15: nutritionist/HealthIndicators.vue 改造
+
+**Files:**
+- Modify: `cfc-web/src/views/nutritionist/HealthIndicators.vue`
+
+**依赖**: T13 (NutritionMappings.vue)
+
+- [ ] **Step 1: 阅读 HealthIndicators.vue 找到指标表格**
+
+找到指标列表表格区域,确认 `status` 列或 `indicatorName` 列位置
+
+- [ ] **Step 2: 在 status 列后增加"关联营养素"标签列**
+
+在表格中添加新列:
+
+```vue
+<el-table-column label="关联营养素" width="120">
+  <template slot-scope="scope">
+    <template v-if="scope.row.linkedNutrients && scope.row.linkedNutrients.length > 0">
+      <el-tag v-for="nutrient in scope.row.linkedNutrients" :key="nutrient" size="mini" style="margin-right: 4px">
+        {{ nutrient }}
+      </el-tag>
+    </template>
+    <span v-else style="color: #999; font-size: 12px">—</span>
+  </template>
+</el-table-column>
+```
+
+后端需在返回指标时,查询 `nutrition_indicator_mapping` 表做 LIKE 匹配,附加 `linkedNutrients` 字段
+
+- [ ] **Step 3: 异常指标高亮**
+
+在 `row-class-name` 或 `cell-style` 中根据 `status` 返回高亮样式:
+
+```javascript
+cellStyle({ row, column, rowIndex, columnIndex }) {
+  if (columnIndex === 1 && row.status && row.status !== '正常') {
+    return 'background: #FEF2F2'; // 红色背景
+  }
+}
+```
+
+- [ ] **Step 4: 提交**
+
+```bash
+git add cfc-web/src/views/nutritionist/HealthIndicators.vue
+git commit -m "feat(web): add nutrient linkage tags and highlight abnormal indicators"
+```
+
+---
+
+## 最终验证Wave
+
+### F1: Plan Compliance Audit
+
+读取设计文档 `docs/superpowers/specs/2026-06-23-P0营养师核心功能设计方案.md`,逐项验证:
+- [ ] Feature 1 (统一视图): 页面 + API + 空状态 + 入口导航 → 全部实现
+- [ ] Feature 2 (结构化缺乏分析): 映射表 + 分析服务 + 4个端点 + 集成 → 全部实现
+- [ ] Feature 3 (可视化解读): 通俗解读 + 趋势 + 雷达 + 矩阵 → 全部实现
+- [ ] 所有 Guardrails: 不含微量营养素Food列、不含PDF导出、不含WHO标准 → 验证无越界
+- [ ] 微信小程序约束: 无 `?.` 可选链 → 全部用 `&&` 替代
+
+### F2: Code Quality Review
+
+- [ ] `mvn clean compile` → 通过(0 error)
+- [ ] 检查所有新增 Service/Controller 无 `as any` 滥用(Spring Boot项目,重点检查 `@Resource` 注解是否正确)
+- [ ] 检查新增前端文件无 `console.log` 残留
+- [ ] 检查所有新增文件 import 完整
+
+### F3: Real Manual QA
+
+使用 Postman 或前端页面逐个端点验证:
+
+```bash
+# T6 端点验证
+curl -X POST http://localhost:8080/api/health/nutrition-profile \
+  -H "Content-Type: application/json" \
+  -H "Authorization: Bearer <token>" \
+  -d '{"childId": 1}'
+# 预期: 200 + 聚合数据
+
+curl -X POST http://localhost:8080/api/health/deficiency/latest \
+  -H "Content-Type: application/json" \
+  -H "Authorization: Bearer <token>" \
+  -d '{"childId": 1}'
+# 预期: 200 + 缺乏分析记录列表
+
+curl -X POST http://localhost:8080/api/health/deficiency/trend \
+  -H "Content-Type: application/json" \
+  -H "Authorization: Bearer <token>" \
+  -d '{"childId": 1}'
+# 预期: 200 + 趋势数据
+
+curl -X POST http://localhost:8080/api/health/deficiency/analyze \
+  -H "Content-Type: application/json" \
+  -H "Authorization: Bearer <token>" \
+  -d '{"reportId": 1, "childId": 1}'
+# 预期: 200 + 分析结果列表
+```
+
+### F4: Scope Fidelity Check
+
+- [ ] 无新增表/字段超出设计范围
+- [ ] 无修改现有核心业务逻辑(只扩展,不修改)
+- [ ] commit 历史清晰(每个 task 一个原子 commit)
+
+---
+
+## 提交策略
+
+每个 Task 独立 commit,总共约 15 个 commit。
+
+```bash
+# Wave 1: DB基础
+git add ... && git commit -m "feat(health): add nutrition tables DDL and seed data"
+git add ... && git commit -m "feat(health): add NutritionIndicatorMapping entity and mapper"
+git add ... && git commit -m "feat(health): add NutritionDeficiencyRecord entity and mapper"
+
+# Wave 2: 服务层
+git add ... && git commit -m "feat(health): implement NutritionDeficiencyService"
+git add ... && git commit -m "feat(health): implement ChildNutritionProfileService"
+
+# Wave 3: API层
+git add ... && git commit -m "feat(api): add 4 health deficiency and profile endpoints"
+git add ... && git commit -m "feat(health): chain NutritionDeficiencyService in HealthAnalysisService"
+git add ... && git commit -m "feat(meal): add deficiency_matrix to Dify inputs"
+
+# Wave 4: 前端
+git add ... && git commit -m "feat(frontend): add nutrition-profile unified view page"
+git add ... && git commit -m "feat(frontend): add interpretation, trend, matrix to health-report"
+git add ... && git commit -m "feat(frontend): add nutrition-profile entry in index"
+git add ... && git commit -m "feat(api): add 4 health deficiency and profile API functions"
+
+# Wave 5: Web Admin
+git add ... && git commit -m "feat(web): add NutritionMappings CRUD page and controller"
+git add ... && git commit -m "feat(web): add deficiency trigger to HealthReports"
+git add ... && git commit -m "feat(web): add nutrient linkage to HealthIndicators"
+
+# 最终
+git push origin cfclub
+```
+
+---
+
+## 成功标准
+
+- [ ] 15个Tasks全部完成
+- [ ] `mvn clean compile` 通过
+- [ ] 小程序编译无错误
+- [ ] 所有4个新API端点可用(验证返回数据格式正确)
+- [ ] nutrition-profile 页面可正常打开(空状态+有数据状态)
+- [ ] health-report 页面可见化解读正常展示
+- [ ] Web 管理端 NutritionMappings 页面可 CRUD 映射规则
+- [ ] 推送到远程 `origin/cfclub`