Compare commits

..

10 Commits

Author SHA1 Message Date
ikmkj
b46a7f8b1b feat(playlist): 实现热门歌单缓存更新功能
- 添加定时任务每小时更新热门歌单缓存
- 实现分页查询所有歌单并计算热门分数- 使用并行流提升歌单评分计算性能
- 按照热门分数排序并限制返回数量
- 将热门歌单列表序列化后存入Redis缓存
- 添加详细的JavaDoc注释说明方法用途
-优化查询逻辑中的条件组合说明- 增强错误处理和日志记录机制
2025-10-10 22:35:55 +08:00
ikmkj
4a379bed83 feat(playlist): 实现歌单收藏与取消收藏功能- 添加收藏状态检查逻辑
- 支持取消已收藏的歌单
- 更新歌单收藏计数- 保存用户收藏记录
- 返回收藏操作结果状态
2025-10-10 22:30:49 +08:00
ikmkj
b771dbdf2d refactor(user): 更新收藏歌手页面的取消收藏功能
- 将 toggleCollectSinger API 调用替换为 uncollectSinger API
- 优化了取消收藏歌手的逻辑,提高了代码的可读性和维护性
2025-08-10 20:12:51 +08:00
ikmkj
d8eee520cc refactor(music): 优化音乐数据请求流程
- 在获取所有热门音乐数据后,添加 await 关键字确保音乐状态更新完成后再执行后续操作
- 这个改动可以提高数据请求的可靠性和用户体验
2025-08-10 20:01:36 +08:00
ikmkj
22a189053e fix(comment): 修复回复评论时的 parentId 错误
-将 replyingTo.value.id 改为 replyingTo.value,以确保使用正确的值作为 parentId
-这个修改解决了回复评论时可能出现的错误或异常
2025-08-10 19:53:19 +08:00
ikmkj
1546cdb3c4 feat(singer): 添加歌手类型相关功能
- 在 pom.xml 中添加 Hutool 工具库依赖
- 优化前端 Singer.vue 中的歌手数据处理逻辑
- 在后端 SingerServiceImpl 中添加默认歌手类型设置功能
2025-08-10 19:36:38 +08:00
ikmkj
a36ef7a561 feat(comment): 优化评论加载效率并添加回复功能
- 在 CommentRepository 中添加 findByParentIdInOrderByCreateTimeAsc 方法,实现批量查询子评论
- 重构 CommentServiceImpl 中的 findCommentsByMusicId 方法,提升评论加载效率
- 在前端 MusicDetail 组件中增加回复功能,支持二级评论和回复展开/收起
- 优化 WebSocket订阅逻辑,实现实时更新评论列表
2025-08-10 16:23:12 +08:00
ikmkj
e30011cee2 refactor(time): 优化时间格式反序列化并统一时间格式- 新增 LocalDateTimeDeserializer 类,用于自定义 LocalDateTime 类型的反序列化逻辑
- 在 MusicDTO、PlaylistDTO、SingerDTO 和 UserDTO 中使用 LocalDateTimeDeserializer
-统一时间格式为 "yyyy-MM-dd HH:mm:ss"
- 移除了 SaTokenConfig 和 StpInterfaceImpl 中的调试信息打印
2025-08-10 14:42:23 +08:00
ikmkj
bf27209022 fix(music-houduan): 修复轮播图图片路径问题
- 修改了 BannerServiceImpl 类中的图片上传逻辑
- 使用 replace 方法移除了绝对路径中的项目目录和操作系统分隔符
-确保保存的图片路径为相对路径,格式为 /upload/images/...
2025-08-10 14:30:55 +08:00
ikmkj
c70a93e9f4 refactor(music-houduan): 修改用户收藏歌单的逻辑
- 移除了 UserCollectPlaylist 中的 collectTime 字段
- 使用 createTime 字段来记录收藏时间
- 更新了相关查询和排序逻辑
2025-08-10 14:23:24 +08:00
18 changed files with 294 additions and 57 deletions

View File

