21301050 | f5f827d | 2025-06-09 15:09:33 +0800 | [diff] [blame^] | 1 | package com.pt5.pthouduan.service; |
| 2 | |
| 3 | import com.pt5.pthouduan.entity.Torrent; |
| 4 | import com.pt5.pthouduan.entity.UserBehavior; |
| 5 | import com.pt5.pthouduan.mapper.RecommendMapper; |
| 6 | import org.springframework.beans.factory.annotation.Autowired; |
| 7 | import org.springframework.stereotype.Service; |
| 8 | |
| 9 | import java.time.LocalDateTime; |
| 10 | import java.time.temporal.ChronoUnit; |
| 11 | import java.util.*; |
| 12 | import java.util.stream.Collectors; |
| 13 | |
| 14 | |
| 15 | @Service |
| 16 | public class RecommendService { |
| 17 | @Autowired |
| 18 | private TorrentService torrentService; |
| 19 | |
| 20 | @Autowired |
| 21 | private RecommendMapper recommendMapper; |
| 22 | //收集用户行为数据 |
| 23 | public void recordUserBehavior(Long userId, Long torrentId, String behaviorType) { |
| 24 | UserBehavior behavior = new UserBehavior(); |
| 25 | behavior.setUserId(userId); |
| 26 | behavior.setTorrentId(torrentId); |
| 27 | behavior.setBehaviorType(behaviorType); |
| 28 | behavior.setWeight(getBehaviorWeight(behaviorType)); |
| 29 | behavior.setTimestamp(LocalDateTime.now()); |
| 30 | |
| 31 | recommendMapper.insertUserBehavior(behavior); |
| 32 | } |
| 33 | //对用户行为数据进行定期清洗 维护内存空间 |
| 34 | private void cleanOldBehaviors() { |
| 35 | recommendMapper.deleteOldBehaviorsForAllUsers(30); |
| 36 | } |
| 37 | |
| 38 | //设定不同用户行为的偏好权重(注意这里权重如果修改 那么BehaviorType同样需要修改 它在entity中) |
| 39 | private int getBehaviorWeight(String behaviorType) { |
| 40 | return switch (behaviorType) { |
| 41 | case "DOWNLOAD" -> 3; |
| 42 | case "FAVORITE" -> 5; |
| 43 | case "SEED" -> 4; |
| 44 | case "VIEW" -> 1; |
| 45 | case "COMMENT" -> 2; |
| 46 | default -> 1; |
| 47 | }; |
| 48 | } |
| 49 | |
| 50 | |
| 51 | |
| 52 | public List<Torrent> recommendForUser(Long userId) {//limit为资源的最大数量限制 |
| 53 | // 检查用户是否有足够的行为数据 |
| 54 | double totalWeight=recommendMapper.sumWeightsByUserId(userId); |
| 55 | |
| 56 | if (totalWeight>50) {//行为数据足够 |
| 57 | // 70% 偏好资源,15% 热门资源,15% 最新资源 |
| 58 | return getPersonalizedRecommendation(userId); |
| 59 | } else {//行为数据不足 |
| 60 | // 50% 热门资源,50% 最新资源 |
| 61 | return getDefaultRecommendation(); |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | //偏好资源推荐 |
| 66 | private List<Torrent> getPersonalizedRecommendation(Long userId) { |
| 67 | |
| 68 | // 获取偏好资源 |
| 69 | |
| 70 | List<Map<String, Object>> categories = recommendMapper.findFavoriteCategories(userId);//获取喜爱的类型及其权重 |
| 71 | categories = categories.stream() |
| 72 | .sorted((a, b) -> Integer.compare( |
| 73 | ((Number)b.get("totalWeight")).intValue(), |
| 74 | ((Number)a.get("totalWeight")).intValue() |
| 75 | )) |
| 76 | .limit(5) // 只取前5种偏好类型 |
| 77 | .toList(); |
| 78 | List<List<Torrent>> allTorrents = categories.stream() |
| 79 | .map(category -> torrentService.getTorrentsByCategory((Integer) category.get("categoryId"))) |
| 80 | .toList(); |
| 81 | // 3. 均匀混合算法 |
| 82 | List<Torrent> combinedList = new ArrayList<>(); |
| 83 | if (!allTorrents.isEmpty()) { |
| 84 | int maxSize = allTorrents.stream() |
| 85 | .mapToInt(List::size) |
| 86 | .max() |
| 87 | .orElse(0); |
| 88 | |
| 89 | // 按权重计算每种类型每次应该添加的元素数量 |
| 90 | int[] weights = categories.stream() |
| 91 | .mapToInt(c -> ((Number)c.get("totalWeight")).intValue()) // 安全转换 |
| 92 | .toArray(); |
| 93 | int totalWeight = Arrays.stream(weights).sum(); |
| 94 | |
| 95 | // 计算每种类型在每轮中应该添加的比例 |
| 96 | double[] ratios = Arrays.stream(weights) |
| 97 | .mapToDouble(w -> (double)w / totalWeight) |
| 98 | .toArray(); |
| 99 | |
| 100 | // 均匀混合实现 |
| 101 | for (int i = 0; i < maxSize; i++) { |
| 102 | for (int j = 0; j < allTorrents.size(); j++) { |
| 103 | List<Torrent> current = allTorrents.get(j); |
| 104 | if (i < current.size()) { |
| 105 | // 根据权重比例决定是否添加当前元素 |
| 106 | if (Math.random() < ratios[j]) { |
| 107 | combinedList.add(current.get(i)); |
| 108 | } |
| 109 | } |
| 110 | } |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | // 常规推荐 |
| 115 | List<Torrent> regular=getDefaultRecommendation(); |
| 116 | //合并结果 把个性化推荐和普通推荐相结合7:3 |
| 117 | List<Torrent> finalList = new ArrayList<>(); |
| 118 | int combinedSize = combinedList.size(); |
| 119 | int regularSize = regular.size(); |
| 120 | int actualCombined = combinedSize; |
| 121 | int actualRegular =regularSize; |
| 122 | Iterator<Torrent> combinedIter = combinedList.iterator(); |
| 123 | Iterator<Torrent> regularIter = regular.iterator(); |
| 124 | while (actualCombined > 0 || actualRegular > 0) { |
| 125 | double ratio = 0.9; |
| 126 | |
| 127 | if (Math.random() < ratio && actualCombined > 0) { |
| 128 | if (combinedIter.hasNext()) { |
| 129 | finalList.add(combinedIter.next()); |
| 130 | actualCombined--; |
| 131 | } |
| 132 | } else if (actualRegular > 0) { |
| 133 | if (regularIter.hasNext()) { |
| 134 | finalList.add(regularIter.next()); |
| 135 | actualRegular--; |
| 136 | } |
| 137 | } |
| 138 | } |
| 139 | while (regularIter.hasNext()) { |
| 140 | finalList.add(regularIter.next()); |
| 141 | } |
| 142 | Set<Integer> seenIds = new HashSet<>(); |
| 143 | List<Torrent> distinctList = finalList.stream() |
| 144 | .filter(torrent -> seenIds.add(torrent.getTorrentid().intValue())) // 首次出现时返回true |
| 145 | .collect(Collectors.toList()); |
| 146 | return distinctList; |
| 147 | } |
| 148 | //无偏好推荐 |
| 149 | private List<Torrent> getDefaultRecommendation() { |
| 150 | List<Torrent> torrents=torrentService.getAllTorrents(); |
| 151 | torrents= torrents.stream() |
| 152 | .sorted((t1, t2) -> { |
| 153 | // 时间衰减因子(新种子加分) |
| 154 | long daysOld1 = ChronoUnit.DAYS.between(t1.getUploadTime(), LocalDateTime.now()); |
| 155 | long daysOld2 = ChronoUnit.DAYS.between(t2.getUploadTime(), LocalDateTime.now()); |
| 156 | |
| 157 | // 综合分数 = downloadCount + (1 / (daysOld + 1)) * 系数 |
| 158 | double score1 = t1.getDownloadCount() + 10.0 / (daysOld1 + 1); |
| 159 | double score2 = t2.getDownloadCount() + 10.0 / (daysOld2 + 1); |
| 160 | |
| 161 | return Double.compare(score2, score1); |
| 162 | }) |
| 163 | .toList(); |
| 164 | return torrents; |
| 165 | } |
| 166 | |
| 167 | // public List<Map<String, Object>> test(){ |
| 168 | // return recommendMapper.findFavoriteCategories(1L); |
| 169 | // } |
| 170 | } |