feat(singer): 实现歌手热度分计算和缓存机制
- 新增 HotSingerScoreUtil工具类,用于计算歌手热度分数 - 在 Singer 实体中添加 hotScore 字段,用于存储热度分数 - 在 SingerDTO 中添加 hotScore 字段,用于展示热度分数 - 修改 SingerServiceImpl 中的 getHotSinger 方法,支持从 Redis 缓存中获取热门歌手列表 - 实现定时任务和异步方法,用于定期更新和缓存热门歌手列表
This commit is contained in:
@@ -60,4 +60,9 @@ public class SingerDTO {
|
||||
* 关联音乐列表
|
||||
*/
|
||||
private List<MusicDTO> musics;
|
||||
|
||||
/**
|
||||
* 热度分
|
||||
*/
|
||||
private double hotScore;
|
||||
}
|
||||
|
||||
@@ -84,4 +84,10 @@ public class Singer {
|
||||
inverseJoinColumns = @JoinColumn(name = "music_id")
|
||||
)
|
||||
private Set<Music> musics = new HashSet<>();
|
||||
|
||||
/**
|
||||
* 热度分,不持久化到数据库
|
||||
*/
|
||||
@Transient
|
||||
private double hotScore;
|
||||
}
|
||||
|
||||
@@ -14,10 +14,20 @@ import com.test.musichouduan.repository.MusicRepository;
|
||||
import com.test.musichouduan.repository.SingerRepository;
|
||||
import com.test.musichouduan.repository.SingerTypeRepository;
|
||||
import com.test.musichouduan.repository.UserRepository;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.test.musichouduan.repository.UserSingerRepository;
|
||||
import com.test.musichouduan.service.FileService;
|
||||
import com.test.musichouduan.service.SingerService;
|
||||
import com.test.musichouduan.util.HotSingerScoreUtil;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
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.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
@@ -39,6 +49,16 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class SingerServiceImpl implements SingerService {
|
||||
|
||||
private static final String HOT_SINGER_CACHE_KEY = "singer:hot_list";
|
||||
private static final long CACHE_EXPIRATION_HOURS = 2;
|
||||
private static final int HOT_SINGER_LIMIT = 50;
|
||||
|
||||
@Autowired
|
||||
private StringRedisTemplate redisTemplate;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private SingerRepository singerRepository;
|
||||
|
||||
@@ -115,14 +135,65 @@ public class SingerServiceImpl implements SingerService {
|
||||
|
||||
@Override
|
||||
public List<SingerDTO> getHotSinger(Integer limit) {
|
||||
// 查询热门歌手
|
||||
List<Singer> singers = singerRepository.findHotSingers(PageRequest.of(0, limit));
|
||||
String hotSingerJson = redisTemplate.opsForValue().get(HOT_SINGER_CACHE_KEY);
|
||||
|
||||
// 转换为DTO
|
||||
if (StringUtils.hasText(hotSingerJson)) {
|
||||
try {
|
||||
List<SingerDTO> cachedList = objectMapper.readValue(hotSingerJson, new TypeReference<List<SingerDTO>>() {});
|
||||
return cachedList.stream().limit(limit).collect(Collectors.toList());
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to deserialize hot singer list from Redis: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
List<SingerDTO> hotList = calculateAndCacheHotSingerSync();
|
||||
return hotList.stream().limit(limit).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<SingerDTO> calculateAndCacheHotSingerSync() {
|
||||
try {
|
||||
return updateHotSingerCache().get();
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to calculate hot singer list synchronously: " + e.getMessage());
|
||||
List<Singer> singers = singerRepository.findHotSingers(PageRequest.of(0, HOT_SINGER_LIMIT));
|
||||
return singers.stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 0 * * * ?")
|
||||
public void scheduleUpdateHotSinger() {
|
||||
System.out.println("Scheduled task started: Updating hot singer cache...");
|
||||
updateHotSingerCache();
|
||||
}
|
||||
|
||||
@Async
|
||||
@Transactional(readOnly = true)
|
||||
public CompletableFuture<List<SingerDTO>> updateHotSingerCache() {
|
||||
List<Singer> allSingers = singerRepository.findAll();
|
||||
|
||||
allSingers.parallelStream().forEach(singer -> {
|
||||
double score = HotSingerScoreUtil.calculateHotScore(singer);
|
||||
singer.setHotScore(score);
|
||||
});
|
||||
|
||||
List<SingerDTO> sortedHotSingers = allSingers.stream()
|
||||
.sorted(Comparator.comparing(Singer::getHotScore).reversed())
|
||||
.limit(HOT_SINGER_LIMIT)
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
try {
|
||||
String jsonToCache = objectMapper.writeValueAsString(sortedHotSingers);
|
||||
redisTemplate.opsForValue().set(HOT_SINGER_CACHE_KEY, jsonToCache, CACHE_EXPIRATION_HOURS, TimeUnit.HOURS);
|
||||
System.out.println("Hot singer cache updated successfully with " + sortedHotSingers.size() + " items.");
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to cache hot singer list to Redis: " + e.getMessage());
|
||||
}
|
||||
|
||||
return CompletableFuture.completedFuture(sortedHotSingers);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@@ -356,6 +427,7 @@ public class SingerServiceImpl implements SingerService {
|
||||
private SingerDTO convertToDTO(Singer singer) {
|
||||
SingerDTO singerDTO = new SingerDTO();
|
||||
BeanUtils.copyProperties(singer, singerDTO);
|
||||
singerDTO.setHotScore(singer.getHotScore());
|
||||
|
||||
// 设置类型信息
|
||||
if (singer.getType() != null) {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.test.musichouduan.util;
|
||||
|
||||
import com.test.musichouduan.entity.Singer;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 歌手热度分数计算工具类
|
||||
*/
|
||||
public class HotSingerScoreUtil {
|
||||
|
||||
// 定义各种交互的权重
|
||||
private static final double FANS_WEIGHT = 2.0; // 粉丝数(收藏数)
|
||||
private static final double COMMENT_WEIGHT = 1.5;
|
||||
|
||||
// 时间衰减因子中的指数
|
||||
private static final double TIME_DECAY_EXPONENT = 1.8;
|
||||
|
||||
/**
|
||||
* 计算单个歌手的热度分数
|
||||
* 算法: 热度 = (交互权重和) / (时间衰减因子)
|
||||
*
|
||||
* @param singer 歌手实体对象
|
||||
* @return 计算出的热度分数
|
||||
*/
|
||||
public static double calculateHotScore(Singer singer) {
|
||||
// 1. 计算交互权重和
|
||||
long fansCount = singer.getCollectCount() != null ? singer.getCollectCount() : 0;
|
||||
long commentCount = singer.getCommentCount() != null ? singer.getCommentCount() : 0;
|
||||
|
||||
double interactionScore = (fansCount * FANS_WEIGHT) +
|
||||
(commentCount * COMMENT_WEIGHT);
|
||||
|
||||
// 2. 计算时间衰减因子
|
||||
LocalDateTime createTime = singer.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