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 List<MusicDTO> musics;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 热度分
|
||||||
|
*/
|
||||||
|
private double hotScore;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,4 +84,10 @@ public class Singer {
|
|||||||
inverseJoinColumns = @JoinColumn(name = "music_id")
|
inverseJoinColumns = @JoinColumn(name = "music_id")
|
||||||
)
|
)
|
||||||
private Set<Music> musics = new HashSet<>();
|
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.SingerRepository;
|
||||||
import com.test.musichouduan.repository.SingerTypeRepository;
|
import com.test.musichouduan.repository.SingerTypeRepository;
|
||||||
import com.test.musichouduan.repository.UserRepository;
|
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.repository.UserSingerRepository;
|
||||||
import com.test.musichouduan.service.FileService;
|
import com.test.musichouduan.service.FileService;
|
||||||
import com.test.musichouduan.service.SingerService;
|
import com.test.musichouduan.service.SingerService;
|
||||||
|
import com.test.musichouduan.util.HotSingerScoreUtil;
|
||||||
import org.springframework.beans.BeanUtils;
|
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.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
@@ -39,6 +49,16 @@ import java.util.stream.Collectors;
|
|||||||
@Service
|
@Service
|
||||||
public class SingerServiceImpl implements SingerService {
|
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
|
@Autowired
|
||||||
private SingerRepository singerRepository;
|
private SingerRepository singerRepository;
|
||||||
|
|
||||||
@@ -115,13 +135,64 @@ public class SingerServiceImpl implements SingerService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<SingerDTO> getHotSinger(Integer limit) {
|
public List<SingerDTO> getHotSinger(Integer limit) {
|
||||||
// 查询热门歌手
|
String hotSingerJson = redisTemplate.opsForValue().get(HOT_SINGER_CACHE_KEY);
|
||||||
List<Singer> singers = singerRepository.findHotSingers(PageRequest.of(0, limit));
|
|
||||||
|
|
||||||
// 转换为DTO
|
if (StringUtils.hasText(hotSingerJson)) {
|
||||||
return singers.stream()
|
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)
|
.map(this::convertToDTO)
|
||||||
.collect(Collectors.toList());
|
.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
|
@Override
|
||||||
@@ -356,6 +427,7 @@ public class SingerServiceImpl implements SingerService {
|
|||||||
private SingerDTO convertToDTO(Singer singer) {
|
private SingerDTO convertToDTO(Singer singer) {
|
||||||
SingerDTO singerDTO = new SingerDTO();
|
SingerDTO singerDTO = new SingerDTO();
|
||||||
BeanUtils.copyProperties(singer, singerDTO);
|
BeanUtils.copyProperties(singer, singerDTO);
|
||||||
|
singerDTO.setHotScore(singer.getHotScore());
|
||||||
|
|
||||||
// 设置类型信息
|
// 设置类型信息
|
||||||
if (singer.getType() != null) {
|
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