面向 AI 代理的工作者: 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(
- [ ])语法来跟踪进度。
目标: 构建饮食模块后端 API,包括调研表、食材推荐、食谱生成、饮食记录、共餐配置等核心功能。
架构: 基于现有菌群报告体系和家庭体系扩展,新增 6 张表 + 5 个 Controller + 4 个 Service。AI 识别和菜单生成复用 AiGateway(LangGraph 接入)。
技术栈: Spring Boot 2.7.18 + MyBatis-Plus + Java 8 + LangGraph Python 服务
src/main/java/com/etotem/cfc/entity/DietPreferences.javasrc/main/java/com/etotem/cfc/entity/MealConfig.javasrc/main/java/com/etotem/cfc/entity/DietRecommendation.javasrc/main/java/com/etotem/cfc/entity/DietRecord.javasrc/main/java/com/etotem/cfc/entity/DietRecordItem.javasrc/main/java/com/etotem/cfc/dto/DietPreferencesDTO.javasrc/main/java/com/etotem/cfc/dto/IngredientRecommendation.javasrc/main/java/com/etotem/cfc/dto/DietRecordSaveRequest.javasrc/main/java/com/etotem/cfc/dto/RecognizeFoodResult.javasrc/main/java/com/etotem/cfc/mapper/DietPreferencesMapper.javasrc/main/java/com/etotem/cfc/mapper/MealConfigMapper.javasrc/main/java/com/etotem/cfc/mapper/DietRecommendationMapper.javasrc/main/java/com/etotem/cfc/mapper/DietRecordMapper.javasrc/main/java/com/etotem/cfc/mapper/DietRecordItemMapper.javasrc/main/java/com/etotem/cfc/service/DietPreferencesService.javasrc/main/java/com/etotem/cfc/service/DietIngredientService.javasrc/main/java/com/etotem/cfc/service/DietRecommendationService.javasrc/main/java/com/etotem/cfc/service/DietRecordService.javasrc/main/java/com/etotem/cfc/service/DietMealConfigService.javasrc/main/java/com/etotem/cfc/controller/diet/DietPreferencesController.javasrc/main/java/com/etotem/cfc/controller/diet/DietIngredientController.javasrc/main/java/com/etotem/cfc/controller/diet/DietRecommendationController.javasrc/main/java/com/etotem/cfc/controller/diet/DietRecordController.javasrc/main/java/com/etotem/cfc/controller/diet/DietMealConfigController.javasrc/main/java/com/etotem/cfc/entity/FamilyMember.java(扩展 7 字段)src/main/java/com/etotem/cfc/config/DatabaseInitializer.java(新增迁移)src/main/resources/schema.sql(新增表定义 + 扩展 family_members)src/main/java/com/etotem/cfc/service/BeijingNutritionService.java(新增方法)src/main/java/com/etotem/cfc/service/AiGateway.java(新增方法)文件:
src/main/java/com/etotem/cfc/config/DatabaseInitializer.java修改:src/main/resources/schema.sql
[ ] 步骤 1:扩展 family_members 表实体字段
在 FamilyMember.java 末尾添加 7 个字段:
// 饮食模块扩展字段
private Integer isLivingTogether;
private Integer mealWithBreakfastWeekday;
private Integer mealWithLunchWeekday;
private Integer mealWithDinnerWeekday;
private Integer mealWithBreakfastWeekend;
private Integer mealWithLunchWeekend;
private Integer mealWithDinnerWeekend;
在 runMigrations() 方法末尾(迁移 169 之后)添加:
// 迁移174: family_members 添加饮食相关字段(饮食模块需求)
try {
jdbcTemplate.execute("ALTER TABLE family_members ADD COLUMN is_living_together TINYINT(1) DEFAULT 1 COMMENT '是否同住(0=不同住,不出现在食谱推荐中)'");
log.info("已添加is_living_together列到family_members表");
} catch (Exception e) {
log.warn("family_members 添加 is_living_together 列失败(可能已存在): {}", e.getMessage());
}
try {
jdbcTemplate.execute("ALTER TABLE family_members ADD COLUMN meal_with_breakfast_weekday TINYINT(1) DEFAULT 0 COMMENT '工作日早餐同餐'");
log.info("已添加meal_with_breakfast_weekday列到family_members表");
} catch (Exception e) {
log.warn("family_members 添加 meal_with_breakfast_weekday 列失败(可能已存在): {}", e.getMessage());
}
// ... 其他 5 个字段类似 ...
[ ] 步骤 3:在 DatabaseInitializer 创建新表
// 迁移175: 创建 diet_preferences 表(饮食偏好调研表)
try {
jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS diet_preferences (" +
"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
"family_member_id BIGINT NOT NULL COMMENT '绑定家庭成员', " +
"allergies JSON COMMENT '过敏原列表', " +
"absolute_avoid JSON COMMENT '绝对忌口列表', " +
"religious_diet VARCHAR(50) DEFAULT 'none' COMMENT '宗教饮食', " +
"spice_level TINYINT DEFAULT NULL COMMENT '辣度 0-5', " +
"flavor_pref JSON COMMENT '口味偏好', " +
"cuisine_pref JSON COMMENT '菜系偏好', " +
"cooking_methods JSON COMMENT '烹饪方式偏好', " +
"health_goals JSON COMMENT '健康目标', " +
"goal_source VARCHAR(20) DEFAULT NULL COMMENT 'manual/system_recommend', " +
"goal_confirmed TINYINT(1) DEFAULT 0 COMMENT '用户已确认', " +
"filled_at TIMESTAMP NULL COMMENT '首次填写时间', " +
"updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
"UNIQUE KEY uk_member (family_member_id), " +
"INDEX idx_updated (updated_at)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='饮食偏好调研表'");
log.info("已创建diet_preferences表");
} catch (Exception e) {
log.warn("创建 diet_preferences 表失败(可能已存在): {}", e.getMessage());
}
// 迁移176: 创建 meal_configs 表(共餐配置表)
try {
jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS meal_configs (" +
"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
"family_id BIGINT NOT NULL COMMENT '家庭ID', " +
"config_date_type VARCHAR(20) NOT NULL COMMENT '工作日/周末/节假日', " +
"meal_type VARCHAR(20) NOT NULL COMMENT 'breakfast/lunch/dinner', " +
"participant_member_ids JSON NOT NULL COMMENT '参与成员ID列表', " +
"notes VARCHAR(255) DEFAULT NULL COMMENT '备注', " +
"created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
"updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
"UNIQUE KEY uk_config (family_id, config_date_type, meal_type), " +
"INDEX idx_family (family_id)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='共餐配置表'");
log.info("已创建meal_configs表");
} catch (Exception e) {
log.warn("创建 meal_configs 表失败(可能已存在): {}", e.getMessage());
}
// 迁移177: 创建 diet_recommendations 表(食谱推荐方案表)
try {
jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS diet_recommendations (" +
"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
"family_id BIGINT NOT NULL COMMENT '家庭ID', " +
"recommendation_date DATE NOT NULL COMMENT '推荐日期', " +
"meal_type VARCHAR(20) NOT NULL COMMENT 'breakfast/lunch/dinner', " +
"participant_member_ids JSON COMMENT '参与成员ID列表', " +
"menu_json MEDIUMTEXT NOT NULL COMMENT '菜品列表JSON', " +
"nutrition_summary JSON COMMENT '营养汇总', " +
"status VARCHAR(20) DEFAULT 'pending' COMMENT 'pending/accepted/adjusted/completed/skipped', " +
"version INT DEFAULT 1 COMMENT '版本号', " +
"created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
"updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
"INDEX idx_family_date (family_id, recommendation_date), " +
"INDEX idx_meal (meal_type, recommendation_date)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='食谱推荐方案表'");
log.info("已创建diet_recommendations表");
} catch (Exception e) {
log.warn("创建 diet_recommendations 表失败(可能已存在): {}", e.getMessage());
}
// 迁移178: 创建 diet_records 表(饮食记录表)
try {
jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS diet_records (" +
"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
"family_id BIGINT NOT NULL COMMENT '家庭ID', " +
"member_id BIGINT NOT NULL COMMENT '吃的成员ID', " +
"meal_type VARCHAR(20) NOT NULL COMMENT 'breakfast/lunch/dinner/snack', " +
"record_date DATE NOT NULL COMMENT '记录日期', " +
"record_method VARCHAR(20) DEFAULT NULL COMMENT 'photo/manual/from_recommendation', " +
"image_url VARCHAR(500) DEFAULT NULL COMMENT '原图OSS路径', " +
"ai_recognized_foods JSON DEFAULT NULL COMMENT 'AI识别结果', " +
"user_confirmed_foods JSON NOT NULL COMMENT '用户最终确认食材', " +
"notes VARCHAR(500) DEFAULT NULL COMMENT '备注', " +
"created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
"INDEX idx_member_date (member_id, record_date), " +
"INDEX idx_family_date (family_id, record_date)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='饮食记录表'");
log.info("已创建diet_records表");
} catch (Exception e) {
log.warn("创建 diet_records 表失败(可能已存在): {}", e.getMessage());
}
// 迁移179: 创建 diet_record_items 表(饮食记录食材明细表)
try {
jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS diet_record_items (" +
"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
"record_id BIGINT NOT NULL COMMENT 'diet_records.id', " +
"food_name VARCHAR(100) NOT NULL COMMENT '食材名称', " +
"food_id BIGINT DEFAULT NULL COMMENT '关联foods表ID', " +
"confidence DECIMAL(3,2) DEFAULT NULL COMMENT 'AI置信度0-1', " +
"source VARCHAR(20) DEFAULT NULL COMMENT 'ai_recognized/manual/from_recommendation', " +
"INDEX idx_record (record_id)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='饮食记录食材明细表'");
log.info("已创建diet_record_items表");
} catch (Exception e) {
log.warn("创建 diet_record_items 表失败(可能已存在): {}", e.getMessage());
}
[ ] 步骤 4:同步 schema.sql
在 schema.sql 末尾追加 6 个表的 CREATE TABLE 语句(参考规格文档 3.2-3.7 节)。
在 schema.sql 的 family_members 表定义中追加 7 个新字段。
[ ] 步骤 5:编译验证
cd /sc-data/cfc/cfc-backend
mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 6:Commit
git add src/main/java/com/etotem/cfc/entity/FamilyMember.java
git add src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
git add src/main/resources/schema.sql
git commit -m "feat: 饮食模块数据模型迁移(6 张新表 + family_members 扩展)"
文件:
src/main/java/com/etotem/cfc/entity/DietPreferences.javasrc/main/java/com/etotem/cfc/entity/MealConfig.javasrc/main/java/com/etotem/cfc/entity/DietRecommendation.javasrc/main/java/com/etotem/cfc/entity/DietRecord.javasrc/main/java/com/etotem/cfc/entity/DietRecordItem.javasrc/main/java/com/etotem/cfc/mapper/DietPreferencesMapper.javasrc/main/java/com/etotem/cfc/mapper/MealConfigMapper.javasrc/main/java/com/etotem/cfc/mapper/DietRecommendationMapper.javasrc/main/java/com/etotem/cfc/mapper/DietRecordMapper.java创建:src/main/java/com/etotem/cfc/mapper/DietRecordItemMapper.java
[ ] 步骤 1:创建 DietPreferences 实体
package com.etotem.cfc.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
@TableName("diet_preferences")
public class DietPreferences implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
private Long familyMemberId;
private String allergies;
private String absoluteAvoid;
private String religiousDiet;
private Integer spiceLevel;
private String flavorPref;
private String cuisinePref;
private String cookingMethods;
private String healthGoals;
private String goalSource;
private Integer goalConfirmed;
private Date filledAt;
private Date updatedAt;
}
[ ] 步骤 2:创建 MealConfig 实体
package com.etotem.cfc.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
@TableName("meal_configs")
public class MealConfig implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
private Long familyId;
private String configDateType;
private String mealType;
private String participantMemberIds;
private String notes;
private Date createdAt;
private Date updatedAt;
}
[ ] 步骤 3-5:创建其他实体类(结构类似,参考规格文档 3.4-3.7 节字段定义)
[ ] 步骤 6:创建 Mapper 接口
package com.etotem.cfc.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.etotem.cfc.entity.DietPreferences;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DietPreferencesMapper extends BaseMapper<DietPreferences> {
}
其他 Mapper 类似(MealConfigMapper、DietRecommendationMapper、DietRecordMapper、DietRecordItemMapper)。
[ ] 步骤 7:编译验证
mvn clean compile
[ ] 步骤 8:Commit
git add src/main/java/com/etotem/cfc/entity/
git add src/main/java/com/etotem/cfc/mapper/
git commit -m "feat: 饮食模块实体类和 Mapper 接口"
文件:
src/main/java/com/etotem/cfc/service/DietPreferencesService.java创建:src/main/java/com/etotem/cfc/dto/DietPreferencesDTO.java
[ ] 步骤 1:创建 DTO 类
package com.etotem.cfc.dto;
import lombok.Data;
import java.util.List;
@Data
public class DietPreferencesDTO {
private Long familyMemberId;
private List<String> allergies;
private List<String> absoluteAvoid;
private String religiousDiet;
private Integer spiceLevel;
private List<String> flavorPref;
private List<String> cuisinePref;
private List<String> cookingMethods;
private List<String> healthGoals;
private String goalSource;
private Integer goalConfirmed;
private String filledAt;
}
[ ] 步骤 2:实现 DietPreferencesService
package com.etotem.cfc.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.etotem.cfc.dto.DietPreferencesDTO;
import com.etotem.cfc.entity.DietPreferences;
import com.etotem.cfc.mapper.DietPreferencesMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Service
public class DietPreferencesService {
@Resource
private DietPreferencesMapper dietPreferencesMapper;
@Resource
private ObjectMapper objectMapper;
/**
* 查询当前成员的调研表
*/
public DietPreferencesDTO getCurrentMemberPreferences(Long familyMemberId) {
LambdaQueryWrapper<DietPreferences> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DietPreferences::getFamilyMemberId, familyMemberId);
DietPreferences entity = dietPreferencesMapper.selectOne(wrapper);
if (entity == null) {
return null;
}
return convertToDTO(entity);
}
/**
* 保存/更新调研表
*/
public void savePreferences(Long familyMemberId, DietPreferencesDTO dto) {
LambdaQueryWrapper<DietPreferences> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DietPreferences::getFamilyMemberId, familyMemberId);
DietPreferences entity = dietPreferencesMapper.selectOne(wrapper);
if (entity == null) {
entity = new DietPreferences();
entity.setFamilyMemberId(familyMemberId);
entity.setFilledAt(new Date());
}
entity.setAllergies(dto.getAllergies() != null ? toJSON(dto.getAllergies()) : null);
entity.setAbsoluteAvoid(dto.getAbsoluteAvoid() != null ? toJSON(dto.getAbsoluteAvoid()) : null);
entity.setReligiousDiet(dto.getReligiousDiet() != null ? dto.getReligiousDiet() : "none");
entity.setSpiceLevel(dto.getSpiceLevel());
entity.setFlavorPref(dto.getFlavorPref() != null ? toJSON(dto.getFlavorPref()) : null);
entity.setCuisinePref(dto.getCuisinePref() != null ? toJSON(dto.getCuisinePref()) : null);
entity.setCookingMethods(dto.getCookingMethods() != null ? toJSON(dto.getCookingMethods()) : null);
entity.setHealthGoals(dto.getHealthGoals() != null ? toJSON(dto.getHealthGoals()) : null);
entity.setGoalSource(dto.getGoalSource());
entity.setGoalConfirmed(dto.getGoalConfirmed() != null ? dto.getGoalConfirmed() : 0);
if (entity.getId() == null) {
dietPreferencesMapper.insert(entity);
} else {
dietPreferencesMapper.updateById(entity);
}
}
private String toJSON(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (Exception e) {
log.warn("JSON 序列化失败: {}", e.getMessage());
return null;
}
}
private DietPreferencesDTO convertToDTO(DietPreferences entity) {
DietPreferencesDTO dto = new DietPreferencesDTO();
dto.setFamilyMemberId(entity.getFamilyMemberId());
dto.setAllergies(entity.getAllergies() != null ? fromJSON(entity.getAllergies()) : null);
dto.setAbsoluteAvoid(entity.getAbsoluteAvoid() != null ? fromJSON(entity.getAbsoluteAvoid()) : null);
dto.setReligiousDiet(entity.getReligiousDiet());
dto.setSpiceLevel(entity.getSpiceLevel());
dto.setFlavorPref(entity.getFlavorPref() != null ? fromJSON(entity.getFlavorPref()) : null);
dto.setCuisinePref(entity.getCuisinePref() != null ? fromJSON(entity.getCuisinePref()) : null);
dto.setCookingMethods(entity.getCookingMethods() != null ? fromJSON(entity.getCookingMethods()) : null);
dto.setHealthGoals(entity.getHealthGoals() != null ? fromJSON(entity.getHealthGoals()) : null);
dto.setGoalSource(entity.getGoalSource());
dto.setGoalConfirmed(entity.getGoalConfirmed());
dto.setFilledAt(entity.getFilledAt() != null ? entity.getFilledAt().toString() : null);
return dto;
}
private <T> T fromJSON(String json) {
if (json == null) return null;
try {
return objectMapper.readValue(json, objectMapper.getTypeFactory().constructType(Class.forName("java.util.List")));
} catch (Exception e) {
log.warn("JSON 反序列化失败: {}", e.getMessage());
return null;
}
}
}
[ ] 步骤 3:编译验证
mvn clean compile
[ ] 步骤 4:Commit
git add src/main/java/com/etotem/cfc/service/DietPreferencesService.java
git add src/main/java/com/etotem/cfc/dto/DietPreferencesDTO.java
git commit -m "feat: 实现 DietPreferencesService(调研表服务)"
文件:
创建:src/main/java/com/etotem/cfc/controller/diet/DietPreferencesController.java
[ ] 步骤 1:创建 Controller
package com.etotem.cfc.controller.diet;
import com.etotem.cfc.common.Result;
import com.etotem.cfc.dto.DietPreferencesDTO;
import com.etotem.cfc.service.DietPreferencesService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@RestController
@RequestMapping("/api/diet/preferences")
public class DietPreferencesController {
@Resource
private DietPreferencesService dietPreferencesService;
@PostMapping("/current-member")
public Result<DietPreferencesDTO> getCurrentMemberPreferences(
@RequestAttribute("familyMemberId") Long familyMemberId) {
DietPreferencesDTO dto = dietPreferencesService.getCurrentMemberPreferences(familyMemberId);
return Result.success(dto);
}
@PostMapping("/save")
public Result<Void> savePreferences(
@RequestAttribute("familyMemberId") Long familyMemberId,
@RequestBody DietPreferencesDTO dto) {
dto.setFamilyMemberId(familyMemberId);
dietPreferencesService.savePreferences(familyMemberId, dto);
return Result.success(null);
}
}
[ ] 步骤 2:编译验证
mvn clean compile
[ ] 步骤 3:Commit
git add src/main/java/com/etotem/cfc/controller/diet/DietPreferencesController.java
git commit -m "feat: 实现 DietPreferencesController(调研表接口)"
文件:
src/main/java/com/etotem/cfc/service/BeijingNutritionService.java创建:src/main/java/com/etotem/cfc/dto/IngredientRecommendation.java
[ ] 步骤 1:创建 IngredientRecommendation DTO
package com.etotem.cfc.dto;
import lombok.Data;
@Data
public class IngredientRecommendation {
private Long foodId;
private String name;
private String reason;
private Integer score;
private String category;
}
[ ] 步骤 2:在 BeijingNutritionService 中添加 generateIngredientList 方法
/**
* 生成食材推荐列表(供饮食首页使用)
*
* @param familyId 家庭ID
* @param date 日期
* @return 推荐食材列表(Top 15,含推荐理由)
*/
public List<IngredientRecommendation> generateIngredientList(Long familyId, LocalDate date) {
// 1. 查 meal_configs → participant_member_ids
// 2. 查每个 member 的 diet_preferences + health_gut_flora
// 3. 过滤禁忌食材(过敏 + 忌口 + 宗教)
// 4. 计算推荐分:food_recommend_idx + bacteria_food_mapping + 目标匹配
// 5. 返回 Top 15
// ... 实现细节(参考规格文档 4.2 节)
}
[ ] 步骤 3:编译验证
mvn clean compile
[ ] 步骤 4:Commit
git add src/main/java/com/etotem/cfc/service/BeijingNutritionService.java
git add src/main/java/com/etotem/cfc/dto/IngredientRecommendation.java
git commit -m "feat: 扩展 BeijingNutritionService(食材推荐方法)"
文件:
src/main/java/com/etotem/cfc/service/DietIngredientService.java创建:src/main/java/com/etotem/cfc/controller/diet/DietIngredientController.java
[ ] 步骤 1:实现 DietIngredientService
包含方法:suggestIngredients(), refreshIngredients(), addIngredient(), removeIngredient(), confirmIngredients()
[ ] 步骤 2:实现 DietIngredientController
package com.etotem.cfc.controller.diet;
import com.etotem.cfc.common.Result;
import com.etotem.cfc.service.DietIngredientService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Map;
@RestController
@RequestMapping("/api/diet/ingredients")
public class DietIngredientController {
@Resource
private DietIngredientService dietIngredientService;
@PostMapping("/suggest")
public Result<Map> suggestIngredients(@RequestAttribute("familyId") Long familyId) {
return Result.success(dietIngredientService.suggestIngredients(familyId));
}
@PostMapping("/recommend")
public Result<Map> recommendIngredients(@RequestAttribute("familyId") Long familyId) {
return Result.success(dietIngredientService.refreshIngredients(familyId));
}
@PostMapping("/confirm")
public Result<Map> confirmIngredients(
@RequestAttribute("familyId") Long familyId,
@RequestBody Map<String, Object> request) {
return Result.success(dietIngredientService.confirmIngredients(familyId, request));
}
}
[ ] 步骤 3:编译验证 + Commit
mvn clean compile
git add src/main/java/com/etotem/cfc/service/DietIngredientService.java
git add src/main/java/com/etotem/cfc/controller/diet/DietIngredientController.java
git commit -m "feat: 实现 DietIngredientService + Controller(食材推荐)"
文件:
src/main/java/com/etotem/cfc/service/AiGateway.java创建:src/main/java/com/etotem/cfc/dto/RecognizeFoodResult.java
[ ] 步骤 1:创建 RecognizeFoodResult DTO
package com.etotem.cfc.dto;
import lombok.Data;
import java.util.List;
@Data
public class RecognizeFoodResult {
private List<FoodsItem> foods;
private String rawResponse;
@Data
public static class FoodsItem {
private String name;
private Double confidence;
private String category;
}
}
[ ] 步骤 2:在 AiGateway 中添加 recognizeFood 和 generateMenu 方法
/**
* 识别图片中的食材
*/
public RecognizeFoodResult recognizeFood(String imageUrl) {
if (!enabled || isCircuitOpen()) return null;
try {
ObjectNode body = objectMapper.createObjectNode();
body.put("image_url", imageUrl);
body.put("prompt", "识别图片中的主要食材,返回 JSON 格式:[{name, confidence, category}]");
HttpEntity<String> entity = new HttpEntity<>(body.toString(), createJsonHeaders());
String url = baseUrl + "/api/v1/food/recognize";
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
JsonNode root = objectMapper.readTree(response.getBody());
RecognizeFoodResult result = new RecognizeFoodResult();
result.setRawResponse(response.getBody());
if (root.has("foods") && root.get("foods").isArray()) {
List<RecognizeFoodResult.FoodsItem> foods = new ArrayList<>();
for (JsonNode food : root.get("foods")) {
RecognizeFoodResult.FoodsItem item = new RecognizeFoodResult.FoodsItem();
item.setName(food.get("name").asText());
item.setConfidence(food.get("confidence").asDouble());
item.setCategory(food.get("category").asText());
foods.add(item);
}
result.setFoods(foods);
}
consecutiveFailures.set(0);
return result;
}
return null;
} catch (Exception e) {
log.warn("AiGateway recognizeFood 调用失败: {}", e.getMessage());
recordFailure();
return null;
}
}
/**
* 生成菜单
*/
public String generateMenu(String selectedFoodsJson, String participantsJson, String date) {
if (!enabled || isCircuitOpen()) return null;
try {
ObjectNode body = objectMapper.createObjectNode();
body.put("selected_foods", selectedFoodsJson);
body.put("participants", participantsJson);
body.put("date", date);
HttpEntity<String> entity = new HttpEntity<>(body.toString(), createJsonHeaders());
String url = baseUrl + "/api/v1/menu/generate";
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
JsonNode root = objectMapper.readTree(response.getBody());
consecutiveFailures.set(0);
return root.has("menu_json") ? root.get("menu_json").asText() : response.getBody();
}
return null;
} catch (Exception e) {
log.warn("AiGateway generateMenu 调用失败: {}", e.getMessage());
recordFailure();
return null;
}
}
[ ] 步骤 3:编译验证 + Commit
mvn clean compile
git add src/main/java/com/etotem/cfc/service/AiGateway.java
git add src/main/java/com/etotem/cfc/dto/RecognizeFoodResult.java
git commit -m "feat: 扩展 AiGateway(视觉识别 + 菜单生成)"
文件:
src/main/java/com/etotem/cfc/service/DietRecordService.java创建:src/main/java/com/etotem/cfc/controller/diet/DietRecordController.java
[ ] 步骤 1:实现 DietRecordService
包含方法:recognizeFood(), saveRecord(), getDailySummary(), getWeeklyTrend()
[ ] 步骤 2:实现 DietRecordController
package com.etotem.cfc.controller.diet;
import com.etotem.cfc.common.Result;
import com.etotem.cfc.service.DietRecordService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Map;
@RestController
@RequestMapping("/api/diet/record")
public class DietRecordController {
@Resource
private DietRecordService dietRecordService;
@PostMapping("/recognize")
public Result<Map> recognizeFood(@RequestBody Map<String, String> request) {
return Result.success(dietRecordService.recognizeFood(request.get("image_url")));
}
@PostMapping("/save")
public Result<Map> saveRecord(
@RequestAttribute("familyId") Long familyId,
@RequestBody Map<String, Object> request) {
return Result.success(dietRecordService.saveRecord(familyId, request));
}
@PostMapping("/daily")
public Result<Map> getDailySummary(
@RequestAttribute("familyMemberId") Long familyMemberId,
@RequestParam String date) {
return Result.success(dietRecordService.getDailySummary(familyMemberId, date));
}
@PostMapping("/weekly")
public Result<Map> getWeeklyTrend(
@RequestAttribute("familyMemberId") Long familyMemberId,
@RequestParam String startDate) {
return Result.success(dietRecordService.getWeeklyTrend(familyMemberId, startDate));
}
}
[ ] 步骤 3:编译验证 + Commit
mvn clean compile
git add src/main/java/com/etotem/cfc/service/DietRecordService.java
git add src/main/java/com/etotem/cfc/controller/diet/DietRecordController.java
git commit -m "feat: 实现 DietRecordService + Controller(饮食记录)"
文件:
src/main/java/com/etotem/cfc/service/DietRecommendationService.java创建:src/main/java/com/etotem/cfc/controller/diet/DietRecommendationController.java
[ ] 步骤 1:实现 DietRecommendationService
包含方法:getTodayRecommendation(), generateRecommendation(), updateRecommendation(), regenerateRecommendation(), completeRecommendation()
[ ] 步骤 2:实现 DietRecommendationController
package com.etotem.cfc.controller.diet;
import com.etotem.cfc.common.Result;
import com.etotem.cfc.service.DietRecommendationService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Map;
@RestController
@RequestMapping("/api/diet/recommendation")
public class DietRecommendationController {
@Resource
private DietRecommendationService dietRecommendationService;
@PostMapping("/today")
public Result<Map> getTodayRecommendation(
@RequestAttribute("familyId") Long familyId,
@RequestParam String date) {
return Result.success(dietRecommendationService.getTodayRecommendation(familyId, date));
}
@PostMapping("/generate")
public Result<Map> generateRecommendation(
@RequestAttribute("familyId") Long familyId,
@RequestBody Map<String, String> request) {
return Result.success(dietRecommendationService.generateRecommendation(familyId, request));
}
@PostMapping("/update")
public Result<Void> updateRecommendation(
@RequestAttribute("familyId") Long familyId,
@RequestBody Map<String, Object> request) {
dietRecommendationService.updateRecommendation(familyId, request);
return Result.success(null);
}
@PostMapping("/regenerate")
public Result<Map> regenerateRecommendation(
@RequestAttribute("familyId") Long familyId,
@RequestBody Map<String, Object> request) {
return Result.success(dietRecommendationService.regenerateRecommendation(familyId, request));
}
@PostMapping("/complete")
public Result<Void> completeRecommendation(
@RequestAttribute("familyId") Long familyId,
@RequestBody Map<String, Object> request) {
dietRecommendationService.completeRecommendation(familyId, request);
return Result.success(null);
}
}
[ ] 步骤 3:编译验证 + Commit
mvn clean compile
git add src/main/java/com/etotem/cfc/service/DietRecommendationService.java
git add src/main/java/com/etotem/cfc/controller/diet/DietRecommendationController.java
git commit -m "feat: 实现 DietRecommendationService + Controller(食谱推荐)"
文件:
src/main/java/com/etotem/cfc/service/DietMealConfigService.java创建:src/main/java/com/etotem/cfc/controller/diet/DietMealConfigController.java
[ ] 步骤 1:实现 DietMealConfigService
包含方法:getConfig(), saveConfig(), getMealSummary()
[ ] 步骤 2:实现 DietMealConfigController
package com.etotem.cfc.controller.diet;
import com.etotem.cfc.common.Result;
import com.etotem.cfc.service.DietMealConfigService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.Map;
@RestController
@RequestMapping("/api/diet/meals")
public class DietMealConfigController {
@Resource
private DietMealConfigService dietMealConfigService;
@PostMapping("/config")
public Result<Map> getConfig(
@RequestAttribute("familyId") Long familyId,
@RequestParam String dateType,
@RequestParam String mealType) {
return Result.success(dietMealConfigService.getConfig(familyId, dateType, mealType));
}
@PostMapping("/config/save")
public Result<Void> saveConfig(
@RequestAttribute("familyId") Long familyId,
@RequestBody Map<String, Object> request) {
dietMealConfigService.saveConfig(familyId, request);
return Result.success(null);
}
}
[ ] 步骤 3:编译验证 + Commit
mvn clean compile
git add src/main/java/com/etotem/cfc/service/DietMealConfigService.java
git add src/main/java/com/etotem/cfc/controller/diet/DietMealConfigController.java
git commit -m "feat: 实现 DietMealConfigService + Controller(共餐配置)"
[ ] 步骤 1:运行编译
cd /sc-data/cfc/cfc-backend
mvn clean compile
[ ] 步骤 2:检查路由冲突
grep -rn '@PostMapping' src/main/java/com/etotem/cfc/controller/diet/ | grep -oP '@PostMapping\("\K[^"]*' | sort -u
[ ] 步骤 3:启动服务测试(可选)
mvn spring-boot:run
访问 http://localhost:9082/swagger-ui.html 查看新接口
[ ] 步骤 4:最终 Commit
git add -A
git commit -m "feat: 饮食模块后端 API 完整实现"
| 规格需求 | 对应任务 | 状态 |
|---|---|---|
| 6 张新表 + family_members 扩展 | 任务 1 | ✅ |
| 实体类 + Mapper | 任务 2 | ✅ |
| 调研表 Service + Controller | 任务 3-4 | ✅ |
| 食材推荐 Service + Controller | 任务 5-6 | ✅ |
| AiGateway 扩展(视觉 + 菜单) | 任务 7 | ✅ |
| 饮食记录 Service + Controller | 任务 8 | ✅ |
| 食谱推荐 Service + Controller | 任务 9 | ✅ |
| 共餐配置 Service + Controller | 任务 10 | ✅ |
| 端到端验证 | 任务 11 | ✅ |
计划已完成。两种执行方式:
1. 子代理驱动(推荐) - 每个任务调度一个新的子代理,任务间进行审查,快速迭代
2. 内联执行 - 在当前会话中使用 executing-plans 执行任务,批量执行并设有检查点供审查
选哪种方式?