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