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:
王鹏
2026-05-08 20:02:27 +08:00
commit 802b4ba229
98 changed files with 5761 additions and 0 deletions

View 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;
}
}