package com.chowbox.service; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.chowbox.mapper.RecipeIngredientMapper; import com.chowbox.mapper.RecipeMapper; import com.chowbox.mapper.RecipeStepMapper; import com.chowbox.model.FridgeMatchResult; import com.chowbox.model.Recipe; import com.chowbox.model.RecipeIngredient; import com.chowbox.model.RecipeStep; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.time.LocalDate; import java.time.LocalTime; import java.util.*; import java.util.stream.Collectors; @Slf4j @Service public class FridgeService { @Autowired private RecipeMapper recipeMapper; @Autowired private RecipeIngredientMapper ingredientMapper; @Autowired private RecipeStepMapper stepMapper; private static final Set DEFAULT_STAPLES = new HashSet<>(Arrays.asList( "油", "盐", "酱油", "生抽", "老抽", "醋", "料酒", "蚝油", "糖", "白糖", "淀粉", "香油", "姜", "蒜", "葱", "干辣椒", "花椒" )); private static final Random RANDOM = new Random(); public List matchRecipes(List userIngredients, List customStaples) { Set userSet = new HashSet<>(); for (String ing : userIngredients) { userSet.add(ing.trim()); } // 合并系统默认 + 用户自定义常备调料 Set allStaples = new HashSet<>(DEFAULT_STAPLES); if (customStaples != null) { for (String s : customStaples) { allStaples.add(s.trim()); } } userSet.addAll(allStaples); List allRecipes = recipeMapper.selectList(null); log.info("FridgeService: total recipes in DB = {}", allRecipes.size()); List results = new ArrayList<>(); int currentHour = LocalTime.now().getHour(); int currentMonth = LocalDate.now().getMonthValue(); String mealTime = getMealTime(currentHour); String season = getSeason(currentMonth); log.info("FridgeService: current mealTime={}, season={}", mealTime, season); for (Recipe recipe : allRecipes) { List recipeIngredients = ingredientMapper.selectList( new QueryWrapper().eq("recipe_id", recipe.getId()) ); List recipeIngNames = recipeIngredients.stream() .map(ri -> ri.getIngredientName().trim()) .collect(Collectors.toList()); // 计算交集 long matchCount = recipeIngNames.stream() .filter(userSet::contains) .count(); double matchRate = recipeIngNames.isEmpty() ? 0 : (double) matchCount / recipeIngNames.size() * 100; // 找出缺失的食材(非调料类) List missing = recipeIngNames.stream() .filter(name -> !userSet.contains(name) && !allStaples.contains(name)) .collect(Collectors.toList()); // 时段/季节加权:匹配当前时段或季节 +8% double timeBoost = 0; if (mealTime.equals(recipe.getMealTime())) timeBoost += 8; if (season.equals(recipe.getSeason())) timeBoost += 8; FridgeMatchResult result = new FridgeMatchResult(); result.setRecipe(recipe); result.setMatchRate(Math.round(matchRate + timeBoost)); result.setMissingIngredients(missing); results.add(result); } // 按匹配度降序排列,取 Top10 候选池,加权随机选 3-5 个 List pool = results.stream() .sorted((a, b) -> Double.compare(b.getMatchRate(), a.getMatchRate())) .limit(10) .collect(Collectors.toList()); return weightedRandomPick(pool); } /** * 从候选池加权随机选 3-5 个。 * 权重 = matchRate²,保证高分菜更有机会但不乏惊喜。 */ private List weightedRandomPick(List pool) { if (pool.size() <= 3) return pool; int pickCount = 3 + RANDOM.nextInt(Math.min(3, pool.size() - 2)); // 3-5 List shuffled = new ArrayList<>(pool); Collections.shuffle(shuffled, RANDOM); // 加权随机 List picked = new ArrayList<>(); List remaining = new ArrayList<>(shuffled); for (int i = 0; i < pickCount && !remaining.isEmpty(); i++) { double totalWeight = remaining.stream() .mapToDouble(r -> Math.pow(r.getMatchRate(), 2)) .sum(); if (totalWeight <= 0) { int idx = RANDOM.nextInt(remaining.size()); picked.add(remaining.remove(idx)); continue; } double dart = RANDOM.nextDouble() * totalWeight; double cumulative = 0; int selectedIdx = 0; for (int j = 0; j < remaining.size(); j++) { cumulative += Math.pow(remaining.get(j).getMatchRate(), 2); if (dart <= cumulative) { selectedIdx = j; break; } } picked.add(remaining.remove(selectedIdx)); } // 匹配度高的排前面 picked.sort((a, b) -> Double.compare(b.getMatchRate(), a.getMatchRate())); return picked; } private String getMealTime(int hour) { if (hour >= 5 && hour < 10) return "早餐"; if (hour >= 10 && hour < 14) return "午餐"; if (hour >= 14 && hour < 17) return "下午茶"; if (hour >= 17 && hour < 21) return "晚餐"; return "夜宵"; } private String getSeason(int month) { if (month >= 3 && month <= 5) return "春季"; if (month >= 6 && month <= 8) return "夏季"; if (month >= 9 && month <= 11) return "秋季"; return "冬季"; } public Recipe getRecipeDetail(Long recipeId) { Recipe recipe = recipeMapper.selectById(recipeId); if (recipe == null) return null; List ingredients = ingredientMapper.selectList( new QueryWrapper().eq("recipe_id", recipeId) ); List steps = stepMapper.selectList( new QueryWrapper().eq("recipe_id", recipeId).orderByAsc("step_order") ); recipe.setIngredients(ingredients); recipe.setSteps(steps); return recipe; } }