- VoiceController: Map.of() -> Collections.singletonMap() 兼容 Java 8 - ExploreController: 补齐 takeoutService.roll() 缺失的 taste/priceRange/allergies 参数 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
184 lines
6.7 KiB
Java
184 lines
6.7 KiB
Java
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;
|
||
}
|
||
}
|