Browse Source

feat(diet): 生成食谱使用所选食材+共餐参与者,归一化LangGraph菜单

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
E2E Test Bot 2 months ago
parent
commit
0c78b912e1

+ 193 - 16
cfc-backend/src/main/java/com/etotem/cfc/service/DietRecommendationService.java

@@ -2,7 +2,11 @@ package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.entity.DietRecommendation;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.entity.MealConfig;
 import com.etotem.cfc.mapper.DietRecommendationMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.mapper.MealConfigMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
@@ -24,6 +28,12 @@ public class DietRecommendationService {
     @Resource
     private AiGateway aiGateway;
 
+    @Resource
+    private MealConfigMapper mealConfigMapper;
+
+    @Resource
+    private FamilyMemberMapper familyMemberMapper;
+
     public Map<String, Object> getTodayRecommendation(Long familyId, String dateStr) {
         LocalDate date = LocalDate.parse(dateStr);
         
@@ -57,36 +67,53 @@ public class DietRecommendationService {
         String dateStr = request.get("date");
         String mealType = request.get("meal_type");
         LocalDate date = LocalDate.parse(dateStr);
-        
-        // 查询共餐配置
-        List<Long> participantIds = new ArrayList<>();
-        // 这里需要从 meal_configs 表查询,简化处理
-        
-        // 生成食材推荐
-        List<com.etotem.cfc.dto.IngredientRecommendation> ingredients = 
-            beijingNutritionService.generateIngredientList(familyId, date);
-        
-        // 调用 AI 生成菜单
-        String selectedFoodsJson = "[]";
+
+        // 1. 解析用户所选食材(前端传 selected_foods: "[{\"food_id\":1,\"name\":\"西红柿\"}]")
+        String selectedFoodsJson = request.get("selected_foods");
+        if (selectedFoodsJson == null || selectedFoodsJson.trim().isEmpty()) {
+            selectedFoodsJson = "[]";
+        }
+
+        // 2. 查询共餐参与者
+        List<Long> participantIds = queryParticipantIds(familyId, date);
+
+        // 3. 调用 AI 生成菜单(参与者数量至少 1)
         String participantsJson = "[]";
+        if (!participantIds.isEmpty()) {
+            try {
+                participantsJson = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(participantIds);
+            } catch (Exception ignore) {}
+        }
         String menuJson = aiGateway.generateMenu(selectedFoodsJson, participantsJson, dateStr);
-        
-        // 保存推荐
+
+        // 4. AI 失败时降级:用推荐食材拼一个最小菜单,避免空壳
+        if (menuJson == null || menuJson.trim().isEmpty() || menuJson.trim().equals("{}")) {
+            menuJson = buildFallbackMenu(familyId, date, participantIds.size());
+        }
+
+        // 5. 归一化 LangGraph 菜单结构 → 前端期望结构 + 聚合营养
+        Map<String, Object> normalized = normalizeMenu(menuJson, participantIds.size());
+        String normalizedMenuJson = safeWrite(normalized);
+        Map<String, Object> nutritionSummary = aggregateNutrition(normalized);
+
+        // 6. 保存推荐
         DietRecommendation record = new DietRecommendation();
         record.setFamilyId(familyId);
         record.setRecommendationDate(java.util.Date.from(date.atStartOfDay(java.time.ZoneId.systemDefault()).toInstant()));
         record.setMealType(mealType != null ? mealType : "all");
-        record.setParticipantMemberIds(new ArrayList<>(participantIds).toString());
-        record.setMenuJson(menuJson != null ? menuJson : "{}");
+        record.setParticipantMemberIds(participantIds.toString());
+        record.setMenuJson(normalizedMenuJson != null ? normalizedMenuJson : "{}");
+        record.setNutritionSummary(safeWrite(nutritionSummary));
         record.setStatus("pending");
         record.setVersion(1);
         record.setCreatedAt(new Date());
         record.setUpdatedAt(new Date());
         dietRecommendationMapper.insert(record);
-        
+
         Map<String, Object> result = new HashMap<>();
         result.put("id", record.getId());
         result.put("menu", record.getMenuJson());
+        result.put("nutritionSummary", record.getNutritionSummary());
         result.put("message", "食谱推荐已生成");
         return result;
     }
@@ -154,4 +181,154 @@ public class DietRecommendationService {
             dietRecommendationMapper.updateById(record);
         }
     }
+
+    // ============================================================
+    // 内部辅助
+    // ============================================================
+
+    /** 查询共餐参与者(按工作日/周末的 meal_configs,未配置则降级为全部家庭成员) */
+    private List<Long> queryParticipantIds(Long familyId, LocalDate date) {
+        boolean weekend = date.getDayOfWeek() == java.time.DayOfWeek.SATURDAY
+                || date.getDayOfWeek() == java.time.DayOfWeek.SUNDAY;
+        String dateType = weekend ? "weekend" : "weekday";
+        Set<Long> ids = new HashSet<>();
+        for (String mealType : Arrays.asList("breakfast", "lunch", "dinner")) {
+            LambdaQueryWrapper<MealConfig> w = new LambdaQueryWrapper<>();
+            w.eq(MealConfig::getFamilyId, familyId)
+             .eq(MealConfig::getConfigDateType, dateType)
+             .eq(MealConfig::getMealType, mealType);
+            MealConfig config = mealConfigMapper.selectOne(w);
+            if (config != null && config.getParticipantMemberIds() != null) {
+                try {
+                    com.fasterxml.jackson.databind.JsonNode nodes = new com.fasterxml.jackson.databind.ObjectMapper()
+                            .readTree(config.getParticipantMemberIds());
+                    if (nodes.isArray()) {
+                        for (com.fasterxml.jackson.databind.JsonNode node : nodes) ids.add(node.asLong());
+                    }
+                } catch (Exception ignore) {}
+            }
+        }
+        if (ids.isEmpty()) {
+            LambdaQueryWrapper<FamilyMember> w = new LambdaQueryWrapper<>();
+            w.eq(FamilyMember::getFamilyId, familyId);
+            List<FamilyMember> members = familyMemberMapper.selectList(w);
+            for (FamilyMember m : members) ids.add(m.getId());
+        }
+        return new ArrayList<>(ids);
+    }
+
+    /** 归一化 LangGraph 菜单 → 前端期望结构(dish.calories / ingredients[].amount / method) */
+    private Map<String, Object> normalizeMenu(String menuJson, int participantCount) {
+        Map<String, Object> result = new LinkedHashMap<>();
+        List<Map<String, Object>> meals = new ArrayList<>();
+        result.put("meals", meals);
+        if (menuJson == null || menuJson.trim().isEmpty()) return result;
+        try {
+            com.fasterxml.jackson.databind.ObjectMapper om = new com.fasterxml.jackson.databind.ObjectMapper();
+            com.fasterxml.jackson.databind.JsonNode root = om.readTree(menuJson);
+            com.fasterxml.jackson.databind.JsonNode mealsNode = root.get("meals");
+            if (mealsNode != null && mealsNode.isArray()) {
+                for (com.fasterxml.jackson.databind.JsonNode mealNode : mealsNode) {
+                    Map<String, Object> meal = new LinkedHashMap<>();
+                    meal.put("type", mealNode.has("type") ? mealNode.get("type").asText() : "lunch");
+                    meal.put("name", mealNode.has("name") ? mealNode.get("name").asText() : "");
+                    meal.put("participants", participantCount > 0 ? participantCount : 1);
+                    List<Map<String, Object>> dishes = new ArrayList<>();
+                    com.fasterxml.jackson.databind.JsonNode dishesNode = mealNode.get("dishes");
+                    if (dishesNode != null && dishesNode.isArray()) {
+                        for (com.fasterxml.jackson.databind.JsonNode dishNode : dishesNode) {
+                            Map<String, Object> dish = new LinkedHashMap<>();
+                            dish.put("name", dishNode.has("name") ? dishNode.get("name").asText() : "");
+                            com.fasterxml.jackson.databind.JsonNode nutrition = dishNode.get("nutrition");
+                            dish.put("calories", nutrition != null && nutrition.has("calories") ? nutrition.get("calories").asInt() : 0);
+                            List<Map<String, Object>> ingredients = new ArrayList<>();
+                            com.fasterxml.jackson.databind.JsonNode ingNode = dishNode.get("ingredients");
+                            if (ingNode != null && ingNode.isArray()) {
+                                for (com.fasterxml.jackson.databind.JsonNode ing : ingNode) {
+                                    Map<String, Object> item = new LinkedHashMap<>();
+                                    item.put("name", ing.has("name") ? ing.get("name").asText() : "");
+                                    item.put("amount", ing.has("grams") ? ing.get("grams").asInt() : 0);
+                                    ingredients.add(item);
+                                }
+                            }
+                            dish.put("ingredients", ingredients);
+                            dish.put("method", dishNode.has("cooking_method") ? dishNode.get("cooking_method").asText() : "");
+                            dishes.add(dish);
+                        }
+                    }
+                    meal.put("dishes", dishes);
+                    meals.add(meal);
+                }
+            }
+        } catch (Exception e) {
+            log.warn("归一化菜单失败: {}", e.getMessage());
+        }
+        return result;
+    }
+
+    /** 聚合营养汇总(目前聚合热量,其余字段置 0,供前端 4 宫格渲染) */
+    private Map<String, Object> aggregateNutrition(Map<String, Object> normalized) {
+        Map<String, Object> summary = new LinkedHashMap<>();
+        int calories = 0;
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> meals = (List<Map<String, Object>>) normalized.get("meals");
+        if (meals != null) {
+            for (Map<String, Object> meal : meals) {
+                @SuppressWarnings("unchecked")
+                List<Map<String, Object>> dishes = (List<Map<String, Object>>) meal.get("dishes");
+                if (dishes == null) continue;
+                for (Map<String, Object> dish : dishes) {
+                    Object cal = dish.get("calories");
+                    if (cal instanceof Number) calories += ((Number) cal).intValue();
+                }
+            }
+        }
+        summary.put("calories", calories);
+        summary.put("protein", 0);
+        summary.put("carbs", 0);
+        summary.put("fat", 0);
+        return summary;
+    }
+
+    /** AI 不可用时的规则降级:用推荐食材拼出三餐占位菜单 */
+    private String buildFallbackMenu(Long familyId, LocalDate date, int participantCount) {
+        List<com.etotem.cfc.dto.IngredientRecommendation> ingredients =
+                beijingNutritionService.generateIngredientList(familyId, date);
+        String[] mealTypes = {"breakfast", "lunch", "dinner"};
+        String[] mealNames = {"早餐", "午餐", "晚餐"};
+        Map<String, Object> root = new LinkedHashMap<>();
+        List<Map<String, Object>> meals = new ArrayList<>();
+        for (int i = 0; i < mealTypes.length; i++) {
+            Map<String, Object> meal = new LinkedHashMap<>();
+            meal.put("type", mealTypes[i]);
+            meal.put("name", mealNames[i]);
+            meal.put("participants", participantCount > 0 ? participantCount : 1);
+            List<Map<String, Object>> dishes = new ArrayList<>();
+            Map<String, Object> dish = new LinkedHashMap<>();
+            dish.put("name", "营养餐");
+            dish.put("calories", 0);
+            List<Map<String, Object>> dishIngredients = new ArrayList<>();
+            for (int j = i * 3; j < Math.min(i * 3 + 3, ingredients.size()); j++) {
+                Map<String, Object> item = new LinkedHashMap<>();
+                item.put("name", ingredients.get(j).getName());
+                item.put("amount", 100);
+                dishIngredients.add(item);
+            }
+            dish.put("ingredients", dishIngredients);
+            dish.put("method", "建议以清淡为主");
+            dishes.add(dish);
+            meal.put("dishes", dishes);
+            meals.add(meal);
+        }
+        root.put("meals", meals);
+        return safeWrite(root);
+    }
+
+    private String safeWrite(Object obj) {
+        try {
+            return new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(obj);
+        } catch (Exception e) {
+            return "{}";
+        }
+    }
 }