refactor(comment): 优化评论功能实现并改进热门音乐缓存更新机制
- 移除CommentController中多余的权限注解导入 - 在CommentController中注入MusicService用于缓存更新 - 添加评论和删除评论时触发热门音乐缓存更新 - 提取评论数更新逻辑到独立方法updateTargetCommentCount - 删除评论时在最后同步评论数避免统计偏大 - 优化HotScoreUtil中播放量权重算法采用对数缩放 - 在MusicController的播放、收藏、点赞操作后更新热门音乐缓存 - 改进MusicRepository查询排序避免数据重复 - 重构MusicServiceImpl中热门音乐排序逻辑增强稳定性
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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) {
|
||||||
// 获取用户
|
// 获取用户
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user