Kaynağa Gözat

docs: add fatloss mini-program implementation plan

Sisyphus 3 gün önce
ebeveyn
işleme
f61b24d243

+ 95 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -2983,6 +2983,12 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
 
         runSystemMigrations();
         registerCollectExpectedIndicators();
+
+        // 减脂专题表创建
+        createFatWeightRecordTable();
+        createFatDietRecordTable();
+        createFatExerciseRecordTable();
+        createFatGoalTable();
     }
 
     private void migrateHealthDimensionsTable() {
@@ -12049,6 +12055,7 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
         return sb.append(']').toString();
     }
 
+
     // 迁移354: 家庭共餐选中食材表(持久化共餐场景食材选择)
     private void createDietFamilySelectedIngredientsTable() {
         try {
@@ -12074,5 +12081,93 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
         } catch (Exception e) {
             log.warn("迁移354: 创建 diet_family_selected_ingredients 表失败: {}", e.getMessage());
         }
+    }    // 迁移355: 创建减脂专题体重记录表
+    private void createFatWeightRecordTable() {
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS fat_weight_record (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "member_id BIGINT NOT NULL, " +
+                    "weight DECIMAL(5,2) NOT NULL, " +
+                    "body_fat_percent DECIMAL(4,2) DEFAULT NULL, " +
+                    "waist_circumference DECIMAL(4,2) DEFAULT NULL, " +
+                    "measure_date DATE NOT NULL, " +
+                    "remark VARCHAR(255) DEFAULT NULL, " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "INDEX idx_fat_weight_member (member_id, measure_date)) " +
+                    "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂专题-体重体脂记录'");
+            log.info("迁移355: 已创建fat_weight_record表");
+        } catch (Exception e) {
+            log.warn("迁移355: 创建fat_weight_record表失败: {}", e.getMessage());
+        }
+    }
+
+    // 迁移356: 创建减脂专题饮食记录表
+    private void createFatDietRecordTable() {
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS fat_diet_record (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "member_id BIGINT NOT NULL, " +
+                    "meal_type VARCHAR(20) NOT NULL, " +
+                    "food_name VARCHAR(255) NOT NULL, " +
+                    "food_weight DECIMAL(6,2) DEFAULT NULL, " +
+                    "calories INT DEFAULT 0, " +
+                    "protein DECIMAL(6,2) DEFAULT 0, " +
+                    "fat DECIMAL(6,2) DEFAULT 0, " +
+                    "carbs DECIMAL(6,2) DEFAULT 0, " +
+                    "eat_date DATE NOT NULL, " +
+                    "remark VARCHAR(255) DEFAULT NULL, " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "INDEX idx_fat_diet_member (member_id, eat_date)) " +
+                    "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂专题-饮食记录'");
+            log.info("迁移356: 已创建fat_diet_record表");
+        } catch (Exception e) {
+            log.warn("迁移356: 创建fat_diet_record表失败: {}", e.getMessage());
+        }
     }
+
+    // 迁移357: 创建减脂专题运动记录表
+    private void createFatExerciseRecordTable() {
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS fat_exercise_record (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "member_id BIGINT NOT NULL, " +
+                    "exercise_type VARCHAR(50) NOT NULL, " +
+                    "duration_min INT DEFAULT 0, " +
+                    "distance_km DECIMAL(5,2) DEFAULT NULL, " +
+                    "calories_burned INT DEFAULT 0, " +
+                    "exercise_date DATE NOT NULL, " +
+                    "remark VARCHAR(255) DEFAULT NULL, " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "INDEX idx_fat_exercise_member (member_id, exercise_date)) " +
+                    "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂专题-运动记录'");
+            log.info("迁移357: 已创建fat_exercise_record表");
+        } catch (Exception e) {
+            log.warn("迁移357: 创建fat_exercise_record表失败: {}", e.getMessage());
+        }
+    }
+
+    // 迁移358: 创建减脂专题目标表
+    private void createFatGoalTable() {
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS fat_goal (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "member_id BIGINT NOT NULL, " +
+                    "target_weight DECIMAL(5,2) NOT NULL, " +
+                    "start_weight DECIMAL(5,2) NOT NULL, " +
+                    "target_body_fat DECIMAL(4,2) DEFAULT NULL, " +
+                    "target_date DATE DEFAULT NULL, " +
+                    "status VARCHAR(20) DEFAULT 'active', " +
+                    "start_date DATE NOT NULL, " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                    "INDEX idx_fat_goal_member (member_id)) " +
+                    "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂专题-减脂目标'");
+            log.info("迁移358: 已创建fat_goal表");
+        } catch (Exception e) {
+            log.warn("迁移358: 创建fat_goal表失败: {}", e.getMessage());
+        }
+    }
+
+
+ (docs: add fatloss mini-program implementation plan)
 }

+ 59 - 0
cfc-backend/src/main/resources/schema.sql

