|
|
@@ -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 "{}";
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|