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