Merge branch 'master' into whx

Change-Id: Ic272087eaf74f7219c49d07a475458240f28be3d
diff --git a/.gitignore b/.gitignore
index 549e00a..d9e9af4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,3 +31,6 @@
 
 ### VS Code ###
 .vscode/
+
+### Upload ###
+./uploaded-torrents/
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 9dfe5e5..9238ad6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -60,6 +60,46 @@
             <artifactId>bencode</artifactId>
             <version>1.4</version>
         </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-api</artifactId>
+            <version>0.12.6</version>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-impl</artifactId>
+            <version>0.12.6</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
+            <version>0.12.6</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <version>4.0.1</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-mail</artifactId>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/src/main/java/com/example/g8backend/G8BackendApplication.java b/src/main/java/com/example/g8backend/G8BackendApplication.java
index f94569a..e380bb9 100644
--- a/src/main/java/com/example/g8backend/G8BackendApplication.java
+++ b/src/main/java/com/example/g8backend/G8BackendApplication.java
@@ -3,7 +3,7 @@
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 
-@SpringBootApplication
+@SpringBootApplication()
 public class G8BackendApplication {
 
     public static void main(String[] args) {
diff --git a/src/main/java/com/example/g8backend/config/JwtConfig.java b/src/main/java/com/example/g8backend/config/JwtConfig.java
new file mode 100644
index 0000000..f824797
--- /dev/null
+++ b/src/main/java/com/example/g8backend/config/JwtConfig.java
@@ -0,0 +1,24 @@
+package com.example.g8backend.config;
+
+
+import io.jsonwebtoken.security.Keys;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import javax.crypto.SecretKey;
+import java.nio.charset.StandardCharsets;
+
+@Configuration
+public class JwtConfig {
+    private static final String SECRET_KEY = "this-is-a-very-long-256-bit-secret-key-for-JWT"; //
+    private static final long EXPIRATION_MS = 3600_000; // 1小时
+
+    @Bean
+    public SecretKey jwtSecretKey() {
+        return Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8));
+    }
+
+    public long getExpirationMs() {
+        return EXPIRATION_MS;
+    }
+}
+
diff --git a/src/main/java/com/example/g8backend/config/RedisConfig.java b/src/main/java/com/example/g8backend/config/RedisConfig.java
new file mode 100644
index 0000000..023772a
--- /dev/null
+++ b/src/main/java/com/example/g8backend/config/RedisConfig.java
@@ -0,0 +1,21 @@
+package com.example.g8backend.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+@Configuration
+public class RedisConfig {
+
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(new StringRedisSerializer());
+        return template;
+    }
+}
diff --git a/src/main/java/com/example/g8backend/config/SecurityConfig.java b/src/main/java/com/example/g8backend/config/SecurityConfig.java
new file mode 100644
index 0000000..179d95f
--- /dev/null
+++ b/src/main/java/com/example/g8backend/config/SecurityConfig.java
@@ -0,0 +1,42 @@
+package com.example.g8backend.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import com.example.g8backend.filter.JwtAuthenticationFilter;
+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.configurers.AbstractHttpConfigurer;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+
+@Configuration
+@EnableWebSecurity
+public class SecurityConfig {
+    @Bean
+    public BCryptPasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+
+    private final JwtAuthenticationFilter jwtAuthenticationFilter;
+
+    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
+        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
+    }
+
+    @Bean
+    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+        return http
+            .csrf(AbstractHttpConfigurer::disable)
+            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
+            .build();
+    }
+
+    @Bean
+    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
+        return config.getAuthenticationManager();
+    }
+}
diff --git a/src/main/java/com/example/g8backend/controller/AuthController.java b/src/main/java/com/example/g8backend/controller/AuthController.java
new file mode 100644
index 0000000..2f36500
--- /dev/null
+++ b/src/main/java/com/example/g8backend/controller/AuthController.java
@@ -0,0 +1,92 @@
+package com.example.g8backend.controller;
+
+import com.example.g8backend.dto.UserRegisterDTO;
+import com.example.g8backend.entity.User;
+import com.example.g8backend.service.IUserService;
+import com.example.g8backend.util.JwtUtil;
+import com.example.g8backend.util.mailUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/auth")
+public class AuthController {
+
+    @Autowired
+    private IUserService userService;
+    @Autowired
+    private mailUtil mailUtil;
+    @Autowired
+    private PasswordEncoder passwordEncoder;
+    @Autowired
+    private JwtUtil jwtUtil;
+    @Autowired
+    RedisTemplate<String, Object> redisTemplate;
+
+    // 发送验证码
+    @PostMapping("/send_verification_code")
+    public ResponseEntity<?> sendVerificationCode(@RequestBody UserRegisterDTO registerDTO) {
+        if (userService.getUserByEmail(registerDTO.getEmail()) != null) {
+            return ResponseEntity.badRequest().body("邮箱已存在");
+        }
+
+        String verificationCode = UUID.randomUUID().toString().substring(0, 6);
+        mailUtil.sendMail(registerDTO.getEmail(), "PT平台注册验证码", "您的验证码为:" + verificationCode + ",验证码十分钟内有效,请勿泄露。");
+
+        redisTemplate.opsForValue().set(registerDTO.getEmail(), verificationCode, 10 * 60, java.util.concurrent.TimeUnit.SECONDS);
+        return ResponseEntity.ok("验证码发送成功");
+    }
+
+    // 用户注册
+    @PostMapping("/register")
+    public ResponseEntity<?> register(@RequestBody UserRegisterDTO registerDTO) {
+        if (userService.getUserByName(registerDTO.getUserName()) != null) {
+            return ResponseEntity.badRequest().body("用户名已存在");
+        }
+
+        if (!redisTemplate.hasKey(registerDTO.getInvitationCode())) {
+            return ResponseEntity.badRequest().body("邀请码错误");
+        }
+        if (!registerDTO.getVerificationCode().equals(redisTemplate.opsForValue().get(registerDTO.getEmail()))) {
+            return ResponseEntity.badRequest().body("验证码错误");
+        }
+        redisTemplate.delete(registerDTO.getEmail());
+
+        User user = new User();
+        user.setUserName(registerDTO.getUserName());
+        user.setPassword(passwordEncoder.encode(registerDTO.getPassword()));
+        user.setEmail(registerDTO.getEmail());
+
+        // passkey 用于在客户端发送announce请求时获取用户信息
+        user.setPasskey(UUID.randomUUID().toString().replace("-", ""));
+        userService.registerUser(user);
+
+        return ResponseEntity.ok("注册成功");
+    }
+
+    // 用户登录
+    @PostMapping("/login")
+    public ResponseEntity<?> login(@RequestBody User user) {
+        User existingUser = userService.getUserByEmail(user.getEmail());
+        if (existingUser == null || !passwordEncoder.matches(user.getPassword(), existingUser.getPassword())) {
+            return ResponseEntity.badRequest().body("用户名或密码错误");
+        }
+        String token = jwtUtil.generateToken(existingUser.getUserId());
+        Map<String, String> response = new HashMap<>();
+        response.put("token", token);
+        return ResponseEntity.ok(response);
+    }
+
+    @GetMapping("/test_redis")
+    public ResponseEntity<?> testRedis() {
+        redisTemplate.opsForValue().get("test");
+        return ResponseEntity.ok("test redis ok");
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/controller/TorrentController.java b/src/main/java/com/example/g8backend/controller/TorrentController.java
new file mode 100644
index 0000000..bec187e
--- /dev/null
+++ b/src/main/java/com/example/g8backend/controller/TorrentController.java
@@ -0,0 +1,46 @@
+package com.example.g8backend.controller;
+
+import com.example.g8backend.entity.User;
+import com.example.g8backend.service.IUserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import com.example.g8backend.service.ITorrentService;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.IOException;
+
+@RestController
+@RequestMapping("/torrent")
+public class TorrentController {
+    @Autowired
+    private ITorrentService torrentService;
+
+    @Autowired
+    private IUserService userService;
+
+    @RequestMapping("/upload")
+    public ResponseEntity<?> handleTorrentUpload(@RequestParam("file") MultipartFile multipartFile) throws IOException {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        long userId = (long) authentication.getPrincipal();
+
+        User user = userService.getById(userId);
+        String passkey = user.getPasskey();
+
+        File tempFile = File.createTempFile("upload-", ".torrent");
+        multipartFile.transferTo(tempFile);
+
+        torrentService.handleTorrentUpload(tempFile, userId, passkey);
+
+        // 删除临时文件
+        if(!tempFile.delete()){
+            throw new IOException("Failed to delete temporary file: " + tempFile.getAbsolutePath());
+        }
+        return ResponseEntity.ok("种子上传成功");
+    }
+}
diff --git a/src/main/java/com/example/g8backend/controller/TrackerController.java b/src/main/java/com/example/g8backend/controller/TrackerController.java
new file mode 100644
index 0000000..1511c76
--- /dev/null
+++ b/src/main/java/com/example/g8backend/controller/TrackerController.java
@@ -0,0 +1,33 @@
+package com.example.g8backend.controller;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import com.example.g8backend.service.ITrackerService;
+
+@RestController
+@RequestMapping("/tracker")
+public class TrackerController {
+
+    @Autowired
+    private ITrackerService trackerService;
+
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
+    @GetMapping("/announce/{passkey}")
+    public ResponseEntity<?> getAnnouncements(
+            @RequestParam(name = "info_hash") String infoHash,
+            @RequestParam(name = "peer_id") String peerId,
+            @RequestParam(name = "port") int port,
+            @RequestParam(name = "uploaded") long uploaded,
+            @RequestParam(name = "downloaded") long downloaded,
+            @RequestParam(name = "left") long left,
+            @RequestParam(name = "compact", required = false) int compact,
+            @RequestParam(name = "event", required = false) String event,
+            @PathVariable String passkey) {
+
+        return null;
+    }
+}
diff --git a/src/main/java/com/example/g8backend/controller/UserController.java b/src/main/java/com/example/g8backend/controller/UserController.java
index 0bbcdc6..2665b4c 100644
--- a/src/main/java/com/example/g8backend/controller/UserController.java
+++ b/src/main/java/com/example/g8backend/controller/UserController.java
@@ -3,52 +3,25 @@
 import com.example.g8backend.entity.User;
 import com.example.g8backend.service.IUserService;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.List;
-
 @RestController
-@RequestMapping("/users")
+@RequestMapping("/user")
 public class UserController {
 
     @Autowired
     private IUserService userService;
 
-    // 获取所有用户
+    // 获取已登录的用户信息
     @GetMapping
-    public List<User> getUsers() {
-        return userService.list();
-    }
-
-    // 通过ID获取用户
-    @GetMapping("/{id}")
-    public User getUserById(@PathVariable Long id) {
-        return userService.getById(id);
-    }
-
-    // 通过用户名获取用户
-    @GetMapping("/name/{name}")
-    public User getUserByName(@PathVariable String name) {
-        return userService.getUserByName(name);
-    }
-
-    // 添加用户
-    @PostMapping
-    public boolean addUser(@RequestBody User user) {
-        System.out.println(user);
-        return userService.save(user);
-    }
-
-    // 修改用户
-    @PutMapping
-    public boolean updateUser(@RequestBody User user) {
-        return userService.updateById(user);
-    }
-
-    // 删除用户
-    @DeleteMapping("/{id}")
-    public boolean deleteUser(@PathVariable Long id) {
-        return userService.removeById(id);
+    public ResponseEntity<?> getUserInfo(){
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        long userId = (long) authentication.getPrincipal();
+        User user = userService.getById(userId);
+        user.setPassword(null);
+        return ResponseEntity.ok(user);
     }
 }
-
diff --git a/src/main/java/com/example/g8backend/dto/UserRegisterDTO.java b/src/main/java/com/example/g8backend/dto/UserRegisterDTO.java
new file mode 100644
index 0000000..2214438
--- /dev/null
+++ b/src/main/java/com/example/g8backend/dto/UserRegisterDTO.java
@@ -0,0 +1,12 @@
+package com.example.g8backend.dto;
+
+import lombok.Data;
+
+@Data
+public class UserRegisterDTO {
+    private String userName;
+    private String password;
+    private String email;
+    private String verificationCode;
+    private String invitationCode;
+}
diff --git a/src/main/java/com/example/g8backend/entity/Peer.java b/src/main/java/com/example/g8backend/entity/Peer.java
new file mode 100644
index 0000000..b59f7ee
--- /dev/null
+++ b/src/main/java/com/example/g8backend/entity/Peer.java
@@ -0,0 +1,17 @@
+package com.example.g8backend.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("peers")
+public class Peer {
+    private Long peerId;
+    private String info_hash;
+    private String passkey;
+    private String ipAddress;
+    private Integer port;
+    private Double uploaded;
+    private Double downloaded;
+
+}
diff --git a/src/main/java/com/example/g8backend/entity/Torrent.java b/src/main/java/com/example/g8backend/entity/Torrent.java
new file mode 100644
index 0000000..cb9a4eb
--- /dev/null
+++ b/src/main/java/com/example/g8backend/entity/Torrent.java
@@ -0,0 +1,31 @@
+package com.example.g8backend.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+
+import java.sql.Timestamp;
+import lombok.Data;
+
+@Data
+@TableName("torrents")
+public class Torrent {
+    @TableId(type = IdType.AUTO)
+    private Long torrentId;
+    private Long userId;
+    private String torrentName;
+    private String infoHash;
+    private Double fileSize;
+
+    @Override
+    public String toString() {
+        return "Torrent{" +
+                "torrentId=" + torrentId +
+                ", userId=" + userId +
+                ", torrentName='" + torrentName + '\'' +
+                ", infoHash='" + infoHash + '\'' +
+                ", fileSize=" + fileSize +
+                '}';
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/entity/User.java b/src/main/java/com/example/g8backend/entity/User.java
index f367a4b..7ba8e03 100644
--- a/src/main/java/com/example/g8backend/entity/User.java
+++ b/src/main/java/com/example/g8backend/entity/User.java
@@ -1,28 +1,26 @@
 package com.example.g8backend.entity;
 
 import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
 
 @Data
-@TableName("user")  // 指定数据库表名
+@TableName("users")
 public class User {
     @TableId(type = IdType.AUTO)
-    private Long id;
+    private Long userId;
 
-    @TableField
-    private String name;
-
-    @TableField
+    private String passkey;
+    private String password;
+    private String userName;
     private String email;
 
     @Override
     public String toString() {
         return "User{" +
-                "id=" + id +
-                ", name='" + name + '\'' +
+                "id=" + userId +
+                ", name='" + userName + '\'' +
                 ", email='" + email + '\'' +
                 '}';
     }
diff --git a/src/main/java/com/example/g8backend/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/g8backend/filter/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..42c7e5b
--- /dev/null
+++ b/src/main/java/com/example/g8backend/filter/JwtAuthenticationFilter.java
@@ -0,0 +1,61 @@
+package com.example.g8backend.filter;
+
+import com.example.g8backend.util.JwtUtil;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.Collections;
+
+@Component
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+    private final JwtUtil jwtUtil;
+
+    public JwtAuthenticationFilter(JwtUtil jwtUtil) {
+        this.jwtUtil = jwtUtil;
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+            throws ServletException, IOException {
+        String path = request.getServletPath();
+        if (path.startsWith("/auth") || path.startsWith("/tracker")) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        String authHeader = request.getHeader("Authorization");
+        if (authHeader != null && authHeader.startsWith("Bearer ")) {
+            String token = authHeader.substring(7);
+            try {
+                Long userId = jwtUtil.validateTokenAndGetUserId(token);
+
+                UsernamePasswordAuthenticationToken authentication =
+                        new UsernamePasswordAuthenticationToken(userId, null, Collections.emptyList());
+
+                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+
+                // 设置用户认证信息到 Security 上下文
+                SecurityContextHolder.getContext().setAuthentication(authentication);
+            } catch (Exception e) {
+                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+                response.getWriter().write("Invalid or expired token");
+                return;
+            }
+        } else {
+            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+            response.getWriter().write("Authorization header not found");
+            return;
+        }
+
+        filterChain.doFilter(request, response);
+    }
+}
diff --git a/src/main/java/com/example/g8backend/mapper/PeerMapper.java b/src/main/java/com/example/g8backend/mapper/PeerMapper.java
new file mode 100644
index 0000000..ce84bbe
--- /dev/null
+++ b/src/main/java/com/example/g8backend/mapper/PeerMapper.java
@@ -0,0 +1,8 @@
+package com.example.g8backend.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.g8backend.entity.Peer;
+
+public interface PeerMapper extends BaseMapper<Peer> {
+
+}
diff --git a/src/main/java/com/example/g8backend/mapper/TorrentMapper.java b/src/main/java/com/example/g8backend/mapper/TorrentMapper.java
new file mode 100644
index 0000000..f78a68b
--- /dev/null
+++ b/src/main/java/com/example/g8backend/mapper/TorrentMapper.java
@@ -0,0 +1,16 @@
+package com.example.g8backend.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.g8backend.entity.Torrent;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+@Mapper
+public interface TorrentMapper extends BaseMapper<Torrent> {
+    int insertTorrent (@Param("userId") Long userId,
+                       @Param("torrentName") String torrentName,
+                       @Param("infoHash") String infoHash,
+                       @Param("fileSize") Double fileSize);
+    Torrent getTorrentByInfoHash (@Param("infoHash") String infoHash);
+    Torrent getTorrentByTorrentId (@Param("torrentId") Long torrentId);
+}
diff --git a/src/main/java/com/example/g8backend/mapper/UserMapper.java b/src/main/java/com/example/g8backend/mapper/UserMapper.java
index 65369cd..0a9e562 100644
--- a/src/main/java/com/example/g8backend/mapper/UserMapper.java
+++ b/src/main/java/com/example/g8backend/mapper/UserMapper.java
@@ -7,5 +7,7 @@
 
 @Mapper
 public interface UserMapper extends BaseMapper<User> {
-    User getUserByName(@Param("name") String name);
+    User getUserByName(@Param("userName") String userName);
+    User getUserByEmail(@Param("email") String email);
+    User getUserByPasskey(@Param("passkey") String passkey);
 }
diff --git a/src/main/java/com/example/g8backend/service/ITorrentService.java b/src/main/java/com/example/g8backend/service/ITorrentService.java
new file mode 100644
index 0000000..5e48dc4
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/ITorrentService.java
@@ -0,0 +1,13 @@
+package com.example.g8backend.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.example.g8backend.entity.Torrent;
+
+import java.io.File;
+import java.io.IOException;
+
+public interface ITorrentService extends IService<Torrent> {
+    Torrent handleTorrentUpload(File file, Long userId, String passkey) throws IOException;
+    Torrent findByInfoHash(String infoHash);
+    Torrent findByTorrentId(Long torrentId);
+}
diff --git a/src/main/java/com/example/g8backend/service/ITrackerService.java b/src/main/java/com/example/g8backend/service/ITrackerService.java
new file mode 100644
index 0000000..e522d77
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/ITrackerService.java
@@ -0,0 +1,6 @@
+package com.example.g8backend.service;
+
+
+public interface ITrackerService {
+
+}
diff --git a/src/main/java/com/example/g8backend/service/IUserService.java b/src/main/java/com/example/g8backend/service/IUserService.java
index 98b2810..f407c37 100644
--- a/src/main/java/com/example/g8backend/service/IUserService.java
+++ b/src/main/java/com/example/g8backend/service/IUserService.java
@@ -6,4 +6,7 @@
 
 public interface IUserService extends IService<User> {
     User getUserByName(@Param("name") String name);
+    User getUserByEmail(@Param("email") String email);
+    User getUserByPasskey(@Param("passkey") String passkey);
+    void registerUser(User user);;
 }
diff --git a/src/main/java/com/example/g8backend/service/UserServiceImpl.java b/src/main/java/com/example/g8backend/service/UserServiceImpl.java
deleted file mode 100644
index 2347878..0000000
--- a/src/main/java/com/example/g8backend/service/UserServiceImpl.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.example.g8backend.service;
-
-import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.example.g8backend.entity.User;
-import com.example.g8backend.mapper.UserMapper;
-import jakarta.annotation.Resource;
-import org.springframework.stereotype.Service;
-
-@Service
-public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
-    @Resource
-    private UserMapper userMapper; // 手动注入 UserMapper
-
-    @Override
-    public User getUserByName(String name) {
-        return userMapper.getUserByName(name); // 调用 UserMapper 的自定义 SQL
-    }
-}
diff --git a/src/main/java/com/example/g8backend/service/impl/TorrentServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/TorrentServiceImpl.java
new file mode 100644
index 0000000..03185ba
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/TorrentServiceImpl.java
@@ -0,0 +1,67 @@
+package com.example.g8backend.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.example.g8backend.entity.Torrent;
+import com.example.g8backend.mapper.TorrentMapper;
+import com.example.g8backend.service.ITorrentService;
+import com.example.g8backend.util.TorrentUtil;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+@Service
+public class TorrentServiceImpl  extends ServiceImpl<TorrentMapper, Torrent> implements ITorrentService {
+    @Resource
+    private TorrentMapper torrentMapper;
+
+    @Override
+    public Torrent handleTorrentUpload(File file, Long userId, String passkey) throws IOException{
+        String tracker = "http://127.0.0.1:8080/announce/" + passkey;
+
+        // 修改 announce 字段
+        byte[] modifiedBytes = TorrentUtil.injectTracker(file, tracker);
+
+        // 计算 info_hash
+        String infoHash = TorrentUtil.getInfoHash(file);
+
+        // 文件大小(以MB为单位)
+        double fileSize = file.length() / 1024.0 / 1024.0;
+
+        // 保存新的种子文件(可选)
+        File outputDir = new File("uploaded-torrents");
+        if (!outputDir.exists()) {
+            if (!outputDir.mkdirs()){
+                throw new IOException("Failed to create directory: " + outputDir.getAbsolutePath());
+            }
+        }
+
+        File savedFile = new File(outputDir, file.getName());
+        try (FileOutputStream fos = new FileOutputStream(savedFile)) {
+            fos.write(modifiedBytes);
+        }
+
+        // 插入数据库
+        torrentMapper.insertTorrent(userId, file.getName(), infoHash, fileSize);
+
+        // 构建返回实体
+        Torrent torrent = new Torrent();
+        torrent.setUserId(userId);
+        torrent.setTorrentName(file.getName());
+        torrent.setInfoHash(infoHash);
+        torrent.setFileSize(fileSize);
+        return torrent;
+    }
+
+    @Override
+    public Torrent findByInfoHash(String infoHash){
+        return torrentMapper.getTorrentByInfoHash(infoHash);
+    }
+
+    @Override
+    public Torrent findByTorrentId(Long torrentId){
+        return torrentMapper.getTorrentByTorrentId(torrentId);
+    }
+}
diff --git a/src/main/java/com/example/g8backend/service/impl/TrackerServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/TrackerServiceImpl.java
new file mode 100644
index 0000000..c0dba70
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/TrackerServiceImpl.java
@@ -0,0 +1,8 @@
+package com.example.g8backend.service.impl;
+
+import com.example.g8backend.service.ITrackerService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class TrackerServiceImpl implements ITrackerService {
+}
diff --git a/src/main/java/com/example/g8backend/service/impl/UserServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/UserServiceImpl.java
new file mode 100644
index 0000000..3f3357c
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/UserServiceImpl.java
@@ -0,0 +1,27 @@
+package com.example.g8backend.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.example.g8backend.entity.User;
+import com.example.g8backend.mapper.UserMapper;
+import com.example.g8backend.service.IUserService;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+
+
+@Service
+public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
+    @Resource
+    private UserMapper userMapper; // 手动注入 UserMapper
+
+    @Override
+    public User getUserByName(String name) { return userMapper.getUserByName(name);} // 调用 UserMapper 的自定义 SQL
+
+    @Override
+    public User getUserByEmail(String email) { return userMapper.getUserByEmail(email);}
+
+    @Override
+    public User getUserByPasskey(String passkey) { return userMapper.getUserByPasskey(passkey);}
+
+    @Override
+    public void registerUser(User user) {userMapper.insert(user);}
+}
diff --git a/src/main/java/com/example/g8backend/util/JwtUtil.java b/src/main/java/com/example/g8backend/util/JwtUtil.java
new file mode 100644
index 0000000..f11e678
--- /dev/null
+++ b/src/main/java/com/example/g8backend/util/JwtUtil.java
@@ -0,0 +1,45 @@
+package com.example.g8backend.util;
+
+import io.jsonwebtoken.*;
+import org.springframework.stereotype.Component;
+import javax.crypto.SecretKey;
+import java.util.Date;
+
+@Component
+public class JwtUtil {
+
+    private final SecretKey secretKey;
+    private final long expirationMs;
+
+    public JwtUtil(SecretKey secretKey) {
+        this.secretKey = secretKey;
+        this.expirationMs = 3600_000; // 1小时
+    }
+
+    // 生成 JWT Token
+    public String generateToken(long userId) {
+        Date now = new Date();
+        Date expiryDate = new Date(now.getTime() + expirationMs);
+
+        return Jwts.builder()
+                .claim("id", userId)
+                .issuedAt(now)
+                .expiration(expiryDate)
+                .signWith(secretKey, Jwts.SIG.HS256)
+                .compact();
+    }
+
+    // 验证Token并解析用户名
+    public Long validateTokenAndGetUserId(String token) {
+        try {
+            Jws<Claims> claims = Jwts.parser()
+                    .verifyWith(secretKey)
+                    .build()
+                    .parseSignedClaims(token);
+
+            return claims.getPayload().get("id", Long.class);
+        } catch (JwtException e) {
+            throw new RuntimeException("Token无效或过期", e);
+        }
+    }
+}
diff --git a/src/main/java/com/example/g8backend/util/TorrentUtil.java b/src/main/java/com/example/g8backend/util/TorrentUtil.java
new file mode 100644
index 0000000..f644fff
--- /dev/null
+++ b/src/main/java/com/example/g8backend/util/TorrentUtil.java
@@ -0,0 +1,66 @@
+package com.example.g8backend.util;
+
+import com.dampcake.bencode.Bencode;
+import com.dampcake.bencode.Type;
+
+import java.io.*;
+import java.security.MessageDigest;
+import java.util.Map;
+
+public class TorrentUtil {
+
+    private static final Bencode bencode = new Bencode();
+
+    public static byte[] injectTracker(File torrentFile, String trackerUrl) throws IOException {
+        byte[] fileBytes = readBytes(torrentFile);
+
+        Map<String, Object> torrentMap = bencode.decode(fileBytes, Type.DICTIONARY);
+
+        // trackerUrl: ip:port + /announce / {passkey}
+        torrentMap.put("announce", trackerUrl);
+
+        return bencode.encode(torrentMap);
+    }
+
+    public static String getInfoHash(File torrentFile) throws IOException {
+        byte[] fileBytes = readBytes(torrentFile);
+        Map<String, Object> torrentMap = bencode.decode(fileBytes, Type.DICTIONARY);
+
+        @SuppressWarnings("unchecked")
+        Map<String, Object> info = (Map<String, Object>) torrentMap.get("info");
+
+        // 对 info 字典重新编码
+        byte[] infoBytes = bencode.encode(info);
+
+        // 计算 SHA-1 hash
+        MessageDigest sha1;
+        try {
+            sha1 = MessageDigest.getInstance("SHA-1");
+        } catch (Exception e) {
+            throw new RuntimeException("SHA-1 not supported", e);
+        }
+
+        byte[] hash = sha1.digest(infoBytes);
+        return bytesToHex(hash);
+    }
+
+    public static void saveToFile(byte[] data, File outputFile) throws IOException {
+        try (FileOutputStream fos = new FileOutputStream(outputFile)) {
+            fos.write(data);
+        }
+    }
+
+    private static byte[] readBytes(File file) throws IOException {
+        try (InputStream in = new FileInputStream(file)) {
+            return in.readAllBytes();
+        }
+    }
+
+    private static String bytesToHex(byte[] hash) {
+        StringBuilder hex = new StringBuilder();
+        for (byte b : hash) {
+            hex.append(String.format("%02x", b));
+        }
+        return hex.toString();
+    }
+}
diff --git a/src/main/java/com/example/g8backend/util/mailUtil.java b/src/main/java/com/example/g8backend/util/mailUtil.java
new file mode 100644
index 0000000..5c8ad55
--- /dev/null
+++ b/src/main/java/com/example/g8backend/util/mailUtil.java
@@ -0,0 +1,24 @@
+package com.example.g8backend.util;
+
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.stereotype.Component;
+
+@Component
+public class mailUtil {
+
+    private final JavaMailSender javaMailSender;
+
+    public mailUtil(JavaMailSender javaMailSender) {
+        this.javaMailSender = javaMailSender;
+    }
+
+    public void sendMail(String to, String subject, String message) {
+        SimpleMailMessage mail = new SimpleMailMessage();
+        mail.setFrom("2038234690@qq.com");
+        mail.setTo(to);
+        mail.setSubject(subject);
+        mail.setText(message);
+        javaMailSender.send(mail);
+    }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index e912ce2..6bde80e 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,6 +1,20 @@
-spring.datasource.password=050301
+spring.datasource.password=123456
 spring.datasource.username=root
 spring.datasource.url=jdbc:mysql://localhost:3306/g8backend
 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
+spring.sql.init.mode=always
 
-mybatis-plus.mapper-locations=classpath*:/mapper/**/*.xml
\ No newline at end of file
+mybatis-plus.mapper-locations=classpath*:/mapper/**/*.xml
+
+spring.data.redis.host = 127.0.0.1
+spring.data.redis.port = 6379
+
+spring.mail.host=smtp.qq.com
+spring.mail.port=465
+spring.mail.username=2038234690@qq.com
+spring.mail.password=dixhyfaxjyauehaa
+spring.mail.default-encoding=utf-8
+spring.mail.protocol=smtp
+spring.mail.properties.mail.smtp.auth=true
+spring.mail.properties.mail.smtp.ssl.enable=true
+spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
diff --git a/src/main/resources/mapper/TorrentMapper.xml b/src/main/resources/mapper/TorrentMapper.xml
new file mode 100644
index 0000000..9b53d29
--- /dev/null
+++ b/src/main/resources/mapper/TorrentMapper.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="com.example.g8backend.mapper.TorrentMapper">
+    <insert id="insertTorrent" >
+        INSERT INTO torrents (user_id, torrent_name, info_hash, file_size)
+        VALUES (#{userId}, #{torrentName}, UNHEX(#{infoHash}), #{fileSize})
+    </insert>
+
+    <select id="getTorrentByInfoHash" resultType="com.example.g8backend.entity.Torrent">
+        SELECT
+            torrent_id,
+            user_id,
+            torrent_name,
+            HEX(info_hash) AS infoHash,
+            file_size
+        FROM torrents
+        WHERE info_hash = UNHEX(#{infoHash})
+    </select>
+
+    <select id="getTorrentByTorrentId" resultType="com.example.g8backend.entity.Torrent">
+        SELECT
+            torrent_id,
+            user_id,
+            torrent_name,
+            HEX(info_hash) AS infoHash,
+            file_size
+        FROM torrents
+        WHERE torrent_id = #{torrentId}
+    </select>
+</mapper>
\ No newline at end of file
diff --git a/src/main/resources/mapper/UserMapper.xml b/src/main/resources/mapper/UserMapper.xml
index e8ffb26..9627647 100644
--- a/src/main/resources/mapper/UserMapper.xml
+++ b/src/main/resources/mapper/UserMapper.xml
@@ -4,6 +4,12 @@
 
 <mapper namespace="com.example.g8backend.mapper.UserMapper">
     <select id="getUserByName" resultType="com.example.g8backend.entity.User">
-        SELECT * FROM user WHERE name = #{name}
+        SELECT * FROM users WHERE user_name = #{userName}
+    </select>
+    <select id="getUserByEmail" resultType="com.example.g8backend.entity.User">
+        SELECT * FROM users WHERE email = #{email}
+    </select>
+    <select id="getUserByPasskey" resultType="com.example.g8backend.entity.User">
+        SELECT * FROM users WHERE passkey = #{passkey}
     </select>
 </mapper>
\ No newline at end of file
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
new file mode 100644
index 0000000..365f9e7
--- /dev/null
+++ b/src/main/resources/schema.sql
@@ -0,0 +1,29 @@
+CREATE TABLE IF NOT EXISTS `users` (
+  user_id INT AUTO_INCREMENT PRIMARY KEY,
+  user_name VARCHAR(255) NOT NULL,
+  password VARCHAR(255) NOT NULL,
+  email VARCHAR(255) NOT NULL UNIQUE,
+  passkey VARCHAR(255) NOT NULL UNIQUE
+);
+
+CREATE TABLE IF NOT EXISTS `torrents` (
+    torrent_id INT AUTO_INCREMENT PRIMARY KEY,
+    user_id INT NOT NULL,
+    torrent_name VARCHAR(255) NOT NULL,
+    info_hash BINARY(20) NOT NULL,
+    file_size FLOAT NOT NULL,
+    FOREIGN KEY (user_id) REFERENCES users(user_id)
+);
+
+CREATE TABLE IF NOT EXISTS `peers` (
+  passkey VARCHAR(255) NOT NULL,
+  info_hash BINARY(20) NOT NULL,
+  peer_id VARCHAR(20) NOT NULL,
+  ip_address VARCHAR(128) NOT NULL,
+  port INT NOT NULL,
+  uploaded FLOAT NOT NULL,
+  downloaded FLOAT NOT NULL,
+  last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  FOREIGN KEY (passkey) REFERENCES users(passkey),
+  PRIMARY KEY (passkey, info_hash, peer_id)
+);
diff --git a/src/test/java/com/example/g8backend/G8BackendApplicationTests.java b/src/test/java/com/example/g8backend/G8BackendApplicationTests.java
index 7fb10ca..0385a17 100644
--- a/src/test/java/com/example/g8backend/G8BackendApplicationTests.java
+++ b/src/test/java/com/example/g8backend/G8BackendApplicationTests.java
@@ -1,13 +1,12 @@
 package com.example.g8backend;
 
 import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
 
-@SpringBootTest
+
 class G8BackendApplicationTests {
 
     @Test
     void contextLoads() {
     }
 
-}
+}
\ No newline at end of file
diff --git a/src/test/java/com/example/g8backend/util/JwtUtilTest.java b/src/test/java/com/example/g8backend/util/JwtUtilTest.java
new file mode 100644
index 0000000..1d2374d
--- /dev/null
+++ b/src/test/java/com/example/g8backend/util/JwtUtilTest.java
@@ -0,0 +1,49 @@
+package com.example.g8backend.util;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.MockitoAnnotations;
+import static org.junit.jupiter.api.Assertions.*;
+import io.jsonwebtoken.security.Keys;
+import javax.crypto.SecretKey;
+import java.security.SecureRandom;
+
+class JwtUtilTest {
+
+    @InjectMocks
+    private JwtUtil jwtUtil;
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+        byte[] keyBytes = new byte[32];
+        new SecureRandom().nextBytes(keyBytes);
+        SecretKey secretKey = Keys.hmacShaKeyFor(keyBytes);
+        jwtUtil = new JwtUtil(secretKey);
+    }
+
+    @Test
+    void testGenerateToken() {
+        long userId = 1L;
+        String token = jwtUtil.generateToken(userId);
+        assertNotNull(token);
+    }
+
+    @Test
+    void testValidateTokenAndGetUserId() {
+        long userId = 1L;
+        String token = jwtUtil.generateToken(userId);
+        long extractedUserId = jwtUtil.validateTokenAndGetUserId(token);
+        assertEquals(userId, extractedUserId);
+    }
+
+    @Test
+    void testValidateTokenAndGetUsername_InvalidToken() {
+        String invalidToken = "invalid.token.here";
+        Exception exception = assertThrows(RuntimeException.class, () ->
+                jwtUtil.validateTokenAndGetUserId(invalidToken)
+        );
+        assertTrue(exception.getMessage().contains("Token无效或过期"));
+    }
+}
diff --git a/uploaded-torrents/upload-1263176031835183570.torrent b/uploaded-torrents/upload-1263176031835183570.torrent
new file mode 100644
index 0000000..8634e79
--- /dev/null
+++ b/uploaded-torrents/upload-1263176031835183570.torrent
Binary files differ