feat(music-houduan): 实现歌单热度分数计算和缓存机制

- 新增 HotPlaylistScoreUtil 工具类用于计算歌单热度分数
- 在 Playlist 实体中添加 hotScore 字段
- 更新 PlaylistDTO 以包含热度分数
- 实现异步计算和缓存热门歌单的功能
- 添加定时任务每天更新热门歌单缓存
This commit is contained in:
ikmkj
2025-07-26 10:52:30 +08:00
parent 2af40c5a32
commit 7ec4c60110
4 changed files with 145 additions and 4 deletions

View File

@@ -70,4 +70,9 @@ public class PlaylistDTO {
* 创建时间 * 创建时间
*/ */
private LocalDateTime createTime; private LocalDateTime createTime;
/**
* 热度分
*/
private double hotScore;
} }

View File

@@ -101,4 +101,10 @@ public class Playlist {
@UpdateTimestamp @UpdateTimestamp
@Column(nullable = false) @Column(nullable = false)
private LocalDateTime updateTime; private LocalDateTime updateTime;
/**
* 热度分,不持久化到数据库
*/
@Transient
private double hotScore;
} }

View File

@@ -11,11 +11,22 @@ import com.test.musichouduan.entity.PlaylistType;
import com.test.musichouduan.entity.User; import com.test.musichouduan.entity.User;
import com.test.musichouduan.exception.BusinessException; import com.test.musichouduan.exception.BusinessException;
import com.test.musichouduan.repository.*; 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.FileService;
import com.test.musichouduan.service.PlaylistService; import com.test.musichouduan.service.PlaylistService;
import com.test.musichouduan.util.HotPlaylistScoreUtil;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page; 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.PageRequest;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
@@ -35,6 +46,16 @@ import java.util.stream.Collectors;
@Service @Service
public class PlaylistServiceImpl implements PlaylistService { 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 @Autowired
private PlaylistRepository playlistRepository; private PlaylistRepository playlistRepository;
@@ -114,13 +135,74 @@ public class PlaylistServiceImpl implements PlaylistService {
@Override @Override
public List<PlaylistDTO> getHotPlaylist(Integer limit) { public List<PlaylistDTO> getHotPlaylist(Integer limit) {
// 查询热门歌单 String hotPlaylistJson = redisTemplate.opsForValue().get(HOT_PLAYLIST_CACHE_KEY);
List<Playlist> playlists = playlistRepository.findMostPlayed(PageRequest.of(0, limit));
// 转换为DTO if (StringUtils.hasText(hotPlaylistJson)) {
return playlists.stream() try {
List<PlaylistDTO> cachedList = objectMapper.readValue(hotPlaylistJson, new TypeReference<List<PlaylistDTO>>() {});
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<PlaylistDTO> hotList = calculateAndCacheHotPlaylistSync();
return hotList.stream().limit(limit).collect(Collectors.toList());
}
private List<PlaylistDTO> calculateAndCacheHotPlaylistSync() {
try {
return updateHotPlaylistCache().get();
} catch (Exception e) {
System.err.println("Failed to calculate hot playlist list synchronously: " + e.getMessage());
List<Playlist> 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<List<PlaylistDTO>> updateHotPlaylistCache() {
List<Playlist> allPlaylists = new ArrayList<>();
Page<Playlist> 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<PlaylistDTO> sortedHotPlaylists = allPlaylists.stream()
.sorted(Comparator.comparing(Playlist::getHotScore).reversed())
.limit(HOT_PLAYLIST_LIMIT)
.map(this::convertToDTO) .map(this::convertToDTO)
.collect(Collectors.toList()); .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 @Override
@@ -450,6 +532,7 @@ public class PlaylistServiceImpl implements PlaylistService {
private PlaylistDTO convertToDTO(Playlist playlist) { private PlaylistDTO convertToDTO(Playlist playlist) {
PlaylistDTO playlistDTO = new PlaylistDTO(); PlaylistDTO playlistDTO = new PlaylistDTO();
BeanUtils.copyProperties(playlist, playlistDTO); BeanUtils.copyProperties(playlist, playlistDTO);
playlistDTO.setHotScore(playlist.getHotScore());
// 设置创建者信息 // 设置创建者信息
if (playlist.getCreator() != null) { if (playlist.getCreator() != null) {

View File

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