@@ -97,6 +97,13 @@
<artifactId>spring-boot-starter-websocket</artifactId> <artifactId>spring-boot-starter-websocket</artifactId>
</dependency> </dependency>
<!-- Hutool工具库 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.22</version>
</dependency>
<!-- 工具类 --> <!-- 工具类 -->
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>

View File

@@ -0,0 +1,38 @@
package com.test.musichouduan.config;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
public class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
private static final DateTimeFormatter FORMATTER_DATETIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter FORMATTER_DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String str = p.getText().trim();
if (str.isEmpty()) {
return null;
}
try {
// 尝试解析 "yyyy-MM-dd HH:mm:ss" 格式
return LocalDateTime.parse(str, FORMATTER_DATETIME);
} catch (DateTimeParseException e) {
// 如果失败,尝试解析 "yyyy-MM-dd" 格式
try {
LocalDate date = LocalDate.parse(str, FORMATTER_DATE);
return date.atStartOfDay();
} catch (DateTimeParseException e2) {
throw new IOException(e2);
}
}
}
}

View File

@@ -19,16 +19,10 @@ public class SaTokenConfig implements WebMvcConfigurer {
// 注册Sa-Token拦截器打开注解式鉴权功能 // 注册Sa-Token拦截器打开注解式鉴权功能
registry.addInterceptor(new SaInterceptor(handle -> { registry.addInterceptor(new SaInterceptor(handle -> {
// 打印调试信息 // 打印调试信息
System.out.println("\n=== Sa-Token 拦截器检查登录状态 ===\n");
// 获取当前登录用户ID // 获取当前登录用户ID
Object loginId = StpUtil.getLoginIdDefaultNull(); Object loginId = StpUtil.getLoginIdDefaultNull();
System.out.println("当前登录用户ID: " + loginId);
// 获取当前用户角色 // 获取当前用户角色
List<String> roleList = StpUtil.getRoleList(); List<String> roleList = StpUtil.getRoleList();
System.out.println("当前用户角色: " + roleList);
// 检查登录状态 // 检查登录状态
StpUtil.checkLogin(); StpUtil.checkLogin();
})) }))

View File

@@ -45,9 +45,6 @@ public class StpInterfaceImpl implements StpInterface {
} else { } else {
roles.add("user"); roles.add("user");
} }
// 打印调试信息
System.out.println("用户ID: " + userId + ", 角色: " + roles);
}); });
return roles; return roles;

View File

@@ -1,6 +1,8 @@
package com.test.musichouduan.dto; package com.test.musichouduan.dto;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.test.musichouduan.config.LocalDateTimeDeserializer;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -101,6 +103,7 @@ public class MusicDTO {
* 发行日期 * 发行日期
*/ */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime releaseDate; private LocalDateTime releaseDate;
/** /**
@@ -117,12 +120,14 @@ public class MusicDTO {
* 创建时间 * 创建时间
*/ */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime createTime; private LocalDateTime createTime;
/** /**
* 收藏时间 * 收藏时间
*/ */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime collectTime; private LocalDateTime collectTime;
/** /**

View File

@@ -1,5 +1,8 @@
package com.test.musichouduan.dto; package com.test.musichouduan.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.test.musichouduan.config.LocalDateTimeDeserializer;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import lombok.Data; import lombok.Data;
@@ -92,6 +95,8 @@ public class PlaylistDTO {
/** /**
* 创建时间 * 创建时间
*/ */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime createTime; private LocalDateTime createTime;
/** /**
@@ -102,5 +107,7 @@ public class PlaylistDTO {
/** /**
* 收藏时间 * 收藏时间
*/ */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime collectTime; private LocalDateTime collectTime;
} }

View File

