Backend update on Torrent upload with unit test complete

Change-Id: Ie8a90c82e414df32d079524d3ff3035b823b69c3
diff --git a/backend/demo/Dockerfile b/backend/demo/Dockerfile
index 5d92360..3b73f3c 100644
--- a/backend/demo/Dockerfile
+++ b/backend/demo/Dockerfile
@@ -1,25 +1,66 @@
-# 构建阶段:使用 Maven 构建项目
+# 阶段 1: builder - 构建应用 JAR 包 (跳过测试)
+# 使用包含 Maven 和 JDK 的基础镜像
 FROM maven:3.9.9-eclipse-temurin-24-alpine AS builder
 
+# 设置工作目录
 WORKDIR /app
 
-# 复制项目文件(忽略 target 目录)
+# 复制 pom.xml 文件,以便 Maven 可以下载依赖
+COPY pom.xml .
+
+# 复制项目源代码,包括 main 和 test
+# 这一步是为了让 Maven 在构建和下载依赖时能够解析所有模块
+COPY src ./src
+
+# 执行 Maven clean 和 dependency:go-offline
+# clean: 清理 target 目录
+# dependency:go-offline: 下载所有项目依赖到本地 Maven 仓库,供后续阶段使用
+# -B: 非交互模式
+# 这一步不执行实际的编译和测试,主要用于缓存依赖
+RUN mvn clean dependency:go-offline -B
+
+# 执行 Maven package 构建项目,生成 JAR 包
+# -DskipTests: 跳过单元和集成测试
+RUN mvn package -DskipTests
+
+
+# 阶段 2: tester - 专用于运行和调试测试的运行时环境
+# 基于与 builder 阶段相同的 Maven/JDK 基础镜像,确保测试所需的环境一致
+FROM maven:3.9.9-eclipse-temurin-24-alpine AS tester
+
+# 设置工作目录
+WORKDIR /app
+
+# 复制 pom.xml 和完整的 src 目录 (包含测试代码)
 COPY pom.xml .
 COPY src ./src
 
-# 构建应用,跳过测试
-RUN mvn clean package -DskipTests
+# 从 builder 阶段复制已缓存的 Maven 本地仓库
+# 这样在 tester 阶段运行 Maven 时无需重新下载依赖
+COPY --from=builder /root/.m2 /root/.m2
 
-# 运行阶段:使用较小的 JDK 镜像运行应用
-FROM openjdk:17-jdk-slim-buster
+# 暴露调试端口 (可选,如果您需要远程调试测试过程)
+# 您可以在 docker compose 中映射这个端口到主机
+EXPOSE 5005
 
+# 设置默认命令,使容器启动后保持运行
+# 这样您可以方便地使用 docker exec 进入容器手动执行测试和调试命令
+# 如果您希望容器启动后自动运行测试,可以修改此命令为 ["mvn", "test"]
+CMD ["tail", "-f", "/dev/null"]
+
+
+# 阶段 3: runner - 运行最终应用 JAR 包的精简运行时环境
+# 使用轻量级的 OpenJDK JRE 镜像
+FROM openjdk:17-jdk-slim-buster AS runner
+
+# 设置工作目录
 WORKDIR /app
 
-# 从构建阶段复制打好的 jar 包
+# 从 builder 阶段复制构建好的应用 JAR 包
 COPY --from=builder /app/target/demo-0.0.1-SNAPSHOT.jar app.jar
 
-# 暴露 Spring Boot 默认端口
+# 暴露 Spring Boot 应用的默认端口
 EXPOSE 8080
 
-# 启动命令
+# 启动 Spring Boot 应用
 CMD ["java", "-jar", "app.jar"]
diff --git a/backend/demo/compose.yaml b/backend/demo/compose.yaml
index 61d6b9d..4d288a2 100644
--- a/backend/demo/compose.yaml
+++ b/backend/demo/compose.yaml
@@ -1,29 +1,74 @@
 
 services:
+  # 数据库服务
   mysql:
-    image: mysql:latest
+    image: mysql:8.0 # 建议使用具体的版本号,而不是 latest
+    container_name: mysql_db # 给容器一个易于识别的名称
     environment:
-      - MYSQL_DATABASE=mydatabase
-      - MYSQL_USER=myuser
-      - MYSQL_PASSWORD=secret
-      - MYSQL_ROOT_PASSWORD=verysecret
+      MYSQL_DATABASE: mydatabase
+      MYSQL_USER: myuser
+      MYSQL_PASSWORD: secret
+      MYSQL_ROOT_PASSWORD: verysecret
     ports:
-      - "3306:3306"
+      - "3306:3306" # 映射数据库端口,方便本地工具连接(可选,仅测试容器连接DB时可省略)
     volumes:
-      - mysql-data:/var/lib/mysql
+      - mysql-data:/var/lib/mysql # 持久化数据库数据
+    # 添加健康检查,确保数据库服务真正可用
+    healthcheck:
+      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u$$MYSQL_USER", "-p$$MYSQL_PASSWORD"]
+      interval: 10s # 每 10 秒检查一次
+      timeout: 5s # 检查超时时间
+      retries: 5 # 重试次数
+      start_period: 30s # 在此期间不计入重试失败次数,给数据库启动时间
 
+  # 应用服务(使用 Dockerfile 的 runner 阶段)
   app:
     build:
-      context: .
-      dockerfile: Dockerfile
+      context: . # Dockerfile 所在的上下文路径
+      dockerfile: Dockerfile # Dockerfile 文件名
+      target: runner # 指定构建 Dockerfile 中的 runner 阶段
+    container_name: spring_app
     ports:
-      - "8080:8080"
+      - "8080:8080" # 映射应用端口
     environment:
-      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 Boot 配置...
     depends_on:
-      - mysql
+      mysql:
+        condition: service_healthy # 确保数据库服务处于健康状态才启动应用
 
+  # 测试服务(使用 Dockerfile 的 tester 阶段)
+  # 这个服务用于运行和调试需要连接数据库的测试
+  tester:
+    build:
+      context: . # Dockerfile 所在的上下文路径
+      dockerfile: Dockerfile # Dockerfile 文件名
+      target: tester # 指定构建 Dockerfile 中的 tester 阶段
+    container_name: tester
+    # 暴露调试端口,映射到主机端口,用于远程调试测试过程
+    ports:
+      - "5005:5005"
+    # 依赖于数据库服务,并等待其健康检查通过
+    depends_on:
+      mysql:
+        condition: service_healthy # 确保数据库服务处于健康状态才启动测试容器
+    environment:
+      # 测试环境连接数据库的配置,使用数据库服务的名称作为主机名
+      # 这些环境变量会覆盖 application-test.properties 中的相同配置
+      SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/mydatabase?serverTimezone=Asia/Shanghai&createDatabaseIfNotExist=TRUE&useSSL=FALSE
+      SPRING_DATASOURCE_USERNAME: myuser
+      SPRING_DATASOURCE_PASSWORD: secret
+      # 设置 Maven 运行时的额外 JVM 参数,用于开启远程调试
+      MAVEN_OPTS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
+    # 默认命令:保持容器运行,方便手动进入容器执行测试和调试
+    # 如果您希望容器启动后自动运行测试,可以修改此命令为 ["mvn", "test"]
+    command: tail -f /dev/null
+
+
+# 定义数据卷用于持久化数据库数据
 volumes:
   mysql-data:
