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>
This commit is contained in:
183
backend/src/main/java/com/chowbox/service/FridgeService.java
Normal file
183
backend/src/main/java/com/chowbox/service/FridgeService.java
Normal file
@@ -0,0 +1,183 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user