feat(music-houduan): 实现歌单热度分数计算和缓存机制
- 新增 HotPlaylistScoreUtil 工具类用于计算歌单热度分数 - 在 Playlist 实体中添加 hotScore 字段 - 更新 PlaylistDTO 以包含热度分数 - 实现异步计算和缓存热门歌单的功能 - 添加定时任务每天更新热门歌单缓存
This commit is contained in:
@@ -70,4 +70,9 @@ public class PlaylistDTO {
|
|||||||
* 创建时间
|
* 创建时间
|
||||||
*/
|
*/
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 热度分
|
||||||
|
*/
|
||||||
|
private double hotScore;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user