refactor(comment): 优化评论功能实现并改进热门音乐缓存更新机制

- 移除CommentController中多余的权限注解导入
- 在CommentController中注入MusicService用于缓存更新
- 添加评论和删除评论时触发热门音乐缓存更新
- 提取评论数更新逻辑到独立方法updateTargetCommentCount
- 删除评论时在最后同步评论数避免统计偏大
- 优化HotScoreUtil中播放量权重算法采用对数缩放
- 在MusicController的播放、收藏、点赞操作后更新热门音乐缓存
- 改进MusicRepository查询排序避免数据重复
- 重构MusicServiceImpl中热门音乐排序逻辑增强稳定性
This commit is contained in:
ikmkj
2026-04-07 21:26:35 +08:00
parent 86b286453a
commit aa1eabfeb7
6 changed files with 67 additions and 54 deletions

View File

@@ -1,13 +1,12 @@
package com.test.musichouduan.controller; package com.test.musichouduan.controller;
import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckRole;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import com.test.musichouduan.dto.CommentDTO; import com.test.musichouduan.dto.CommentDTO;
import com.test.musichouduan.dto.PageResult; import com.test.musichouduan.dto.PageResult;
import com.test.musichouduan.dto.Result; import com.test.musichouduan.dto.Result;
import com.test.musichouduan.service.CommentService; import com.test.musichouduan.service.CommentService;
import com.test.musichouduan.service.MusicService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -21,6 +20,9 @@ public class CommentController {
@Autowired @Autowired
private CommentService commentService; private CommentService commentService;
@Autowired
private MusicService musicService;
/** /**
* 根据ID获取评论 * 根据ID获取评论
* *
@@ -62,6 +64,9 @@ public class CommentController {
@PostMapping("/add") @PostMapping("/add")
public Result<CommentDTO> addComment(@RequestBody CommentDTO commentDTO) { public Result<CommentDTO> addComment(@RequestBody CommentDTO commentDTO) {
CommentDTO result = commentService.addComment(commentDTO); CommentDTO result = commentService.addComment(commentDTO);
if (commentDTO.getTargetType() != null && commentDTO.getTargetType() == 1) {
musicService.updateHotMusicCache();
}
return Result.success("评论成功", result); return Result.success("评论成功", result);
} }
@@ -74,7 +79,11 @@ public class CommentController {
@SaCheckLogin @SaCheckLogin
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public Result<Boolean> deleteComment(@PathVariable Long id) { public Result<Boolean> deleteComment(@PathVariable Long id) {
CommentDTO commentDTO = commentService.getCommentById(id);
boolean result = commentService.deleteComment(id); boolean result = commentService.deleteComment(id);
if (commentDTO.getTargetType() != null && commentDTO.getTargetType() == 1) {
musicService.updateHotMusicCache();
}
return Result.success("删除成功", result); return Result.success("删除成功", result);
} }

View File

@@ -166,6 +166,7 @@ public class MusicController {
@RequestPart(value = "cover", required = false) MultipartFile cover, @RequestPart(value = "cover", required = false) MultipartFile cover,
@RequestPart(value = "lyric", required = false) MultipartFile lyric) { @RequestPart(value = "lyric", required = false) MultipartFile lyric) {
MusicDTO result = musicService.updateMusic(id, musicDTO, file, cover, lyric); MusicDTO result = musicService.updateMusic(id, musicDTO, file, cover, lyric);
musicService.updateHotMusicCache();
return Result.success("更新成功", result); return Result.success("更新成功", result);
} }
@@ -195,6 +196,7 @@ public class MusicController {
// 获取当前登录用户ID // 获取当前登录用户ID
Long userId = StpUtil.getLoginIdAsLong(); Long userId = StpUtil.getLoginIdAsLong();
boolean isCollected = musicService.toggleCollectMusic(id, userId); boolean isCollected = musicService.toggleCollectMusic(id, userId);
musicService.updateHotMusicCache();
String message = isCollected ? "收藏成功" : "取消收藏成功"; String message = isCollected ? "收藏成功" : "取消收藏成功";
return Result.success(message, isCollected); return Result.success(message, isCollected);
} }
@@ -227,6 +229,9 @@ public class MusicController {
@PostMapping("/{id}/play") @PostMapping("/{id}/play")
public Result<MusicDTO> playMusic(@PathVariable Long id) { public Result<MusicDTO> playMusic(@PathVariable Long id) {
MusicDTO musicDTO = musicService.playMusic(id); MusicDTO musicDTO = musicService.playMusic(id);
if (musicDTO != null) {
musicService.updateHotMusicCache();
}
return Result.success(musicDTO); return Result.success(musicDTO);
} }
@@ -242,6 +247,7 @@ public class MusicController {
// 获取当前登录用户ID // 获取当前登录用户ID
Long userId = StpUtil.getLoginIdAsLong(); Long userId = StpUtil.getLoginIdAsLong();
boolean isLiked = musicService.toggleLikeMusic(id, userId); boolean isLiked = musicService.toggleLikeMusic(id, userId);
musicService.updateHotMusicCache();
String message = isLiked ? "点赞成功" : "取消点赞成功"; String message = isLiked ? "点赞成功" : "取消点赞成功";
return Result.success(message, isLiked); return Result.success(message, isLiked);
} }

View File

@@ -78,7 +78,7 @@ public interface MusicRepository extends JpaRepository<Music, Long>, JpaSpecific
* @param pageable 分页 * @param pageable 分页
* @return 音乐列表 * @return 音乐列表
*/ */
@Query("SELECT m FROM Music m ORDER BY m.createTime DESC") @Query("SELECT m FROM Music m ORDER BY m.createTime DESC, m.id DESC")
List<Music> findLatest(Pageable pageable); List<Music> findLatest(Pageable pageable);
/** /**
@@ -87,7 +87,7 @@ public interface MusicRepository extends JpaRepository<Music, Long>, JpaSpecific
* @param pageable 分页 * @param pageable 分页
* @return 音乐列表 * @return 音乐列表
*/ */
@Query("SELECT m FROM Music m ORDER BY m.playCount DESC") @Query("SELECT m FROM Music m ORDER BY m.playCount DESC, m.createTime DESC, m.id DESC")
List<Music> findMostPlayed(Pageable pageable); List<Music> findMostPlayed(Pageable pageable);
/** /**

View File

@@ -136,33 +136,7 @@ public class CommentServiceImpl implements CommentService {
Comment savedComment = commentRepository.save(comment); Comment savedComment = commentRepository.save(comment);
// 如果评论的是音乐 (targetType=1, 这个值需要与前端约定好) updateTargetCommentCount(savedComment.getTargetType(), savedComment.getTargetId());
// 通常可以定义一个枚举或常量类来管理这些类型值
if (savedComment.getTargetType() == 1) {
musicRepository.findById(savedComment.getTargetId()).ifPresent(music -> {
// 首先判断类型是否等于1
if (savedComment.getTargetType() == 1) {
// 查询评论表使用COUNT(id)统计该音乐的评论数量
long commentCount = commentRepository.countByTargetIdAndTargetType(savedComment.getTargetId(), 1);
// 设置到音乐中
music.setCommentCount((int) commentCount);
// 保存音乐实体
musicRepository.save(music);
}
});
} else if (savedComment.getTargetType() == 2) { // 2 代表歌单
playlistRepository.findById(savedComment.getTargetId()).ifPresent(playlist -> {
long commentCount = commentRepository.countByTargetIdAndTargetType(savedComment.getTargetId(), 2);
playlist.setCommentCount((int) commentCount);
playlistRepository.save(playlist);
});
} else if (savedComment.getTargetType() == 3) { // 3 代表歌手
singerRepository.findById(savedComment.getTargetId()).ifPresent(singer -> {
long commentCount = commentRepository.countByTargetIdAndTargetType(savedComment.getTargetId(), 3);
singer.setCommentCount((int) commentCount);
singerRepository.save(singer);
});
}
// 将新评论转换为DTO // 将新评论转换为DTO
CommentDTO savedCommentDTO = convertToDTO(savedComment); CommentDTO savedCommentDTO = convertToDTO(savedComment);
@@ -194,27 +168,6 @@ public class CommentServiceImpl implements CommentService {
// 删除评论 // 删除评论
commentRepository.delete(comment); commentRepository.delete(comment);
// 更新相关实体的评论数
if (comment.getTargetType() == 1) {
musicRepository.findById(comment.getTargetId()).ifPresent(music -> {
long commentCount = commentRepository.countByTargetIdAndTargetType(comment.getTargetId(), 1);
music.setCommentCount((int) commentCount);
musicRepository.save(music);
});
} else if (comment.getTargetType() == 2) {
playlistRepository.findById(comment.getTargetId()).ifPresent(playlist -> {
long commentCount = commentRepository.countByTargetIdAndTargetType(comment.getTargetId(), 2);
playlist.setCommentCount((int) commentCount);
playlistRepository.save(playlist);
});
} else if (comment.getTargetType() == 3) {
singerRepository.findById(comment.getTargetId()).ifPresent(singer -> {
long commentCount = commentRepository.countByTargetIdAndTargetType(comment.getTargetId(), 3);
singer.setCommentCount((int) commentCount);
singerRepository.save(singer);
});
}
// 如果是父评论,同时删除子评论 // 如果是父评论,同时删除子评论
if (comment.getParentId() == null) { if (comment.getParentId() == null) {
List<Comment> children = commentRepository.findByParentIdOrderByCreateTimeAsc(id); List<Comment> children = commentRepository.findByParentIdOrderByCreateTimeAsc(id);
@@ -223,6 +176,9 @@ public class CommentServiceImpl implements CommentService {
} }
} }
// 删除完成后再同步评论数,避免父评论删除后子评论残留导致统计偏大
updateTargetCommentCount(comment.getTargetType(), comment.getTargetId());
// 通过 WebSocket 广播删除事件 // 通过 WebSocket 广播删除事件
String destination = String.format("/topic/comments/%d/%d", comment.getTargetType(), comment.getTargetId()); String destination = String.format("/topic/comments/%d/%d", comment.getTargetType(), comment.getTargetId());
Map<String, Object> deleteEvent = new HashMap<>(); Map<String, Object> deleteEvent = new HashMap<>();
@@ -234,6 +190,31 @@ public class CommentServiceImpl implements CommentService {
return true; return true;
} }
private void updateTargetCommentCount(Integer targetType, Long targetId) {
if (targetType == null || targetId == null) {
return;
}
long commentCount = commentRepository.countByTargetIdAndTargetType(targetId, targetType);
if (targetType == 1) {
musicRepository.findById(targetId).ifPresent(music -> {
music.setCommentCount((int) commentCount);
musicRepository.save(music);
});
} else if (targetType == 2) {
playlistRepository.findById(targetId).ifPresent(playlist -> {
playlist.setCommentCount((int) commentCount);
playlistRepository.save(playlist);
});
} else if (targetType == 3) {
singerRepository.findById(targetId).ifPresent(singer -> {
singer.setCommentCount((int) commentCount);
singerRepository.save(singer);
});
}
}
@Override @Override
public PageResult<CommentDTO> getUserCommentList(Long userId, Integer page, Integer size) { public PageResult<CommentDTO> getUserCommentList(Long userId, Integer page, Integer size) {
// 获取用户 // 获取用户

View File

@@ -243,6 +243,14 @@ public class MusicServiceImpl implements MusicService {
public CompletableFuture<List<MusicDTO>> updateHotMusicCache() { public CompletableFuture<List<MusicDTO>> updateHotMusicCache() {
// 使用 JOIN FETCH 一次性加载所有音乐及其关联的歌手信息 // 使用 JOIN FETCH 一次性加载所有音乐及其关联的歌手信息
List<Music> allMusic = musicRepository.findAllWithSingers(); List<Music> allMusic = musicRepository.findAllWithSingers();
Comparator<Music> hotMusicComparator = Comparator
.comparing((Music music) -> music.getHotScore() != null ? music.getHotScore() : 0D, Comparator.reverseOrder())
.thenComparing(music -> music.getPlayCount() != null ? music.getPlayCount() : 0L, Comparator.reverseOrder())
.thenComparing(music -> music.getCollectCount() != null ? music.getCollectCount() : 0, Comparator.reverseOrder())
.thenComparing(music -> music.getLikeCount() != null ? music.getLikeCount() : 0, Comparator.reverseOrder())
.thenComparing(music -> music.getCommentCount() != null ? music.getCommentCount() : 0, Comparator.reverseOrder())
.thenComparing(Music::getCreateTime, Comparator.reverseOrder())
.thenComparing(Music::getId, Comparator.reverseOrder());
// 使用并行流来加速计算过程 // 使用并行流来加速计算过程
allMusic.parallelStream().forEach(music -> { allMusic.parallelStream().forEach(music -> {
@@ -252,7 +260,7 @@ public class MusicServiceImpl implements MusicService {
// 对所有音乐按计算出的热度分进行降序排序 // 对所有音乐按计算出的热度分进行降序排序
List<MusicDTO> sortedHotMusic = allMusic.stream() List<MusicDTO> sortedHotMusic = allMusic.stream()
.sorted(Comparator.comparing(Music::getHotScore).reversed()) .sorted(hotMusicComparator)
.limit(HOT_MUSIC_LIMIT) // 取热度最高的前50 .limit(HOT_MUSIC_LIMIT) // 取热度最高的前50
.map(this::convertToDTO) .map(this::convertToDTO)
.collect(Collectors.toList()); .collect(Collectors.toList());

View File

@@ -9,6 +9,8 @@ import java.time.LocalDateTime;
*/ */
public class HotScoreUtil { public class HotScoreUtil {
// 播放量采用对数缩放,避免高播放量对热门榜产生碾压
private static final double PLAY_WEIGHT = 1.2;
// 定义各种交互的权重 // 定义各种交互的权重
private static final double LIKE_WEIGHT = 1.0; private static final double LIKE_WEIGHT = 1.0;
private static final double COLLECT_WEIGHT = 2.0; private static final double COLLECT_WEIGHT = 2.0;
@@ -27,17 +29,24 @@ public class HotScoreUtil {
public static double calculateHotScore(Music music) { public static double calculateHotScore(Music music) {
// 1. 计算交互权重和 // 1. 计算交互权重和
// 如果交互数据为 null则默认为 0 // 如果交互数据为 null则默认为 0
long playCount = music.getPlayCount() != null ? music.getPlayCount() : 0;
long likeCount = music.getLikeCount() != null ? music.getLikeCount() : 0; long likeCount = music.getLikeCount() != null ? music.getLikeCount() : 0;
long collectCount = music.getCollectCount() != null ? music.getCollectCount() : 0; long collectCount = music.getCollectCount() != null ? music.getCollectCount() : 0;
long commentCount = music.getCommentCount() != null ? music.getCommentCount() : 0; long commentCount = music.getCommentCount() != null ? music.getCommentCount() : 0;
double interactionScore = (likeCount * LIKE_WEIGHT) + double playScore = Math.log1p(playCount) * PLAY_WEIGHT;
double interactionScore = playScore +
(likeCount * LIKE_WEIGHT) +
(collectCount * COLLECT_WEIGHT) + (collectCount * COLLECT_WEIGHT) +
(commentCount * COMMENT_WEIGHT); (commentCount * COMMENT_WEIGHT);
// 2. 计算时间衰减因子 // 2. 计算时间衰减因子
// (当前时间 - 创建时间)的小时数 // (当前时间 - 创建时间)的小时数
LocalDateTime createTime = music.getCreateTime(); LocalDateTime createTime = music.getCreateTime();
if (createTime == null) {
createTime = LocalDateTime.now();
}
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
long hoursElapsed = Duration.between(createTime, now).toHours(); long hoursElapsed = Duration.between(createTime, now).toHours();