diff --git a/music-houduan/src/main/java/com/test/musichouduan/dto/PlaylistDTO.java b/music-houduan/src/main/java/com/test/musichouduan/dto/PlaylistDTO.java index c167312..34809da 100644 --- a/music-houduan/src/main/java/com/test/musichouduan/dto/PlaylistDTO.java +++ b/music-houduan/src/main/java/com/test/musichouduan/dto/PlaylistDTO.java @@ -70,4 +70,9 @@ public class PlaylistDTO { * 创建时间 */ private LocalDateTime createTime; + + /** + * 热度分 + */ + private double hotScore; } diff --git a/music-houduan/src/main/java/com/test/musichouduan/entity/Playlist.java b/music-houduan/src/main/java/com/test/musichouduan/entity/Playlist.java index 37861b0..fe9a155 100644 --- a/music-houduan/src/main/java/com/test/musichouduan/entity/Playlist.java +++ b/music-houduan/src/main/java/com/test/musichouduan/entity/Playlist.java @@ -101,4 +101,10 @@ public class Playlist { @UpdateTimestamp @Column(nullable = false) private LocalDateTime updateTime; + + /** + * 热度分,不持久化到数据库 + */ + @Transient + private double hotScore; } diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/impl/PlaylistServiceImpl.java b/music-houduan/src/main/java/com/test/musichouduan/service/impl/PlaylistServiceImpl.java index 86ad1df..3c2836f 100644 --- a/music-houduan/src/main/java/com/test/musichouduan/service/impl/PlaylistServiceImpl.java +++ b/music-houduan/src/main/java/com/test/musichouduan/service/impl/PlaylistServiceImpl.java @@ -11,11 +11,22 @@ import com.test.musichouduan.entity.PlaylistType; import com.test.musichouduan.entity.User; import com.test.musichouduan.exception.BusinessException; import com.test.musichouduan.repository.*; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import com.test.musichouduan.service.FileService; import com.test.musichouduan.service.PlaylistService; +import com.test.musichouduan.util.HotPlaylistScoreUtil; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.util.CollectionUtils; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.Comparator; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; @@ -35,6 +46,16 @@ import java.util.stream.Collectors; @Service public class PlaylistServiceImpl implements PlaylistService { + private static final String HOT_PLAYLIST_CACHE_KEY = "playlist:hot_list"; + private static final long CACHE_EXPIRATION_HOURS = 2; + private static final int HOT_PLAYLIST_LIMIT = 50; + + @Autowired + private StringRedisTemplate redisTemplate; + + @Autowired + private ObjectMapper objectMapper; + @Autowired private PlaylistRepository playlistRepository; @@ -114,13 +135,74 @@ public class PlaylistServiceImpl implements PlaylistService { @Override public List getHotPlaylist(Integer limit) { - // 查询热门歌单 - List playlists = playlistRepository.findMostPlayed(PageRequest.of(0, limit)); + String hotPlaylistJson = redisTemplate.opsForValue().get(HOT_PLAYLIST_CACHE_KEY); - // 转换为DTO - return playlists.stream() + if (StringUtils.hasText(hotPlaylistJson)) { + try { + List cachedList = objectMapper.readValue(hotPlaylistJson, new TypeReference>() {}); + return cachedList.stream().limit(limit).collect(Collectors.toList()); + } catch (Exception e) { + System.err.println("Failed to deserialize hot playlist list from Redis: " + e.getMessage()); + } + } + + List hotList = calculateAndCacheHotPlaylistSync(); + return hotList.stream().limit(limit).collect(Collectors.toList()); + } + + private List calculateAndCacheHotPlaylistSync() { + try { + return updateHotPlaylistCache().get(); + } catch (Exception e) { + System.err.println("Failed to calculate hot playlist list synchronously: " + e.getMessage()); + List playlists = playlistRepository.findMostPlayed(PageRequest.of(0, HOT_PLAYLIST_LIMIT)); + return playlists.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + } + + @Scheduled(cron = "0 0 * * * ?") + public void scheduleUpdateHotPlaylist() { + System.out.println("Scheduled task started: Updating hot playlist cache..."); + updateHotPlaylistCache(); + } + + @Async + @Transactional(readOnly = true) + public CompletableFuture> updateHotPlaylistCache() { + List allPlaylists = new ArrayList<>(); + Page playlistPage; + int pageNum = 0; + int pageSize = 500; + + do { + Pageable pageable = PageRequest.of(pageNum, pageSize); + playlistPage = playlistRepository.findAll(pageable); + allPlaylists.addAll(playlistPage.getContent()); + pageNum++; + } while (playlistPage.hasNext()); + + allPlaylists.parallelStream().forEach(playlist -> { + double score = HotPlaylistScoreUtil.calculateHotScore(playlist); + playlist.setHotScore(score); + }); + + List sortedHotPlaylists = allPlaylists.stream() + .sorted(Comparator.comparing(Playlist::getHotScore).reversed()) + .limit(HOT_PLAYLIST_LIMIT) .map(this::convertToDTO) .collect(Collectors.toList()); + + try { + String jsonToCache = objectMapper.writeValueAsString(sortedHotPlaylists); + redisTemplate.opsForValue().set(HOT_PLAYLIST_CACHE_KEY, jsonToCache, CACHE_EXPIRATION_HOURS, TimeUnit.HOURS); + System.out.println("Hot playlist cache updated successfully with " + sortedHotPlaylists.size() + " items."); + } catch (Exception e) { + System.err.println("Failed to cache hot playlist list to Redis: " + e.getMessage()); + } + + return CompletableFuture.completedFuture(sortedHotPlaylists); } @Override @@ -450,6 +532,7 @@ public class PlaylistServiceImpl implements PlaylistService { private PlaylistDTO convertToDTO(Playlist playlist) { PlaylistDTO playlistDTO = new PlaylistDTO(); BeanUtils.copyProperties(playlist, playlistDTO); + playlistDTO.setHotScore(playlist.getHotScore()); // 设置创建者信息 if (playlist.getCreator() != null) { diff --git a/music-houduan/src/main/java/com/test/musichouduan/util/HotPlaylistScoreUtil.java b/music-houduan/src/main/java/com/test/musichouduan/util/HotPlaylistScoreUtil.java new file mode 100644 index 0000000..f59b431 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/util/HotPlaylistScoreUtil.java @@ -0,0 +1,47 @@ +package com.test.musichouduan.util; + +import com.test.musichouduan.entity.Playlist; +import java.time.Duration; +import java.time.LocalDateTime; + +/** + * 歌单热度分数计算工具类 + */ +public class HotPlaylistScoreUtil { + + // 定义各种交互的权重 + private static final double PLAY_WEIGHT = 1.0; + private static final double COLLECT_WEIGHT = 2.0; + private static final double COMMENT_WEIGHT = 1.5; + + // 时间衰减因子中的指数 + private static final double TIME_DECAY_EXPONENT = 1.8; + + /** + * 计算单张歌单的热度分数 + * 算法: 热度 = (交互权重和) / (时间衰减因子) + * + * @param playlist 歌单实体对象 + * @return 计算出的热度分数 + */ + public static double calculateHotScore(Playlist playlist) { + // 1. 计算交互权重和 + long playCount = playlist.getPlayCount() != null ? playlist.getPlayCount() : 0; + long collectCount = playlist.getCollectCount() != null ? playlist.getCollectCount() : 0; + long commentCount = playlist.getCommentCount() != null ? playlist.getCommentCount() : 0; + + double interactionScore = (playCount * PLAY_WEIGHT) + + (collectCount * COLLECT_WEIGHT) + + (commentCount * COMMENT_WEIGHT); + + // 2. 计算时间衰减因子 + LocalDateTime createTime = playlist.getCreateTime(); + LocalDateTime now = LocalDateTime.now(); + long hoursElapsed = Duration.between(createTime, now).toHours(); + + double timeDecayFactor = Math.pow(hoursElapsed + 2, TIME_DECAY_EXPONENT); + + // 3. 计算最终热度分数 + return interactionScore / timeDecayFactor; + } +}