Files
ChowBox/backend/src/main/java/com/chowbox/service/FridgeService.java
王鹏 802b4ba229 fix: 修复 VoiceController Map.of 兼容性 + ExploreController 参数不匹配
- VoiceController: Map.of() -> Collections.singletonMap() 兼容 Java 8
- ExploreController: 补齐 takeoutService.roll() 缺失的 taste/priceRange/allergies 参数

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 20:02:27 +08:00

184 lines
6.7 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String> DEFAULT_STAPLES = new HashSet<>(Arrays.asList(
"", "", "酱油", "生抽", "老抽", "", "料酒", "蚝油",
"", "白糖", "淀粉", "香油", "", "", "", "干辣椒", "花椒"
));
private static final Random RANDOM = new Random();
public List<FridgeMatchResult> matchRecipes(List<String> userIngredients, List<String> customStaples) {
Set<String> userSet = new HashSet<>();
for (String ing : userIngredients) {
userSet.add(ing.trim());
}
// 合并系统默认 + 用户自定义常备调料
Set<String> allStaples = new HashSet<>(DEFAULT_STAPLES);
if (customStaples != null) {
for (String s : customStaples) {
allStaples.add(s.trim());
}
}
userSet.addAll(allStaples);
List<Recipe> allRecipes = recipeMapper.selectList(null);
log.info("FridgeService: total recipes in DB = {}", allRecipes.size());
List<FridgeMatchResult> 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<RecipeIngredient> recipeIngredients = ingredientMapper.selectList(
new QueryWrapper<RecipeIngredient>().eq("recipe_id", recipe.getId())
);
List<String> 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<String> 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<FridgeMatchResult> 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<FridgeMatchResult> weightedRandomPick(List<FridgeMatchResult> pool) {
if (pool.size() <= 3) return pool;
int pickCount = 3 + RANDOM.nextInt(Math.min(3, pool.size() - 2)); // 3-5
List<FridgeMatchResult> shuffled = new ArrayList<>(pool);
Collections.shuffle(shuffled, RANDOM);
// 加权随机
List<FridgeMatchResult> picked = new ArrayList<>();
List<FridgeMatchResult> 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<RecipeIngredient> ingredients = ingredientMapper.selectList(
new QueryWrapper<RecipeIngredient>().eq("recipe_id", recipeId)
);
List<RecipeStep> steps = stepMapper.selectList(
new QueryWrapper<RecipeStep>().eq("recipe_id", recipeId).orderByAsc("step_order")
);
recipe.setIngredients(ingredients);
recipe.setSteps(steps);
return recipe;
}
}