FoodRecommendService.java 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. package com.etotem.cfc.service;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import com.etotem.cfc.entity.Food;
  4. import com.etotem.cfc.entity.FoodRecommendIndex;
  5. import com.etotem.cfc.entity.FoodRecommendIdxLog;
  6. import com.etotem.cfc.entity.FamilyMember;
  7. import com.etotem.cfc.entity.HealthReport;
  8. import com.etotem.cfc.mapper.FoodRecommendIndexMapper;
  9. import com.etotem.cfc.mapper.FoodRecommendIdxLogMapper;
  10. import com.etotem.cfc.mapper.FamilyMemberMapper;
  11. import lombok.extern.slf4j.Slf4j;
  12. import org.springframework.stereotype.Service;
  13. import org.springframework.transaction.annotation.Transactional;
  14. import javax.annotation.Resource;
  15. import java.util.*;
  16. import java.util.stream.Collectors;
  17. @Slf4j
  18. @Service
  19. public class FoodRecommendService {
  20. @Resource
  21. private FoodRecommendIndexMapper foodRecommendIndexMapper;
  22. @Resource
  23. private FoodRecommendIdxLogMapper foodRecommendIdxLogMapper;
  24. @Resource
  25. private FamilyMemberMapper familyMemberMapper;
  26. @Resource
  27. private HealthReportService healthReportService;
  28. @Resource
  29. private FoodService foodService;
  30. @Transactional
  31. public void recalculateForUser(Long userId) {
  32. HealthReport report = healthReportService.getLatestReport(userId);
  33. if (report == null) {
  34. foodRecommendIndexMapper.delete(new LambdaQueryWrapper<FoodRecommendIndex>()
  35. .eq(FoodRecommendIndex::getUserId, userId));
  36. log.info("用户{}无最新健康报告,已清除推荐指数", userId);
  37. return;
  38. }
  39. // 从 foods 表读取最新报告关联的食材推荐(正常确认流程写入 foods 表)
  40. List<Food> reportFoods = foodService.listByReportId(report.getId());
  41. if (reportFoods.isEmpty()) {
  42. foodRecommendIndexMapper.delete(new LambdaQueryWrapper<FoodRecommendIndex>()
  43. .eq(FoodRecommendIndex::getUserId, userId));
  44. log.info("报告{}无食材推荐数据,已清除用户{}的推荐指数", report.getId(), userId);
  45. return;
  46. }
  47. List<FoodRecommendIndex> existingIndices = foodRecommendIndexMapper.selectList(
  48. new LambdaQueryWrapper<FoodRecommendIndex>()
  49. .eq(FoodRecommendIndex::getUserId, userId)
  50. );
  51. Map<Long, FoodRecommendIndex> existingMap = existingIndices.stream()
  52. .collect(Collectors.toMap(FoodRecommendIndex::getFoodId, idx -> idx));
  53. Date now = new Date();
  54. Long reportId = report.getId();
  55. for (Food food : reportFoods) {
  56. Long foodId = food.getId();
  57. Integer newIndex = food.getScore();
  58. String foodName = food.getName();
  59. FoodRecommendIndex existing = existingMap.get(foodId);
  60. if (existing != null) {
  61. if (!Objects.equals(existing.getIndexScore(), newIndex)) {
  62. Integer oldIndex = existing.getIndexScore();
  63. existing.setIndexScore(newIndex);
  64. existing.setFoodName(foodName);
  65. existing.setHealthReportId(reportId);
  66. existing.setCalculatedAt(now);
  67. foodRecommendIndexMapper.updateById(existing);
  68. FoodRecommendIdxLog logEntry = new FoodRecommendIdxLog();
  69. logEntry.setUserId(userId);
  70. logEntry.setFoodId(foodId);
  71. logEntry.setFoodName(foodName);
  72. logEntry.setPreviousIndex(oldIndex);
  73. logEntry.setNewIndex(newIndex);
  74. logEntry.setHealthReportId(reportId);
  75. logEntry.setChangeReason("健康报告更新");
  76. logEntry.setChangedAt(now);
  77. foodRecommendIdxLogMapper.insert(logEntry);
  78. log.info("用户{}食材{}推荐指数变更: {} -> {}", userId, foodId, oldIndex, newIndex);
  79. }
  80. } else {
  81. FoodRecommendIndex index = new FoodRecommendIndex();
  82. index.setUserId(userId);
  83. index.setFoodId(foodId);
  84. index.setFoodName(foodName);
  85. index.setIndexScore(newIndex);
  86. index.setHealthReportId(reportId);
  87. index.setCalculatedAt(now);
  88. index.setCreatedAt(now);
  89. foodRecommendIndexMapper.insert(index);
  90. }
  91. existingMap.remove(foodId);
  92. }
  93. for (Map.Entry<Long, FoodRecommendIndex> entry : existingMap.entrySet()) {
  94. FoodRecommendIndex removed = entry.getValue();
  95. foodRecommendIndexMapper.deleteById(removed.getId());
  96. FoodRecommendIdxLog logEntry = new FoodRecommendIdxLog();
  97. logEntry.setUserId(userId);
  98. logEntry.setFoodId(removed.getFoodId());
  99. logEntry.setFoodName(removed.getFoodName());
  100. logEntry.setPreviousIndex(removed.getIndexScore());
  101. logEntry.setNewIndex(null);
  102. logEntry.setHealthReportId(reportId);
  103. logEntry.setChangeReason("已从最新健康报告中移除");
  104. logEntry.setChangedAt(now);
  105. foodRecommendIdxLogMapper.insert(logEntry);
  106. log.info("用户{}食材{}已从最新报告中移除", userId, removed.getFoodId());
  107. }
  108. }
  109. public List<FoodRecommendIndex> getIndexByUser(Long userId) {
  110. return foodRecommendIndexMapper.selectList(
  111. new LambdaQueryWrapper<FoodRecommendIndex>()
  112. .eq(FoodRecommendIndex::getUserId, userId)
  113. );
  114. }
  115. /**
  116. * 获取用户"当前食材"推荐列表(含分类与营养信息,供饮食推荐页展示)。
  117. * 数据来自 food_recommend_idx(有新报告时 recalculateForUser 有则更新/无则添加),
  118. * join foods 表补充 category/protein 等字段。
  119. */
  120. public List<Map<String, Object>> getCurrentFoods(Long userId) {
  121. List<FoodRecommendIndex> indices = getIndexByUser(userId);
  122. if (indices.isEmpty()) return new ArrayList<>();
  123. Set<Long> foodIds = new HashSet<>();
  124. for (FoodRecommendIndex idx : indices) {
  125. if (idx.getFoodId() != null) foodIds.add(idx.getFoodId());
  126. }
  127. Map<Long, Food> foodMap = new HashMap<>();
  128. if (!foodIds.isEmpty()) {
  129. List<Food> foods = foodService.listByIds(foodIds);
  130. for (Food f : foods) {
  131. if (f != null) foodMap.put(f.getId(), f);
  132. }
  133. }
  134. List<Map<String, Object>> result = new ArrayList<>();
  135. for (FoodRecommendIndex idx : indices) {
  136. Food food = foodMap.get(idx.getFoodId());
  137. Map<String, Object> m = new LinkedHashMap<>();
  138. m.put("name", food != null && food.getName() != null ? food.getName() : idx.getFoodName());
  139. m.put("foodName", m.get("name"));
  140. m.put("score", idx.getIndexScore());
  141. m.put("category", food != null ? food.getCategory() : "");
  142. if (food != null) {
  143. m.put("protein", food.getProtein());
  144. m.put("fat", food.getFat());
  145. m.put("carbs", food.getCarbs());
  146. m.put("fiber", food.getFiber());
  147. m.put("energyKj", food.getEnergyKj());
  148. }
  149. result.add(m);
  150. }
  151. return result;
  152. }
  153. public List<FoodRecommendIdxLog> getIndexHistory(Long userId, Long foodId) {
  154. return foodRecommendIdxLogMapper.selectList(
  155. new LambdaQueryWrapper<FoodRecommendIdxLog>()
  156. .eq(FoodRecommendIdxLog::getUserId, userId)
  157. .eq(FoodRecommendIdxLog::getFoodId, foodId)
  158. .orderByDesc(FoodRecommendIdxLog::getChangedAt)
  159. );
  160. }
  161. public Map<Long, List<FoodRecommendIndex>> getByFamily(Long familyId) {
  162. List<FamilyMember> members = familyMemberMapper.selectList(
  163. new LambdaQueryWrapper<FamilyMember>()
  164. .eq(FamilyMember::getFamilyId, familyId)
  165. );
  166. Map<Long, List<FoodRecommendIndex>> result = new HashMap<>();
  167. for (FamilyMember member : members) {
  168. Long memberUserId = member.getUserId();
  169. if (memberUserId != null) {
  170. List<FoodRecommendIndex> indices = foodRecommendIndexMapper.selectList(
  171. new LambdaQueryWrapper<FoodRecommendIndex>()
  172. .eq(FoodRecommendIndex::getUserId, memberUserId)
  173. );
  174. if (!indices.isEmpty()) {
  175. result.put(memberUserId, indices);
  176. }
  177. }
  178. }
  179. return result;
  180. }
  181. /**
  182. * 获取戒备食材列表(推荐指数 < 0),按指数升序排列(最需戒备的排最前)
  183. */
  184. public List<FoodRecommendIndex> getCautionFoods(Long userId) {
  185. return foodRecommendIndexMapper.selectList(
  186. new LambdaQueryWrapper<FoodRecommendIndex>()
  187. .eq(FoodRecommendIndex::getUserId, userId)
  188. .lt(FoodRecommendIndex::getIndexScore, 0)
  189. .orderByAsc(FoodRecommendIndex::getIndexScore)
  190. );
  191. }
  192. }