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

This commit is contained in:
heyi 2025-12-21 21:47:25 +08:00
parent 0820e62b6e
commit 6b7bd6fab3
20 changed files with 453 additions and 67 deletions

BIN
BindPort.class Normal file

Binary file not shown.

24
BindPort.java Normal file
View File

@ -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();
}
}
}

BIN
TestApi.class Normal file

Binary file not shown.

39
TestApi.java Normal file
View File

@ -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();
}
}
}

BIN
TestPort.class Normal file

Binary file not shown.

13
TestPort.java Normal file
View File

@ -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());
}
}
}

View File

@ -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();
}

View File

@ -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());
}
}
/**

View File

@ -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";
}
}

View File

@ -39,19 +39,61 @@ public class PlaylistController {
this.playlistService = playlistService;
}
// TODO: 实现 GET /api/playlists
// 实现 GET /api/playlists - 获取所有歌单公开
@GetMapping
public List<PlaylistDTO> 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<PlaylistDTO> 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);
}
}

View File

@ -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<Song> 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);
}
}

View File

@ -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() {

View File

@ -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;
}

View File

@ -18,5 +18,6 @@ import java.util.List;
*/
@Repository
public interface PlaylistRepository extends JpaRepository<Playlist, Long> {
// TODO [Advanced]: 添加高级查询方法
List<Playlist> findByOwner(User owner);
List<Playlist> findByNameContainingIgnoreCase(String keyword);
}

View File

@ -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<Song, Long> {
}

View File

@ -15,5 +15,6 @@ import java.util.Optional;
*/
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// TODO: 添加必要的查询方法
Optional<User> findByUsername(String username);
boolean existsByUsername(String username);
}

View File

@ -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<SimpleGrantedAuthority> 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);
}

View File

@ -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> T extractClaim(String token, Function<Claims, T> 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);
}
/**

View File

@ -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<PlaylistDTO> getAllPlaylists() {
// TODO: 实现获取所有歌单
throw new UnsupportedOperationException("待实现");
List<Playlist> 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<PlaylistDTO> searchPlaylists(String keyword) {
// TODO [Advanced]: 实现按关键字搜索歌单
throw new UnsupportedOperationException("待实现");
List<Playlist> 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<SongDTO> 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");
}
}
}

View File

@ -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