@@ -1,5 +1,8 @@
package com.test.musichouduan.dto; package com.test.musichouduan.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.test.musichouduan.config.LocalDateTimeDeserializer;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import lombok.Data; import lombok.Data;
@@ -70,6 +73,8 @@ public class SingerDTO {
/** /**
* 创建时间 * 创建时间
*/ */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime createTime; private LocalDateTime createTime;
/** /**

View File

@@ -1,5 +1,8 @@
package com.test.musichouduan.dto; package com.test.musichouduan.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.test.musichouduan.config.LocalDateTimeDeserializer;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -63,6 +66,8 @@ public class UserDTO {
/** /**
* 创建时间 * 创建时间
*/ */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime createTime; private LocalDateTime createTime;
/** /**

View File

@@ -35,6 +35,7 @@ public class UserCollectPlaylist {
/** /**
* 收藏时间 * 收藏时间
*/ */
@CreationTimestamp
@Column(nullable = false, updatable = false) @Column(nullable = false, updatable = false)
private LocalDateTime collectTime; private LocalDateTime createTime;
} }

View File

@@ -34,6 +34,14 @@ public interface CommentRepository extends JpaRepository<Comment, Long>, JpaSpec
*/ */
List<Comment> findByParentIdOrderByCreateTimeAsc(Long parentId); List<Comment> findByParentIdOrderByCreateTimeAsc(Long parentId);
/**
* 根据父评论ID列表查询所有子评论
*
* @param parentIds 父评论ID列表
* @return 子评论列表
*/
List<Comment> findByParentIdInOrderByCreateTimeAsc(List<Long> parentIds);
/** /**
* 根据用户查询评论 * 根据用户查询评论
* *

View File

@@ -101,7 +101,8 @@ public class BannerServiceImpl implements BannerService {
// 上传新图片 // 上传新图片
String imageUrl = fileService.uploadImage(image); String imageUrl = fileService.uploadImage(image);
banner.setImageUrl(imageUrl); // imageUrl已经是 /upload/images/... 格式 String Url = imageUrl.replace(System.getProperty("user.dir") + File.separator + "music-houduan", "").replace(File.separator, "/");
banner.setImageUrl(Url); // imageUrl已经是 /upload/images/... 格式
} }
// 更新轮播图信息 // 更新轮播图信息

View File

@@ -74,16 +74,28 @@ public class CommentServiceImpl implements CommentService {
.map(this::convertToDTO) .map(this::convertToDTO)
.collect(Collectors.toList()); .collect(Collectors.toList());
// 获取子评论 // 优化:一次性获取所有子评论
for (CommentDTO commentDTO : commentDTOs) { // 1. 获取所有父评论的ID
List<Comment> children = commentRepository.findByParentIdOrderByCreateTimeAsc(commentDTO.getId()); List<Long> parentIds = commentDTOs.stream()
if (!children.isEmpty()) { .map(CommentDTO::getId)
commentDTO.setChildren(children.stream() .collect(Collectors.toList());
// 2. 如果有父评论,则一次性查询出它们所有的子评论
if (!parentIds.isEmpty()) {
List<Comment> allChildren = commentRepository.findByParentIdInOrderByCreateTimeAsc(parentIds);
// 3. 将子评论按其父ID分组
java.util.Map<Long, List<CommentDTO>> childrenMap = allChildren.stream()
.map(this::convertToDTO) .map(this::convertToDTO)
.collect(Collectors.toList())); .collect(Collectors.groupingBy(CommentDTO::getParentId));
// 4. 将子评论列表设置到对应的父评论DTO中
commentDTOs.forEach(parentDTO -> parentDTO.setChildren(
childrenMap.getOrDefault(parentDTO.getId(), new ArrayList<>())
));
} else { } else {
commentDTO.setChildren(new ArrayList<>()); // 如果没有父评论确保children字段不是null
} commentDTOs.forEach(parentDTO -> parentDTO.setChildren(new ArrayList<>()));
} }
return new PageResult<>( return new PageResult<>(

View File

@@ -83,38 +83,54 @@ public class PlaylistServiceImpl implements PlaylistService {
private FileService fileService; private FileService fileService;
@Override @Override
/**
* 根据歌单ID获取歌单信息
* @param id 歌单的唯一标识符
* @return PlaylistDTO 歌单的数据传输对象,包含歌单的详细信息
* @throws BusinessException 当指定的歌单不存在时抛出业务异常
*/
public PlaylistDTO getPlaylistById(Long id) { public PlaylistDTO getPlaylistById(Long id) {
// 从数据库中查找指定ID的歌单如果不存在则抛出业务异常
Playlist playlist = playlistRepository.findById(id) Playlist playlist = playlistRepository.findById(id)
.orElseThrow(() -> new BusinessException("歌单不存在")); .orElseThrow(() -> new BusinessException("歌单不存在"));
// 将实体对象转换为DTO对象并返回
return convertToDTO(playlist); return convertToDTO(playlist);
} }
@Override @Override
/**
* 获取歌单列表
* @param page 当前页码
* @param size 每页大小
* @param keyword 搜索关键词
* @param typeId 歌单类型ID
* @return PageResult<PlaylistDTO> 歌单列表分页结果
*/
public PageResult<PlaylistDTO> getPlaylistList(Integer page, Integer size, String keyword, Long typeId) { public PageResult<PlaylistDTO> getPlaylistList(Integer page, Integer size, String keyword, Long typeId) {
// 构建查询条件 // 构建查询条件
Specification<Playlist> spec = (root, query, cb) -> { Specification<Playlist> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>(); List<Predicate> predicates = new ArrayList<>();
query.distinct(true); // 添加 distinct 查询 query.distinct(true); // 添加 distinct 查询,避免重复结果
// 关键字查询 // 关键字查询
if (StringUtils.hasText(keyword)) { if (StringUtils.hasText(keyword)) {
// 搜索歌单名、描述、创建者用户名以及歌单内的音乐名 // 搜索歌单名、描述、创建者用户名以及歌单内的音乐名
Predicate p1 = cb.like(root.get("name"), "%" + keyword + "%"); Predicate p1 = cb.like(root.get("name"), "%" + keyword + "%"); // 匹配歌单名
Predicate p2 = cb.like(root.get("description"), "%" + keyword + "%"); Predicate p2 = cb.like(root.get("description"), "%" + keyword + "%"); // 匹配歌单描述
Predicate p3 = cb.like(root.join("creator").get("username"), "%" + 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 + "%"); Predicate p4 = cb.like(root.join("musics", jakarta.persistence.criteria.JoinType.LEFT).get("name"), "%" + keyword + "%"); // 匹配歌单内的音乐名
predicates.add(cb.or(p1, p2, p3, p4)); predicates.add(cb.or(p1, p2, p3, p4)); // 将多个查询条件组合为OR关系
} }
// 类型查询 // 类型查询
if (typeId != null) { if (typeId != null) {
predicates.add(cb.equal(root.get("type").get("id"), typeId)); predicates.add(cb.equal(root.get("type").get("id"), typeId)); // 添加类型ID查询条件
} }
return cb.and(predicates.toArray(new Predicate[0])); return cb.and(predicates.toArray(new Predicate[0])); // 将所有查询条件组合为AND关系
}; };
// 执行查询 // 执行查询,按创建时间降序排列
Page<Playlist> playlistPage = playlistRepository.findAll( Page<Playlist> playlistPage = playlistRepository.findAll(
spec, spec,
PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime")) PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime"))
@@ -173,46 +189,80 @@ public class PlaylistServiceImpl implements PlaylistService {
} }
} }
/**
* 定时任务方法:用于更新热门歌单缓存
* 使用@Scheduled注解配置定时任务cron表达式设置为"0 0 * * * ?"
* 表示每小时整点执行一次任务即每小时的0分0秒触发
*/
@Scheduled(cron = "0 0 * * * ?") @Scheduled(cron = "0 0 * * * ?")
public void scheduleUpdateHotPlaylist() { public void scheduleUpdateHotPlaylist() {
// 打印任务开始日志
System.out.println("Scheduled task started: Updating hot playlist cache..."); System.out.println("Scheduled task started: Updating hot playlist cache...");
// 调用更新热门歌单缓存的方法
updateHotPlaylistCache(); updateHotPlaylistCache();
} }
/**
* 更新热门歌单缓存的方法
* 使用异步方式和只读事务注解
* @return 返回包含热门歌单DTO列表的CompletableFuture
*/
@Async @Async
@Transactional(readOnly = true) @Transactional(readOnly = true)
public CompletableFuture<List<PlaylistDTO>> updateHotPlaylistCache() { public CompletableFuture<List<PlaylistDTO>> updateHotPlaylistCache() {
// 用于存储所有歌单的列表
List<Playlist> allPlaylists = new ArrayList<>(); List<Playlist> allPlaylists = new ArrayList<>();
// 分页查询结果对象
Page<Playlist> playlistPage; Page<Playlist> playlistPage;
// 当前页码从0开始
int pageNum = 0; int pageNum = 0;
// 每页大小设置为500
int pageSize = 500; int pageSize = 500;
// 分页查询所有歌单,直到没有更多数据
do { do {
// 创建分页对象
Pageable pageable = PageRequest.of(pageNum, pageSize); Pageable pageable = PageRequest.of(pageNum, pageSize);
// 执行分页查询
playlistPage = playlistRepository.findAll(pageable); playlistPage = playlistRepository.findAll(pageable);
// 将当前页的歌单添加到总列表中
allPlaylists.addAll(playlistPage.getContent()); allPlaylists.addAll(playlistPage.getContent());
// 页码递增
pageNum++; pageNum++;
} while (playlistPage.hasNext()); } while (playlistPage.hasNext());
// 使用并行流为每个歌单计算热门分数
allPlaylists.parallelStream().forEach(playlist -> { allPlaylists.parallelStream().forEach(playlist -> {
// 计算热门歌单分数
double score = HotPlaylistScoreUtil.calculateHotScore(playlist); double score = HotPlaylistScoreUtil.calculateHotScore(playlist);
// 设置歌单的热门分数
playlist.setHotScore(score); playlist.setHotScore(score);
}); });
// 将歌单按热门分数降序排序,并限制热门歌单数量
List<PlaylistDTO> sortedHotPlaylists = allPlaylists.stream() List<PlaylistDTO> sortedHotPlaylists = allPlaylists.stream()
// 按热门分数降序排序
.sorted(Comparator.comparing(Playlist::getHotScore).reversed()) .sorted(Comparator.comparing(Playlist::getHotScore).reversed())
// 限制热门歌单数量
.limit(HOT_PLAYLIST_LIMIT) .limit(HOT_PLAYLIST_LIMIT)
// 将歌单实体转换为DTO对象
.map(this::convertToDTO) .map(this::convertToDTO)
// 收集为列表
.collect(Collectors.toList()); .collect(Collectors.toList());
try { try {
// 将热门歌单列表转换为JSON字符串
String jsonToCache = objectMapper.writeValueAsString(sortedHotPlaylists); String jsonToCache = objectMapper.writeValueAsString(sortedHotPlaylists);
// 将JSON字符串存入Redis缓存设置过期时间
redisTemplate.opsForValue().set(HOT_PLAYLIST_CACHE_KEY, jsonToCache, CACHE_EXPIRATION_HOURS, TimeUnit.HOURS); 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."); System.out.println("Hot playlist cache updated successfully with " + sortedHotPlaylists.size() + " items.");
} catch (Exception e) { } catch (Exception e) {
// 打印更新失败的错误信息
System.err.println("Failed to cache hot playlist list to Redis: " + e.getMessage()); System.err.println("Failed to cache hot playlist list to Redis: " + e.getMessage());
} }
// 返回包含热门歌单DTO列表的CompletableFuture
return CompletableFuture.completedFuture(sortedHotPlaylists); return CompletableFuture.completedFuture(sortedHotPlaylists);
} }
@@ -368,22 +418,29 @@ public class PlaylistServiceImpl implements PlaylistService {
// 检查是否已收藏 // 检查是否已收藏
if (userCollectPlaylistRepository.existsByUserAndPlaylist(user, playlist)) { 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 false; // 表示取消收藏
} else {
// 未收藏则进行收藏
com.test.musichouduan.entity.UserCollectPlaylist userCollectPlaylist = new com.test.musichouduan.entity.UserCollectPlaylist(); com.test.musichouduan.entity.UserCollectPlaylist userCollectPlaylist = new com.test.musichouduan.entity.UserCollectPlaylist();
userCollectPlaylist.setUser(user); userCollectPlaylist.setUser(user);
userCollectPlaylist.setPlaylist(playlist); userCollectPlaylist.setPlaylist(playlist);
userCollectPlaylist.setCollectTime(java.time.LocalDateTime.now());
userCollectPlaylistRepository.save(userCollectPlaylist); userCollectPlaylistRepository.save(userCollectPlaylist);
// 更新收藏数 // 更新收藏数
playlist.setCollectCount((playlist.getCollectCount() == null ? 0 : playlist.getCollectCount()) + 1); playlist.setCollectCount((playlist.getCollectCount() == null ? 0 : playlist.getCollectCount()) + 1);
playlistRepository.save(playlist); playlistRepository.save(playlist);
return true; return true; // 表示收藏成功
} }
}
@Override @Override
@Transactional @Transactional
@@ -429,7 +486,7 @@ public class PlaylistServiceImpl implements PlaylistService {
// 获取用户收藏的歌单 // 获取用户收藏的歌单
Page<Playlist> playlistPage = userCollectPlaylistRepository.findCollectedPlaylist( Page<Playlist> playlistPage = userCollectPlaylistRepository.findCollectedPlaylist(
userId, userId,
PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "collectTime")) PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime"))
); );
// 转换为DTO // 转换为DTO
@@ -438,7 +495,7 @@ public class PlaylistServiceImpl implements PlaylistService {
PlaylistDTO dto = convertToDTO(playlist); PlaylistDTO dto = convertToDTO(playlist);
// 从收藏记录中获取收藏时间 // 从收藏记录中获取收藏时间
userCollectPlaylistRepository.findByUserIdAndPlaylistId(userId, playlist.getId()) userCollectPlaylistRepository.findByUserIdAndPlaylistId(userId, playlist.getId())
.ifPresent(ucp -> dto.setCollectTime(ucp.getCollectTime())); .ifPresent(ucp -> dto.setCollectTime(ucp.getCreateTime()));
return dto; return dto;
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());

