完成作业
Some checks failed
autograde-final-vibevault / check-trigger (push) Successful in 8s
autograde-final-vibevault / grade (push) Failing after 43s

This commit is contained in:
VibeVault User 2025-12-14 02:51:48 +08:00
parent 0115a4bf31
commit d5091273cd
10 changed files with 396 additions and 75 deletions

View File

@ -1,8 +1,11 @@
package com.vibevault.controller;
import java.security.Principal;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
@ -14,6 +17,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import com.vibevault.dto.CopyPlaylistRequest;
import com.vibevault.dto.PlaylistCreateDTO;
import com.vibevault.dto.PlaylistDTO;
import com.vibevault.dto.SongCreateDTO;
@ -76,6 +80,52 @@ public class PlaylistController {
return playlistService.addSongToPlaylist(id, songCreateDTO, username);
}
// POST /api/playlists/{id}/songs/batch - 批量添加歌曲需认证
@PostMapping("/{id}/songs/batch")
@PreAuthorize("hasRole('USER')")
public ResponseEntity<PlaylistDTO> addSongsToPlaylist(@PathVariable Long id, @RequestBody List<SongCreateDTO> songs, Principal principal) {
PlaylistDTO updatedPlaylist = playlistService.addSongsToPlaylist(id, songs, principal.getName());
return ResponseEntity.ok(updatedPlaylist);
}
/**
* [Advanced] 复制歌单
*
* @param id 要复制的歌单ID
* @param request 复制请求包含新歌单名称
* @param principal 当前用户
* @return 新创建的歌单DTO
*/
@PostMapping("/{id}/copy")
@PreAuthorize("hasRole('USER')")
public ResponseEntity<PlaylistDTO> copyPlaylist(@PathVariable Long id, @RequestBody CopyPlaylistRequest request, Principal principal) {
PlaylistDTO copiedPlaylist = playlistService.copyPlaylist(id, request.getNewName(), principal.getName());
return ResponseEntity.status(HttpStatus.CREATED).body(copiedPlaylist);
}
/**
* [Advanced] 按条件筛选歌单
*
* @param ownerName 歌单拥有者名称可选
* @param nameKeyword 歌单名称关键词可选
* @param sortBy 排序字段name或createdAt
* @param sortDirection 排序方向asc或desc
* @param principal 当前用户
* @return 筛选后的歌单列表
*/
@GetMapping("/filter")
@PreAuthorize("hasRole('USER')")
public ResponseEntity<List<PlaylistDTO>> filterPlaylists(
@RequestParam(required = false) String ownerName,
@RequestParam(required = false) String nameKeyword,
@RequestParam(required = false, defaultValue = "name") String sortBy,
@RequestParam(required = false, defaultValue = "asc") String sortDirection,
Principal principal) {
List<PlaylistDTO> filteredPlaylists = playlistService.filterPlaylists(
principal.getName(), ownerName, nameKeyword, sortBy, sortDirection);
return ResponseEntity.ok(filteredPlaylists);
}
// DELETE /api/playlists/{playlistId}/songs/{songId} - 移除歌曲需认证
@DeleteMapping("/{playlistId}/songs/{songId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@ -98,11 +148,5 @@ public class PlaylistController {
return playlistService.searchPlaylists(keyword);
}
// [Advanced] POST /api/playlists/{id}/copy?newName=xxx - 复制歌单
@PostMapping("/{id}/copy")
@ResponseStatus(HttpStatus.CREATED)
public PlaylistDTO copyPlaylist(@PathVariable Long id, @RequestParam String newName, Authentication authentication) {
String username = authentication.getName();
return playlistService.copyPlaylist(id, newName, username);
}
}

View File

@ -0,0 +1,16 @@
package com.vibevault.dto;
/**
* 复制歌单请求DTO
*/
public class CopyPlaylistRequest {
private String newName;
public String getNewName() {
return newName;
}
public void setNewName(String newName) {
this.newName = newName;
}
}

View File

