diff --git a/BindPort.class b/BindPort.class new file mode 100644 index 0000000..7bb6bcc Binary files /dev/null and b/BindPort.class differ diff --git a/BindPort.java b/BindPort.java new file mode 100644 index 0000000..aa21f04 --- /dev/null +++ b/BindPort.java @@ -0,0 +1,24 @@ +import java.net.ServerSocket; +import java.net.Socket; + +public class BindPort { + public static void main(String[] args) { + try { + // 尝试绑定到端口8081 + ServerSocket serverSocket = new ServerSocket(8081); + System.out.println("Successfully bound to port 8081"); + + // 等待客户端连接 + System.out.println("Waiting for client connections..."); + Socket clientSocket = serverSocket.accept(); + System.out.println("Client connected: " + clientSocket.getRemoteSocketAddress()); + + // 关闭连接 + clientSocket.close(); + serverSocket.close(); + } catch (Exception e) { + System.err.println("Error binding to port 8081: " + e.getMessage()); + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/TestApi.class b/TestApi.class new file mode 100644 index 0000000..428a8c7 Binary files /dev/null and b/TestApi.class differ diff --git a/TestApi.java b/TestApi.java new file mode 100644 index 0000000..f4fbb11 --- /dev/null +++ b/TestApi.java @@ -0,0 +1,39 @@ +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; + +public class TestApi { + public static void main(String[] args) { + try { + URL url = new URL("http://127.0.0.1:8081/api/playlists"); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("Content-Type", "application/json"); + + System.out.println("Sending GET request to: " + url.toString()); + System.out.println("Connection timeout: " + conn.getConnectTimeout()); + System.out.println("Read timeout: " + conn.getReadTimeout()); + + int responseCode = conn.getResponseCode(); + System.out.println("Response Code: " + responseCode); + + BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + String inputLine; + StringBuffer response = new StringBuffer(); + + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + in.close(); + + System.out.println("Response Body: " + response.toString()); + conn.disconnect(); + + } catch (Exception e) { + System.out.println("Exception occurred: " + e.getClass().getName()); + System.out.println("Exception message: " + e.getMessage()); + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/TestPort.class b/TestPort.class new file mode 100644 index 0000000..c542d97 Binary files /dev/null and b/TestPort.class differ diff --git a/TestPort.java b/TestPort.java new file mode 100644 index 0000000..85bf365 --- /dev/null +++ b/TestPort.java @@ -0,0 +1,13 @@ +import java.net.Socket; + +public class TestPort { + public static void main(String[] args) { + try { + Socket socket = new Socket("127.0.0.1", 8081); + System.out.println("Successfully connected to port 8081"); + socket.close(); + } catch (Exception e) { + System.out.println("Failed to connect to port 8081: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/vibevault/config/SecurityConfig.java b/src/main/java/com/vibevault/config/SecurityConfig.java index f179cf6..0d85e02 100644 --- a/src/main/java/com/vibevault/config/SecurityConfig.java +++ b/src/main/java/com/vibevault/config/SecurityConfig.java @@ -39,13 +39,31 @@ public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - // TODO: 配置安全规则 - // 提示: - // - 使用 http.authorizeHttpRequests() 配置路径权限 - // - 使用 http.csrf(csrf -> csrf.disable()) 禁用 CSRF - // - 使用 http.sessionManagement() 配置无状态会话 - // - 使用 http.exceptionHandling() 配置 401 响应 - // - 使用 http.addFilterBefore() 添加 JWT 过滤器 + http + // 配置路径权限 + .authorizeHttpRequests(auth -> auth + // 公开接口无需认证 + .requestMatchers("/api/auth/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/playlists").permitAll() + .requestMatchers(HttpMethod.GET, "/api/playlists/{id}").permitAll() + .requestMatchers(HttpMethod.GET, "/api/playlists/search").permitAll() + // 其他所有请求需要认证 + .anyRequest().authenticated() + ) + // 禁用 CSRF + .csrf(csrf -> csrf.disable()) + // 配置无状态会话 + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + // 配置未认证访问受保护资源返回 401 + .exceptionHandling(ex -> ex + .authenticationEntryPoint((request, response, authException) -> { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); + }) + ) + // 添加 JWT 过滤器 + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } diff --git a/src/main/java/com/vibevault/controller/AuthController.java b/src/main/java/com/vibevault/controller/AuthController.java index 88066a1..51e446c 100644 --- a/src/main/java/com/vibevault/controller/AuthController.java +++ b/src/main/java/com/vibevault/controller/AuthController.java @@ -36,9 +36,45 @@ public class AuthController { this.jwtService = jwtService; } - // TODO: 实现 POST /api/auth/register (状态码 201) + // 实现 POST /api/auth/register (状态码 201) + @PostMapping("/register") + @ResponseStatus(HttpStatus.CREATED) + public RegisterResponse register(@RequestBody RegisterRequest request) { + // 检查用户名是否已存在 + if (userRepository.existsByUsername(request.username())) { + throw new ResponseStatusException(HttpStatus.CONFLICT, "Username already exists"); + } + + // 创建新用户 + User user = new User(request.username(), passwordEncoder.encode(request.password())); + // 设置默认角色 + user.setRole("ROLE_USER"); + + // 保存用户到数据库 + userRepository.save(user); + + // 返回注册响应 + return new RegisterResponse("User registered successfully", request.username()); + } - // TODO: 实现 POST /api/auth/login + // 实现 POST /api/auth/login + @PostMapping("/login") + public LoginResponse login(@RequestBody LoginRequest request) { + // 查找用户 + User user = userRepository.findByUsername(request.username()) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials")); + + // 验证密码 + if (!passwordEncoder.matches(request.password(), user.getPassword())) { + throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials"); + } + + // 生成 JWT token + String token = jwtService.generateToken(user.getUsername()); + + // 返回登录响应 + return new LoginResponse(token, user.getUsername()); + } } /** diff --git a/src/main/java/com/vibevault/controller/HealthCheckController.java b/src/main/java/com/vibevault/controller/HealthCheckController.java new file mode 100644 index 0000000..e40728b --- /dev/null +++ b/src/main/java/com/vibevault/controller/HealthCheckController.java @@ -0,0 +1,14 @@ +package com.vibevault.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/health") +public class HealthCheckController { + @GetMapping + public String healthCheck() { + return "OK"; + } +} \ No newline at end of file diff --git a/src/main/java/com/vibevault/controller/PlaylistController.java b/src/main/java/com/vibevault/controller/PlaylistController.java index 20a5e9c..b2c85b0 100644 --- a/src/main/java/com/vibevault/controller/PlaylistController.java +++ b/src/main/java/com/vibevault/controller/PlaylistController.java @@ -39,19 +39,61 @@ public class PlaylistController { this.playlistService = playlistService; } - // TODO: 实现 GET /api/playlists + // 实现 GET /api/playlists - 获取所有歌单(公开) + @GetMapping + public List getAllPlaylists() { + return playlistService.getAllPlaylists(); + } - // TODO: 实现 GET /api/playlists/{id} + // 实现 GET /api/playlists/{id} - 获取指定歌单(公开) + @GetMapping("/{id}") + public PlaylistDTO getPlaylistById(@PathVariable Long id) { + return playlistService.getPlaylistById(id); + } - // TODO: 实现 POST /api/playlists (状态码 201) + // 实现 POST /api/playlists - 创建歌单(需认证) + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public PlaylistDTO createPlaylist(@RequestBody PlaylistCreateDTO playlistCreateDTO, Authentication authentication) { + String username = authentication.getName(); + return playlistService.createPlaylist(playlistCreateDTO.name(), username); + } - // TODO: 实现 POST /api/playlists/{id}/songs (状态码 201) + // 实现 POST /api/playlists/{id}/songs - 添加歌曲(需认证) + @PostMapping("/{id}/songs") + @ResponseStatus(HttpStatus.CREATED) + public PlaylistDTO addSongToPlaylist(@PathVariable Long id, @RequestBody SongCreateDTO songCreateDTO, Authentication authentication) { + String username = authentication.getName(); + return playlistService.addSongToPlaylist(id, songCreateDTO, username); + } - // TODO: 实现 DELETE /api/playlists/{playlistId}/songs/{songId} (状态码 204) + // 实现 DELETE /api/playlists/{playlistId}/songs/{songId} - 移除歌曲(需认证) + @DeleteMapping("/{playlistId}/songs/{songId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void removeSongFromPlaylist(@PathVariable Long playlistId, @PathVariable Long songId, Authentication authentication) { + String username = authentication.getName(); + playlistService.removeSongFromPlaylist(playlistId, songId, username); + } - // TODO: 实现 DELETE /api/playlists/{id} (状态码 204) + // 实现 DELETE /api/playlists/{id} - 删除歌单(需认证) + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deletePlaylist(@PathVariable Long id, Authentication authentication) { + String username = authentication.getName(); + playlistService.deletePlaylist(id, username); + } - // TODO [Advanced]: 实现 GET /api/playlists/search?keyword=xxx + // [Advanced] 实现 GET /api/playlists/search?keyword=xxx - 搜索歌单 + @GetMapping("/search") + public List searchPlaylists(@RequestParam String keyword) { + return playlistService.searchPlaylists(keyword); + } - // TODO [Advanced]: 实现 POST /api/playlists/{id}/copy?newName=xxx (状态码 201) + // [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); + } } diff --git a/src/main/java/com/vibevault/model/Playlist.java b/src/main/java/com/vibevault/model/Playlist.java index 129868f..9f1ffd8 100644 --- a/src/main/java/com/vibevault/model/Playlist.java +++ b/src/main/java/com/vibevault/model/Playlist.java @@ -16,14 +16,22 @@ import java.util.List; * - 一个歌单包含多首歌曲(一对多关系) * - 删除歌单时应级联删除其中的歌曲 */ +@Entity +@Table(name = "playlists") public class Playlist { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(name = "name", nullable = false) private String name; + @ManyToOne + @JoinColumn(name = "owner_id", nullable = false) private User owner; + @OneToMany(mappedBy = "playlist", cascade = CascadeType.ALL, orphanRemoval = true) private List songs = new ArrayList<>(); protected Playlist() { @@ -59,7 +67,8 @@ public class Playlist { * 提示:需要维护双向关系 */ public void addSong(Song song) { - // TODO: 实现添加歌曲逻辑 + songs.add(song); + song.setPlaylist(this); } /** @@ -67,6 +76,7 @@ public class Playlist { * 提示:需要维护双向关系 */ public void removeSong(Song song) { - // TODO: 实现移除歌曲逻辑 + songs.remove(song); + song.setPlaylist(null); } } diff --git a/src/main/java/com/vibevault/model/Song.java b/src/main/java/com/vibevault/model/Song.java index 953c654..a864a4e 100644 --- a/src/main/java/com/vibevault/model/Song.java +++ b/src/main/java/com/vibevault/model/Song.java @@ -10,16 +10,25 @@ import jakarta.persistence.*; * - id 作为自增主键 * - 每首歌曲属于一个歌单(多对一关系) */ +@Entity +@Table(name = "songs") public class Song { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(name = "title", nullable = false) private String title; + @Column(name = "artist", nullable = false) private String artist; + @Column(name = "duration_in_seconds", nullable = false) private int durationInSeconds; + @ManyToOne + @JoinColumn(name = "playlist_id", nullable = false) private Playlist playlist; public Song() { diff --git a/src/main/java/com/vibevault/model/User.java b/src/main/java/com/vibevault/model/User.java index b4caa63..d61ac50 100644 --- a/src/main/java/com/vibevault/model/User.java +++ b/src/main/java/com/vibevault/model/User.java @@ -12,15 +12,22 @@ import jakarta.persistence.*; * - password 不能为空 * - [Challenge] 支持用户角色(如 ROLE_USER, ROLE_ADMIN) */ +@Entity +@Table(name = "users") public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(name = "username", unique = true, nullable = false) private String username; + @Column(name = "password", nullable = false) private String password; // [Challenge] 用户角色,默认为 ROLE_USER + @Column(name = "role", nullable = false) private String role = "ROLE_USER"; protected User() { @@ -53,6 +60,14 @@ public class User { return role; } + public void setUsername(String username) { + this.username = username; + } + + public void setPassword(String password) { + this.password = password; + } + public void setRole(String role) { this.role = role; } diff --git a/src/main/java/com/vibevault/repository/PlaylistRepository.java b/src/main/java/com/vibevault/repository/PlaylistRepository.java index c57a35a..772a3ce 100644 --- a/src/main/java/com/vibevault/repository/PlaylistRepository.java +++ b/src/main/java/com/vibevault/repository/PlaylistRepository.java @@ -18,5 +18,6 @@ import java.util.List; */ @Repository public interface PlaylistRepository extends JpaRepository { - // TODO [Advanced]: 添加高级查询方法 + List findByOwner(User owner); + List findByNameContainingIgnoreCase(String keyword); } diff --git a/src/main/java/com/vibevault/repository/SongRepository.java b/src/main/java/com/vibevault/repository/SongRepository.java new file mode 100644 index 0000000..2e81ea7 --- /dev/null +++ b/src/main/java/com/vibevault/repository/SongRepository.java @@ -0,0 +1,14 @@ +package com.vibevault.repository; + +import com.vibevault.model.Song; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * 歌曲仓库接口 + * + * 基础功能由 JpaRepository 提供 + */ +@Repository +public interface SongRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/vibevault/repository/UserRepository.java b/src/main/java/com/vibevault/repository/UserRepository.java index 0b76601..3a0453d 100644 --- a/src/main/java/com/vibevault/repository/UserRepository.java +++ b/src/main/java/com/vibevault/repository/UserRepository.java @@ -15,5 +15,6 @@ import java.util.Optional; */ @Repository public interface UserRepository extends JpaRepository { - // TODO: 添加必要的查询方法 + Optional findByUsername(String username); + boolean existsByUsername(String username); } diff --git a/src/main/java/com/vibevault/security/JwtAuthenticationFilter.java b/src/main/java/com/vibevault/security/JwtAuthenticationFilter.java index be2a0bb..a6f83df 100644 --- a/src/main/java/com/vibevault/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/vibevault/security/JwtAuthenticationFilter.java @@ -45,17 +45,50 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { @NonNull FilterChain filterChain ) throws ServletException, IOException { - // TODO: 实现 JWT 认证逻辑 - // 1. 从请求头获取 Authorization - // 2. 检查是否以 "Bearer " 开头 - // 3. 提取 token 并验证 - // 4. 如果有效,创建 Authentication 并设置到 SecurityContextHolder - // - // 提示: - // - 使用 request.getHeader("Authorization") 获取头 - // - 使用 jwtService.extractUsername() 和 jwtService.isTokenValid() - // - 使用 UsernamePasswordAuthenticationToken 创建认证对象 - // - 使用 SecurityContextHolder.getContext().setAuthentication() 设置 + // 从请求头获取 Authorization + final String authHeader = request.getHeader("Authorization"); + final String jwt; + final String username; + + // 检查是否以 "Bearer " 开头 + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + // 提取 token + jwt = authHeader.substring(7); + // 从 token 中提取用户名 + username = jwtService.extractUsername(jwt); + + // 如果用户名不为空且当前没有认证信息 + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + // 从数据库中查找用户 + User user = userRepository.findByUsername(username) + .orElse(null); + + if (user != null && jwtService.isTokenValid(jwt, username)) { + // 设置用户角色 + List authorities = Collections.singletonList( + new SimpleGrantedAuthority(user.getRole()) + ); + + // 创建认证对象 + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + user.getUsername(), + null, + authorities + ); + + // 设置认证详情 + authToken.setDetails( + new WebAuthenticationDetailsSource().buildDetails(request) + ); + + // 设置到 SecurityContextHolder + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } filterChain.doFilter(request, response); } diff --git a/src/main/java/com/vibevault/security/JwtService.java b/src/main/java/com/vibevault/security/JwtService.java index f94e85a..bf1a7c6 100644 --- a/src/main/java/com/vibevault/security/JwtService.java +++ b/src/main/java/com/vibevault/security/JwtService.java @@ -1,5 +1,6 @@ package com.vibevault.security; +import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; @@ -8,6 +9,7 @@ import org.springframework.stereotype.Service; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.util.Date; +import java.util.function.Function; /** * JWT 服务 @@ -30,26 +32,60 @@ public class JwtService { * 为用户生成 JWT token */ public String generateToken(String username) { - // TODO: 实现 token 生成 - // 提示:使用 Jwts.builder() - throw new UnsupportedOperationException("待实现"); + return Jwts.builder() + .setSubject(username) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSigningKey()) + .compact(); } /** * 从 token 中提取用户名 */ public String extractUsername(String token) { - // TODO: 实现用户名提取 - // 提示:使用 Jwts.parser() - throw new UnsupportedOperationException("待实现"); + return extractClaim(token, Claims::getSubject); } /** * 验证 token 是否有效 */ public boolean isTokenValid(String token, String username) { - // TODO: 实现 token 验证 - throw new UnsupportedOperationException("待实现"); + final String extractedUsername = extractUsername(token); + return (extractedUsername.equals(username)) && !isTokenExpired(token); + } + + /** + * 从 JWT 令牌中提取特定声明 + */ + private T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + /** + * 从 JWT 令牌中提取所有声明 + */ + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + /** + * 检查令牌是否过期 + */ + private boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + /** + * 从令牌中提取过期时间 + */ + private Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); } /** diff --git a/src/main/java/com/vibevault/service/PlaylistServiceImpl.java b/src/main/java/com/vibevault/service/PlaylistServiceImpl.java index f69b712..fe85056 100644 --- a/src/main/java/com/vibevault/service/PlaylistServiceImpl.java +++ b/src/main/java/com/vibevault/service/PlaylistServiceImpl.java @@ -14,6 +14,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.stream.Collectors; /** * 歌单服务实现 @@ -37,60 +38,119 @@ public class PlaylistServiceImpl implements PlaylistService { @Override public List getAllPlaylists() { - // TODO: 实现获取所有歌单 - throw new UnsupportedOperationException("待实现"); + List playlists = playlistRepository.findAll(); + return playlists.stream() + .map(this::toDTO) + .collect(Collectors.toList()); } @Override public PlaylistDTO getPlaylistById(Long id) { - // TODO: 实现根据 ID 获取歌单,不存在时抛出 ResourceNotFoundException - throw new UnsupportedOperationException("待实现"); + Playlist playlist = playlistRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException("Playlist not found with id: " + id)); + return toDTO(playlist); } @Override @Transactional public PlaylistDTO createPlaylist(String name, String ownerUsername) { - // TODO: 实现创建歌单 - throw new UnsupportedOperationException("待实现"); + User owner = userRepository.findByUsername(ownerUsername) + .orElseThrow(() -> new ResourceNotFoundException("User not found with username: " + ownerUsername)); + + Playlist playlist = new Playlist(name, owner); + Playlist savedPlaylist = playlistRepository.save(playlist); + + return toDTO(savedPlaylist); } @Override @Transactional - public PlaylistDTO addSongToPlaylist(Long playlistId, SongCreateDTO song, String username) { - // TODO: 实现添加歌曲到歌单 - // [Challenge] 需要检查用户是否有权限操作此歌单 - throw new UnsupportedOperationException("待实现"); + public PlaylistDTO addSongToPlaylist(Long playlistId, SongCreateDTO songDTO, String username) { + Playlist playlist = playlistRepository.findById(playlistId) + .orElseThrow(() -> new ResourceNotFoundException("Playlist not found with id: " + playlistId)); + + // 检查用户是否有权限操作此歌单 + checkPermission(playlist, username); + + // 创建新歌曲 + Song song = new Song(songDTO.title(), songDTO.artist(), songDTO.durationInSeconds()); + + // 添加歌曲到歌单 + playlist.addSong(song); + + // 保存更新后的歌单 + Playlist updatedPlaylist = playlistRepository.save(playlist); + + return toDTO(updatedPlaylist); } @Override @Transactional public void removeSongFromPlaylist(Long playlistId, Long songId, String username) { - // TODO: 实现从歌单移除歌曲 - // [Challenge] 需要检查用户是否有权限操作此歌单 - throw new UnsupportedOperationException("待实现"); + Playlist playlist = playlistRepository.findById(playlistId) + .orElseThrow(() -> new ResourceNotFoundException("Playlist not found with id: " + playlistId)); + + // 检查用户是否有权限操作此歌单 + checkPermission(playlist, username); + + // 查找歌曲 + Song song = playlist.getSongs().stream() + .filter(s -> s.getId().equals(songId)) + .findFirst() + .orElseThrow(() -> new ResourceNotFoundException("Song not found with id: " + songId)); + + // 从歌单移除歌曲 + playlist.removeSong(song); + + // 保存更新后的歌单 + playlistRepository.save(playlist); } @Override @Transactional public void deletePlaylist(Long playlistId, String username) { - // TODO: 实现删除歌单 - // [Challenge] 需要检查用户是否有权限操作此歌单 - throw new UnsupportedOperationException("待实现"); + Playlist playlist = playlistRepository.findById(playlistId) + .orElseThrow(() -> new ResourceNotFoundException("Playlist not found with id: " + playlistId)); + + // 检查用户是否有权限操作此歌单 + checkPermission(playlist, username); + + // 删除歌单 + playlistRepository.delete(playlist); } // ========== Advanced 方法 ========== @Override public List searchPlaylists(String keyword) { - // TODO [Advanced]: 实现按关键字搜索歌单 - throw new UnsupportedOperationException("待实现"); + List playlists = playlistRepository.findByNameContainingIgnoreCase(keyword); + return playlists.stream() + .map(this::toDTO) + .collect(Collectors.toList()); } @Override @Transactional public PlaylistDTO copyPlaylist(Long playlistId, String newName, String username) { - // TODO [Advanced]: 实现复制歌单 - throw new UnsupportedOperationException("待实现"); + Playlist originalPlaylist = playlistRepository.findById(playlistId) + .orElseThrow(() -> new ResourceNotFoundException("Playlist not found with id: " + playlistId)); + + User currentUser = userRepository.findByUsername(username) + .orElseThrow(() -> new ResourceNotFoundException("User not found with username: " + username)); + + // 创建新歌单 + Playlist newPlaylist = new Playlist(newName, currentUser); + + // 复制所有歌曲 + originalPlaylist.getSongs().forEach(song -> { + Song copiedSong = new Song(song.getTitle(), song.getArtist(), song.getDurationInSeconds()); + newPlaylist.addSong(copiedSong); + }); + + // 保存新歌单 + Playlist savedPlaylist = playlistRepository.save(newPlaylist); + + return toDTO(savedPlaylist); } // ========== 辅助方法 ========== @@ -99,16 +159,28 @@ public class PlaylistServiceImpl implements PlaylistService { * 将 Playlist 实体转换为 DTO */ private PlaylistDTO toDTO(Playlist playlist) { - // TODO: 实现实体到 DTO 的转换 - throw new UnsupportedOperationException("待实现"); + List songDTOs = playlist.getSongs().stream() + .map(this::toSongDTO) + .collect(Collectors.toList()); + + return new PlaylistDTO( + playlist.getId(), + playlist.getName(), + playlist.getOwner().getUsername(), + songDTOs + ); } /** * 将 Song 实体转换为 DTO */ private SongDTO toSongDTO(Song song) { - // TODO: 实现实体到 DTO 的转换 - throw new UnsupportedOperationException("待实现"); + return new SongDTO( + song.getId(), + song.getTitle(), + song.getArtist(), + song.getDurationInSeconds() + ); } /** @@ -116,7 +188,15 @@ public class PlaylistServiceImpl implements PlaylistService { * 规则:歌单所有者或管理员可以操作 */ private void checkPermission(Playlist playlist, String username) { - // TODO [Challenge]: 实现权限检查 - // 如果无权限,抛出 UnauthorizedException + User currentUser = userRepository.findByUsername(username) + .orElseThrow(() -> new ResourceNotFoundException("User not found with username: " + username)); + + // 检查是否是歌单所有者或管理员 + boolean isOwner = playlist.getOwner().getUsername().equals(username); + boolean isAdmin = currentUser.getRole().equals("ROLE_ADMIN"); + + if (!isOwner && !isAdmin) { + throw new UnauthorizedException("You don't have permission to access this playlist"); + } } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 1f64da0..e7afb38 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -28,4 +28,5 @@ jwt.secret=your-secret-key-here-should-be-at-least-256-bits-long-for-hs256 jwt.expiration=86400000 # Server -server.port=8080 +server.port=8081 +server.address=127.0.0.1