View File

@@ -1,6 +1,8 @@
package com.test.musichouduan.service.impl; package com.test.musichouduan.service.impl;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.test.musichouduan.dto.PageResult; import com.test.musichouduan.dto.PageResult;
import com.test.musichouduan.dto.SingerDTO; import com.test.musichouduan.dto.SingerDTO;
import com.test.musichouduan.dto.MusicDTO; import com.test.musichouduan.dto.MusicDTO;
@@ -225,6 +227,12 @@ public class SingerServiceImpl implements SingerService {
singer.setAvatar(avatarUrl); singer.setAvatar(avatarUrl);
singer.setIntroduction(singerDTO.getIntroduction()); singer.setIntroduction(singerDTO.getIntroduction());
singer.setCollectCount(0); singer.setCollectCount(0);
// 设置默认歌手类型
if (ObjectUtil.isEmpty(singerDTO.getTypeId())){
SingerType defaultType = singerTypeRepository.findById(1L)
.orElseThrow(() -> new BusinessException("默认歌手类型不存在"));
singer.setType(defaultType);
}
// 设置歌手类型 // 设置歌手类型
if (singerDTO.getTypeId() != null) { if (singerDTO.getTypeId() != null) {

View File

@@ -136,7 +136,7 @@ const fetchHotMusic = async (params = {}) => {
}) })
// 更新状态 // 更新状态
updateMusicStatuses(allHotMusic) await updateMusicStatuses(allHotMusic)
// 根据当前页码和每页数量进行前端分页 // 根据当前页码和每页数量进行前端分页
const page = params.page || currentPage.value const page = params.page || currentPage.value