@@ -6113,3 +6113,62 @@ CREATE TABLE IF NOT EXISTS task_reminder_config (
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
     UNIQUE KEY uk_user (user_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='任务提醒配置表';
+
+-- 迁移355: 减脂专题 - 体重记录表
+CREATE TABLE IF NOT EXISTS fat_weight_record (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
+    member_id BIGINT NOT NULL COMMENT '家庭成员ID(family_members.id)',
+    weight DECIMAL(5,2) NOT NULL COMMENT '体重(kg)',
+    body_fat_percent DECIMAL(4,2) DEFAULT NULL COMMENT '体脂率(%)',
+    waist_circumference DECIMAL(4,2) DEFAULT NULL COMMENT '腰围(cm)',
+    measure_date DATE NOT NULL COMMENT '测量日期',
+    remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    INDEX idx_fat_weight_member (member_id, measure_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂专题-体重体脂记录';
+
+-- 迁移356: 减脂专题 - 饮食记录表
+CREATE TABLE IF NOT EXISTS fat_diet_record (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
+    member_id BIGINT NOT NULL COMMENT '家庭成员ID(family_members.id)',
+    meal_type VARCHAR(20) NOT NULL COMMENT 'meal_type: breakfast/lunch/dinner/snack',
+    food_name VARCHAR(255) NOT NULL COMMENT '食物名称',
+    food_weight DECIMAL(6,2) DEFAULT NULL COMMENT '食物重量(克)',
+    calories INT DEFAULT 0 COMMENT '热量(kcal)',
+    protein DECIMAL(6,2) DEFAULT 0 COMMENT '蛋白质(g)',
+    fat DECIMAL(6,2) DEFAULT 0 COMMENT '脂肪(g)',
+    carbs DECIMAL(6,2) DEFAULT 0 COMMENT '碳水(g)',
+    eat_date DATE NOT NULL COMMENT '进餐日期',
+    remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    INDEX idx_fat_diet_member (member_id, eat_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂专题-饮食记录';
+
+-- 迁移357: 减脂专题 - 运动记录表
+CREATE TABLE IF NOT EXISTS fat_exercise_record (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
+    member_id BIGINT NOT NULL COMMENT '家庭成员ID(family_members.id)',
+    exercise_type VARCHAR(50) NOT NULL COMMENT '运动类型',
+    duration_min INT DEFAULT 0 COMMENT '运动时长(分钟)',
+    distance_km DECIMAL(5,2) DEFAULT NULL COMMENT '距离(km)',
+    calories_burned INT DEFAULT 0 COMMENT '消耗热量(kcal)',
+    exercise_date DATE NOT NULL COMMENT '运动日期',
+    remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    INDEX idx_fat_exercise_member (member_id, exercise_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂专题-运动记录';
+
+-- 迁移358: 减脂专题 - 减脂目标表
+CREATE TABLE IF NOT EXISTS fat_goal (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
+    member_id BIGINT NOT NULL COMMENT '家庭成员ID(family_members.id)',
+    target_weight DECIMAL(5,2) NOT NULL COMMENT '目标体重(kg)',
+    start_weight DECIMAL(5,2) NOT NULL COMMENT '初始体重(kg)',
+    target_body_fat DECIMAL(4,2) DEFAULT NULL COMMENT '目标体脂率(%)',
+    target_date DATE DEFAULT NULL COMMENT '目标达成日期',
+    status VARCHAR(20) DEFAULT 'active' COMMENT '状态: active/completed/abandoned',
+    start_date DATE NOT NULL COMMENT '开始日期',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    INDEX idx_fat_goal_member (member_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂专题-减脂目标';

+ 3784 - 0
docs/superpowers/plans/2026-10-09-fatloss-miniprogram-plan.md

@@ -0,0 +1,3784 @@
+# 减脂专题小程序 实现计划
+
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
+
+**目标:** 开发一个独立的「减脂专题」微信小程序(`xiaofat-miniprogram/`),共享主系统用户体系与健康数据,提供体重/体脂追踪、饮食记录、运动打卡、AI 个性化建议四大核心功能。
+
+**架构:** 新 uni-app Vue 2 项目,通过主系统 `/api/auth/*` 认证接口共享 OpenID 用户体系,复用主系统 `cfc-backend`(9082)健康数据接口,通过 `AiGateway` 调用 `cfc-langgraph`(9000)AI 服务。专题特有数据(体重/体脂/饮食/运动/目标)新增表并存主库。
+
+**技术栈:** uni-app Vue 2(Options API)、微信小程序、Spring Boot 2.7.18 后端扩展、MyBatis-Plus、LangGraph Python AI 服务。
+
+---
+
+## 文件结构
+
+```
+xiaofat-miniprogram/                    # 新小程序前端(uni-app Vue 2)
+├─ manifest.json                        # 独立 AppID
+├─ pages.json                           # TabBar 5 个:首页/体重/饮食/运动/AI
+├─ App.vue                              # 全局生命周期,登录态检查
+├─ main.js                              # Vue 实例入口
+├─ config.js                            # API baseUrl 环境自适应
+├─ utils/
+│   ├─ api.js                           # 请求封装(从主项目 api.js 精简)
+│   └─ format.js                        # 时间格式化 parseDate
+├─ store/
+│   ├─ index.js                         # Vuex 根 store
+│   └─ modules/
+│       └─ user.js                      # 用户/登录态/角色
+├─ pages/
+│   ├─ index/index.vue                  # 首页(数据概览 + 快捷入口)
+│   ├─ weight/index.vue                 # 体重/体脂记录列表
+│   ├─ weight/create.vue                # 新增体重/体脂记录
+│   ├─ weight/chart.vue                 # 体重趋势图表
+│   ├─ diet/index.vue                   # 饮食记录
+│   ├─ diet/create.vue                  # 新增饮食记录
+│   ├─ diet/food-query.vue              # 食物热量查询
+│   ├─ exercise/index.vue               # 运动打卡列表
+│   ├─ exercise/create.vue              # 新增运动记录
+│   ├─ ai-coach/index.vue               # AI 教练对话
+│   └─ profile/index.vue                # 个人设置 + 目标设定
+└─ components/
+    ├─ weight-card.vue                  # 体重展示卡片
+    ├─ progress-ring.vue                # 环形进度
+    ├─ date-picker.vue                  # 日期选择
+    └─ ai-chat-bubble.vue               # AI 聊天气泡
+
+cfc-backend/src/main/java/com/etotem/cfc/   # 后端扩展(减脂专题)
+├─ controller/fatloss/
+│   ├─ FatWeightController.java         # 体重/体脂 CRUD
+│   ├─ FatDietController.java           # 饮食记录 CRUD
+│   ├─ FatExerciseController.java       # 运动记录 CRUD
+│   └─ FatGoalController.java           # 目标管理
+├─ entity/
+│   ├─ FatWeightRecord.java             # 体重体脂记录实体
+│   ├─ FatDietRecord.java               # 饮食记录实体
+│   ├─ FatExerciseRecord.java           # 运动记录实体
+│   └─ FatGoal.java                     # 减脂目标实体
+├─ mapper/
+│   ├─ FatWeightRecordMapper.java
+│   ├─ FatDietRecordMapper.java
+│   ├─ FatExerciseRecordMapper.java
+│   └─ FatGoalMapper.java
+└─ service/
+    ├─ FatWeightService.java
+    ├─ FatDietService.java
+    ├─ FatExerciseService.java
+    └─ FatGoalService.java
+
+cfc-backend/src/main/resources/
+├─ schema.sql                           # 新增 4 张表定义
+└─ config/DatabaseInitializer.java      # 新增迁移 N+1 ~ N+4
+```
+
+---
+
+## 任务分解
+
+### 任务 1:后端 - 建表迁移(4 张新表)
+
+**文件:**
+- 修改:`cfc-backend/src/main/resources/schema.sql`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`
+
+- [ ] **步骤 1:在 schema.sql 末尾追加 4 张表定义**
+
+```sql
+-- ============ 减脂专题 ============
+CREATE TABLE IF NOT EXISTS fat_weight_record (
+  id BIGINT AUTO_INCREMENT PRIMARY KEY,
+  member_id BIGINT NOT NULL COMMENT '家庭成员ID',
+  weight DECIMAL(5,2) NOT NULL COMMENT '体重(kg)',
+  body_fat_percent DECIMAL(4,2) DEFAULT NULL COMMENT '体脂率(%)',
+  waist_circumference DECIMAL(4,2) DEFAULT NULL COMMENT '腰围(cm)',
+  measure_date DATE NOT NULL COMMENT '测量日期',
+  remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
+  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+  INDEX idx_fat_weight_member (member_id, measure_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂-体重记录';
+
+CREATE TABLE IF NOT EXISTS fat_diet_record (
+  id BIGINT AUTO_INCREMENT PRIMARY KEY,
+  member_id BIGINT NOT NULL COMMENT '家庭成员ID',
+  meal_type VARCHAR(20) NOT NULL COMMENT 'breakfast/lunch/dinner/snack',
+  food_name VARCHAR(255) NOT NULL COMMENT '食物名称',
+  food_weight DECIMAL(6,2) DEFAULT NULL COMMENT '食物重量(克)',
+  calories INT DEFAULT 0 COMMENT '热量(kcal)',
+  protein DECIMAL(6,2) DEFAULT 0 COMMENT '蛋白质(g)',
+  fat DECIMAL(6,2) DEFAULT 0 COMMENT '脂肪(g)',
+  carbs DECIMAL(6,2) DEFAULT 0 COMMENT '碳水(g)',
+  eat_date DATE NOT NULL COMMENT '进餐日期',
+  remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
+  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+  INDEX idx_fat_diet_member (member_id, eat_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂-饮食记录';
+
+CREATE TABLE IF NOT EXISTS fat_exercise_record (
+  id BIGINT AUTO_INCREMENT PRIMARY KEY,
+  member_id BIGINT NOT NULL COMMENT '家庭成员ID',
+  exercise_type VARCHAR(50) NOT NULL COMMENT '运动类型',
+  duration_min INT DEFAULT 0 COMMENT '时长(分钟)',
+  distance_km DECIMAL(5,2) DEFAULT NULL COMMENT '距离(km)',
+  calories_burned INT DEFAULT 0 COMMENT '消耗热量(kcal)',
+  exercise_date DATE NOT NULL COMMENT '运动日期',
+  remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
+  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+  INDEX idx_fat_exercise_member (member_id, exercise_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂-运动记录';
+
+CREATE TABLE IF NOT EXISTS fat_goal (
+  id BIGINT AUTO_INCREMENT PRIMARY KEY,
+  member_id BIGINT NOT NULL COMMENT '家庭成员ID',
+  target_weight DECIMAL(5,2) NOT NULL COMMENT '目标体重(kg)',
+  start_weight DECIMAL(5,2) NOT NULL COMMENT '初始体重(kg)',
+  target_body_fat DECIMAL(4,2) DEFAULT NULL COMMENT '目标体脂率(%)',
+  target_date DATE DEFAULT NULL COMMENT '目标日期',
+  status VARCHAR(20) DEFAULT 'active' COMMENT 'active/completed/abandoned',
+  start_date DATE NOT NULL COMMENT '开始日期',
+  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  INDEX idx_fat_goal_member (member_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂-目标';
+```
+
+- [ ] **步骤 2:在 DatabaseInitializer.runMigrations() 末尾追加迁移**
+
+```java
+// 迁移N+1: 创建减脂专题体重记录表
+try {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS fat_weight_record (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "member_id BIGINT NOT NULL, " +
+            "weight DECIMAL(5,2) NOT NULL, " +
+            "body_fat_percent DECIMAL(4,2) DEFAULT NULL, " +
+            "waist_circumference DECIMAL(4,2) DEFAULT NULL, " +
+            "measure_date DATE NOT NULL, " +
+            "remark VARCHAR(255) DEFAULT NULL, " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "INDEX idx_fat_weight_member (member_id, measure_date)) " +
+            "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂-体重记录'");
+    log.info("已创建fat_weight_record表");
+} catch (Exception e) {
+    // 表已存在,忽略错误
+}
+
+// 迁移N+2: 创建减脂专题饮食记录表
+try {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS fat_diet_record (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "member_id BIGINT NOT NULL, " +
+            "meal_type VARCHAR(20) NOT NULL, " +
+            "food_name VARCHAR(255) NOT NULL, " +
+            "food_weight DECIMAL(6,2) DEFAULT NULL, " +
+            "calories INT DEFAULT 0, " +
+            "protein DECIMAL(6,2) DEFAULT 0, " +
+            "fat DECIMAL(6,2) DEFAULT 0, " +
+            "carbs DECIMAL(6,2) DEFAULT 0, " +
+            "eat_date DATE NOT NULL, " +
+            "remark VARCHAR(255) DEFAULT NULL, " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "INDEX idx_fat_diet_member (member_id, eat_date)) " +
+            "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂-饮食记录'");
+    log.info("已创建fat_diet_record表");
+} catch (Exception e) {
+    // 表已存在,忽略错误
+}
+
+// 迁移N+3: 创建减脂专题运动记录表
+try {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS fat_exercise_record (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "member_id BIGINT NOT NULL, " +
+            "exercise_type VARCHAR(50) NOT NULL, " +
+            "duration_min INT DEFAULT 0, " +
+            "distance_km DECIMAL(5,2) DEFAULT NULL, " +
+            "calories_burned INT DEFAULT 0, " +
+            "exercise_date DATE NOT NULL, " +
+            "remark VARCHAR(255) DEFAULT NULL, " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "INDEX idx_fat_exercise_member (member_id, exercise_date)) " +
+            "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂-运动记录'");
+    log.info("已创建fat_exercise_record表");
+} catch (Exception e) {
+    // 表已存在,忽略错误
+}
+
+// 迁移N+4: 创建减脂专题目标表
+try {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS fat_goal (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "member_id BIGINT NOT NULL, " +
+            "target_weight DECIMAL(5,2) NOT NULL, " +
+            "start_weight DECIMAL(5,2) NOT NULL, " +
+            "target_body_fat DECIMAL(4,2) DEFAULT NULL, " +
+            "target_date DATE DEFAULT NULL, " +
+            "status VARCHAR(20) DEFAULT 'active', " +
+            "start_date DATE NOT NULL, " +
+            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+            "INDEX idx_fat_goal_member (member_id)) " +
+            "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='减脂-目标'");
+    log.info("已创建fat_goal表");
+} catch (Exception e) {
+    // 表已存在,忽略错误
+}
+```
+
+- [ ] **步骤 3:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 4:Commit**
+
+```bash
+git add cfc-backend/src/main/resources/schema.sql cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
+git commit -m "feat(fatloss): add 4 fat-loss tables migration"
+```
+
+---
+
+### 任务 2:后端 - 实体类与 Mapper
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/FatWeightRecord.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/FatDietRecord.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/FatExerciseRecord.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/FatGoal.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/FatWeightRecordMapper.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/FatDietRecordMapper.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/FatExerciseRecordMapper.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/FatGoalMapper.java`
+
+- [ ] **步骤 1:创建 4 个实体类**
+
+```java
+// FatWeightRecord.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.util.Date;
+
+@Data
+@TableName("fat_weight_record")
+public class FatWeightRecord {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long memberId;
+    private BigDecimal weight;
+    private BigDecimal bodyFatPercent;
+    private BigDecimal waistCircumference;
+    private Date measureDate;
+    private String remark;
+    private Date createdAt;
+}
+```
+
+```java
+// FatDietRecord.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.util.Date;
+
+@Data
+@TableName("fat_diet_record")
+public class FatDietRecord {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long memberId;
+    private String mealType;
+    private String foodName;
+    private BigDecimal foodWeight;
+    private Integer calories;
+    private BigDecimal protein;
+    private BigDecimal fat;
+    private BigDecimal carbs;
+    private Date eatDate;
+    private String remark;
+    private Date createdAt;
+}
+```
+
+```java
+// FatExerciseRecord.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.util.Date;
+
+@Data
+@TableName("fat_exercise_record")
+public class FatExerciseRecord {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long memberId;
+    private String exerciseType;
+    private Integer durationMin;
+    private BigDecimal distanceKm;
+    private Integer caloriesBurned;
+    private Date exerciseDate;
+    private String remark;
+    private Date createdAt;
+}
+```
+
+```java
+// FatGoal.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.util.Date;
+
+@Data
+@TableName("fat_goal")
+public class FatGoal {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long memberId;
+    private BigDecimal targetWeight;
+    private BigDecimal startWeight;
+    private BigDecimal targetBodyFat;
+    private Date targetDate;
+    private String status;
+    private Date startDate;
+    private Date createdAt;
+    private Date updatedAt;
+}
+```
+
+- [ ] **步骤 2:创建 4 个 Mapper 接口**
+
+```java
+// FatWeightRecordMapper.java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.FatWeightRecord;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface FatWeightRecordMapper extends BaseMapper<FatWeightRecord> {
+}
+```
+
+```java
+// FatDietRecordMapper.java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.FatDietRecord;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface FatDietRecordMapper extends BaseMapper<FatDietRecord> {
+}
+```
+
+```java
+// FatExerciseRecordMapper.java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.FatExerciseRecord;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface FatExerciseRecordMapper extends BaseMapper<FatExerciseRecord> {
+}
+```
+
+```java
+// FatGoalMapper.java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.FatGoal;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface FatGoalMapper extends BaseMapper<FatGoal> {
+}
+```
+
+- [ ] **步骤 3:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 4:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/Fat*.java cfc-backend/src/main/java/com/etotem/cfc/mapper/Fat*Mapper.java
+git commit -m "feat(fatloss): add entities and mappers"
+```
+
+---
+
+### 任务 3:后端 - Service 层
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/service/FatWeightService.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/service/FatDietService.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/service/FatExerciseService.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/service/FatGoalService.java`
+
+- [ ] **步骤 1:创建 FatWeightService**
+
+```java
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.FatWeightRecord;
+import com.etotem.cfc.mapper.FatWeightRecordMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class FatWeightService {
+
+    @Resource
+    private FatWeightRecordMapper fatWeightRecordMapper;
+
+    public List<FatWeightRecord> list(Long memberId, Date startDate, Date endDate) {
+        LambdaQueryWrapper<FatWeightRecord> wrapper = new LambdaQueryWrapper<FatWeightRecord>()
+                .eq(FatWeightRecord::getMemberId, memberId)
+                .orderByDesc(FatWeightRecord::getMeasureDate);
+        if (startDate != null) {
+            wrapper.ge(FatWeightRecord::getMeasureDate, startDate);
+        }
+        if (endDate != null) {
+            wrapper.le(FatWeightRecord::getMeasureDate, endDate);
+        }
+        return fatWeightRecordMapper.selectList(wrapper);
+    }
+
+    public FatWeightRecord create(FatWeightRecord record) {
+        record.setCreatedAt(new Date());
+        fatWeightRecordMapper.insert(record);
+        return record;
+    }
+
+    public FatWeightRecord update(FatWeightRecord record) {
+        fatWeightRecordMapper.updateById(record);
+        return record;
+    }
+
+    public boolean delete(Long id) {
+        return fatWeightRecordMapper.deleteById(id) > 0;
+    }
+}
+```
+
+- [ ] **步骤 2:创建 FatDietService**
+
+```java
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.FatDietRecord;
+import com.etotem.cfc.mapper.FatDietRecordMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class FatDietService {
+
+    @Resource
+    private FatDietRecordMapper fatDietRecordMapper;
+
+    public List<FatDietRecord> list(Long memberId, Date startDate, Date endDate) {
+        LambdaQueryWrapper<FatDietRecord> wrapper = new LambdaQueryWrapper<FatDietRecord>()
+                .eq(FatDietRecord::getMemberId, memberId)
+                .orderByDesc(FatDietRecord::getEatDate);
+        if (startDate != null) {
+            wrapper.ge(FatDietRecord::getEatDate, startDate);
+        }
+        if (endDate != null) {
+            wrapper.le(FatDietRecord::getEatDate, endDate);
+        }
+        return fatDietRecordMapper.selectList(wrapper);
+    }
+
+    public FatDietRecord create(FatDietRecord record) {
+        record.setCreatedAt(new Date());
+        fatDietRecordMapper.insert(record);
+        return record;
+    }
+
+    public boolean delete(Long id) {
+        return fatDietRecordMapper.deleteById(id) > 0;
+    }
+
+    /** 统计指定日期总热量 */
+    public int sumCalories(Long memberId, Date date) {
+        List<FatDietRecord> records = fatDietRecordMapper.selectList(
+                new LambdaQueryWrapper<FatDietRecord>()
+                        .eq(FatDietRecord::getMemberId, memberId)
+                        .eq(FatDietRecord::getEatDate, date));
+        int total = 0;
+        for (FatDietRecord r : records) {
+            if (r.getCalories() != null) total += r.getCalories();
+        }
+        return total;
+    }
+}
+```
+
+- [ ] **步骤 3:创建 FatExerciseService**
+
+```java
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.FatExerciseRecord;
+import com.etotem.cfc.mapper.FatExerciseRecordMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class FatExerciseService {
+
+    @Resource
+    private FatExerciseRecordMapper fatExerciseRecordMapper;
+
+    public List<FatExerciseRecord> list(Long memberId, Date startDate, Date endDate) {
+        LambdaQueryWrapper<FatExerciseRecord> wrapper = new LambdaQueryWrapper<FatExerciseRecord>()
+                .eq(FatExerciseRecord::getMemberId, memberId)
+                .orderByDesc(FatExerciseRecord::getExerciseDate);
+        if (startDate != null) {
+            wrapper.ge(FatExerciseRecord::getExerciseDate, startDate);
+        }
+        if (endDate != null) {
+            wrapper.le(FatExerciseRecord::getExerciseDate, endDate);
+        }
+        return fatExerciseRecordMapper.selectList(wrapper);
+    }
+
+    public FatExerciseRecord create(FatExerciseRecord record) {
+        record.setCreatedAt(new Date());
+        fatExerciseRecordMapper.insert(record);
+        return record;
+    }
+
+    public boolean delete(Long id) {
+        return fatExerciseRecordMapper.deleteById(id) > 0;
+    }
+}
+```
+
+- [ ] **步骤 4:创建 FatGoalService**
+
+```java
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.FatGoal;
+import com.etotem.cfc.mapper.FatGoalMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Date;
+
+@Service
+public class FatGoalService {
+
+    @Resource
+    private FatGoalMapper fatGoalMapper;
+
+    public FatGoal getActive(Long memberId) {
+        return fatGoalMapper.selectOne(
+                new LambdaQueryWrapper<FatGoal>()
+                        .eq(FatGoal::getMemberId, memberId)
+                        .eq(FatGoal::getStatus, "active")
+                        .orderByDesc(FatGoal::getCreatedAt)
+                        .last("LIMIT 1"));
+    }
+
+    public FatGoal create(FatGoal goal) {
+        goal.setStatus("active");
+        goal.setCreatedAt(new Date());
+        goal.setUpdatedAt(new Date());
+        fatGoalMapper.insert(goal);
+        return goal;
+    }
+
+    public FatGoal update(FatGoal goal) {
+        goal.setUpdatedAt(new Date());
+        fatGoalMapper.updateById(goal);
+        return goal;
+    }
+
+    /** 完成目标 */
+    public FatGoal complete(Long id) {
+        FatGoal goal = fatGoalMapper.selectById(id);
+        if (goal != null) {
+            goal.setStatus("completed");
+            goal.setUpdatedAt(new Date());
+            fatGoalMapper.updateById(goal);
+        }
+        return goal;
+    }
+
+    /** 放弃目标 */
+    public FatGoal abandon(Long id) {
+        FatGoal goal = fatGoalMapper.selectById(id);
+        if (goal != null) {
+            goal.setStatus("abandoned");
+            goal.setUpdatedAt(new Date());
+            fatGoalMapper.updateById(goal);
+        }
+        return goal;
+    }
+}
+```
+
+- [ ] **步骤 5:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 6:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/Fat*Service.java
+git commit -m "feat(fatloss): add service layer"
+```
+
+---
+
+### 任务 4:后端 - Controller 层
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/controller/fatloss/FatWeightController.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/controller/fatloss/FatDietController.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/controller/fatloss/FatExerciseController.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/controller/fatloss/FatGoalController.java`
+
+- [ ] **步骤 1:创建 FatWeightController**
+
+```java
+package com.etotem.cfc.controller.fatloss;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.FatWeightRecord;
+import com.etotem.cfc.service.FatWeightService;
+import com.etotem.cfc.util.ParamUtils;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "减脂-体重管理")
+@RestController
+@RequestMapping("/api/fatloss/weight")
+public class FatWeightController {
+
+    @Resource
+    private FatWeightService fatWeightService;
+
+    @Operation(summary = "获取体重记录列表")
+    @PostMapping("/list")
+    public Result<List<FatWeightRecord>> list(@RequestBody(required = false) Map<String, Object> params,
+                                              @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+        Long memberId = ParamUtils.getLong(params == null ? null : params.get("memberId"), currentMemberId);
+        if (memberId == null) return Result.error("memberId不能为空");
+        Date startDate = ParamUtils.getDate(params == null ? null : params.get("startDate"));
+        Date endDate = ParamUtils.getDate(params == null ? null : params.get("endDate"));
+        return Result.success(fatWeightService.list(memberId, startDate, endDate));
+    }
+
+    @Operation(summary = "创建体重记录")
+    @PostMapping("/create")
+    public Result<FatWeightRecord> create(@RequestBody FatWeightRecord record,
+                                          @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+        if (record.getMemberId() == null) record.setMemberId(currentMemberId);
+        if (record.getMemberId() == null) return Result.error("memberId不能为空");
+        if (record.getWeight() == null) return Result.error("体重不能为空");
+        if (record.getMeasureDate() == null) return Result.error("测量日期不能为空");
+        return Result.success(fatWeightService.create(record));
+    }
+
+    @Operation(summary = "更新体重记录")
+    @PostMapping("/update")
+    public Result<FatWeightRecord> update(@RequestBody FatWeightRecord record) {
+        if (record.getId() == null) return Result.error("id不能为空");
+        return Result.success(fatWeightService.update(record));
+    }
+
+    @Operation(summary = "删除体重记录")
+    @PostMapping("/delete")
+    public Result<Boolean> delete(@RequestBody Map<String, Object> params) {
+        Long id = ParamUtils.getLong(params.get("id"));
+        if (id == null) return Result.error("id不能为空");
+        return Result.success(fatWeightService.delete(id));
+    }
+}
+```
+
+- [ ] **步骤 2:创建 FatDietController**
+
+```java
+package com.etotem.cfc.controller.fatloss;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.FatDietRecord;
+import com.etotem.cfc.service.FatDietService;
+import com.etotem.cfc.util.ParamUtils;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "减脂-饮食记录")
+@RestController
+@RequestMapping("/api/fatloss/diet")
+public class FatDietController {
+
+    @Resource
+    private FatDietService fatDietService;
+
+    @Operation(summary = "获取饮食记录列表")
+    @PostMapping("/list")
+    public Result<List<FatDietRecord>> list(@RequestBody(required = false) Map<String, Object> params,
+                                            @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+        Long memberId = ParamUtils.getLong(params == null ? null : params.get("memberId"), currentMemberId);
+        if (memberId == null) return Result.error("memberId不能为空");
+        Date startDate = ParamUtils.getDate(params == null ? null : params.get("startDate"));
+        Date endDate = ParamUtils.getDate(params == null ? null : params.get("endDate"));
+        return Result.success(fatDietService.list(memberId, startDate, endDate));
+    }
+
+    @Operation(summary = "创建饮食记录")
+    @PostMapping("/create")
+    public Result<FatDietRecord> create(@RequestBody FatDietRecord record,
+                                        @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+        if (record.getMemberId() == null) record.setMemberId(currentMemberId);
+        if (record.getMemberId() == null) return Result.error("memberId不能为空");
+        if (record.getEatDate() == null) return Result.error("进餐日期不能为空");
+        if (record.getFoodName() == null || record.getFoodName().isEmpty()) return Result.error("食物名称不能为空");
+        return Result.success(fatDietService.create(record));
+    }
+
+    @Operation(summary = "删除饮食记录")
+    @PostMapping("/delete")
+    public Result<Boolean> delete(@RequestBody Map<String, Object> params) {
+        Long id = ParamUtils.getLong(params.get("id"));
+        if (id == null) return Result.error("id不能为空");
+        return Result.success(fatDietService.delete(id));
+    }
+
+    @Operation(summary = "获取当日总热量")
+    @PostMapping("/calories-summary")
+    public Result<Integer> caloriesSummary(@RequestBody(required = false) Map<String, Object> params,
+                                           @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+        Long memberId = ParamUtils.getLong(params == null ? null : params.get("memberId"), currentMemberId);
+        if (memberId == null) return Result.error("memberId不能为空");
+        Date date = ParamUtils.getDate(params == null ? null : params.get("date"));
+        if (date == null) date = new Date();
+        return Result.success(fatDietService.sumCalories(memberId, date));
+    }
+}
+```
+
+- [ ] **步骤 3:创建 FatExerciseController**
+
+```java
+package com.etotem.cfc.controller.fatloss;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.FatExerciseRecord;
+import com.etotem.cfc.service.FatExerciseService;
+import com.etotem.cfc.util.ParamUtils;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "减脂-运动记录")
+@RestController
+@RequestMapping("/api/fatloss/exercise")
+public class FatExerciseController {
+
+    @Resource
+    private FatExerciseService fatExerciseService;
+
+    @Operation(summary = "获取运动记录列表")
+    @PostMapping("/list")
+    public Result<List<FatExerciseRecord>> list(@RequestBody(required = false) Map<String, Object> params,
+                                                @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+        Long memberId = ParamUtils.getLong(params == null ? null : params.get("memberId"), currentMemberId);
+        if (memberId == null) return Result.error("memberId不能为空");
+        Date startDate = ParamUtils.getDate(params == null ? null : params.get("startDate"));
+        Date endDate = ParamUtils.getDate(params == null ? null : params.get("endDate"));
+        return Result.success(fatExerciseService.list(memberId, startDate, endDate));
+    }
+
+    @Operation(summary = "创建运动记录")
+    @PostMapping("/create")
+    public Result<FatExerciseRecord> create(@RequestBody FatExerciseRecord record,
+                                            @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+        if (record.getMemberId() == null) record.setMemberId(currentMemberId);
+        if (record.getMemberId() == null) return Result.error("memberId不能为空");
+        if (record.getExerciseDate() == null) return Result.error("运动日期不能为空");
+        if (record.getExerciseType() == null || record.getExerciseType().isEmpty()) return Result.error("运动类型不能为空");
+        return Result.success(fatExerciseService.create(record));
+    }
+
+    @Operation(summary = "删除运动记录")
+    @PostMapping("/delete")
+    public Result<Boolean> delete(@RequestBody Map<String, Object> params) {
+        Long id = ParamUtils.getLong(params.get("id"));
+        if (id == null) return Result.error("id不能为空");
+        return Result.success(fatExerciseService.delete(id));
+    }
+}
+```
+
+- [ ] **步骤 4:创建 FatGoalController**
+
+```java
+package com.etotem.cfc.controller.fatloss;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.FatGoal;
+import com.etotem.cfc.service.FatGoalService;
+import com.etotem.cfc.util.ParamUtils;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Map;
+
+@Tag(name = "减脂-目标管理")
+@RestController
+@RequestMapping("/api/fatloss/goal")
+public class FatGoalController {
+
+    @Resource
+    private FatGoalService fatGoalService;
+
+    @Operation(summary = "获取当前减脂目标")
+    @PostMapping("/active")
+    public Result<FatGoal> active(@RequestBody(required = false) Map<String, Object> params,
+                                  @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+        Long memberId = ParamUtils.getLong(params == null ? null : params.get("memberId"), currentMemberId);
+        if (memberId == null) return Result.error("memberId不能为空");
+        return Result.success(fatGoalService.getActive(memberId));
+    }
+
+    @Operation(summary = "创建减脂目标")
+    @PostMapping("/create")
+    public Result<FatGoal> create(@RequestBody FatGoal goal,
+                                  @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+        if (goal.getMemberId() == null) goal.setMemberId(currentMemberId);
+        if (goal.getMemberId() == null) return Result.error("memberId不能为空");
+        if (goal.getTargetWeight() == null) return Result.error("目标体重不能为空");
+        if (goal.getStartWeight() == null) return Result.error("初始体重不能为空");
+        if (goal.getStartDate() == null) return Result.error("开始日期不能为空");
+        return Result.success(fatGoalService.create(goal));
+    }
+
+    @Operation(summary = "更新减脂目标")
+    @PostMapping("/update")
+    public Result<FatGoal> update(@RequestBody FatGoal goal) {
+        if (goal.getId() == null) return Result.error("id不能为空");
+        return Result.success(fatGoalService.update(goal));
+    }
+
+    @Operation(summary = "完成减脂目标")
+    @PostMapping("/complete")
+    public Result<FatGoal> complete(@RequestBody Map<String, Object> params) {
+        Long id = ParamUtils.getLong(params.get("id"));
+        if (id == null) return Result.error("id不能为空");
+        return Result.success(fatGoalService.complete(id));
+    }
+
+    @Operation(summary = "放弃减脂目标")
+    @PostMapping("/abandon")
+    public Result<FatGoal> abandon(@RequestBody Map<String, Object> params) {
+        Long id = ParamUtils.getLong(params.get("id"));
+        if (id == null) return Result.error("id不能为空");
+        return Result.success(fatGoalService.abandon(id));
+    }
+}
+```
+
+- [ ] **步骤 5:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 6:检查路由是否重复**
+
+运行:`grep -rn '@Mapping' cfc-backend/src/.../controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u | grep fatloss`
+预期:输出 `/api/fatloss/weight/*`、`/api/fatloss/diet/*`、`/api/fatloss/exercise/*`、`/api/fatloss/goal/*`,无重复
+
+- [ ] **步骤 7:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/fatloss/
+git commit -m "feat(fatloss): add controllers for weight/diet/exercise/goal"
+```
+
+---
+
+### 任务 5:后端 - JwtInterceptor 白名单确认
+
+**文件:**
+- 修改:确认 `cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java` 已拦截 `/api/**`
+
+- [ ] **步骤 1:确认 JWT 拦截覆盖新接口**
+
+运行:`grep -n "includePathPatterns\|addPathPatterns" cfc-backend/src/main/java/com/etotem/cfc/config/*Interceptor*.java`
+预期:`addPathPatterns("/api/**")` 已存在,新接口自动受保护(`/api/fatloss/*` 需要登录)
+
+- [ ] **步骤 2:记录确认结果(无需代码变更)**
+
+如果拦截器配置已覆盖 `/api/**` 且 8 个公开路径不包含 `/api/fatloss`,则无需修改。
+
+---
+
+### 任务 6:前端 - 项目脚手架
+
+**文件:**
+- 创建:`xiaofat-miniprogram/manifest.json`
+- 创建:`xiaofat-miniprogram/pages.json`
+- 创建:`xiaofat-miniprogram/App.vue`
+- 创建:`xiaofat-miniprogram/main.js`
+- 创建:`xiaofat-miniprogram/config.js`
+
+- [ ] **步骤 1:创建 manifest.json**
+
+```json
+{
+  "name": "减脂营",
+  "appid": "__UNI__FATLOSS",
+  "description": "科学减脂,从记录开始",
+  "versionName": "1.0.0",
+  "versionCode": "100",
+  "transformPx": false,
+  "mp-weixin": {
+    "appid": "待注册新AppID",
+    "setting": {
+      "urlCheck": false,
+      "es6": true,
+      "postcss": true,
+      "minified": true
+    },
+    "usingComponents": true,
+    "permission": {}
+  },
+  "vueVersion": "2"
+}
+```
+
+- [ ] **步骤 2:创建 pages.json(5 个 TabBar)**
+
+```json
+{
+  "pages": [
+    {
+      "path": "pages/index/index",
+      "style": { "navigationBarTitleText": "减脂营" }
+    },
+    {
+      "path": "pages/weight/index",
+      "style": { "navigationBarTitleText": "体重" }
+    },
+    {
+      "path": "pages/diet/index",
+      "style": { "navigationBarTitleText": "饮食" }
+    },
+    {
+      "path": "pages/exercise/index",
+      "style": { "navigationBarTitleText": "运动" }
+    },
+    {
+      "path": "pages/ai-coach/index",
+      "style": { "navigationBarTitleText": "AI教练" }
+    }
+  ],
+  "subPackages": [
+    {
+      "root": "pages/weight",
+      "pages": [
+        { "path": "create", "style": { "navigationBarTitleText": "记录体重" } },
+        { "path": "chart", "style": { "navigationBarTitleText": "体重趋势" } }
+      ]
+    },
+    {
+      "root": "pages/diet",
+      "pages": [
+        { "path": "create", "style": { "navigationBarTitleText": "记录饮食" } },
+        { "path": "food-query", "style": { "navigationBarTitleText": "食物热量查询" } }
+      ]
+    },
+    {
+      "root": "pages/exercise",
+      "pages": [
+        { "path": "create", "style": { "navigationBarTitleText": "记录运动" } }
+      ]
+    },
+    {
+      "root": "pages/profile",
+      "pages": [
+        { "path": "index", "style": { "navigationBarTitleText": "我的" } }
+      ]
+    }
+  ],
+  "globalStyle": {
+    "navigationBarTextStyle": "black",
+    "navigationBarTitleText": "减脂营",
+    "navigationBarBackgroundColor": "#FFFFFF",
+    "backgroundColor": "#F8F8F8"
+  },
+  "tabBar": {
+    "color": "#7A7E83",
+    "selectedColor": "#F97316",
+    "borderStyle": "black",
+    "backgroundColor": "#FFFFFF",
+    "list": [
+      { "pagePath": "pages/index/index", "text": "首页" },
+      { "pagePath": "pages/weight/index", "text": "体重" },
+      { "pagePath": "pages/diet/index", "text": "饮食" },
+      { "pagePath": "pages/exercise/index", "text": "运动" },
+      { "pagePath": "pages/ai-coach/index", "text": "AI教练" }
+    ]
+  },
+  "easycom": {
+    "autoscan": true,
+    "custom": {
+      "^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
+    }
+  }
+}
+```
+
+- [ ] **步骤 3:创建 config.js(环境自适应)**
+
+```javascript
+/**
+ * 减脂营 API 配置
+ * 环境自动检测(同主项目 config.js 逻辑)
+ */
+let API_BASE_URL = 'https://cfc.etotem.com.cn'
+
+try {
+  const accountInfo = uni.getAccountInfoSync()
+  const env = accountInfo && accountInfo.miniProgram && accountInfo.miniProgram.envVersion
+  if (env) {
+    switch (env) {
+      case 'develop':
+        API_BASE_URL = 'https://cfc.etotem.com.cn'
+        break
+      case 'trial':
+        API_BASE_URL = 'https://cfc.etotem.com.cn'
+        break
+      case 'release':
+        API_BASE_URL = 'https://cfc.etotem.com.cn'
+        break
+    }
+  }
+} catch (e) {
+  // 非小程序环境使用默认地址
+}
+
+export default {
+  API_BASE_URL,
+  api(path) {
+    return API_BASE_URL + path
+  }
+}
+```
+
+- [ ] **步骤 4:创建 App.vue**
+
+```vue
+<script>
+import { silentLogin } from './utils/api.js'
+
+export default {
+  onLaunch: function() {
+    var cachedOpenid = uni.getStorageSync('openid')
+    if (!cachedOpenid) {
+      // 首次启动,静默登录获取 openid
+      uni.login({
+        provider: 'weixin',
+        success: function(loginRes) {
+          if (loginRes && loginRes.code) {
+            silentLogin(loginRes.code).then(function(res) {
+              if (res && res.data && res.data.token) {
+                uni.setStorageSync('token', res.data.token)
+                uni.setStorageSync('openid', res.data.openid)
+                uni.setStorageSync('userId', res.data.userId)
+                uni.setStorageSync('userInfo', {
+                  nickname: res.data.nickname,
+                  userId: res.data.userId,
+                  familyId: res.data.familyId
+                })
+              }
+            }).catch(function() {
+              console.warn('[减脂营] 静默登录失败,用户需手动登录')
+            })
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style>
+/* 全局样式 */
+page {
+  background-color: #F8F8F8;
+  font-size: 28rpx;
+  color: #333;
+}
+</style>
+```
+
+- [ ] **步骤 5:创建 main.js**
+
+```javascript
+import Vue from 'vue'
+import App from './App'
+import store from './store'
+
+Vue.config.productionTip = false
+Vue.prototype.$store = store
+
+App.mpType = 'app'
+
+const app = new Vue({
+  ...App,
+  store
+})
+
+app.$mount()
+```
+
+- [ ] **步骤 6:语法校验**
+
+运行:`node --check xiaofat-miniprogram/config.js && node --check xiaofat-miniprogram/main.js`
+预期:无输出,exit code 0
+
+- [ ] **步骤 7:Commit**
+
+```bash
+git add xiaofat-miniprogram/
+git commit -m "feat(fatloss-miniprogram): init project scaffold"
+```
+
+---
+
+### 任务 7:前端 - 请求封装与工具函数
+
+**文件:**
+- 创建:`xiaofat-miniprogram/utils/api.js`
+- 创建:`xiaofat-miniprogram/utils/format.js`
+- 创建:`xiaofat-miniprogram/store/index.js`
+- 创建:`xiaofat-miniprogram/store/modules/user.js`
+
+- [ ] **步骤 1:创建 utils/api.js(精简版请求封装)**
+
+```javascript
+import config from '../config.js'
+
+const BASE_URL = config.API_BASE_URL
+var _dedupMap = {}
+var DEDUP_WINDOW_MS = 3000
+
+function _makeDedupKey(method, url, body) {
+  var bodyStr = ''
+  try { bodyStr = JSON.stringify(body || {}) } catch (e) { bodyStr = '' }
+  return method + '|' + url + '|' + bodyStr
+}
+
+const request = (url, method = 'POST', data = {}, options = {}) => {
+  var fullUrl = BASE_URL + url
+  const token = uni.getStorageSync('token')
+
+  var key = _makeDedupKey(method, fullUrl, data)
+  if (!options._skipDedup) {
+    var cached = _dedupMap[key]
+    if (cached && Date.now() - cached.ts < DEDUP_WINDOW_MS) {
+      return cached.promise
+    }
+  }
+
+  var _promise = new Promise((resolve, reject) => {
+    uni.request({
+      url: fullUrl,
+      method: method,
+      data: data,
+      header: {
+        'Content-Type': 'application/json',
+        'Authorization': token ? 'Bearer ' + token : ''
+      },
+      success: function(res) {
+        if (res.statusCode === 401) {
+          // 登录过期静默续期
+          uni.login({
+            provider: 'weixin',
+            success: function(loginRes) {
+              if (loginRes && loginRes.code) {
+                silentLogin(loginRes.code).then(function(sr) {
+                  if (sr && sr.data && sr.data.token) {
+                    uni.setStorageSync('token', sr.data.token)
+                    uni.setStorageSync('openid', sr.data.openid)
+                    resolve(sr.data)
+                  } else {
+                    uni.showToast({ title: '登录已过期,请重新进入', icon: 'none' })
+                    reject(res.data)
+                  }
+                })
+              }
+            }
+          })
+          return
+        }
+        if (res.data && res.data.code === 200) {
+          resolve(res.data)
+        } else {
+          var msg = res.data && res.data.message ? res.data.message : '请求失败'
+          uni.showToast({ title: msg, icon: 'none' })
+          reject(res.data)
+        }
+      },
+      fail: function(err) {
+        console.error('[API] 网络错误 ' + method + ' ' + fullUrl, err)
+        uni.showToast({ title: '网络请求失败', icon: 'none' })
+        reject(err)
+      }
+    })
+  })
+
+  if (!options._skipDedup) {
+    _dedupMap[key] = { promise: _promise, ts: Date.now() }
+    _promise.then(function() {
+      setTimeout(function() { delete _dedupMap[key] }, DEDUP_WINDOW_MS)
+    }, function() {
+      setTimeout(function() { delete _dedupMap[key] }, DEDUP_WINDOW_MS)
+    })
+  }
+
+  return _promise
+}
+
+// ===== 认证 =====
+export const silentLogin = (code, options) => {
+  return request('/api/auth/silent-login', 'POST', { code }, options || {})
+}
+
+export const wechatLogin = (code, userInfo) => {
+  return request('/api/auth/wechat-login', 'POST', { code, nickname: userInfo.nickname, avatar: userInfo.avatar })
+}
+
+export const getUserInfo = () => {
+  return request('/api/user/info', 'POST', {})
+}
+
+// ===== 减脂专题 API =====
+// 体重
+export const getWeightList = (memberId, startDate, endDate) => {
+  var data = {}
+  if (memberId) data.memberId = memberId
+  if (startDate) data.startDate = startDate
+  if (endDate) data.endDate = endDate
+  return request('/api/fatloss/weight/list', 'POST', data)
+}
+
+export const createWeightRecord = (data) => {
+  return request('/api/fatloss/weight/create', 'POST', data)
+}
+
+export const updateWeightRecord = (data) => {
+  return request('/api/fatloss/weight/update', 'POST', data)
+}
+
+export const deleteWeightRecord = (id) => {
+  return request('/api/fatloss/weight/delete', 'POST', { id: id })
+}
+
+// 饮食
+export const getDietList = (memberId, startDate, endDate) => {
+  var data = {}
+  if (memberId) data.memberId = memberId
+  if (startDate) data.startDate = startDate
+  if (endDate) data.endDate = endDate
+  return request('/api/fatloss/diet/list', 'POST', data)
+}
+
+export const createDietRecord = (data) => {
+  return request('/api/fatloss/diet/create', 'POST', data)
+}
+
+export const deleteDietRecord = (id) => {
+  return request('/api/fatloss/diet/delete', 'POST', { id: id })
+}
+
+export const getCaloriesSummary = (date) => {
+  var data = {}
+  if (date) data.date = date
+  return request('/api/fatloss/diet/calories-summary', 'POST', data)
+}
+
+// 运动
+export const getExerciseList = (memberId, startDate, endDate) => {
+  var data = {}
+  if (memberId) data.memberId = memberId
+  if (startDate) data.startDate = startDate
+  if (endDate) data.endDate = endDate
+  return request('/api/fatloss/exercise/list', 'POST', data)
+}
+
+export const createExerciseRecord = (data) => {
+  return request('/api/fatloss/exercise/create', 'POST', data)
+}
+
+export const deleteExerciseRecord = (id) => {
+  return request('/api/fatloss/exercise/delete', 'POST', { id: id })
+}
+
+// 目标
+export const getActiveGoal = () => {
+  return request('/api/fatloss/goal/active', 'POST', {})
+}
+
+export const createGoal = (data) => {
+  return request('/api/fatloss/goal/create', 'POST', data)
+}
+
+export const updateGoal = (data) => {
+  return request('/api/fatloss/goal/update', 'POST', data)
+}
+
+export const completeGoal = (id) => {
+  return request('/api/fatloss/goal/complete', 'POST', { id: id })
+}
+
+export const abandonGoal = (id) => {
+  return request('/api/fatloss/goal/abandon', 'POST', { id: id })
+}
+
+// ===== AI 教练(走主系统 AiGateway) =====
+export const aiCoachSend = (data) => {
+  return request('/api/ai/health-coach/send', 'POST', data)
+}
+```
+
+- [ ] **步骤 2:创建 utils/format.js**
+
+```javascript
+/**
+ * 时间格式化工具
+ * iOS 安全:统一用 parseDate 解析后手动拼格式
+ */
+
+export function parseDate(str) {
+  if (!str) return null
+  if (str instanceof Date) return str
+  var normalized = String(str)
+    .replace(' ', 'T')
+    .replace('CST', '')
+    .trim()
+  var d = new Date(normalized)
+  return isNaN(d.getTime()) ? null : d
+}
+
+export function formatDateTime(str) {
+  var d = parseDate(str)
+  if (!d) return ''
+  function pad(n) { return n < 10 ? '0' + n : '' + n }
+  return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) +
+    ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds())
+}
+
+export function formatDate(str) {
+  var d = parseDate(str)
+  if (!d) return ''
+  function pad(n) { return n < 10 ? '0' + n : '' + n }
+  return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
+}
+
+export function today() {
+  var d = new Date()
+  function pad(n) { return n < 10 ? '0' + n : '' + n }
+  return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
+}
+```
+
+- [ ] **步骤 3:创建 store/index.js 与 store/modules/user.js**
+
+```javascript
+// store/index.js
+import Vue from 'vue'
+import Vuex from 'vuex'
+import user from './modules/user'
+
+Vue.use(Vuex)
+
+export default new Vuex.Store({
+  modules: {
+    user: user
+  }
+})
+```
+
+```javascript
+// store/modules/user.js
+export default {
+  state: {
+    token: '',
+    userId: null,
+    memberId: null,
+    nickname: '',
+    familyId: null,
+    openid: '',
+    isLoggedIn: false
+  },
+  mutations: {
+    setToken(state, token) {
+      state.token = token
+      uni.setStorageSync('token', token)
+    },
+    setUserInfo(state, info) {
+      state.userId = info.userId
+      state.memberId = info.memberId
+      state.nickname = info.nickname
+      state.familyId = info.familyId
+      state.openid = info.openid
+      state.isLoggedIn = !!info.userId
+    },
+    logout(state) {
+      state.token = ''
+      state.userId = null
+      state.memberId = null
+      state.nickname = ''
+      state.familyId = null
+      state.openid = ''
+      state.isLoggedIn = false
+      uni.removeStorageSync('token')
+      uni.removeStorageSync('openid')
+      uni.removeStorageSync('userId')
+    }
+  },
+  actions: {
+    initFromStorage({ commit }) {
+      var token = uni.getStorageSync('token')
+      var openid = uni.getStorageSync('openid')
+      var userId = uni.getStorageSync('userId')
+      if (token) {
+        commit('setToken', token)
+      }
+      if (userId) {
+        var userInfo = uni.getStorageSync('userInfo') || {}
+        commit('setUserInfo', {
+          userId: userId,
+          memberId: userInfo.memberId,
+          nickname: userInfo.nickname,
+          familyId: userInfo.familyId,
+          openid: openid
+        })
+      }
+    }
+  }
+}
+```
+
+- [ ] **步骤 4:语法校验**
+
+运行:`node --check xiaofat-miniprogram/utils/api.js && node --check xiaofat-miniprogram/utils/format.js && node --check xiaofat-miniprogram/store/index.js && node --check xiaofat-miniprogram/store/modules/user.js`
+预期:无输出,exit code 0
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add xiaofat-miniprogram/utils/ xiaofat-miniprogram/store/
+git commit -m "feat(fatloss-miniprogram): add api wrappers and store"
+```
+
+---
+
+### 任务 8:前端 - 首页(数据概览 + 快捷入口)
+
+**文件:**
+- 创建:`xiaofat-miniprogram/pages/index/index.vue`
+
+- [ ] **步骤 1:创建 index.vue**
+
+```vue
+<template>
+  <view class="page">
+    <!-- 未登录引导 -->
+    <view v-if="!isLoggedIn" class="login-guide">
+      <view class="logo">🏋️</view>
+      <text class="title">科学减脂,从记录开始</text>
+      <text class="subtitle">记录体重、饮食、运动,AI 为您定制专属方案</text>
+      <button class="btn-primary" @click="handleLogin">微信一键登录</button>
+    </view>
+
+    <!-- 已登录主界面 -->
+    <view v-else>
+      <!-- 今日概览卡片 -->
+      <view class="overview-card">
+        <view class="overview-header">
+          <text class="overview-title">今日概览</text>
+          <text class="overview-date">{{ todayStr }}</text>
+        </view>
+        <view class="overview-grid">
+          <view class="overview-item" @click="goPage('/pages/weight/index')">
+            <text class="ov-value">{{ todayCalories }}</text>
+            <text class="ov-label">摄入热量(kcal)</text>
+          </view>
+          <view class="overview-item" @click="goPage('/pages/exercise/index')">
+            <text class="ov-value">{{ todayBurned }}</text>
+            <text class="ov-label">消耗热量(kcal)</text>
+          </view>
+          <view class="overview-item">
+            <text class="ov-value">{{ todayWeight }}</text>
+            <text class="ov-label">最新体重(kg)</text>
+          </view>
+        </view>
+        <view class="net-balance">
+          热量缺口:{{ netCalories }} kcal
+        </view>
+      </view>
+
+      <!-- 目标进度 -->
+      <view class="goal-card" v-if="goal">
+        <text class="goal-title">当前目标</text>
+        <text class="goal-desc">从 {{ goal.startWeight }}kg → {{ goal.targetWeight }}kg</text>
+        <view class="progress-bar">
+          <view class="progress-fill" :style="{ width: goalProgress + '%' }"></view>
+        </view>
+        <text class="goal-progress-text">{{ goalProgress }}%</text>
+      </view>
+      <view class="goal-card" v-else @click="goPage('/pages/profile/index')">
+        <text class="goal-title">还没有减脂目标?</text>
+        <text class="goal-desc">点击设置目标,开启减脂之旅 →</text>
+      </view>
+
+      <!-- 快捷操作 -->
+      <view class="quick-actions">
+        <view class="action-btn" @click="goPage('/pages/weight/create')">记录体重</view>
+        <view class="action-btn" @click="goPage('/pages/diet/create')">记录饮食</view>
+        <view class="action-btn" @click="goPage('/pages/exercise/create')">记录运动</view>
+      </view>
+
+      <!-- AI 教练入口 -->
+      <view class="ai-entry" @click="goPage('/pages/ai-coach/index')">
+        <text class="ai-icon">🤖</text>
+        <view class="ai-info">
+          <text class="ai-title">AI 减脂教练</text>
+          <text class="ai-subtitle">随时提问,获得个性化建议</text>
+        </view>
+        <text class="ai-arrow">→</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getCaloriesSummary, getWeightList, getExerciseList, getActiveGoal } from '../../utils/api.js'
+import { formatDate, today } from '../../utils/format.js'
+
+export default {
+  data() {
+    return {
+      isLoggedIn: false,
+      todayStr: formatDate(new Date()),
+      todayCalories: 0,
+      todayBurned: 0,
+      todayWeight: '--',
+      netCalories: 0,
+      goal: null,
+      goalProgress: 0
+    }
+  },
+  onShow() {
+    this.isLoggedIn = !!uni.getStorageSync('token')
+    if (this.isLoggedIn) {
+      this.loadOverview()
+    }
+  },
+  methods: {
+    loadOverview() {
+      var self = this
+      var todayStr = today()
+      // 当日热量摄入
+      getCaloriesSummary(todayStr).then(function(res) {
+        self.todayCalories = res.data || 0
+      }).catch(function() {})
+      // 当日运动消耗
+      getExerciseList(null, todayStr, todayStr).then(function(res) {
+        var records = res.data || []
+        var burned = 0
+        records.forEach(function(r) {
+          burned += (r.caloriesBurned || 0)
+        })
+        self.todayBurned = burned
+      }).catch(function() {})
+      // 最新体重
+      getWeightList(null, null, null).then(function(res) {
+        var records = res.data || []
+        if (records.length > 0) {
+          self.todayWeight = records[0].weight
+        }
+      }).catch(function() {})
+      // 当前目标
+      getActiveGoal().then(function(res) {
+        var goal = res.data
+        if (goal) {
+          self.goal = goal
+          var start = parseFloat(goal.startWeight)
+          var target = parseFloat(goal.targetWeight)
+          var diff = start - target
+          if (diff > 0) {
+            // 需要计算当前体重
+            getWeightList(null, null, null).then(function(wres) {
+              var records = wres.data || []
+              if (records.length > 0) {
+                var current = parseFloat(records[0].weight)
+                var progress = Math.min(100, Math.max(0, (start - current) / diff * 100))
+                self.goalProgress = Math.round(progress)
+              }
+            })
+          } else {
+            self.goalProgress = 100
+          }
+        }
+      }).catch(function() {})
+    },
+    handleLogin() {
+      var self = this
+      uni.login({
+        provider: 'weixin',
+        success: function(loginRes) {
+          if (loginRes && loginRes.code) {
+            // 调用主系统静默登录(首次会创建账号)
+            uni.request({
+              url: 'https://cfc.etotem.com.cn/api/auth/silent-login',
+              method: 'POST',
+              data: { code: loginRes.code },
+              header: { 'Content-Type': 'application/json' },
+              success: function(res) {
+                if (res.data && res.data.code === 200 && res.data.data) {
+                  var d = res.data.data
+                  uni.setStorageSync('token', d.token)
+                  uni.setStorageSync('openid', d.openid)
+                  uni.setStorageSync('userId', d.userId)
+                  uni.setStorageSync('userInfo', {
+                    nickname: d.nickname,
+                    userId: d.userId,
+                    familyId: d.familyId
+                  })
+                  self.$store.commit('setToken', d.token)
+                  self.$store.commit('setUserInfo', {
+                    userId: d.userId,
+                    nickname: d.nickname,
+                    familyId: d.familyId,
+                    openid: d.openid
+                  })
+                  self.isLoggedIn = true
+                  self.loadOverview()
+                } else {
+                  uni.showToast({ title: '登录失败', icon: 'none' })
+                }
+              },
+              fail: function() {
+                uni.showToast({ title: '网络请求失败', icon: 'none' })
+              }
+            })
+          }
+        }
+      })
+    },
+    goPage(url) {
+      uni.navigateTo({ url: url })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page {
+  min-height: 100vh;
+  padding: 30rpx;
+  box-sizing: border-box;
+}
+.login-guide {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding-top: 200rpx;
+}
+.logo {
+  font-size: 120rpx;
+  margin-bottom: 30rpx;
+}
+.title {
+  font-size: 40rpx;
+  font-weight: bold;
+  margin-bottom: 20rpx;
+}
+.subtitle {
+  font-size: 26rpx;
+  color: #999;
+  text-align: center;
+  margin-bottom: 60rpx;
+  padding: 0 60rpx;
+}
+.btn-primary {
+  background: #F97316;
+  color: #fff;
+  border-radius: 44rpx;
+  padding: 20rpx 80rpx;
+  font-size: 32rpx;
+}
+.overview-card {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 30rpx;
+  margin-bottom: 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
+}
+.overview-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 24rpx;
+}
+.overview-title {
+  font-size: 32rpx;
+  font-weight: bold;
+}
+.overview-date {
+  font-size: 24rpx;
+  color: #999;
+}
+.overview-grid {
+  display: flex;
+  justify-content: space-between;
+}
+.overview-item {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.ov-value {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #F97316;
+}
+.ov-label {
+  font-size: 22rpx;
+  color: #999;
+  margin-top: 8rpx;
+}
+.net-balance {
+  margin-top: 24rpx;
+  padding-top: 20rpx;
+  border-top: 1rpx solid #f0f0f0;
+  font-size: 26rpx;
+  color: #666;
+  text-align: center;
+}
+.goal-card {
+  background: linear-gradient(135deg, #FF8C42, #F97316);
+  border-radius: 16rpx;
+  padding: 30rpx;
+  margin-bottom: 30rpx;
+  color: #fff;
+}
+.goal-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  display: block;
+  margin-bottom: 12rpx;
+}
+.goal-desc {
+  font-size: 26rpx;
+  display: block;
+  margin-bottom: 20rpx;
+}
+.progress-bar {
+  height: 16rpx;
+  background: rgba(255, 255, 255, 0.3);
+  border-radius: 8rpx;
+  overflow: hidden;
+  margin-bottom: 12rpx;
+}
+.progress-fill {
+  height: 100%;
+  background: #fff;
+  border-radius: 8rpx;
+}
+.goal-progress-text {
+  font-size: 24rpx;
+  opacity: 0.9;
+}
+.quick-actions {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 30rpx;
+}
+.action-btn {
+  flex: 1;
+  margin: 0 10rpx;
+  background: #fff;
+  border-radius: 12rpx;
+  padding: 24rpx 0;
+  text-align: center;
+  font-size: 26rpx;
+  color: #333;
+  border: 1rpx solid #eee;
+}
+.action-btn:first-child {
+  margin-left: 0;
+}
+.action-btn:last-child {
+  margin-right: 0;
+}
+.ai-entry {
+  display: flex;
+  align-items: center;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 30rpx;
+  margin-bottom: 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
+}
+.ai-icon {
+  font-size: 60rpx;
+  margin-right: 20rpx;
+}
+.ai-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+.ai-title {
+  font-size: 30rpx;
+  font-weight: bold;
+}
+.ai-subtitle {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 6rpx;
+}
+.ai-arrow {
+  font-size: 32rpx;
+  color: #ccc;
+}
+</style>
+```
+
+- [ ] **步骤 2:校验 Vue 文件脚本块语法**
+
+运行:`node -e "const fs=require('fs');const s=fs.readFileSync('xiaofat-miniprogram/pages/index/index.vue','utf8');const m=s.match(/<script>([\s\S]*?)<\/script>/);if(m){new Function(m[1].replace(/export default/g,'return'))}console.log('OK')"`
+预期:输出 OK
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add xiaofat-miniprogram/pages/index/index.vue
+git commit -m "feat(fatloss-miniprogram): add home page"
+```
+
+---
+
+### 任务 9:前端 - 体重记录页(列表 + 创建 + 图表)
+
+**文件:**
+- 创建:`xiaofat-miniprogram/pages/weight/index.vue`
+- 创建:`xiaofat-miniprogram/pages/weight/create.vue`
+- 创建:`xiaofat-miniprogram/pages/weight/chart.vue`
+
+- [ ] **步骤 1:创建 weight/index.vue(列表页)**
+
+```vue
+<template>
+  <view class="page">
+    <view class="summary-header">
+      <view class="summary-item">
+        <text class="s-value">{{ latestWeight }}</text>
+        <text class="s-label">最新体重(kg)</text>
+      </view>
+      <view class="summary-item">
+        <text class="s-value">{{ latestBodyFat }}</text>
+        <text class="s-label">体脂率(%)</text>
+      </view>
+      <view class="summary-item">
+        <text class="s-value">{{ totalRecords }}</text>
+        <text class="s-label">记录次数</text>
+      </view>
+    </view>
+
+    <view class="list-header">
+      <text class="list-title">历史记录</text>
+      <text class="chart-link" @click="goChart">查看趋势</text>
+    </view>
+
+    <view v-if="records.length === 0" class="empty">
+      <text>还没有体重记录,点击下方按钮开始记录</text>
+    </view>
+
+    <view v-else class="record-list">
+      <view class="record-item" v-for="(item, index) in records" :key="'w' + index">
+        <view class="record-info">
+          <text class="record-date">{{ item.measureDate }}</text>
+          <text class="record-remark">{{ item.remark || '' }}</text>
+        </view>
+        <view class="record-values">
+          <text class="record-weight">{{ item.weight }} kg</text>
+          <text v-if="item.bodyFatPercent" class="record-fat">{{ item.bodyFatPercent }}%</text>
+        </view>
+        <text class="record-delete" @click="deleteRecord(item.id)">删除</text>
+      </view>
+    </view>
+
+    <button class="btn-primary fixed-bottom" @click="goCreate">+ 记录体重</button>
+  </view>
+</template>
+
+<script>
+import { getWeightList, deleteWeightRecord } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      records: [],
+      latestWeight: '--',
+      latestBodyFat: '--',
+      totalRecords: 0
+    }
+  },
+  onShow() {
+    this.loadData()
+  },
+  methods: {
+    loadData() {
+      var self = this
+      getWeightList(null, null, null).then(function(res) {
+        self.records = res.data || []
+        if (self.records.length > 0) {
+          self.latestWeight = self.records[0].weight
+          self.latestBodyFat = self.records[0].bodyFatPercent || '--'
+        }
+        self.totalRecords = self.records.length
+      }).catch(function() {})
+    },
+    deleteRecord(id) {
+      var self = this
+      uni.showModal({
+        title: '确认删除',
+        content: '确定删除这条体重记录吗?',
+        success: function(res) {
+          if (res.confirm) {
+            deleteWeightRecord(id).then(function() {
+              uni.showToast({ title: '已删除', icon: 'success' })
+              self.loadData()
+            }).catch(function() {})
+          }
+        }
+      })
+    },
+    goCreate() {
+      uni.navigateTo({ url: '/pages/weight/create' })
+    },
+    goChart() {
+      uni.navigateTo({ url: '/pages/weight/chart' })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page {
+  min-height: 100vh;
+  padding: 30rpx;
+  padding-bottom: 140rpx;
+  box-sizing: border-box;
+}
+.summary-header {
+  display: flex;
+  justify-content: space-around;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 30rpx 0;
+  margin-bottom: 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
+}
+.summary-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.s-value {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #F97316;
+}
+.s-label {
+  font-size: 22rpx;
+  color: #999;
+  margin-top: 8rpx;
+}
+.list-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20rpx;
+}
+.list-title {
+  font-size: 30rpx;
+  font-weight: bold;
+}
+.chart-link {
+  font-size: 26rpx;
+  color: #F97316;
+}
+.empty {
+  text-align: center;
+  color: #999;
+  font-size: 26rpx;
+  padding: 80rpx 0;
+}
+.record-list {
+  background: #fff;
+  border-radius: 16rpx;
+  overflow: hidden;
+}
+.record-item {
+  display: flex;
+  align-items: center;
+  padding: 24rpx 30rpx;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.record-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+.record-date {
+  font-size: 26rpx;
+  color: #333;
+}
+.record-remark {
+  font-size: 22rpx;
+  color: #999;
+  margin-top: 4rpx;
+}
+.record-values {
+  display: flex;
+  align-items: baseline;
+  margin-right: 20rpx;
+}
+.record-weight {
+  font-size: 30rpx;
+  font-weight: bold;
+}
+.record-fat {
+  font-size: 24rpx;
+  color: #999;
+  margin-left: 12rpx;
+}
+.record-delete {
+  font-size: 24rpx;
+  color: #ff4d4f;
+  padding: 10rpx;
+}
+.fixed-bottom {
+  position: fixed;
+  bottom: 40rpx;
+  left: 30rpx;
+  right: 30rpx;
+  background: #F97316;
+  color: #fff;
+  border-radius: 44rpx;
+}
+</style>
+```
+
+- [ ] **步骤 2:创建 weight/create.vue**
+
+```vue
+<template>
+  <view class="page">
+    <view class="form-card">
+      <view class="form-item">
+        <text class="label">体重 (kg) *</text>
+        <input class="input" type="digit" v-model="form.weight" placeholder="请输入体重" />
+      </view>
+      <view class="form-item">
+        <text class="label">体脂率 (%)</text>
+        <input class="input" type="digit" v-model="form.bodyFatPercent" placeholder="可选" />
+      </view>
+      <view class="form-item">
+        <text class="label">腰围 (cm)</text>
+        <input class="input" type="digit" v-model="form.waistCircumference" placeholder="可选" />
+      </view>
+      <view class="form-item">
+        <text class="label">测量日期 *</text>
+        <picker mode="date" :value="form.measureDate" @change="onDateChange">
+          <view class="picker-value">{{ form.measureDate || '选择日期' }}</view>
+        </picker>
+      </view>
+      <view class="form-item">
+        <text class="label">备注</text>
+        <input class="input" v-model="form.remark" placeholder="可选" />
+      </view>
+    </view>
+    <button class="btn-primary" @click="submit">保存记录</button>
+  </view>
+</template>
+
+<script>
+import { createWeightRecord } from '../../utils/api.js'
+import { today } from '../../utils/format.js'
+
+export default {
+  data() {
+    return {
+      form: {
+        weight: '',
+        bodyFatPercent: '',
+        waistCircumference: '',
+        measureDate: today(),
+        remark: ''
+      }
+    }
+  },
+  methods: {
+    onDateChange(e) {
+      this.form.measureDate = e.detail.value
+    },
+    submit() {
+      var self = this
+      if (!this.form.weight) {
+        uni.showToast({ title: '请输入体重', icon: 'none' })
+        return
+      }
+      var data = {
+        weight: parseFloat(this.form.weight),
+        bodyFatPercent: this.form.bodyFatPercent ? parseFloat(this.form.bodyFatPercent) : null,
+        waistCircumference: this.form.waistCircumference ? parseFloat(this.form.waistCircumference) : null,
+        measureDate: this.form.measureDate,
+        remark: this.form.remark
+      }
+      createWeightRecord(data).then(function() {
+        uni.showToast({ title: '记录成功', icon: 'success' })
+        setTimeout(function() {
+          uni.navigateBack()
+        }, 1000)
+      }).catch(function() {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page {
+  min-height: 100vh;
+  padding: 30rpx;
+  box-sizing: border-box;
+}
+.form-card {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 30rpx;
+  margin-bottom: 40rpx;
+}
+.form-item {
+  margin-bottom: 30rpx;
+}
+.label {
+  display: block;
+  font-size: 28rpx;
+  color: #333;
+  margin-bottom: 12rpx;
+}
+.input {
+  border: 1rpx solid #e5e5e5;
+  border-radius: 8rpx;
+  padding: 16rpx 20rpx;
+  font-size: 28rpx;
+}
+.picker-value {
+  border: 1rpx solid #e5e5e5;
+  border-radius: 8rpx;
+  padding: 16rpx 20rpx;
+  font-size: 28rpx;
+  color: #333;
+}
+.btn-primary {
+  background: #F97316;
+  color: #fff;
+  border-radius: 44rpx;
+}
+</style>
+```
+
+- [ ] **步骤 3:创建 weight/chart.vue(趋势图 - 使用 Canvas)**
+
+```vue
+<template>
+  <view class="page">
+    <view class="chart-container">
+      <canvas canvas-id="weightChart" id="weightChart" class="chart-canvas"></canvas>
+    </view>
+    <view v-if="records.length === 0" class="empty">
+      <text>暂无数据,请先记录体重</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getWeightList } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      records: []
+    }
+  },
+  onReady() {
+    this.loadData()
+  },
+  methods: {
+    loadData() {
+      var self = this
+      getWeightList(null, null, null).then(function(res) {
+        self.records = res.data || []
+        // 按日期升序
+        self.records.reverse()
+        if (self.records.length > 1) {
+          self.drawChart()
+        }
+      }).catch(function() {})
+    },
+    drawChart() {
+      var ctx = uni.createCanvasContext('weightChart', this)
+      var data = this.records
+      var width = 340
+      var height = 300
+      var padding = 30
+
+      // 计算数据范围
+      var weights = data.map(function(r) { return parseFloat(r.weight) })
+      var min = Math.min.apply(null, weights)
+      var max = Math.max.apply(null, weights)
+      var range = (max - min) || 1
+      // 上下留10%空间
+      min = min - range * 0.1
+      max = max + range * 0.1
+      range = (max - min) || 1
+
+      var chartW = width - padding * 2
+      var chartH = height - padding * 2
+
+      // 绘制网格和折线
+      ctx.setStrokeStyle('#eee')
+      ctx.setLineWidth(1)
+      for (var i = 0; i <= 4; i++) {
+        var y = padding + chartH * i / 4
+        ctx.beginPath()
+        ctx.moveTo(padding, y)
+        ctx.lineTo(width - padding, y)
+        ctx.stroke()
+      }
+
+      // 绘制折线
+      ctx.setStrokeStyle('#F97316')
+      ctx.setLineWidth(2)
+      ctx.beginPath()
+      var stepX = data.length > 1 ? chartW / (data.length - 1) : 0
+      for (var j = 0; j < data.length; j++) {
+        var x = padding + j * stepX
+        var value = parseFloat(data[j].weight)
+        var y2 = padding + chartH * (1 - (value - min) / range)
+        if (j === 0) {
+          ctx.moveTo(x, y2)
+        } else {
+          ctx.lineTo(x, y2)
+        }
+      }
+      ctx.stroke()
+
+      // 绘制数据点
+      ctx.setFillStyle('#F97316')
+      for (var k = 0; k < data.length; k++) {
+        var x2 = padding + k * stepX
+        var y3 = padding + chartH * (1 - (parseFloat(data[k].weight) - min) / range)
+        ctx.beginPath()
+        ctx.arc(x2, y3, 3, 0, Math.PI * 2)
+        ctx.fill()
+      }
+
+      // 标签:首尾日期
+      if (data.length > 0) {
+        ctx.setFillStyle('#999')
+        ctx.setFontSize(10)
+        ctx.fillText(data[0].measureDate, padding, height - 5)
+        if (data.length > 1) {
+          ctx.fillText(data[data.length - 1].measureDate, width - padding - 50, height - 5)
+        }
+        // 最大最小值标注
+        ctx.fillText('max ' + Math.max.apply(null, weights), padding + 5, padding + 10)
+        ctx.fillText('min ' + Math.min.apply(null, weights), padding + 5, padding + chartH - 5)
+      }
+
+      ctx.draw()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page {
+  min-height: 100vh;
+  padding: 30rpx;
+  box-sizing: border-box;
+}
+.chart-container {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 20rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
+}
+.chart-canvas {
+  width: 680rpx;
+  height: 600rpx;
+}
+.empty {
+  text-align: center;
+  color: #999;
+  font-size: 26rpx;
+  padding: 80rpx 0;
+}
+</style>
+```
+
+- [ ] **步骤 4:校验 Vue 文件语法 + Commit**
+
+```bash
+git add xiaofat-miniprogram/pages/weight/
+git commit -m "feat(fatloss-miniprogram): add weight tracking pages"
+```
+
+---
+
+### 任务 10:前端 - 饮食记录页
+
+**文件:**
+- 创建:`xiaofat-miniprogram/pages/diet/index.vue`
+- 创建:`xiaofat-miniprogram/pages/diet/create.vue`
+
+- [ ] **步骤 1:创建 diet/index.vue**
+
+```vue
+<template>
+  <view class="page">
+    <!-- 今日热量概览 -->
+    <view class="calories-card">
+      <text class="calories-title">今日摄入</text>
+      <text class="calories-value">{{ todayCalories }} kcal</text>
+      <text class="calories-target">目标:{{ dailyTarget }} kcal</text>
+      <view class="calories-bar">
+        <view class="calories-fill" :style="{ width: caloriesPercent + '%' }"></view>
+      </view>
+    </view>
+
+    <view class="diet-list">
+      <view v-for="mealGroup in mealGroups" :key="mealGroup.type" class="meal-section">
+        <view class="meal-header">
+          <text class="meal-name">{{ mealGroup.label }}</text>
+          <text class="meal-calories">{{ mealGroup.total }} kcal</text>
+        </view>
+        <view v-for="(item, index) in mealGroup.items" :key="'m' + index" class="meal-item">
+          <view class="meal-info">
+            <text class="meal-food">{{ item.foodName }}</text>
+            <text class="meal-gram">{{ item.foodWeight ? item.foodWeight + 'g' : '' }}</text>
+          </view>
+          <text class="meal-item-calories">{{ item.calories }} kcal</text>
+          <text class="meal-delete" @click="deleteRecord(item.id)">删除</text>
+        </view>
+        <view v-if="mealGroup.items.length === 0" class="meal-empty">暂无记录</view>
+      </view>
+    </view>
+
+    <button class="btn-primary fixed-bottom" @click="goCreate">+ 记录饮食</button>
+  </view>
+</template>
+
+<script>
+import { getDietList, deleteDietRecord, getCaloriesSummary } from '../../utils/api.js'
+import { today } from '../../utils/format.js'
+
+export default {
+  data() {
+    return {
+      records: [],
+      todayCalories: 0,
+      dailyTarget: 1500,
+      mealGroups: [
+        { type: 'breakfast', label: '早餐', items: [], total: 0 },
+        { type: 'lunch', label: '午餐', items: [], total: 0 },
+        { type: 'dinner', label: '晚餐', items: [], total: 0 },
+        { type: 'snack', label: '加餐', items: [], total: 0 }
+      ]
+    }
+  },
+  onShow() {
+    this.loadData()
+  },
+  computed: {
+    caloriesPercent() {
+      var pct = this.todayCalories / this.dailyTarget * 100
+      return Math.min(100, pct)
+    }
+  },
+  methods: {
+    loadData() {
+      var self = this
+      var todayStr = today()
+      getDietList(null, todayStr, todayStr).then(function(res) {
+        self.records = res.data || []
+        // 按餐次分组
+        self.mealGroups.forEach(function(g) {
+          g.items = self.records.filter(function(r) { return r.mealType === g.type })
+          g.total = g.items.reduce(function(sum, r) { return sum + (r.calories || 0) }, 0)
+        })
+      }).catch(function() {})
+      getCaloriesSummary(todayStr).then(function(res) {
+        self.todayCalories = res.data || 0
+      }).catch(function() {})
+    },
+    deleteRecord(id) {
+      var self = this
+      uni.showModal({
+        title: '确认删除',
+        content: '确定删除这条饮食记录吗?',
+        success: function(res) {
+          if (res.confirm) {
+            deleteDietRecord(id).then(function() {
+              uni.showToast({ title: '已删除', icon: 'success' })
+              self.loadData()
+            }).catch(function() {})
+          }
+        }
+      })
+    },
+    goCreate() {
+      uni.navigateTo({ url: '/pages/diet/create' })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page {
+  min-height: 100vh;
+  padding: 30rpx;
+  padding-bottom: 140rpx;
+  box-sizing: border-box;
+}
+.calories-card {
+  background: linear-gradient(135deg, #FF8C42, #F97316);
+  border-radius: 16rpx;
+  padding: 30rpx;
+  margin-bottom: 30rpx;
+  color: #fff;
+}
+.calories-title {
+  display: block;
+  font-size: 26rpx;
+  opacity: 0.9;
+}
+.calories-value {
+  display: block;
+  font-size: 56rpx;
+  font-weight: bold;
+  margin: 10rpx 0;
+}
+.calories-target {
+  display: block;
+  font-size: 24rpx;
+  opacity: 0.8;
+  margin-bottom: 16rpx;
+}
+.calories-bar {
+  height: 12rpx;
+  background: rgba(255, 255, 255, 0.3);
+  border-radius: 6rpx;
+  overflow: hidden;
+}
+.calories-fill {
+  height: 100%;
+  background: #fff;
+  border-radius: 6rpx;
+}
+.diet-list {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 20rpx 30rpx;
+}
+.meal-section {
+  margin-bottom: 30rpx;
+}
+.meal-header {
+  display: flex;
+  justify-content: space-between;
+  padding-bottom: 16rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+  margin-bottom: 12rpx;
+}
+.meal-name {
+  font-size: 30rpx;
+  font-weight: bold;
+}
+.meal-calories {
+  font-size: 26rpx;
+  color: #F97316;
+}
+.meal-item {
+  display: flex;
+  align-items: center;
+  padding: 16rpx 0;
+}
+.meal-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+.meal-food {
+  font-size: 26rpx;
+}
+.meal-gram {
+  font-size: 22rpx;
+  color: #999;
+}
+.meal-item-calories {
+  font-size: 26rpx;
+  color: #666;
+  margin-right: 20rpx;
+}
+.meal-delete {
+  font-size: 24rpx;
+  color: #ff4d4f;
+}
+.meal-empty {
+  text-align: center;
+  color: #ccc;
+  font-size: 24rpx;
+  padding: 16rpx 0;
+}
+.fixed-bottom {
+  position: fixed;
+  bottom: 40rpx;
+  left: 30rpx;
+  right: 30rpx;
+  background: #F97316;
+  color: #fff;
+  border-radius: 44rpx;
+}
+</style>
+```
+
+- [ ] **步骤 2:创建 diet/create.vue**
+
+```vue
+<template>
+  <view class="page">
+    <view class="form-card">
+      <view class="form-item">
+        <text class="label">餐次 *</text>
+        <picker :range="mealTypeOptions" range-key="label" @change="onMealTypeChange">
+          <view class="picker-value">{{ mealTypeLabel }}</view>
+        </picker>
+      </view>
+      <view class="form-item">
+        <text class="label">食物名称 *</text>
+        <input class="input" v-model="form.foodName" placeholder="如:鸡胸肉" />
+      </view>
+      <view class="form-item">
+        <text class="label">食物重量 (克)</text>
+        <input class="input" type="digit" v-model="form.foodWeight" placeholder="可选" />
+      </view>
+      <view class="form-item">
+        <text class="label">热量 (kcal) *</text>
+        <input class="input" type="number" v-model="form.calories" placeholder="输入热量" />
+      </view>
+      <view class="form-item">
+        <text class="label">日期 *</text>
+        <picker mode="date" :value="form.eatDate" @change="onDateChange">
+          <view class="picker-value">{{ form.eatDate || '选择日期' }}</view>
+        </picker>
+      </view>
+      <view class="form-item">
+        <text class="label">备注</text>
+        <input class="input" v-model="form.remark" placeholder="可选" />
+      </view>
+    </view>
+    <button class="btn-primary" @click="submit">保存记录</button>
+  </view>
+</template>
+
+<script>
+import { createDietRecord } from '../../utils/api.js'
+import { today } from '../../utils/format.js'
+
+export default {
+  data() {
+    return {
+      mealTypeOptions: [
+        { type: 'breakfast', label: '早餐' },
+        { type: 'lunch', label: '午餐' },
+        { type: 'dinner', label: '晚餐' },
+        { type: 'snack', label: '加餐' }
+      ],
+      mealTypeLabel: '早餐',
+      form: {
+        mealType: 'breakfast',
+        foodName: '',
+        foodWeight: '',
+        calories: '',
+        eatDate: today(),
+        remark: ''
+      }
+    }
+  },
+  methods: {
+    onMealTypeChange(e) {
+      var idx = e.detail.value
+      this.mealTypeLabel = this.mealTypeOptions[idx].label
+      this.form.mealType = this.mealTypeOptions[idx].type
+    },
+    onDateChange(e) {
+      this.form.eatDate = e.detail.value
+    },
+    submit() {
+      var self = this
+      if (!this.form.foodName) {
+        uni.showToast({ title: '请输入食物名称', icon: 'none' })
+        return
+      }
+      if (!this.form.calories) {
+        uni.showToast({ title: '请输入热量', icon: 'none' })
+        return
+      }
+      var data = {
+        mealType: this.form.mealType,
+        foodName: this.form.foodName,
+        foodWeight: this.form.foodWeight ? parseFloat(this.form.foodWeight) : null,
+        calories: parseInt(this.form.calories),
+        eatDate: this.form.eatDate,
+        remark: this.form.remark
+      }
+      createDietRecord(data).then(function() {
+        uni.showToast({ title: '记录成功', icon: 'success' })
+        setTimeout(function() {
+          uni.navigateBack()
+        }, 1000)
+      }).catch(function() {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page {
+  min-height: 100vh;
+  padding: 30rpx;
+  box-sizing: border-box;
+}
+.form-card {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 30rpx;
+  margin-bottom: 40rpx;
+}
+.form-item {
+  margin-bottom: 30rpx;
+}
+.label {
+  display: block;
+  font-size: 28rpx;
+  color: #333;
+  margin-bottom: 12rpx;
+}
+.input {
+  border: 1rpx solid #e5e5e5;
+  border-radius: 8rpx;
+  padding: 16rpx 20rpx;
+  font-size: 28rpx;
+}
+.picker-value {
+  border: 1rpx solid #e5e5e5;
+  border-radius: 8rpx;
+  padding: 16rpx 20rpx;
+  font-size: 28rpx;
+  color: #333;
+}
+.btn-primary {
+  background: #F97316;
+  color: #fff;
+  border-radius: 44rpx;
+}
+</style>
+```
+
+- [ ] **步骤 3:校验 + Commit**
+
+```bash
+git add xiaofat-miniprogram/pages/diet/
+git commit -m "feat(fatloss-miniprogram): add diet tracking pages"
+```
+
+---
+
+### 任务 11:前端 - 运动记录页
+
+**文件:**
+- 创建:`xiaofat-miniprogram/pages/exercise/index.vue`
+- 创建:`xiaofat-miniprogram/pages/exercise/create.vue`
+
+- [ ] **步骤 1:创建 exercise/index.vue**
+
+```vue
+<template>
+  <view class="page">
+    <view class="summary-card">
+      <view class="summary-item">
+        <text class="s-value">{{ totalSessions }}</text>
+        <text class="s-label">本周运动次数</text>
+      </view>
+      <view class="summary-item">
+        <text class="s-value">{{ totalMinutes }}</text>
+        <text class="s-label">本周运动时长(分)</text>
+      </view>
+      <view class="summary-item">
+        <text class="s-value">{{ totalBurned }}</text>
+        <text class="s-label">本周消耗(kcal)</text>
+      </view>
+    </view>
+
+    <view v-if="records.length === 0" class="empty">
+      <text>还没有运动记录,点击下方按钮开始打卡</text>
+    </view>
+
+    <view v-else class="record-list">
+      <view class="record-item" v-for="(item, index) in records" :key="'e' + index">
+        <view class="record-info">
+          <text class="record-date">{{ item.exerciseDate }}</text>
+          <text class="record-type">{{ item.exerciseType }}</text>
+        </view>
+        <view class="record-values">
+          <text class="record-duration">{{ item.durationMin }} 分</text>
+          <text v-if="item.distanceKm" class="record-distance">{{ item.distanceKm }} km</text>
+          <text class="record-burned">{{ item.caloriesBurned }} kcal</text>
+        </view>
+        <text class="record-delete" @click="deleteRecord(item.id)">删除</text>
+      </view>
+    </view>
+
+    <button class="btn-primary fixed-bottom" @click="goCreate">+ 运动打卡</button>
+  </view>
+</template>
+
+<script>
+import { getExerciseList, deleteExerciseRecord } from '../../utils/api.js'
+import { today } from '../../utils/format.js'
+
+export default {
+  data() {
+    return {
+      records: [],
+      totalSessions: 0,
+      totalMinutes: 0,
+      totalBurned: 0
+    }
+  },
+  onShow() {
+    this.loadData()
+  },
+  methods: {
+    loadData() {
+      var self = this
+      getExerciseList(null, null, null).then(function(res) {
+        self.records = res.data || []
+        var burned = 0
+        var minutes = 0
+        self.records.forEach(function(r) {
+          burned += (r.caloriesBurned || 0)
+          minutes += (r.durationMin || 0)
+        })
+        self.totalSessions = self.records.length
+        self.totalMinutes = minutes
+        self.totalBurned = burned
+      }).catch(function() {})
+    },
+    deleteRecord(id) {
+      var self = this
+      uni.showModal({
+        title: '确认删除',
+        content: '确定删除这条运动记录吗?',
+        success: function(res) {
+          if (res.confirm) {
+            deleteExerciseRecord(id).then(function() {
+              uni.showToast({ title: '已删除', icon: 'success' })
+              self.loadData()
+            }).catch(function() {})
+          }
+        }
+      })
+    },
+    goCreate() {
+      uni.navigateTo({ url: '/pages/exercise/create' })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page {
+  min-height: 100vh;
+  padding: 30rpx;
+  padding-bottom: 140rpx;
+  box-sizing: border-box;
+}
+.summary-card {
+  display: flex;
+  justify-content: space-around;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 30rpx 0;
+  margin-bottom: 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
+}
+.summary-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.s-value {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #10B981;
+}
+.s-label {
+  font-size: 22rpx;
+  color: #999;
+  margin-top: 8rpx;
+}
+.empty {
+  text-align: center;
+  color: #999;
+  font-size: 26rpx;
+  padding: 80rpx 0;
+}
+.record-list {
+  background: #fff;
+  border-radius: 16rpx;
+  overflow: hidden;
+}
+.record-item {
+  display: flex;
+  align-items: center;
+  padding: 24rpx 30rpx;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.record-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+.record-date {
+  font-size: 26rpx;
+  color: #333;
+}
+.record-type {
+  font-size: 22rpx;
+  color: #10B981;
+  margin-top: 4rpx;
+}
+.record-values {
+  display: flex;
+  align-items: center;
+  margin-right: 20rpx;
+}
+.record-duration {
+  font-size: 26rpx;
+  font-weight: bold;
+}
+.record-distance {
+  font-size: 22rpx;
+  color: #999;
+  margin-left: 10rpx;
+}
+.record-burned {
+  font-size: 24rpx;
+  color: #F97316;
+  margin-left: 16rpx;
+}
+.record-delete {
+  font-size: 24rpx;
+  color: #ff4d4f;
+  padding: 10rpx;
+}
+.fixed-bottom {
+  position: fixed;
+  bottom: 40rpx;
+  left: 30rpx;
+  right: 30rpx;
+  background: #10B981;
+  color: #fff;
+  border-radius: 44rpx;
+}
+</style>
+```
+
+- [ ] **步骤 2:创建 exercise/create.vue**
+
+```vue
+<template>
+  <view class="page">
+    <view class="form-card">
+      <view class="form-item">
+        <text class="label">运动类型 *</text>
+        <picker :range="exerciseTypes" @change="onTypeChange">
+          <view class="picker-value">{{ form.exerciseType || '选择运动类型' }}</view>
+        </picker>
+      </view>
+      <view class="form-item">
+        <text class="label">时长 (分钟) *</text>
+        <input class="input" type="number" v-model="form.durationMin" placeholder="请输入时长" />
+      </view>
+      <view class="form-item">
+        <text class="label">距离 (km)</text>
+        <input class="input" type="digit" v-model="form.distanceKm" placeholder="可选" />
+      </view>
+      <view class="form-item">
+        <text class="label">消耗热量 (kcal)</text>
+        <input class="input" type="number" v-model="form.caloriesBurned" placeholder="可选" />
+      </view>
+      <view class="form-item">
+        <text class="label">日期 *</text>
+        <picker mode="date" :value="form.exerciseDate" @change="onDateChange">
+          <view class="picker-value">{{ form.exerciseDate || '选择日期' }}</view>
+        </picker>
+      </view>
+      <view class="form-item">
+        <text class="label">备注</text>
+        <input class="input" v-model="form.remark" placeholder="可选" />
+      </view>
+    </view>
+    <button class="btn-primary" @click="submit">保存记录</button>
+  </view>
+</template>
+
+<script>
+import { createExerciseRecord } from '../../utils/api.js'
+import { today } from '../../utils/format.js'
+
+export default {
+  data() {
+    return {
+      exerciseTypes: ['跑步', '快走', '游泳', '骑行', '健身', '瑜伽', '跳绳', '其他'],
+      form: {
+        exerciseType: '',
+        durationMin: '',
+        distanceKm: '',
+        caloriesBurned: '',
+        exerciseDate: today(),
+        remark: ''
+      }
+    }
+  },
+  methods: {
+    onTypeChange(e) {
+      this.form.exerciseType = this.exerciseTypes[e.detail.value]
+    },
+    onDateChange(e) {
+      this.form.exerciseDate = e.detail.value
+    },
+    submit() {
+      var self = this
+      if (!this.form.exerciseType) {
+        uni.showToast({ title: '请选择运动类型', icon: 'none' })
+        return
+      }
+      if (!this.form.durationMin) {
+        uni.showToast({ title: '请输入运动时长', icon: 'none' })
+        return
+      }
+      var data = {
+        exerciseType: this.form.exerciseType,
+        durationMin: parseInt(this.form.durationMin),
+        distanceKm: this.form.distanceKm ? parseFloat(this.form.distanceKm) : null,
+        caloriesBurned: this.form.caloriesBurned ? parseInt(this.form.caloriesBurned) : 0,
+        exerciseDate: this.form.exerciseDate,
+        remark: this.form.remark
+      }
+      createExerciseRecord(data).then(function() {
+        uni.showToast({ title: '打卡成功', icon: 'success' })
+        setTimeout(function() {
+          uni.navigateBack()
+        }, 1000)
+      }).catch(function() {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page {
+  min-height: 100vh;
+  padding: 30rpx;
+  box-sizing: border-box;
+}
+.form-card {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 30rpx;
+  margin-bottom: 40rpx;
+}
+.form-item {
+  margin-bottom: 30rpx;
+}
+.label {
+  display: block;
+  font-size: 28rpx;
+  color: #333;
+  margin-bottom: 12rpx;
+}
+.input {
+  border: 1rpx solid #e5e5e5;
+  border-radius: 8rpx;
+  padding: 16rpx 20rpx;
+  font-size: 28rpx;
+}
+.picker-value {
+  border: 1rpx solid #e5e5e5;
+  border-radius: 8rpx;
+  padding: 16rpx 20rpx;
+  font-size: 28rpx;
+  color: #333;
+}
+.btn-primary {
+  background: #10B981;
+  color: #fff;
+  border-radius: 44rpx;
+}
+</style>
+```
+
+- [ ] **步骤 3:校验 + Commit**
+
+```bash
+git add xiaofat-miniprogram/pages/exercise/
+git commit -m "feat(fatloss-miniprogram): add exercise tracking pages"
+```
+
+---
+
+### 任务 12:前端 - AI 教练对话页
+
+**文件:**
+- 创建:`xiaofat-miniprogram/pages/ai-coach/index.vue`
+
+- [ ] **步骤 1:创建 ai-coach/index.vue**
+
+```vue
+<template>
+  <view class="chat-page">
+    <!-- 聊天消息列表 -->
+    <scroll-view scroll-y class="chat-list" :scroll-into-view="scrollIntoView">
+      <view v-for="(msg, index) in messages" :key="'m' + index" class="msg-row" :class="msg.role === 'user' ? 'row-user' : 'row-ai'">
+        <view class="msg-bubble" :class="msg.role === 'user' ? 'bubble-user' : 'bubble-ai'">
+          <text class="msg-text">{{ msg.content }}</text>
+        </view>
+      </view>
+      <view v-if="thinking" class="msg-row row-ai">
+        <view class="msg-bubble bubble-ai thinking">
+          <text>正在思考...</text>
+        </view>
+      </view>
+      <view id="chat-bottom" style="height: 10px;"></view>
+    </scroll-view>
+
+    <!-- 快捷提问 -->
+    <scroll-view scroll-x class="quick-questions">
+      <view class="quick-item" v-for="(q, index) in quickQuestions" :key="'q' + index" @click="askQuick(q)">
+        <text>{{ q }}</text>
+      </view>
+    </scroll-view>
+
+    <!-- 输入栏 -->
+    <view class="input-bar">
+      <input class="chat-input" v-model="inputText" placeholder="问AI教练任何减脂问题" confirm-type="send" @confirm="sendMessage" />
+      <button class="send-btn" @click="sendMessage" :disabled="!inputText">发送</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { aiCoachSend } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      messages: [
+        { role: 'ai', content: '你好!我是你的 AI 减脂教练。我可以帮你分析体重数据、制定饮食计划、提供运动建议。有什么想问的吗?' }
+      ],
+      inputText: '',
+      thinking: false,
+      scrollIntoView: 'chat-bottom',
+      quickQuestions: ['今天吃多少卡路里合适?', '我的体重为什么不降?', '推荐减脂早餐', '怎么安排运动?']
+    }
+  },
+  onShow() {
+    this.scrollToBottom()
+  },
+  methods: {
+    askQuick(q) {
+      this.inputText = q
+      this.sendMessage()
+    },
+    sendMessage() {
+      var self = this
+      var text = this.inputText.trim()
+      if (!text || this.thinking) return
+      this.messages.push({ role: 'user', content: text })
+      this.inputText = ''
+      this.thinking = true
+      this.scrollToBottom()
+
+      aiCoachSend({ message: text }).then(function(res) {
+        self.thinking = false
+        var answer = res.data && res.data.reply
+          ? res.data.reply
+          : (res.data && res.data.content
+            ? res.data.content
+            : '抱歉,我暂时无法回答这个问题。')
+        self.messages.push({ role: 'ai', content: answer })
+        self.scrollToBottom()
+      }).catch(function() {
+        self.thinking = false
+        self.messages.push({ role: 'ai', content: '网络异常,请稍后再试。' })
+        self.scrollToBottom()
+      })
+    },
+    scrollToBottom() {
+      var self = this
+      setTimeout(function() {
+        self.scrollIntoView = 'chat-bottom'
+      }, 100)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.chat-page {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+}
+.chat-list {
+  flex: 1;
+  padding: 20rpx;
+  box-sizing: border-box;
+}
+.msg-row {
+  display: flex;
+  margin-bottom: 20rpx;
+}
+.row-user {
+  justify-content: flex-end;
+}
+.row-ai {
+  justify-content: flex-start;
+}
+.msg-bubble {
+  max-width: 70%;
+  padding: 20rpx 24rpx;
+  border-radius: 16rpx;
+  font-size: 28rpx;
+  line-height: 1.5;
+}
+.bubble-user {
+  background: #F97316;
+  color: #fff;
+  border-top-right-radius: 4rpx;
+}
+.bubble-ai {
+  background: #fff;
+  color: #333;
+  border-top-left-radius: 4rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
+}
+.thinking {
+  color: #999;
+}
+.quick-questions {
+  white-space: nowrap;
+  padding: 16rpx 20rpx;
+  background: #fff;
+}
+.quick-item {
+  display: inline-block;
+  background: #FFF7ED;
+  color: #F97316;
+  border: 1rpx solid #FFE4CC;
+  border-radius: 24rpx;
+  padding: 10rpx 24rpx;
+  font-size: 24rpx;
+  margin-right: 16rpx;
+}
+.input-bar {
+  display: flex;
+  align-items: center;
+  padding: 16rpx 20rpx;
+  background: #fff;
+  border-top: 1rpx solid #eee;
+}
+.chat-input {
+  flex: 1;
+  border: 1rpx solid #e5e5e5;
+  border-radius: 36rpx;
+  padding: 14rpx 24rpx;
+  font-size: 28rpx;
+  margin-right: 16rpx;
+}
+.send-btn {
+  background: #F97316;
+  color: #fff;
+  border-radius: 36rpx;
+  font-size: 26rpx;
+  padding: 0 30rpx;
+  line-height: 64rpx;
+  height: 64rpx;
+}
+.send-btn[disabled] {
+  background: #ccc;
+}
+</style>
+```
+
+- [ ] **步骤 2:校验 + Commit**
+
+```bash
+git add xiaofat-miniprogram/pages/ai-coach/
+git commit -m "feat(fatloss-miniprogram): add AI coach chat page"
+```
+
+---
+
+### 任务 13:前端 - 个人页(目标设定 + 个人信息)
+
+**文件:**
+- 创建:`xiaofat-miniprogram/pages/profile/index.vue`
+
+- [ ] **步骤 1:创建 profile/index.vue**
+
+```vue
+<template>
+  <view class="page">
+    <!-- 用户信息 -->
+    <view class="user-card">
+      <view class="avatar">{{ nickname ? nickname.slice(0, 1) : '减' }}</view>
+      <view class="user-info">
+        <text class="user-name">{{ nickname || '减脂用户' }}</text>
+        <text class="user-days">累计打卡 {{ totalDays }} 天</text>
+      </view>
+    </view>
+
+    <!-- 目标设置 -->
+    <view class="section">
+      <text class="section-title">减脂目标</text>
+      <view v-if="goal" class="goal-detail">
+        <view class="goal-row">
+          <text class="goal-label">初始体重</text>
+          <text class="goal-value">{{ goal.startWeight }} kg</text>
+        </view>
+        <view class="goal-row">
+          <text class="goal-label">目标体重</text>
+          <text class="goal-value">{{ goal.targetWeight }} kg</text>
+        </view>
+        <view class="goal-row" v-if="goal.targetBodyFat">
+          <text class="goal-label">目标体脂率</text>
+          <text class="goal-value">{{ goal.targetBodyFat }}%</text>
+        </view>
+        <view class="goal-row" v-if="goal.targetDate">
+          <text class="goal-label">目标日期</text>
+          <text class="goal-value">{{ goal.targetDate }}</text>
+        </view>
+        <view class="goal-actions">
+          <button class="btn-sm btn-complete" @click="completeGoal">达成目标</button>
+          <button class="btn-sm btn-abandon" @click="abandonGoal">放弃目标</button>
+        </view>
+      </view>
+      <view v-else class="goal-empty">
+        <text>还没有设定减脂目标</text>
+        <button class="btn-primary btn-small" @click="showGoalForm = true">立即设定</button>
+      </view>
+    </view>
+
+    <!-- 设定目标表单(弹层) -->
+    <view v-if="showGoalForm" class="modal-mask" @click="showGoalForm = false">
+      <view class="modal-content" @click.stop>
+        <text class="modal-title">设定减脂目标</text>
+        <view class="form-item">
+          <text class="label">初始体重 (kg) *</text>
+          <input class="input" type="digit" v-model="goalForm.startWeight" placeholder="当前体重" />
+        </view>
+        <view class="form-item">
+          <text class="label">目标体重 (kg) *</text>
+          <input class="input" type="digit" v-model="goalForm.targetWeight" placeholder="目标体重" />
+        </view>
+        <view class="form-item">
+          <text class="label">目标体脂率 (%)</text>
+          <input class="input" type="digit" v-model="goalForm.targetBodyFat" placeholder="可选" />
+        </view>
+        <view class="form-item">
+          <text class="label">目标日期</text>
+          <picker mode="date" :value="goalForm.targetDate" @change="onTargetDateChange">
+            <view class="picker-value">{{ goalForm.targetDate || '选择日期' }}</view>
+          </picker>
+        </view>
+        <view class="modal-actions">
+          <button class="btn-sm btn-cancel" @click="showGoalForm = false">取消</button>
+          <button class="btn-sm btn-save" @click="submitGoal">保存目标</button>
+        </view>
+      </view>
+    </view>
+
+    <!-- 其他设置 -->
+    <view class="section">
+      <text class="section-title">其他</text>
+      <view class="setting-item" @click="logout">
+        <text class="setting-text">退出登录</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getActiveGoal, createGoal, completeGoal, abandonGoal } from '../../utils/api.js'
+import { today } from '../../utils/format.js'
+
+export default {
+  data() {
+    return {
+      nickname: uni.getStorageSync('userInfo') ? uni.getStorageSync('userInfo').nickname : '',
+      totalDays: 0,
+      goal: null,
+      showGoalForm: false,
+      goalForm: {
+        startWeight: '',
+        targetWeight: '',
+        targetBodyFat: '',
+        targetDate: ''
+      }
+    }
+  },
+  onShow() {
+    this.loadData()
+  },
+  methods: {
+    loadData() {
+      var self = this
+      getActiveGoal().then(function(res) {
+        self.goal = res.data || null
+        if (self.goal) {
+          self.goalForm.startWeight = self.goal.startWeight
+          self.goalForm.targetWeight = self.goal.targetWeight
+          self.goalForm.targetBodyFat = self.goal.targetBodyFat || ''
+          self.goalForm.targetDate = self.goal.targetDate || ''
+        }
+      }).catch(function() {})
+    },
+    onTargetDateChange(e) {
+      this.goalForm.targetDate = e.detail.value
+    },
+    submitGoal() {
+      var self = this
+      if (!this.goalForm.startWeight || !this.goalForm.targetWeight) {
+        uni.showToast({ title: '请填写体重', icon: 'none' })
+        return
+      }
+      var data = {
+        startWeight: parseFloat(this.goalForm.startWeight),
+        targetWeight: parseFloat(this.goalForm.targetWeight),
+        targetBodyFat: this.goalForm.targetBodyFat ? parseFloat(this.goalForm.targetBodyFat) : null,
+        targetDate: this.goalForm.targetDate || null,
+        startDate: today()
+      }
+      var promise = this.goal
+        ? createGoal(data)  // 简化:实现上应改为 update
+        : createGoal(data)
+      promise.then(function() {
+        uni.showToast({ title: '目标已保存', icon: 'success' })
+        self.showGoalForm = false
+        self.loadData()
+      }).catch(function() {})
+    },
+    completeGoal() {
+      var self = this
+      if (this.goal) {
+        completeGoal(this.goal.id).then(function() {
+          uni.showToast({ title: '恭喜达成目标!', icon: 'success' })
+          self.loadData()
+        }).catch(function() {})
+      }
+    },
+    abandonGoal() {
+      var self = this
+      if (this.goal) {
+        uni.showModal({
+          title: '放弃目标',
+          content: '确定要放弃当前的减脂目标吗?',
+          success: function(res) {
+            if (res.confirm) {
+              abandonGoal(self.goal.id).then(function() {
+                uni.showToast({ title: '已放弃', icon: 'none' })
+                self.loadData()
+              }).catch(function() {})
+            }
+          }
+        })
+      }
+    },
+    logout() {
+      var self = this
+      uni.showModal({
+        title: '退出登录',
+        content: '确定退出当前账号吗?',
+        success: function(res) {
+          if (res.confirm) {
+            uni.removeStorageSync('token')
+            uni.removeStorageSync('openid')
+            uni.removeStorageSync('userId')
+            uni.removeStorageSync('userInfo')
+            self.$store.commit('logout')
+            uni.reLaunch({ url: '/pages/index/index' })
+          }
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page {
+  min-height: 100vh;
+  padding: 30rpx;
+  box-sizing: border-box;
+}
+.user-card {
+  display: flex;
+  align-items: center;
+  background: linear-gradient(135deg, #FF8C42, #F97316);
+  border-radius: 16rpx;
+  padding: 40rpx 30rpx;
+  margin-bottom: 30rpx;
+  color: #fff;
+}
+.avatar {
+  width: 100rpx;
+  height: 100rpx;
+  border-radius: 50%;
+  background: rgba(255, 255, 255, 0.3);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 44rpx;
+  margin-right: 24rpx;
+}
+.user-info {
+  display: flex;
+  flex-direction: column;
+}
+.user-name {
+  font-size: 36rpx;
+  font-weight: bold;
+}
+.user-days {
+  font-size: 24rpx;
+  opacity: 0.8;
+  margin-top: 8rpx;
+}
+.section {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 30rpx;
+  margin-bottom: 30rpx;
+}
+.section-title {
+  display: block;
+  font-size: 30rpx;
+  font-weight: bold;
+  margin-bottom: 20rpx;
+}
+.goal-row {
+  display: flex;
+  justify-content: space-between;
+  padding: 14rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.goal-label {
+  font-size: 26rpx;
+  color: #666;
+}
+.goal-value {
+  font-size: 26rpx;
+  font-weight: bold;
+}
+.goal-actions {
+  display: flex;
+  margin-top: 20rpx;
+}
+.btn-sm {
+  font-size: 24rpx;
+  border-radius: 32rpx;
+  padding: 0 30rpx;
+  height: 56rpx;
+  line-height: 56rpx;
+  margin-right: 20rpx;
+}
+.btn-complete {
+  background: #10B981;
+  color: #fff;
+}
+.btn-abandon {
+  background: #fff;
+  color: #666;
+  border: 1rpx solid #ddd;
+}
+.goal-empty {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 30rpx 0;
+  color: #999;
+  font-size: 26rpx;
+}
+.btn-small {
+  margin-top: 20rpx;
+  background: #F97316;
+  color: #fff;
+  border-radius: 32rpx;
+  font-size: 26rpx;
+  padding: 0 40rpx;
+  height: 60rpx;
+  line-height: 60rpx;
+}
+.modal-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 100;
+}
+.modal-content {
+  width: 80%;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 40rpx 30rpx;
+}
+.modal-title {
+  display: block;
+  font-size: 32rpx;
+  font-weight: bold;
+  text-align: center;
+  margin-bottom: 30rpx;
+}
+.form-item {
+  margin-bottom: 24rpx;
+}
+.label {
+  display: block;
+  font-size: 26rpx;
+  color: #333;
+  margin-bottom: 10rpx;
+}
+.input {
+  border: 1rpx solid #e5e5e5;
+  border-radius: 8rpx;
+  padding: 14rpx 18rpx;
+  font-size: 26rpx;
+}
+.picker-value {
+  border: 1rpx solid #e5e5e5;
+  border-radius: 8rpx;
+  padding: 14rpx 18rpx;
+  font-size: 26rpx;
+  color: #333;
+}
+.modal-actions {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 30rpx;
+}
+.btn-cancel {
+  background: #fff;
+  color: #666;
+  border: 1rpx solid #ddd;
+}
+.btn-save {
+  background: #F97316;
+  color: #fff;
+}
+.setting-item {
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.setting-text {
+  font-size: 28rpx;
+  color: #ff4d4f;
+}
+</style>
+```
+
+- [ ] **步骤 2:校验 + Commit**
+
+```bash
+git add xiaofat-miniprogram/pages/profile/
+git commit -m "feat(fatloss-miniprogram): add profile and goal page"
+```
+
+---
+
+### 任务 14:端到端验证
+
+- [ ] **步骤 1:后端编译**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 2:前端语法校验**
+
+运行:`find xiaofat-miniprogram -name "*.js" | xargs -I{} node --check {}`
+预期:所有文件无语法错误
+
+- [ ] **步骤 3:API 路由重复检查**
+
+运行:`grep -rn '@Mapping' cfc-backend/src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u | grep fatloss`
+预期:`/api/fatloss/weight/list` 等 14 个路由,每个唯一
+
+- [ ] **步骤 4:检查 API_REFERENCE.md 是否需要同步**
+
+运行:`grep -c "fatloss" docs/superpowers/api/API_REFERENCE.md`
+预期:0(需要追加新接口文档)
+
+- [ ] **步骤 5:在 API_REFERENCE.md 追加减脂专题接口文档**
+
+在 `docs/superpowers/api/API_REFERENCE.md` 末尾追加:
+
+```markdown
+## 减脂专题(/api/fatloss/*)
+
+| 模块 | 端点 | 说明 |
+|------|------|------|
+| 体重 | `POST /api/fatloss/weight/list` | 体重记录列表(memberId/startDate/endDate) |
+| 体重 | `POST /api/fatloss/weight/create` | 创建体重记录 |
+| 体重 | `POST /api/fatloss/weight/update` | 更新体重记录 |
+| 体重 | `POST /api/fatloss/weight/delete` | 删除体重记录 |
+| 饮食 | `POST /api/fatloss/diet/list` | 饮食记录列表 |
+| 饮食 | `POST /api/fatloss/diet/create` | 创建饮食记录 |
+| 饮食 | `POST /api/fatloss/diet/delete` | 删除饮食记录 |
+| 饮食 | `POST /api/fatloss/diet/calories-summary` | 当日总热量 |
+| 运动 | `POST /api/fatloss/exercise/list` | 运动记录列表 |
+| 运动 | `POST /api/fatloss/exercise/create` | 创建运动记录 |
+| 运动 | `POST /api/fatloss/exercise/delete` | 删除运动记录 |
+| 目标 | `POST /api/fatloss/goal/active` | 获取当前目标 |
+| 目标 | `POST /api/fatloss/goal/create` | 创建目标 |
+| 目标 | `POST /api/fatloss/goal/update` | 更新目标 |
+| 目标 | `POST /api/fatloss/goal/complete` | 完成目标 |
+| 目标 | `POST /api/fatloss/goal/abandon` | 放弃目标 |
+```
+
+- [ ] **步骤 6:最终 Commit**
+
+```bash
+git add docs/superpowers/api/API_REFERENCE.md
+git commit -m "docs(api): add fatloss API reference"
+```
+
+---
+
+## 后续专题扩展(需求确认后再启用)
+
+> 以下专题在本计划(减脂 MVP)完成后进入规划,复用减脂专题已验证的基础设施
+> (登录流程、请求封装、AI 接入、数据表模式)。
+
+### 睡眠专题(Phase 2)
+- 复用主系统 `HealthSleepRecord` 表(睡眠打卡)
+- 新增:睡眠周报分析、助眠建议、作息计划表
+- AI:LangGraph 睡眠分析 graph
+
+### 心理健康专题(Phase 2)
+- 复用主系统冥想、情绪打卡模块
+- 新增:情绪日记、减压课程、量表测评
+- AI:LangGraph 情绪分析 graph
+
+### 家庭教育专题(Phase 3)
+- 复用主系统任务、心愿、成长档案能力
+- 新增:亲子活动推荐、成长里程碑、
+- AI:LangGraph 教育建议 graph
+
+### 专题 5(Phase 3)
+- 待与用户确认具体方向