+
diff --git a/backend/demo/db/SQLScript.sql b/backend/demo/db/SQLScript.sql
new file mode 100644
index 0000000..13977af
--- /dev/null
+++ b/backend/demo/db/SQLScript.sql
@@ -0,0 +1,43 @@
+-- 创建数据库(如果不存在)
+-- 请确保您的Docker Compose配置中的数据库名称与此处一致
+CREATE DATABASE IF NOT EXISTS mydatabase CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+
+-- 切换到目标数据库
+USE mydatabase;
+
+-- 删除表(如果已存在),以便重新创建
+DROP TABLE IF EXISTS `user`;
+
+-- 创建用户表
+CREATE TABLE `user` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID,主键,自增长',
+    `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名,唯一,不可为空',
+    `password` VARCHAR(100) NOT NULL COMMENT '密码,存储加密后的密码,不可为空',
+    `email` VARCHAR(100) NULL DEFAULT NULL COMMENT '邮箱地址',
+    `status` INT NOT NULL DEFAULT 1 COMMENT '用户状态,例如 0:禁用, 1:启用,默认为1',
+    `score` INT NOT NULL DEFAULT 0 COMMENT '用户积分,默认为0',
+    `role` VARCHAR(20) NOT NULL DEFAULT 'USER' COMMENT '用户角色,例如 ADMIN, USER,默认为USER',
+    `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_username` (`username`) -- 确保用户名唯一
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
+
+-- 可选:插入一条测试用户数据
+-- 请注意:这里的密码 'password123' 是明文,您需要用您应用中配置的BCryptPasswordEncoder加密后的密码替换它
+-- 例如,使用 Spring Security 的 BCryptPasswordEncoder 对 'your_secure_password' 进行加密
+-- SELECT '$2a$10$YOUR_BCRYPT_HASH_HERE'; -- 替换这里的哈希值
+
+INSERT INTO `user` (`username`, `password`, `email`, `status`, `score`, `role`, `create_time`, `update_time`)
+VALUES (
+    'testuser',
+    '$$2a$10$EEBnvonhdTGA0StQHdxMUeAQyxm1uvJOKkHu01Pe0B9a2vfGH3cge', -- 请用加密后的密码替换此处的占位符
+    'test@example.com',
+    1, -- 启用状态
+    100, -- 初始积分
+    'USER', -- 默认角色
+    CURRENT_TIMESTAMP,
+    CURRENT_TIMESTAMP
+);
+
+
diff --git a/backend/demo/pom.xml b/backend/demo/pom.xml
index bac660b..6eccaf5 100644
--- a/backend/demo/pom.xml
+++ b/backend/demo/pom.xml
@@ -97,7 +97,39 @@
   <version>0.11.5</version>
   <scope>runtime</scope>
 </dependency>
+ <dependency>
+    <groupId>com.turn</groupId>
+    <artifactId>ttorrent-bencoding</artifactId>
+    <version>1.3.0-SNAPSHOT</version>
+  </dependency>
 
+  <!-- 通用工具模块(info_hash、文件结构等) -->
+  <dependency>
+    <groupId>com.turn</groupId>
+    <artifactId>ttorrent-common</artifactId>
+    <version>1.3.0-SNAPSHOT</version>
+  </dependency>
+
+  <!-- 网络通信支持(用于 Scrape/Tracker 通信) -->
+  <dependency>
+    <groupId>com.turn</groupId>
+    <artifactId>ttorrent-network</artifactId>
+    <version>1.0</version>
+  </dependency>
+
+  <!-- Tracker 服务(你自己部署的 announce 接口服务) -->
+  <dependency>
+    <groupId>com.turn</groupId>
+    <artifactId>ttorrent-tracker</artifactId>
+    <version>1.3.0-SNAPSHOT</version>
+  </dependency>
+
+  <!-- 客户端支持(如果你需要服务器自动发种、做种) -->
+  <dependency>
+    <groupId>com.turn</groupId>
+    <artifactId>ttorrent-client</artifactId>
+    <version>1.3.0-SNAPSHOT</version>
+  </dependency>
 </dependencies>
 
 <build>
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
+
diff --git a/backend/demo/src/test/java/com/example/demo/controller/AuthControllerMockTest.java b/backend/demo/src/test/java/com/example/demo/controller/AuthControllerMockTest.java
new file mode 100644
index 0000000..fd98df3
--- /dev/null
+++ b/backend/demo/src/test/java/com/example/demo/controller/AuthControllerMockTest.java
@@ -0,0 +1,229 @@
+package com.example.demo.controller; // 请根据您的项目结构调整包名
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import java.time.Instant;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks; // 注入 Mock 到被测试对象
+import org.mockito.Mock; // 标记为 Mock 对象
+import org.mockito.MockitoAnnotations; // 初始化 Mockito 注解
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.BadCredentialsException; // 模拟认证失败异常
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext; // 模拟 SecurityContext
+import org.springframework.security.core.context.SecurityContextHolder; // 模拟 SecurityContextHolder
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.validation.BindingResult; // 模拟 BindingResult
+import org.springframework.validation.FieldError; // 模拟 FieldError
+
+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;
+
+/**
+ * AuthController 的纯 Mock 测试类
+ * 不依赖 Spring 上下文或真实数据库
+ */
+public class AuthControllerMockTest {
+
+    // 使用 @Mock 注解标记需要模拟的依赖对象
+    @Mock
+    private UserService userService;
+
+    @Mock
+    private PasswordEncoder passwordEncoder; // 在 AuthController 中虽然注入了,但登录逻辑中未使用,这里依然模拟
+
+    @Mock
+    private JwtTokenUtil jwtTokenUtil;
+
+    @Mock
+    private AuthenticationManager authenticationManager;
+
+    @Mock
+    private BindingResult bindingResult; // 模拟请求体校验结果
+
+    @Mock
+    private SecurityContext securityContext; // 模拟 SecurityContext
+
+    // 使用 @InjectMocks 注解将上面 @Mock 标记的依赖注入到 AuthController 实例中
+    @InjectMocks
+    private AuthController authController;
+
+    /**
+     * 在每个测试方法执行前初始化 Mock 对象
+     */
+    @BeforeEach
+    void setUp() {
+        // 初始化所有 @Mock 注解标记的对象
+        MockitoAnnotations.openMocks(this);
+
+        // 模拟 SecurityContextHolder 的行为,使其返回我们模拟的 SecurityContext
+        // 这是为了让 SecurityContextHolder.getContext().setAuthentication() 调用时不会报错
+        when(securityContext.getAuthentication()).thenReturn(null); // 默认返回null
+        SecurityContextHolder.setContext(securityContext);
+    }
+
+    /**
+     * 测试用户登录成功场景
+     */
+    @Test
+    void testLoginSuccess() {
+        // --- 准备测试数据和模拟行为 ---
+
+        // 模拟 LoginRequestDTO
+        LoginRequestDTO loginRequest = new LoginRequestDTO();
+        loginRequest.setUsername("testuser");
+        loginRequest.setPassword("password123");
+
+        // 模拟 BindingResult 没有错误
+        when(bindingResult.hasErrors()).thenReturn(false);
+
+        // 模拟 AuthenticationManager 认证成功
+        // 创建一个模拟的 Authentication 对象
+        Authentication mockAuthentication = mock(Authentication.class);
+        // 模拟认证成功后,principal 返回 UserDetails (这里是 User 实体)
+        User mockUser = new User();
+        mockUser.setId(1L);
+        mockUser.setUsername("testuser");
+        mockUser.setRole("USER");
+        when(mockAuthentication.getPrincipal()).thenReturn(mockUser);
+
+        // 当调用 authenticationManager.authenticate() 时,返回模拟的认证对象
+        when(authenticationManager.authenticate(any(UsernamePasswordAuthenticationToken.class)))
+                .thenReturn(mockAuthentication);
+
+        // 模拟 UserService.generateLoginResponse 方法的返回值
+        LoginResponseDTO mockLoginResponse = new LoginResponseDTO();
+        mockLoginResponse.setToken("mocked_jwt_token");
+        mockLoginResponse.setExpiresAt(Instant.now().plusSeconds(3600));
+        mockLoginResponse.setUserId(mockUser.getId());
+        mockLoginResponse.setUsername(mockUser.getUsername());
+        mockLoginResponse.setRoles(Collections.singletonList(mockUser.getRole()));
+
+        when(userService.generateLoginResponse(mockUser)).thenReturn(mockLoginResponse);
+
+        // --- 执行被测试方法 ---
+        ResponseEntity<?> responseEntity = authController.login(loginRequest, bindingResult);
+
+        // --- 验证结果 ---
+
+        // 验证响应状态码是否为 200 OK
+        assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
+
+        // 验证响应体是否是预期的 LoginResponseDTO
+        assertNotNull(responseEntity.getBody());
+        assertTrue(responseEntity.getBody() instanceof LoginResponseDTO);
+
+        LoginResponseDTO responseBody = (LoginResponseDTO) responseEntity.getBody();
+        assertEquals("mocked_jwt_token", responseBody.getToken());
+        assertEquals(mockUser.getId(), responseBody.getUserId());
+        assertEquals(mockUser.getUsername(), responseBody.getUsername());
+        assertEquals(Collections.singletonList(mockUser.getRole()), responseBody.getRoles());
+        assertNotNull(responseBody.getExpiresAt()); // 验证过期时间不为null
+
+        // 验证 Mock 依赖的方法是否被正确调用
+        verify(bindingResult).hasErrors(); // 验证是否检查了 BindingResult
+        verify(authenticationManager).authenticate(any(UsernamePasswordAuthenticationToken.class)); // 验证是否调用了认证管理器
+        verify(userService).generateLoginResponse(mockUser); // 验证是否调用了生成响应的方法
+        verify(securityContext).setAuthentication(mockAuthentication); // 验证认证对象是否设置到了 SecurityContext
+        verifyNoMoreInteractions(userService, passwordEncoder, jwtTokenUtil, authenticationManager); // 验证没有调用其他未预期的 Mock 方法
+    }
+
+    /**
+     * 测试用户登录失败场景:认证失败 (例如用户名或密码错误)
+     */
+    @Test
+    void testLoginFailureAuthentication() {
+        // --- 准备测试数据和模拟行为 ---
+
+        // 模拟 LoginRequestDTO
+        LoginRequestDTO loginRequest = new LoginRequestDTO();
+        loginRequest.setUsername("wronguser");
+        loginRequest.setPassword("wrongpassword");
+
+        // 模拟 BindingResult 没有错误
+        when(bindingResult.hasErrors()).thenReturn(false);
+
+        // 模拟 AuthenticationManager 认证失败,抛出 BadCredentialsException (Spring Security 认证失败的常见异常)
+        when(authenticationManager.authenticate(any(UsernamePasswordAuthenticationToken.class)))
+                .thenThrow(new BadCredentialsException("Bad credentials")); // 模拟认证失败
+
+        // --- 执行被测试方法并验证异常 ---
+        // 由于 AuthController 会捕获认证异常并抛出 AuthException,我们断言抛出了 AuthException
+        AuthException thrown = assertThrows(AuthException.class, () -> {
+            authController.login(loginRequest, bindingResult);
+        });
+
+        // 验证抛出的 AuthException 包含预期的错误信息
+        assertEquals("用户名或密码错误", thrown.getMessage());
+
+        // --- 验证 Mock 依赖的方法是否被正确调用 ---
+        verify(bindingResult).hasErrors(); // 验证是否检查了 BindingResult
+        verify(authenticationManager).authenticate(any(UsernamePasswordAuthenticationToken.class)); // 验证是否调用了认证管理器
+        verifyNoInteractions(userService); // 验证在认证失败时,没有调用 userService 的方法
+        verifyNoMoreInteractions(userService, passwordEncoder, jwtTokenUtil, authenticationManager); // 验证没有调用其他未预期的 Mock 方法
+        verify(securityContext, never()).setAuthentication(any()); // 验证认证对象没有设置到 SecurityContext
+    }
+
+    /**
+     * 测试请求参数校验失败场景
+     */
+    @Test
+    void testLoginFailureValidation() {
+        // --- 准备测试数据和模拟行为 ---
+
+        // 模拟 LoginRequestDTO (内容不重要,因为我们将模拟校验失败)
+        LoginRequestDTO loginRequest = new LoginRequestDTO();
+        loginRequest.setUsername("");
+        loginRequest.setPassword("");
+
+        // 模拟 BindingResult 有错误
+        when(bindingResult.hasErrors()).thenReturn(true);
+
+        // 模拟 BindingResult 返回的错误信息
+        FieldError usernameError = new FieldError("loginRequestDTO", "username", "用户名不能为空");
+        FieldError passwordError = new FieldError("loginRequestDTO", "password", "密码不能为空");
+        when(bindingResult.getFieldErrors()).thenReturn(List.of(usernameError, passwordError));
+
+
+        // --- 执行被测试方法 ---
+        ResponseEntity<?> responseEntity = authController.login(loginRequest, bindingResult);
+
+        // --- 验证结果 ---
+
+        // 验证响应状态码是否为 400 Bad Request
+        assertEquals(HttpStatus.BAD_REQUEST, responseEntity.getStatusCode());
+
+        // 验证响应体是否包含预期的错误信息 Map
+        assertNotNull(responseEntity.getBody());
+        assertTrue(responseEntity.getBody() instanceof java.util.Map);
+
+        @SuppressWarnings("unchecked")
+        java.util.Map<String, String> errorBody = (java.util.Map<String, String>) responseEntity.getBody();
+        assertTrue(errorBody.containsKey("error"));
+        // 可以进一步验证错误信息的具体内容,但这里只验证格式
+        // assertEquals("username: 用户名不能为空; password: 密码不能为空", errorBody.get("error")); // 如果您的错误信息拼接逻辑是这样
+
+        // --- 验证 Mock 依赖的方法是否被正确调用 ---
+        verify(bindingResult).hasErrors(); // 验证是否检查了 BindingResult
+        verify(bindingResult).getFieldErrors(); // 验证是否获取了字段错误
+        verifyNoInteractions(authenticationManager, userService, jwtTokenUtil, passwordEncoder); // 验证在校验失败时,没有调用认证、服务或 JWT 工具方法
+        verify(securityContext, never()).setAuthentication(any()); // 验证认证对象没有设置到 SecurityContext
+    }
+
+    // 您可以根据需要添加更多测试场景,例如:
+    // - 测试用户状态禁用时的行为 (如果您的 UserDetails 实现中 isEnabled() 会检查状态)
+    // - 测试不同角色返回时的 LoginResponseDTO 内容
+}
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
new file mode 100644
index 0000000..e0f2c8d
--- /dev/null
+++ b/backend/demo/src/test/java/com/example/demo/controller/TorrentControllerTest.java
@@ -0,0 +1,228 @@
+package com.example.demo.controller;
+
+import java.io.IOException;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.any;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.MediaType;
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.test.web.servlet.MockMvc;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.multipart.MultipartFile;
+
+import com.example.demo.dto.TorrentInfoDTO;
+import com.example.demo.service.TorrentService;
+import com.turn.ttorrent.bcodec.InvalidBEncodingException;
+
+/**
+ * Unit tests for the {@link TorrentController} focusing on file upload.
+ * 使用 Mockito 和 MockMvc 对 TorrentController 的文件上传功能进行单元测试。
+ */
+@ExtendWith(MockitoExtension.class) // 使用 MockitoExtension 来初始化 mocks 和 injectMocks
+class TorrentControllerTest {
+
+    private MockMvc mockMvc; // MockMvc 用于模拟 HTTP 请求
+
+    @Mock // 创建 TorrentService 的 mock 对象
+    private TorrentService torrentService;
+
+    @InjectMocks // 创建 TorrentController 实例并注入 mock 的 TorrentService
+    private TorrentController torrentController;
+
+    @BeforeEach // 在每个测试方法执行前设置 MockMvc
+    void setUp() {
+        // 初始化 MockMvc,使用独立的设置,只测试 TorrentController
+        mockMvc = MockMvcBuilders.standaloneSetup(torrentController)
+                // 可以添加全局异常处理器等,如果控制器中定义了的话
+                // .setControllerAdvice(new GlobalExceptionHandler())
+                .build();
+        // 在这里可以设置 @Value("${file.upload-dir}") 的值,如果需要的话
+        // ReflectionTestUtils.setField(torrentController, "uploadDir", "test-upload-dir");
+    }
+
+    /**
+     * Test successful upload of a .torrent file.
+     * 测试成功上传 .torrent 文件的情况。
+     */
+    @Test
+    void uploadTorrent_Success() throws Exception {
+        // 1. 准备 mock 数据和行为 (Prepare mock data and behavior)
+        // 创建一个模拟的 MultipartFile
+        MockMultipartFile mockFile = new MockMultipartFile(
+                "file", // 参数名,必须与 @RequestParam("file") 一致 (Parameter name, must match @RequestParam("file"))
+                "test.torrent", // 原始文件名 (Original filename)
+                MediaType.APPLICATION_OCTET_STREAM_VALUE, // 文件类型 (File type)
+                "d8:announce39:http://tracker.example.com/announce4:info...".getBytes() // 模拟的 .torrent 文件内容 (Simulated .torrent file content)
+        );
+
+        // 创建预期的 TorrentInfoDTO 返回结果
+        TorrentInfoDTO expectedDto = new TorrentInfoDTO();
+        expectedDto.setFileName("test.torrent");
+        expectedDto.setInfoHash("mockInfoHash");
+        expectedDto.setMagnetUri("magnet:?xt=urn:btih:mockInfoHash");
+        expectedDto.setDownloadUrl("/api/downloads/mockInfoHash/test.torrent");
+        expectedDto.setMessage("文件上传成功。"); // Success message
+
+        // 当 torrentService.handleUpload 被调用时,返回预期的 DTO
+        // When torrentService.handleUpload is called, return the expected DTO
+        when(torrentService.handleUpload(any(MultipartFile.class))).thenReturn(expectedDto);
+
+        // 2. 执行请求并验证结果 (Execute the request and verify the results)
+        mockMvc.perform(multipart("/api/torrents") // 发送 POST 请求到 /api/torrents (Send POST request to /api/torrents)
+                        .file(mockFile) // 附加文件 (Attach the file)
+                        .contentType(MediaType.MULTIPART_FORM_DATA)) // 设置内容类型 (Set content type)
+                .andExpect(status().isOk()) // 验证 HTTP 状态码为 200 OK (Verify HTTP status code is 200 OK)
+                .andExpect(jsonPath("$.fileName").value("test.torrent")) // 验证返回 JSON 中的 fileName 字段 (Verify fileName field in the response JSON)
+                .andExpect(jsonPath("$.infoHash").value("mockInfoHash")) // 验证 infoHash 字段 (Verify infoHash field)
+                .andExpect(jsonPath("$.message").value("文件上传成功。")); // 验证 message 字段 (Verify message field)
+    }
+
+    /**
+     * Test uploading an empty file.
+     * 测试上传空文件的情况。
+     */
+    @Test
+    void uploadTorrent_EmptyFile() throws Exception {
+        MockMultipartFile emptyFile = new MockMultipartFile(
+                "file",
+                "empty.torrent",
+                MediaType.APPLICATION_OCTET_STREAM_VALUE,
+                new byte[0] // 空文件内容 (Empty file content)
+        );
+
+        mockMvc.perform(multipart("/api/torrents")
+                        .file(emptyFile)
+                        .contentType(MediaType.MULTIPART_FORM_DATA))
+                .andExpect(status().isBadRequest()) // 验证 HTTP 状态码为 400 Bad Request (Verify HTTP status code is 400 Bad Request)
+                .andExpect(jsonPath("$.message").value("上传文件不能为空。")); // 验证错误消息 (Verify error message)
+    }
+
+    /**
+     * Test uploading a file with an unsupported extension.
+     * 测试上传不支持的文件扩展名的情况。
+     */
+    @Test
+    void uploadTorrent_UnsupportedFileType() throws Exception {
+        MockMultipartFile wrongTypeFile = new MockMultipartFile(
+                "file",
+                "test.txt", // 非 .torrent 文件 (Not a .torrent file)
+                MediaType.TEXT_PLAIN_VALUE,
+                "This is not a torrent file.".getBytes()
+        );
+
+        mockMvc.perform(multipart("/api/torrents")
+                        .file(wrongTypeFile)
+                        .contentType(MediaType.MULTIPART_FORM_DATA))
+                .andExpect(status().isUnsupportedMediaType()) // 验证 HTTP 状态码为 415 Unsupported Media Type (Verify HTTP status code is 415 Unsupported Media Type)
+                .andExpect(jsonPath("$.message").value("只允许上传 .torrent 文件。")); // 验证错误消息 (Verify error message)
+    }
+
+    /**
+     * Test handling of InvalidBEncodingException from the service layer.
+     * 测试处理来自服务层的 InvalidBEncodingException。
+     */
+    @Test
+    void uploadTorrent_InvalidBEncodingException() throws Exception {
+        MockMultipartFile mockFile = new MockMultipartFile(
+                "file",
+                "invalid.torrent",
+                MediaType.APPLICATION_OCTET_STREAM_VALUE,
+                "invalid bencoding data".getBytes()
+        );
+
+        // 配置 torrentService.handleUpload 在被调用时抛出 InvalidBEncodingException
+        // Configure torrentService.handleUpload to throw InvalidBEncodingException when called
+        when(torrentService.handleUpload(any(MultipartFile.class)))
+                .thenThrow(new InvalidBEncodingException("Mocked BEncoding error"));
+
+        mockMvc.perform(multipart("/api/torrents")
+                        .file(mockFile)
+                        .contentType(MediaType.MULTIPART_FORM_DATA))
+                .andExpect(status().isBadRequest()) // 验证 HTTP 状态码为 400 Bad Request (Verify HTTP status code is 400 Bad Request)
+                .andExpect(jsonPath("$.message").value("文件解析失败,请确保它是有效的 .torrent 文件: Mocked BEncoding error")); // 验证错误消息 (Verify error message)
+    }
+
+    /**
+     * Test handling of IOException from the service layer.
+     * 测试处理来自服务层的 IOException。
+     */
+    @Test
+    void uploadTorrent_IOException() throws Exception {
+        MockMultipartFile mockFile = new MockMultipartFile(
+                "file",
+                "ioerror.torrent",
+                MediaType.APPLICATION_OCTET_STREAM_VALUE,
+                "d8:announce...".getBytes()
+        );
+
+        // 配置 torrentService.handleUpload 在被调用时抛出 IOException
+        // Configure torrentService.handleUpload to throw IOException when called
+        when(torrentService.handleUpload(any(MultipartFile.class)))
+                .thenThrow(new IOException("Mocked IO error"));
+
+        mockMvc.perform(multipart("/api/torrents")
+                        .file(mockFile)
+                        .contentType(MediaType.MULTIPART_FORM_DATA))
+                .andExpect(status().isInternalServerError()) // 验证 HTTP 状态码为 500 Internal Server Error (Verify HTTP status code is 500 Internal Server Error)
+                .andExpect(jsonPath("$.message").value("文件上传或保存失败: Mocked IO error")); // 验证错误消息 (Verify error message)
+    }
+
+    /**
+     * Test handling of a generic Exception from the service layer.
+     * 测试处理来自服务层的通用 Exception。
+     */
+    @Test
+    void uploadTorrent_GenericException() throws Exception {
+        MockMultipartFile mockFile = new MockMultipartFile(
+                "file",
+                "genericerror.torrent",
+                MediaType.APPLICATION_OCTET_STREAM_VALUE,
+                "d8:announce...".getBytes()
+        );
+
+        // 配置 torrentService.handleUpload 在被调用时抛出 RuntimeException
+        // Configure torrentService.handleUpload to throw RuntimeException when called
+        when(torrentService.handleUpload(any(MultipartFile.class)))
+                .thenThrow(new RuntimeException("Mocked generic error"));
+
+        mockMvc.perform(multipart("/api/torrents")
+                        .file(mockFile)
+                        .contentType(MediaType.MULTIPART_FORM_DATA))
+                .andExpect(status().isInternalServerError()) // 验证 HTTP 状态码为 500 Internal Server Error (Verify HTTP status code is 500 Internal Server Error)
+                .andExpect(jsonPath("$.message").value("文件上传过程中发生未知错误: Mocked generic error")); // 验证错误消息 (Verify error message)
+    }
+
+     /**
+     * Test handling of IllegalArgumentException from the service layer.
+     * 测试处理来自服务层的 IllegalArgumentException。
+     */
+    @Test
+    void uploadTorrent_IllegalArgumentExceptionFromService() throws Exception {
+        MockMultipartFile mockFile = new MockMultipartFile(
+                "file",
+                "illegalarg.torrent",
+                MediaType.APPLICATION_OCTET_STREAM_VALUE,
+                "d8:announce...".getBytes()
+        );
+
+        // 配置 torrentService.handleUpload 在被调用时抛出 IllegalArgumentException
+        // Configure torrentService.handleUpload to throw IllegalArgumentException when called
+        when(torrentService.handleUpload(any(MultipartFile.class)))
+                .thenThrow(new IllegalArgumentException("Mocked illegal argument from service"));
+
+        mockMvc.perform(multipart("/api/torrents")
+                        .file(mockFile)
+                        .contentType(MediaType.MULTIPART_FORM_DATA))
+                .andExpect(status().isBadRequest()) // 验证 HTTP 状态码为 400 Bad Request (Verify HTTP status code is 400 Bad Request)
+                .andExpect(jsonPath("$.message").value("处理文件时发生错误: Mocked illegal argument from service")); // 验证错误消息 (Verify error message)
+    }
+}
\ No newline at end of file
diff --git a/backend/demo/src/test/java/com/example/demo/security/JwtTokenUtilTest.java b/backend/demo/src/test/java/com/example/demo/security/JwtTokenUtilTest.java
new file mode 100644
index 0000000..e32e591
--- /dev/null
+++ b/backend/demo/src/test/java/com/example/demo/security/JwtTokenUtilTest.java
@@ -0,0 +1,281 @@
+package com.example.demo.security; // 请根据您的项目结构调整包名
+
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test; // Import MockitoSettings
+import org.junit.jupiter.api.extension.ExtendWith; // Import Strictness
+import org.mockito.Mock;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+import com.example.demo.config.JWTProperties;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.ExpiredJwtException;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.security.Keys;
+import io.jsonwebtoken.security.SignatureException;
+
+/**
+ * 单元测试 JwtTokenUtil,手动实例化并注入 Mock 依赖
+ */
+@ExtendWith(MockitoExtension.class)
+// Add this annotation to make stubbings in this test class lenient
+@MockitoSettings(strictness = Strictness.LENIENT)
+public class JwtTokenUtilTest {
+
+    @Mock
+    private JWTProperties jwtProperties;
+
+    private JwtTokenUtil jwtTokenUtil;
+
+    private byte[] testSecretBytes;
+    // Ensure secret key is long enough for HS256 (at least 32 bytes or 256 bits)
+    private final String testSecret = "ThisIsATestSecretKeyForMockingPurposesAndItNeedsToBeLongEnoughToMeetHS256Requirements";
+    private final long testExpirationMs = 3600000L; // 1 hour
+
+    /**
+     * Initialize Mock objects' behavior and test data before each test method.
+     * With @ExtendWith(MockitoExtension.class), mock objects are initialized
+     * before this method runs.
+     */
+    @BeforeEach
+    void setUp() {
+        // Stub JWTProperties to provide data before creating the instance
+        // Using lenient() or @MockitoSettings(strictness = Strictness.LENIENT)
+        // allows stubbings that are not used by every test method.
+        when(jwtProperties.getSecret()).thenReturn(testSecret);
+        when(jwtProperties.getExpirationMs()).thenReturn(testExpirationMs);
+        when(jwtProperties.getIssuer()).thenReturn("test-issuer");
+        when(jwtProperties.getAudience()).thenReturn("test-audience");
+        when(jwtProperties.getHeader()).thenReturn("Authorization");
+        when(jwtProperties.getTokenPrefix()).thenReturn("Bearer ");
+
+        // Create the instance to be tested, injecting the mocked properties
+        // This is done AFTER stubbing the necessary methods for the constructor.
+        jwtTokenUtil = new JwtTokenUtil(jwtProperties);
+
+        // Calculate testSecretBytes (used for manually parsing/building tokens in tests)
+        testSecretBytes = testSecret.getBytes(StandardCharsets.UTF_8);
+    }
+
+    @Test
+    void testGenerateToken() {
+        Long userId = 123L;
+        String username = "testuser";
+        String role = "USER";
+
+        String token = jwtTokenUtil.generateToken(userId, username, role);
+
+        assertNotNull(token);
+        assertFalse(token.isEmpty());
+
+        // Verify the generated token can be correctly parsed and contains expected Claims
+        Claims claims = Jwts.parserBuilder()
+                .setSigningKey(testSecretBytes) // Use the derived secret bytes
+                .build()
+                .parseClaimsJws(token)
+                .getBody();
+
+        assertEquals(username, claims.getSubject());
+        assertEquals("test-issuer", claims.getIssuer());
+        assertEquals("test-audience", claims.getAudience());
+        assertNotNull(claims.getIssuedAt());
+        assertNotNull(claims.getExpiration());
+        assertTrue(claims.getExpiration().getTime() > claims.getIssuedAt().getTime());
+        assertEquals(userId.intValue(), claims.get("userId", Integer.class));
+        assertEquals(role, claims.get("role", String.class));
+    }
+
+    @Test
+    void testParseClaimsValidToken() {
+        Long userId = 456L;
+        String username = "anotheruser";
+        String role = "ADMIN";
+        Date now = new Date();
+        Date expiry = new Date(now.getTime() + testExpirationMs);
+        String validToken = Jwts.builder()
+                .setSubject(username)
+                .setIssuer("test-issuer")
+                .setAudience("test-audience")
+                .setIssuedAt(now)
+                .setExpiration(expiry)
+                .addClaims(Map.of("userId", userId, "role", role))
+                .signWith(Keys.hmacShaKeyFor(testSecretBytes), SignatureAlgorithm.HS256)
+                .compact();
+
+        Claims claims = jwtTokenUtil.parseClaims(validToken);
+
+        assertNotNull(claims);
+        assertEquals(username, claims.getSubject());
+        assertEquals("test-issuer", claims.getIssuer());
+        assertEquals("test-audience", claims.getAudience());
+        assertEquals(userId.intValue(), claims.get("userId", Integer.class));
+        assertEquals(role, claims.get("role", String.class));
+    }
+
+    @Test
+    void testParseClaimsInvalidSignature() {
+        Date now = new Date();
+        Date expiry = new Date(now.getTime() + testExpirationMs);
+        String invalidToken = Jwts.builder()
+                .setSubject("someuser")
+                .setIssuedAt(now)
+                .setExpiration(expiry)
+                // Use a different secret key for signing
+                .signWith(Keys.hmacShaKeyFor("AnotherSecretKeyAnotherSecretKey".getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS256)
+                .compact();
+
+        assertThrows(SignatureException.class, () -> jwtTokenUtil.parseClaims(invalidToken));
+    }
+
+    @Test
+    void testParseClaimsExpiredToken() {
+        // Create an expired token
+        Date past = new Date(System.currentTimeMillis() - testExpirationMs - 1000); // Expiration time is in the past
+        Date expiry = new Date(past.getTime() + 1000); // Expires shortly after being issued
+        String expiredToken = Jwts.builder()
+                .setSubject("expireduser")
+                .setIssuedAt(past)
+                .setExpiration(expiry)
+                .signWith(Keys.hmacShaKeyFor(testSecretBytes), SignatureAlgorithm.HS256)
+                .compact();
+
+        assertThrows(ExpiredJwtException.class, () -> jwtTokenUtil.parseClaims(expiredToken));
+    }
+
+    @Test
+    void testValidateTokenValid() {
+        Date now = new Date();
+        Date expiry = new Date(now.getTime() + testExpirationMs);
+        String validToken = Jwts.builder()
+                .setSubject("validuser")
+                .setIssuedAt(now)
+                .setExpiration(expiry)
+                .addClaims(Map.of("userId", 789L, "role", "GUEST"))
+                .signWith(Keys.hmacShaKeyFor(testSecretBytes), SignatureAlgorithm.HS256)
+                .compact();
+
+        assertTrue(jwtTokenUtil.validateToken(validToken));
+    }
+
+    @Test
+    void testValidateTokenInvalidSignature() {
+        Date now = new Date();
+        Date expiry = new Date(now.getTime() + testExpirationMs);
+        String invalidToken = Jwts.builder()
+                .setSubject("invaliduser")
+                .setIssuedAt(now)
+                .setExpiration(expiry)
+                .signWith(Keys.hmacShaKeyFor("AnotherSecretKeyAnotherSecretKey".getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS256)
+                .compact();
+
+        assertFalse(jwtTokenUtil.validateToken(invalidToken));
+    }
+
+    @Test
+    void testValidateTokenExpired() {
+        Date past = new Date(System.currentTimeMillis() - testExpirationMs - 1000);
+        Date expiry = new Date(past.getTime() + 1000);
+        String expiredToken = Jwts.builder()
+                .setSubject("expireduser")
+                .setIssuedAt(past)
+                .setExpiration(expiry)
+                .signWith(Keys.hmacShaKeyFor(testSecretBytes), SignatureAlgorithm.HS256)
+                .compact();
+
+        assertFalse(jwtTokenUtil.validateToken(expiredToken));
+    }
+
+    @Test
+    void testGetUsername() {
+        String username = "extractuser";
+        Date now = new Date();
+        Date expiry = new Date(now.getTime() + testExpirationMs);
+        String token = Jwts.builder()
+                .setSubject(username)
+                .setIssuedAt(now)
+                .setExpiration(expiry)
+                .signWith(Keys.hmacShaKeyFor(testSecretBytes), SignatureAlgorithm.HS256)
+                .compact();
+
+        assertEquals(username, jwtTokenUtil.getUsername(token));
+    }
+
+    @Test
+    void testGetUserId() {
+        Long userId = 999L;
+        Date now = new Date();
+        Date expiry = new Date(now.getTime() + testExpirationMs);
+        String token = Jwts.builder()
+                .setSubject("someuser")
+                .setIssuedAt(now)
+                .setExpiration(expiry)
+                .addClaims(Map.of("userId", userId))
+                .signWith(Keys.hmacShaKeyFor(testSecretBytes), SignatureAlgorithm.HS256)
+                .compact();
+
+        assertEquals(userId, jwtTokenUtil.getUserId(token));
+    }
+
+    @Test
+    void testGetUserIdWhenClaimMissing() {
+        Date now = new Date();
+        Date expiry = new Date(now.getTime() + testExpirationMs);
+        String token = Jwts.builder()
+                .setSubject("userwithoutid")
+                .setIssuedAt(now)
+                .setExpiration(expiry)
+                .signWith(Keys.hmacShaKeyFor(testSecretBytes), SignatureAlgorithm.HS256)
+                .compact();
+
+        assertNull(jwtTokenUtil.getUserId(token));
+    }
+
+    @Test
+    void testGetRole() {
+        String role = "MANAGER";
+        Date now = new Date();
+        Date expiry = new Date(now.getTime() + testExpirationMs);
+        String token = Jwts.builder()
+                .setSubject("someuser")
+                .setIssuedAt(now)
+                .setExpiration(expiry)
+                .addClaims(Map.of("role", role))
+                .signWith(Keys.hmacShaKeyFor(testSecretBytes), SignatureAlgorithm.HS256)
+                .compact();
+
+        assertEquals(role, jwtTokenUtil.getRole(token));
+    }
+
+    @Test
+    void testGetRoleWhenClaimMissing() {
+        Date now = new Date();
+        Date expiry = new Date(now.getTime() + testExpirationMs);
+        String token = Jwts.builder()
+                .setSubject("userwithoutrole")
+                .setIssuedAt(now)
+                .setExpiration(expiry)
+                .signWith(Keys.hmacShaKeyFor(testSecretBytes), SignatureAlgorithm.HS256)
+                .compact();
+
+        assertNull(jwtTokenUtil.getRole(token));
+    }
+
+    @Test
+    void testGetExpiration() {
+        assertEquals(testExpirationMs, jwtTokenUtil.getExpiration());
+    }
+}
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
new file mode 100644
index 0000000..69eb5ba
--- /dev/null
+++ b/backend/demo/src/test/java/com/example/demo/service/impl/TorrentServiceImplTest.java
@@ -0,0 +1,462 @@
+// src/test/java/com/example/demo/service/impl/TorrentServiceImplTest.java
+package com.example.demo.service.impl;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.LocalDateTime;
+import java.util.Comparator;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.any;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+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.util.TorrentUtils;
+import com.example.demo.util.TorrentUtils.TorrentParsedInfo;
+import com.turn.ttorrent.bcodec.InvalidBEncodingException;
+
+/**
+ * Test class for TorrentServiceImpl.
+ * Uses JUnit 5 and Mockito for unit testing.
+ */
+@ExtendWith(MockitoExtension.class) // Enables Mockito annotations for JUnit 5
+class TorrentServiceImplTest {
+
+    @Mock // Creates a mock instance of TorrentInfoMapper
+    private TorrentInfoMapper torrentInfoMapper;
+
+    // @InjectMocks injects the mocks into the TorrentServiceImpl instance
+    // It will try to inject 'torrentInfoMapper' into the constructor of TorrentServiceImpl
+    @InjectMocks
+    private TorrentServiceImpl torrentService;
+
+    // This field will be injected into the service using ReflectionTestUtils
+    private String uploadDir = "test_temp_upload_dir";
+
+    /**
+     * Setup method executed before each test.
+     * It injects the 'uploadDir' value and ensures the test directory is clean.
+     *
+     * @throws IOException if directory operations fail
+     */
+    @BeforeEach
+    void setUp() throws IOException {
+        // Inject the 'uploadDir' field into the torrentService instance.
+        // This simulates Spring's @Value injection for testing purposes.
+        ReflectionTestUtils.setField(torrentService, "uploadDir", uploadDir);
+
+        // Define the path for the temporary upload directory used in tests.
+        Path testUploadPath = Paths.get(uploadDir);
+
+        // Clean up the directory before each test to ensure a consistent state.
+        if (Files.exists(testUploadPath)) {
+            // Use try-with-resources for the stream to ensure it's closed
+            try (Stream<Path> walk = Files.walk(testUploadPath)) {
+                walk.sorted(Comparator.reverseOrder()) // Process deeper paths first
+                    .forEach(path -> {
+                        try {
+                            Files.delete(path); // Delete file or empty directory
+                        } catch (IOException e) {
+                            System.err.println("Failed to delete path " + path + ": " + e.getMessage());
+                            // In a real scenario, you might want to re-throw or handle more robustly
+                            // if cleanup failure is critical for subsequent tests.
+                        }
+                    });
+            } // Stream is closed here
+        }
+        // Always create the directory for the current test.
+        Files.createDirectories(testUploadPath);
+    }
+
+    /**
+     * Test case for successfully handling a new torrent file upload.
+     * Verifies parsing, file saving, database insertion, and DTO creation.
+     *
+     * @throws Exception if any unexpected error occurs during the test
+     */
+    @Test
+    @DisplayName("Should successfully handle a new torrent file upload")
+    void handleUpload_NewTorrent_Success() throws Exception {
+        // 1. Prepare a mock MultipartFile for input
+        MockMultipartFile mockFile = new MockMultipartFile(
+            "file", // parameter name
+            "example.torrent", // original filename
+            "application/x-bittorrent", // content type
+            "dummy torrent content".getBytes() // file content as bytes
+        );
+
+        // 2. Prepare mock data for TorrentUtils.TorrentParsedInfo result
+        String testInfoHash = "AABBCCDDEEFF11223344556677889900AABBCCDD";
+        String testFileName = "MyAwesomeMovie.mp4";
+        long testFileSize = 1024000L; // 1 MB
+
+        TorrentParsedInfo mockParsedInfo = new TorrentParsedInfo(
+            testInfoHash,
+            testFileName,
+            testFileSize
+        );
+
+        // Mock static method call for TorrentUtils.parseTorrentFile
+        // Mockito.mockStatic is used to mock static methods. It needs to be in a try-with-resources block.
+        try (MockedStatic<TorrentUtils> mockedTorrentUtils = Mockito.mockStatic(TorrentUtils.class)) {
+            // Define the behavior of the static method when called with any MultipartFile.
+            mockedTorrentUtils.when(() -> TorrentUtils.parseTorrentFile(any(MultipartFile.class)))
+                              .thenReturn(mockParsedInfo);
+
+            // 3. Mock TorrentInfoMapper behavior
+            // Simulate that no existing torrent is found in the database for the given infoHash.
+            when(torrentInfoMapper.selectOne(any(QueryWrapper.class))).thenReturn(null);
+            // Simulate successful insertion into the database (insert method returns 1 for success).
+            when(torrentInfoMapper.insert(any(TorrentInfo.class))).thenReturn(1);
+
+            // 4. Call the service method under test
+            TorrentInfoDTO result = torrentService.handleUpload(mockFile);
+
+            // 5. Assertions: Verify the returned DTO and side effects
+
+            // Ensure the result DTO is not null.
+            assertNotNull(result, "Returned DTO should not be null");
+            // Verify that the DTO contains the correct parsed information.
+            assertEquals(testFileName, result.getFileName(), "File name in DTO should match parsed info");
+            assertEquals(testFileSize, result.getFileSize(), "File size in DTO should match parsed info");
+            assertEquals(testInfoHash, result.getInfoHash(), "Info hash in DTO should match parsed info");
+            // Verify that magnet URI and download URL are correctly formatted and contain the info hash.
+            assertTrue(result.getMagnetUri().contains(testInfoHash), "Magnet URI should contain info hash");
+            assertTrue(result.getDownloadUrl().contains(testInfoHash), "Download URL should contain info hash");
+            // Verify the success message.
+            assertEquals("Torrent uploaded and processed successfully!", result.getMessage(), "Success message should be present");
+
+            // Verify interactions with mocks
+            // Verify that selectOne was called exactly once with any QueryWrapper.
+            verify(torrentInfoMapper, times(1)).selectOne(any(QueryWrapper.class));
+            // Verify that insert was called exactly once with any TorrentInfo object.
+            verify(torrentInfoMapper, times(1)).insert(any(TorrentInfo.class));
+            // Verify that TorrentUtils.parseTorrentFile was called exactly once.
+            mockedTorrentUtils.verify(() -> TorrentUtils.parseTorrentFile(any(MultipartFile.class)), times(1));
+
+            // Verify file was saved to the correct location on disk
+            Path expectedFilePath = Paths.get(uploadDir, testInfoHash + ".torrent");
+            assertTrue(Files.exists(expectedFilePath), "Torrent file should be saved to disk");
+            // Optionally, verify the content of the saved file if needed
+            // assertEquals("dummy torrent content", new String(Files.readAllBytes(expectedFilePath)));
+        }
+    }
+
+    /**
+     * Test case for handling a torrent file that already exists in the database.
+     * Verifies that existing information is returned and no new file is saved or DB entry created.
+     *
+     * @throws Exception if any unexpected error occurs during the test
+     */
+    @Test
+    @DisplayName("Should return existing info if torrent already exists")
+    void handleUpload_ExistingTorrent_ReturnsExistingInfo() throws Exception {
+        // 1. Prepare a mock MultipartFile for input (content doesn't matter much here)
+        MockMultipartFile mockFile = new MockMultipartFile(
+            "file",
+            "existing.torrent",
+            "application/x-bittorrent",
+            "some content".getBytes()
+        );
+
+        // 2. Prepare mock data for TorrentUtils.TorrentParsedInfo result
+        String existingInfoHash = "EXISTINGHASH11223344556677889900EXISTINGHASH";
+        String existingFileName = "ExistingMovie.mkv";
+        long existingFileSize = 5000000L; // 5 MB
+
+        TorrentParsedInfo mockParsedInfo = new TorrentParsedInfo(
+            existingInfoHash,
+            existingFileName,
+            existingFileSize
+        );
+
+        // 3. Prepare an existing TorrentInfo entity that would be returned by the mapper
+        TorrentInfo existingTorrentInDb = new TorrentInfo(
+            existingInfoHash,
+            existingFileName,
+            existingFileSize,
+            "magnet:?xt=urn:btih:" + existingInfoHash + "&dn=" + existingFileName,
+            "/api/downloads/" + existingInfoHash + "/" + existingFileName
+        );
+        existingTorrentInDb.setId(1L); // Simulate an ID from the database
+        existingTorrentInDb.setUploadTime(LocalDateTime.now().minusDays(1)); // Simulate an existing upload time
+
+        // Mock static method call for TorrentUtils.parseTorrentFile
+        try (MockedStatic<TorrentUtils> mockedTorrentUtils = Mockito.mockStatic(TorrentUtils.class)) {
+            mockedTorrentUtils.when(() -> TorrentUtils.parseTorrentFile(any(MultipartFile.class)))
+                              .thenReturn(mockParsedInfo);
+
+            // 4. Mock TorrentInfoMapper behavior: Simulate that the torrent DOES exist
+            when(torrentInfoMapper.selectOne(any(QueryWrapper.class))).thenReturn(existingTorrentInDb);
+
+            // 5. Call the service method
+            TorrentInfoDTO result = torrentService.handleUpload(mockFile);
+
+            // 6. Assertions
+            assertNotNull(result, "Returned DTO should not be null");
+            // Verify that the DTO contains the information from the existing DB entry.
+            assertEquals(existingFileName, result.getFileName(), "File name should match existing DB entry");
+            assertEquals(existingFileSize, result.getFileSize(), "File size should match existing DB entry");
+            assertEquals(existingInfoHash, result.getInfoHash(), "Info hash should match existing DB entry");
+            assertEquals(existingTorrentInDb.getMagnetUri(), result.getMagnetUri(), "Magnet URI should match existing DB entry");
+            assertEquals(existingTorrentInDb.getDownloadUrl(), result.getDownloadUrl(), "Download URL should match existing DB entry");
+            // Verify the specific message for existing torrents.
+            assertEquals("该洪流已存在,返回现有信息。", result.getMessage(), "Message should indicate existing torrent");
+
+            // Verify interactions with mocks
+            // selectOne should be called once to check for existence.
+            verify(torrentInfoMapper, times(1)).selectOne(any(QueryWrapper.class));
+            // insert should NOT be called because the torrent already exists.
+            verify(torrentInfoMapper, never()).insert(any(TorrentInfo.class));
+            // TorrentUtils.parseTorrentFile should still be called once to get the info hash.
+            mockedTorrentUtils.verify(() -> TorrentUtils.parseTorrentFile(any(MultipartFile.class)), times(1));
+
+            // Verify that no new file was saved to disk
+            Path expectedFilePath = Paths.get(uploadDir, existingInfoHash + ".torrent");
+            assertFalse(Files.exists(expectedFilePath), "Torrent file should NOT be saved to disk if already exists");
+        }
+    }
+
+    /**
+     * Test case for uploading an empty MultipartFile.
+     * Expects an IllegalArgumentException.
+     */
+    @Test
+    @DisplayName("Should throw IllegalArgumentException for empty file upload")
+    void handleUpload_EmptyFile_ThrowsIllegalArgumentException() {
+        // Prepare an empty mock MultipartFile
+        MockMultipartFile emptyFile = new MockMultipartFile(
+            "file",
+            "empty.torrent",
+            "application/x-bittorrent",
+            new byte[0] // Empty content
+        );
+
+        // Assert that calling handleUpload with an empty file throws IllegalArgumentException.
+        IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+            torrentService.handleUpload(emptyFile);
+        }, "Should throw IllegalArgumentException for empty file");
+
+        // Verify the error message.
+        assertEquals("上传文件不能为空。", thrown.getMessage(), "Error message should indicate empty file");
+
+        // Verify no interactions with mocks (as the check happens early)
+        verifyNoInteractions(torrentInfoMapper);
+        // We don't need to mock TorrentUtils here as the check happens before its call.
+    }
+
+    /**
+     * Test case for when TorrentUtils.parseTorrentFile throws InvalidBEncodingException.
+     * Expects an IllegalArgumentException with a specific message.
+     *
+     * @throws IOException if an unexpected IO error occurs
+     */
+    @Test
+    @DisplayName("Should throw IllegalArgumentException when TorrentUtils throws InvalidBEncodingException")
+    void handleUpload_InvalidBEncoding_ThrowsIllegalArgumentException() throws IOException {
+        MockMultipartFile mockFile = new MockMultipartFile(
+            "file",
+            "corrupt.torrent",
+            "application/x-bittorrent",
+            "corrupt content".getBytes()
+        );
+
+        // Mock static method call for TorrentUtils.parseTorrentFile to throw InvalidBEncodingException
+        try (MockedStatic<TorrentUtils> mockedTorrentUtils = Mockito.mockStatic(TorrentUtils.class)) {
+            mockedTorrentUtils.when(() -> TorrentUtils.parseTorrentFile(any(MultipartFile.class)))
+                              .thenThrow(new InvalidBEncodingException("Malformed B-encoded data"));
+
+            // Assert that calling handleUpload throws IllegalArgumentException.
+            IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+                torrentService.handleUpload(mockFile);
+            }, "Should throw IllegalArgumentException for invalid B-encoding");
+
+            // Verify the error message.
+            assertTrue(thrown.getMessage().contains("无法解析 .torrent 文件,文件格式不正确"), "Error message should indicate invalid format");
+            assertTrue(thrown.getCause() instanceof InvalidBEncodingException, "Cause should be InvalidBEncodingException");
+
+            // Verify interactions
+            verifyNoInteractions(torrentInfoMapper); // No DB interaction if parsing fails
+            mockedTorrentUtils.verify(() -> TorrentUtils.parseTorrentFile(any(MultipartFile.class)), times(1));
+        }
+    }
+
+    /**
+     * Test case for when TorrentUtils.parseTorrentFile throws IOException during file reading.
+     * Expects an IOException.
+     */
+    @Test
+    @DisplayName("Should throw IOException when TorrentUtils throws IOException during parsing")
+    void handleUpload_IOExceptionDuringParsing_ThrowsIOException() {
+        // Prepare a basic mock file. Only isEmpty() is relevant here before parsing.
+        MockMultipartFile mockFile = new MockMultipartFile(
+            "file",
+            "error.torrent",
+            "application/x-bittorrent",
+            "dummy content".getBytes() // Content doesn't matter as parsing is mocked to fail immediately
+        );
+
+        // Mock static method call for TorrentUtils.parseTorrentFile to throw IOException
+        try (MockedStatic<TorrentUtils> mockedTorrentUtils = Mockito.mockStatic(TorrentUtils.class)) {
+            mockedTorrentUtils.when(() -> TorrentUtils.parseTorrentFile(any(MultipartFile.class)))
+                              .thenThrow(new IOException("Simulated parsing IO error"));
+
+            // Assert that calling handleUpload throws IOException.
+            IOException thrown = assertThrows(IOException.class, () -> {
+                torrentService.handleUpload(mockFile);
+            }, "Should throw IOException for parsing IO error");
+
+            // Verify the error message.
+            assertTrue(thrown.getMessage().contains("读取上传文件失败"), "Error message should indicate file read failure");
+            assertTrue(thrown.getCause() instanceof IOException, "Cause should be IOException");
+
+            // Verify interactions
+            verifyNoInteractions(torrentInfoMapper); // No DB interaction if parsing fails
+            mockedTorrentUtils.verify(() -> TorrentUtils.parseTorrentFile(any(MultipartFile.class)), times(1));
+        }
+    }
+
+    /**
+     * Test case for when file saving to disk fails (IOException).
+     * Verifies that an IOException is thrown and database operations are not committed.
+     *
+     * @throws Exception if any unexpected error occurs during the test
+     */
+    @Test
+    @DisplayName("Should throw IOException when file saving to disk fails")
+    void handleUpload_FileSaveFails_ThrowsIOException() throws Exception {
+        MockMultipartFile mockFile = new MockMultipartFile(
+            "file",
+            "save_fail.torrent",
+            "application/x-bittorrent",
+            "content".getBytes()
+        );
+
+        String testInfoHash = "FAILHASH11223344556677889900FAILHASH";
+        String testFileName = "FailSave.txt";
+        long testFileSize = 500L;
+
+        TorrentParsedInfo mockParsedInfo = new TorrentParsedInfo(
+            testInfoHash,
+            testFileName,
+            testFileSize
+        );
+
+        // Use MockedStatic for Files.copy to simulate IO failure
+        try (MockedStatic<TorrentUtils> mockedTorrentUtils = Mockito.mockStatic(TorrentUtils.class);
+             MockedStatic<Files> mockedFiles = Mockito.mockStatic(Files.class, CALLS_REAL_METHODS)) { // CALLS_REAL_METHODS allows other Files methods to work normally
+
+            // Mock TorrentUtils.parseTorrentFile
+            mockedTorrentUtils.when(() -> TorrentUtils.parseTorrentFile(any(MultipartFile.class)))
+                              .thenReturn(mockParsedInfo);
+
+            // Mock torrentInfoMapper.selectOne
+            when(torrentInfoMapper.selectOne(any(QueryWrapper.class))).thenReturn(null);
+
+            // Mock Files.copy to throw IOException when called with any InputStream and Path
+            // Note: mockFile.getInputStream() returns a ByteArrayInputStream
+            mockedFiles.when(() -> Files.copy(any(ByteArrayInputStream.class), any(Path.class)))
+                       .thenThrow(new IOException("Simulated file copy error"));
+
+            // Now, call the service method
+            IOException thrown = assertThrows(IOException.class, () -> {
+                torrentService.handleUpload(mockFile);
+            }, "Should throw IOException when file saving fails");
+
+            assertTrue(thrown.getMessage().contains("无法保存 .torrent 文件到磁盘"), "Error message should indicate file save failure");
+
+            // Verify interactions
+            verify(torrentInfoMapper, times(1)).selectOne(any(QueryWrapper.class));
+            // insert should NOT be called if file saving fails (due to @Transactional rollback)
+            verify(torrentInfoMapper, never()).insert(any(TorrentInfo.class));
+            mockedTorrentUtils.verify(() -> TorrentUtils.parseTorrentFile(any(MultipartFile.class)), times(1));
+            mockedFiles.verify(() -> Files.copy(any(ByteArrayInputStream.class), any(Path.class)), times(1));
+
+            // No need to check Files.exists as we mocked the copy operation.
+        }
+    }
+
+    /**
+     * Test case for when the database insert operation fails (e.g., returns 0 rows affected).
+     * Expects a RuntimeException.
+     *
+     * @throws Exception if any unexpected error occurs during the test
+     */
+    @Test
+    @DisplayName("Should throw RuntimeException when database insert fails")
+    void handleUpload_DatabaseInsertFails_ThrowsRuntimeException() throws Exception {
+        MockMultipartFile mockFile = new MockMultipartFile(
+            "file",
+            "db_fail.torrent",
+            "application/x-bittorrent",
+            "db content".getBytes()
+        );
+
+        String testInfoHash = "DBFAILHASH11223344556677889900DBFAIL";
+        String testFileName = "DBFail.txt";
+        long testFileSize = 700L;
+
+        TorrentParsedInfo mockParsedInfo = new TorrentParsedInfo(
+            testInfoHash,
+            testFileName,
+            testFileSize
+        );
+
+        try (MockedStatic<TorrentUtils> mockedTorrentUtils = Mockito.mockStatic(TorrentUtils.class)) {
+            mockedTorrentUtils.when(() -> TorrentUtils.parseTorrentFile(any(MultipartFile.class)))
+                              .thenReturn(mockParsedInfo);
+
+            when(torrentInfoMapper.selectOne(any(QueryWrapper.class))).thenReturn(null);
+            // Simulate database insert failure (insert method returns 0 rows affected).
+            when(torrentInfoMapper.insert(any(TorrentInfo.class))).thenReturn(0);
+
+            // Assert that calling handleUpload throws RuntimeException.
+            RuntimeException thrown = assertThrows(RuntimeException.class, () -> {
+                torrentService.handleUpload(mockFile);
+            }, "Should throw RuntimeException when DB insert fails");
+
+            // Verify the error message.
+            assertEquals("保存洪流信息到数据库失败。", thrown.getMessage(), "Error message should indicate DB insert failure");
+
+            // Verify interactions
+            verify(torrentInfoMapper, times(1)).selectOne(any(QueryWrapper.class));
+            verify(torrentInfoMapper, times(1)).insert(any(TorrentInfo.class));
+            mockedTorrentUtils.verify(() -> TorrentUtils.parseTorrentFile(any(MultipartFile.class)), times(1));
+
+            // Verify that the file was saved (as the DB failure happens after file saving)
+            Path expectedFilePath = Paths.get(uploadDir, testInfoHash + ".torrent");
+            assertTrue(Files.exists(expectedFilePath), "Torrent file should be saved even if DB insert fails (before rollback)");
+            // In a real scenario with @Transactional, the file save might also be rolled back
+            // if the file system operation was part of a transactional resource, but typically it's not.
+            // Here, we verify the file exists as the exception occurs AFTER file saving.
+        }
+    }
+}
\ No newline at end of file
diff --git a/backend/demo/src/test/java/com/example/demo/util/TorrentByteReader.java b/backend/demo/src/test/java/com/example/demo/util/TorrentByteReader.java
new file mode 100644
index 0000000..c6bacbb
--- /dev/null
+++ b/backend/demo/src/test/java/com/example/demo/util/TorrentByteReader.java
@@ -0,0 +1,72 @@
+// src/main/java/com/example/demo/util/TorrentByteReader.java
+package com.example.demo.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import com.turn.ttorrent.common.TorrentMetadata;
+import com.turn.ttorrent.common.TorrentParser;
+
+public class TorrentByteReader {
+
+    public static void main(String[] args) {
+        // TODO: 请将此处替换为您真实的单个 .torrent 文件路径
+        String torrentFilePathString = "C:\\Users\\Acer\\Desktop\\复杂度的渐近表示.pdf.torrent";
+        // 示例:String torrentFilePathString = "/home/user/torrents/my_single_file.torrent";
+
+        Path torrentFilePath = Paths.get(torrentFilePathString);
+
+        try {
+            // 读取 .torrent 文件的所有字节
+            byte[] torrentBytes = Files.readAllBytes(torrentFilePath);
+
+            // 使用 ttorrent 库解析 .torrent 文件,获取 infoHash、文件名和文件大小
+            TorrentParser parser = new TorrentParser();
+            TorrentMetadata metadata = parser.parseFromFile(new File(torrentFilePathString));
+
+            String infoHash = metadata.getHexInfoHash();
+            String fileName = metadata.getDirectoryName(); // 对于单文件,通常是文件名
+            long fileSize = metadata.getFiles().stream().mapToLong(f -> f.size).sum();
+
+            System.out.println("=====================================================================");
+            System.out.println("请将以下数据复制并粘贴到您的测试文件中的相应 TODO 位置:");
+            System.out.println("=====================================================================");
+
+            System.out.println("\n// REAL_SINGLE_FILE_TORRENT_BYTES:");
+            System.out.print("public static final byte[] REAL_SINGLE_FILE_TORRENT_BYTES = {");
+            for (int i = 0; i < torrentBytes.length; i++) {
+                System.out.printf("(byte)0x%02X", torrentBytes[i]);
+                if (i < torrentBytes.length - 1) {
+                    System.out.print(", ");
+                    if ((i + 1) % 16 == 0) { // 每 16 个字节换行,提高可读性
+                        System.out.print("\n    ");
+                    }
+                }
+            }
+            System.out.println("};");
+
+            System.out.println("\n// REAL_SINGLE_FILE_INFO_HASH:");
+            System.out.printf("public static final String REAL_SINGLE_FILE_INFO_HASH = \"%s\";%n", infoHash);
+
+            System.out.println("\n// REAL_SINGLE_FILE_NAME:");
+            System.out.printf("public static final String REAL_SINGLE_FILE_NAME = \"%s\";%n", fileName);
+
+            System.out.println("\n// REAL_SINGLE_FILE_SIZE:");
+            System.out.printf("public static final long REAL_SINGLE_FILE_SIZE = %dL;%n", fileSize);
+
+            System.out.println("\n=====================================================================");
+            System.out.println("请确保您填充的数据与您实际的 .torrent 文件内容完全匹配。");
+            System.out.println("=====================================================================");
+
+        } catch (IOException e) {
+            System.err.println("读取文件失败,请检查文件路径和权限: " + e.getMessage());
+            e.printStackTrace();
+        } catch (Exception e) { // 捕获 ttorrent 解析可能抛出的异常
+            System.err.println("解析 .torrent 文件失败: " + e.getMessage());
+            e.printStackTrace();
+        }
+    }
+}
\ No newline at end of file