# 减脂专题小程序 实现计划 > **面向 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` - [x] **步骤 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='减脂-目标'; ``` - [x] **步骤 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) { // 表已存在,忽略错误 } ``` - [x] **步骤 3:编译验证**** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [x] **步骤 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` - [x] **步骤 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; } ``` - [x] **步骤 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 { } ``` ```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 { } ``` ```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 { } ``` ```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 { } ``` - [x] **步骤 3:编译验证**** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [x] **步骤 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 list(Long memberId, Date startDate, Date endDate) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .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 list(Long memberId, Date startDate, Date endDate) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .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 records = fatDietRecordMapper.selectList( new LambdaQueryWrapper() .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 list(Long memberId, Date startDate, Date endDate) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .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() .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(@RequestBody(required = false) Map 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 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 update(@RequestBody FatWeightRecord record) { if (record.getId() == null) return Result.error("id不能为空"); return Result.success(fatWeightService.update(record)); } @Operation(summary = "删除体重记录") @PostMapping("/delete") public Result delete(@RequestBody Map 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(@RequestBody(required = false) Map 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 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 delete(@RequestBody Map 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 caloriesSummary(@RequestBody(required = false) Map 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(@RequestBody(required = false) Map 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 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 delete(@RequestBody Map 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 active(@RequestBody(required = false) Map 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 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 update(@RequestBody FatGoal goal) { if (goal.getId() == null) return Result.error("id不能为空"); return Result.success(fatGoalService.update(goal)); } @Operation(summary = "完成减脂目标") @PostMapping("/complete") public Result complete(@RequestBody Map 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 abandon(@RequestBody Map 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/**` - [x] **步骤 1:确认 JWT 拦截覆盖新接口**** 运行:`grep -n "includePathPatterns\|addPathPatterns" cfc-backend/src/main/java/com/etotem/cfc/config/*Interceptor*.java` 预期:`addPathPatterns("/api/**")` 已存在,新接口自动受保护(`/api/fatloss/*` 需要登录) - [x] **步骤 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 ``` - [ ] **步骤 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 ``` - [ ] **步骤 2:校验 Vue 文件脚本块语法** 运行:`node -e "const fs=require('fs');const s=fs.readFileSync('xiaofat-miniprogram/pages/index/index.vue','utf8');const m=s.match(/ ``` - [ ] **步骤 2:创建 weight/create.vue** ```vue ``` - [ ] **步骤 3:创建 weight/chart.vue(趋势图 - 使用 Canvas)** ```vue ``` - [ ] **步骤 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 ``` - [ ] **步骤 2:创建 diet/create.vue** ```vue ``` - [ ] **步骤 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 ``` - [ ] **步骤 2:创建 exercise/create.vue** ```vue ``` - [ ] **步骤 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 ``` - [ ] **步骤 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 ``` - [ ] **步骤 2:校验 + Commit** ```bash git add xiaofat-miniprogram/pages/profile/ git commit -m "feat(fatloss-miniprogram): add profile and goal page" ``` --- ### 任务 14:端到端验证 - [x] **步骤 1:后端编译**** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [x] **步骤 2:前端语法校验**** 运行:`find xiaofat-miniprogram -name "*.js" | xargs -I{} node --check {}` 预期:所有文件无语法错误 - [x] **步骤 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 个路由,每个唯一 - [x] **步骤 4:检查 API_REFERENCE.md 是否需要同步**** 运行:`grep -c "fatloss" docs/superpowers/api/API_REFERENCE.md` 预期:0(需要追加新接口文档) - [x] **步骤 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` | 放弃目标 | ``` - [x] **步骤 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) - 待与用户确认具体方向