From 6b7bd6fab37c6eb64cc397d9b07644e18ae94a8b Mon Sep 17 00:00:00 2001 From: heyi Date: Sun, 21 Dec 2025 21:47:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=BD=9C=E4=B8=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BindPort.class | Bin 0 -> 1532 bytes BindPort.java | 24 ++++ TestApi.class | Bin 0 -> 2489 bytes TestApi.java | 39 ++++++ TestPort.class | Bin 0 -> 1177 bytes TestPort.java | 13 ++ .../com/vibevault/config/SecurityConfig.java | 32 ++++- .../vibevault/controller/AuthController.java | 40 +++++- .../controller/HealthCheckController.java | 14 ++ .../controller/PlaylistController.java | 58 ++++++-- .../java/com/vibevault/model/Playlist.java | 14 +- src/main/java/com/vibevault/model/Song.java | 9 ++ src/main/java/com/vibevault/model/User.java | 15 ++ .../repository/PlaylistRepository.java | 3 +- .../vibevault/repository/SongRepository.java | 14 ++ .../vibevault/repository/UserRepository.java | 3 +- .../security/JwtAuthenticationFilter.java | 55 ++++++-- .../com/vibevault/security/JwtService.java | 52 +++++-- .../service/PlaylistServiceImpl.java | 132 ++++++++++++++---- src/main/resources/application.properties | 3 +- 20 files changed, 453 insertions(+), 67 deletions(-) create mode 100644 BindPort.class create mode 100644 BindPort.java create mode 100644 TestApi.class create mode 100644 TestApi.java create mode 100644 TestPort.class create mode 100644 TestPort.java create mode 100644 src/main/java/com/vibevault/controller/HealthCheckController.java create mode 100644 src/main/java/com/vibevault/repository/SongRepository.java diff --git a/BindPort.class b/BindPort.class new file mode 100644 index 0000000000000000000000000000000000000000..7bb6bcca8dc881043d529516dd511e682113ab69 GIT binary patch literal 1532 zcmaJ>+fo}x5Iw_f#d<*oaf$6<)(LhbUnJuYhaiUpWQa*5gIOZt_-V8n@LH=G)$WK2 z5BZ3^c{@DpALxjZiH*4_wTGmJe6WT4j>hO@b#Z(`(cN%NaBt+Gw0JG%;FHnWOD3IFIk>Jf@3K@n)ukHIHj*r@5 zc6f^MXnzPLTcAnQW6D$l5swtH}x$zxQRvLgi~5A$~}|Kbs!Tq5*~^+Zs8*f zA3IpWZHDxFwvuy(xnrUCMN?_fVQ!;dkElq7>w{?BGXnhtSqnJ_cX6+u%%lz~^Mr?O zv3taDr?<>b8;jjEdn=zN1w3$&M}c9!$s3}iq|fzXpkI>}Yc4hA)CYFK*&vOk=w%0w zjEF}4P{qRVwQS+Di?$@8w|+9b5u-*NpF3DJ$`}?=WMc(u4vKifU=u$TB7`3chM5aa z`cr3kV6J@S;A;~yo%Gn#+;1F2+!q!$8B&{ptnZOlNRTB}C$Mxmkm7l}SrgF#uZ6TT z(&PatV=nt@u%=v(mc8Sos5G5XlUZ6#h z!P%iF6_r9B$0D|H$Z&h0rpr>b@RH%?|GUz&?9@e4+@Q~z`3-^Oh%p*Vv|6B<(L8$h z9oSnk7@pE-j)4)3V+-448`s}4On+eiFIX?97Jqq%@$w&Vrl-#^W9I8L-#f#H6vI!T6#v~e&8A^#%a#_x%OC|>D1_nzND!s9g=)&94PZghZE|V1O?H>v8>A|} z#kYbF#OFtuVFqXLLpqbrIDGPhU;Lo|hW~l5tFC&hFlG?zzA7JCFO@-{oHb z9K;V%G@)5TNJk683>~xP1v8O1?dinPsac*C4B`EjZHWU6&AojmHAEP6Wn^=aI5D1% zAj+_5MhG{VNDS=WvvU{y2aVG%)HAZSVwlI>*lSjDN!V5eaDtgHt2W& zaR#I2gdBZhKO(i5*b!jX;L|?P-{@G`*rNVPKH$lbBc5 za9-~$id`7cuv^DNlE+TP`%-WWnk%8^63x^tqA+vZKr{Afc#@&Jigd7e z?i}}cPP*Zqh7pEXRX1w8#R|oNTKYb$V-!yXXV)1JnN_wa{yGIEXBcT&>thV7E42R0 zM3(yyj!L6r#A3}TIF{N$!*~^R9sdFND2}69Iyj-@q$&ZAf=OF8mzrmEOyV@PF4Zo# za}53eE46`Z0zZUjbv!5MK}$C8_{wLmfe`v78>Wsasf*?;KO1zY20A3D1O;{YVwStI zW8`=_r(+s3RHZWidDEv->#XvotkohNJBv9Dc^w5AP`k=VO;0sN`;V9f%4mxOl7*|| zyflrHVvPH~IZe87pc6WZ5@fq7#0g<$=Q5s|Y$UGSF4|r?|bQtJK1c}qt;QXt2AzEszQ(f(zJ*1c@$sZ zOAXg#Uw_5WQ_H4hUvTDlA{b4Cng-E`nH7#lqw{)womLD35)PYoF3Zn{+c;HkJ6GV}QfOYjsWX_W3FPn9b7HGIRc`M#JMLDldb!-oGh zrA`gv!bV)6j*-76nu>HKle|b@>*>kp*|Kc`>^8kH%#)OADO>RpUZ!_4WmjmMMyr1= zLYp*N7qBe7y@a+qFvj|gl_hld8!=;T3G08xMpA4oV>?hnfq(+fC~3@zgLfoGs(|s^b@Kpf4Su;U!pQ*i{6C&nd%GpY-nER`K4%E$aRz#lUHFQ>?9s z(%--`y4!Wc(S`)t33&&`XmuQ&IEyawvx-=X;WF0XD%Rl|HsD9P$ZygGe2b|6i7wFyCg++GhIM$kf$cR piMQz4PrE;(INnzBvXbv9`GJxjDfx+#S8$znM9KPVe2ec9`v*rEgnIx0 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c542d97a41ba33b1695044fbdf231ad8ede496f2 GIT binary patch literal 1177 zcmaJ=TTc@~6#k}^Zo6zPl$%v9MJ~1=6z~F4M6r=bQk0~m#^}qi9m~?~PP02D@-uw% zANY(9kYM5u@JAWXwk5P_<8F4(IWuRz^PS85`Fs5vz%F)m1kk4;XrLb4~t)DA;mDqYkNW{5SM9HBA=X;y>^LuZJrt#7QX(z~WZgW=;v8OBoi zTf~BLEvKB_PZuMIVNk=6fjEXcq883QC8`um)+zRA^Ry*1hjdgCfZ)|^7^4icg}P}9 z&-+rh?enB59f#P8Qc}reO}Z+%xw=VA#&K6e!oWS;XBci!=>%iYYCg2>P_!Oo(?vg> zNdr@UdHPLTdO}0;2CPx3H(yOrQE-~=~!@f=G%r)i#TE1ui zB``rV9%w*M(Z%osL`jY!j06T~H$qHw+BcA)T@8WH8m42!V3o`O*04@8OkhvxdyFhu MI%(V3!7C*G0RoUI_5c6? literal 0 HcmV?d00001 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