Final push for project
Change-Id: I9103078156eca93df2482b9fe3854d9301bb98b3
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 b3e7f91..0c3672b 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
@@ -12,6 +12,9 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.web.cors.CorsConfiguration; // 导入 CorsConfiguration
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource; // 导入 UrlBasedCorsConfigurationSource
+import org.springframework.web.filter.CorsFilter; // 导入 CorsFilter
import com.example.demo.security.JwtAuthenticationFilter;
@@ -41,6 +44,22 @@
}
/**
+ * 配置 CORS 过滤器。
+ * 允许前端应用 (http://localhost:5173) 访问后端 API。
+ */
+ @Bean
+ public CorsFilter corsFilter() {
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ CorsConfiguration config = new CorsConfiguration();
+ config.setAllowCredentials(true); // 允许发送 Cookie
+ config.addAllowedOrigin("http://localhost:5173"); // 允许的前端源
+ config.addAllowedHeader("*"); // 允许所有请求头
+ config.addAllowedMethod("*"); // 允许所有 HTTP 方法 (GET, POST, PUT, DELETE等)
+ source.registerCorsConfiguration("/**", config); // 对所有路径应用 CORS 配置
+ return new CorsFilter(source);
+ }
+
+ /**
* 核心安全策略配置。
* 配置内容包括:禁用 CSRF、禁用 Session 管理(使用 JWT 无状态)、
* 配置请求的授权规则,并将自定义的 JWT 认证过滤器添加到过滤器链中。
@@ -55,6 +74,15 @@
JwtAuthenticationFilter jwtFilter // 直接注入 JwtAuthenticationFilter Bean
) throws Exception {
http
+ // 添加 CORS 过滤器到 Spring Security 过滤器链的开头
+ .cors(cors -> cors.configurationSource(request -> {
+ CorsConfiguration config = new CorsConfiguration();
+ config.setAllowCredentials(true);
+ config.addAllowedOrigin("http://localhost:5173");
+ config.addAllowedHeader("*");
+ config.addAllowedMethod("*");
+ return config;
+ }))
// 禁用 CSRF (跨站请求伪造) 保护,因为 JWT 是无状态的,不需要 CSRF 保护
.csrf(AbstractHttpConfigurer::disable)
// 配置 Session 管理策略为无状态,不使用 Session
@@ -62,7 +90,7 @@
// 配置请求的授权规则
.authorizeHttpRequests(auth -> auth
// 允许对 /auth/** 路径下的所有请求进行匿名访问 (例如登录、注册接口)
- .requestMatchers("/auth/**").permitAll()
+ .requestMatchers("/api/auth/**").permitAll() // 确保这里的路径与您的 AuthController 匹配
// 其他所有请求都需要进行身份认证
.anyRequest().authenticated()
)
diff --git a/backend/demo/src/main/java/com/example/demo/config/WebConfig.java b/backend/demo/src/main/java/com/example/demo/config/WebConfig.java
new file mode 100644
index 0000000..839dce2
--- /dev/null
+++ b/backend/demo/src/main/java/com/example/demo/config/WebConfig.java
@@ -0,0 +1,19 @@
+package com.example.demo.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+
+ // @Override
+ public void addCorsMappings(CorsRegistry registry) {
+ registry.addMapping("/api/**") // Apply to your API path
+ .allowedOrigins("http://localhost:5173") // URL of your React app
+ .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
+ .allowedHeaders("*")
+ .allowCredentials(true);
+ }
+
+}
\ No newline at end of file
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 d7dfe50..18f3105 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
@@ -1,17 +1,13 @@
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.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder; // 导入 AuthenticationManager
+import org.springframework.security.crypto.password.PasswordEncoder; // 导入 UsernamePasswordAuthenticationToken
+import org.springframework.validation.BindingResult; // 导入 Authentication
+import org.springframework.web.bind.annotation.PostMapping; // 导入 SecurityContextHolder
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -45,64 +41,45 @@
}
@PostMapping("/login")
- public ResponseEntity<?> login(
- @Valid @RequestBody LoginRequestDTO loginRequest,
- BindingResult bindingResult) {
+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));
- }
+ if (bindingResult.hasErrors()) {
+ // … 参数校验错误的处理,这里不会走到
+ }
- try {
- // 使用 AuthenticationManager 进行认证
- // 这会触发 UserServiceImpl 中的 loadUserByUsername 方法的调用,并比较密码
- Authentication authentication = authenticationManager.authenticate(
- new UsernamePasswordAuthenticationToken(
- loginRequest.getUsername(),
- loginRequest.getPassword()
- )
- );
- // 将认证信息设置到 Spring Security 的上下文中
- SecurityContextHolder.getContext().setAuthentication(authentication);
+ try {
+ System.out.println("login(): 开始调用 authenticationManager.authenticate");
+ Authentication authentication = authenticationManager.authenticate(
+ new UsernamePasswordAuthenticationToken(
+ loginRequest.getUsername(),
+ loginRequest.getPassword()
+ )
+ );
+ System.out.println("login(): authenticate 成功,principal = "
+ + authentication.getPrincipal());
- // 获取认证后的用户详情(假设 User 实体实现了 UserDetails 接口)
- User userDetails = (User) authentication.getPrincipal();
+ // 设置到上下文
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ System.out.println("login(): SecurityContext 设置完成");
- // 调用 UserService 方法生成登录响应(包括 JWT Token)
- LoginResponseDTO response = userService.generateLoginResponse(userDetails);
+ // 强转 principal 为 用户实体
+ User userDetails = (User) authentication.getPrincipal();
+ System.out.println("login(): 拿到 userDetails,username = " + userDetails.getUsername());
- 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()));
+ // 调用生成 JWT 的方法
+ System.out.println("login(): 调用 userService.generateLoginResponse");
+ LoginResponseDTO response = userService.generateLoginResponse(userDetails);
+ System.out.println("login(): generateLoginResponse 返回 token = " + response.getToken());
return ResponseEntity.ok(response);
- */
+
+ } catch (Exception e) {
+ System.out.println("login(): 捕获到异常: " + e.getClass().getSimpleName() + " - " + e.getMessage());
+ e.printStackTrace();
+ throw new AuthException("用户名或密码错误", e);
}
+}
+
}
\ 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
index 0bd501a..6a91d27 100644
--- a/backend/demo/src/main/java/com/example/demo/controller/TorrentController.java
+++ b/backend/demo/src/main/java/com/example/demo/controller/TorrentController.java
@@ -1,16 +1,17 @@
// 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 java.io.IOException;
+import java.net.MalformedURLException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
-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.beans.factory.annotation.Value;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType; // 导入 MediaType
+import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -27,38 +28,37 @@
/**
* 处理 .torrent 文件上传和下载的 RESTful 控制器。
*/
-@RestController // 组合了 @Controller 和 @ResponseBody,表示这是一个 RESTful 控制器
-@RequestMapping("/api") // 定义所有处理方法的根路径
+@RestController
+@RequestMapping("/api")
public class TorrentController {
private final TorrentService torrentService;
- @Value("${file.upload-dir}") // 从 application.properties 中注入文件上传目录
+ @Value("${file.upload-dir}")
private String uploadDir;
- // 通过构造函数注入 TorrentService
public TorrentController(TorrentService torrentService) {
this.torrentService = torrentService;
}
/**
- * 处理 .torrent 文件的上传请求。
+ * 上传 .torrent 文件并处理其元信息。
*
- * @param file 上传的 MultipartFile 对象,通过 @RequestParam("file") 绑定。
- * @return 包含 TorrentInfoDTO 的 ResponseEntity,表示操作结果。
+ * @param file 上传的 .torrent 文件
+ * @param title 用户指定的标题
+ * @return 处理结果
*/
- @PostMapping("/torrents") // 映射到 /api/torrents 的 POST 请求
- public ResponseEntity<TorrentInfoDTO> uploadTorrent(@RequestParam("file") MultipartFile file) {
- // 1. 接收 HTTP 请求 (通过 @PostMapping 和 @RequestParam)
+ @PostMapping("/torrents/upload")
+ public ResponseEntity<TorrentInfoDTO> uploadTorrent(
+ @RequestParam("file") MultipartFile file,
+ @RequestParam("title") String title) {
- // 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 文件。");
@@ -66,65 +66,66 @@
}
try {
- // 3. 调用 Service 层处理业务逻辑
- TorrentInfoDTO result = torrentService.handleUpload(file);
-
- // 4. 返回 TorrentInfoDTO 给前端
+ TorrentInfoDTO result = torrentService.handleUpload(file, title);
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(); // 打印堆栈跟踪以便调试
+ e.printStackTrace();
return new ResponseEntity<>(errorDto, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
/**
- * 实现 .torrent 文件的下载功能。
- * 根据 infoHash 找到对应的 .torrent 文件并提供下载。
+ * 下载已上传的 .torrent 文件。
*
- * @param infoHash 洪流的 info hash,用于查找服务器上的文件。
- * @param fileName 原始文件名 (用于设置下载时的文件名,实际查找文件仍依赖 infoHash)。
- * @return 包含 .torrent 文件内容的 ResponseEntity。
+ * @param infoHash 该文件的 info hash,用于定位文件
+ * @param fileName 用户下载时看到的文件名
+ * @return 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);
+ // —— 2. 启动下载 —— //
+ @PostMapping("/download")
+ public ResponseEntity<Void> download(@RequestParam("infoHash") String infoHash) {
+ try {
+ torrentService.startDownload(infoHash);
+ return ResponseEntity.accepted().build();
+ } catch (Exception e) {
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
- } catch (IOException e) { // 只保留 IOException
- return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
- // MalformedURLException 相关的 catch 块已移除
+
+ // —— 3. 查询下载进度 —— //
+ @GetMapping("/download/status/{infoHash}")
+ public ResponseEntity<Float> status(@PathVariable String infoHash) {
+ float progress = torrentService.getDownloadProgress(infoHash);
+ return ResponseEntity.ok(progress);
+ }
+
+ // —— 4. 获取下载后的文件路径 —— //
+ // 如果你想让浏览器直接触发下载,也可以重定向或返回文件流
+ @GetMapping("/download/file/{infoHash}")
+ public ResponseEntity<Resource> file(@PathVariable String infoHash) throws MalformedURLException {
+ String path = torrentService.getDownloadedFilePath(infoHash);
+ if (path == null) {
+ return ResponseEntity.notFound().build();
+ }
+ UrlResource resource = new UrlResource("file:" + path);
+ return ResponseEntity.ok()
+ .header(HttpHeaders.CONTENT_DISPOSITION,
+ "attachment; filename=\"" + Paths.get(path).getFileName() + "\"")
+ .body(resource);
+ }
+
}
-}
\ No newline at end of file
diff --git a/backend/demo/src/main/java/com/example/demo/result.txt b/backend/demo/src/main/java/com/example/demo/result.txt
index bd1dd30..d7cdef4 100644
--- a/backend/demo/src/main/java/com/example/demo/result.txt
+++ b/backend/demo/src/main/java/com/example/demo/result.txt
@@ -110,6 +110,9 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.web.cors.CorsConfiguration; // 导入 CorsConfiguration
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource; // 导入 UrlBasedCorsConfigurationSource
+import org.springframework.web.filter.CorsFilter; // 导入 CorsFilter
import com.example.demo.security.JwtAuthenticationFilter;
@@ -139,6 +142,22 @@
}
/**
+ * 配置 CORS 过滤器。
+ * 允许前端应用 (http://localhost:5173) 访问后端 API。
+ */
+ @Bean
+ public CorsFilter corsFilter() {
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ CorsConfiguration config = new CorsConfiguration();
+ config.setAllowCredentials(true); // 允许发送 Cookie
+ config.addAllowedOrigin("http://localhost:5173"); // 允许的前端源
+ config.addAllowedHeader("*"); // 允许所有请求头
+ config.addAllowedMethod("*"); // 允许所有 HTTP 方法 (GET, POST, PUT, DELETE等)
+ source.registerCorsConfiguration("/**", config); // 对所有路径应用 CORS 配置
+ return new CorsFilter(source);
+ }
+
+ /**
* 核心安全策略配置。
* 配置内容包括:禁用 CSRF、禁用 Session 管理(使用 JWT 无状态)、
* 配置请求的授权规则,并将自定义的 JWT 认证过滤器添加到过滤器链中。
@@ -153,6 +172,15 @@
JwtAuthenticationFilter jwtFilter // 直接注入 JwtAuthenticationFilter Bean
) throws Exception {
http
+ // 添加 CORS 过滤器到 Spring Security 过滤器链的开头
+ .cors(cors -> cors.configurationSource(request -> {
+ CorsConfiguration config = new CorsConfiguration();
+ config.setAllowCredentials(true);
+ config.addAllowedOrigin("http://localhost:5173");
+ config.addAllowedHeader("*");
+ config.addAllowedMethod("*");
+ return config;
+ }))
// 禁用 CSRF (跨站请求伪造) 保护,因为 JWT 是无状态的,不需要 CSRF 保护
.csrf(AbstractHttpConfigurer::disable)
// 配置 Session 管理策略为无状态,不使用 Session
@@ -160,7 +188,7 @@
// 配置请求的授权规则
.authorizeHttpRequests(auth -> auth
// 允许对 /auth/** 路径下的所有请求进行匿名访问 (例如登录、注册接口)
- .requestMatchers("/auth/**").permitAll()
+ .requestMatchers("/api/auth/**").permitAll() // 确保这里的路径与您的 AuthController 匹配
// 其他所有请求都需要进行身份认证
.anyRequest().authenticated()
)
@@ -185,18 +213,14 @@
--- 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.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder; // 导入 AuthenticationManager
+import org.springframework.security.crypto.password.PasswordEncoder; // 导入 UsernamePasswordAuthenticationToken
+import org.springframework.validation.BindingResult; // 导入 Authentication
+import org.springframework.web.bind.annotation.PostMapping; // 导入 SecurityContextHolder
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -230,68 +254,175 @@
}
@PostMapping("/login")
- public ResponseEntity<?> login(
- @Valid @RequestBody LoginRequestDTO loginRequest,
- BindingResult bindingResult) {
+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));
+ if (bindingResult.hasErrors()) {
+ // … 参数校验错误的处理,这里不会走到
+ }
+
+ try {
+ System.out.println("login(): 开始调用 authenticationManager.authenticate");
+ Authentication authentication = authenticationManager.authenticate(
+ new UsernamePasswordAuthenticationToken(
+ loginRequest.getUsername(),
+ loginRequest.getPassword()
+ )
+ );
+ System.out.println("login(): authenticate 成功,principal = "
+ + authentication.getPrincipal());
+
+ // 设置到上下文
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ System.out.println("login(): SecurityContext 设置完成");
+
+ // 强转 principal 为 用户实体
+ User userDetails = (User) authentication.getPrincipal();
+ System.out.println("login(): 拿到 userDetails,username = " + userDetails.getUsername());
+
+ // 调用生成 JWT 的方法
+ System.out.println("login(): 调用 userService.generateLoginResponse");
+ LoginResponseDTO response = userService.generateLoginResponse(userDetails);
+ System.out.println("login(): generateLoginResponse 返回 token = " + response.getToken());
+
+ return ResponseEntity.ok(response);
+
+ } catch (Exception e) {
+ System.out.println("login(): 捕获到异常: " + e.getClass().getSimpleName() + " - " + e.getMessage());
+ e.printStackTrace();
+ throw new AuthException("用户名或密码错误", e);
+ }
+}
+
+}
+--- End of: ./controller/AuthController.java ---
+
+--- Content of: ./controller/TorrentController.java ---
+// src/main/java/com/example/demo/controller/TorrentController.java
+package com.example.demo.controller;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.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
+@RequestMapping("/api")
+public class TorrentController {
+
+ private final TorrentService torrentService;
+
+ @Value("${file.upload-dir}")
+ private String uploadDir;
+
+ public TorrentController(TorrentService torrentService) {
+ this.torrentService = torrentService;
+ }
+
+ /**
+ * 上传 .torrent 文件并处理其元信息。
+ *
+ * @param file 上传的 .torrent 文件
+ * @param title 用户指定的标题
+ * @return 处理结果
+ */
+ @PostMapping("/torrents/upload")
+ public ResponseEntity<TorrentInfoDTO> uploadTorrent(
+ @RequestParam("file") MultipartFile file,
+ @RequestParam("title") String title) {
+
+ 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 {
- // 使用 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);
-
+ TorrentInfoDTO result = torrentService.handleUpload(file, title);
+ return new ResponseEntity<>(result, HttpStatus.OK);
+ } catch (InvalidBEncodingException e) {
+ 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) {
- // 认证失败(用户名不存在或密码错误),抛出自定义认证异常
- // GlobalExceptionHandler 会捕获 AuthException 并返回 401
- throw new AuthException("用户名或密码错误", e);
+ TorrentInfoDTO errorDto = new TorrentInfoDTO();
+ errorDto.setMessage("文件上传过程中发生未知错误: " + e.getMessage());
+ e.printStackTrace();
+ return new ResponseEntity<>(errorDto, HttpStatus.INTERNAL_SERVER_ERROR);
}
+ }
- // 移除原有的手动查找用户和密码比对逻辑
- /*
- User user = userService.lambdaQuery()
- .eq(User::getUsername, loginRequest.getUsername())
- .one();
+ /**
+ * 下载已上传的 .torrent 文件。
+ *
+ * @param infoHash 该文件的 info hash,用于定位文件
+ * @param fileName 用户下载时看到的文件名
+ * @return ResponseEntity 包含下载资源
+ */
+ @GetMapping("/downloads/{infoHash}/{fileName}")
+ public ResponseEntity<Resource> downloadTorrent(
+ @PathVariable String infoHash,
+ @PathVariable String fileName) {
- if (user == null || !passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) {
- throw new AuthException("用户名或密码错误");
+ try {
+ Path fileStorageLocation = Paths.get(uploadDir).toAbsolutePath().normalize();
+ Path filePath = fileStorageLocation.resolve(infoHash + ".torrent").normalize();
+ Resource resource = new UrlResource(filePath.toUri());
+
+ 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) {
+ return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
-
- 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 ---
+
+--- End of: ./controller/TorrentController.java ---
--- Content of: ./DemoApplication.java ---
package com.example.demo;
@@ -414,6 +545,96 @@
--- End of: ./dto/LoginResponseDTO.java ---
+--- Content of: ./dto/TorrentInfoDTO.java ---
+// 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 + '\'' +
+ '}';
+ }
+}
+--- End of: ./dto/TorrentInfoDTO.java ---
+
--- Content of: ./entity/User.java ---
package com.example.demo.entity;
@@ -579,6 +800,127 @@
--- End of: ./entity/User.java ---
+--- Content of: ./entity/TorrentInfo.java ---
+// 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 +
+ '}';
+ }
+}
+--- End of: ./entity/TorrentInfo.java ---
+
--- Content of: ./exception/AuthException.java ---
package com.example.demo.exception;
@@ -666,15 +1008,31 @@
--- End of: ./mapper/UserMapper.java ---
+--- Content of: ./mapper/TorrentInfoMapper.java ---
+// 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() 等。
+ // 无需编写任何实现代码。
+}
+--- End of: ./mapper/TorrentInfoMapper.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.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
@@ -682,20 +1040,23 @@
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
+@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenUtil tokenUtil;
private final JWTProperties props;
+ private final UserDetailsService userDetailsService; // ✅ 注入接口
- public JwtAuthenticationFilter(JwtTokenUtil tokenUtil, JWTProperties props) {
+ public JwtAuthenticationFilter(JwtTokenUtil tokenUtil,
+ JWTProperties props,
+ UserDetailsService userDetailsService) {
this.tokenUtil = tokenUtil;
this.props = props;
+ this.userDetailsService = userDetailsService;
}
@Override
@@ -715,16 +1076,16 @@
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);
+ String username = tokenUtil.getUsername(token);
+
+ // ✅ 使用 UserServiceImpl 加载完整的用户对象(包括权限信息)
+ UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
- userId,
- null,
- List.of(new SimpleGrantedAuthority(role))
+ userDetails, // ✅ 设置完整的用户对象
+ null,
+ userDetails.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(auth);
@@ -875,26 +1236,15 @@
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;
@@ -902,12 +1252,12 @@
* 构造函数注入非BaseMapper的依赖
* ServiceImpl会自动注入BaseMapper
* @param passwordEncoder 密码编码器
- * // 移除 authenticationManager 参数
+ *
* @param jwtTokenUtil JWT工具类
*/
- // @Autowired // 当只有一个构造函数时,@Autowired 可选
+
public UserServiceImpl(PasswordEncoder passwordEncoder,
- // AuthenticationManager authenticationManager, // 移除此参数
+ // 移除此参数
JwtTokenUtil jwtTokenUtil) {
// 当继承ServiceImpl时,不需要在构造函数中注入并初始化BaseMapper
// this.userMapper = userMapper; // 移除这行
@@ -1008,22 +1358,6 @@
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 ---
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 520908c..d69361f 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
@@ -1,11 +1,11 @@
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.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
@@ -13,20 +13,23 @@
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
+@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenUtil tokenUtil;
private final JWTProperties props;
+ private final UserDetailsService userDetailsService; // ✅ 注入接口
- public JwtAuthenticationFilter(JwtTokenUtil tokenUtil, JWTProperties props) {
+ public JwtAuthenticationFilter(JwtTokenUtil tokenUtil,
+ JWTProperties props,
+ UserDetailsService userDetailsService) {
this.tokenUtil = tokenUtil;
this.props = props;
+ this.userDetailsService = userDetailsService;
}
@Override
@@ -46,16 +49,16 @@
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);
+ String username = tokenUtil.getUsername(token);
+
+ // ✅ 使用 UserServiceImpl 加载完整的用户对象(包括权限信息)
+ UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
- userId,
- null,
- List.of(new SimpleGrantedAuthority(role))
+ userDetails, // ✅ 设置完整的用户对象
+ null,
+ userDetails.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(auth);
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
index b863798..2be7aad 100644
--- a/backend/demo/src/main/java/com/example/demo/service/TorrentService.java
+++ b/backend/demo/src/main/java/com/example/demo/service/TorrentService.java
@@ -1,4 +1,3 @@
-// src/main/java/com/example/demo/service/TorrentService.java
package com.example.demo.service;
import java.io.IOException;
@@ -17,10 +16,35 @@
* 处理上传的 .torrent 文件,解析其内容,保存到数据库,并生成相关链接。
*
* @param file 上传的 MultipartFile 对象
+ * @param title 洪流的标题
* @return 包含解析信息、磁力链接和下载 URL 的 TorrentInfoDTO
* @throws IOException 如果文件读取或保存失败
* @throws InvalidBEncodingException 如果 .torrent 文件内容不符合 B 编码规范
* @throws IllegalArgumentException 如果文件为空或解析后的元数据不完整
*/
- TorrentInfoDTO handleUpload(MultipartFile file) throws IOException, InvalidBEncodingException, IllegalArgumentException;
+ TorrentInfoDTO handleUpload(MultipartFile file, String title) throws IOException, InvalidBEncodingException, IllegalArgumentException;
+
+ /**
+ * 根据 infoHash 启动与 Tracker 联动的下载任务(异步执行)。
+ *
+ * @param infoHash 种子的 infoHash(十六进制字符串)
+ * @throws Exception 启动任务失败时抛出
+ */
+void startDownload(String infoHash) throws Exception;
+
+/**
+ * 查询指定 infoHash 的当前下载进度,返回值范围 0.0 到 1.0。
+ *
+ * @param infoHash 种子的 infoHash
+ * @return 下载完成比例(0.0–1.0)
+ */
+float getDownloadProgress(String infoHash);
+
+/**
+ * 下载完成后,获取本地保存的文件路径;若未完成或未启动下载,则返回 null。
+ *
+ * @param infoHash 种子的 infoHash
+ * @return 已下载文件的绝对路径或 null
+ */
+String getDownloadedFilePath(String infoHash);
}
\ No newline at end of file
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
index eb00160..eedad56 100644
--- 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
@@ -2,9 +2,15 @@
package com.example.demo.service.impl;
import java.io.IOException;
+import java.net.InetAddress;
import java.nio.file.Files; // 导入 DTO
import java.nio.file.Path; // 导入实体类
import java.nio.file.Paths; // 导入 Mapper
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Value; // 导入服务接口
import org.springframework.stereotype.Service; // 导入工具类
@@ -18,6 +24,14 @@
import com.example.demo.service.TorrentService; // 用于文件操作
import com.example.demo.util.TorrentUtils; // 用于文件路径
import com.turn.ttorrent.bcodec.InvalidBEncodingException; // 用于文件路径
+import com.turn.ttorrent.client.CommunicationManager;
+import com.turn.ttorrent.client.PeerInformation;
+import com.turn.ttorrent.client.PieceInformation;
+import com.turn.ttorrent.client.SharedTorrent;
+import com.turn.ttorrent.client.TorrentListener;
+import com.turn.ttorrent.client.TorrentManager;
+
+import jakarta.annotation.PostConstruct;
/**
* TorrentService 接口的实现类,负责处理 .torrent 文件的上传、解析、持久化及链接生成。
@@ -27,8 +41,11 @@
private final TorrentInfoMapper torrentInfoMapper;
- @Value("${file.upload-dir}") // 从 application.properties 中注入文件上传目录
- private String uploadDir;
+ @Value("${file.upload-dir}")
+ private String uploadDir;
+
+ @Value("${file.download-dir}")
+ private String downloadDir;
// 通过构造函数注入 Mapper
public TorrentServiceImpl(TorrentInfoMapper torrentInfoMapper) {
@@ -37,7 +54,7 @@
@Override
@Transactional // 确保文件保存和数据库操作的原子性
- public TorrentInfoDTO handleUpload(MultipartFile file) throws IOException, InvalidBEncodingException, IllegalArgumentException {
+ public TorrentInfoDTO handleUpload(MultipartFile file,String title) throws IOException, InvalidBEncodingException, IllegalArgumentException {
if (file.isEmpty()) {
throw new IllegalArgumentException("上传文件不能为空。");
}
@@ -123,4 +140,82 @@
return dto;
}
+
+ private CommunicationManager commManager;
+ private final Map<String, TorrentManager> managers = new ConcurrentHashMap<>();
+ private final ExecutorService executor = Executors.newFixedThreadPool(4);
+
+ @PostConstruct
+ public void init() throws Exception {
+ // 1. 初始化 CommunicationManager
+ ExecutorService netPool = Executors.newFixedThreadPool(4);
+ ExecutorService ioPool = Executors.newFixedThreadPool(2);
+ this.commManager = new CommunicationManager(netPool, ioPool);
+ this.commManager.start(InetAddress.getLocalHost());
+ }
+
+ @Override
+ public void startDownload(String infoHash) throws Exception {
+ // 2.1 .torrent 文件路径 & 本地输出目录
+ String torrentPath = uploadDir + "/" + infoHash + ".torrent";
+ String outDir = downloadDir + "/" + infoHash;
+ TorrentManager tm = commManager.addTorrent(torrentPath, outDir);
+
+ // 2.2 注册进度监听
+ tm.addListener(new TorrentListener() {
+ @Override
+ public void pieceDownloaded(PieceInformation pi, PeerInformation peer) {
+ // no-op
+ }
+ @Override public void downloadComplete() {
+ System.out.println("[" + infoHash + "] 下载完成");
+ }
+ @Override public void validationComplete(int valid, int total) {}
+ @Override public void peerConnected(PeerInformation peer) {}
+ @Override public void peerDisconnected(PeerInformation peer) {}
+ @Override public void pieceReceived(PieceInformation pi, PeerInformation peer) {}
+ @Override public void downloadFailed(Throwable cause) {
+ cause.printStackTrace();
+ }
+ });
+
+ // 2.3 异步等待完成
+ executor.submit(() -> {
+ try {
+ tm.awaitDownloadComplete(Integer.MAX_VALUE, TimeUnit.SECONDS);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+
+ managers.put(infoHash, tm);
+ }
+
+ @Override
+public float getDownloadProgress(String infoHash) {
+ for (SharedTorrent t : commManager.getTorrents()) {
+ if (t.getHexInfoHash().equalsIgnoreCase(infoHash)) {
+ long downloaded = t.getDownloaded(); // 已下载字节数
+ long left = t.getLeft(); // 剩余字节数
+ long total = downloaded + left; // 总字节数
+ return total > 0
+ ? (float) downloaded / total
+ : 0f;
+ }
+ }
+ return 0f;
+}
+
+
+ @Override
+ public String getDownloadedFilePath(String infoHash) {
+ for (SharedTorrent t : commManager.getTorrents()) {
+ if (t.getHexInfoHash().equalsIgnoreCase(infoHash) && t.isComplete()) {
+ // 拿到第一个文件的相对路径,再拼上下载目录
+ String rel = t.getFiles().get(0).getRelativePathAsString();
+ return downloadDir + "/" + infoHash + "/" + rel;
+ }
+ }
+ return null;
+ }
}
\ 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 1fc434c..1b72d0f 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
@@ -19,26 +19,15 @@
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;
@@ -46,12 +35,12 @@
* 构造函数注入非BaseMapper的依赖
* ServiceImpl会自动注入BaseMapper
* @param passwordEncoder 密码编码器
- * // 移除 authenticationManager 参数
+ *
* @param jwtTokenUtil JWT工具类
*/
- // @Autowired // 当只有一个构造函数时,@Autowired 可选
+
public UserServiceImpl(PasswordEncoder passwordEncoder,
- // AuthenticationManager authenticationManager, // 移除此参数
+ // 移除此参数
JwtTokenUtil jwtTokenUtil) {
// 当继承ServiceImpl时,不需要在构造函数中注入并初始化BaseMapper
// this.userMapper = userMapper; // 移除这行
@@ -152,20 +141,4 @@
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
index 3be143b..7b30510 100644
--- a/backend/demo/src/main/java/com/example/demo/sh.py
+++ b/backend/demo/src/main/java/com/example/demo/sh.py
@@ -53,13 +53,17 @@
'./config/JWTProperties.java',
'./config/SecurityConfig.java',
'./controller/AuthController.java',
+ './controller/TorrentController.java',
'./DemoApplication.java',
'./dto/LoginRequestDTO.java',
'./dto/LoginResponseDTO.java',
+ './dto/TorrentInfoDTO.java',
'./entity/User.java',
+ './entity/TorrentInfo.java',
'./exception/AuthException.java',
'./exception/GlobalExceptionHandler.java',
'./mapper/UserMapper.java',
+ './mapper/TorrentInfoMapper.java',
'./security/JwtAuthenticationFilter.java',
'./security/JwtTokenUtil.java',
'./service/UserService.java',
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
index cf06b61..3da4c86 100644
--- 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
@@ -1,5 +1,12 @@
-{"properties": [{
- "name": "file.upload-dir",
- "type": "java.lang.String",
- "description": "Torrent dir for uploading'"
-}]}
\ No newline at end of file
+{"properties": [
+ {
+ "name": "file.upload-dir",
+ "type": "java.lang.String",
+ "description": "Torrent dir for uploading'"
+ },
+ {
+ "name": "app.torrent.download-dir",
+ "type": "java.lang.String",
+ "description": "A description for 'app.torrent.download-dir'"
+ }
+]}
\ 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 9bbb45b..720d673 100644
--- a/backend/demo/src/main/resources/application.properties
+++ b/backend/demo/src/main/resources/application.properties
@@ -1,15 +1,21 @@
# ========== 数据源 ==========
-spring.datasource.url=jdbc:mysql://mysql:3306/mydatabase?serverTimezone=Asia/Shanghai&createDatabaseIfNotExist=TRUE&useSSL=FALSE&allowPublicKeyRetrieval=TRUE
+spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=myuser
spring.datasource.password=secret
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# ========== JWT 配置 ==========
-jwt.secret=YourJWTSecretKeyHere1234567890
+jwt.secret=YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=
jwt.expirationMs=3600000
# ========== 日志输出(可选) ==========
logging.level.org.springframework.security=DEBUG
+logging.level.root=INFO
+logging.level.org.springframework.web=DEBUG
+logging.level.org.springframework.web.servlet=DEBUG
+logging.level.com.example=DEBUG
file.upload-dir=./torrents
-
+app.torrent.download-dir=downloads
+spring.docker.compose.enabled=false
+spring.web.resources.static-locations=file:downloads/
\ No newline at end of file
diff --git a/backend/demo/src/main/resources/lib b/backend/demo/src/main/resources/lib
new file mode 160000
index 0000000..ed28512
--- /dev/null
+++ b/backend/demo/src/main/resources/lib
@@ -0,0 +1 @@
+Subproject commit ed28512530673c7363ae21f6fa09543955963de8
diff --git a/backend/demo/src/test/java/com/example/demo/controller/TorrentControllerTest.java b/backend/demo/src/test/java/com/example/demo/controller/TorrentControllerTest.java
index e0f2c8d..2ff389d 100644
--- a/backend/demo/src/test/java/com/example/demo/controller/TorrentControllerTest.java
+++ b/backend/demo/src/test/java/com/example/demo/controller/TorrentControllerTest.java
@@ -74,7 +74,7 @@
// 当 torrentService.handleUpload 被调用时,返回预期的 DTO
// When torrentService.handleUpload is called, return the expected DTO
- when(torrentService.handleUpload(any(MultipartFile.class))).thenReturn(expectedDto);
+ when(torrentService.handleUpload(any(MultipartFile.class), null)).thenReturn(expectedDto);
// 2. 执行请求并验证结果 (Execute the request and verify the results)
mockMvc.perform(multipart("/api/torrents") // 发送 POST 请求到 /api/torrents (Send POST request to /api/torrents)
@@ -141,7 +141,7 @@
// 配置 torrentService.handleUpload 在被调用时抛出 InvalidBEncodingException
// Configure torrentService.handleUpload to throw InvalidBEncodingException when called
- when(torrentService.handleUpload(any(MultipartFile.class)))
+ when(torrentService.handleUpload(any(MultipartFile.class), null))
.thenThrow(new InvalidBEncodingException("Mocked BEncoding error"));
mockMvc.perform(multipart("/api/torrents")
@@ -166,7 +166,7 @@
// 配置 torrentService.handleUpload 在被调用时抛出 IOException
// Configure torrentService.handleUpload to throw IOException when called
- when(torrentService.handleUpload(any(MultipartFile.class)))
+ when(torrentService.handleUpload(any(MultipartFile.class), null))
.thenThrow(new IOException("Mocked IO error"));
mockMvc.perform(multipart("/api/torrents")
@@ -191,7 +191,7 @@
// 配置 torrentService.handleUpload 在被调用时抛出 RuntimeException
// Configure torrentService.handleUpload to throw RuntimeException when called
- when(torrentService.handleUpload(any(MultipartFile.class)))
+ when(torrentService.handleUpload(any(MultipartFile.class), null))
.thenThrow(new RuntimeException("Mocked generic error"));
mockMvc.perform(multipart("/api/torrents")
@@ -216,7 +216,7 @@
// 配置 torrentService.handleUpload 在被调用时抛出 IllegalArgumentException
// Configure torrentService.handleUpload to throw IllegalArgumentException when called
- when(torrentService.handleUpload(any(MultipartFile.class)))
+ when(torrentService.handleUpload(any(MultipartFile.class), null))
.thenThrow(new IllegalArgumentException("Mocked illegal argument from service"));
mockMvc.perform(multipart("/api/torrents")
diff --git a/backend/demo/src/test/java/com/example/demo/service/impl/TorrentServiceImplTest.java b/backend/demo/src/test/java/com/example/demo/service/impl/TorrentServiceImplTest.java
index 69eb5ba..a9da766 100644
--- a/backend/demo/src/test/java/com/example/demo/service/impl/TorrentServiceImplTest.java
+++ b/backend/demo/src/test/java/com/example/demo/service/impl/TorrentServiceImplTest.java
@@ -138,7 +138,7 @@
when(torrentInfoMapper.insert(any(TorrentInfo.class))).thenReturn(1);
// 4. Call the service method under test
- TorrentInfoDTO result = torrentService.handleUpload(mockFile);
+ TorrentInfoDTO result = torrentService.handleUpload(mockFile, testFileName);
// 5. Assertions: Verify the returned DTO and side effects
@@ -218,7 +218,7 @@
when(torrentInfoMapper.selectOne(any(QueryWrapper.class))).thenReturn(existingTorrentInDb);
// 5. Call the service method
- TorrentInfoDTO result = torrentService.handleUpload(mockFile);
+ TorrentInfoDTO result = torrentService.handleUpload(mockFile, existingFileName);
// 6. Assertions
assertNotNull(result, "Returned DTO should not be null");
@@ -262,7 +262,7 @@
// Assert that calling handleUpload with an empty file throws IllegalArgumentException.
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
- torrentService.handleUpload(emptyFile);
+ torrentService.handleUpload(emptyFile, uploadDir);
}, "Should throw IllegalArgumentException for empty file");
// Verify the error message.
@@ -296,7 +296,7 @@
// Assert that calling handleUpload throws IllegalArgumentException.
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
- torrentService.handleUpload(mockFile);
+ torrentService.handleUpload(mockFile, uploadDir);
}, "Should throw IllegalArgumentException for invalid B-encoding");
// Verify the error message.
@@ -331,7 +331,7 @@
// Assert that calling handleUpload throws IOException.
IOException thrown = assertThrows(IOException.class, () -> {
- torrentService.handleUpload(mockFile);
+ torrentService.handleUpload(mockFile, uploadDir);
}, "Should throw IOException for parsing IO error");
// Verify the error message.
@@ -388,7 +388,7 @@
// Now, call the service method
IOException thrown = assertThrows(IOException.class, () -> {
- torrentService.handleUpload(mockFile);
+ torrentService.handleUpload(mockFile, testFileName);
}, "Should throw IOException when file saving fails");
assertTrue(thrown.getMessage().contains("无法保存 .torrent 文件到磁盘"), "Error message should indicate file save failure");
@@ -440,7 +440,7 @@
// Assert that calling handleUpload throws RuntimeException.
RuntimeException thrown = assertThrows(RuntimeException.class, () -> {
- torrentService.handleUpload(mockFile);
+ torrentService.handleUpload(mockFile, testFileName);
}, "Should throw RuntimeException when DB insert fails");
// Verify the error message.