View File

@@ -129,8 +129,9 @@
<div class="comment-list"> <div class="comment-list">
<div v-for="comment in comments" :key="comment.id" class="comment-item"> <div v-for="comment in comments" :key="comment.id" class="comment-item">
<!-- 一级评论 -->
<div class="comment-avatar"> <div class="comment-avatar">
<el-avatar :src="comment.user.avatar" :size="40" /> <el-avatar :src="processImageUrl(comment.user.avatar)" :size="40" />
</div> </div>
<div class="comment-content"> <div class="comment-content">
<div class="comment-header"> <div class="comment-header">
@@ -150,19 +151,69 @@
<div class="comment-text">{{ comment.content }}</div> <div class="comment-text">{{ comment.content }}</div>
<div class="comment-footer"> <div class="comment-footer">
<div class="comment-time">{{ comment.createTime }}</div> <div class="comment-time">{{ comment.createTime }}</div>
<el-button type="text" size="small" @click="toggleReplies(comment)">
{{ expandedComments.includes(comment.id) ? '收起回复' : `查看回复 (${comment.children ? comment.children.length : 0})` }}
</el-button>
</div> </div>
<div v-if="replyingTo && replyingTo.id === comment.id" class="comment-input child-comment-input">
<!-- 回复输入框 -->
<div v-if="replyingTo === comment.id" class="comment-input child-comment-input">
<el-input <el-input
v-model="replyContent" v-model="replyContent"
type="textarea" type="textarea"
:rows="2" :rows="2"
:placeholder="`回复 @${replyingTo.user.username}`" :placeholder="`回复 @${replyTo.user.username}`"
/> />
<div class="comment-submit-actions"> <div class="comment-submit-actions">
<el-button type="primary" size="small" @click="submitComment(true)">回复</el-button> <el-button type="primary" size="small" @click="submitComment(true)">回复</el-button>
<el-button size="small" @click="cancelReply">取消</el-button> <el-button size="small" @click="cancelReply">取消</el-button>
</div> </div>
</div> </div>
<!-- 二级评论 -->
<div v-if="expandedComments.includes(comment.id) && comment.children && comment.children.length > 0" class="child-comments">
<div v-for="child in comment.children" :key="child.id" class="comment-item">
<div class="comment-avatar">
<el-avatar :src="processImageUrl(child.user.avatar)" :size="32" />
</div>
<div class="comment-content">
<div class="comment-header">
<div class="comment-user">{{ child.user.username }}</div>
<div class="comment-actions">
<el-button type="text" size="small" @click="setReplyTo(child)">回复</el-button>
<el-button
v-if="userStore.isAdmin || child.user.id === userStore.user?.id"
type="danger"
size="small"
@click="deleteComment(child.id)"
>
删除
</el-button>
</div>
</div>
<div class="comment-text">
<span v-if="child.parentCommentUser" class="reply-to">@{{ child.parentCommentUser.username }}</span>
{{ child.content }}
</div>
<div class="comment-footer">
<div class="comment-time">{{ child.createTime }}</div>
</div>
<!-- 二级回复输入框 -->
<div v-if="replyingTo === child.id" class="comment-input child-comment-input">
<el-input
v-model="replyContent"
type="textarea"
:rows="2"
:placeholder="`回复 @${replyTo.user.username}`"
/>
<div class="comment-submit-actions">
<el-button type="primary" size="small" @click="submitComment(true, child.id)">回复</el-button>
<el-button size="small" @click="cancelReply">取消</el-button>
</div>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
<el-empty v-if="!comments.length" description="还没有评论,快来抢沙发吧!" /> <el-empty v-if="!comments.length" description="还没有评论,快来抢沙发吧!" />
@@ -210,6 +261,7 @@ import { getCommentList, addComment, deleteComment as apiDeleteComment } from '.
import { connect, subscribe, unsubscribe, disconnect } from '../utils/websocket' import { connect, subscribe, unsubscribe, disconnect } from '../utils/websocket'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { VideoPlay, VideoPause, Star, StarFilled, Plus } from '@element-plus/icons-vue' import { VideoPlay, VideoPause, Star, StarFilled, Plus } from '@element-plus/icons-vue'
import { processImageUrl } from '../utils/imageUtils'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -226,6 +278,7 @@ const comments = ref([])
const commentContent = ref('') const commentContent = ref('')
const replyContent = ref('') const replyContent = ref('')
const replyingTo = ref(null) const replyingTo = ref(null)
const replyTo = ref(null)
const expandedComments = ref([]) const expandedComments = ref([])
let commentSubscription = null; let commentSubscription = null;
@@ -351,7 +404,7 @@ const submitComment = async (isReply = false) => {
targetId: music.value.id, targetId: music.value.id,
targetType: 1, // 1表示音乐 targetType: 1, // 1表示音乐
content: content, content: content,
parentId: isReply ? replyingTo.value.id : null, parentId: isReply ? replyingTo.value : null,
}); });
if (res.code === 200) { if (res.code === 200) {
@@ -398,15 +451,34 @@ const deleteComment = async (commentId) => {
// 设置回复对象 // 设置回复对象
const setReplyTo = (comment) => { const setReplyTo = (comment) => {
replyingTo.value = comment; replyingTo.value = comment.id;
replyTo.value = comment;
if (!expandedComments.value.includes(comment.id)) {
expandedComments.value.push(comment.id);
}
}; };
// 取消回复 // 取消回复
const cancelReply = () => { const cancelReply = () => {
replyingTo.value = null; replyingTo.value = null;
replyTo.value = null;
replyContent.value = ''; replyContent.value = '';
}; };
// 切换回复可见性
const toggleReplies = (comment) => {
const commentId = comment.id;
const index = expandedComments.value.indexOf(commentId);
if (index > -1) {
expandedComments.value.splice(index, 1);
if (replyingTo.value === commentId) {
cancelReply();
}
} else {
expandedComments.value.push(commentId);
}
};
// WebSocket 订阅 // WebSocket 订阅
const setupWebSocket = () => { const setupWebSocket = () => {
if (!music.value) return; if (!music.value) return;
@@ -414,10 +486,30 @@ const setupWebSocket = () => {
const destination = `/topic/comments/1/${music.value.id}`; // 1 代表音乐类型 const destination = `/topic/comments/1/${music.value.id}`; // 1 代表音乐类型
commentSubscription = subscribe(destination, (newComment) => { commentSubscription = subscribe(destination, (newComment) => {
if (newComment && newComment.id) { if (newComment && newComment.id) {
// 简单处理:直接重新拉取整个评论列表 if (newComment.user && newComment.user.avatar) {
fetchComments(); newComment.user.avatar = processImageUrl(newComment.user.avatar);
}
if (newComment.parentId) {
const parentComment = comments.value.find(c => c.id === newComment.parentId);
if (parentComment) {
if (!parentComment.children) {
parentComment.children = [];
}
parentComment.children.push(newComment);
}
} else {
if (!comments.value.some(c => c.id === newComment.id)) {
comments.value.unshift(newComment);
}
}
} else {
console.warn("Received invalid comment data via WebSocket:", newComment);
} }
}); });
}, (error) => {
console.error("WebSocket connection error:", error);
// ElMessage.error('评论服务连接失败,实时更新不可用');
}); });
}; };

View File

@@ -261,7 +261,7 @@ const submitForm = async () => {
const singerData = { const singerData = {
name: form.value.name, name: form.value.name,
introduction: form.value.introduction, introduction: form.value.introduction,
type: form.value.typeId ? { id: form.value.typeId } : null typeId: form.value.typeId || null
} }
let res let res

View File

@@ -2,7 +2,7 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { getCollectedSinger, toggleCollectSinger } from '../../api/singer' import { getCollectedSinger, uncollectSinger } from '../../api/singer'
import { processImageUrl } from '../../utils/imageUtils' import { processImageUrl } from '../../utils/imageUtils'
const router = useRouter() const router = useRouter()
@@ -59,7 +59,7 @@ const cancelCollect = (id, event) => {
type: 'warning' type: 'warning'
}).then(async () => { }).then(async () => {
try { try {
const res = await toggleCollectSinger(id) const res = await uncollectSinger(id)
if (res.code === 200) { if (res.code === 200) {
// 从列表中移除该歌手 // 从列表中移除该歌手
collectSingers.value = collectSingers.value.filter(item => item.id !== id) collectSingers.value = collectSingers.value.filter(item => item.id !== id)