diff --git a/music-houduan/src/main/java/com/test/musichouduan/service/impl/PlaylistServiceImpl.java b/music-houduan/src/main/java/com/test/musichouduan/service/impl/PlaylistServiceImpl.java index 5aded12..5a19fb3 100644 --- a/music-houduan/src/main/java/com/test/musichouduan/service/impl/PlaylistServiceImpl.java +++ b/music-houduan/src/main/java/com/test/musichouduan/service/impl/PlaylistServiceImpl.java @@ -1,688 +1,689 @@ -package com.test.musichouduan.service.impl; - -import cn.dev33.satoken.stp.StpUtil; -import com.test.musichouduan.dto.MusicDTO; -import com.test.musichouduan.dto.PageResult; -import com.test.musichouduan.dto.PlaylistDTO; -import com.test.musichouduan.dto.UserDTO; -import com.test.musichouduan.entity.Music; -import com.test.musichouduan.entity.Playlist; -import com.test.musichouduan.entity.PlaylistType; -import com.test.musichouduan.entity.User; -// No extra imports needed -import com.test.musichouduan.exception.BusinessException; -import com.test.musichouduan.repository.*; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.test.musichouduan.service.FileService; -import com.test.musichouduan.service.PlaylistService; -import com.test.musichouduan.util.HotPlaylistScoreUtil; -import org.springframework.beans.BeanUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -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.data.domain.PageRequest; -import org.springframework.data.domain.Sort; -import org.springframework.data.jpa.domain.Specification; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; -import org.springframework.web.multipart.MultipartFile; - -import jakarta.persistence.criteria.Predicate; -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -/** - * 歌单服务实现类 - */ -@Service -public class PlaylistServiceImpl implements PlaylistService { - - private static final String HOT_PLAYLIST_CACHE_KEY = "playlist:hot_list"; - private static final long CACHE_EXPIRATION_HOURS = 2; - private static final int HOT_PLAYLIST_LIMIT = 50; - - @Autowired - private StringRedisTemplate redisTemplate; - - @Autowired - private ObjectMapper objectMapper; - - @Autowired - private PlaylistRepository playlistRepository; - - @Autowired - private PlaylistTypeRepository playlistTypeRepository; - - @Autowired - private MusicRepository musicRepository; - - @Autowired - private UserRepository userRepository; - - @Autowired - private UserCollectPlaylistRepository userCollectPlaylistRepository; - - @Autowired - private PlayHistoryRepository playHistoryRepository; - - @Autowired - private CommentRepository commentRepository; - - @Autowired - private FileService fileService; - - @Override - /** - * 根据歌单ID获取歌单信息 - * @param id 歌单的唯一标识符 - * @return PlaylistDTO 歌单的数据传输对象,包含歌单的详细信息 - * @throws BusinessException 当指定的歌单不存在时抛出业务异常 - */ - public PlaylistDTO getPlaylistById(Long id) { - // 从数据库中查找指定ID的歌单,如果不存在则抛出业务异常 - Playlist playlist = playlistRepository.findById(id) - .orElseThrow(() -> new BusinessException("歌单不存在")); - // 将实体对象转换为DTO对象并返回 - return convertToDTO(playlist); - } - - @Override - /** - * 获取歌单列表 - * @param page 当前页码 - * @param size 每页大小 - * @param keyword 搜索关键词 - * @param typeId 歌单类型ID - * @return PageResult 歌单列表分页结果 - */ - public PageResult getPlaylistList(Integer page, Integer size, String keyword, Long typeId) { - // 构建查询条件 - Specification spec = (root, query, cb) -> { - List predicates = new ArrayList<>(); - query.distinct(true); // 添加 distinct 查询,避免重复结果 - - // 关键字查询 - if (StringUtils.hasText(keyword)) { - // 搜索歌单名、描述、创建者用户名以及歌单内的音乐名 - Predicate p1 = cb.like(root.get("name"), "%" + keyword + "%"); // 匹配歌单名 - Predicate p2 = cb.like(root.get("description"), "%" + keyword + "%"); // 匹配歌单描述 - Predicate p3 = cb.like(root.join("creator").get("username"), "%" + keyword + "%"); // 匹配创建者用户名 - Predicate p4 = cb.like(root.join("musics", jakarta.persistence.criteria.JoinType.LEFT).get("name"), "%" + keyword + "%"); // 匹配歌单内的音乐名 - predicates.add(cb.or(p1, p2, p3, p4)); // 将多个查询条件组合为OR关系 - } - - // 类型查询 - if (typeId != null) { - predicates.add(cb.equal(root.get("type").get("id"), typeId)); // 添加类型ID查询条件 - } - - return cb.and(predicates.toArray(new Predicate[0])); // 将所有查询条件组合为AND关系 - }; - - // 执行查询,按创建时间降序排列 - Page playlistPage = playlistRepository.findAll( - spec, - PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime")) - ); - - // 转换为DTO - List playlistDTOs = playlistPage.getContent().stream() - .map(this::convertToDTO) - .collect(Collectors.toList()); - - return new PageResult<>( - playlistDTOs, - playlistPage.getTotalElements(), - page, - size - ); - } - - @Override - public List getLatestPlaylist(Integer limit) { - // 查询最新歌单 - List playlists = playlistRepository.findLatest(PageRequest.of(0, limit)); - - // 转换为DTO - return playlists.stream() - .map(this::convertToDTO) - .collect(Collectors.toList()); - } - - @Override - public List getHotPlaylist(Integer limit) { - String hotPlaylistJson = redisTemplate.opsForValue().get(HOT_PLAYLIST_CACHE_KEY); - - if (StringUtils.hasText(hotPlaylistJson)) { - try { - List cachedList = objectMapper.readValue(hotPlaylistJson, new TypeReference>() {}); - return cachedList.stream().limit(limit).collect(Collectors.toList()); - } catch (Exception e) { - System.err.println("Failed to deserialize hot playlist list from Redis: " + e.getMessage()); - } - } - - List hotList = calculateAndCacheHotPlaylistSync(); - return hotList.stream().limit(limit).collect(Collectors.toList()); - } - - private List calculateAndCacheHotPlaylistSync() { - try { - return updateHotPlaylistCache().get(); - } catch (Exception e) { - System.err.println("Failed to calculate hot playlist list synchronously: " + e.getMessage()); - List playlists = playlistRepository.findMostPlayed(PageRequest.of(0, HOT_PLAYLIST_LIMIT)); - return playlists.stream() - .map(this::convertToDTO) - .collect(Collectors.toList()); - } - } - -/** - * 定时任务方法:用于更新热门歌单缓存 - * 使用@Scheduled注解配置定时任务,cron表达式设置为"0 0 * * * ?" - * 表示每小时整点执行一次任务(即每小时的0分0秒触发) - */ - @Scheduled(cron = "0 0 * * * ?") - public void scheduleUpdateHotPlaylist() { - // 打印任务开始日志 - System.out.println("Scheduled task started: Updating hot playlist cache..."); - // 调用更新热门歌单缓存的方法 - updateHotPlaylistCache(); - } - -/** - * 更新热门歌单缓存的方法 - * 使用异步方式和只读事务注解 - * @return 返回包含热门歌单DTO列表的CompletableFuture - */ - @Async - @Transactional(readOnly = true) - public CompletableFuture> updateHotPlaylistCache() { - // 用于存储所有歌单的列表 - List allPlaylists = new ArrayList<>(); - // 分页查询结果对象 - Page playlistPage; - // 当前页码,从0开始 - int pageNum = 0; - // 每页大小,设置为500 - int pageSize = 500; - - // 分页查询所有歌单,直到没有更多数据 - do { - // 创建分页对象 - Pageable pageable = PageRequest.of(pageNum, pageSize); - // 执行分页查询 - playlistPage = playlistRepository.findAll(pageable); - // 将当前页的歌单添加到总列表中 - allPlaylists.addAll(playlistPage.getContent()); - // 页码递增 - pageNum++; - } while (playlistPage.hasNext()); - - // 使用并行流为每个歌单计算热门分数 - allPlaylists.parallelStream().forEach(playlist -> { - // 计算热门歌单分数 - double score = HotPlaylistScoreUtil.calculateHotScore(playlist); - // 设置歌单的热门分数 - playlist.setHotScore(score); - }); - - // 将歌单按热门分数降序排序,并限制热门歌单数量 - List sortedHotPlaylists = allPlaylists.stream() - // 按热门分数降序排序 - .sorted(Comparator.comparing(Playlist::getHotScore).reversed()) - // 限制热门歌单数量 - .limit(HOT_PLAYLIST_LIMIT) - // 将歌单实体转换为DTO对象 - .map(this::convertToDTO) - // 收集为列表 - .collect(Collectors.toList()); - - try { - // 将热门歌单列表转换为JSON字符串 - String jsonToCache = objectMapper.writeValueAsString(sortedHotPlaylists); - // 将JSON字符串存入Redis缓存,设置过期时间 - redisTemplate.opsForValue().set(HOT_PLAYLIST_CACHE_KEY, jsonToCache, CACHE_EXPIRATION_HOURS, TimeUnit.HOURS); - // 打印更新成功的日志 - System.out.println("Hot playlist cache updated successfully with " + sortedHotPlaylists.size() + " items."); - } catch (Exception e) { - // 打印更新失败的错误信息 - System.err.println("Failed to cache hot playlist list to Redis: " + e.getMessage()); - } - - // 返回包含热门歌单DTO列表的CompletableFuture - return CompletableFuture.completedFuture(sortedHotPlaylists); - } - - @Override - public PageResult getOfficialPlaylist(Integer page, Integer size) { - // 查询官方歌单 - Page playlistPage = playlistRepository.findByIsOfficial( - 1, - PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime")) - ); - - // 转换为DTO - List playlistDTOs = playlistPage.getContent().stream() - .map(this::convertToDTO) - .collect(Collectors.toList()); - - return new PageResult<>( - playlistDTOs, - playlistPage.getTotalElements(), - page, - size - ); - } - - @Override - @Transactional - public PlaylistDTO addPlaylist(PlaylistDTO playlistDTO, MultipartFile cover) { - // 获取当前登录用户 - Long userId = StpUtil.getLoginIdAsLong(); - User user = userRepository.findById(userId) - .orElseThrow(() -> new BusinessException("用户不存在")); - - // 上传封面 - String coverUrl = null; - if (cover != null && !cover.isEmpty()) { - String coverPhysicalPath = fileService.uploadImage(cover); - coverUrl = coverPhysicalPath.replace(System.getProperty("user.dir") + File.separator + "music-houduan", "").replace(File.separator, "/"); - } - - // 创建歌单实体 - Playlist playlist = new Playlist(); - playlist.setName(playlistDTO.getName()); - playlist.setDescription(playlistDTO.getDescription()); - playlist.setCover(coverUrl); - playlist.setCreator(user); - playlist.setPlayCount(0L); - playlist.setCollectCount(0); - playlist.setIsOfficial(0); // 默认为非官方歌单 - - // 设置歌单类型 - if (playlistDTO.getTypeId() != null) { - PlaylistType type = playlistTypeRepository.findById(playlistDTO.getTypeId()) - .orElseThrow(() -> new BusinessException("歌单类型不存在")); - playlist.setType(type); - } - - // 保存歌单 - Playlist savedPlaylist = playlistRepository.save(playlist); - - return convertToDTO(savedPlaylist); - } - - @Override - @Transactional - public PlaylistDTO updatePlaylist(Long id, PlaylistDTO playlistDTO, MultipartFile cover) { - // 获取歌单 - Playlist playlist = playlistRepository.findById(id) - .orElseThrow(() -> new BusinessException("歌单不存在")); - - // 获取当前登录用户 - Long userId = StpUtil.getLoginIdAsLong(); - - // 检查是否是歌单创建者或管理员 - if (!playlist.getCreator().getId().equals(userId) && !StpUtil.hasRole("admin")) { - throw new BusinessException("无权修改该歌单"); - } - - // 如果有新的封面,则上传并更新URL - if (cover != null && !cover.isEmpty()) { - // 删除旧封面 - if (playlist.getCover() != null) { - fileService.deleteFile(playlist.getCover()); - } - - // 上传新封面 - String coverPhysicalPath = fileService.uploadImage(cover); - String coverUrl = coverPhysicalPath.replace(System.getProperty("user.dir") + File.separator + "music-houduan", "").replace(File.separator, "/"); - playlist.setCover(coverUrl); - } - - // 更新歌单信息 - playlist.setName(playlistDTO.getName()); - playlist.setDescription(playlistDTO.getDescription()); - - // 更新是否官方字段 - System.out.println("接收到的isOfficial值: " + playlistDTO.getIsOfficial()); - playlist.setIsOfficial(playlistDTO.getIsOfficial()); - System.out.println("更新后的歌单isOfficial值: " + playlist.getIsOfficial()); - - // 更新歌单类型 - if (playlistDTO.getTypeId() != null) { - PlaylistType type = playlistTypeRepository.findById(playlistDTO.getTypeId()) - .orElseThrow(() -> new BusinessException("歌单类型不存在")); - playlist.setType(type); - } - - // 保存歌单 - Playlist updatedPlaylist = playlistRepository.save(playlist); - - return convertToDTO(updatedPlaylist); - } - - @Override - @Transactional - public boolean deletePlaylist(Long id) { - // 获取歌单 - Playlist playlist = playlistRepository.findById(id) - .orElseThrow(() -> new BusinessException("歌单不存在")); - - // 获取当前登录用户 - Long userId = StpUtil.getLoginIdAsLong(); - - // 检查是否是歌单创建者或管理员 - if (!playlist.getCreator().getId().equals(userId) && !StpUtil.hasRole("admin")) { - throw new BusinessException("无权删除该歌单"); - } - - // 删除封面 - if (playlist.getCover() != null) { - fileService.deleteFile(playlist.getCover()); - } - - // 删除相关的收藏记录 - userCollectPlaylistRepository.deleteByPlaylistId(id); - - // 删除歌单 - playlistRepository.delete(playlist); - - return true; - } - - @Override - @Transactional - public boolean collectPlaylist(Long id) { - // 获取当前登录用户 - Long userId = StpUtil.getLoginIdAsLong(); - User user = userRepository.findById(userId) - .orElseThrow(() -> new BusinessException("用户不存在")); - - // 获取歌单 - Playlist playlist = playlistRepository.findById(id) - .orElseThrow(() -> new BusinessException("歌单不存在")); - - // 检查是否已收藏 - if (userCollectPlaylistRepository.existsByUserAndPlaylist(user, playlist)) { - // 已收藏则取消收藏 - userCollectPlaylistRepository.deleteByUserAndPlaylist(user, playlist); - - // 更新收藏数 - playlist.setCollectCount((playlist.getCollectCount() == null ? 0 : playlist.getCollectCount()) - 1); - playlistRepository.save(playlist); - - return false; // 表示取消收藏 - } else { - // 未收藏则进行收藏 - com.test.musichouduan.entity.UserCollectPlaylist userCollectPlaylist = new com.test.musichouduan.entity.UserCollectPlaylist(); - userCollectPlaylist.setUser(user); - userCollectPlaylist.setPlaylist(playlist); - userCollectPlaylistRepository.save(userCollectPlaylist); - - // 更新收藏数 - playlist.setCollectCount((playlist.getCollectCount() == null ? 0 : playlist.getCollectCount()) + 1); - playlistRepository.save(playlist); - - return true; // 表示收藏成功 - } - } - - - @Override - @Transactional - public boolean uncollectPlaylist(Long id) { - // 获取当前登录用户 - Long userId = StpUtil.getLoginIdAsLong(); - User user = userRepository.findById(userId) - .orElseThrow(() -> new BusinessException("用户不存在")); - - // 获取歌单 - Playlist playlist = playlistRepository.findById(id) - .orElseThrow(() -> new BusinessException("歌单不存在")); - - // 检查是否已收藏 - if (!userCollectPlaylistRepository.existsByUserAndPlaylist(user, playlist)) { - throw new BusinessException("未收藏该歌单"); - } - - // 取消收藏歌单 - userCollectPlaylistRepository.deleteByUserAndPlaylist(user, playlist); - - // 更新收藏数 - playlist.setCollectCount((playlist.getCollectCount() == null ? 0 : playlist.getCollectCount()) - 1); - playlistRepository.save(playlist); - - return true; - } - - @Override - public boolean isCollected(Long id) { - if (!StpUtil.isLogin()) { - return false; - } - Long userId = StpUtil.getLoginIdAsLong(); - return userCollectPlaylistRepository.existsByUserIdAndPlaylistId(userId, id); - } - - @Override - public PageResult getCollectedPlaylist(Integer page, Integer size) { - // 获取当前登录用户 - Long userId = StpUtil.getLoginIdAsLong(); - - // 获取用户收藏的歌单 - Page playlistPage = userCollectPlaylistRepository.findCollectedPlaylist( - userId, - PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime")) - ); - - // 转换为DTO - List playlistDTOs = playlistPage.getContent().stream() - .map(playlist -> { - PlaylistDTO dto = convertToDTO(playlist); - // 从收藏记录中获取收藏时间 - userCollectPlaylistRepository.findByUserIdAndPlaylistId(userId, playlist.getId()) - .ifPresent(ucp -> dto.setCollectTime(ucp.getCreateTime())); - return dto; - }) - .collect(Collectors.toList()); - - return new PageResult<>( - playlistDTOs, - playlistPage.getTotalElements(), - page, - size - ); - } - - @Override - public PageResult getUserPlaylist(Integer page, Integer size) { - // 获取当前登录用户 - Long userId = StpUtil.getLoginIdAsLong(); - User user = userRepository.findById(userId) - .orElseThrow(() -> new BusinessException("用户不存在")); - - // 获取用户创建的歌单 - Page playlistPage = playlistRepository.findByCreator( - user, - PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime")) - ); - - // 转换为DTO - List playlistDTOs = playlistPage.getContent().stream() - .map(this::convertToDTO) - .collect(Collectors.toList()); - - return new PageResult<>( - playlistDTOs, - playlistPage.getTotalElements(), - page, - size - ); - } - +package com.test.musichouduan.service.impl; + +import cn.dev33.satoken.stp.StpUtil; +import com.test.musichouduan.dto.MusicDTO; +import com.test.musichouduan.dto.PageResult; +import com.test.musichouduan.dto.PlaylistDTO; +import com.test.musichouduan.dto.UserDTO; +import com.test.musichouduan.entity.Music; +import com.test.musichouduan.entity.Playlist; +import com.test.musichouduan.entity.PlaylistType; +import com.test.musichouduan.entity.User; +// No extra imports needed +import com.test.musichouduan.exception.BusinessException; +import com.test.musichouduan.repository.*; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.test.musichouduan.service.FileService; +import com.test.musichouduan.service.PlaylistService; +import com.test.musichouduan.util.HotPlaylistScoreUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +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.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.persistence.criteria.Predicate; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 歌单服务实现类 + */ +@Service +public class PlaylistServiceImpl implements PlaylistService { + + private static final String HOT_PLAYLIST_CACHE_KEY = "playlist:hot_list"; + private static final long CACHE_EXPIRATION_HOURS = 2; + private static final int HOT_PLAYLIST_LIMIT = 50; + + @Autowired + private StringRedisTemplate redisTemplate; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private PlaylistRepository playlistRepository; + + @Autowired + private PlaylistTypeRepository playlistTypeRepository; + + @Autowired + private MusicRepository musicRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserCollectPlaylistRepository userCollectPlaylistRepository; + + @Autowired + private PlayHistoryRepository playHistoryRepository; + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private FileService fileService; + + @Override + /** + * 根据歌单ID获取歌单信息 + * @param id 歌单的唯一标识符 + * @return PlaylistDTO 歌单的数据传输对象,包含歌单的详细信息 + * @throws BusinessException 当指定的歌单不存在时抛出业务异常 + */ + public PlaylistDTO getPlaylistById(Long id) { + // 从数据库中查找指定ID的歌单,如果不存在则抛出业务异常 + Playlist playlist = playlistRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌单不存在")); + // 将实体对象转换为DTO对象并返回 + return convertToDTO(playlist); + } + + @Override + /** + * 获取歌单列表 + * @param page 当前页码 + * @param size 每页大小 + * @param keyword 搜索关键词 + * @param typeId 歌单类型ID + * @return PageResult 歌单列表分页结果 + */ + public PageResult getPlaylistList(Integer page, Integer size, String keyword, Long typeId) { + // 构建查询条件 + Specification spec = (root, query, cb) -> { + List predicates = new ArrayList<>(); + query.distinct(true); // 添加 distinct 查询,避免重复结果 + + // 关键字查询 + if (StringUtils.hasText(keyword)) { + // 搜索歌单名、描述、创建者用户名以及歌单内的音乐名 + Predicate p1 = cb.like(root.get("name"), "%" + keyword + "%"); // 匹配歌单名 + Predicate p2 = cb.like(root.get("description"), "%" + keyword + "%"); // 匹配歌单描述 + Predicate p3 = cb.like(root.join("creator").get("username"), "%" + keyword + "%"); // 匹配创建者用户名 + Predicate p4 = cb.like(root.join("musics", jakarta.persistence.criteria.JoinType.LEFT).get("name"), "%" + keyword + "%"); // 匹配歌单内的音乐名 + predicates.add(cb.or(p1, p2, p3, p4)); // 将多个查询条件组合为OR关系 + } + + // 类型查询 + if (typeId != null) { + predicates.add(cb.equal(root.get("type").get("id"), typeId)); // 添加类型ID查询条件 + } + + return cb.and(predicates.toArray(new Predicate[0])); // 将所有查询条件组合为AND关系 + }; + + // 执行查询,按创建时间降序排列 + Page playlistPage = playlistRepository.findAll( + spec, + PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime")) + ); + + // 转换为DTO + List playlistDTOs = playlistPage.getContent().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + + return new PageResult<>( + playlistDTOs, + playlistPage.getTotalElements(), + page, + size + ); + } + + @Override + public List getLatestPlaylist(Integer limit) { + // 查询最新歌单 + List playlists = playlistRepository.findLatest(PageRequest.of(0, limit)); + + // 转换为DTO + return playlists.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + @Override + public List getHotPlaylist(Integer limit) { + String hotPlaylistJson = redisTemplate.opsForValue().get(HOT_PLAYLIST_CACHE_KEY); + + if (StringUtils.hasText(hotPlaylistJson)) { + try { + List cachedList = objectMapper.readValue(hotPlaylistJson, new TypeReference>() {}); + return cachedList.stream().limit(limit).collect(Collectors.toList()); + } catch (Exception e) { + System.err.println("Failed to deserialize hot playlist list from Redis: " + e.getMessage()); + } + } + + List hotList = calculateAndCacheHotPlaylistSync(); + return hotList.stream().limit(limit).collect(Collectors.toList()); + } + + private List calculateAndCacheHotPlaylistSync() { + try { + return updateHotPlaylistCache().get(); + } catch (Exception e) { + System.err.println("Failed to calculate hot playlist list synchronously: " + e.getMessage()); + List playlists = playlistRepository.findMostPlayed(PageRequest.of(0, HOT_PLAYLIST_LIMIT)); + return playlists.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + } + +/** + * 定时任务方法:用于更新热门歌单缓存 + * 使用@Scheduled注解配置定时任务,cron表达式设置为"0 0 * * * ?" + * 表示每小时整点执行一次任务(即每小时的0分0秒触发) + */ + @Scheduled(cron = "0 0 * * * ?") + public void scheduleUpdateHotPlaylist() { + // 打印任务开始日志 + System.out.println("Scheduled task started: Updating hot playlist cache..."); + // 调用更新热门歌单缓存的方法 + updateHotPlaylistCache(); + } + +/** + * 更新热门歌单缓存的方法 + * 使用异步方式和只读事务注解 + * @return 返回包含热门歌单DTO列表的CompletableFuture + */ + @Async + @Transactional(readOnly = true) + public CompletableFuture> updateHotPlaylistCache() { + // 用于存储所有歌单的列表 + List allPlaylists = new ArrayList<>(); + // 分页查询结果对象 + Page playlistPage; + // 当前页码,从0开始 + int pageNum = 0; + // 每页大小,设置为500 + int pageSize = 500; + + // 分页查询所有歌单,直到没有更多数据 + do { + // 创建分页对象 + Pageable pageable = PageRequest.of(pageNum, pageSize); + // 执行分页查询 + playlistPage = playlistRepository.findAll(pageable); + // 将当前页的歌单添加到总列表中 + allPlaylists.addAll(playlistPage.getContent()); + // 页码递增 + pageNum++; + } while (playlistPage.hasNext()); + + // 使用并行流为每个歌单计算热门分数 + allPlaylists.parallelStream().forEach(playlist -> { + // 计算热门歌单分数 + double score = HotPlaylistScoreUtil.calculateHotScore(playlist); + // 设置歌单的热门分数 + playlist.setHotScore(score); + }); + + // 将歌单按热门分数降序排序,并限制热门歌单数量 + List sortedHotPlaylists = allPlaylists.stream() + // 按热门分数降序排序 + .sorted(Comparator.comparing(Playlist::getHotScore).reversed()) + // 限制热门歌单数量 + .limit(HOT_PLAYLIST_LIMIT) + // 将歌单实体转换为DTO对象 + .map(this::convertToDTO) + // 收集为列表 + .collect(Collectors.toList()); + + try { + // 将热门歌单列表转换为JSON字符串 + String jsonToCache = objectMapper.writeValueAsString(sortedHotPlaylists); + // 将JSON字符串存入Redis缓存,设置过期时间 + redisTemplate.opsForValue().set(HOT_PLAYLIST_CACHE_KEY, jsonToCache, CACHE_EXPIRATION_HOURS, TimeUnit.HOURS); + // 打印更新成功的日志 + System.out.println("Hot playlist cache updated successfully with " + sortedHotPlaylists.size() + " items."); + } catch (Exception e) { + // 打印更新失败的错误信息 + System.err.println("Failed to cache hot playlist list to Redis: " + e.getMessage()); + } + + // 返回包含热门歌单DTO列表的CompletableFuture + return CompletableFuture.completedFuture(sortedHotPlaylists); + } + + @Override + public PageResult getOfficialPlaylist(Integer page, Integer size) { + // 查询官方歌单 + Page playlistPage = playlistRepository.findByIsOfficial( + 1, + PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime")) + ); + + // 转换为DTO + List playlistDTOs = playlistPage.getContent().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + + return new PageResult<>( + playlistDTOs, + playlistPage.getTotalElements(), + page, + size + ); + } + + @Override + @Transactional + public PlaylistDTO addPlaylist(PlaylistDTO playlistDTO, MultipartFile cover) { + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException("用户不存在")); + + // 上传封面 + String coverUrl = null; + if (cover != null && !cover.isEmpty()) { + String coverPhysicalPath = fileService.uploadImage(cover); + coverUrl = coverPhysicalPath.replace(System.getProperty("user.dir") + File.separator + "music-houduan", "").replace(File.separator, "/"); + } + + // 创建歌单实体 + Playlist playlist = new Playlist(); + playlist.setName(playlistDTO.getName()); + playlist.setDescription(playlistDTO.getDescription()); + playlist.setCover(coverUrl); + playlist.setCreator(user); + playlist.setPlayCount(0L); + playlist.setCollectCount(0); + playlist.setIsOfficial(0); // 默认为非官方歌单 + + // 设置歌单类型 + if (playlistDTO.getTypeId() != null) { + PlaylistType type = playlistTypeRepository.findById(playlistDTO.getTypeId()) + .orElseThrow(() -> new BusinessException("歌单类型不存在")); + playlist.setType(type); + } + + // 保存歌单 + Playlist savedPlaylist = playlistRepository.save(playlist); + + return convertToDTO(savedPlaylist); + } + + @Override + @Transactional + public PlaylistDTO updatePlaylist(Long id, PlaylistDTO playlistDTO, MultipartFile cover) { + // 获取歌单 + Playlist playlist = playlistRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌单不存在")); + + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + + // 检查是否是歌单创建者或管理员 + if (!playlist.getCreator().getId().equals(userId) && !StpUtil.hasRole("admin")) { + throw new BusinessException("无权修改该歌单"); + } + + // 如果有新的封面,则上传并更新URL + if (cover != null && !cover.isEmpty()) { + // 删除旧封面 + if (playlist.getCover() != null) { + fileService.deleteFile(playlist.getCover()); + } + + // 上传新封面 + String coverPhysicalPath = fileService.uploadImage(cover); + String coverUrl = coverPhysicalPath.replace(System.getProperty("user.dir") + File.separator + "music-houduan", "").replace(File.separator, "/"); + playlist.setCover(coverUrl); + } + + // 更新歌单信息 + playlist.setName(playlistDTO.getName()); + playlist.setDescription(playlistDTO.getDescription()); + + // 更新是否官方字段 + System.out.println("接收到的isOfficial值: " + playlistDTO.getIsOfficial()); + playlist.setIsOfficial(playlistDTO.getIsOfficial()); + System.out.println("更新后的歌单isOfficial值: " + playlist.getIsOfficial()); + + // 更新歌单类型 + if (playlistDTO.getTypeId() != null) { + PlaylistType type = playlistTypeRepository.findById(playlistDTO.getTypeId()) + .orElseThrow(() -> new BusinessException("歌单类型不存在")); + playlist.setType(type); + } + + // 保存歌单 + Playlist updatedPlaylist = playlistRepository.save(playlist); + + return convertToDTO(updatedPlaylist); + } + + @Override + @Transactional + public boolean deletePlaylist(Long id) { + // 获取歌单 + Playlist playlist = playlistRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌单不存在")); + + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + + // 检查是否是歌单创建者或管理员 + if (!playlist.getCreator().getId().equals(userId) && !StpUtil.hasRole("admin")) { + throw new BusinessException("无权删除该歌单"); + } + + // 删除封面 + if (playlist.getCover() != null) { + fileService.deleteFile(playlist.getCover()); + } + + // 删除相关的收藏记录 + userCollectPlaylistRepository.deleteByPlaylistId(id); + + // 删除歌单 + playlistRepository.delete(playlist); + + return true; + } + + @Override + @Transactional + public boolean collectPlaylist(Long id) { + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException("用户不存在")); + + // 获取歌单 + Playlist playlist = playlistRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌单不存在")); + + // 检查是否已收藏 + if (userCollectPlaylistRepository.existsByUserAndPlaylist(user, playlist)) { + // 已收藏则取消收藏 + userCollectPlaylistRepository.deleteByUserAndPlaylist(user, playlist); + + // 更新收藏数 + playlist.setCollectCount((playlist.getCollectCount() == null ? 0 : playlist.getCollectCount()) - 1); + playlistRepository.save(playlist); + + return false; // 表示取消收藏 + } else { + // 未收藏则进行收藏 + com.test.musichouduan.entity.UserCollectPlaylist userCollectPlaylist = new com.test.musichouduan.entity.UserCollectPlaylist(); + userCollectPlaylist.setUser(user); + userCollectPlaylist.setPlaylist(playlist); + userCollectPlaylistRepository.save(userCollectPlaylist); + + // 更新收藏数 + playlist.setCollectCount((playlist.getCollectCount() == null ? 0 : playlist.getCollectCount()) + 1); + playlistRepository.save(playlist); + + return true; // 表示收藏成功 + } + } + + + @Override + @Transactional + public boolean uncollectPlaylist(Long id) { + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException("用户不存在")); + + // 获取歌单 + Playlist playlist = playlistRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌单不存在")); + + // 检查是否已收藏 + if (!userCollectPlaylistRepository.existsByUserAndPlaylist(user, playlist)) { + throw new BusinessException("未收藏该歌单"); + } + + // 取消收藏歌单 + userCollectPlaylistRepository.deleteByUserAndPlaylist(user, playlist); + + // 更新收藏数 + playlist.setCollectCount((playlist.getCollectCount() == null ? 0 : playlist.getCollectCount()) - 1); + playlistRepository.save(playlist); + + return true; + } + + @Override + public boolean isCollected(Long id) { + if (!StpUtil.isLogin()) { + return false; + } + Long userId = StpUtil.getLoginIdAsLong(); + return userCollectPlaylistRepository.existsByUserIdAndPlaylistId(userId, id); + } + + @Override + public PageResult getCollectedPlaylist(Integer page, Integer size) { + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + + // 获取用户收藏的歌单 + Page playlistPage = userCollectPlaylistRepository.findCollectedPlaylist( + userId, + PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime")) + ); + + // 转换为DTO + List playlistDTOs = playlistPage.getContent().stream() + .map(playlist -> { + PlaylistDTO dto = convertToDTO(playlist); + // 从收藏记录中获取收藏时间 + userCollectPlaylistRepository.findByUserIdAndPlaylistId(userId, playlist.getId()) + .ifPresent(ucp -> dto.setCollectTime(ucp.getCreateTime())); + return dto; + }) + .collect(Collectors.toList()); + + return new PageResult<>( + playlistDTOs, + playlistPage.getTotalElements(), + page, + size + ); + } + + @Override + public PageResult getUserPlaylist(Integer page, Integer size) { + // 获取当前登录用户 + Long userId = StpUtil.getLoginIdAsLong(); + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException("用户不存在")); + + // 获取用户创建的歌单 + Page playlistPage = playlistRepository.findByCreator( + user, + PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime")) + ); + + // 转换为DTO + List playlistDTOs = playlistPage.getContent().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + + return new PageResult<>( + playlistDTOs, + playlistPage.getTotalElements(), + page, + size + ); + } + @Override @Transactional public boolean addMusicToPlaylist(Long id, Long musicId) { // 获取歌单 Playlist playlist = playlistRepository.findById(id) .orElseThrow(() -> new BusinessException("歌单不存在")); - - // 获取当前登录用户 - Long userId = StpUtil.getLoginIdAsLong(); - - // 检查是否是歌单创建者 - if (!playlist.getCreator().getId().equals(userId)) { - throw new BusinessException("无权修改该歌单"); - } + validatePlaylistManagePermission(playlist, "无权修改该歌单"); // 获取音乐 Music music = musicRepository.findById(musicId) .orElseThrow(() -> new BusinessException("音乐不存在")); - - // 检查音乐是否已在歌单中 - if (playlist.getMusics().contains(music)) { - throw new BusinessException("该音乐已在歌单中"); - } - - // 添加音乐到歌单 - playlist.getMusics().add(music); - playlistRepository.save(playlist); - - return true; - } - - @Override - @Transactional + + // 检查音乐是否已在歌单中 + if (playlist.getMusics().contains(music)) { + throw new BusinessException("该音乐已在歌单中"); + } + + // 添加音乐到歌单 + playlist.getMusics().add(music); + playlistRepository.save(playlist); + + return true; + } + + @Override + @Transactional public boolean removeMusicFromPlaylist(Long id, Long musicId) { // 获取歌单 Playlist playlist = playlistRepository.findById(id) .orElseThrow(() -> new BusinessException("歌单不存在")); - - // 获取当前登录用户 - Long userId = StpUtil.getLoginIdAsLong(); - - // 检查是否是歌单创建者 - if (!playlist.getCreator().getId().equals(userId)) { - throw new BusinessException("无权修改该歌单"); - } + validatePlaylistManagePermission(playlist, "无权修改该歌单"); // 获取音乐 Music music = musicRepository.findById(musicId) .orElseThrow(() -> new BusinessException("音乐不存在")); - - // 检查音乐是否在歌单中 - if (!playlist.getMusics().contains(music)) { - throw new BusinessException("该音乐不在歌单中"); - } - - // 从歌单中移除音乐 - playlist.getMusics().remove(music); - playlistRepository.save(playlist); - - return true; - } - - @Override - @Transactional - public PlaylistDTO playPlaylist(Long id) { - System.out.println("接收到播放歌单请求,歌单ID: " + id); - // 获取歌单 - Playlist playlist = playlistRepository.findById(id) - .orElseThrow(() -> new BusinessException("歌单不存在")); - - // 增加播放次数 - long currentPlayCount = playlist.getPlayCount() == null ? 0 : playlist.getPlayCount(); - System.out.println("当前播放次数: " + currentPlayCount); - playlist.setPlayCount(currentPlayCount + 1); - playlistRepository.save(playlist); - System.out.println("播放次数已增加,更新后为: " + playlist.getPlayCount()); - - return convertToDTO(playlist); - } - - @Override + + // 检查音乐是否在歌单中 + if (!playlist.getMusics().contains(music)) { + throw new BusinessException("该音乐不在歌单中"); + } + + // 从歌单中移除音乐 + playlist.getMusics().remove(music); + playlistRepository.save(playlist); + + return true; + } + + @Override + @Transactional + public PlaylistDTO playPlaylist(Long id) { + System.out.println("接收到播放歌单请求,歌单ID: " + id); + // 获取歌单 + Playlist playlist = playlistRepository.findById(id) + .orElseThrow(() -> new BusinessException("歌单不存在")); + + // 增加播放次数 + long currentPlayCount = playlist.getPlayCount() == null ? 0 : playlist.getPlayCount(); + System.out.println("当前播放次数: " + currentPlayCount); + playlist.setPlayCount(currentPlayCount + 1); + playlistRepository.save(playlist); + System.out.println("播放次数已增加,更新后为: " + playlist.getPlayCount()); + + return convertToDTO(playlist); + } + + @Override public Playlist getById(Long id) { return playlistRepository.findById(id) .orElseThrow(() -> new BusinessException("歌单不存在")); } /** - * 将实体转换为DTO + * 校验当前登录用户是否有权限管理歌单。 + * + * 允许歌单创建者或管理员执行管理操作,避免后台管理员无法维护用户歌单。 * * @param playlist 歌单实体 - * @return 歌单DTO + * @param errorMessage 无权限时的提示文案 */ - private PlaylistDTO convertToDTO(Playlist playlist) { - PlaylistDTO playlistDTO = new PlaylistDTO(); - BeanUtils.copyProperties(playlist, playlistDTO); - playlistDTO.setHotScore(playlist.getHotScore()); - if (playlist.getPlayCount() == null) { - playlistDTO.setPlayCount(0L); + private void validatePlaylistManagePermission(Playlist playlist, String errorMessage) { + Long userId = StpUtil.getLoginIdAsLong(); + if (!playlist.getCreator().getId().equals(userId) && !StpUtil.hasRole("admin")) { + throw new BusinessException(errorMessage); } - - // 设置创建者信息 - if (playlist.getCreator() != null) { - UserDTO creatorDTO = new UserDTO(); - BeanUtils.copyProperties(playlist.getCreator(), creatorDTO); - playlistDTO.setCreator(creatorDTO); - } - - // 设置类型信息 - if (playlist.getType() != null) { - playlistDTO.setTypeId(playlist.getType().getId()); - playlistDTO.setTypeName(playlist.getType().getName()); - } - - // 设置音乐列表 - if (playlist.getMusics() != null && !playlist.getMusics().isEmpty()) { - List musicDTOs = playlist.getMusics().stream() - .map(music -> { - MusicDTO musicDTO = new MusicDTO(); - BeanUtils.copyProperties(music, musicDTO); - if (music.getSingers() != null && !music.getSingers().isEmpty()) { - List singerNames = music.getSingers().stream() - .map(singer -> singer.getName()) - .collect(Collectors.toList()); - musicDTO.setSingerNames(singerNames); - } - return musicDTO; - }) - .collect(Collectors.toList()); - playlistDTO.setMusics(musicDTOs); - playlistDTO.setMusicCount(musicDTOs.size()); - playlistDTO.setSongCount(musicDTOs.size()); - } else { - playlistDTO.setMusicCount(0); - playlistDTO.setSongCount(0); - } - - // 设置评论数 (targetType=2 代表歌单) - long commentCount = commentRepository.countByTargetIdAndTargetType(playlist.getId(), 2); - playlistDTO.setCommentCount((int) commentCount); - - // 检查当前登录用户是否已收藏该歌单 - if (StpUtil.isLogin()) { - Long userId = StpUtil.getLoginIdAsLong(); - boolean isCollected = userCollectPlaylistRepository.existsByUserIdAndPlaylistId(userId, playlist.getId()); - playlistDTO.setIsCollected(isCollected); - } else { - playlistDTO.setIsCollected(false); - } - - return playlistDTO; } -} + + /** + * 将实体转换为DTO + * + * @param playlist 歌单实体 + * @return 歌单DTO + */ + private PlaylistDTO convertToDTO(Playlist playlist) { + PlaylistDTO playlistDTO = new PlaylistDTO(); + BeanUtils.copyProperties(playlist, playlistDTO); + playlistDTO.setHotScore(playlist.getHotScore()); + if (playlist.getPlayCount() == null) { + playlistDTO.setPlayCount(0L); + } + + // 设置创建者信息 + if (playlist.getCreator() != null) { + UserDTO creatorDTO = new UserDTO(); + BeanUtils.copyProperties(playlist.getCreator(), creatorDTO); + playlistDTO.setCreator(creatorDTO); + } + + // 设置类型信息 + if (playlist.getType() != null) { + playlistDTO.setTypeId(playlist.getType().getId()); + playlistDTO.setTypeName(playlist.getType().getName()); + } + + // 设置音乐列表 + if (playlist.getMusics() != null && !playlist.getMusics().isEmpty()) { + List musicDTOs = playlist.getMusics().stream() + .map(music -> { + MusicDTO musicDTO = new MusicDTO(); + BeanUtils.copyProperties(music, musicDTO); + if (music.getSingers() != null && !music.getSingers().isEmpty()) { + List singerNames = music.getSingers().stream() + .map(singer -> singer.getName()) + .collect(Collectors.toList()); + musicDTO.setSingerNames(singerNames); + } + return musicDTO; + }) + .collect(Collectors.toList()); + playlistDTO.setMusics(musicDTOs); + playlistDTO.setMusicCount(musicDTOs.size()); + playlistDTO.setSongCount(musicDTOs.size()); + } else { + playlistDTO.setMusicCount(0); + playlistDTO.setSongCount(0); + } + + // 设置评论数 (targetType=2 代表歌单) + long commentCount = commentRepository.countByTargetIdAndTargetType(playlist.getId(), 2); + playlistDTO.setCommentCount((int) commentCount); + + // 检查当前登录用户是否已收藏该歌单 + if (StpUtil.isLogin()) { + Long userId = StpUtil.getLoginIdAsLong(); + boolean isCollected = userCollectPlaylistRepository.existsByUserIdAndPlaylistId(userId, playlist.getId()); + playlistDTO.setIsCollected(isCollected); + } else { + playlistDTO.setIsCollected(false); + } + + return playlistDTO; + } +} diff --git a/music-qianduan/src/views/admin/Playlist.vue b/music-qianduan/src/views/admin/Playlist.vue index 0f368e5..8d782c9 100644 --- a/music-qianduan/src/views/admin/Playlist.vue +++ b/music-qianduan/src/views/admin/Playlist.vue @@ -1,9 +1,10 @@