feat(music): 添加音乐点赞功能及前端交互

- 后端新增点赞接口 /music/{id}/like,支持用户登录后点赞/取消点赞音乐,记录点赞数据到 user_like_music 表
- 后端修改 MusicController.getMusicById 接口,支持传入用户ID以判断当前音乐是否被该用户点赞
- 后端 MusicDTO 新增 isLiked 字段,用于前端展示点赞状态
- 数据库新增 user_like_music 表,记录用户与音乐的点赞关系,包含唯一约束防止重复点赞
- 前端 MusicDetail.vue 页面新增点赞按钮及状态展示,支持点击切换点赞状态并实时更新 UI 与点赞数
- 前端新增 toggleLikeMusic API 调用及相应交互逻辑,成功后展示操作提示信息
- 评论表 comment 新增 target_type 字段注释及状态字段注释,播放历史与各关联表新增索引优化与外键约束微调
This commit is contained in:
ikmkj
2025-07-25 23:25:45 +08:00
parent 59b6b251db
commit 4435315acf
11 changed files with 431 additions and 248 deletions

View File

@@ -31,7 +31,12 @@ public class MusicController {
*/
@GetMapping("/{id}")
public Result<MusicDTO> getMusicById(@PathVariable Long id) {
MusicDTO musicDTO = musicService.getMusicById(id);
Long userId = null;
// 检查用户是否登录,但不强制要求登录
if (StpUtil.isLogin()) {
userId = StpUtil.getLoginIdAsLong();
}
MusicDTO musicDTO = musicService.getMusicById(id, userId);
if (musicDTO == null) {
// 如果音乐不存在,返回空数据而不是错误
return Result.success(null);
@@ -213,4 +218,20 @@ public class MusicController {
MusicDTO musicDTO = musicService.playMusic(id);
return Result.success(musicDTO);
}
/**
* 点赞/取消点赞音乐
*
* @param id 音乐ID
* @return 响应结果
*/
@SaCheckLogin
@PostMapping("/{id}/like")
public Result<Boolean> toggleLikeMusic(@PathVariable Long id) {
// 获取当前登录用户ID
Long userId = StpUtil.getLoginIdAsLong();
boolean isLiked = musicService.toggleLikeMusic(id, userId);
String message = isLiked ? "点赞成功" : "取消点赞成功";
return Result.success(message, isLiked);
}
}

View File

@@ -91,6 +91,11 @@ public class MusicDTO {
*/
private Boolean isCollected;
/**
* 是否已点赞
*/
private Boolean isLiked;
/**
* 创建时间
*/
@@ -107,4 +112,11 @@ public class MusicDTO {
public void setCollected(boolean collected) {
this.isCollected = collected;
}
/**
* 设置是否已点赞
*/
public void setLiked(boolean liked) {
this.isLiked = liked;
}
}

View File

@@ -0,0 +1,29 @@
package com.test.musichouduan.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "user_like_music", uniqueConstraints = {
@UniqueConstraint(columnNames = {"userId", "musicId"})
})
public class UserLikeMusic {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private Long userId;
@Column(nullable = false)
private Long musicId;
@CreationTimestamp
@Column(nullable = false, updatable = false)
private LocalDateTime createTime;
}

View File

@@ -1,109 +1,109 @@
package com.test.musichouduan.init;
import cn.dev33.satoken.secure.BCrypt;
import com.test.musichouduan.entity.*;
import com.test.musichouduan.repository.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.List;
/**
* 数据初始化器
*/
@Component
public class DataInitializer implements CommandLineRunner {
@Autowired
private UserRepository userRepository;
@Autowired
private MusicTypeRepository musicTypeRepository;
@Autowired
private SingerTypeRepository singerTypeRepository;
@Autowired
private PlaylistTypeRepository playlistTypeRepository;
@Override
@Transactional
public void run(String... args) throws Exception {
// 初始化管理员用户
initAdminUser();
// 初始化音乐类型
initMusicTypes();
// 初始化歌手类型
initSingerTypes();
// 初始化歌单类型
initPlaylistTypes();
}
/**
* 初始化管理员用户
*/
private void initAdminUser() {
if (!userRepository.existsByUsername("admin")) {
User admin = new User();
admin.setUsername("admin");
admin.setPassword(BCrypt.hashpw("123456"));
admin.setNickname("管理员");
admin.setGender(0);
admin.setRole(1);
admin.setStatus(1);
userRepository.save(admin);
}
}
/**
* 初始化音乐类型
*/
private void initMusicTypes() {
if (musicTypeRepository.count() == 0) {
List<String> typeNames = Arrays.asList("流行", "摇滚", "民谣", "电子", "古典", "嘻哈", "爵士", "乡村");
for (String name : typeNames) {
MusicType type = new MusicType();
type.setName(name);
type.setDescription(name + "音乐");
musicTypeRepository.save(type);
}
}
}
/**
* 初始化歌手类型
*/
private void initSingerTypes() {
if (singerTypeRepository.count() == 0) {
List<String> typeNames = Arrays.asList("华语男歌手", "华语女歌手", "华语组合", "欧美男歌手", "欧美女歌手", "欧美组合", "日韩男歌手", "日韩女歌手", "日韩组合");
for (String name : typeNames) {
SingerType type = new SingerType();
type.setName(name);
type.setDescription(name);
singerTypeRepository.save(type);
}
}
}
/**
* 初始化歌单类型
*/
private void initPlaylistTypes() {
if (playlistTypeRepository.count() == 0) {
List<String> typeNames = Arrays.asList("流行", "摇滚", "民谣", "电子", "古典", "嘻哈", "爵士", "乡村", "轻音乐", "ACG");
for (String name : typeNames) {
PlaylistType type = new PlaylistType();
type.setName(name);
type.setDescription(name + "歌单");
playlistTypeRepository.save(type);
}
}
}
}
//package com.test.musichouduan.init;
//
//import cn.dev33.satoken.secure.BCrypt;
//import com.test.musichouduan.entity.*;
//import com.test.musichouduan.repository.*;
//
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.boot.CommandLineRunner;
//import org.springframework.stereotype.Component;
//import org.springframework.transaction.annotation.Transactional;
//
//import java.util.Arrays;
//import java.util.List;
//
///**
// * 数据初始化器
// */
//@Component
//public class DataInitializer implements CommandLineRunner {
//
// @Autowired
// private UserRepository userRepository;
//
// @Autowired
// private MusicTypeRepository musicTypeRepository;
//
// @Autowired
// private SingerTypeRepository singerTypeRepository;
//
// @Autowired
// private PlaylistTypeRepository playlistTypeRepository;
//
// @Override
// @Transactional
// public void run(String... args) throws Exception {
// // 初始化管理员用户
// initAdminUser();
//
// // 初始化音乐类型
// initMusicTypes();
//
// // 初始化歌手类型
// initSingerTypes();
//
// // 初始化歌单类型
// initPlaylistTypes();
// }
//
// /**
// * 初始化管理员用户
// */
// private void initAdminUser() {
// if (!userRepository.existsByUsername("admin")) {
// User admin = new User();
// admin.setUsername("admin");
// admin.setPassword(BCrypt.hashpw("123456"));
// admin.setNickname("管理员");
// admin.setGender(0);
// admin.setRole(1);
// admin.setStatus(1);
// userRepository.save(admin);
// }
// }
//
// /**
// * 初始化音乐类型
// */
// private void initMusicTypes() {
// if (musicTypeRepository.count() == 0) {
// List<String> typeNames = Arrays.asList("流行", "摇滚", "民谣", "电子", "古典", "嘻哈", "爵士", "乡村");
// for (String name : typeNames) {
// MusicType type = new MusicType();
// type.setName(name);
// type.setDescription(name + "音乐");
// musicTypeRepository.save(type);
// }
// }
// }
//
// /**
// * 初始化歌手类型
// */
// private void initSingerTypes() {
// if (singerTypeRepository.count() == 0) {
// List<String> typeNames = Arrays.asList("华语男歌手", "华语女歌手", "华语组合", "欧美男歌手", "欧美女歌手", "欧美组合", "日韩男歌手", "日韩女歌手", "日韩组合");
// for (String name : typeNames) {
// SingerType type = new SingerType();
// type.setName(name);
// type.setDescription(name);
// singerTypeRepository.save(type);
// }
// }
// }
//
// /**
// * 初始化歌单类型
// */
// private void initPlaylistTypes() {
// if (playlistTypeRepository.count() == 0) {
// List<String> typeNames = Arrays.asList("流行", "摇滚", "民谣", "电子", "古典", "嘻哈", "爵士", "乡村", "轻音乐", "ACG");
// for (String name : typeNames) {
// PlaylistType type = new PlaylistType();
// type.setName(name);
// type.setDescription(name + "歌单");
// playlistTypeRepository.save(type);
// }
// }
// }
//}

View File

@@ -43,6 +43,15 @@ public interface UserCollectMusicRepository extends JpaRepository<UserCollectMus
*/
boolean existsByUserAndMusic(User user, Music music);
/**
* 判断用户是否收藏了音乐
*
* @param userId 用户ID
* @param musicId 音乐ID
* @return 是否收藏
*/
boolean existsByUserIdAndMusicId(Long userId, Long musicId);
/**
* 根据用户统计收藏的音乐数量
*

View File

@@ -0,0 +1,29 @@
package com.test.musichouduan.repository;
import com.test.musichouduan.entity.UserLikeMusic;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserLikeMusicRepository extends JpaRepository<UserLikeMusic, Long> {
/**
* 根据用户ID和音乐ID查找点赞记录
*
* @param userId 用户ID
* @param musicId 音乐ID
* @return 点赞记录
*/
Optional<UserLikeMusic> findByUserIdAndMusicId(Long userId, Long musicId);
/**
* 检查用户是否点赞了某音乐
*
* @param userId 用户ID
* @param musicId 音乐ID
* @return 是否存在
*/
boolean existsByUserIdAndMusicId(Long userId, Long musicId);
}

View File

@@ -13,6 +13,15 @@ import java.util.concurrent.CompletableFuture;
*/
public interface MusicService {
/**
* 根据ID获取音乐并根据用户ID判断是否收藏和点赞
*
* @param id 音乐ID
* @param userId 用户ID (可以为null)
* @return 音乐DTO
*/
MusicDTO getMusicById(Long id, Long userId);
/**
* 根据ID获取音乐
*
@@ -133,4 +142,13 @@ public interface MusicService {
* @return CompletableFuture 包装的音乐DTO列表
*/
CompletableFuture<List<MusicDTO>> updateHotMusicCache();
/**
* 切换音乐点赞状态
*
* @param musicId 音乐ID
* @param userId 用户ID
* @return 是否点赞成功 (true: 点赞, false: 取消点赞)
*/
boolean toggleLikeMusic(Long musicId, Long userId);
}

View File

@@ -7,6 +7,8 @@ import com.test.musichouduan.entity.Music;
import com.test.musichouduan.entity.MusicType;
import com.test.musichouduan.entity.Singer;
import com.test.musichouduan.entity.User;
import com.test.musichouduan.entity.UserCollectMusic;
import com.test.musichouduan.entity.UserLikeMusic;
import com.test.musichouduan.entity.UserMusic;
import com.test.musichouduan.exception.BusinessException;
import com.fasterxml.jackson.core.type.TypeReference;
@@ -72,7 +74,10 @@ public class MusicServiceImpl implements MusicService {
private UserRepository userRepository;
@Autowired
private UserMusicRepository userMusicRepository;
private UserCollectMusicRepository userCollectMusicRepository;
@Autowired
private UserLikeMusicRepository userLikeMusicRepository;
@Autowired
private PlayHistoryRepository playHistoryRepository;
@@ -81,13 +86,19 @@ public class MusicServiceImpl implements MusicService {
private FileService fileService;
@Override
public MusicDTO getMusicById(Long id) {
public MusicDTO getMusicById(Long id, Long userId) {
// 查找音乐如果不存在则返回null
return musicRepository.findById(id)
.map(this::convertToDTO)
.map(music -> convertToDTO(music, userId))
.orElse(null);
}
@Override
public MusicDTO getMusicById(Long id) {
// 对于未登录用户或不需要用户状态的场景直接调用新方法并传入null
return getMusicById(id, null);
}
@Override
public PageResult<MusicDTO> getMusicList(Integer page, Integer size, String keyword, Long typeId) {
// 构建查询条件
@@ -463,35 +474,37 @@ public class MusicServiceImpl implements MusicService {
@Override
@Transactional
public boolean toggleCollectMusic(Long id, Long userId) {
// 获取用户
Optional<User> userOpt = userRepository.findById(userId);
if (!userOpt.isPresent()) {
// 如果用户不存在返回false
return false;
}
User user = userOpt.get();
// 获取音乐
Optional<Music> musicOpt = musicRepository.findById(id);
if (!musicOpt.isPresent()) {
// 如果音乐不存在返回false
return false;
}
Music music = musicOpt.get();
// 检查是否已收藏
boolean isCollected = userMusicRepository.existsByUserAndMusic(user, music);
public boolean toggleCollectMusic(Long musicId, Long userId) {
// 直接通过ID判断是否存在避免查询完整实体
boolean isCollected = userCollectMusicRepository.existsByUserIdAndMusicId(userId, musicId);
Music music = musicRepository.findById(musicId)
.orElseThrow(() -> new BusinessException("音乐不存在"));
if (isCollected) {
// 如果已收藏,取消收藏
userMusicRepository.uncollectMusic(user.getId(), music.getId());
// 已收藏,取消收藏
User user = userRepository.findById(userId).orElseThrow(() -> new BusinessException("用户不存在"));
UserCollectMusic collect = userCollectMusicRepository.findByUserAndMusic(user, music)
.orElseThrow(() -> new BusinessException("收藏记录不存在"));
userCollectMusicRepository.delete(collect);
int currentCollectCount = music.getCollectCount() != null ? music.getCollectCount() : 0;
music.setCollectCount(Math.max(0, currentCollectCount - 1));
musicRepository.save(music);
return false; // 返回 "未收藏" 状态
} else {
// 如果未收藏,收藏
userMusicRepository.collectMusic(user.getId(), music.getId());
}
// 未收藏,执行收藏
User user = userRepository.findById(userId).orElseThrow(() -> new BusinessException("用户不存在"));
UserCollectMusic newCollect = new UserCollectMusic();
newCollect.setUser(user);
newCollect.setMusic(music);
userCollectMusicRepository.save(newCollect);
return !isCollected; // 返回当前收藏状态true表示已收藏false表示未收藏
int currentCollectCount = music.getCollectCount() != null ? music.getCollectCount() : 0;
music.setCollectCount(currentCollectCount + 1);
musicRepository.save(music);
return true; // 返回 "已收藏" 状态
}
}
@Override
@@ -501,24 +514,24 @@ public class MusicServiceImpl implements MusicService {
.orElseThrow(() -> new BusinessException("用户不存在"));
// 获取用户收藏的音乐
Page<UserMusic> userMusicPage = userMusicRepository.findCollectedMusic(
user.getId(),
PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "collectTime"))
Page<UserCollectMusic> userCollectMusicPage = userCollectMusicRepository.findByUser(
user,
PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "createTime"))
);
// 转换为DTO
List<MusicDTO> musicDTOs = userMusicPage.getContent().stream()
.map(userMusic -> {
MusicDTO musicDTO = convertToDTO(userMusic.getMusic());
List<MusicDTO> musicDTOs = userCollectMusicPage.getContent().stream()
.map(userCollectMusic -> {
MusicDTO musicDTO = convertToDTO(userCollectMusic.getMusic());
// 设置收藏时间
musicDTO.setCollectTime(userMusic.getCollectTime());
musicDTO.setCollectTime(userCollectMusic.getCreateTime());
return musicDTO;
})
.collect(Collectors.toList());
return new PageResult<>(
musicDTOs,
userMusicPage.getTotalElements(),
userCollectMusicPage.getTotalElements(),
page,
size
);
@@ -539,8 +552,6 @@ public class MusicServiceImpl implements MusicService {
// 增加播放次数
long currentPlayCount = music.getPlayCount() != null ? music.getPlayCount() : 0;
music.setPlayCount(currentPlayCount + 1);
// 根据要求,将点赞数与播放次数同步
music.setLikeCount((int) (currentPlayCount + 1));
musicRepository.save(music);
// 如果用户已登录,记录播放历史
@@ -560,6 +571,34 @@ public class MusicServiceImpl implements MusicService {
return musicRepository.findById(id).orElse(null);
}
@Override
@Transactional
public boolean toggleLikeMusic(Long musicId, Long userId) {
Music music = musicRepository.findById(musicId)
.orElseThrow(() -> new BusinessException("音乐不存在"));
Optional<UserLikeMusic> likeOpt = userLikeMusicRepository.findByUserIdAndMusicId(userId, musicId);
if (likeOpt.isPresent()) {
// 已点赞,取消点赞
userLikeMusicRepository.delete(likeOpt.get());
int currentLikeCount = music.getLikeCount() != null ? music.getLikeCount() : 0;
music.setLikeCount(Math.max(0, currentLikeCount - 1));
musicRepository.save(music);
return false; // 返回 "未点赞" 状态
} else {
// 未点赞,执行点赞
UserLikeMusic newLike = new UserLikeMusic();
newLike.setUserId(userId);
newLike.setMusicId(musicId);
userLikeMusicRepository.save(newLike);
int currentLikeCount = music.getLikeCount() != null ? music.getLikeCount() : 0;
music.setLikeCount(currentLikeCount + 1);
musicRepository.save(music);
return true; // 返回 "已点赞" 状态
}
}
/**
* 将实体转换为DTO
*
@@ -567,52 +606,49 @@ public class MusicServiceImpl implements MusicService {
* @return 音乐DTO
*/
private MusicDTO convertToDTO(Music music) {
// 保留此方法以兼容不需要用户状态的转换
return convertToDTO(music, null);
}
/**
* 将实体转换为DTO并根据用户ID判断是否收藏和点赞
*
* @param music 音乐实体
* @param userId 用户ID (可以为null)
* @return 音乐DTO
*/
private MusicDTO convertToDTO(Music music, Long userId) {
MusicDTO musicDTO = new MusicDTO();
BeanUtils.copyProperties(music, musicDTO);
// 确保专辑字段正确复制
musicDTO.setAlbum(music.getAlbum());
// 设置类型ID列表
if (music.getTypes() != null) {
List<Long> typeIds = music.getTypes().stream()
.map(MusicType::getId)
.collect(Collectors.toList());
musicDTO.setTypeIds(typeIds);
// 设置类型名称列表
List<String> typeNames = music.getTypes().stream()
.map(MusicType::getName)
.collect(Collectors.toList());
musicDTO.setTypeNames(typeNames);
// 设置类型ID和名称列表
if (!CollectionUtils.isEmpty(music.getTypes())) {
musicDTO.setTypeIds(music.getTypes().stream().map(MusicType::getId).collect(Collectors.toList()));
musicDTO.setTypeNames(music.getTypes().stream().map(MusicType::getName).collect(Collectors.toList()));
}
// 设置歌手ID列表
if (music.getSingers() != null) {
List<Long> singerIds = music.getSingers().stream()
.map(Singer::getId)
.collect(Collectors.toList());
musicDTO.setSingerIds(singerIds);
// 设置歌手名称列表
List<String> singerNames = music.getSingers().stream()
.map(Singer::getName)
.collect(Collectors.toList());
// 设置歌手ID和名称列表
if (!CollectionUtils.isEmpty(music.getSingers())) {
musicDTO.setSingerIds(music.getSingers().stream().map(Singer::getId).collect(Collectors.toList()));
List<String> singerNames = music.getSingers().stream().map(Singer::getName).collect(Collectors.toList());
musicDTO.setSingerNames(singerNames);
// 设置歌手名称字符串(用于前端显示)
if (!singerNames.isEmpty()) {
musicDTO.setSinger(String.join(", ", singerNames));
}
musicDTO.setSinger(String.join("", singerNames));
}
// 检查当前登录用户是否已收藏该音乐
if (StpUtil.isLogin()) {
Long userId = StpUtil.getLoginIdAsLong();
boolean isCollected = userMusicRepository.existsByUserIdAndMusicId(userId, music.getId());
// 如果传入了用户ID则查询该用户的收藏和点赞状态
if (userId != null) {
boolean isCollected = userCollectMusicRepository.existsByUserIdAndMusicId(userId, music.getId());
musicDTO.setCollected(isCollected);
boolean isLiked = userLikeMusicRepository.existsByUserIdAndMusicId(userId, music.getId());
musicDTO.setLiked(isLiked);
} else {
// 如果未传入用户ID则默认为未收藏和未点赞
musicDTO.setCollected(false);
musicDTO.setLiked(false);
}
return musicDTO;