Backend update on Torrent upload with unit test complete
Change-Id: Ie8a90c82e414df32d079524d3ff3035b823b69c3
diff --git a/backend/demo/src/main/java/com/example/demo/config/SecurityConfig.java b/backend/demo/src/main/java/com/example/demo/config/SecurityConfig.java
index bfd7b1b..b3e7f91 100644
--- a/backend/demo/src/main/java/com/example/demo/config/SecurityConfig.java
+++ b/backend/demo/src/main/java/com/example/demo/config/SecurityConfig.java
@@ -5,6 +5,8 @@
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@@ -12,13 +14,17 @@
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.example.demo.security.JwtAuthenticationFilter;
-import com.example.demo.security.JwtTokenUtil;
+/**
+ * Spring Security 配置类,用于定义安全策略和相关的 Bean。
+ */
@Configuration
+@EnableWebSecurity // 启用 Spring Security 的 Web 安全功能
public class SecurityConfig {
/**
- * 密码加密器,用于注册用户时对密码加密、登录时校验
+ * 密码加密器,用于注册用户时对密码加密、登录时校验。
+ * 使用 BCryptPasswordEncoder,一种安全的密码哈希算法。
*/
@Bean
public PasswordEncoder passwordEncoder() {
@@ -27,7 +33,7 @@
/**
* 将 Spring Security 的 AuthenticationManager 暴露为 Bean,
- * 方便在 AuthController 或其它地方手动调用。
+ * 方便在 AuthController 或其它地方手动调用进行认证。
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
@@ -35,17 +41,43 @@
}
/**
- * 核心安全策略:禁用 CSRF、无状态 Session、开放登录接口、其余接口需认证。
+ * 核心安全策略配置。
+ * 配置内容包括:禁用 CSRF、禁用 Session 管理(使用 JWT 无状态)、
+ * 配置请求的授权规则,并将自定义的 JWT 认证过滤器添加到过滤器链中。
+ *
+ * @param http HttpSecurity 配置对象
+ * @param jwtFilter 自定义的 JWT 认证过滤器,通过 Spring 容器注入
+ * @return 配置好的 SecurityFilterChain
+ * @throws Exception 配置过程中可能抛出的异常
*/
@Bean
-public SecurityFilterChain filterChain(HttpSecurity http,
- JwtTokenUtil tokenUtil,
- JWTProperties props) throws Exception {
- JwtAuthenticationFilter jwtFilter = new JwtAuthenticationFilter(tokenUtil, props);
- http
- // 省略其他配置…
- .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
- return http.build();
-}
-}
+ public SecurityFilterChain filterChain(HttpSecurity http,
+ JwtAuthenticationFilter jwtFilter // 直接注入 JwtAuthenticationFilter Bean
+ ) throws Exception {
+ http
+ // 禁用 CSRF (跨站请求伪造) 保护,因为 JWT 是无状态的,不需要 CSRF 保护
+ .csrf(AbstractHttpConfigurer::disable)
+ // 配置 Session 管理策略为无状态,不使用 Session
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ // 配置请求的授权规则
+ .authorizeHttpRequests(auth -> auth
+ // 允许对 /auth/** 路径下的所有请求进行匿名访问 (例如登录、注册接口)
+ .requestMatchers("/auth/**").permitAll()
+ // 其他所有请求都需要进行身份认证
+ .anyRequest().authenticated()
+ )
+ // 禁用默认的表单登录功能,因为我们使用 JWT 进行认证
+ .formLogin(AbstractHttpConfigurer::disable)
+ // 禁用默认的 HTTP Basic 认证
+ .httpBasic(AbstractHttpConfigurer::disable)
+ // 将自定义的 JwtAuthenticationFilter 添加到 UsernamePasswordAuthenticationFilter 之前
+ // 确保在进行基于用户名密码的认证之前,先进行 JWT 认证
+ .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
+ // 构建并返回配置好的 SecurityFilterChain
+ return http.build();
+ }
+
+ // 注意:JwtAuthenticationFilter 需要被 Spring 扫描到并作为 Bean 管理
+ // 确保 JwtAuthenticationFilter 类上有 @Component 或其他 Spring Bean 相关的注解
+}
diff --git a/backend/demo/src/main/java/com/example/demo/controller/AuthController.java b/backend/demo/src/main/java/com/example/demo/controller/AuthController.java
index 08411d7..d7dfe50 100644
--- a/backend/demo/src/main/java/com/example/demo/controller/AuthController.java
+++ b/backend/demo/src/main/java/com/example/demo/controller/AuthController.java
@@ -4,8 +4,11 @@
import java.util.List;
import java.util.Map;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
+import org.springframework.security.authentication.AuthenticationManager; // 导入 AuthenticationManager
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; // 导入 UsernamePasswordAuthenticationToken
+import org.springframework.security.core.Authentication; // 导入 Authentication
+import org.springframework.security.core.context.SecurityContextHolder; // 导入 SecurityContextHolder
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
@@ -27,16 +30,18 @@
public class AuthController {
private final UserService userService;
- private final PasswordEncoder passwordEncoder;
+ private final PasswordEncoder passwordEncoder; // 虽然在AuthController中不再直接用于比对,但如果UserServiceImpl需要,这里保留注入
private final JwtTokenUtil jwtTokenUtil;
+ private final AuthenticationManager authenticationManager; // 注入 AuthenticationManager
- @Autowired
public AuthController(UserService userService,
PasswordEncoder passwordEncoder,
- JwtTokenUtil jwtTokenUtil) {
+ JwtTokenUtil jwtTokenUtil,
+ AuthenticationManager authenticationManager) { // 注入 AuthenticationManager
this.userService = userService;
this.passwordEncoder = passwordEncoder;
this.jwtTokenUtil = jwtTokenUtil;
+ this.authenticationManager = authenticationManager; // 初始化 AuthenticationManager
}
@PostMapping("/login")
@@ -46,12 +51,40 @@
if (bindingResult.hasErrors()) {
String errMsg = bindingResult.getFieldErrors().stream()
- .map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
- .reduce((a, b) -> a + "; " + b)
- .orElse("Invalid parameters");
+ .map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
+ .reduce((a, b) -> a + "; " + b)
+ .orElse("Invalid parameters");
return ResponseEntity.badRequest().body(Map.of("error", errMsg));
}
+ try {
+ // 使用 AuthenticationManager 进行认证
+ // 这会触发 UserServiceImpl 中的 loadUserByUsername 方法的调用,并比较密码
+ Authentication authentication = authenticationManager.authenticate(
+ new UsernamePasswordAuthenticationToken(
+ loginRequest.getUsername(),
+ loginRequest.getPassword()
+ )
+ );
+ // 将认证信息设置到 Spring Security 的上下文中
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+
+ // 获取认证后的用户详情(假设 User 实体实现了 UserDetails 接口)
+ User userDetails = (User) authentication.getPrincipal();
+
+ // 调用 UserService 方法生成登录响应(包括 JWT Token)
+ LoginResponseDTO response = userService.generateLoginResponse(userDetails);
+
+ return ResponseEntity.ok(response);
+
+ } catch (Exception e) {
+ // 认证失败(用户名不存在或密码错误),抛出自定义认证异常
+ // GlobalExceptionHandler 会捕获 AuthException 并返回 401
+ throw new AuthException("用户名或密码错误", e);
+ }
+
+ // 移除原有的手动查找用户和密码比对逻辑
+ /*
User user = userService.lambdaQuery()
.eq(User::getUsername, loginRequest.getUsername())
.one();
@@ -70,5 +103,6 @@
response.setRoles(List.of(user.getRole()));
return ResponseEntity.ok(response);
+ */
}
-}
+}
\ No newline at end of file
diff --git a/backend/demo/src/main/java/com/example/demo/controller/TorrentController.java b/backend/demo/src/main/java/com/example/demo/controller/TorrentController.java
new file mode 100644
index 0000000..0bd501a
--- /dev/null
+++ b/backend/demo/src/main/java/com/example/demo/controller/TorrentController.java
@@ -0,0 +1,130 @@
+// src/main/java/com/example/demo/controller/TorrentController.java
+package com.example.demo.controller;
+
+import java.io.IOException; // 导入 DTO
+import java.nio.file.Path; // 导入服务接口
+import java.nio.file.Paths; // 导入 ttorrent 的异常
+
+import org.springframework.beans.factory.annotation.Value; // 导入 @Value 用于读取配置
+import org.springframework.core.io.Resource; // 导入 Resource
+import org.springframework.core.io.UrlResource; // 导入 UrlResource
+import org.springframework.http.HttpHeaders; // 导入 HttpHeaders
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType; // 导入 MediaType
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import com.example.demo.dto.TorrentInfoDTO;
+import com.example.demo.service.TorrentService;
+import com.turn.ttorrent.bcodec.InvalidBEncodingException;
+
+/**
+ * 处理 .torrent 文件上传和下载的 RESTful 控制器。
+ */
+@RestController // 组合了 @Controller 和 @ResponseBody,表示这是一个 RESTful 控制器
+@RequestMapping("/api") // 定义所有处理方法的根路径
+public class TorrentController {
+
+ private final TorrentService torrentService;
+
+ @Value("${file.upload-dir}") // 从 application.properties 中注入文件上传目录
+ private String uploadDir;
+
+ // 通过构造函数注入 TorrentService
+ public TorrentController(TorrentService torrentService) {
+ this.torrentService = torrentService;
+ }
+
+ /**
+ * 处理 .torrent 文件的上传请求。
+ *
+ * @param file 上传的 MultipartFile 对象,通过 @RequestParam("file") 绑定。
+ * @return 包含 TorrentInfoDTO 的 ResponseEntity,表示操作结果。
+ */
+ @PostMapping("/torrents") // 映射到 /api/torrents 的 POST 请求
+ public ResponseEntity<TorrentInfoDTO> uploadTorrent(@RequestParam("file") MultipartFile file) {
+ // 1. 接收 HTTP 请求 (通过 @PostMapping 和 @RequestParam)
+
+ // 2. 校验参数
+ if (file.isEmpty()) {
+ TorrentInfoDTO errorDto = new TorrentInfoDTO();
+ errorDto.setMessage("上传文件不能为空。");
+ return new ResponseEntity<>(errorDto, HttpStatus.BAD_REQUEST);
+ }
+
+ // 校验文件类型 (可选,但推荐)
+ if (!file.getOriginalFilename().toLowerCase().endsWith(".torrent")) {
+ TorrentInfoDTO errorDto = new TorrentInfoDTO();
+ errorDto.setMessage("只允许上传 .torrent 文件。");
+ return new ResponseEntity<>(errorDto, HttpStatus.UNSUPPORTED_MEDIA_TYPE);
+ }
+
+ try {
+ // 3. 调用 Service 层处理业务逻辑
+ TorrentInfoDTO result = torrentService.handleUpload(file);
+
+ // 4. 返回 TorrentInfoDTO 给前端
+ return new ResponseEntity<>(result, HttpStatus.OK);
+ } catch (InvalidBEncodingException e) {
+ // 捕获 .torrent 文件解析错误
+ TorrentInfoDTO errorDto = new TorrentInfoDTO();
+ errorDto.setMessage("文件解析失败,请确保它是有效的 .torrent 文件: " + e.getMessage());
+ return new ResponseEntity<>(errorDto, HttpStatus.BAD_REQUEST);
+ } catch (IllegalArgumentException e) {
+ // 捕获服务层抛出的非法参数异常 (如文件为空)
+ TorrentInfoDTO errorDto = new TorrentInfoDTO();
+ errorDto.setMessage("处理文件时发生错误: " + e.getMessage());
+ return new ResponseEntity<>(errorDto, HttpStatus.BAD_REQUEST);
+ } catch (IOException e) {
+ // 捕获文件读写或保存失败的异常
+ TorrentInfoDTO errorDto = new TorrentInfoDTO();
+ errorDto.setMessage("文件上传或保存失败: " + e.getMessage());
+ return new ResponseEntity<>(errorDto, HttpStatus.INTERNAL_SERVER_ERROR);
+ } catch (Exception e) {
+ // 捕获其他未知异常
+ TorrentInfoDTO errorDto = new TorrentInfoDTO();
+ errorDto.setMessage("文件上传过程中发生未知错误: " + e.getMessage());
+ e.printStackTrace(); // 打印堆栈跟踪以便调试
+ return new ResponseEntity<>(errorDto, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ /**
+ * 实现 .torrent 文件的下载功能。
+ * 根据 infoHash 找到对应的 .torrent 文件并提供下载。
+ *
+ * @param infoHash 洪流的 info hash,用于查找服务器上的文件。
+ * @param fileName 原始文件名 (用于设置下载时的文件名,实际查找文件仍依赖 infoHash)。
+ * @return 包含 .torrent 文件内容的 ResponseEntity。
+ */
+ // 修正后的 downloadTorrent 方法片段
+@GetMapping("/downloads/{infoHash}/{fileName}")
+public ResponseEntity<Resource> downloadTorrent(@PathVariable String infoHash, @PathVariable String fileName) {
+ try {
+ Path fileStorageLocation = Paths.get(uploadDir).toAbsolutePath().normalize();
+ Path filePath = fileStorageLocation.resolve(infoHash + ".torrent").normalize();
+ Resource resource = new UrlResource(filePath.toUri()); // 这一行不会抛出 MalformedURLException
+
+ if (resource.exists() && resource.isReadable()) {
+ String contentType = "application/x-bittorrent";
+ String headerValue = "attachment; filename=\"" + fileName + "\"";
+
+ return ResponseEntity.ok()
+ .contentType(MediaType.parseMediaType(contentType))
+ .header(HttpHeaders.CONTENT_DISPOSITION, headerValue)
+ .body(resource);
+ } else {
+ return new ResponseEntity<>(HttpStatus.NOT_FOUND);
+ }
+ } catch (IOException e) { // 只保留 IOException
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ // MalformedURLException 相关的 catch 块已移除
+}
+}
\ No newline at end of file
diff --git a/backend/demo/src/main/java/com/example/demo/dto/TorrentInfoDTO.java b/backend/demo/src/main/java/com/example/demo/dto/TorrentInfoDTO.java
new file mode 100644
index 0000000..45cd326
--- /dev/null
+++ b/backend/demo/src/main/java/com/example/demo/dto/TorrentInfoDTO.java
@@ -0,0 +1,87 @@
+// src/main/java/com/example/demo/dto/TorrentInfoDTO.java
+package com.example.demo.dto;
+
+import java.io.Serializable;
+
+public class TorrentInfoDTO implements Serializable {
+ private String fileName;
+ private long fileSize;
+ private String infoHash;
+ private String magnetUri;
+ private String downloadUrl;
+ private String message; // For success or error messages
+
+ // Constructors
+ public TorrentInfoDTO() {
+ }
+
+ public TorrentInfoDTO(String fileName, long fileSize, String infoHash, String magnetUri, String downloadUrl, String message) {
+ this.fileName = fileName;
+ this.fileSize = fileSize;
+ this.infoHash = infoHash;
+ this.magnetUri = magnetUri;
+ this.downloadUrl = downloadUrl;
+ this.message = message;
+ }
+
+ // Getters and Setters
+ public String getFileName() {
+ return fileName;
+ }
+
+ public void setFileName(String fileName) {
+ this.fileName = fileName;
+ }
+
+ public long getFileSize() {
+ return fileSize;
+ }
+
+ public void setFileSize(long fileSize) {
+ this.fileSize = fileSize;
+ }
+
+ public String getInfoHash() {
+ return infoHash;
+ }
+
+ public void setInfoHash(String infoHash) {
+ this.infoHash = infoHash;
+ }
+
+ public String getMagnetUri() {
+ return magnetUri;
+ }
+
+ public void setMagnetUri(String magnetUri) {
+ this.magnetUri = magnetUri;
+ }
+
+ public String getDownloadUrl() {
+ return downloadUrl;
+ }
+
+ public void setDownloadUrl(String downloadUrl) {
+ this.downloadUrl = downloadUrl;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ @Override
+ public String toString() {
+ return "TorrentInfoDTO{" +
+ "fileName='" + fileName + '\'' +
+ ", fileSize=" + fileSize +
+ ", infoHash='" + infoHash + '\'' +
+ ", magnetUri='" + magnetUri + '\'' +
+ ", downloadUrl='" + downloadUrl + '\'' +
+ ", message='" + message + '\'' +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/backend/demo/src/main/java/com/example/demo/entity/TorrentInfo.java b/backend/demo/src/main/java/com/example/demo/entity/TorrentInfo.java
new file mode 100644
index 0000000..da39654
--- /dev/null
+++ b/backend/demo/src/main/java/com/example/demo/entity/TorrentInfo.java
@@ -0,0 +1,118 @@
+// src/main/java/com/example/demo/entity/TorrentInfo.java
+package com.example.demo.entity;
+
+import java.time.LocalDateTime;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+
+@TableName("torrent_info") // 映射到数据库中的 torrent_info 表
+public class TorrentInfo {
+
+ @TableId(value = "id", type = IdType.AUTO) // 标记为主键,并设置为数据库自增
+ private Long id;
+
+ @TableField("info_hash") // 映射到数据库中的 info_hash 列
+ private String infoHash; // 洪流的唯一标识符(信息哈希)
+
+ @TableField("file_name") // 映射到数据库中的 file_name 列
+ private String fileName; // 原始文件名
+
+ @TableField("file_size") // 映射到数据库中的 file_size 列
+ private Long fileSize; // 文件大小(字节)
+
+ @TableField("magnet_uri") // 映射到数据库中的 magnet_uri 列
+ private String magnetUri; // 生成的磁力链接
+
+ @TableField("download_url") // 映射到数据库中的 download_url 列
+ private String downloadUrl; // .torrent 文件的下载 URL
+
+ @TableField("upload_time") // 映射到数据库中的 upload_time 列
+ private LocalDateTime uploadTime; // 上传时间
+
+ // 默认构造函数,MyBatis-Plus 需要
+ public TorrentInfo() {
+ }
+
+ // 带有所有字段的构造函数(不包含 ID 和 uploadTime,因为它们是自动生成的)
+ public TorrentInfo(String infoHash, String fileName, Long fileSize, String magnetUri, String downloadUrl) {
+ this.infoHash = infoHash;
+ this.fileName = fileName;
+ this.fileSize = fileSize;
+ this.magnetUri = magnetUri;
+ this.downloadUrl = downloadUrl;
+ this.uploadTime = LocalDateTime.now(); // 设置当前上传时间
+ }
+
+ // Getters and Setters
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getInfoHash() {
+ return infoHash;
+ }
+
+ public void setInfoHash(String infoHash) {
+ this.infoHash = infoHash;
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public void setFileName(String fileName) {
+ this.fileName = fileName;
+ }
+
+ public Long getFileSize() {
+ return fileSize;
+ }
+
+ public void setFileSize(Long fileSize) {
+ this.fileSize = fileSize;
+ }
+
+ public String getMagnetUri() {
+ return magnetUri;
+ }
+
+ public void setMagnetUri(String magnetUri) {
+ this.magnetUri = magnetUri;
+ }
+
+ public String getDownloadUrl() {
+ return downloadUrl;
+ }
+
+ public void setDownloadUrl(String downloadUrl) {
+ this.downloadUrl = downloadUrl;
+ }
+
+ public LocalDateTime getUploadTime() {
+ return uploadTime;
+ }
+
+ public void setUploadTime(LocalDateTime uploadTime) {
+ this.uploadTime = uploadTime;
+ }
+
+ @Override
+ public String toString() {
+ return "TorrentInfo{" +
+ "id=" + id +
+ ", infoHash='" + infoHash + '\'' +
+ ", fileName='" + fileName + '\'' +
+ ", fileSize=" + fileSize +
+ ", magnetUri='" + magnetUri + '\'' +
+ ", downloadUrl='" + downloadUrl + '\'' +
+ ", uploadTime=" + uploadTime +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/backend/demo/src/main/java/com/example/demo/entity/User.java b/backend/demo/src/main/java/com/example/demo/entity/User.java
index bcbe6de..ba308af 100644
--- a/backend/demo/src/main/java/com/example/demo/entity/User.java
+++ b/backend/demo/src/main/java/com/example/demo/entity/User.java
@@ -1,7 +1,13 @@
package com.example.demo.entity;
-import java.io.Serializable;
-import java.time.LocalDateTime;
+import java.time.LocalDateTime; // Keep import if Serializable is used elsewhere, otherwise remove
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List; // 用于返回空的权限集合,如果用户没有角色/权限
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority; // 用于构建权限列表
+import org.springframework.security.core.userdetails.UserDetails;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -9,7 +15,8 @@
@TableName("user")
-public class User implements Serializable {
+// 实现UserDetails接口 (UserDetails extends Serializable, so Serializable is redundant here)
+public class User implements UserDetails {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
@@ -21,16 +28,18 @@
private String email;
- private Integer status;
+ private Integer status; // 假设status字段表示用户状态,例如 0:禁用, 1:启用
- private Integer score;
+ private Integer score;
- private String role;
+ private String role; // 假设role字段存储用户的角色,例如 "ADMIN", "USER"
private LocalDateTime createTime;
private LocalDateTime updateTime;
+ // --- Getters and Setters ---
+
public Long getId() {
return id;
}
@@ -39,6 +48,7 @@
this.id = id;
}
+ @Override // 实现UserDetails接口的getUsername方法
public String getUsername() {
return username;
}
@@ -47,6 +57,7 @@
this.username = username;
}
+ @Override // 实现UserDetails接口的getPassword方法
public String getPassword() {
return password;
}
@@ -102,4 +113,49 @@
public void setUpdateTime(LocalDateTime updateTime) {
this.updateTime = updateTime;
}
+
+ // --- 实现 UserDetails 接口的方法 ---
+
+ @Override
+ public Collection<? extends GrantedAuthority> getAuthorities() {
+ // 根据用户的角色构建权限列表
+ List<GrantedAuthority> authorities = new ArrayList<>();
+ if (this.role != null && !this.role.isEmpty()) {
+ // Spring Security 推荐角色以 "ROLE_" 开头
+ authorities.add(new SimpleGrantedAuthority("ROLE_" + this.role));
+ }
+ // 如果用户没有角色,返回一个空的集合
+ return authorities;
+ }
+
+ @Override
+ public boolean isAccountNonExpired() {
+ // 根据您的业务逻辑判断账户是否过期
+ // 例如,如果您的User实体有过期时间字段,可以在这里检查
+ // 这里简单返回true,表示账户永不过期
+ return true;
+ }
+
+ @Override
+ public boolean isAccountNonLocked() {
+ // 根据您的业务逻辑判断账户是否被锁定
+ // 例如,如果您的User实体有锁定状态字段,可以在这里检查
+ // 这里简单返回true,表示账户永不锁定
+ return true;
+ }
+
+ @Override
+ public boolean isCredentialsNonExpired() {
+ // 根据您的业务逻辑判断凭证(密码)是否过期
+ // 例如,如果您的User实体有密码最后修改时间字段,可以在这里检查
+ // 这里简单返回true,表示凭证永不过期
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ // 根据您的业务逻辑判断账户是否启用
+ // 假设status字段为1表示启用
+ return this.status != null && this.status == 1;
+ }
}
diff --git a/backend/demo/src/main/java/com/example/demo/mapper/TorrentInfoMapper.java b/backend/demo/src/main/java/com/example/demo/mapper/TorrentInfoMapper.java
new file mode 100644
index 0000000..3f4f245
--- /dev/null
+++ b/backend/demo/src/main/java/com/example/demo/mapper/TorrentInfoMapper.java
@@ -0,0 +1,13 @@
+// src/main/java/com/example/demo/mapper/TorrentInfoMapper.java
+package com.example.demo.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.demo.entity.TorrentInfo; // MyBatis 的 Mapper 注解
+
+@Mapper // 标记这是一个 MyBatis Mapper 接口
+public interface TorrentInfoMapper extends BaseMapper<TorrentInfo> {
+ // BaseMapper 提供了基本的 CRUD 操作,例如 insert(), selectById(), selectList(), deleteById() 等。
+ // 无需编写任何实现代码。
+}
\ No newline at end of file
diff --git a/backend/demo/src/main/java/com/example/demo/mapper/UserMapper.java b/backend/demo/src/main/java/com/example/demo/mapper/UserMapper.java
index 94ba7a8..0198c39 100644
--- a/backend/demo/src/main/java/com/example/demo/mapper/UserMapper.java
+++ b/backend/demo/src/main/java/com/example/demo/mapper/UserMapper.java
@@ -1,8 +1,9 @@
package com.example.demo.mapper;
+import org.apache.ibatis.annotations.Mapper;
+
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.User;
-import org.apache.ibatis.annotations.Mapper;
/**
* UserMapper 接口
@@ -11,6 +12,10 @@
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
+
+ User findById(Long id);
+
+ User findByUsername(String username);
// List<User> selectByStatus(@Param("status") Integer status);
}
diff --git a/backend/demo/src/main/java/com/example/demo/result.txt b/backend/demo/src/main/java/com/example/demo/result.txt
new file mode 100644
index 0000000..bd1dd30
--- /dev/null
+++ b/backend/demo/src/main/java/com/example/demo/result.txt
@@ -0,0 +1,1031 @@
+--- Start File Content ---
+
+--- Content of: ./config/JWTProperties.java ---
+package com.example.demo.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * JWT 配置属性,从 application.properties 中 jwt 开头的配置加载
+ */
+@Component
+@ConfigurationProperties(prefix = "jwt")
+public class JWTProperties {
+ /**
+ * 用于签名的密钥
+ */
+ private String secret;
+
+ /**
+ * Token 过期时长,单位毫秒
+ */
+ private long expirationMs;
+
+ /**
+ * HTTP Header 中放置 JWT 的字段名
+ */
+ private String header = "Authorization";
+
+ /**
+ * Header 中 Token 的前缀
+ */
+ private String tokenPrefix = "Bearer ";
+
+ /**
+ * 签发者信息(可选)
+ */
+ private String issuer;
+
+ /**
+ * 接收者信息(可选)
+ */
+ private String audience;
+
+ // --- getters & setters ---
+
+ public String getSecret() {
+ return secret;
+ }
+
+ public void setSecret(String secret) {
+ this.secret = secret;
+ }
+
+ public long getExpirationMs() {
+ return expirationMs;
+ }
+
+ public void setExpirationMs(long expirationMs) {
+ this.expirationMs = expirationMs;
+ }
+
+ public String getHeader() {
+ return header;
+ }
+
+ public void setHeader(String header) {
+ this.header = header;
+ }
+
+ public String getTokenPrefix() {
+ return tokenPrefix;
+ }
+
+ public void setTokenPrefix(String tokenPrefix) {
+ this.tokenPrefix = tokenPrefix;
+ }
+
+ public String getIssuer() {
+ return issuer;
+ }
+
+ public void setIssuer(String issuer) {
+ this.issuer = issuer;
+ }
+
+ public String getAudience() {
+ return audience;
+ }
+
+ public void setAudience(String audience) {
+ this.audience = audience;
+ }
+}
+
+--- End of: ./config/JWTProperties.java ---
+
+--- Content of: ./config/SecurityConfig.java ---
+package com.example.demo.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+import com.example.demo.security.JwtAuthenticationFilter;
+
+/**
+ * Spring Security 配置类,用于定义安全策略和相关的 Bean。
+ */
+@Configuration
+@EnableWebSecurity // 启用 Spring Security 的 Web 安全功能
+public class SecurityConfig {
+
+ /**
+ * 密码加密器,用于注册用户时对密码加密、登录时校验。
+ * 使用 BCryptPasswordEncoder,一种安全的密码哈希算法。
+ */
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ /**
+ * 将 Spring Security 的 AuthenticationManager 暴露为 Bean,
+ * 方便在 AuthController 或其它地方手动调用进行认证。
+ */
+ @Bean
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
+ return config.getAuthenticationManager();
+ }
+
+ /**
+ * 核心安全策略配置。
+ * 配置内容包括:禁用 CSRF、禁用 Session 管理(使用 JWT 无状态)、
+ * 配置请求的授权规则,并将自定义的 JWT 认证过滤器添加到过滤器链中。
+ *
+ * @param http HttpSecurity 配置对象
+ * @param jwtFilter 自定义的 JWT 认证过滤器,通过 Spring 容器注入
+ * @return 配置好的 SecurityFilterChain
+ * @throws Exception 配置过程中可能抛出的异常
+ */
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http,
+ JwtAuthenticationFilter jwtFilter // 直接注入 JwtAuthenticationFilter Bean
+ ) throws Exception {
+ http
+ // 禁用 CSRF (跨站请求伪造) 保护,因为 JWT 是无状态的,不需要 CSRF 保护
+ .csrf(AbstractHttpConfigurer::disable)
+ // 配置 Session 管理策略为无状态,不使用 Session
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ // 配置请求的授权规则
+ .authorizeHttpRequests(auth -> auth
+ // 允许对 /auth/** 路径下的所有请求进行匿名访问 (例如登录、注册接口)
+ .requestMatchers("/auth/**").permitAll()
+ // 其他所有请求都需要进行身份认证
+ .anyRequest().authenticated()
+ )
+ // 禁用默认的表单登录功能,因为我们使用 JWT 进行认证
+ .formLogin(AbstractHttpConfigurer::disable)
+ // 禁用默认的 HTTP Basic 认证
+ .httpBasic(AbstractHttpConfigurer::disable)
+ // 将自定义的 JwtAuthenticationFilter 添加到 UsernamePasswordAuthenticationFilter 之前
+ // 确保在进行基于用户名密码的认证之前,先进行 JWT 认证
+ .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
+
+ // 构建并返回配置好的 SecurityFilterChain
+ return http.build();
+ }
+
+ // 注意:JwtAuthenticationFilter 需要被 Spring 扫描到并作为 Bean 管理
+ // 确保 JwtAuthenticationFilter 类上有 @Component 或其他 Spring Bean 相关的注解
+}
+
+--- End of: ./config/SecurityConfig.java ---
+
+--- Content of: ./controller/AuthController.java ---
+package com.example.demo.controller;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.authentication.AuthenticationManager; // 导入 AuthenticationManager
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; // 导入 UsernamePasswordAuthenticationToken
+import org.springframework.security.core.Authentication; // 导入 Authentication
+import org.springframework.security.core.context.SecurityContextHolder; // 导入 SecurityContextHolder
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.example.demo.dto.LoginRequestDTO;
+import com.example.demo.dto.LoginResponseDTO;
+import com.example.demo.entity.User;
+import com.example.demo.exception.AuthException;
+import com.example.demo.security.JwtTokenUtil;
+import com.example.demo.service.UserService;
+
+import jakarta.validation.Valid;
+
+@RestController
+@RequestMapping("/api/auth")
+public class AuthController {
+
+ private final UserService userService;
+ private final PasswordEncoder passwordEncoder; // 虽然在AuthController中不再直接用于比对,但如果UserServiceImpl需要,这里保留注入
+ private final JwtTokenUtil jwtTokenUtil;
+ private final AuthenticationManager authenticationManager; // 注入 AuthenticationManager
+
+ public AuthController(UserService userService,
+ PasswordEncoder passwordEncoder,
+ JwtTokenUtil jwtTokenUtil,
+ AuthenticationManager authenticationManager) { // 注入 AuthenticationManager
+ this.userService = userService;
+ this.passwordEncoder = passwordEncoder;
+ this.jwtTokenUtil = jwtTokenUtil;
+ this.authenticationManager = authenticationManager; // 初始化 AuthenticationManager
+ }
+
+ @PostMapping("/login")
+ public ResponseEntity<?> login(
+ @Valid @RequestBody LoginRequestDTO loginRequest,
+ BindingResult bindingResult) {
+
+ if (bindingResult.hasErrors()) {
+ String errMsg = bindingResult.getFieldErrors().stream()
+ .map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
+ .reduce((a, b) -> a + "; " + b)
+ .orElse("Invalid parameters");
+ return ResponseEntity.badRequest().body(Map.of("error", errMsg));
+ }
+
+ try {
+ // 使用 AuthenticationManager 进行认证
+ // 这会触发 UserServiceImpl 中的 loadUserByUsername 方法的调用,并比较密码
+ Authentication authentication = authenticationManager.authenticate(
+ new UsernamePasswordAuthenticationToken(
+ loginRequest.getUsername(),
+ loginRequest.getPassword()
+ )
+ );
+ // 将认证信息设置到 Spring Security 的上下文中
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+
+ // 获取认证后的用户详情(假设 User 实体实现了 UserDetails 接口)
+ User userDetails = (User) authentication.getPrincipal();
+
+ // 调用 UserService 方法生成登录响应(包括 JWT Token)
+ LoginResponseDTO response = userService.generateLoginResponse(userDetails);
+
+ return ResponseEntity.ok(response);
+
+ } catch (Exception e) {
+ // 认证失败(用户名不存在或密码错误),抛出自定义认证异常
+ // GlobalExceptionHandler 会捕获 AuthException 并返回 401
+ throw new AuthException("用户名或密码错误", e);
+ }
+
+ // 移除原有的手动查找用户和密码比对逻辑
+ /*
+ User user = userService.lambdaQuery()
+ .eq(User::getUsername, loginRequest.getUsername())
+ .one();
+
+ if (user == null || !passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) {
+ throw new AuthException("用户名或密码错误");
+ }
+
+ String token = jwtTokenUtil.generateToken(user.getId(), user.getUsername(), user.getRole());
+
+ LoginResponseDTO response = new LoginResponseDTO();
+ response.setToken(token);
+ response.setExpiresAt(Instant.now().plusMillis(jwtTokenUtil.getExpiration()));
+ response.setUserId(user.getId());
+ response.setUsername(user.getUsername());
+ response.setRoles(List.of(user.getRole()));
+
+ return ResponseEntity.ok(response);
+ */
+ }
+}
+--- End of: ./controller/AuthController.java ---
+
+--- Content of: ./DemoApplication.java ---
+package com.example.demo;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+@SpringBootApplication
+@MapperScan("com.example.demo.mapper")
+public class DemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(DemoApplication.class, args);
+ }
+
+}
+
+--- End of: ./DemoApplication.java ---
+
+--- Content of: ./dto/LoginRequestDTO.java ---
+package com.example.demo.dto;
+
+import java.io.Serializable;
+
+import jakarta.validation.constraints.NotBlank;
+
+
+public class LoginRequestDTO implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @NotBlank(message = "用户名不能为空")
+ private String username;
+
+ @NotBlank(message = "密码不能为空")
+ private String password;
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+}
+--- End of: ./dto/LoginRequestDTO.java ---
+
+--- Content of: ./dto/LoginResponseDTO.java ---
+package com.example.demo.dto;
+
+import java.io.Serializable;
+import java.time.Instant;
+import java.util.List;
+
+public class LoginResponseDTO implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /** 登录成功后返回的 JWT 或会话标识 */
+ private String token;
+
+ /** Token 过期时间戳 */
+ private Instant expiresAt;
+
+ /** 用户 ID,可选 */
+ private Long userId;
+
+ /** 用户名,可选 */
+ private String username;
+
+ /** 用户角色列表,可选 */
+ private List<String> roles;
+
+ public String getToken() {
+ return token;
+ }
+
+ public void setToken(String token) {
+ this.token = token;
+ }
+
+ public Instant getExpiresAt() {
+ return expiresAt;
+ }
+
+ public void setExpiresAt(Instant expiresAt) {
+ this.expiresAt = expiresAt;
+ }
+
+ public Long getUserId() {
+ return userId;
+ }
+
+ public void setUserId(Long userId) {
+ this.userId = userId;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public List<String> getRoles() {
+ return roles;
+ }
+
+ public void setRoles(List<String> roles) {
+ this.roles = roles;
+ }
+}
+
+--- End of: ./dto/LoginResponseDTO.java ---
+
+--- Content of: ./entity/User.java ---
+package com.example.demo.entity;
+
+import java.time.LocalDateTime; // Keep import if Serializable is used elsewhere, otherwise remove
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List; // 用于返回空的权限集合,如果用户没有角色/权限
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority; // 用于构建权限列表
+import org.springframework.security.core.userdetails.UserDetails;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+
+
+@TableName("user")
+// 实现UserDetails接口 (UserDetails extends Serializable, so Serializable is redundant here)
+public class User implements UserDetails {
+ private static final long serialVersionUID = 1L;
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private String username;
+
+ private String password;
+
+ private String email;
+
+ private Integer status; // 假设status字段表示用户状态,例如 0:禁用, 1:启用
+
+ private Integer score;
+
+ private String role; // 假设role字段存储用户的角色,例如 "ADMIN", "USER"
+
+ private LocalDateTime createTime;
+
+ private LocalDateTime updateTime;
+
+ // --- Getters and Setters ---
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ @Override // 实现UserDetails接口的getUsername方法
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ @Override // 实现UserDetails接口的getPassword方法
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public Integer getStatus() {
+ return status;
+ }
+
+ public void setStatus(Integer status) {
+ this.status = status;
+ }
+
+ public Integer getScore() {
+ return score;
+ }
+
+ public void setScore(Integer score) {
+ this.score = score;
+ }
+
+ public String getRole() {
+ return role;
+ }
+
+ public void setRole(String role) {
+ this.role = role;
+ }
+
+ public LocalDateTime getCreateTime() {
+ return createTime;
+ }
+
+ public void setCreateTime(LocalDateTime createTime) {
+ this.createTime = createTime;
+ }
+
+ public LocalDateTime getUpdateTime() {
+ return updateTime;
+ }
+
+ public void setUpdateTime(LocalDateTime updateTime) {
+ this.updateTime = updateTime;
+ }
+
+ // --- 实现 UserDetails 接口的方法 ---
+
+ @Override
+ public Collection<? extends GrantedAuthority> getAuthorities() {
+ // 根据用户的角色构建权限列表
+ List<GrantedAuthority> authorities = new ArrayList<>();
+ if (this.role != null && !this.role.isEmpty()) {
+ // Spring Security 推荐角色以 "ROLE_" 开头
+ authorities.add(new SimpleGrantedAuthority("ROLE_" + this.role));
+ }
+ // 如果用户没有角色,返回一个空的集合
+ return authorities;
+ }
+
+ @Override
+ public boolean isAccountNonExpired() {
+ // 根据您的业务逻辑判断账户是否过期
+ // 例如,如果您的User实体有过期时间字段,可以在这里检查
+ // 这里简单返回true,表示账户永不过期
+ return true;
+ }
+
+ @Override
+ public boolean isAccountNonLocked() {
+ // 根据您的业务逻辑判断账户是否被锁定
+ // 例如,如果您的User实体有锁定状态字段,可以在这里检查
+ // 这里简单返回true,表示账户永不锁定
+ return true;
+ }
+
+ @Override
+ public boolean isCredentialsNonExpired() {
+ // 根据您的业务逻辑判断凭证(密码)是否过期
+ // 例如,如果您的User实体有密码最后修改时间字段,可以在这里检查
+ // 这里简单返回true,表示凭证永不过期
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ // 根据您的业务逻辑判断账户是否启用
+ // 假设status字段为1表示启用
+ return this.status != null && this.status == 1;
+ }
+}
+
+--- End of: ./entity/User.java ---
+
+--- Content of: ./exception/AuthException.java ---
+package com.example.demo.exception;
+
+/**
+ * 自定义认证/授权异常
+ */
+public class AuthException extends RuntimeException {
+ public AuthException(String message) {
+ super(message);
+ }
+
+ public AuthException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
+
+--- End of: ./exception/AuthException.java ---
+
+--- Content of: ./exception/GlobalExceptionHandler.java ---
+package com.example.demo.exception;
+
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@ControllerAdvice
+public class GlobalExceptionHandler {
+
+ /** 处理自定义认证异常 */
+ @ExceptionHandler(AuthException.class)
+ public ResponseEntity<Map<String, String>> handleAuthException(AuthException ex) {
+ Map<String, String> body = Map.of("error", ex.getMessage());
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(body);
+ }
+
+ /** 处理请求参数校验失败 */
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
+ Map<String, String> errors = new HashMap<>();
+ for (FieldError fe : ex.getBindingResult().getFieldErrors()) {
+ errors.put(fe.getField(), fe.getDefaultMessage());
+ }
+ return ResponseEntity.badRequest().body(errors);
+ }
+
+ /** 处理其它未捕获的异常 */
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity<Map<String, String>> handleAllExceptions(Exception ex) {
+ Map<String, String> body = Map.of("error", "Internal server error");
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
+ }
+}
+
+--- End of: ./exception/GlobalExceptionHandler.java ---
+
+--- Content of: ./mapper/UserMapper.java ---
+package com.example.demo.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.demo.entity.User;
+
+/**
+ * UserMapper 接口
+ *
+ * 继承 MyBatis-Plus 提供的 BaseMapper,实现基本的 CRUD 操作
+ */
+@Mapper
+public interface UserMapper extends BaseMapper<User> {
+
+ User findById(Long id);
+
+ User findByUsername(String username);
+
+ // List<User> selectByStatus(@Param("status") Integer status);
+}
+
+
+--- End of: ./mapper/UserMapper.java ---
+
+--- Content of: ./security/JwtAuthenticationFilter.java ---
+package com.example.demo.security;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import com.example.demo.config.JWTProperties;
+import com.example.demo.exception.AuthException;
+
+import io.jsonwebtoken.Claims;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+@Component
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+ private final JwtTokenUtil tokenUtil;
+ private final JWTProperties props;
+
+ public JwtAuthenticationFilter(JwtTokenUtil tokenUtil, JWTProperties props) {
+ this.tokenUtil = tokenUtil;
+ this.props = props;
+ }
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain chain)
+ throws ServletException, IOException {
+
+ String headerValue = request.getHeader(props.getHeader());
+ if (!StringUtils.hasText(headerValue) || !headerValue.startsWith(props.getTokenPrefix())) {
+ chain.doFilter(request, response);
+ return;
+ }
+
+ String token = headerValue.substring(props.getTokenPrefix().length());
+ if (!tokenUtil.validateToken(token)) {
+ throw new AuthException("Invalid or expired JWT token");
+ }
+
+ Claims claims = tokenUtil.parseClaims(token);
+ String username =tokenUtil.getUsername(token);
+ Long userId = tokenUtil.getUserId(token);
+ String role = tokenUtil.getRole(token);
+
+ UsernamePasswordAuthenticationToken auth =
+ new UsernamePasswordAuthenticationToken(
+ userId,
+ null,
+ List.of(new SimpleGrantedAuthority(role))
+ );
+ SecurityContextHolder.getContext().setAuthentication(auth);
+
+ chain.doFilter(request, response);
+ }
+}
+
+--- End of: ./security/JwtAuthenticationFilter.java ---
+
+--- Content of: ./security/JwtTokenUtil.java ---
+package com.example.demo.security;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.Map;
+
+import org.springframework.stereotype.Component;
+
+import com.example.demo.config.JWTProperties;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.JwtException;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.security.Keys;
+@Component
+public class JwtTokenUtil {
+
+ private final JWTProperties props;
+ private final byte[] secretBytes;
+
+ public JwtTokenUtil(JWTProperties props) {
+ this.props = props;
+ this.secretBytes = props.getSecret().getBytes(StandardCharsets.UTF_8);
+ }
+
+ /** 生成 JWT */
+ public String generateToken(Long userId, String username, String role) {
+ Date now = new Date();
+ Date expiry = new Date(now.getTime() + props.getExpirationMs());
+
+ return Jwts.builder()
+ .setSubject(username)
+ .setIssuer(props.getIssuer())
+ .setAudience(props.getAudience())
+ .setIssuedAt(now)
+ .setExpiration(expiry)
+ .addClaims(Map.of("userId", userId, "role", role))
+ .signWith(Keys.hmacShaKeyFor(secretBytes), SignatureAlgorithm.HS256)
+ .compact();
+ }
+
+ /** 验证并解析 Token,抛出异常则验证失败 */
+ public Claims parseClaims(String token) {
+ return Jwts.parserBuilder()
+ .setSigningKey(secretBytes)
+ .build()
+ .parseClaimsJws(token)
+ .getBody();
+ }
+
+ /** 校验 Token 是否有效 */
+ public boolean validateToken(String token) {
+ try {
+ parseClaims(token);
+ return true;
+ } catch (JwtException e) {
+ return false;
+ }
+ }
+
+ /** 从 Token 获取用户名(Subject) */
+ public String getUsername(String token) {
+ return parseClaims(token).getSubject();
+ }
+
+ /** 从 Token 获取用户 ID */
+ public Long getUserId(String token) {
+ Object id = parseClaims(token).get("userId");
+ return id == null ? null : Long.valueOf(id.toString());
+ }
+
+ /** 从 Token 获取角色 */
+ public String getRole(String token) {
+ return parseClaims(token).get("role", String.class);
+ }
+
+ /**
+ * 获取 JWT 的过期时间(毫秒)
+ * 修复:返回 JWTProperties 中配置的过期时间
+ */
+ public long getExpiration() {
+ return props.getExpirationMs();
+ }
+}
+
+--- End of: ./security/JwtTokenUtil.java ---
+
+--- Content of: ./service/UserService.java ---
+package com.example.demo.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.example.demo.dto.LoginResponseDTO;
+import com.example.demo.entity.User; // 导入 LoginResponseDTO
+
+/**
+ * User 业务层接口
+ * 继承了 MyBatis-Plus 的 IService,并定义了自定义的用户业务方法。
+ */
+public interface UserService extends IService<User> {
+ // 自定义方法:根据ID查找用户
+ User findById(Long id);
+
+ // 自定义方法:根据用户名查找用户
+ User findByUsername(String username);
+
+ // 自定义方法:创建新用户
+ User createUser(User user);
+
+ // 新增方法:生成用户登录响应(包括 JWT Token)
+ // 这个方法在 AuthController 中被调用,因此必须在接口中声明
+ LoginResponseDTO generateLoginResponse(User userDetails);
+
+ // 如果需要其他自定义方法,可以在这里再添加
+}
+
+--- End of: ./service/UserService.java ---
+
+--- Content of: ./service/impl/UserServiceImpl.java ---
+package com.example.demo.service.impl;
+
+// 导入UserService接口
+import java.time.Instant;
+import java.util.List;
+
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.example.demo.dto.LoginResponseDTO;
+import com.example.demo.entity.User;
+import com.example.demo.mapper.UserMapper;
+import com.example.demo.security.JwtTokenUtil;
+import com.example.demo.service.UserService;
+
+
+/**
+ * 用户业务逻辑服务实现类
+ * 继承了MyBatis-Plus的ServiceImpl,并实现了UserService接口和Spring Security的UserDetailsService接口。
+ * 利用ServiceImpl提供的通用CRUD方法,并实现自定义的用户业务操作和加载用户详情。
+ * 用户登录认证逻辑已移至 Controller 层,以解决循环依赖问题。
+ */
+@Service
+// 继承ServiceImpl,指定Mapper和实体类型。
+// 这样就自动拥有了IService中大部分通用方法的实现。
+public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService, UserDetailsService {
+
+ // 当继承ServiceImpl后,ServiceImpl内部已经注入了BaseMapper,
+ // 通常不需要在这里再次声明UserMapper,除非您需要调用UserMapper中自定义的非BaseMapper方法。
+ // private final UserMapper userMapper; // 如果继承ServiceImpl,这行通常可以移除
+
+ // 注入密码编码器,用于密码加密和校验
+ private final PasswordEncoder passwordEncoder;
+ // 移除 AuthenticationManager 注入,认证逻辑已移至 Controller
+ // private final AuthenticationManager authenticationManager;
+ // 注入JWT工具类,用于生成和解析Token
+ private final JwtTokenUtil jwtTokenUtil;
+
+
+ /**
+ * 构造函数注入非BaseMapper的依赖
+ * ServiceImpl会自动注入BaseMapper
+ * @param passwordEncoder 密码编码器
+ * // 移除 authenticationManager 参数
+ * @param jwtTokenUtil JWT工具类
+ */
+ // @Autowired // 当只有一个构造函数时,@Autowired 可选
+ public UserServiceImpl(PasswordEncoder passwordEncoder,
+ // AuthenticationManager authenticationManager, // 移除此参数
+ JwtTokenUtil jwtTokenUtil) {
+ // 当继承ServiceImpl时,不需要在构造函数中注入并初始化BaseMapper
+ // this.userMapper = userMapper; // 移除这行
+
+ this.passwordEncoder = passwordEncoder;
+ // this.authenticationManager = authenticationManager; // 移除此初始化
+ this.jwtTokenUtil = jwtTokenUtil;
+ }
+
+ /**
+ * 根据用户ID查找用户
+ * 实现UserService接口中的findById方法。
+ * 可以直接使用ServiceImpl提供的getById方法。
+ * @param id 用户ID
+ * @return 找到的用户实体,如果不存在则返回null
+ */
+ @Override
+ public User findById(Long id) {
+ // 直接使用ServiceImpl提供的通用getById方法
+ return this.getById(id);
+ // 如果UserMapper有自定义的findById方法且需要使用,可以通过getBaseMapper()调用:
+ // return this.getBaseMapper().findById(id);
+ }
+
+ /**
+ * 根据用户名查找用户
+ * 实现UserService接口中的findByUsername方法。
+ * 可以使用ServiceImpl提供的getOne方法结合Wrapper进行条件查询。
+ * @param username 用户名
+ * @return 找到的用户实体,如果不存在则返回null
+ */
+ @Override
+ public User findByUsername(String username) {
+ // 使用ServiceImpl提供的通用getOne方法结合QueryWrapper进行条件查询
+ return this.getOne(new QueryWrapper<User>().eq("username", username));
+ // 如果UserMapper有自定义的findByUsername方法且需要使用,可以通过getBaseMapper()调用:
+ // return this.getBaseMapper().findByUsername(username);
+ }
+
+ /**
+ * 创建新用户
+ * 实现UserService接口中的createUser方法。
+ * 可以使用ServiceImpl提供的save方法。
+ * @param user 待创建的用户实体
+ * @return 创建成功的用户实体
+ */
+ @Override
+ public User createUser(User user) {
+ // 在保存前对用户密码进行加密
+ user.setPassword(passwordEncoder.encode(user.getPassword()));
+
+ // 使用ServiceImpl提供的通用save方法将用户插入数据库
+ this.save(user);
+ return user;
+ }
+
+ /**
+ * 实现Spring Security的UserDetailsService接口方法
+ * 根据用户名加载用户详情。
+ * 这个方法会被Spring Security的认证流程调用。
+ * @param username 用户名
+ * @return 实现UserDetails接口的用户详情对象
+ * @throws UsernameNotFoundException 如果用户不存在
+ */
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ // 调用findByUsername方法从数据库查找用户
+ User user = findByUsername(username);
+ if (user == null) {
+ // 如果用户不存在,抛出UsernameNotFoundException
+ throw new UsernameNotFoundException("User not found with username: " + username);
+ }
+
+ // 返回Spring Security需要的UserDetails对象。
+ // 假设您的User实体类实现了UserDetails接口,并且其getAuthorities()方法正确返回了权限列表。
+ return user;
+ }
+
+ /**
+ * 用户登录认证成功后,生成JWT Token和构建响应DTO。
+ * 这个方法实现了UserService接口中声明的 generateLoginResponse 方法。
+ * @param userDetails 认证成功的用户详情(User实体)
+ * @return 包含JWT Token和相关信息的登录响应DTO
+ */
+ @Override // 添加 @Override 注解,明确表示实现接口方法
+ public LoginResponseDTO generateLoginResponse(User userDetails) {
+ // 使用JwtTokenUtil生成JWT Token
+ String token = jwtTokenUtil.generateToken(userDetails.getId(), userDetails.getUsername(), userDetails.getRole());
+
+ // 构建登录响应DTO
+ LoginResponseDTO response = new LoginResponseDTO();
+ response.setToken(token);
+ // 调用 JwtTokenUtil 的 getExpiration() 方法获取过期时间毫秒数
+ response.setExpiresAt(Instant.now().plusMillis(jwtTokenUtil.getExpiration()));
+ response.setUserId(userDetails.getId());
+ response.setUsername(userDetails.getUsername());
+ response.setRoles(List.of(userDetails.getRole())); // 假设角色是单个字符串,如果多个角色需要调整
+
+ return response;
+ }
+
+
+ // 当继承ServiceImpl后,IService中的通用方法(如 save, updateById, list, page 等)
+ // 已经由ServiceImpl提供默认实现,无需在此重复编写。
+ // 如果您需要对IService中的某个通用方法进行定制(例如在保存前进行额外处理),
+ // 可以在这里@Override该方法并添加您的逻辑,然后调用super.方法名(...)来执行ServiceImpl的默认逻辑。
+ /*
+ @Override
+ public boolean saveBatch(Collection<User> entityList, int batchSize) {
+ // 在调用ServiceImpl的批量保存前/后添加自定义逻辑
+ // ...
+ return super.saveBatch(entityList, batchSize); // 调用ServiceImpl的默认实现
+ }
+
+ // ... 其他IService中的方法实现
+ */
+}
+
+--- End of: ./service/impl/UserServiceImpl.java ---
+
+--- End File Content ---
diff --git a/backend/demo/src/main/java/com/example/demo/security/JwtAuthenticationFilter.java b/backend/demo/src/main/java/com/example/demo/security/JwtAuthenticationFilter.java
index 108e966..520908c 100644
--- a/backend/demo/src/main/java/com/example/demo/security/JwtAuthenticationFilter.java
+++ b/backend/demo/src/main/java/com/example/demo/security/JwtAuthenticationFilter.java
@@ -47,7 +47,7 @@
}
Claims claims = tokenUtil.parseClaims(token);
- String username = claims.getSubject();
+ String username =tokenUtil.getUsername(token);
Long userId = tokenUtil.getUserId(token);
String role = tokenUtil.getRole(token);
diff --git a/backend/demo/src/main/java/com/example/demo/security/JwtTokenUtil.java b/backend/demo/src/main/java/com/example/demo/security/JwtTokenUtil.java
index 3f44d8a..7a9be5a 100644
--- a/backend/demo/src/main/java/com/example/demo/security/JwtTokenUtil.java
+++ b/backend/demo/src/main/java/com/example/demo/security/JwtTokenUtil.java
@@ -75,7 +75,11 @@
return parseClaims(token).get("role", String.class);
}
+ /**
+ * 获取 JWT 的过期时间(毫秒)
+ * 修复:返回 JWTProperties 中配置的过期时间
+ */
public long getExpiration() {
- throw new UnsupportedOperationException("Not supported yet.");
+ return props.getExpirationMs();
}
}
diff --git a/backend/demo/src/main/java/com/example/demo/service/TorrentService.java b/backend/demo/src/main/java/com/example/demo/service/TorrentService.java
new file mode 100644
index 0000000..b863798
--- /dev/null
+++ b/backend/demo/src/main/java/com/example/demo/service/TorrentService.java
@@ -0,0 +1,26 @@
+// src/main/java/com/example/demo/service/TorrentService.java
+package com.example.demo.service;
+
+import java.io.IOException;
+
+import org.springframework.web.multipart.MultipartFile;
+
+import com.example.demo.dto.TorrentInfoDTO;
+import com.turn.ttorrent.bcodec.InvalidBEncodingException; // 导入 InvalidBEncodingException
+
+/**
+ * 处理 .torrent 文件上传及相关业务逻辑的服务接口。
+ */
+public interface TorrentService {
+
+ /**
+ * 处理上传的 .torrent 文件,解析其内容,保存到数据库,并生成相关链接。
+ *
+ * @param file 上传的 MultipartFile 对象
+ * @return 包含解析信息、磁力链接和下载 URL 的 TorrentInfoDTO
+ * @throws IOException 如果文件读取或保存失败
+ * @throws InvalidBEncodingException 如果 .torrent 文件内容不符合 B 编码规范
+ * @throws IllegalArgumentException 如果文件为空或解析后的元数据不完整
+ */
+ TorrentInfoDTO handleUpload(MultipartFile file) throws IOException, InvalidBEncodingException, IllegalArgumentException;
+}
\ No newline at end of file
diff --git a/backend/demo/src/main/java/com/example/demo/service/UserService.java b/backend/demo/src/main/java/com/example/demo/service/UserService.java
index 60e8aef..e4c5d7b 100644
--- a/backend/demo/src/main/java/com/example/demo/service/UserService.java
+++ b/backend/demo/src/main/java/com/example/demo/service/UserService.java
@@ -1,13 +1,26 @@
package com.example.demo.service;
-
import com.baomidou.mybatisplus.extension.service.IService;
-import com.example.demo.entity.User;
+import com.example.demo.dto.LoginResponseDTO;
+import com.example.demo.entity.User; // 导入 LoginResponseDTO
/**
* User 业务层接口
+ * 继承了 MyBatis-Plus 的 IService,并定义了自定义的用户业务方法。
*/
public interface UserService extends IService<User> {
- // 如果需要自定义方法,可以在这里再添加,例如:
- // User findByUsername(String username);
+ // 自定义方法:根据ID查找用户
+ User findById(Long id);
+
+ // 自定义方法:根据用户名查找用户
+ User findByUsername(String username);
+
+ // 自定义方法:创建新用户
+ User createUser(User user);
+
+ // 新增方法:生成用户登录响应(包括 JWT Token)
+ // 这个方法在 AuthController 中被调用,因此必须在接口中声明
+ LoginResponseDTO generateLoginResponse(User userDetails);
+
+ // 如果需要其他自定义方法,可以在这里再添加
}
diff --git a/backend/demo/src/main/java/com/example/demo/service/impl/TorrentServiceImpl.java b/backend/demo/src/main/java/com/example/demo/service/impl/TorrentServiceImpl.java
new file mode 100644
index 0000000..eb00160
--- /dev/null
+++ b/backend/demo/src/main/java/com/example/demo/service/impl/TorrentServiceImpl.java
@@ -0,0 +1,126 @@
+// src/main/java/com/example/demo/service/impl/TorrentServiceImpl.java
+package com.example.demo.service.impl;
+
+import java.io.IOException;
+import java.nio.file.Files; // 导入 DTO
+import java.nio.file.Path; // 导入实体类
+import java.nio.file.Paths; // 导入 Mapper
+
+import org.springframework.beans.factory.annotation.Value; // 导入服务接口
+import org.springframework.stereotype.Service; // 导入工具类
+import org.springframework.transaction.annotation.Transactional; // 导入 ttorrent 的异常
+import org.springframework.web.multipart.MultipartFile; // 导入 @Value 用于读取配置
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.example.demo.dto.TorrentInfoDTO; // 导入事务注解
+import com.example.demo.entity.TorrentInfo;
+import com.example.demo.mapper.TorrentInfoMapper;
+import com.example.demo.service.TorrentService; // 用于文件操作
+import com.example.demo.util.TorrentUtils; // 用于文件路径
+import com.turn.ttorrent.bcodec.InvalidBEncodingException; // 用于文件路径
+
+/**
+ * TorrentService 接口的实现类,负责处理 .torrent 文件的上传、解析、持久化及链接生成。
+ */
+@Service // 标记这是一个 Spring Service 组件
+public class TorrentServiceImpl implements TorrentService {
+
+ private final TorrentInfoMapper torrentInfoMapper;
+
+ @Value("${file.upload-dir}") // 从 application.properties 中注入文件上传目录
+ private String uploadDir;
+
+ // 通过构造函数注入 Mapper
+ public TorrentServiceImpl(TorrentInfoMapper torrentInfoMapper) {
+ this.torrentInfoMapper = torrentInfoMapper;
+ }
+
+ @Override
+ @Transactional // 确保文件保存和数据库操作的原子性
+ public TorrentInfoDTO handleUpload(MultipartFile file) throws IOException, InvalidBEncodingException, IllegalArgumentException {
+ if (file.isEmpty()) {
+ throw new IllegalArgumentException("上传文件不能为空。");
+ }
+
+ // 1. 调用 TorrentUtils 解析 .torrent 文件
+ TorrentUtils.TorrentParsedInfo parsedInfo;
+ try {
+ parsedInfo = TorrentUtils.parseTorrentFile(file);
+ } catch (InvalidBEncodingException e) {
+ throw new IllegalArgumentException("无法解析 .torrent 文件,文件格式不正确: " + e.getMessage(), e);
+ } catch (IOException e) {
+ throw new IOException("读取上传文件失败: " + e.getMessage(), e);
+ }
+
+ String infoHash = parsedInfo.getInfoHash();
+ String fileName = parsedInfo.getFileName();
+ long fileSize = parsedInfo.getFileSize();
+
+ // 2. 检查 infoHash 是否已存在,避免重复上传相同的洪流
+ QueryWrapper<TorrentInfo> queryWrapper = new QueryWrapper<>();
+ queryWrapper.eq("info_hash", infoHash); // 根据 info_hash 查询
+ TorrentInfo existingTorrent = torrentInfoMapper.selectOne(queryWrapper);
+
+ if (existingTorrent != null) {
+ System.out.println("Torrent with infoHash " + infoHash + " already exists. Returning existing info.");
+ // 如果已存在,直接返回现有信息对应的 DTO
+ return new TorrentInfoDTO(
+ existingTorrent.getFileName(),
+ existingTorrent.getFileSize(),
+ existingTorrent.getInfoHash(),
+ existingTorrent.getMagnetUri(),
+ existingTorrent.getDownloadUrl(),
+ "该洪流已存在,返回现有信息。"
+ );
+ }
+
+ // 3. 保存 .torrent 文件到服务器文件系统
+ // 使用 infoHash 作为文件名,确保唯一性,并保留原始扩展名 .torrent
+ String storedFileName = infoHash + ".torrent";
+ Path uploadPath = Paths.get(uploadDir).toAbsolutePath().normalize();
+ Path filePath = uploadPath.resolve(storedFileName);
+
+ try {
+ Files.createDirectories(uploadPath); // 如果目录不存在则创建
+ Files.copy(file.getInputStream(), filePath); // 将文件流复制到目标路径
+ System.out.println("Torrent file saved to: " + filePath.toString());
+ } catch (IOException e) {
+ // 如果文件保存失败,需要回滚之前的数据库操作 (如果前面有的话)
+ // @Transactional 会自动处理
+ throw new IOException("无法保存 .torrent 文件到磁盘: " + e.getMessage(), e);
+ }
+
+ // 4. 生成 magnet 链接 & downloadUrl
+ String magnetUri = String.format("magnet:?xt=urn:btih:%s&dn=%s", infoHash, fileName);
+ // downloadUrl 指向后端实际的下载接口
+ String downloadUrl = String.format("/api/downloads/%s/%s", infoHash, fileName);
+
+ // 5. 组装实体 TorrentInfo
+ TorrentInfo torrentInfo = new TorrentInfo(
+ infoHash,
+ fileName,
+ fileSize,
+ magnetUri,
+ downloadUrl
+ );
+
+ // 6. 调用 Mapper.insert() 持久化一条记录到数据库
+ int insertedRows = torrentInfoMapper.insert(torrentInfo);
+ if (insertedRows != 1) {
+ throw new RuntimeException("保存洪流信息到数据库失败。");
+ }
+ System.out.println("Saved TorrentInfo to database: " + torrentInfo);
+
+ // 7. 构造 DTO 返回给前端
+ TorrentInfoDTO dto = new TorrentInfoDTO(
+ torrentInfo.getFileName(),
+ torrentInfo.getFileSize(),
+ torrentInfo.getInfoHash(),
+ torrentInfo.getMagnetUri(),
+ torrentInfo.getDownloadUrl(),
+ "Torrent uploaded and processed successfully!"
+ );
+
+ return dto;
+ }
+}
\ No newline at end of file
diff --git a/backend/demo/src/main/java/com/example/demo/service/impl/UserServiceImpl.java b/backend/demo/src/main/java/com/example/demo/service/impl/UserServiceImpl.java
index 5bd1693..1fc434c 100644
--- a/backend/demo/src/main/java/com/example/demo/service/impl/UserServiceImpl.java
+++ b/backend/demo/src/main/java/com/example/demo/service/impl/UserServiceImpl.java
@@ -1,23 +1,171 @@
package com.example.demo.service.impl;
-// UserServiceImpl.java
+// 导入UserService接口
+import java.time.Instant;
+import java.util.List;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.example.demo.dto.LoginResponseDTO;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
+import com.example.demo.security.JwtTokenUtil;
import com.example.demo.service.UserService;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
+
/**
- * User 业务层默认实现
+ * 用户业务逻辑服务实现类
+ * 继承了MyBatis-Plus的ServiceImpl,并实现了UserService接口和Spring Security的UserDetailsService接口。
+ * 利用ServiceImpl提供的通用CRUD方法,并实现自定义的用户业务操作和加载用户详情。
+ * 用户登录认证逻辑已移至 Controller 层,以解决循环依赖问题。
*/
@Service
-@Transactional(rollbackFor = Exception.class)
-public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
- // 如果你在接口中声明了自定义方法,这里可以重写并实现:
- // @Override
- // public User findByUsername(String username) {
- // return lambdaQuery().eq(User::getUsername, username).one();
- // }
+// 继承ServiceImpl,指定Mapper和实体类型。
+// 这样就自动拥有了IService中大部分通用方法的实现。
+public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService, UserDetailsService {
+
+ // 当继承ServiceImpl后,ServiceImpl内部已经注入了BaseMapper,
+ // 通常不需要在这里再次声明UserMapper,除非您需要调用UserMapper中自定义的非BaseMapper方法。
+ // private final UserMapper userMapper; // 如果继承ServiceImpl,这行通常可以移除
+
+ // 注入密码编码器,用于密码加密和校验
+ private final PasswordEncoder passwordEncoder;
+ // 移除 AuthenticationManager 注入,认证逻辑已移至 Controller
+ // private final AuthenticationManager authenticationManager;
+ // 注入JWT工具类,用于生成和解析Token
+ private final JwtTokenUtil jwtTokenUtil;
+
+
+ /**
+ * 构造函数注入非BaseMapper的依赖
+ * ServiceImpl会自动注入BaseMapper
+ * @param passwordEncoder 密码编码器
+ * // 移除 authenticationManager 参数
+ * @param jwtTokenUtil JWT工具类
+ */
+ // @Autowired // 当只有一个构造函数时,@Autowired 可选
+ public UserServiceImpl(PasswordEncoder passwordEncoder,
+ // AuthenticationManager authenticationManager, // 移除此参数
+ JwtTokenUtil jwtTokenUtil) {
+ // 当继承ServiceImpl时,不需要在构造函数中注入并初始化BaseMapper
+ // this.userMapper = userMapper; // 移除这行
+
+ this.passwordEncoder = passwordEncoder;
+ // this.authenticationManager = authenticationManager; // 移除此初始化
+ this.jwtTokenUtil = jwtTokenUtil;
+ }
+
+ /**
+ * 根据用户ID查找用户
+ * 实现UserService接口中的findById方法。
+ * 可以直接使用ServiceImpl提供的getById方法。
+ * @param id 用户ID
+ * @return 找到的用户实体,如果不存在则返回null
+ */
+ @Override
+ public User findById(Long id) {
+ // 直接使用ServiceImpl提供的通用getById方法
+ return this.getById(id);
+ // 如果UserMapper有自定义的findById方法且需要使用,可以通过getBaseMapper()调用:
+ // return this.getBaseMapper().findById(id);
+ }
+
+ /**
+ * 根据用户名查找用户
+ * 实现UserService接口中的findByUsername方法。
+ * 可以使用ServiceImpl提供的getOne方法结合Wrapper进行条件查询。
+ * @param username 用户名
+ * @return 找到的用户实体,如果不存在则返回null
+ */
+ @Override
+ public User findByUsername(String username) {
+ // 使用ServiceImpl提供的通用getOne方法结合QueryWrapper进行条件查询
+ return this.getOne(new QueryWrapper<User>().eq("username", username));
+ // 如果UserMapper有自定义的findByUsername方法且需要使用,可以通过getBaseMapper()调用:
+ // return this.getBaseMapper().findByUsername(username);
+ }
+
+ /**
+ * 创建新用户
+ * 实现UserService接口中的createUser方法。
+ * 可以使用ServiceImpl提供的save方法。
+ * @param user 待创建的用户实体
+ * @return 创建成功的用户实体
+ */
+ @Override
+ public User createUser(User user) {
+ // 在保存前对用户密码进行加密
+ user.setPassword(passwordEncoder.encode(user.getPassword()));
+
+ // 使用ServiceImpl提供的通用save方法将用户插入数据库
+ this.save(user);
+ return user;
+ }
+
+ /**
+ * 实现Spring Security的UserDetailsService接口方法
+ * 根据用户名加载用户详情。
+ * 这个方法会被Spring Security的认证流程调用。
+ * @param username 用户名
+ * @return 实现UserDetails接口的用户详情对象
+ * @throws UsernameNotFoundException 如果用户不存在
+ */
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ // 调用findByUsername方法从数据库查找用户
+ User user = findByUsername(username);
+ if (user == null) {
+ // 如果用户不存在,抛出UsernameNotFoundException
+ throw new UsernameNotFoundException("User not found with username: " + username);
+ }
+
+ // 返回Spring Security需要的UserDetails对象。
+ // 假设您的User实体类实现了UserDetails接口,并且其getAuthorities()方法正确返回了权限列表。
+ return user;
+ }
+
+ /**
+ * 用户登录认证成功后,生成JWT Token和构建响应DTO。
+ * 这个方法实现了UserService接口中声明的 generateLoginResponse 方法。
+ * @param userDetails 认证成功的用户详情(User实体)
+ * @return 包含JWT Token和相关信息的登录响应DTO
+ */
+ @Override // 添加 @Override 注解,明确表示实现接口方法
+ public LoginResponseDTO generateLoginResponse(User userDetails) {
+ // 使用JwtTokenUtil生成JWT Token
+ String token = jwtTokenUtil.generateToken(userDetails.getId(), userDetails.getUsername(), userDetails.getRole());
+
+ // 构建登录响应DTO
+ LoginResponseDTO response = new LoginResponseDTO();
+ response.setToken(token);
+ // 调用 JwtTokenUtil 的 getExpiration() 方法获取过期时间毫秒数
+ response.setExpiresAt(Instant.now().plusMillis(jwtTokenUtil.getExpiration()));
+ response.setUserId(userDetails.getId());
+ response.setUsername(userDetails.getUsername());
+ response.setRoles(List.of(userDetails.getRole())); // 假设角色是单个字符串,如果多个角色需要调整
+
+ return response;
+ }
+
+
+ // 当继承ServiceImpl后,IService中的通用方法(如 save, updateById, list, page 等)
+ // 已经由ServiceImpl提供默认实现,无需在此重复编写。
+ // 如果您需要对IService中的某个通用方法进行定制(例如在保存前进行额外处理),
+ // 可以在这里@Override该方法并添加您的逻辑,然后调用super.方法名(...)来执行ServiceImpl的默认逻辑。
+ /*
+ @Override
+ public boolean saveBatch(Collection<User> entityList, int batchSize) {
+ // 在调用ServiceImpl的批量保存前/后添加自定义逻辑
+ // ...
+ return super.saveBatch(entityList, batchSize); // 调用ServiceImpl的默认实现
+ }
+
+ // ... 其他IService中的方法实现
+ */
}
diff --git a/backend/demo/src/main/java/com/example/demo/sh.py b/backend/demo/src/main/java/com/example/demo/sh.py
new file mode 100644
index 0000000..3be143b
--- /dev/null
+++ b/backend/demo/src/main/java/com/example/demo/sh.py
@@ -0,0 +1,76 @@
+import os
+
+def read_files_and_write_to_file(file_paths, output_file):
+ """
+ Reads the content of specified files and writes it to an output file.
+
+ Args:
+ file_paths (list): A list of strings, where each string is the path to a file.
+ output_file (str): The path to the file where the output will be written.
+ """
+ try:
+ # Open the output file in write mode ('w').
+ # Use 'utf-8' encoding for wide compatibility.
+ with open(output_file, 'w', encoding='utf-8') as outfile:
+ outfile.write("--- Start File Content ---\n")
+ print(f"Reading files and writing to {output_file}...") # Print to console for feedback
+
+ for file_path in file_paths:
+ if not os.path.exists(file_path):
+ error_msg = f"Error: File not found at path: {file_path}\n"
+ outfile.write(error_msg)
+ print(error_msg.strip()) # Also print error to console
+ continue
+ if not os.path.isfile(file_path):
+ error_msg = f"Error: Path is not a file: {file_path}\n"
+ outfile.write(error_msg)
+ print(error_msg.strip()) # Also print error to console
+ continue
+
+ try:
+ with open(file_path, 'r', encoding='utf-8') as infile:
+ content = infile.read()
+ outfile.write(f"\n--- Content of: {file_path} ---\n")
+ outfile.write(content)
+ outfile.write(f"\n--- End of: {file_path} ---\n")
+ print(f"Successfully read: {file_path}") # Print success to console
+ except Exception as e:
+ error_msg = f"Error reading file {file_path}: {e}\n"
+ outfile.write(error_msg)
+ print(error_msg.strip()) # Also print error to console
+
+ outfile.write("\n--- End File Content ---\n")
+ print("Finished writing file content.")
+
+ except Exception as e:
+ print(f"Error opening or writing to output file {output_file}: {e}")
+
+
+if __name__ == "__main__":
+ # 请在这里列出您想要读取的文件路径
+ # 确保路径是相对于您运行脚本的位置,或者使用绝对路径
+ your_file_paths = [
+ './config/JWTProperties.java',
+ './config/SecurityConfig.java',
+ './controller/AuthController.java',
+ './DemoApplication.java',
+ './dto/LoginRequestDTO.java',
+ './dto/LoginResponseDTO.java',
+ './entity/User.java',
+ './exception/AuthException.java',
+ './exception/GlobalExceptionHandler.java',
+ './mapper/UserMapper.java',
+ './security/JwtAuthenticationFilter.java',
+ './security/JwtTokenUtil.java',
+ './service/UserService.java',
+ './service/impl/UserServiceImpl.java', # 假设您已经创建了这个文件
+
+ ]
+
+ # 指定输出文件的名称
+ output_filename = "result.txt"
+
+ # 调用函数,将文件内容写入到 output_filename
+ read_files_and_write_to_file(your_file_paths, output_filename)
+
+
diff --git a/backend/demo/src/main/java/com/example/demo/util/TorrentUtils.java b/backend/demo/src/main/java/com/example/demo/util/TorrentUtils.java
new file mode 100644
index 0000000..465a167
--- /dev/null
+++ b/backend/demo/src/main/java/com/example/demo/util/TorrentUtils.java
@@ -0,0 +1,102 @@
+// src/main/java/com/example/demo/util/TorrentUtils.java
+package com.example.demo.util;
+
+import java.io.IOException; // 导入 TorrentMetadata 接口
+
+import org.springframework.web.multipart.MultipartFile; // 导入 TorrentParser 类
+
+import com.turn.ttorrent.bcodec.InvalidBEncodingException;
+import com.turn.ttorrent.common.TorrentMetadata;
+import com.turn.ttorrent.common.TorrentParser;
+
+public class TorrentUtils {
+
+ /**
+ * 解析 MultipartFile 中的 .torrent 文件,提取关键信息。
+ *
+ * 该方法利用 ttorrent-common 库中的 TorrentParser 来解析 .torrent 文件的字节内容,
+ * 并从解析得到的 TorrentMetadata 对象中提取 info hash、文件名和文件大小。
+ *
+ * @param file 上传的 MultipartFile 对象
+ * @return 包含解析信息的 TorrentParsedInfo 对象
+ * @throws IOException 如果文件读取失败
+ * @throws BDecodingException 如果 .torrent 文件内容不符合 B 编码规范
+ * @throws IllegalArgumentException 如果文件为空或解析后的元数据不完整
+ */
+ public static TorrentParsedInfo parseTorrentFile(MultipartFile file)
+ throws IOException, IllegalArgumentException {
+
+ if (file.isEmpty()) {
+ throw new IllegalArgumentException("Uploaded file is empty.");
+ }
+
+ byte[] torrentBytes = file.getBytes();
+
+ // 使用 ttorrent-common 库的 TorrentParser 来解析字节数组
+ TorrentParser parser = new TorrentParser();
+ TorrentMetadata metadata;
+ try {
+ metadata = parser.parse(torrentBytes);
+ } catch (com.turn.ttorrent.bcodec.InvalidBEncodingException e) {
+ // 将 InvalidBEncodingException 转换为 BDecodingException
+ throw new InvalidBEncodingException("Invalid B-encoding in .torrent file: " + e.getMessage());
+ } catch (RuntimeException e) {
+ // TorrentParser.parse 可能会包装 IOException 为 RuntimeException
+ if (e.getCause() instanceof IOException) {
+ throw (IOException) e.getCause();
+ }
+ throw e; // 重新抛出其他 RuntimeException
+ }
+
+ // 从 TorrentMetadata 对象中提取所需信息
+ String infoHash = metadata.getHexInfoHash(); // 获取十六进制表示的 info hash
+ String fileName = metadata.getDirectoryName(); // 获取洪流的名称(通常是目录名或单文件名)
+ long totalSize = 0;
+
+ // 计算总文件大小
+ // TorrentMetadata.getFiles() 返回 List<TorrentFile>
+ // TorrentFile 包含 size 字段
+ if (metadata.getFiles() != null && !metadata.getFiles().isEmpty()) {
+ for (com.turn.ttorrent.common.TorrentFile torrentFile : metadata.getFiles()) {
+ totalSize += torrentFile.size;
+ }
+ } else {
+ // 如果没有文件列表,可能是单文件模式,但 TorrentMetadata 接口没有直接提供总大小
+ // 在实际应用中,如果 TorrentMetadata 无法直接提供总大小,
+ // 并且 getFiles() 为空,可能需要回溯到原始的 BEValue 来获取 'length' 字段
+ // 但根据 TorrentMetadata 接口,getFiles() 应该能覆盖所有情况。
+ System.err.println("Warning: Could not determine total file size from TorrentMetadata.getFiles().");
+ }
+
+
+ // 构建并返回解析信息
+ return new TorrentParsedInfo(infoHash, fileName, totalSize);
+ }
+
+ /**
+ * 内部类,用于封装解析后的洪流信息。
+ */
+ public static class TorrentParsedInfo {
+ private final String infoHash;
+ private final String fileName;
+ private final long fileSize;
+
+ public TorrentParsedInfo(String infoHash, String fileName, long fileSize) {
+ this.infoHash = infoHash;
+ this.fileName = fileName;
+ this.fileSize = fileSize;
+ }
+
+ public String getInfoHash() {
+ return infoHash;
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public long getFileSize() {
+ return fileSize;
+ }
+ }
+}
\ No newline at end of file
diff --git a/backend/demo/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/backend/demo/src/main/resources/META-INF/additional-spring-configuration-metadata.json
new file mode 100644
index 0000000..cf06b61
--- /dev/null
+++ b/backend/demo/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -0,0 +1,5 @@
+{"properties": [{
+ "name": "file.upload-dir",
+ "type": "java.lang.String",
+ "description": "Torrent dir for uploading'"
+}]}
\ No newline at end of file
diff --git a/backend/demo/src/main/resources/application.properties b/backend/demo/src/main/resources/application.properties
index 4472028..9bbb45b 100644
--- a/backend/demo/src/main/resources/application.properties
+++ b/backend/demo/src/main/resources/application.properties
@@ -1,5 +1,5 @@
# ========== 数据源 ==========
-spring.datasource.url=jdbc:mysql://mysql:3306/mydatabase?serverTimezone=Asia/Shanghai
+spring.datasource.url=jdbc:mysql://mysql:3306/mydatabase?serverTimezone=Asia/Shanghai&createDatabaseIfNotExist=TRUE&useSSL=FALSE&allowPublicKeyRetrieval=TRUE
spring.datasource.username=myuser
spring.datasource.password=secret
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
@@ -10,3 +10,6 @@
# ========== 日志输出(可选) ==========
logging.level.org.springframework.security=DEBUG
+
+file.upload-dir=./torrents
+