feat(singer): 实现歌手热度分计算和缓存机制

- 新增 HotSingerScoreUtil工具类,用于计算歌手热度分数
- 在 Singer 实体中添加 hotScore 字段,用于存储热度分数
- 在 SingerDTO 中添加 hotScore 字段,用于展示热度分数
- 修改 SingerServiceImpl 中的 getHotSinger 方法,支持从 Redis 缓存中获取热门歌手列表
- 实现定时任务和异步方法,用于定期更新和缓存热门歌手列表
This commit is contained in:
ikmkj
2025-07-26 11:05:02 +08:00
parent 7ec4c60110
commit 5d9eca4929
4 changed files with 131 additions and 4 deletions

View File

@@ -60,4 +60,9 @@ public class SingerDTO {
* 关联音乐列表
*/
private List<MusicDTO> musics;
/**
* 热度分
*/
private double hotScore;
}

View File

@@ -84,4 +84,10 @@ public class Singer {
inverseJoinColumns = @JoinColumn(name = "music_id")
)
private Set<Music> musics = new HashSet<>();
/**
* 热度分,不持久化到数据库
*/
@Transient
private double hotScore;
}

View File

@@ -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,13 +135,64 @@ 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
return singers.stream()
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
@@ -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) {

View File

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