diff --git a/src/main/java/com/vibevault/config/SecurityConfig.java b/src/main/java/com/vibevault/config/SecurityConfig.java index f179cf6..931bb0a 100644 --- a/src/main/java/com/vibevault/config/SecurityConfig.java +++ b/src/main/java/com/vibevault/config/SecurityConfig.java @@ -39,13 +39,33 @@ 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 + // 1. 禁用 CSRF(REST API 通常不需要) + .csrf(csrf -> csrf.disable()) + + // 2. 配置无状态会话 + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + + // 3. 配置路径权限 + .authorizeHttpRequests(authorize -> authorize + // 公开接口无需认证 + .requestMatchers("/api/auth/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/playlists").permitAll() + .requestMatchers(HttpMethod.GET, "/api/playlists/{id}").permitAll() + // 其他接口需要认证 + .anyRequest().authenticated()) + + // 4. 配置异常处理,未认证返回 401 + .exceptionHandling(exception -> exception + .authenticationEntryPoint((request, response, authException) -> { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json"); + response.getWriter().write("{\"error\":\"Unauthorized\",\"message\":\"Authentication required\"}"); + })) + + // 5. 添加 JWT 过滤器,在 UsernamePasswordAuthenticationFilter 之前执行 + .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..6825381 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) + @PostMapping("/register") + @ResponseStatus(HttpStatus.CREATED) + public RegisterResponse register(@RequestBody RegisterRequest request) { + // 检查用户名是否已存在 + if (userRepository.existsByUsername(request.username())) { + throw new ResponseStatusException(HttpStatus.CONFLICT, "Username already exists"); + } + + // 加密密码 + String encodedPassword = passwordEncoder.encode(request.password()); + + // 创建新用户 + User user = new User(); + user.setUsername(request.username()); + user.setPassword(encodedPassword); + user.setRole("ROLE_USER"); // 默认角色 + + // 保存用户 + userRepository.save(user); + + return new RegisterResponse("User registered successfully", request.username()); + } - // TODO: 实现 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); + + return new LoginResponse(token, user.getUsername()); + } } /** diff --git a/src/main/java/com/vibevault/controller/PlaylistController.java b/src/main/java/com/vibevault/controller/PlaylistController.java index 20a5e9c..e6f057d 100644 --- a/src/main/java/com/vibevault/controller/PlaylistController.java +++ b/src/main/java/com/vibevault/controller/PlaylistController.java @@ -5,8 +5,10 @@ import com.vibevault.dto.PlaylistDTO; import com.vibevault.dto.SongCreateDTO; import com.vibevault.service.PlaylistService; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; +import jakarta.validation.Valid; import java.util.List; @@ -39,19 +41,58 @@ public class PlaylistController { this.playlistService = playlistService; } - // TODO: 实现 GET /api/playlists + @GetMapping + public ResponseEntity> getAllPlaylists() { + List playlists = playlistService.getAllPlaylists(); + return ResponseEntity.ok(playlists); + } - // TODO: 实现 GET /api/playlists/{id} + @GetMapping("/{id}") + public ResponseEntity getPlaylistById(@PathVariable Long id) { + PlaylistDTO playlist = playlistService.getPlaylistById(id); + return ResponseEntity.ok(playlist); + } - // TODO: 实现 POST /api/playlists (状态码 201) + @PostMapping + public ResponseEntity createPlaylist(@Valid @RequestBody PlaylistCreateDTO playlistDTO, Authentication authentication) { + String username = authentication.getName(); + PlaylistDTO createdPlaylist = playlistService.createPlaylist(playlistDTO.name(), username); + return ResponseEntity.status(HttpStatus.CREATED).body(createdPlaylist); + } - // TODO: 实现 POST /api/playlists/{id}/songs (状态码 201) + @PostMapping("/{id}/songs") + public ResponseEntity addSong(@PathVariable Long id, @Valid @RequestBody SongCreateDTO songDTO, Authentication authentication) { + String username = authentication.getName(); + PlaylistDTO updatedPlaylist = playlistService.addSongToPlaylist(id, songDTO, username); + return ResponseEntity.status(HttpStatus.CREATED).body(updatedPlaylist); + } - // TODO: 实现 DELETE /api/playlists/{playlistId}/songs/{songId} (状态码 204) + @DeleteMapping("/{playlistId}/songs/{songId}") + public ResponseEntity removeSong(@PathVariable Long playlistId, @PathVariable Long songId, Authentication authentication) { + String username = authentication.getName(); + playlistService.removeSongFromPlaylist(playlistId, songId, username); + return ResponseEntity.noContent().build(); + } - // TODO: 实现 DELETE /api/playlists/{id} (状态码 204) + @DeleteMapping("/{id}") + public ResponseEntity deletePlaylist(@PathVariable Long id, Authentication authentication) { + String username = authentication.getName(); + playlistService.deletePlaylist(id, username); + return ResponseEntity.noContent().build(); + } - // TODO [Advanced]: 实现 GET /api/playlists/search?keyword=xxx + // ========== Advanced 轨道(进阶) ========== - // TODO [Advanced]: 实现 POST /api/playlists/{id}/copy?newName=xxx (状态码 201) + @GetMapping("/search") + public ResponseEntity> searchPlaylists(@RequestParam String keyword) { + List playlists = playlistService.searchPlaylists(keyword); + return ResponseEntity.ok(playlists); + } + + @PostMapping("/{id}/copy") + public ResponseEntity copyPlaylist(@PathVariable Long id, @RequestParam String newName, Authentication authentication) { + String username = authentication.getName(); + PlaylistDTO copiedPlaylist = playlistService.copyPlaylist(id, newName, username); + return ResponseEntity.status(HttpStatus.CREATED).body(copiedPlaylist); + } } diff --git a/src/main/java/com/vibevault/model/Playlist.java b/src/main/java/com/vibevault/model/Playlist.java index 129868f..6b07b2c 100644 --- a/src/main/java/com/vibevault/model/Playlist.java +++ b/src/main/java/com/vibevault/model/Playlist.java @@ -1,6 +1,7 @@ package com.vibevault.model; import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -16,14 +17,23 @@ import java.util.List; * - 一个歌单包含多首歌曲(一对多关系) * - 删除歌单时应级联删除其中的歌曲 */ +@Entity +@Table(name = "playlists") public class Playlist { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = false) + @NotEmpty(message = "歌单名称不能为空") 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() { @@ -45,6 +55,14 @@ public class Playlist { public void setName(String name) { this.name = name; } + + public void setOwner(User owner) { + this.owner = owner; + } + + public void setId(Long id) { + this.id = id; + } public User getOwner() { return owner; @@ -59,7 +77,8 @@ public class Playlist { * 提示:需要维护双向关系 */ public void addSong(Song song) { - // TODO: 实现添加歌曲逻辑 + songs.add(song); + song.setPlaylist(this); } /** @@ -67,6 +86,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..b7e2ddf 100644 --- a/src/main/java/com/vibevault/model/Song.java +++ b/src/main/java/com/vibevault/model/Song.java @@ -1,6 +1,8 @@ package com.vibevault.model; import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Positive; /** * 歌曲实体类 @@ -10,16 +12,28 @@ import jakarta.persistence.*; * - id 作为自增主键 * - 每首歌曲属于一个歌单(多对一关系) */ +@Entity +@Table(name = "songs") public class Song { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = false) + @NotEmpty(message = "歌曲标题不能为空") private String title; + @Column(nullable = false) + @NotEmpty(message = "歌曲艺术家不能为空") private String artist; + @Column(nullable = false) + @Positive(message = "歌曲时长必须为正数") private int durationInSeconds; + @ManyToOne + @JoinColumn(name = "playlist_id", nullable = false) private Playlist playlist; public Song() { @@ -54,4 +68,20 @@ public class Song { public void setPlaylist(Playlist playlist) { this.playlist = playlist; } + + public void setId(Long id) { + this.id = id; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setArtist(String artist) { + this.artist = artist; + } + + public void setDurationInSeconds(int durationInSeconds) { + this.durationInSeconds = durationInSeconds; + } } diff --git a/src/main/java/com/vibevault/model/User.java b/src/main/java/com/vibevault/model/User.java index b4caa63..655c8c4 100644 --- a/src/main/java/com/vibevault/model/User.java +++ b/src/main/java/com/vibevault/model/User.java @@ -1,6 +1,13 @@ package com.vibevault.model; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; /** * 用户实体类 @@ -12,18 +19,29 @@ import jakarta.persistence.*; * - password 不能为空 * - [Challenge] 支持用户角色(如 ROLE_USER, ROLE_ADMIN) */ +@Entity +@Table(name = "users", uniqueConstraints = {@UniqueConstraint(columnNames = "username")}) public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = false, unique = true) + @NotEmpty(message = "用户名不能为空") private String username; + @Column(nullable = false) + @NotEmpty(message = "密码不能为空") private String password; // [Challenge] 用户角色,默认为 ROLE_USER + @Column(nullable = false) private String role = "ROLE_USER"; - protected User() { + public User() { + // JPA 要求的无参构造函数 + this.role = "ROLE_USER"; } public User(String username, String password) { @@ -56,4 +74,16 @@ public class User { public void setRole(String role) { this.role = role; } + + public void setPassword(String password) { + this.password = password; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setId(Long id) { + this.id = id; + } } diff --git a/src/main/java/com/vibevault/repository/PlaylistRepository.java b/src/main/java/com/vibevault/repository/PlaylistRepository.java index c57a35a..bf451f0 100644 --- a/src/main/java/com/vibevault/repository/PlaylistRepository.java +++ b/src/main/java/com/vibevault/repository/PlaylistRepository.java @@ -18,5 +18,9 @@ import java.util.List; */ @Repository public interface PlaylistRepository extends JpaRepository { - // TODO [Advanced]: 添加高级查询方法 + // [Advanced] 按所有者查询歌单 + List findByOwner(User owner); + + // [Advanced] 按名称模糊搜索歌单 + List findByNameContainingIgnoreCase(String keyword); } diff --git a/src/main/java/com/vibevault/repository/UserRepository.java b/src/main/java/com/vibevault/repository/UserRepository.java index 0b76601..7a47bc5 100644 --- a/src/main/java/com/vibevault/repository/UserRepository.java +++ b/src/main/java/com/vibevault/repository/UserRepository.java @@ -15,5 +15,9 @@ 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..ea3324a 100644 --- a/src/main/java/com/vibevault/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/vibevault/security/JwtAuthenticationFilter.java @@ -45,17 +45,45 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { @NonNull FilterChain filterChain ) throws ServletException, IOException { - // TODO: 实现 JWT 认证逻辑 // 1. 从请求头获取 Authorization + String authorizationHeader = request.getHeader("Authorization"); + + String token = null; + String username = null; + // 2. 检查是否以 "Bearer " 开头 - // 3. 提取 token 并验证 + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + token = authorizationHeader.substring(7); + try { + // 3. 提取用户名 + username = jwtService.extractUsername(token); + } catch (Exception e) { + // 解析失败,继续过滤器链 + filterChain.doFilter(request, response); + return; + } + } + // 4. 如果有效,创建 Authentication 并设置到 SecurityContextHolder - // - // 提示: - // - 使用 request.getHeader("Authorization") 获取头 - // - 使用 jwtService.extractUsername() 和 jwtService.isTokenValid() - // - 使用 UsernamePasswordAuthenticationToken 创建认证对象 - // - 使用 SecurityContextHolder.getContext().setAuthentication() 设置 + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + User user = userRepository.findByUsername(username).orElse(null); + + if (user != null && jwtService.isTokenValid(token, username)) { + // [Challenge] 获取用户角色 + List authorities = Collections.singletonList( + new SimpleGrantedAuthority(user.getRole()) + ); + + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + username, + null, + authorities + ); + + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } 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..161646f 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 com.vibevault.model.User; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; @@ -29,27 +30,53 @@ public class JwtService { /** * 为用户生成 JWT token */ - public String generateToken(String username) { - // TODO: 实现 token 生成 - // 提示:使用 Jwts.builder() - throw new UnsupportedOperationException("待实现"); + public String generateToken(User user) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + expiration); + + return Jwts.builder() + .subject(user.getUsername()) + .issuedAt(now) + .expiration(expiryDate) + .signWith(getSigningKey()) + .compact(); } /** * 从 token 中提取用户名 */ public String extractUsername(String token) { - // TODO: 实现用户名提取 - // 提示:使用 Jwts.parser() - throw new UnsupportedOperationException("待实现"); + return Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload() + .getSubject(); } /** * 验证 token 是否有效 */ public boolean isTokenValid(String token, String username) { - // TODO: 实现 token 验证 - throw new UnsupportedOperationException("待实现"); + String extractedUsername = extractUsername(token); + boolean isUsernameMatch = extractedUsername.equals(username); + boolean isExpired = isTokenExpired(token); + + return isUsernameMatch && !isExpired; + } + + /** + * 检查 token 是否已过期 + */ + private boolean isTokenExpired(String token) { + Date expiration = Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload() + .getExpiration(); + + return expiration.before(new Date()); } /** diff --git a/src/main/java/com/vibevault/service/PlaylistService.java b/src/main/java/com/vibevault/service/PlaylistService.java index 226d083..d70e680 100644 --- a/src/main/java/com/vibevault/service/PlaylistService.java +++ b/src/main/java/com/vibevault/service/PlaylistService.java @@ -59,6 +59,11 @@ public interface PlaylistService { */ List searchPlaylists(String keyword); + /** + * [Advanced] 按所有者查询歌单 + */ + List getPlaylistsByOwner(String username); + /** * [Advanced] 复制歌单 */ diff --git a/src/main/java/com/vibevault/service/PlaylistServiceImpl.java b/src/main/java/com/vibevault/service/PlaylistServiceImpl.java index f69b712..7fe322c 100644 --- a/src/main/java/com/vibevault/service/PlaylistServiceImpl.java +++ b/src/main/java/com/vibevault/service/PlaylistServiceImpl.java @@ -37,60 +37,128 @@ public class PlaylistServiceImpl implements PlaylistService { @Override public List getAllPlaylists() { - // TODO: 实现获取所有歌单 - throw new UnsupportedOperationException("待实现"); + return playlistRepository.findAll() + .stream() + .map(this::toDTO) + .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: " + 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("待实现"); + Playlist playlist = playlistRepository.findById(playlistId) + .orElseThrow(() -> new ResourceNotFoundException("Playlist not found with id: " + playlistId)); + + // [Challenge] 检查用户是否有权限操作此歌单 + checkPermission(playlist, username); + + Song newSong = new Song(song.title(), song.artist(), song.durationInSeconds()); + playlist.addSong(newSong); + + 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)); + + // [Challenge] 检查用户是否有权限操作此歌单 + 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)); + + // [Challenge] 检查用户是否有权限操作此歌单 + checkPermission(playlist, username); + + playlistRepository.delete(playlist); } // ========== Advanced 方法 ========== @Override + @Transactional(readOnly = true) + public List getPlaylistsByOwner(String username) { + User owner = userRepository.findByUsername(username) + .orElseThrow(() -> new ResourceNotFoundException("User not found: " + username)); + + return playlistRepository.findByOwner(owner) + .stream() + .map(this::toDTO) + .toList(); + } + + @Override + @Transactional(readOnly = true) public List searchPlaylists(String keyword) { - // TODO [Advanced]: 实现按关键字搜索歌单 - throw new UnsupportedOperationException("待实现"); + return playlistRepository.findByNameContainingIgnoreCase(keyword) + .stream() + .map(this::toDTO) + .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: " + username)); + + // 创建新歌单 + Playlist copiedPlaylist = new Playlist(newName, currentUser); + + // 复制原歌单中的所有歌曲 + originalPlaylist.getSongs().forEach(originalSong -> { + Song copiedSong = new Song( + originalSong.getTitle(), + originalSong.getArtist(), + originalSong.getDurationInSeconds() + ); + copiedPlaylist.addSong(copiedSong); + }); + + // 保存新歌单 + Playlist savedPlaylist = playlistRepository.save(copiedPlaylist); + + return toDTO(savedPlaylist); } // ========== 辅助方法 ========== @@ -99,16 +167,29 @@ 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) + .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 +197,12 @@ 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: " + username)); + + // 检查是否是歌单所有者或管理员 + if (!playlist.getOwner().getUsername().equals(username) && !currentUser.getRole().equals("ROLE_ADMIN")) { + throw new UnauthorizedException("You don't have permission to modify this playlist"); + } } }