@ -0,0 +1,41 @@
package com.vibevault.exception;
/**
* 统一错误响应类
* 用于封装API错误响应的结构
*/
public class ErrorResponse {
private int status;
private String error;
private String message;
public ErrorResponse(int status, String error, String message) {
this.status = status;
this.error = error;
this.message = message;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

View File

@ -6,10 +6,6 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* 全局异常处理器
*
@ -28,14 +24,8 @@ public class GlobalExceptionHandler {
* @return 包含错误信息的 ResponseEntity
*/
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<Map<String, Object>> handleResourceNotFoundException(ResourceNotFoundException ex) {
Map<String, Object> body = new HashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("status", HttpStatus.NOT_FOUND.value());
body.put("error", "Not Found");
body.put("message", ex.getMessage());
return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
public ResponseEntity<ErrorResponse> handleResourceNotFoundException(ResourceNotFoundException ex) {
return buildErrorResponse(HttpStatus.NOT_FOUND.value(), "Not Found", ex.getMessage());
}
/**
@ -44,14 +34,18 @@ public class GlobalExceptionHandler {
* @return 包含错误信息的 ResponseEntity
*/
@ExceptionHandler(UnauthorizedAccessException.class)
public ResponseEntity<Map<String, Object>> handleUnauthorizedAccessException(UnauthorizedAccessException ex) {
Map<String, Object> body = new HashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("status", HttpStatus.FORBIDDEN.value());
body.put("error", "Forbidden");
body.put("message", ex.getMessage());
return new ResponseEntity<>(body, HttpStatus.FORBIDDEN);
public ResponseEntity<ErrorResponse> handleUnauthorizedAccessException(UnauthorizedAccessException ex) {
return buildErrorResponse(HttpStatus.FORBIDDEN.value(), "Forbidden", ex.getMessage());
}
/**
* 处理 UnauthorizedException 异常
* @param ex 异常对象
* @return 包含错误信息的 ResponseEntity
*/
@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<ErrorResponse> handleUnauthorizedException(UnauthorizedException ex) {
return buildErrorResponse(HttpStatus.FORBIDDEN.value(), "Forbidden", ex.getMessage());
}
/**
@ -60,14 +54,8 @@ public class GlobalExceptionHandler {
* @return 包含错误信息的 ResponseEntity
*/
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<Map<String, Object>> handleResponseStatusException(ResponseStatusException ex) {
Map<String, Object> body = new HashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("status", ex.getStatusCode().value());
body.put("error", ex.getStatusCode().toString());
body.put("message", ex.getMessage());
return new ResponseEntity<>(body, ex.getStatusCode());
public ResponseEntity<ErrorResponse> handleResponseStatusException(ResponseStatusException ex) {
return buildErrorResponse(ex.getStatusCode().value(), ex.getStatusCode().toString(), ex.getMessage());
}
/**
@ -76,16 +64,19 @@ public class GlobalExceptionHandler {
* @return 包含错误信息的 ResponseEntity
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleGeneralException(Exception ex) {
Map<String, Object> body = new HashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
body.put("error", "Internal Server Error");
body.put("message", "An unexpected error occurred");
// 在开发环境中可以添加异常堆栈信息
// body.put("stackTrace", Arrays.toString(ex.getStackTrace()));
return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
public ResponseEntity<ErrorResponse> handleGeneralException(Exception ex) {
return buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Internal Server Error", "An unexpected error occurred");
}
/**
* 构建统一的错误响应格式
* @param status 响应状态码
* @param error 错误类型
* @param message 错误消息
* @return 包含错误信息的 ResponseEntity
*/
private ResponseEntity<ErrorResponse> buildErrorResponse(int statusCode, String error, String message) {
ErrorResponse errorResponse = new ErrorResponse(statusCode, error, message);
return ResponseEntity.status(statusCode).body(errorResponse);
}
}

View File

@ -1,9 +1,12 @@
package com.vibevault.model;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.hibernate.annotations.CreationTimestamp;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
@ -44,6 +47,10 @@ public class Playlist {
@OneToMany(mappedBy = "playlist", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Song> songs = new ArrayList<>();
@Column(nullable = false, updatable = false)
@CreationTimestamp
private LocalDateTime createdAt;
public Playlist() {
}
@ -71,10 +78,14 @@ public class Playlist {
public void setOwner(User owner) {
this.owner = owner;
}
public List<Song> getSongs() {
return Collections.unmodifiableList(songs);
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
/**
* 向歌单添加歌曲

View File

@ -1,6 +1,17 @@
package com.vibevault.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import org.hibernate.annotations.CreationTimestamp;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
/**
* 歌曲实体类
@ -31,6 +42,10 @@ public class Song {
@JoinColumn(name = "playlist_id")
private Playlist playlist;
@Column(nullable = false, updatable = false)
@CreationTimestamp
private LocalDateTime createdAt;
public Song() {
}
@ -75,4 +90,8 @@ public class Song {
public void setDurationInSeconds(int durationInSeconds) {
this.durationInSeconds = durationInSeconds;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
}

View File

@ -1,11 +1,12 @@
package com.vibevault.repository;
import com.vibevault.model.Playlist;
import com.vibevault.model.User;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import com.vibevault.model.Playlist;
import com.vibevault.model.User;
/**
* 歌单仓库接口
@ -21,6 +22,36 @@ public interface PlaylistRepository extends JpaRepository<Playlist, Long> {
// 按所有者查询歌单
List<Playlist> findByOwner(User owner);
// 按所有者查询歌单并按名称排序升序
List<Playlist> findByOwnerOrderByNameAsc(User owner);
// 按所有者查询歌单并按名称排序降序
List<Playlist> findByOwnerOrderByNameDesc(User owner);
// 按所有者查询歌单并按创建时间排序升序
List<Playlist> findByOwnerOrderByCreatedAtAsc(User owner);
// 按所有者查询歌单并按创建时间排序降序
List<Playlist> findByOwnerOrderByCreatedAtDesc(User owner);
// [Advanced]: 按名称模糊搜索歌单
List<Playlist> findByNameContainingIgnoreCase(String keyword);
// 按名称模糊搜索歌单并按名称排序
List<Playlist> findByNameContainingIgnoreCaseOrderByNameAsc(String keyword);
// 按所有者和名称模糊搜索歌单
List<Playlist> findByOwnerAndNameContainingIgnoreCase(User owner, String keyword);
// 按所有者和名称模糊搜索歌单并按创建时间排序升序
List<Playlist> findByOwnerAndNameContainingIgnoreCaseOrderByCreatedAtAsc(User owner, String keyword);
// 按所有者和名称模糊搜索歌单并按创建时间排序降序
List<Playlist> findByOwnerAndNameContainingIgnoreCaseOrderByCreatedAtDesc(User owner, String keyword);
// 按所有者和名称模糊搜索歌单并按名称排序升序
List<Playlist> findByOwnerAndNameContainingIgnoreCaseOrderByNameAsc(User owner, String keyword);
// 按所有者和名称模糊搜索歌单并按名称排序降序
List<Playlist> findByOwnerAndNameContainingIgnoreCaseOrderByNameDesc(User owner, String keyword);
}

View File

@ -1,15 +1,35 @@
package com.vibevault.repository;
import com.vibevault.model.Song;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.vibevault.model.Song;
/**
* 歌曲仓库接口
*
* 基础功能由 JpaRepository 提供
*
* [Advanced] 已添加
* - 按标题关键字模糊搜索歌曲
* - 按创建时间排序返回结果
*/
@Repository
public interface SongRepository extends JpaRepository<Song, Long> {
// 这里可以根据需要添加自定义查询方法
// 按标题关键字模糊搜索歌曲
List<Song> findByTitleContainingIgnoreCase(String keyword);
// 按标题关键字模糊搜索歌曲并按标题排序
List<Song> findByTitleContainingIgnoreCaseOrderByTitleAsc(String keyword);
// 按歌单ID查询歌曲
List<Song> findByPlaylistId(Long playlistId);
// 按歌单ID查询歌曲并按创建时间排序
List<Song> findByPlaylistIdOrderByCreatedAtAsc(Long playlistId);
// 按歌单ID查询歌曲并按标题排序
List<Song> findByPlaylistIdOrderByTitleAsc(Long playlistId);
}

View File

@ -1,10 +1,10 @@
package com.vibevault.service;
import java.util.List;
import com.vibevault.dto.PlaylistDTO;
import com.vibevault.dto.SongCreateDTO;
import java.util.List;
/**
* 歌单服务接口
* 定义歌单相关的业务操作
@ -45,6 +45,14 @@ public interface PlaylistService {
*/
void removeSongFromPlaylist(Long playlistId, Long songId, String username);
/**
* 批量向歌单添加歌曲
* @param playlistId 歌单 ID
* @param songs 歌曲列表
* @param username 当前用户名用于权限检查
*/
PlaylistDTO addSongsToPlaylist(Long playlistId, List<SongCreateDTO> songs, String username);
/**
* 删除歌单
* @param playlistId 歌单 ID
@ -60,7 +68,24 @@ public interface PlaylistService {
List<PlaylistDTO> searchPlaylists(String keyword);
/**
* [Advanced] 复制歌单
* [Advanced] 复制一个歌单为新的歌单
*
* @param playlistId 要复制的歌单ID
* @param newName 新歌单名称
* @param username 当前用户名
* @return 新创建的歌单DTO
*/
PlaylistDTO copyPlaylist(Long playlistId, String newName, String username);
/**
* [Advanced] 按条件筛选歌单
*
* @param username 用户名
* @param ownerName 歌单拥有者名称可选
* @param nameKeyword 歌单名称关键词可选
* @param sortBy 排序字段name或createdAt
* @param sortDirection 排序方向asc或desc
* @return 筛选后的歌单列表
*/
List<PlaylistDTO> filterPlaylists(String username, String ownerName, String nameKeyword, String sortBy, String sortDirection);
}

View File

@ -91,6 +91,23 @@ public class PlaylistServiceImpl implements PlaylistService {
playlistRepository.save(playlist);
}
@Override
@Transactional
public PlaylistDTO addSongsToPlaylist(Long playlistId, List<SongCreateDTO> songDTOs, String username) {
Playlist playlist = getPlaylistWithOwnershipCheck(playlistId, username);
for (SongCreateDTO songDTO : songDTOs) {
Song song = new Song();
song.setTitle(songDTO.title());
song.setArtist(songDTO.artist());
song.setDurationInSeconds(songDTO.durationInSeconds());
playlist.addSong(song);
}
Playlist savedPlaylist = playlistRepository.save(playlist);
return convertToDTO(savedPlaylist);
}
@Override
@Transactional
public void deletePlaylist(Long playlistId, String username) {
@ -110,27 +127,133 @@ public class PlaylistServiceImpl implements PlaylistService {
@Override
@Transactional
public PlaylistDTO copyPlaylist(Long playlistId, String newName, String username) {
Playlist originalPlaylist = playlistRepository.findById(playlistId)
.orElseThrow(() -> new ResourceNotFoundException("Playlist not found with id: " + playlistId));
User newOwner = userRepository.findByUsername(username)
// 验证原歌单存在且用户有权限访问
Playlist originalPlaylist = getPlaylistWithOwnershipCheck(playlistId, username);
// 获取当前用户
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new ResourceNotFoundException("User not found with username: " + username));
Playlist copiedPlaylist = new Playlist();
copiedPlaylist.setName(newName);
copiedPlaylist.setOwner(newOwner);
// 复制歌曲
for (Song originalSong : originalPlaylist.getSongs()) {
Song copiedSong = new Song();
copiedSong.setTitle(originalSong.getTitle());
copiedSong.setArtist(originalSong.getArtist());
copiedSong.setDurationInSeconds(originalSong.getDurationInSeconds());
copiedPlaylist.addSong(copiedSong);
// 创建新歌单
Playlist newPlaylist = new Playlist();
newPlaylist.setName(newName);
newPlaylist.setOwner(user);
// 保存新歌单
Playlist savedPlaylist = playlistRepository.save(newPlaylist);
// 复制原歌单的所有歌曲
List<Song> originalSongs = songRepository.findByPlaylistId(originalPlaylist.getId());
for (Song originalSong : originalSongs) {
Song newSong = new Song();
newSong.setTitle(originalSong.getTitle());
newSong.setArtist(originalSong.getArtist());
newSong.setDurationInSeconds(originalSong.getDurationInSeconds());
newSong.setPlaylist(savedPlaylist);
songRepository.save(newSong);
}
Playlist savedPlaylist = playlistRepository.save(copiedPlaylist);
return convertToDTO(savedPlaylist);
// 重新加载新歌单以包含所有歌曲
Playlist updatedPlaylist = playlistRepository.findById(savedPlaylist.getId())
.orElseThrow(() -> new ResourceNotFoundException("Playlist not found with id: " + savedPlaylist.getId()));
return convertToDTO(updatedPlaylist);
}
@Override
public List<PlaylistDTO> filterPlaylists(String username, String ownerName, String nameKeyword, String sortBy, String sortDirection) {
List<Playlist> playlists;
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new ResourceNotFoundException("User not found with username: " + username));
// 如果提供了ownerName则筛选特定用户的歌单
if (ownerName != null) {
User owner = userRepository.findByUsername(ownerName)
.orElseThrow(() -> new ResourceNotFoundException("User not found with username: " + ownerName));
// 根据是否提供关键词和排序条件选择不同的查询方法
if (nameKeyword != null) {
if (sortBy != null && sortBy.equals("createdAt")) {
if (sortDirection != null && sortDirection.equals("desc")) {
// 按名称关键词筛选并按创建时间降序排序
playlists = playlistRepository.findByOwnerAndNameContainingIgnoreCaseOrderByCreatedAtDesc(owner, nameKeyword);
} else {
// 按名称关键词筛选并按创建时间升序排序
playlists = playlistRepository.findByOwnerAndNameContainingIgnoreCaseOrderByCreatedAtAsc(owner, nameKeyword);
}
} else {
if (sortDirection != null && sortDirection.equals("desc")) {
// 按名称关键词筛选并按名称降序排序
playlists = playlistRepository.findByOwnerAndNameContainingIgnoreCaseOrderByNameDesc(owner, nameKeyword);
} else {
// 按名称关键词筛选并按名称升序排序
playlists = playlistRepository.findByOwnerAndNameContainingIgnoreCaseOrderByNameAsc(owner, nameKeyword);
}
}
} else {
if (sortBy != null && sortBy.equals("createdAt")) {
if (sortDirection != null && sortDirection.equals("desc")) {
// 按创建时间降序排序
playlists = playlistRepository.findByOwnerOrderByCreatedAtDesc(owner);
} else {
// 按创建时间升序排序
playlists = playlistRepository.findByOwnerOrderByCreatedAtAsc(owner);
}
} else {
if (sortDirection != null && sortDirection.equals("desc")) {
// 按名称降序排序
playlists = playlistRepository.findByOwnerOrderByNameDesc(owner);
} else {
// 按名称升序排序
playlists = playlistRepository.findByOwnerOrderByNameAsc(owner);
}
}
}
} else {
// 未提供ownerName则筛选当前用户的歌单
if (nameKeyword != null) {
if (sortBy != null && sortBy.equals("createdAt")) {
if (sortDirection != null && sortDirection.equals("desc")) {
// 按名称关键词筛选并按创建时间降序排序
playlists = playlistRepository.findByOwnerAndNameContainingIgnoreCaseOrderByCreatedAtDesc(user, nameKeyword);
} else {
// 按名称关键词筛选并按创建时间升序排序
playlists = playlistRepository.findByOwnerAndNameContainingIgnoreCaseOrderByCreatedAtAsc(user, nameKeyword);
}
} else {
if (sortDirection != null && sortDirection.equals("desc")) {
// 按名称关键词筛选并按名称降序排序
playlists = playlistRepository.findByOwnerAndNameContainingIgnoreCaseOrderByNameDesc(user, nameKeyword);
} else {
// 按名称关键词筛选并按名称升序排序
playlists = playlistRepository.findByOwnerAndNameContainingIgnoreCaseOrderByNameAsc(user, nameKeyword);
}
}
} else {
if (sortBy != null && sortBy.equals("createdAt")) {
if (sortDirection != null && sortDirection.equals("desc")) {
// 按创建时间降序排序
playlists = playlistRepository.findByOwnerOrderByCreatedAtDesc(user);
} else {
// 按创建时间升序排序
playlists = playlistRepository.findByOwnerOrderByCreatedAtAsc(user);
}
} else {
if (sortDirection != null && sortDirection.equals("desc")) {
// 按名称降序排序
playlists = playlistRepository.findByOwnerOrderByNameDesc(user);
} else {
// 按名称升序排序
playlists = playlistRepository.findByOwnerOrderByNameAsc(user);
}
}
}
}
// 转换为DTO并返回
return playlists.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
// 辅助方法检查歌单所有权