diff --git a/music-houduan/src/main/java/com/test/musichouduan/MusicHouduanApplication.java b/music-houduan/src/main/java/com/test/musichouduan/MusicHouduanApplication.java index 668ac50..4a8134a 100644 --- a/music-houduan/src/main/java/com/test/musichouduan/MusicHouduanApplication.java +++ b/music-houduan/src/main/java/com/test/musichouduan/MusicHouduanApplication.java @@ -2,6 +2,7 @@ package com.test.musichouduan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -11,6 +12,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication @EnableTransactionManagement @EnableScheduling +@EnableAsync public class MusicHouduanApplication { public static void main(String[] args) { diff --git a/music-houduan/src/main/java/com/test/musichouduan/controller/HotMusicController.java b/music-houduan/src/main/java/com/test/musichouduan/controller/HotMusicController.java new file mode 100644 index 0000000..dc44079 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/controller/HotMusicController.java @@ -0,0 +1,54 @@ +package com.test.musichouduan.controller; + +import com.test.musichouduan.dto.MusicDTO; +import com.test.musichouduan.dto.Result; +import com.test.musichouduan.service.MusicService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 热门音乐控制器 + */ +@RestController +@RequestMapping("/api/hot-music") +public class HotMusicController { + + @Autowired + private MusicService musicService; + + /** + * 获取基于新热度算法的热门音乐列表 + * + * @param limit 限制数量 + * @return 响应结果 + */ + /** + * 获取基于新热度算法的热门音乐列表 + * + * @param limit 限制数量 + * @return 响应结果 + */ + @GetMapping("/list") + public Result> getHotMusic(@RequestParam(defaultValue = "10") Integer limit) { + // 调用 service 层的新方法,该方法会优先从 Redis 缓存获取 + List musicList = musicService.getHotMusic(limit); + return Result.success(musicList); + } + + /** + * 手动触发一次热门音乐缓存的更新。 + * 主要用于测试或紧急情况。 + * @return 响应结果 + */ + @GetMapping("/refresh-cache") + public Result refreshHotMusicCache() { + // 异步执行,立即返回,不阻塞当前请求 + musicService.updateHotMusicCache(); + return Result.success("热门音乐缓存更新任务已触发,请稍后查看结果。"); + } +} diff --git a/music-houduan/src/main/java/com/test/musichouduan/entity/Music.java b/music-houduan/src/main/java/com/test/musichouduan/entity/Music.java index 64a6421..f6e14c4 100644 --- a/music-houduan/src/main/java/com/test/musichouduan/entity/Music.java +++ b/music-houduan/src/main/java/com/test/musichouduan/entity/Music.java @@ -64,6 +64,30 @@ public class Music { @Column private Long playCount; + /** + * 点赞数 + */ + @Column + private Integer likeCount; + + /** + * 收藏数 + */ + @Column + private Integer collectCount; + + /** + * 评论数 + */ + @Column + private Integer commentCount; + + /** + * 热度分数 + */ + @Column + private Double hotScore; + /** * 歌手列表 */ diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/MusicService.java b/music-houduan/src/main/java/com/test/musichouduan/service/MusicService.java index 86f49e4..658eb7c 100644 --- a/music-houduan/src/main/java/com/test/musichouduan/service/MusicService.java +++ b/music-houduan/src/main/java/com/test/musichouduan/service/MusicService.java @@ -6,6 +6,7 @@ import com.test.musichouduan.entity.Music; import org.springframework.web.multipart.MultipartFile; import java.util.List; +import java.util.concurrent.CompletableFuture; /** * 音乐服务接口 @@ -121,4 +122,15 @@ public interface MusicService { * @return 音乐实体 */ Music getById(Long id); + + /** + * 定时任务接口,用于更新热门音乐缓存 + */ + void scheduleUpdateHotMusic(); + + /** + * 异步更新热门音乐缓存的接口 + * @return CompletableFuture 包装的音乐DTO列表 + */ + CompletableFuture> updateHotMusicCache(); } diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/impl/MusicServiceImpl.java b/music-houduan/src/main/java/com/test/musichouduan/service/impl/MusicServiceImpl.java index 02acb1d..4e6dea6 100644 --- a/music-houduan/src/main/java/com/test/musichouduan/service/impl/MusicServiceImpl.java +++ b/music-houduan/src/main/java/com/test/musichouduan/service/impl/MusicServiceImpl.java @@ -9,21 +9,29 @@ import com.test.musichouduan.entity.Singer; import com.test.musichouduan.entity.User; import com.test.musichouduan.entity.UserMusic; import com.test.musichouduan.exception.BusinessException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import com.test.musichouduan.repository.*; import com.test.musichouduan.service.FileService; import com.test.musichouduan.service.MusicService; +import com.test.musichouduan.util.HotScoreUtil; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; +import org.springframework.data.domain.*; import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; +import java.util.Comparator; import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import jakarta.persistence.criteria.Predicate; import java.util.ArrayList; @@ -38,9 +46,22 @@ import java.util.stream.Collectors; @Service public class MusicServiceImpl implements MusicService { + // Redis 缓存键 + private static final String HOT_MUSIC_CACHE_KEY = "music:hot_list"; + // Redis 缓存过期时间(小时) + private static final long CACHE_EXPIRATION_HOURS = 2; + // 热门音乐列表缓存数量 + private static final int HOT_MUSIC_LIMIT = 50; + @Autowired private MusicRepository musicRepository; + @Autowired + private StringRedisTemplate redisTemplate; + + @Autowired + private ObjectMapper objectMapper; + @Autowired private MusicTypeRepository musicTypeRepository; @@ -146,13 +167,105 @@ public class MusicServiceImpl implements MusicService { @Override public List getHotMusic(Integer limit) { - // 查询热门音乐 - List musics = musicRepository.findMostPlayed(PageRequest.of(0, limit)); + // 1. 尝试从 Redis 缓存中获取热门音乐列表 + String hotMusicJson = redisTemplate.opsForValue().get(HOT_MUSIC_CACHE_KEY); - // 转换为DTO - return musics.stream() + if (StringUtils.hasText(hotMusicJson)) { + try { + // 如果缓存命中,则反序列化并返回 + List cachedList = objectMapper.readValue(hotMusicJson, new TypeReference>() {}); + // 根据请求的 limit 参数返回相应数量的音乐 + return cachedList.stream().limit(limit).collect(Collectors.toList()); + } catch (Exception e) { + // 日志记录反序列化失败 + System.err.println("Failed to deserialize hot music list from Redis: " + e.getMessage()); + } + } + + // 2. 如果缓存未命中或反序列化失败,则立即计算并阻塞等待结果 + // 这是一个同步调用,确保首次请求能拿到数据 + List hotList = calculateAndCacheHotMusicSync(); + return hotList.stream().limit(limit).collect(Collectors.toList()); + } + + /** + * 同步计算并缓存热门音乐列表。 + * 用于在缓存失效时,立即为用户提供数据。 + * @return 计算后的热门音乐列表 + */ + private List calculateAndCacheHotMusicSync() { + try { + // 调用异步方法并等待其完成 + return updateHotMusicCache().get(); + } catch (Exception e) { + // 在异常情况下,返回一个降级方案:例如按播放量排序的列表 + System.err.println("Failed to calculate hot music list synchronously: " + e.getMessage()); + List musics = musicRepository.findMostPlayed(PageRequest.of(0, HOT_MUSIC_LIMIT)); + return musics.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + } + + /** + * 定时任务,定期更新热门音乐缓存。 + * cron表达式表示每小时的0分执行 (e.g., 1:00, 2:00, 3:00) + */ + @Scheduled(cron = "0 0 * * * ?") + @Override + public void scheduleUpdateHotMusic() { + System.out.println("Scheduled task started: Updating hot music cache..."); + updateHotMusicCache(); + } + + /** + * 异步计算所有音乐的热度,并将热度最高的前50首缓存到Redis。 + * 使用 @Async 注解使其在独立的线程中执行。 + * @return 返回一个 CompletableFuture,包含计算后的热门音乐列表 + */ + @Async + @Transactional(readOnly = true) // 使用只读事务,提高查询性能 + @Override + public CompletableFuture> updateHotMusicCache() { + // 为了处理大数据量,我们使用分页查询来代替一次性加载所有数据 + List allMusic = new ArrayList<>(); + Page musicPage; + int pageNum = 0; + int pageSize = 500; // 每次查询500条 + + do { + Pageable pageable = PageRequest.of(pageNum, pageSize); + musicPage = musicRepository.findAll(pageable); + allMusic.addAll(musicPage.getContent()); + pageNum++; + } while (musicPage.hasNext()); + + // 使用并行流来加速计算过程 + allMusic.parallelStream().forEach(music -> { + double score = HotScoreUtil.calculateHotScore(music); + music.setHotScore(score); + }); + + // 对所有音乐按计算出的热度分进行降序排序 + List sortedHotMusic = allMusic.stream() + .sorted(Comparator.comparing(Music::getHotScore).reversed()) + .limit(HOT_MUSIC_LIMIT) // 取热度最高的前50 .map(this::convertToDTO) .collect(Collectors.toList()); + + try { + // 将排序后的列表序列化为JSON字符串 + String jsonToCache = objectMapper.writeValueAsString(sortedHotMusic); + // 存入Redis,并设置过期时间 + redisTemplate.opsForValue().set(HOT_MUSIC_CACHE_KEY, jsonToCache, CACHE_EXPIRATION_HOURS, TimeUnit.HOURS); + System.out.println("Hot music cache updated successfully with " + sortedHotMusic.size() + " items."); + } catch (Exception e) { + // 日志记录序列化或Redis操作失败 + System.err.println("Failed to cache hot music list to Redis: " + e.getMessage()); + } + + // 返回一个已完成的 CompletableFuture + return CompletableFuture.completedFuture(sortedHotMusic); } @Override diff --git a/music-houduan/src/main/java/com/test/musichouduan/util/HotScoreUtil.java b/music-houduan/src/main/java/com/test/musichouduan/util/HotScoreUtil.java new file mode 100644 index 0000000..3b1c3e5 --- /dev/null +++ b/music-houduan/src/main/java/com/test/musichouduan/util/HotScoreUtil.java @@ -0,0 +1,51 @@ +package com.test.musichouduan.util; + +import com.test.musichouduan.entity.Music; +import java.time.Duration; +import java.time.LocalDateTime; + +/** + * 热度分数计算工具类 + */ +public class HotScoreUtil { + + // 定义各种交互的权重 + private static final double LIKE_WEIGHT = 1.0; + private static final double COLLECT_WEIGHT = 2.0; + private static final double COMMENT_WEIGHT = 1.5; + + // 时间衰减因子中的指数,可以调整以改变时间衰减的速度 + private static final double TIME_DECAY_EXPONENT = 1.8; + + /** + * 计算单首音乐的热度分数 + * 采用类似 Hacker News 的热度算法: 热度 = (交互权重和) / (时间衰减因子) + * + * @param music 音乐实体对象 + * @return 计算出的热度分数 + */ + public static double calculateHotScore(Music music) { + // 1. 计算交互权重和 + // 如果交互数据为 null,则默认为 0 + long likeCount = music.getLikeCount() != null ? music.getLikeCount() : 0; + long collectCount = music.getCollectCount() != null ? music.getCollectCount() : 0; + long commentCount = music.getCommentCount() != null ? music.getCommentCount() : 0; + + double interactionScore = (likeCount * LIKE_WEIGHT) + + (collectCount * COLLECT_WEIGHT) + + (commentCount * COMMENT_WEIGHT); + + // 2. 计算时间衰减因子 + // (当前时间 - 创建时间)的小时数 + LocalDateTime createTime = music.getCreateTime(); + LocalDateTime now = LocalDateTime.now(); + long hoursElapsed = Duration.between(createTime, now).toHours(); + + // 为了避免新发布内容因分母过小(接近0)而导致分数无限大的问题, + // 我们给分母加上一个常数(这里是2),确保其初始值不会太小。 + double timeDecayFactor = Math.pow(hoursElapsed + 2, TIME_DECAY_EXPONENT); + + // 3. 计算最终热度分数 + return interactionScore / timeDecayFactor; + } +} diff --git a/sql/music_db.sql b/sql/music_db.sql index 00aa587..20ee2eb 100644 --- a/sql/music_db.sql +++ b/sql/music_db.sql @@ -61,17 +61,25 @@ CREATE TABLE `comment` ( -- ---------------------------- DROP TABLE IF EXISTS `music`; CREATE TABLE `music` ( - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '音乐ID', - `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '音乐名称', - `cover` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '封面URL', - `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '音乐文件URL', - `lyric` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '歌词', - `duration` int NULL DEFAULT 0 COMMENT '时长(秒)', - `play_count` bigint NULL DEFAULT 0 COMMENT '播放次数', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `album` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, - PRIMARY KEY (`id`) USING BTREE +`id` bigint NOT NULL AUTO_INCREMENT COMMENT '音乐ID', +`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '音乐名称', +`cover` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '封面URL', +`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '音乐文件URL', +`lyric` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '歌词', +`duration` int NULL DEFAULT 0 COMMENT '时长(秒)', +`album` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + + -- Interaction and Hot Score Fields +`play_count` bigint NULL DEFAULT 0 COMMENT '播放次数', +`like_count` int NULL DEFAULT 0 COMMENT '点赞数', +`collect_count` int NULL DEFAULT 0 COMMENT '收藏数', +`comment_count` int NULL DEFAULT 0 COMMENT '评论数', +`hot_score` double NULL DEFAULT 0.0 COMMENT '热度分数', + + -- Timestamps +`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', +`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', +PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '音乐表' ROW_FORMAT = Dynamic; -- ----------------------------