Merge "添加Comment相关文件"
diff --git a/lib/ttorrent-bencoding-1.3.0-SNAPSHOT.jar b/lib/ttorrent-bencoding-1.3.0-SNAPSHOT.jar
new file mode 100644
index 0000000..3234e88
--- /dev/null
+++ b/lib/ttorrent-bencoding-1.3.0-SNAPSHOT.jar
Binary files differ
diff --git a/lib/ttorrent-cli-1.3.0-SNAPSHOT.jar b/lib/ttorrent-cli-1.3.0-SNAPSHOT.jar
new file mode 100644
index 0000000..1b51e32
--- /dev/null
+++ b/lib/ttorrent-cli-1.3.0-SNAPSHOT.jar
Binary files differ
diff --git a/lib/ttorrent-client-1.3.0-SNAPSHOT.jar b/lib/ttorrent-client-1.3.0-SNAPSHOT.jar
new file mode 100644
index 0000000..4fb3ba9
--- /dev/null
+++ b/lib/ttorrent-client-1.3.0-SNAPSHOT.jar
Binary files differ
diff --git a/lib/ttorrent-common-1.3.0-SNAPSHOT.jar b/lib/ttorrent-common-1.3.0-SNAPSHOT.jar
new file mode 100644
index 0000000..40e6ff1
--- /dev/null
+++ b/lib/ttorrent-common-1.3.0-SNAPSHOT.jar
Binary files differ
diff --git a/lib/ttorrent-network-1.0.jar b/lib/ttorrent-network-1.0.jar
new file mode 100644
index 0000000..d27eb9f
--- /dev/null
+++ b/lib/ttorrent-network-1.0.jar
Binary files differ
diff --git a/lib/ttorrent-tracker-1.3.0-SNAPSHOT.jar b/lib/ttorrent-tracker-1.3.0-SNAPSHOT.jar
new file mode 100644
index 0000000..4e5006a
--- /dev/null
+++ b/lib/ttorrent-tracker-1.3.0-SNAPSHOT.jar
Binary files differ
diff --git a/pom.xml b/pom.xml
index 22f8f1c..ea99d5b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,6 +11,7 @@
<groupId>com.pt5</groupId>
<artifactId>PT-houduan</artifactId>
<version>0.0.1-SNAPSHOT</version>
+<!-- <packaging>pom</packaging>-->
<name>PT-houduan</name>
<description>PT-houduan</description>
<url/>
@@ -30,13 +31,191 @@
<java.version>17</java.version>
</properties>
<dependencies>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-jpa</artifactId>
- </dependency>
+<!-- <dependency>-->
+<!-- <groupId>com.turn</groupId>-->
+<!-- <artifactId>ttorrent-client</artifactId>-->
+<!-- <version>1.3.0-SNAPSHOT</version>-->
+<!-- </dependency>-->
+<!-- <dependency>-->
+<!-- <groupId>com.turn</groupId>-->
+<!-- <artifactId>ttorrent-tracker</artifactId>-->
+<!-- <version>1.3.0-SNAPSHOT</version>-->
+<!-- </dependency>-->
+<!-- <dependency>-->
+<!-- <groupId>com.turn</groupId>-->
+<!-- <artifactId>ttorrent</artifactId>-->
+<!-- <version>1.3.0-SNAPSHOT</version>-->
+<!-- </dependency>-->
+
+
+<!-- <dependency>-->
+<!-- <groupId>org.springframework.boot</groupId>-->
+<!-- <artifactId>spring-boot-starter-data-jpa</artifactId>-->
+<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
+ <exclusions>
+ <exclusion>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-logging</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-logging</artifactId>
+ </dependency>
+
+ <!-- 这边开始是tracker的-->
+ <dependency>
+ <groupId>com.turn</groupId>
+ <artifactId>tracker</artifactId>
+ <version>1.3.0-SNAPSHOT</version>
+ <scope>system</scope>
+ <systemPath>${project.basedir}/lib/ttorrent-tracker-1.3.0-SNAPSHOT.jar</systemPath>
+ <exclusions>
+ <!-- 排除旧版本的 simpleframework -->
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>log4j</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>com.turn</groupId>
+ <artifactId>client</artifactId>
+ <version>1.3.0-SNAPSHOT</version>
+ <scope>system</scope>
+ <systemPath>${project.basedir}/lib/ttorrent-client-1.3.0-SNAPSHOT.jar</systemPath>
+ <exclusions>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>log4j</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+<!-- <dependency>-->
+<!-- <groupId>com.turn</groupId>-->
+<!-- <artifactId>cli</artifactId>-->
+<!-- <version>1.3.0-SNAPSHOT</version>-->
+<!-- <scope>system</scope>-->
+<!-- <systemPath>${project.basedir}/lib/ttorrent-cli-1.3.0-SNAPSHOT.jar</systemPath>-->
+<!-- <exclusions>-->
+<!-- <exclusion>-->
+<!-- <groupId>org.slf4j</groupId>-->
+<!-- <artifactId>*</artifactId>-->
+<!-- </exclusion>-->
+<!-- <exclusion>-->
+<!-- <groupId>log4j</groupId>-->
+<!-- <artifactId>*</artifactId>-->
+<!-- </exclusion>-->
+<!-- <exclusion>-->
+<!-- <groupId>ch.qos.logback</groupId>-->
+<!-- <artifactId>*</artifactId>-->
+<!-- </exclusion>-->
+<!-- </exclusions>-->
+<!-- </dependency>-->
+ <dependency>
+ <groupId>com.turn</groupId>
+ <artifactId>bencode</artifactId>
+ <version>1.3.0-SNAPSHOT</version>
+ <scope>system</scope>
+ <systemPath>${project.basedir}/lib/ttorrent-bencoding-1.3.0-SNAPSHOT.jar</systemPath>
+ <exclusions>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>log4j</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>com.turn</groupId>
+ <artifactId>commom</artifactId>
+ <version>1.3.0-SNAPSHOT</version>
+ <scope>system</scope>
+ <systemPath>${project.basedir}/lib/ttorrent-common-1.3.0-SNAPSHOT.jar</systemPath>
+ <exclusions>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>log4j</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>com.turn</groupId>
+ <artifactId>network</artifactId>
+ <version>1.3.0-SNAPSHOT</version>
+ <scope>system</scope>
+ <systemPath>${project.basedir}/lib/ttorrent-network-1.0.jar</systemPath>
+ <exclusions>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>log4j</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+<!-- <dependency>-->
+<!-- <groupId>org.simpleframework</groupId>-->
+<!-- <artifactId>simple-http</artifactId>-->
+<!-- <version>6.0.1</version>-->
+<!-- </dependency>-->
+<!-- <dependency>-->
+<!-- <groupId>org.simpleframework</groupId>-->
+<!-- <artifactId>simple-transport</artifactId>-->
+<!-- <version>6.0.1</version> <!– 使用最新稳定版本 –>-->
+<!-- </dependency>-->
+ <!-- 替换现有的 simple-transport 依赖 -->
+ <dependency>
+ <groupId>org.simpleframework</groupId>
+ <artifactId>simple</artifactId>
+ <version>5.1.6</version> <!-- 与 ttorrent 兼容的版本 -->
+ <exclusions>
+ <exclusion>
+ <groupId>*</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
</dependency>
<!-- MyBatis-Plus -->
@@ -51,6 +230,7 @@
<version>3.0.3</version>
</dependency>
+
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
@@ -77,11 +257,51 @@
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
+<!-- <dependency>-->
+<!-- <groupId>log4j</groupId>-->
+<!-- <artifactId>log4j</artifactId>-->
+<!-- <version>1.2.17</version>-->
+<!-- </dependency>-->
</dependencies>
<build>
<plugins>
<plugin>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-maven-plugin</artifactId>
+ <configuration>
+ <includeSystemScope>true</includeSystemScope> <!-- 包含system范围的依赖 -->
+ <executable>true</executable>
+ <layers>
+ <enabled>true</enabled>
+ </layers>
+ <excludes>
+ <exclude>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ </exclude>
+ </excludes>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-dependency-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>copy-dependencies</id>
+ <phase>prepare-package</phase>
+ <goals>
+ <goal>copy-dependencies</goal>
+ </goals>
+ <configuration>
+ <outputDirectory>${project.build.directory}/lib</outputDirectory>
+ <includeScope>system</includeScope> <!-- 专门复制system范围的依赖 -->
+ <excludeTransitive>true</excludeTransitive>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
@@ -93,19 +313,24 @@
</annotationProcessorPaths>
</configuration>
</plugin>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- <configuration>
- <excludes>
- <exclude>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- </exclude>
- </excludes>
- </configuration>
- </plugin>
</plugins>
+ <resources>
+ <resource>
+ <directory>src/main/resources</directory>
+ <filtering>true</filtering>
+ </resource>
+ <resource>
+ <directory>lib</directory>
+ <targetPath>BOOT-INF/lib/</targetPath>
+ <includes>
+ <include>**/*.jar</include>
+ </includes>
+ </resource>
+ </resources>
</build>
+<!-- <modules>-->
+<!-- <module>ttorrent</module>-->
+<!-- </modules>-->
+
</project>
diff --git a/src/main/java/com/pt5/pthouduan/config/TorrentClientMain.java b/src/main/java/com/pt5/pthouduan/config/TorrentClientMain.java
new file mode 100644
index 0000000..08216f0
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/config/TorrentClientMain.java
@@ -0,0 +1,35 @@
+package com.pt5.pthouduan.config;
+
+import com.turn.ttorrent.client.SimpleClient;
+
+import java.net.InetAddress;
+
+public class TorrentClientMain {
+
+ public static void main(String[] args) {
+ try {
+ // 1. 创建客户端对象
+ SimpleClient client = new SimpleClient();
+
+ // 2. 获取本地监听 IP(注意不能用 127.0.0.1,如果其他 peer 不在同一机器)
+ InetAddress address = InetAddress.getLocalHost();
+
+ // 3. 开始下载
+ client.downloadTorrent(
+ "D:\\torrenttest\\doenload\\video.mp4.torrent", // torrent 文件路径
+ "D://torrenttest/111111", // 下载输出目录
+ address // 本机地址
+ );
+
+ System.out.println("Download completed.");
+
+ // 4. 如果不想继续做 Seeder,可以停止客户端
+ client.stop();
+
+ } catch (Exception e) {
+ System.err.println("Download failed:");
+ e.printStackTrace();
+ }
+ }
+}
+
diff --git a/src/main/java/com/pt5/pthouduan/config/TorrentInfoHashPrinter.java b/src/main/java/com/pt5/pthouduan/config/TorrentInfoHashPrinter.java
new file mode 100644
index 0000000..73d0d23
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/config/TorrentInfoHashPrinter.java
@@ -0,0 +1,24 @@
+package com.pt5.pthouduan.config;
+
+import com.turn.ttorrent.tracker.TrackedTorrent;
+import java.io.File;
+
+public class TorrentInfoHashPrinter {
+ public static void main(String[] args) throws Exception {
+ File torrentFile = new File("D:\\torrenttest\\111111\\output.torrent");
+ TrackedTorrent torrent = TrackedTorrent.load(torrentFile);
+ byte[] infoHash = torrent.getInfoHash();
+
+ // 打印十六进制形式的 info_hash
+ System.out.println("info_hash (hex): " + bytesToHex(infoHash));
+ }
+
+ private static String bytesToHex(byte[] bytes) {
+ StringBuilder sb = new StringBuilder();
+ for (byte b : bytes) {
+ sb.append(String.format("%02X", b));
+ }
+ return sb.toString();
+ }
+}
+
diff --git a/src/main/java/com/pt5/pthouduan/config/TrackerConfig.java b/src/main/java/com/pt5/pthouduan/config/TrackerConfig.java
new file mode 100644
index 0000000..93784b5
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/config/TrackerConfig.java
@@ -0,0 +1,93 @@
+package com.pt5.pthouduan.config;
+
+import com.pt5.pthouduan.entity.StatsRecorder;
+import com.pt5.pthouduan.entity.TrackerInitializer;
+import com.pt5.pthouduan.entity.TrackeredTorrentWithStats;
+import com.pt5.pthouduan.service.impl.updatePeerStatsService;
+import com.turn.ttorrent.tracker.TorrentsRepository;
+import com.turn.ttorrent.tracker.TrackedTorrent;
+import com.turn.ttorrent.tracker.Tracker;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import org.springframework.beans.factory.annotation.Value;
+
+import javax.sql.DataSource;
+
+@Configuration
+public class TrackerConfig {
+
+ @Value("${tracker.port:6969}")
+ private int trackerPort;
+
+ @Value("${tracker.accept.foreign:false}")
+ private boolean acceptForeignTorrents;
+
+ @Autowired
+ private DataSource dataSource;
+ @Autowired
+ private TrackerInitializer trackerInitializer;
+ @Autowired
+ private updatePeerStatsService statsService;
+
+ @Bean
+ public Tracker tracker() throws IOException {
+ // 1. 创建 Tracker 实例
+ //Tracker tracker = new Tracker(6969);
+ //Tracker tracker = new Tracker(6969,"127.0.0.1");
+ Tracker tracker = trackerInitializer.createTracker();
+ // 4. 启动 Tracker(true 表示作为守护线程运行)
+ tracker.start(true);
+
+ // 2. 自动加载目录下的所有种子文件
+ File torrentsDirectory = new File("D:\\torrenttest\\111111");
+ if (torrentsDirectory.exists() && torrentsDirectory.isDirectory()) {
+ FilenameFilter filter = (dir, name) -> name.endsWith(".torrent");
+
+ for (File torrentFile : torrentsDirectory.listFiles(filter)) {
+ try {
+ TrackedTorrent baseTorrent = TrackedTorrent.load(torrentFile);
+ //获取HashInfo
+ byte[] infoHash = baseTorrent.getInfoHash();
+ System.out.println("Registered torrent hash: " + infoHash);
+ System.out.println("Registered torrent hash: " + baseTorrent.getHexInfoHash());
+ TrackeredTorrentWithStats trackedTorrentWithStats = new TrackeredTorrentWithStats(infoHash,dataSource,"",statsService);
+ tracker.announce(trackedTorrentWithStats);
+ //tracker.announce(TrackedTorrent.load(torrentFile));
+ System.out.println("Registered torrent: " + torrentFile.getName());
+ } catch (IOException e) {
+ System.err.println("Failed to load torrent: " + torrentFile.getName());
+ e.printStackTrace();
+ }
+ }
+ }
+
+ // 3. 设置是否接受未知种子
+ tracker.setAcceptForeignTorrents(true);
+
+
+
+ System.out.println("Tracker started on port " + trackerPort +
+ ", accepting foreign torrents: " + acceptForeignTorrents);
+
+ // 添加JVM关闭钩子确保Tracker正常停止
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ tracker.stop();
+ System.out.println("Tracker stopped gracefully");
+ }));
+
+ System.out.println("All registered torrents:");
+ for (TrackedTorrent t : tracker.getTrackedTorrents()) {
+ System.out.println(t.getHexInfoHash());
+ }
+
+
+ return tracker;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/pt5/pthouduan/config/WebMvcConfig.java b/src/main/java/com/pt5/pthouduan/config/WebMvcConfig.java
new file mode 100644
index 0000000..85145f4
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/config/WebMvcConfig.java
@@ -0,0 +1,23 @@
+package com.pt5.pthouduan.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+ @Override
+ public void addResourceHandlers(ResourceHandlerRegistry registry) {
+ // 帖子图片:D:/postuploads/ → /images/**
+ registry.addResourceHandler("/images/**")
+ .addResourceLocations("file:D:/postuploads/");//路径改一下
+
+ // ✅ 求助帖图片:D:/uploads/ → /uploads/**
+ registry.addResourceHandler("/uploads/**")
+ .addResourceLocations("file:D:/uploads/");
+ }
+
+
+}
+
diff --git a/src/main/java/com/pt5/pthouduan/controller/TorrentController.java b/src/main/java/com/pt5/pthouduan/controller/TorrentController.java
new file mode 100644
index 0000000..71cc76f
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/controller/TorrentController.java
@@ -0,0 +1,301 @@
+package com.pt5.pthouduan.controller;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.pt5.pthouduan.entity.PeerInfo;
+import com.pt5.pthouduan.entity.Torrent;
+import com.pt5.pthouduan.entity.TrackeredTorrentWithStats;
+import com.pt5.pthouduan.entity.User;
+import com.pt5.pthouduan.exception.TorrentNotFoundException;
+import com.pt5.pthouduan.mapper.UserMapper;
+import com.pt5.pthouduan.service.TorrentService;
+//import com.pt5.pthouduan.service.TorrentStatsService;
+//import com.pt5.pthouduan.service.TrackeredTorrentService;
+import com.pt5.pthouduan.service.impl.PeerService;
+//import com.pt5.pthouduan.service.impl.UserServiceImpl;
+import com.turn.ttorrent.bcodec.BDecoder;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import com.turn.ttorrent.tracker.Tracker;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.*;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.*;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+//@Controller
+@RestController
+@CrossOrigin // 允许前端跨域访问
+@RequestMapping("/torrent")
+public class TorrentController {
+ @Autowired
+ private TorrentService torrentService;
+ @Autowired
+ private UserMapper userMapper;
+// @Autowired
+// private UserServiceImpl userServiceImpl;
+ @Autowired
+ private Tracker tracker;
+ @Autowired
+ private PeerService peerService;
+// @Autowired
+// private TorrentStatsService statsService;
+// @Autowired
+// private TrackeredTorrentService trackeredTorrentService;
+
+
+ @GetMapping("/{infoHash}/seeders")
+ public ResponseEntity<List<PeerInfo>> getSeedersByInfoHash(@PathVariable String infoHash) {
+ List<PeerInfo> seeders = peerService.getSeedersByInfoHash(infoHash.toUpperCase());
+ System.out.println("Response: " + seeders);
+ return ResponseEntity.ok(seeders);
+ }
+ //添加搜索
+ @GetMapping("/search")
+ public ResponseEntity<List<Torrent>> searchTorrents(@RequestParam("keyword") String keyword) {
+ List<Torrent> result = torrentService.searchByKeyword(keyword);
+ return ResponseEntity.ok(result);
+ }
+
+ //显示所有种子
+ @GetMapping("/list")
+ public List<Torrent> getAllTorrents() {
+ return torrentService.getAllTorrents();
+ }
+ // 按分类获取种子(带 category 参数)
+ @GetMapping("/listByCategory")
+ public List<Torrent> getTorrentsByCategory(@RequestParam(required = false) Integer categoryid) {
+ if (categoryid == null) {
+ return torrentService.getAllTorrents();
+ }
+ return torrentService.getTorrentsByCategory(categoryid); // 否则按分类过滤
+ }
+ @GetMapping("/listByCategorywithfilter")
+ public List<Torrent> getTorrentsByCategorywithFilter(
+ @RequestParam Integer categoryid,
+ @RequestParam Map<String, String> filters
+ ){
+ filters.remove("categoryid");
+ List<Torrent> torrents = torrentService.getTorrentsByCategorywithfilters(categoryid,filters);
+ return torrents;
+ }
+ // 获取单个种子详情
+ @GetMapping("/{id}")
+ public ResponseEntity<?> getTorrentById(@PathVariable Long id) {
+ try {
+ Torrent torrent = torrentService.getTorrentById(id);
+ if (torrent == null) {
+ return ResponseEntity.notFound().build(); // 如果种子不存在,返回 404
+ }
+ System.out.println(torrent);
+ return ResponseEntity.ok(torrent); // 返回种子详情
+ } catch (Exception e) {
+ return ResponseEntity.badRequest().body("获取种子详情失败: " + e.getMessage());
+ }
+ }
+//未更新前端前
+// @PostMapping("/upload")
+// public ResponseEntity<?> uploadTorrent(
+// @RequestParam("file") MultipartFile torrentFile,
+// @RequestParam("title") String title,
+// @RequestParam("description") String description
+// //@AuthenticationPrincipal User user
+// //User user
+// ){
+// // 创建临时用户对象
+// User user = new User(1L,"testuser");
+// //user.setUserid(1L); // 设置测试用户ID
+// try{
+// //torrentService.Upload(torrentFile,title,description,user);
+// return torrentService.Upload(torrentFile, title, description, user);
+// }catch(Exception e){
+// return ResponseEntity.badRequest().body("Upload failed:" + e.getMessage());
+// }
+// }
+//@PostMapping("/upload")
+//public ResponseEntity<?> uploadTorrent(
+// @RequestParam("file") MultipartFile torrentFile,
+// @RequestParam("title") String title,
+// @RequestParam("description") String description,
+// @RequestParam("categoryId") Integer categoryId,
+// @RequestParam(value = "dpi",required = false) String dpi,
+// @RequestParam(value = "caption", required = false) String caption){
+// // 创建临时用户对象
+// User user = new User(1L,"testuser");
+// //user.setUserid(1L); // 设置测试用户ID
+// try{
+// //torrentService.Upload(torrentFile,title,description,user);
+// return torrentService.Upload(torrentFile, title, description, categoryId, dpi,caption,user);
+// }catch(Exception e){
+// return ResponseEntity.badRequest().body("Upload failed:" + e.getMessage());
+// }
+//}
+@PostMapping("/upload")
+public ResponseEntity<?> uploadTorrent(
+ @RequestParam("file") MultipartFile torrentFile,
+ @RequestParam("title") String title,
+ @RequestParam("description") String description,
+ @RequestParam("categoryId") Integer categoryId,
+ // 以下为通用扩展字段(根据类型选择性使用)
+ @RequestParam(value = "dpi",required = false) String dpi,
+ @RequestParam(value = "caption", required = false) String caption,
+ @RequestParam(value = "region", required = false) String region,
+ @RequestParam(value = "year", required = false) Integer year,
+ @RequestParam(value = "genre", required = false) String genre,
+ @RequestParam(value = "format", required = false) String format,
+ @RequestParam(value = "resolution", required = false) String resolution,
+ @RequestParam(value = "codecFormat", required = false) String codecFormat,
+ @RequestParam(value = "platform", required = false) String platform,
+ @RequestParam(value = "language", required = false) String language,
+ @RequestParam(value = "eventType", required = false) String eventType,
+ @RequestParam(value = "dataType", required = false) String dataType,
+ @RequestParam(value = "source", required = false) String source,
+ @RequestParam(value = "style", required = false) String style,
+ @RequestParam(value = "isMainland", required = false) Boolean isMainland){
+ // 创建临时用户对象
+ User user = new User(1L,"testuser");
+
+ // 构建扩展参数Map
+ Map<String, String> extraParams = new HashMap<>();
+
+ // 通用参数
+ putIfNotNull(extraParams, "dpi", dpi);
+ putIfNotNull(extraParams, "caption", caption);
+ putIfNotNull(extraParams, "region", region);
+ putIfNotNull(extraParams, "year", year != null ? year.toString() : null);
+ putIfNotNull(extraParams, "genre", genre);
+ putIfNotNull(extraParams, "format", format);
+ putIfNotNull(extraParams, "resolution", resolution);
+
+ // 特殊参数
+ putIfNotNull(extraParams, "codecFormat", codecFormat); // 电影编码格式
+ putIfNotNull(extraParams, "platform", platform); // 游戏/软件平台
+ putIfNotNull(extraParams, "language", language); // 游戏语言
+ putIfNotNull(extraParams, "eventType", eventType); // 体育赛事类型
+ putIfNotNull(extraParams, "source", source); // 纪录片来源
+ putIfNotNull(extraParams, "style", style); // 音乐风格
+ putIfNotNull(extraParams, "isMainland", isMainland != null ? isMainland.toString() : null); // 综艺是否大陆
+ putIfNotNull(extraParams, "dataType", dataType);
+// extraParams.put("region", "CN");
+// extraParams.put("year", "2023");
+// extraParams.put("genre", "Action");
+// extraParams.put("encodeFormat", "H.264");
+// extraParams.put("resolution", "1080p");
+
+
+ //user.setUserid(1L); // 设置测试用户ID
+ try{
+ //torrentService.Upload(torrentFile,title,description,user);
+ //return torrentService.Upload(torrentFile, title, description, categoryId, dpi,caption,user);
+
+// return torrentService.uploadWithCategory(
+// torrentFile, title, description, categoryId, user, dpi, caption,
+// region, year, genre, format, resolution, codecFormat,
+// platform, language, eventType, source, style, isMainland
+// );
+ // 调用Service
+ return torrentService.uploadWithCategory(
+ torrentFile,
+ title,
+ description,
+ categoryId,
+ user,
+ extraParams
+ );
+ }catch(Exception e){
+ return ResponseEntity.badRequest().body("Upload failed:" + e.getMessage());
+ }
+}
+ // 辅助方法:避免重复的空值判断
+ private void putIfNotNull(Map<String, String> map, String key, String value) {
+ if (value != null) {
+ map.put(key, value);
+ }
+ }
+
+// @GetMapping("/download/{id}")
+// public ResponseEntity<Resource> downloadTorrent(@PathVariable Long id, HttpServletRequest request){
+// try{
+// //获取种子文件资源
+// Torrent torrentFile = torrentService.getTorrentFile(id); //确定文件内容类型
+// String filePath = torrentFile.getFilePath();
+// File file = new File(filePath);
+// if(!file.exists()){
+// return ResponseEntity.notFound().build(); //如果文件不存在就返回异常
+// }
+// String contentType = request.getServletContext().getMimeType(torrentFile.getFilePath());
+// if(contentType == null){
+// contentType = "application/octet-stream"; //如果没有文件类型,默认使用二进制流
+// }
+// //构建resource对象
+// Resource resource = torrentService.loadTorrentFileAsResource(torrentFile.getFilepath()); //将文件加载为Spring的resource对象
+// return ResponseEntity.ok()
+// .contentType(MediaType.parseMediaType(contentType))
+// .header(HttpHeaders.CONTENT_DISPOSITION,"attachment;filename=\"" + resource.getFilename() + "\"")
+// .body(resource);
+// }catch(TorrentNotFoundException e){
+// return ResponseEntity.notFound().build();
+// }catch(Exception e){
+// return ResponseEntity.internalServerError().build();
+// }
+// }
+@GetMapping("/download/{id}")
+public ResponseEntity<byte[]> downloadTorrent(@PathVariable Long id, HttpServletRequest request){
+ try{
+ //需要加入passkey
+ User user = new User(1L,"testuser");
+ user.setPasskey("111111");
+ String passkey = "111111"; // 先模拟一下,到时候记得改
+ //获取种子文件资源
+ Torrent torrentFile = torrentService.getTorrentFile(id); //确定文件内容类型
+ // 2. 原子性更新下载次数(推荐直接通过 SQL 递增)
+ torrentService.incrementDownloadCount(id); // 在 Service 层实现原子更新
+ System.out.println("Requested ID: " + id);
+ System.out.println("Returned Torrent: " + torrentFile);
+ String filePath = torrentFile.getFilePath();
+ // 解码.torrent文件
+ FileInputStream fis = new FileInputStream(filePath);
+ Map<String, BEValue> decoded = BDecoder.bdecode(fis).getMap();
+ fis.close();
+
+ //替换announce字段
+ String announceUrl = "http://127.0.0.1:6969/announce?passkey=" + passkey;
+ decoded.put("announce", new BEValue(announceUrl));
+ System.out.println("Modified announce: " + decoded.get("announce"));
+
+ //重新编码为.torrent字节数组
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ BEncoder.bencode(new BEValue(decoded), baos);
+ byte[] modifiedTorrent = baos.toByteArray();
+
+ String outputPath = "D:/torrenttest/" + torrentFile.getFilename();
+ try(FileOutputStream fos = new FileOutputStream(outputPath)){
+ fos.write(modifiedTorrent);
+ }
+ System.out.println("Modified torrent bytes: " + Arrays.toString(modifiedTorrent));
+
+ // 构建响应,下载.torrent文件
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
+ headers.setContentDisposition(ContentDisposition.attachment()
+ .filename(torrentFile.getFilename())
+ .build());
+
+ return new ResponseEntity<>(modifiedTorrent, headers, HttpStatus.OK);
+ }catch (TorrentNotFoundException e) {
+ return ResponseEntity.notFound().build();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return ResponseEntity.internalServerError().build();
+ }
+}
+
+
+
+}
diff --git a/src/main/java/com/pt5/pthouduan/entity/PasskeyContext.java b/src/main/java/com/pt5/pthouduan/entity/PasskeyContext.java
new file mode 100644
index 0000000..6a3229f
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/entity/PasskeyContext.java
@@ -0,0 +1,17 @@
+package com.pt5.pthouduan.entity;
+
+public class PasskeyContext {
+ private static final ThreadLocal<String> passkeyHolder = new ThreadLocal<>();
+
+ public static void setPasskey(String passkey) {
+ passkeyHolder.set(passkey);
+ }
+
+ public static String getPasskey() {
+ return passkeyHolder.get();
+ }
+
+ public static void clear() {
+ passkeyHolder.remove();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pt5/pthouduan/entity/PasskeyTrackerProcessor.java b/src/main/java/com/pt5/pthouduan/entity/PasskeyTrackerProcessor.java
new file mode 100644
index 0000000..555dbd5
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/entity/PasskeyTrackerProcessor.java
@@ -0,0 +1,162 @@
+package com.pt5.pthouduan.entity;
+
+import com.pt5.pthouduan.service.PasskeyValidator;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.InvalidBEncodingException;
+import com.turn.ttorrent.common.protocol.TrackerMessage;
+import com.turn.ttorrent.common.protocol.http.HTTPAnnounceRequestMessage;
+import com.turn.ttorrent.common.protocol.http.HTTPTrackerErrorMessage;
+import com.turn.ttorrent.tracker.TrackerRequestProcessor;
+import com.turn.ttorrent.tracker.TorrentsRepository;
+//import org.simpleframework.http.Status;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+import java.io.IOException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+public class PasskeyTrackerProcessor extends TrackerRequestProcessor {
+
+ private final PasskeyValidator passkeyValidator;
+ private static final String[] NUMERIC_REQUEST_FIELDS = new String[]{
+ "port", "uploaded", "downloaded", "left", "compact", "no_peer_id", "numwant"
+ };
+
+ public PasskeyTrackerProcessor(TorrentsRepository torrentsRepository,
+ PasskeyValidator passkeyValidator) {
+ super(torrentsRepository);
+ this.passkeyValidator = passkeyValidator;
+ }
+
+ @Override
+ public void process(String uri, String hostAddress, RequestHandler requestHandler)
+ throws IOException {
+ try {
+ // 1. 使用自定义解析方法(已包含passkey检查)
+ ParsedAnnounceRequest parsedRequest = parseQueryWithPasskey(uri, hostAddress);
+ System.out.println(uri);
+
+ // 2. 直接获取passkey(已在parseQueryWithPasskey中确保存在)
+ String passkey = parsedRequest.getPasskey();
+
+ // 3. 验证passkey
+ if (!passkeyValidator.isValid(passkey)) {
+ System.out.println("Invalid passkey: " + passkey);
+ return ;
+ }
+ else {
+ PasskeyContext.setPasskey(passkey);
+ System.out.println("Valid passkey: " + passkey);
+ }
+ // 如果需要,可以将passkey存储在请求上下文中,供后续处理使用
+ // 例如,通过ThreadLocal或其他方式
+ // 4. 调用父类处理
+ // 注意:这里假设父类的process方法不需要直接访问passkey
+ // 如果需要,可能需要重构父类或传递passkey
+ super.process(uri, hostAddress, requestHandler);
+ } catch (Exception e) {
+ System.out.println(e.getMessage());
+ throw new RuntimeException("Failed to update peer stats", e); // 可根据需求决定是否抛出异常
+ }
+ }
+
+
+
+ /**
+ * 自定义解析逻辑,强制要求passkey参数,并返回解析后的请求对象
+ */
+ private ParsedAnnounceRequest parseQueryWithPasskey(String uri, String hostAddress)
+ throws IOException, TrackerMessage.MessageValidationException {
+
+ Map<String, BEValue> params = new HashMap<>();
+
+ try {
+ // 分割查询参数
+ String query = uri.contains("?") ? uri.split("\\?")[1] : "";
+ String[] pairs = query.split("&");
+
+ for (String pair : pairs) {
+ if (pair.isEmpty()) continue;
+
+ String[] kv = pair.split("=", 2);
+ String key = URLDecoder.decode(kv[0], StandardCharsets.ISO_8859_1.toString());
+ String value = kv.length > 1 ? URLDecoder.decode(kv[1], StandardCharsets.ISO_8859_1.toString()) : "";
+
+ // 处理数字型参数
+ if (Arrays.asList(NUMERIC_REQUEST_FIELDS).contains(key)) {
+ try {
+ params.put(key, new BEValue(Long.parseLong(value)));
+ } catch (NumberFormatException e) {
+ throw new TrackerMessage.MessageValidationException(
+ "Invalid numeric value for field: " + key);
+ }
+ } else {
+ params.put(key, new BEValue(value, StandardCharsets.ISO_8859_1.toString()));
+ }
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new TrackerMessage.MessageValidationException("Invalid request format");
+ }
+
+ // 强制检查passkey
+ if (!params.containsKey("passkey")) {
+ throw new TrackerMessage.MessageValidationException("passkey is required");
+ }
+
+ // 如果未提供IP,使用客户端IP
+ if (!params.containsKey("ip")) {
+ params.put("ip", new BEValue(hostAddress, StandardCharsets.ISO_8859_1.toString()));
+ }
+
+ // 创建并返回自定义的ParsedAnnounceRequest对象
+ return new ParsedAnnounceRequest(params);
+ }
+// private void serveError(HttpStatus status, HTTPTrackerErrorMessage error, RequestHandler requestHandler) throws IOException {
+// requestHandler.serveResponse(status.getCode(), status.getDescription(), error.getData());
+// }
+//
+// private void serveError(Status status, String error, RequestHandler requestHandler) throws IOException {
+// this.serveError(status, HTTPTrackerErrorMessage.craft(error), requestHandler);
+// }
+//
+// private void serveError(Status status, TrackerMessage.ErrorMessage.FailureReason reason, RequestHandler requestHandler) throws IOException {
+// this.serveError(status, reason.getMessage(), requestHandler);
+// }
+
+
+ /**
+ * 自定义类用于存储解析后的请求参数
+ */
+ private static class ParsedAnnounceRequest {
+ private final Map<String, BEValue> params;
+
+ public ParsedAnnounceRequest(Map<String, BEValue> params) {
+ this.params = params;
+ }
+
+ /**
+ * 获取passkey参数
+ */
+ public String getPasskey() throws InvalidBEncodingException {
+ BEValue passkeyValue = params.get("passkey");
+ if (passkeyValue == null) {
+ throw new IllegalStateException("passkey is missing");
+ }
+ return passkeyValue.getString();
+ }
+
+ /**
+ * 获取其他参数(如果需要)
+ */
+ public BEValue getParam(String key) {
+ return params.get(key);
+ }
+
+ // 可根据需要添加更多方法
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pt5/pthouduan/entity/PeerInfo.java b/src/main/java/com/pt5/pthouduan/entity/PeerInfo.java
new file mode 100644
index 0000000..440d870
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/entity/PeerInfo.java
@@ -0,0 +1,109 @@
+package com.pt5.pthouduan.entity;
+
+
+import java.sql.Timestamp;
+import java.time.LocalDateTime;
+
+public class PeerInfo {
+ private String username;
+ private long uploaded;
+ private long uploadSpeed;
+ private long downloaded;
+ private long downloadSpeed;
+ private String lastEvent;
+ private Timestamp lastUpdated;
+ private Timestamp createdAt;
+ private LocalDateTime completedtime;
+ private String client;
+ private int port;
+
+ // Getters and Setters
+ public int getPort() {
+ return port;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public long getUploaded() {
+ return uploaded;
+ }
+
+ public void setUploaded(long uploaded) {
+ this.uploaded = uploaded;
+ }
+
+ public long getUploadSpeed() {
+ return uploadSpeed;
+ }
+
+ public void setUploadSpeed(long uploadSpeed) {
+ this.uploadSpeed = uploadSpeed;
+ }
+
+ public long getDownloaded() {
+ return downloaded;
+ }
+
+ public void setDownloaded(long downloaded) {
+ this.downloaded = downloaded;
+ }
+
+ public long getDownloadSpeed() {
+ return downloadSpeed;
+ }
+
+ public void setDownloadSpeed(long downloadSpeed) {
+ this.downloadSpeed = downloadSpeed;
+ }
+
+ public String getLastEvent() {
+ return lastEvent;
+ }
+
+ public void setLastEvent(String lastEvent) {
+ this.lastEvent = lastEvent;
+ }
+
+ public Timestamp getLastUpdated() {
+ return lastUpdated;
+ }
+
+ public void setLastUpdated(Timestamp lastUpdated) {
+ this.lastUpdated = lastUpdated;
+ }
+
+ public Timestamp getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(Timestamp createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ public LocalDateTime getCompleted_time() {
+ return completedtime;
+ }
+
+ public void setCompleted_time(LocalDateTime completed_time) {
+ this.completedtime = completed_time;
+ }
+
+ public String getClient() {
+ return client;
+ }
+
+ public void setClient(String client) {
+ this.client = client;
+ }
+}
+
diff --git a/src/main/java/com/pt5/pthouduan/entity/StatsRecorder.java b/src/main/java/com/pt5/pthouduan/entity/StatsRecorder.java
new file mode 100644
index 0000000..eb59b15
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/entity/StatsRecorder.java
@@ -0,0 +1,22 @@
+package com.pt5.pthouduan.entity;
+
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.sql.DataSource;
+import java.sql.*;
+
+public interface StatsRecorder{
+ void recordStats(
+ String passkey,
+ String infoHash,
+ String ip,
+ int port,
+ String peerId,
+ long uploaded,
+ long downloaded
+ );
+}
diff --git a/src/main/java/com/pt5/pthouduan/entity/Torrent.java b/src/main/java/com/pt5/pthouduan/entity/Torrent.java
index 5c85587..fa078c8 100644
--- a/src/main/java/com/pt5/pthouduan/entity/Torrent.java
+++ b/src/main/java/com/pt5/pthouduan/entity/Torrent.java
@@ -3,7 +3,14 @@
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
import java.io.Serializable;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.security.NoSuchAlgorithmException;
+import java.time.LocalDateTime;
/**
* <p>
@@ -21,19 +28,129 @@
@TableId("torrentid")
private Long torrentid;
+ private Long uploaderid;
+
private Long promotionid;
private Integer categoryid;
- private String infoHash;
+ private String infohash;
- private String torrentTitle;
+ private String torrenttitle;
private String dpi;
private String caption;
- private byte[] torrentSize;
+ private Long torrentsize;
+
+ //private String announce;
+
+ private String description;
+
+ private LocalDateTime uploadtime;
+
+ private LocalDateTime lastseed;
+
+ private Long downloadCount = 0L;
+
+ private String filename;
+
+ private String path;
+
+
+
+
+ public void setFilename(String filename) {
+ this.filename = filename;
+ }
+ public String getFilepath() {
+ return path;
+ }
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public LocalDateTime getLastseed(){
+ return lastseed;
+ }
+ public void setLastseed(LocalDateTime lastseed){
+ this.lastseed = lastseed;
+ }
+
+
+
+ //private String createdBy;
+
+ public LocalDateTime getUploadTime() {
+ return uploadtime;
+ }
+ public void setUploadTime(LocalDateTime uploadTime) {
+ this.uploadtime = uploadTime;
+ }
+ public Long getDownloadCount() {
+ return downloadCount;
+ }
+ public void setDownloadCount(Long downloadCount) {
+ this.downloadCount = downloadCount;
+ }
+
+ public String getDescription(){
+ return description;
+ }
+ public void setDescription(String description){
+ this.description = description;
+ }
+
+
+ public Long getUploader_id(){
+ return uploaderid;
+ }
+
+ public void setUploader_id(Long uploader_id){
+ this.uploaderid = uploader_id;
+ }
+
+
+// public String getAnnounce() {
+// return announce;
+// }
+//
+// public void setAnnounce(String announce) {
+// this.announce = announce;
+// }
+
+// public static Torrent create(File source, URI announce, String createdBy)throws NoSuchAlgorithmException, IOException {
+// if (source == null || !source.exists()) {
+// throw new IllegalArgumentException("源文件不存在");
+// }
+// if (announce == null) {
+// throw new IllegalArgumentException("Announce URL不能为空");
+// }
+//
+// Torrent torrent = Torrent.create(source, announce, createdBy != null ? createdBy : "PT站点");
+//
+// //torrent.save(outputFile);
+// return torrent;
+// }
+// public void save(File outputFile) throws IOException{
+// if (outputFile == null ) {
+// throw new IllegalArgumentException("输出文件不能为空");
+// }
+// //确保目录存在
+// File parent = outputFile.getParentFile();
+// if (!parent.exists() || parent != null) {
+// parent.mkdirs();
+// }
+// //序列化并写入文件
+// try(FileOutputStream fos = new FileOutputStream(outputFile);){
+// byte[] data = torrent.encode();
+// fos.write(buffer.array(),buffer.arrayOffset(),buffer.remaining());
+// }
+// if(!outputFile.setReadable(true, false)){
+// System.err.println("警告:无法设置文件可读权限");
+// }
+// }
public Long getTorrentid() {
return torrentid;
@@ -60,19 +177,19 @@
}
public String getInfoHash() {
- return infoHash;
+ return infohash;
}
public void setInfoHash(String infoHash) {
- this.infoHash = infoHash;
+ this.infohash = infoHash;
}
public String getTorrentTitle() {
- return torrentTitle;
+ return torrenttitle;
}
public void setTorrentTitle(String torrentTitle) {
- this.torrentTitle = torrentTitle;
+ this.torrenttitle = torrentTitle;
}
public String getDpi() {
@@ -91,12 +208,12 @@
this.caption = caption;
}
- public byte[] getTorrentSize() {
- return torrentSize;
+ public Long getTorrentSize() {
+ return torrentsize;
}
- public void setTorrentSize(byte[] torrentSize) {
- this.torrentSize = torrentSize;
+ public void setTorrentSize(Long torrentSize) {
+ this.torrentsize = torrentSize;
}
@Override
@@ -105,11 +222,24 @@
"torrentid = " + torrentid +
", promotionid = " + promotionid +
", categoryid = " + categoryid +
- ", infoHash = " + infoHash +
- ", torrentTitle = " + torrentTitle +
+ ", infoHash = " + infohash +
+ ", torrentTitle = " + torrenttitle +
", dpi = " + dpi +
", caption = " + caption +
- ", torrentSize = " + torrentSize +
+ ", torrentSize = " + torrentsize +
"}";
}
+
+
+ public String getFilePath() {
+ return path;
+ }
+
+
+ public String getFilename() {
+ return filename;
+ }
+ public void setfilename(String filename) {
+ this.filename = filename;
+ }
}
diff --git a/src/main/java/com/pt5/pthouduan/entity/TrackerInitializer.java b/src/main/java/com/pt5/pthouduan/entity/TrackerInitializer.java
new file mode 100644
index 0000000..b9632ea
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/entity/TrackerInitializer.java
@@ -0,0 +1,40 @@
+package com.pt5.pthouduan.entity;
+
+import com.pt5.pthouduan.service.PasskeyValidator;
+import com.pt5.pthouduan.service.UserService;
+import com.turn.ttorrent.tracker.Tracker;
+import com.turn.ttorrent.tracker.TorrentsRepository;
+import com.turn.ttorrent.tracker.TrackerRequestProcessor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+
+@Service
+public class TrackerInitializer {
+
+ @Autowired
+ private PasskeyValidator passkeyValidator;
+
+ public Tracker createTracker() throws IOException {
+ TorrentsRepository repository = new TorrentsRepository(1000);
+
+ // 1. 创建自定义的Processor
+ TrackerRequestProcessor processor = new PasskeyTrackerProcessor(
+ repository,
+ passkeyValidator
+ );
+
+ // 2. 使用四参数构造器
+ Tracker tracker = new Tracker(
+ 6969, // 端口(与Nginx配置一致)
+ "http://127.0.0.1/announce", // 对外公告地址
+ processor, // 自定义的Processor
+ repository // Torrent仓库
+ );
+ tracker.setAnnounceInterval(100);
+
+ tracker.setAcceptForeignTorrents(false); // 禁止未注册的种子
+ return tracker;
+ }
+}
diff --git a/src/main/java/com/pt5/pthouduan/entity/TrackeredTorrentWithStats.java b/src/main/java/com/pt5/pthouduan/entity/TrackeredTorrentWithStats.java
new file mode 100644
index 0000000..6f015d2
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/entity/TrackeredTorrentWithStats.java
@@ -0,0 +1,346 @@
+package com.pt5.pthouduan.entity;
+
+import com.pt5.pthouduan.mapper.UserMapper;
+import com.pt5.pthouduan.service.impl.updatePeerStatsService;
+import com.turn.ttorrent.common.PeerUID;
+import com.turn.ttorrent.common.protocol.AnnounceRequestMessage;
+import com.turn.ttorrent.tracker.TrackedPeer;
+import com.turn.ttorrent.tracker.TrackedTorrent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.sql.DataSource;
+import java.io.UnsupportedEncodingException;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+//import static com.pt5.pthouduan.entity.CustomTorrentsRepository.hexToBytes;
+
+public class TrackeredTorrentWithStats extends TrackedTorrent{
+ private static final Logger logger = LoggerFactory.getLogger(TrackeredTorrentWithStats.class);
+ private final ConcurrentMap<PeerUID,long[]> lastStats = new ConcurrentHashMap<>();
+ private final DataSource dataSource;
+ private final String passkey;
+ private final updatePeerStatsService statsService;
+ public TrackeredTorrentWithStats(byte[] infoHash, DataSource datasource, String passkey, updatePeerStatsService statsService){
+ super(infoHash);
+ this.dataSource = datasource;
+ this.passkey = passkey;
+ this.statsService = statsService;
+ }
+
+ @Override
+ public TrackedPeer update(AnnounceRequestMessage.RequestEvent event, ByteBuffer peerId, String hexPeerId, String ip, int port, long uploaded, long downloaded, long left)
+ throws UnsupportedEncodingException{
+ System.out.println("Peer update triggered!");
+ String passkey = PasskeyContext.getPasskey();
+ System.out.println("AcceptedPasskey: " + passkey);
+ PeerUID peerUID = new PeerUID(new InetSocketAddress(ip, port),this.getHexInfoHash());
+ long[] last = lastStats.getOrDefault(peerUID, new long[]{0L,0L});
+ long deltaUpload = Math.max(0,uploaded - last[0]);
+ long deltaDownloaded = Math.max(0,downloaded - last[1]);
+ lastStats.put(peerUID,new long[]{uploaded,downloaded});
+ //updatePeerStatsInDB(this.getHexInfoHash(),ip,port,hexPeerId,uploaded,downloaded,deltaUpload,deltaDownloaded,passkey);
+ try {
+ // 调用服务层更新统计
+ statsService.updatePeerStatsInDB(
+ this.getHexInfoHash(),
+ ip,
+ port,
+ hexPeerId,
+ uploaded,
+ downloaded,
+ deltaUpload,
+ deltaDownloaded,
+ passkey,
+ event.name(), // 加上 event 类型
+ left // 用于判断是否完成下载
+ );
+ // 新增:更新 Torrent 的最晚做种时间
+ updateLastSeedTimeInDB(this.getHexInfoHash().toLowerCase());
+ // 2. 插入流水表记录
+ String sql = "INSERT INTO user_traffic_log " +
+ "(info_hash, ip, port, hex_peer_id, uploaded, downloaded, delta_upload, delta_download, passkey, event, lefted) " +
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql)) {
+ ps.setString(1, this.getHexInfoHash());
+ ps.setString(2, ip);
+ ps.setInt(3, port);
+ ps.setString(4, hexPeerId);
+ ps.setLong(5, uploaded);
+ ps.setLong(6, downloaded);
+ ps.setLong(7, deltaUpload);
+ ps.setLong(8, deltaDownloaded);
+ ps.setString(9, passkey);
+ ps.setString(10, event.name());
+ ps.setLong(11, left);
+ ps.executeUpdate();
+ }
+ } catch (Exception e) {
+ // 使用日志框架记录错误
+ logger.error("Failed to update peer stats", e);
+ throw new RuntimeException("Failed to update peer stats", e); // 可根据需求决定是否抛出异常
+ }
+ System.out.println("接收到 peer announce: " + ip + ":" + port + ", uploaded=" + uploaded + ", downloaded=" + downloaded);
+ logger.debug("Peer updated: {}:{} (up={}, down={}, Δup={}, Δdown={})",
+ ip, port, uploaded, downloaded, deltaUpload, deltaDownloaded);
+ return super.update(event,peerId,hexPeerId,ip,port,uploaded,downloaded,left);
+ }
+ // 新增方法:更新 Torrent 的最晚做种时间
+ private void updateLastSeedTimeInDB(String infoHash) {
+ String sql = "UPDATE torrent SET last_seed = now() WHERE info_hash = ?";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement stmt = conn.prepareStatement(sql)) {
+ System.out.println("Current info_hash: " + infoHash);
+
+ stmt.setString(1, infoHash);
+ stmt.executeUpdate();
+ System.out.println("更新最后做种时间");
+
+ } catch (SQLException e) {
+ logger.error("Failed to update last_seed_time for info_hash: {}", infoHash, e);
+ }
+ }
+
+//package com.pt5.pthouduan.entity;
+//
+//import com.pt5.pthouduan.mapper.UserMapper;
+//import com.pt5.pthouduan.service.impl.updatePeerStatsService;
+//import com.turn.ttorrent.common.PeerUID;
+//import com.turn.ttorrent.common.protocol.AnnounceRequestMessage;
+//import com.turn.ttorrent.tracker.TrackedPeer;
+//import com.turn.ttorrent.tracker.TrackedTorrent;
+//import org.slf4j.Logger;
+//import org.slf4j.LoggerFactory;
+//
+//import javax.sql.DataSource;
+//import java.io.UnsupportedEncodingException;
+//import java.net.InetSocketAddress;
+//import java.nio.ByteBuffer;
+//import java.sql.Connection;
+//import java.sql.PreparedStatement;
+//import java.sql.SQLException;
+//import java.util.concurrent.ConcurrentHashMap;
+//import java.util.concurrent.ConcurrentMap;
+//
+////import static com.pt5.pthouduan.entity.CustomTorrentsRepository.hexToBytes;
+//
+//public class TrackeredTorrentWithStats extends TrackedTorrent{
+// private static final Logger logger = LoggerFactory.getLogger(TrackeredTorrentWithStats.class);
+// private final ConcurrentMap<PeerUID,long[]> lastStats = new ConcurrentHashMap<>();
+// private final DataSource dataSource;
+// private final String passkey;
+// private final updatePeerStatsService statsService;
+// public TrackeredTorrentWithStats(byte[] infoHash, DataSource datasource, String passkey, updatePeerStatsService statsService){
+// super(infoHash);
+// this.dataSource = datasource;
+// this.passkey = passkey;
+// this.statsService = statsService;
+// }
+//
+// @Override
+// public TrackedPeer update(AnnounceRequestMessage.RequestEvent event, ByteBuffer peerId, String hexPeerId, String ip, int port, long uploaded, long downloaded, long left)
+// throws UnsupportedEncodingException{
+// System.out.println("Peer update triggered!");
+// String passkey = PasskeyContext.getPasskey();
+// System.out.println("AcceptedPasskey: " + passkey);
+// PeerUID peerUID = new PeerUID(new InetSocketAddress(ip, port),this.getHexInfoHash());
+// long[] last = lastStats.getOrDefault(peerUID, new long[]{0L,0L});
+// long deltaUpload = Math.max(0,uploaded - last[0]);
+// long deltaDownloaded = Math.max(0,downloaded - last[1]);
+// lastStats.put(peerUID,new long[]{uploaded,downloaded});
+// //updatePeerStatsInDB(this.getHexInfoHash(),ip,port,hexPeerId,uploaded,downloaded,deltaUpload,deltaDownloaded,passkey);
+// try {
+// // 调用服务层更新统计
+// statsService.updatePeerStatsInDB(
+// this.getHexInfoHash(),
+// ip,
+// port,
+// hexPeerId,
+// uploaded,
+// downloaded,
+// deltaUpload,
+// deltaDownloaded,
+// passkey
+// );
+// // 新增:更新 Torrent 的最晚做种时间
+// updateLastSeedTimeInDB(this.getHexInfoHash().toLowerCase());
+// } catch (Exception e) {
+// // 使用日志框架记录错误
+// logger.error("Failed to update peer stats", e);
+// throw new RuntimeException("Failed to update peer stats", e); // 可根据需求决定是否抛出异常
+// }
+// System.out.println("接收到 peer announce: " + ip + ":" + port + ", uploaded=" + uploaded + ", downloaded=" + downloaded);
+// logger.debug("Peer updated: {}:{} (up={}, down={}, Δup={}, Δdown={})",
+// ip, port, uploaded, downloaded, deltaUpload, deltaDownloaded);
+// return super.update(event,peerId,hexPeerId,ip,port,uploaded,downloaded,left);
+// }
+// // 新增方法:更新 Torrent 的最晚做种时间
+// private void updateLastSeedTimeInDB(String infoHash) {
+// String sql = "UPDATE torrent SET last_seed = now() WHERE info_hash = ?";
+// try (Connection conn = dataSource.getConnection();
+// PreparedStatement stmt = conn.prepareStatement(sql)) {
+// System.out.println("Current info_hash: " + infoHash);
+//
+// stmt.setString(1, infoHash);
+// stmt.executeUpdate();
+// System.out.println("更新最后做种时间");
+//
+// } catch (SQLException e) {
+// logger.error("Failed to update last_seed_time for info_hash: {}", infoHash, e);
+// }
+// }
+
+// private void updatePeerStatsInDB(String infoHash, String ip, int port, String peerId, long uploaded, long downloaded,long deltaUpload, long deltaDownload,String passkey) {
+// double shareRatio = downloaded == 0 ? 0.0 : (double) uploaded / downloaded;
+// UserMapper userMapper;
+// String sql = """
+// INSERT INTO peer_stats (info_hash, ip, port, peer_id, uploaded, downloaded, delta_upload, delta_download,share_ratio,passkey)
+// VALUES (?, ?, ?, ?, ?, ?, ?,?,?,?)
+// ON DUPLICATE KEY UPDATE
+// uploaded = VALUES(uploaded),
+// downloaded = VALUES(downloaded),
+// delta_upload = VALUES(delta_upload),
+// delta_download = VALUES(delta_download),
+// share_ratio = VALUES(share_ratio),
+// last_updated = CURRENT_TIMESTAMP,
+// passkey = passkey;
+// """;
+// try (Connection conn = dataSource.getConnection();
+// PreparedStatement stmt = conn.prepareStatement(sql)) {
+//
+// stmt.setString(1, infoHash);
+// stmt.setString(2, ip);
+// stmt.setInt(3, port);
+// stmt.setString(4, peerId);
+// stmt.setLong(5, uploaded);
+// stmt.setLong(6, downloaded);
+// stmt.setLong(7, deltaUpload);
+// stmt.setLong(8, deltaDownload);
+// stmt.setDouble(9, shareRatio);
+// //stmt.setString(10,this.passkey);
+// stmt.setString(10,passkey);
+// stmt.executeUpdate();
+// userMapper.incrementUserTraffic(passkey, deltaUpload, deltaDownload);
+//
+// } catch (SQLException e) {
+// e.printStackTrace(); // 建议换成日志记录
+// }
+//
+// }
+}
+//package com.pt5.pthouduan.entity;
+//
+//import com.turn.ttorrent.common.PeerUID;
+//import com.turn.ttorrent.common.protocol.AnnounceRequestMessage;
+//import com.turn.ttorrent.tracker.TrackedPeer;
+//import com.turn.ttorrent.tracker.TrackedTorrent;
+//import org.slf4j.Logger;
+//import org.slf4j.LoggerFactory;
+//
+//import javax.sql.DataSource;
+//import java.io.UnsupportedEncodingException;
+//import java.net.InetSocketAddress;
+//import java.nio.ByteBuffer;
+//import java.sql.Connection;
+//import java.sql.PreparedStatement;
+//import java.sql.SQLException;
+//import java.util.concurrent.ConcurrentHashMap;
+//import java.util.concurrent.ConcurrentMap;
+//
+//public class TrackeredTorrentWithStats extends TrackedTorrent {
+// private static final Logger logger = LoggerFactory.getLogger(TrackeredTorrentWithStats.class);
+//
+// private final ConcurrentMap<PeerUID, long[]> lastStats = new ConcurrentHashMap<>();
+// private final DataSource dataSource;
+// private final String passkey; // 新增passkey字段
+//
+// // 修改构造函数,增加passkey参数
+// public TrackeredTorrentWithStats(byte[] infoHash, DataSource datasource, String passkey) {
+// super(infoHash);
+// this.dataSource = datasource;
+// this.passkey = passkey;
+// }
+//
+// @Override
+// public TrackedPeer update(AnnounceRequestMessage.RequestEvent event,
+// ByteBuffer peerId, String hexPeerId,
+// String ip, int port,
+// long uploaded, long downloaded, long left)
+// throws UnsupportedEncodingException {
+// PeerUID peerUID = new PeerUID(new InetSocketAddress(ip, port), this.getHexInfoHash());
+//
+// // 计算增量流量(防止负数)
+// long[] last = lastStats.getOrDefault(peerUID, new long[]{0L, 0L});
+// long deltaUpload = Math.max(0, uploaded - last[0]);
+// long deltaDownload = Math.max(0, downloaded - last[1]);
+//
+// // 更新内存中的最新值
+// lastStats.put(peerUID, new long[]{uploaded, downloaded});
+//
+// // 记录到数据库(新增passkey字段)
+// updatePeerStatsInDB(
+// this.getHexInfoHash(),
+// ip, port, hexPeerId,
+// uploaded, downloaded,
+// deltaUpload, deltaDownload
+// );
+//
+// logger.debug("Peer updated: {}:{} (up={}, down={}, Δup={}, Δdown={})",
+// ip, port, uploaded, downloaded, deltaUpload, deltaDownload);
+//
+// return super.update(event, peerId, hexPeerId, ip, port, uploaded, downloaded, left);
+// }
+//
+// private void updatePeerStatsInDB(String infoHash, String ip, int port,
+// String peerId, long uploaded, long downloaded,
+// long deltaUpload, long deltaDownload) {
+// double shareRatio = (downloaded == 0) ? 0.0 : (double) uploaded / downloaded;
+//
+// // 修改SQL,增加passkey字段
+// String sql = """
+// INSERT INTO peer_stats (
+// info_hash, ip, port, peer_id,
+// uploaded, downloaded, delta_upload, delta_download,
+// share_ratio, passkey
+// ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+// ON DUPLICATE KEY UPDATE
+// uploaded = VALUES(uploaded),
+// downloaded = VALUES(downloaded),
+// delta_upload = VALUES(delta_upload),
+// delta_download = VALUES(delta_download),
+// share_ratio = VALUES(share_ratio),
+// last_updated = CURRENT_TIMESTAMP
+// """;
+//
+// try (Connection conn = dataSource.getConnection();
+// PreparedStatement stmt = conn.prepareStatement(sql)) {
+//
+// // 设置参数(新增passkey)
+// stmt.setString(1, infoHash);
+// stmt.setString(2, ip);
+// stmt.setInt(3, port);
+// stmt.setString(4, peerId);
+// stmt.setLong(5, uploaded);
+// stmt.setLong(6, downloaded);
+// stmt.setLong(7, deltaUpload);
+// stmt.setLong(8, deltaDownload);
+// stmt.setDouble(9, shareRatio);
+// stmt.setString(10, this.passkey); // 新增passkey
+//
+// stmt.executeUpdate();
+//
+// } catch (SQLException e) {
+// logger.error("Failed to update peer stats: {}", e.getMessage());
+// }
+// }
+//}
\ No newline at end of file
diff --git a/src/main/java/com/pt5/pthouduan/exception/TorrentNotFoundException.java b/src/main/java/com/pt5/pthouduan/exception/TorrentNotFoundException.java
new file mode 100644
index 0000000..14a7334
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/exception/TorrentNotFoundException.java
@@ -0,0 +1,8 @@
+package com.pt5.pthouduan.exception;
+
+
+public class TorrentNotFoundException extends RuntimeException {
+ public TorrentNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/pt5/pthouduan/mapper/TorrentMapper.java b/src/main/java/com/pt5/pthouduan/mapper/TorrentMapper.java
index 9c7b608..e7ba2a3 100644
--- a/src/main/java/com/pt5/pthouduan/mapper/TorrentMapper.java
+++ b/src/main/java/com/pt5/pthouduan/mapper/TorrentMapper.java
@@ -3,6 +3,11 @@
import com.pt5.pthouduan.entity.Torrent;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+import java.util.Map;
/**
* <p>
@@ -14,5 +19,23 @@
*/
@Mapper
public interface TorrentMapper extends BaseMapper<Torrent> {
+ // 仅保留方法声明,移除所有SQL注解
+ List<Torrent> selectByUploaderId(@Param("uploaderId") Long uploaderId);
+ List<Torrent> searchByName(@Param("keyword") String keyword);
+ List<Torrent> selectTopPopular();
+ List<Torrent> getAllTorrents();
+ Integer existsByInfoHash(@Param("infoHash") String infoHash);
+ void incrementDownloadCount(@Param("id") Long id);
+ void save(Torrent entity);
+ List<Torrent> selectTorrentsByCategory(Integer category);
+ Torrent selectById(@Param("torrentid") Long id);
+ List<Torrent> searchByKeyword(String keyword);
+ void setFreePromotion(); // 免费下载
+ void setDoubleUpload(); // 上传加倍
+ void setHalfDownload(); // 下载减半
+ void clearPromotion(); // 恢复促销
+ Torrent selectByinfohash(@Param("infohash") String infohash);
+
+ List<Torrent> listByCategoryWithFilters(Integer categoryid, Map<String, String> filters, String extendTable);
}
diff --git a/src/main/java/com/pt5/pthouduan/mapper/UserMapper.java b/src/main/java/com/pt5/pthouduan/mapper/UserMapper.java
index 89c37ab..2f96180 100644
--- a/src/main/java/com/pt5/pthouduan/mapper/UserMapper.java
+++ b/src/main/java/com/pt5/pthouduan/mapper/UserMapper.java
@@ -1,11 +1,21 @@
package com.pt5.pthouduan.mapper;
+import com.pt5.pthouduan.entity.PeerInfo;
import com.pt5.pthouduan.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.*;
import org.springframework.stereotype.Repository;
+import java.util.List;
+/**
+ * <p>
+ * Mapper 接口
+ * </p>
+ *
+ * @author ljx
+ * @since 2025-04-14
+ */
@Repository
public interface UserMapper extends BaseMapper<User> {
@Insert("INSERT INTO user(username, password, email, passkey) VALUES(#{username}, #{password}, #{email}, #{passkey})")
@@ -17,6 +27,15 @@
@Select("SELECT * FROM User WHERE email = #{email}")
User selectByEmail(String email);
+ @Select("SELECT credit FROM User WHERE username = #{username}")
+ int getcreditByUsername(String username);
+
+ @Update("UPDATE user SET credit = credit - #{price} WHERE username = #{username}")
+ int deductCreditByUsername(@Param("username") String username, @Param("price") int price);
+
+ @Update("UPDATE user SET upload = upload + #{upload} WHERE username = #{username}")
+ int increaseUploadByUsername(@Param("username") String username, @Param("upload") Integer upload);
+
@Update("UPDATE user SET password = #{password} WHERE username = #{username}")
int updatePassword(@Param("username") String username, @Param("password") String password);
@@ -28,4 +47,14 @@
@Update("UPDATE user SET gradeId = #{gradeId} WHERE username = #{username}")
int updateGrade(@Param("username") String username, @Param("gradeId") Integer gradeId);
+
+ @Update("UPDATE user SET decoration = CONCAT(IFNULL(decoration, ''), ' ', #{newDecoration}) WHERE username = #{username}")
+ int appendUserDecoration(@Param("username") String username, @Param("newDecoration") String newDecoration);
+
+ boolean existsByPasskey(String passkey);
+ void incrementUserTraffic( @Param("info_hash") String infoHash,@Param("passkey") String passkey, @Param("user_upload") long uploaded, @Param("user_download") long downloaded);
+
+ String getUsernameByPasskey(String passkey);
+
+ List<PeerInfo> findSeedersByInfoHash(@Param("infoHash") String infoHash);
}
diff --git a/src/main/java/com/pt5/pthouduan/service/PasskeyValidator.java b/src/main/java/com/pt5/pthouduan/service/PasskeyValidator.java
new file mode 100644
index 0000000..a4b4d0f
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/service/PasskeyValidator.java
@@ -0,0 +1,5 @@
+package com.pt5.pthouduan.service;
+
+public interface PasskeyValidator {
+ boolean isValid(String passkey);
+}
\ No newline at end of file
diff --git a/src/main/java/com/pt5/pthouduan/service/TorrentService.java b/src/main/java/com/pt5/pthouduan/service/TorrentService.java
new file mode 100644
index 0000000..0d887da
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/service/TorrentService.java
@@ -0,0 +1,30 @@
+package com.pt5.pthouduan.service;
+
+import com.pt5.pthouduan.entity.Torrent;
+import com.pt5.pthouduan.entity.TrackeredTorrentWithStats;
+import com.pt5.pthouduan.entity.User;
+import org.springframework.core.io.Resource;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+import java.util.Map;
+
+public interface TorrentService {
+// public void upload(File file);
+
+
+ ////未连接口ResponseEntity<Resource> Upload(MultipartFile torrentFile, String title, String description, User user) throws Exception;
+ ResponseEntity<Resource> Upload(MultipartFile torrentFile, String title, String description, Integer catrgoryId, String dpi, String caption, User user) throws Exception;
+ Torrent getTorrentFile(Long torrentid);
+ Resource loadTorrentFileAsResource(String filepath) throws Exception;
+ void incrementDownloadCount(Long torrentid);
+ List<Torrent> getAllTorrents();
+ List<Torrent> getTorrentsByCategory(Integer category);
+ Torrent getTorrentById(Long id);
+ List<Torrent> searchByKeyword(String keyword);
+ ResponseEntity<Resource> uploadWithCategory(MultipartFile torrentFile, String title, String description, Integer categoryId, User user, Map<String, String> extraParams) throws Exception;
+
+ List<Torrent> getTorrentsByCategorywithfilters(Integer categoryid, Map<String, String> filters);
+ //TrackeredTorrentWithStats.TorrentStats getTorrentStats(String infoHash);
+}
diff --git a/src/main/java/com/pt5/pthouduan/service/impl/DatabasePasskeyValidator.java b/src/main/java/com/pt5/pthouduan/service/impl/DatabasePasskeyValidator.java
new file mode 100644
index 0000000..2c7b3df
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/service/impl/DatabasePasskeyValidator.java
@@ -0,0 +1,21 @@
+package com.pt5.pthouduan.service.impl;
+
+import com.pt5.pthouduan.service.PasskeyValidator;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+
+// 示例实现(使用Spring JDBC)
+@Service
+public class DatabasePasskeyValidator implements PasskeyValidator {
+
+ @Autowired
+ private JdbcTemplate jdbcTemplate;
+
+ @Override
+ public boolean isValid(String passkey) {
+ String sql = "SELECT COUNT(*) FROM user WHERE passkey = ?";
+ int count = jdbcTemplate.queryForObject(sql, Integer.class, passkey);
+ return count > 0;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pt5/pthouduan/service/impl/PeerService.java b/src/main/java/com/pt5/pthouduan/service/impl/PeerService.java
new file mode 100644
index 0000000..27faa92
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/service/impl/PeerService.java
@@ -0,0 +1,20 @@
+package com.pt5.pthouduan.service.impl;
+
+import com.pt5.pthouduan.entity.PeerInfo;
+import com.pt5.pthouduan.entity.User;
+import com.pt5.pthouduan.mapper.UserMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class PeerService {
+ @Autowired
+ private UserMapper userMapper;
+
+ public List<PeerInfo> getSeedersByInfoHash(String infoHash){
+ return userMapper.findSeedersByInfoHash(infoHash);
+ }
+
+}
diff --git a/src/main/java/com/pt5/pthouduan/service/impl/TorrentServiceImpl.java b/src/main/java/com/pt5/pthouduan/service/impl/TorrentServiceImpl.java
new file mode 100644
index 0000000..93aad92
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/service/impl/TorrentServiceImpl.java
@@ -0,0 +1,434 @@
+package com.pt5.pthouduan.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.pt5.pthouduan.entity.*;
+import com.pt5.pthouduan.exception.TorrentNotFoundException;
+import com.pt5.pthouduan.mapper.*;
+import com.pt5.pthouduan.service.TorrentService;
+import com.pt5.pthouduan.util.TorrentParser;
+import com.turn.ttorrent.bcodec.BDecoder;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import com.turn.ttorrent.tracker.TrackedTorrent;
+import com.turn.ttorrent.tracker.Tracker;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+import org.springframework.http.*;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.*;
+import java.net.MalformedURLException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+@Service
+@Transactional
+public class TorrentServiceImpl implements TorrentService {
+
+ @Value("${tracker.url}") //等等看看这个有没有传进来
+ private String trackerUrl;
+ @Autowired
+ private TorrentMapper torrentMapper;
+ @Autowired // 确保使用接口类型(推荐)
+ private Tracker tracker;
+ @Autowired
+ private MovieInfoMapper movieInfoMapper;
+ // 🎵 Music 音乐
+ @Autowired
+ private MusicInfoMapper musicInfoMapper;
+
+ // 🎮 Game 游戏
+ @Autowired
+ private GameInfoMapper gameInfoMapper;
+
+ // 📺 TV 剧集
+ @Autowired
+ private TvInfoMapper tvInfoMapper;
+
+ // 🧸 Anime 动漫
+ @Autowired
+ private AnimeInfoMapper animeInfoMapper;
+
+ // 📚 Learning 学习
+ @Autowired
+ private EduInfoMapper eduInfoMapper;
+
+ // � Software 软件
+ @Autowired
+ private SoftwareInfoMapper softwareInfoMapper;
+
+ // 🎤 Variety 综艺
+ @Autowired
+ private ShowInfoMapper showInfoMapper;
+
+ // ⚽ Sports 体育
+ @Autowired
+ private SportInfoMapper sportInfoMapper;
+
+ // 🎬 Documentary 纪录片
+ @Autowired
+ private DocumentaryInfoMapper documentaryInfoMapper;
+
+ // 📦 Other 其他
+ @Autowired
+ private OtherInfoMapper otherInfoMapper;
+
+
+ public List<Torrent> getAllTorrents() {
+ return torrentMapper.getAllTorrents();
+ }
+ public List<Torrent> getTorrentsByCategory(Integer category) {
+ return torrentMapper.selectTorrentsByCategory(category);
+ }
+ public Torrent getTorrentById(Long id) {
+ return torrentMapper.selectById(id);
+ }
+
+
+
+// @Override
+// public void Upload(MultipartFile torrentFile, String title, String description, User user) throws Exception {
+// //读取torrent文件
+// InputStream inputStream = torrentFile.getInputStream();
+// //解析torrent文件
+// byte[] torrentData = torrentFile.getBytes();
+// Torrent torrent = TorrentParser.parse(torrentData);
+// //验证info_hash是否存在
+// Integer result;
+// result = torrentMapper.existsByInfoHash(torrent.getInfoHash());
+// if(result >= 1){
+// throw new IllegalArgumentException("Torrent already exists");
+// }
+// String fileSavePath = "D:/torrenttest/" + torrentFile.getOriginalFilename();
+// torrentFile.transferTo(new File(fileSavePath));
+// // 4. 注册到Tracker(关键步骤!)
+// TrackedTorrent trackedTorrent = TrackedTorrent.load(new File(fileSavePath));
+// tracker.announce(trackedTorrent);
+// //存入数据库
+// Torrent entity = new Torrent();
+// entity.setInfoHash(torrent.getInfoHash());
+// entity.setUploader_id(user.getUserid());
+// entity.setTorrentTitle(title);
+// entity.setDescription(description);
+// entity.setTorrentSize(torrent.getTorrentSize());
+// entity.setFilename(torrentFile.getOriginalFilename());
+// entity.setPath(fileSavePath); // 保存路径到数据库
+// torrentMapper.save(entity);
+// }
+@Override
+public ResponseEntity<Resource> Upload(MultipartFile torrentFile, String title, String description, Integer categoryId, String dpi, String caption,User user) throws Exception {
+ //读取torrent文件
+ InputStream inputStream = torrentFile.getInputStream();
+ // === 1. 基础校验 ===
+ if (torrentFile.isEmpty()) {
+ throw new IllegalArgumentException("Torrent file cannot be empty");
+ }
+ // === 2. 注入passkey ===
+ byte[] modifiedData = addPasskeyToTorrent(torrentFile.getBytes(), "111111");
+ Torrent torrent = TorrentParser.parse(modifiedData);
+ // === 4. 检查重复 ===
+ if (torrentMapper.existsByInfoHash(torrent.getInfoHash()) >= 1) {
+ throw new IllegalArgumentException("Torrent already exists");
+ }
+ String fileSavePath = "D:/torrenttest/" + torrentFile.getOriginalFilename();
+ //torrentFile.transferTo(new File(fileSavePath));
+ Files.write(new File(fileSavePath).toPath(), modifiedData);
+ // 4. 注册到Tracker(关键步骤!)
+ TrackedTorrent trackedTorrent = TrackedTorrent.load(new File(fileSavePath));
+ tracker.announce(trackedTorrent);
+ //存入数据库
+ Torrent entity = new Torrent();
+ entity.setInfoHash(torrent.getInfoHash());
+ entity.setUploader_id(user.getUserid());
+ entity.setTorrentTitle(title);
+ entity.setDescription(description);
+ entity.setTorrentSize(torrent.getTorrentSize());
+ entity.setFilename(torrentFile.getOriginalFilename());
+ entity.setCategoryid(categoryId);
+ entity.setCaption(caption);
+ entity.setDpi(dpi);
+ entity.setUploadTime(LocalDateTime.now());
+ entity.setPath(fileSavePath); // 保存路径到数据库
+ torrentMapper.save(entity);
+ // 返回修改后的.torrent文件给用户下载
+ ByteArrayResource resource = new ByteArrayResource(modifiedData);
+ // 2. 设置正确的Content-Type
+ // 构建响应,下载.torrent文件
+ // 构建响应,下载.torrent文件
+// HttpHeaders headers = new HttpHeaders();
+// headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
+// headers.setContentDisposition(ContentDisposition.attachment()
+// .filename(torrentFile.getOriginalFilename())
+// .build());
+// return new ResponseEntity<>(resource, headers, HttpStatus.OK);
+ return ResponseEntity.ok()
+ .header(HttpHeaders.CONTENT_DISPOSITION,
+ "attachment; filename=\"" + torrentFile.getOriginalFilename() + "\"")
+ .contentType(MediaType.parseMediaType("application/x-bittorrent"))
+ .body(new ByteArrayResource(modifiedData));
+}
+ @Override
+ public ResponseEntity<Resource> uploadWithCategory(MultipartFile torrentFile, String title, String description, Integer categoryId, User user, Map<String, String> extraParams ) throws Exception {
+ //读取torrent文件
+ InputStream inputStream = torrentFile.getInputStream();
+ // === 1. 基础校验 ===
+ if (torrentFile.isEmpty()) {
+ throw new IllegalArgumentException("Torrent file cannot be empty");
+ }
+ // === 2. 注入passkey ===
+ byte[] modifiedData = addPasskeyToTorrent(torrentFile.getBytes(), "111111");
+ Torrent torrent = TorrentParser.parse(modifiedData);
+ // === 4. 检查重复 ===
+ if (torrentMapper.existsByInfoHash(torrent.getInfoHash()) >= 1) {
+ throw new IllegalArgumentException("Torrent already exists");
+ }
+ String fileSavePath = "D:/torrenttest/" + torrentFile.getOriginalFilename();
+ //torrentFile.transferTo(new File(fileSavePath));
+ Files.write(new File(fileSavePath).toPath(), modifiedData);
+ // 4. 注册到Tracker(关键步骤!)
+ TrackedTorrent trackedTorrent = TrackedTorrent.load(new File(fileSavePath));
+ tracker.announce(trackedTorrent);
+ //存入数据库
+ Torrent entity = new Torrent();
+ entity.setInfoHash(torrent.getInfoHash());
+ entity.setUploader_id(user.getUserid());
+ entity.setTorrentTitle(title);
+ entity.setDescription(description);
+ entity.setTorrentSize(torrent.getTorrentSize());
+ entity.setFilename(torrentFile.getOriginalFilename());
+ entity.setCategoryid(categoryId);
+ entity.setUploadTime(LocalDateTime.now());
+ entity.setPath(fileSavePath); // 保存路径到数据库
+ torrentMapper.save(entity);
+ switch (categoryId) {
+ case 1: // 🎬 Movie 电影
+ MovieInfo movie = new MovieInfo();
+ movie.setTorrentid(entity.getTorrentid());
+ System.out.println(entity.getTorrentid());
+ movie.setRegion(extraParams.get("region"));
+ movie.setYear(extraParams.get("year") != null ? Integer.parseInt(extraParams.get("year")) : null);
+ movie.setGenre(extraParams.get("genre"));
+ movie.setCodecFormat(extraParams.get("codecFormat"));
+ movie.setResolution(extraParams.get("resolution"));
+ movieInfoMapper.insert(movie);
+ break;
+
+ case 3: // 🎵 Music 音乐
+ MusicInfo music = new MusicInfo();
+ music.setTorrentid(entity.getTorrentid());
+ music.setGenre(extraParams.get("genre"));
+ music.setRegion(extraParams.get("region"));
+ music.setStyle(extraParams.get("style"));
+ music.setFormat(extraParams.get("format"));
+ musicInfoMapper.insert(music);
+ break;
+
+ case 5: // 🎮 Game 游戏
+ GameInfo game = new GameInfo();
+ game.setTorrentid(entity.getTorrentid());
+ game.setPlatform(extraParams.get("platform"));
+ game.setGenre(extraParams.get("genre"));
+ game.setFormat(extraParams.get("dataType"));
+ game.setLanguage(extraParams.get("language"));
+ gameInfoMapper.insert(game);
+ break;
+
+ case 2: // 📺 TV 剧集
+ TvInfo tv = new TvInfo();
+ tv.setTorrentId(entity.getTorrentid());
+ tv.setRegion(extraParams.get("region"));
+ tv.setFormat(extraParams.get("format"));
+ tv.setGenre(extraParams.get("genre"));
+ tvInfoMapper.insert(tv);
+ break;
+
+ case 4: // 🧸 Anime 动漫
+ AnimeInfo anime = new AnimeInfo();
+ anime.setTorrentid(entity.getTorrentid());
+ anime.setGenre(extraParams.get("genre"));
+ anime.setFormat(extraParams.get("format"));
+ anime.setResolution(extraParams.get("resolution"));
+ animeInfoMapper.insert(anime);
+ break;
+
+ case 9: // 📚 Learning 学习
+ EduInfo learning = new EduInfo();
+ learning.setTorrentid(entity.getTorrentid());
+ learning.setGenre(extraParams.get("genre"));
+ learning.setFormat(extraParams.get("format"));
+ eduInfoMapper.insert(learning);
+ break;
+
+ case 8: // 🧰 Software 软件
+ SoftwareInfo software = new SoftwareInfo();
+ software.setTorrentid(entity.getTorrentid());
+ software.setPlatform(extraParams.get("platform"));
+ software.setGenre(extraParams.get("genre"));
+ software.setFormat(extraParams.get("format"));
+ softwareInfoMapper.insert(software);
+ break;
+
+ case 6: // 🎤 Variety 综艺
+ ShowInfo variety = new ShowInfo();
+ variety.setTorrentid(entity.getTorrentid());
+ variety.setIsMainland(Boolean.valueOf(extraParams.get("mainland"))); // 是否大陆综艺
+ variety.setGenre(extraParams.get("genre"));
+ variety.setFormat(extraParams.get("format"));
+ showInfoMapper.insert(variety);
+ break;
+
+ case 7: // ⚽ Sports 体育
+ SportInfo sports = new SportInfo();
+ sports.setTorrentid(entity.getTorrentid());
+ sports.setGenre(extraParams.get("genre"));
+ sports.setEventType(extraParams.get("eventType"));
+ sports.setFormat(extraParams.get("format"));
+ sportInfoMapper.insert(sports);
+ break;
+
+ case 10: // 🎬 Documentary 纪录片
+ DocumentaryInfo doc = new DocumentaryInfo();
+ doc.setTorrentid(entity.getTorrentid());
+ doc.setYear(extraParams.get("year") != null ? Integer.parseInt(extraParams.get("year")) : null);
+ doc.setSource(extraParams.get("source")); // 节目源
+ doc.setFormat(extraParams.get("format"));
+ documentaryInfoMapper.insert(doc);
+ break;
+
+ case 11: // 📦 Other 其他
+ OtherInfo other = new OtherInfo();
+ other.setTorrentid(entity.getTorrentid());
+ other.setGenre(extraParams.get("genre"));
+ otherInfoMapper.insert(other);
+ break;
+
+ default:
+ System.out.println("不支持的分类,或无扩展表记录");
+ }
+
+ // 返回修改后的.torrent文件给用户下载
+ ByteArrayResource resource = new ByteArrayResource(modifiedData);
+ // 2. 设置正确的Content-Type
+ // 构建响应,下载.torrent文件
+ // 构建响应,下载.torrent文件
+// HttpHeaders headers = new HttpHeaders();
+// headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
+// headers.setContentDisposition(ContentDisposition.attachment()
+// .filename(torrentFile.getOriginalFilename())
+// .build());
+// return new ResponseEntity<>(resource, headers, HttpStatus.OK);
+ System.out.println("运行到这里了");
+ return ResponseEntity.ok()
+ .header(HttpHeaders.CONTENT_DISPOSITION,
+ "attachment; filename=\"" + torrentFile.getOriginalFilename() + "\"")
+ .contentType(MediaType.parseMediaType("application/x-bittorrent"))
+ .body(new ByteArrayResource(modifiedData));
+ }
+ // 专用方法:向torrent文件注入passkey
+ private byte[] addPasskeyToTorrent(byte[] originalData, String passkey) throws Exception {
+ // 1. 解析原始torrent
+ BEValue decoded = BDecoder.bdecode(new ByteArrayInputStream(originalData));
+ Map<String, BEValue> torrentMap = decoded.getMap();
+
+ // 2. 修改announce URL
+ String originalAnnounce = torrentMap.get("announce").getString();
+ String newAnnounce = originalAnnounce.contains("?")
+ ? originalAnnounce + "&passkey=" + passkey
+ : originalAnnounce + "?passkey=" + passkey;
+
+ // 3. 重建torrent文件
+ torrentMap.put("announce", new BEValue(newAnnounce));
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ BEncoder.bencode(torrentMap, baos);
+ return baos.toByteArray();
+ }
+
+ @Override
+ public List<Torrent> getTorrentsByCategorywithfilters(Integer categoryid, Map<String, String> filters) {
+ String extendTable = getExtendTableByCategoryId(categoryid);
+ if(extendTable == null) {
+ System.out.println("未知表");
+ }
+ return torrentMapper.listByCategoryWithFilters(categoryid,filters,extendTable);
+ }
+
+ @Override
+ public List<Torrent> searchByKeyword(String keyword) {
+ return torrentMapper.searchByKeyword(keyword);
+ }
+
+ private String getExtendTableByCategoryId(Integer categoryid) {
+ switch(categoryid) {
+ case 1:return "movie_info";
+ case 2:return "tvseries_info";
+ case 3:return "music_info";
+ case 4:return "anime_info";
+ case 5:return "game_info";
+ case 6:return "variety_info";
+ case 7:return "sport_info";
+ case 8:return "software_info";
+ case 9:return "edu_info";
+ case 10:return "documentary_info";
+ case 11: return "other_info";
+ }
+ return null;
+ }
+
+ // @Override
+// public Torrent getTorrentFile(Long torrentid) {
+// System.out.println("查询结果:" + torrentid);
+// Torrent result = torrentMapper.selectById(torrentid);
+// if(result != null){
+// System.out.println("not null:" + torrentid);
+// }else{
+// System.out.println("null:" + torrentid);
+// }
+// return torrentMapper.selectById(torrentid);
+// }
+@Override
+public Torrent getTorrentFile(Long torrentid) {
+ System.out.println("查询参数:" + torrentid);
+ try {
+ Torrent result = torrentMapper.selectById(torrentid);
+ System.out.println("第一次查询结果:" + (result != null ? "非空" : "空"));
+ return result;
+ } catch (Exception e) {
+ System.out.println("查询异常:" + e.getMessage());
+ e.printStackTrace();
+ return null;
+ }
+}
+
+
+ @Override
+ public Resource loadTorrentFileAsResource(String filepath) throws Exception{
+ try{
+ Path file = Paths.get(filepath).normalize();
+ Resource resource = new UrlResource(file.toUri());
+
+ if(resource.exists() || resource.isReadable()){
+ return resource;
+ }else{
+ throw new Exception("Could not read file: " + filepath);
+ }
+ }catch(MalformedURLException e){
+ throw new Exception("Could not read file: " + filepath,e);
+ }
+ }
+
+ @Override
+ public void incrementDownloadCount(Long torrentid) {
+ torrentMapper.incrementDownloadCount(torrentid);
+ }
+}
diff --git a/src/main/java/com/pt5/pthouduan/service/impl/updatePeerStatsService.java b/src/main/java/com/pt5/pthouduan/service/impl/updatePeerStatsService.java
new file mode 100644
index 0000000..bdcdc65
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/service/impl/updatePeerStatsService.java
@@ -0,0 +1,141 @@
+package com.pt5.pthouduan.service.impl;
+
+import com.pt5.pthouduan.mapper.UserMapper;
+import com.turn.ttorrent.tracker.Tracker;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.sql.DataSource;
+import java.nio.charset.StandardCharsets;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+import static com.pt5.pthouduan.util.TorrentUtils.hexStringToByteArray;
+
+@Service
+public class updatePeerStatsService {
+
+ @Autowired
+ private DataSource dataSource;
+
+ @Autowired
+ private UserMapper userMapper;
+
+
+ public void updatePeerStatsInDB(String infoHash, String ip, int port, String peerId,
+ long uploaded, long downloaded, long deltaUpload,
+ long deltaDownload, String passkey, String event,
+ long left) {
+ double shareRatio = downloaded == 0 ? 0.0 : (double) uploaded / downloaded;
+
+ //记得在config里面写一下
+ int intervalSeconds = 100;
+ String username = userMapper.getUsernameByPasskey(passkey); // ← 获取用户名
+ String client = parseClientFromPeerId(peerId); // ← 解析客户端
+ System.out.println("剩下的:" + left);
+ boolean isCompleted = left == 0;
+ double uploadSpeed = intervalSeconds > 0 ? (double) deltaUpload / intervalSeconds : 0.0;
+ double downloadSpeed = intervalSeconds > 0 ? (double) deltaDownload / intervalSeconds : 0.0;
+
+// String sql = """
+// INSERT INTO peer_stats (info_hash, ip, port, peer_id, uploaded, downloaded, delta_upload, delta_download, share_ratio, passkey)
+// VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+// ON DUPLICATE KEY UPDATE
+// uploaded = VALUES(uploaded),
+// downloaded = VALUES(downloaded),
+// delta_upload = VALUES(delta_upload),
+// delta_download = VALUES(delta_download),
+// share_ratio = VALUES(share_ratio),
+// last_updated = CURRENT_TIMESTAMP,
+// passkey = VALUES(passkey);
+// """;
+//
+// try (Connection conn = dataSource.getConnection();
+// PreparedStatement stmt = conn.prepareStatement(sql)) {
+//
+// stmt.setString(1, infoHash);
+// stmt.setString(2, ip);
+// stmt.setInt(3, port);
+// stmt.setString(4, peerId);
+// stmt.setLong(5, uploaded);
+// stmt.setLong(6, downloaded);
+// stmt.setLong(7, deltaUpload);
+// stmt.setLong(8, deltaDownload);
+// stmt.setDouble(9, shareRatio);
+// stmt.setString(10, passkey);
+//
+// stmt.executeUpdate();
+ String sql = """
+ INSERT INTO peer_stats (
+ info_hash, ip, port, peer_id, uploaded, downloaded,
+ delta_upload, delta_download, share_ratio, passkey,
+ username, last_event, last_updated, client, created_at,
+ completed_time, upload_speed, download_speed
+ )
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?, CURRENT_TIMESTAMP, ?, ?, ?)
+ ON DUPLICATE KEY UPDATE
+ uploaded = VALUES(uploaded),
+ downloaded = VALUES(downloaded),
+ delta_upload = VALUES(delta_upload),
+ delta_download = VALUES(delta_download),
+ share_ratio = VALUES(share_ratio),
+ last_updated = CURRENT_TIMESTAMP,
+ last_event = VALUES(last_event),
+ username = VALUES(username),
+ client = VALUES(client),
+ passkey = VALUES(passkey),
+ completed_time = IF(VALUES(completed_time) IS NOT NULL, VALUES(completed_time), completed_time),
+ upload_speed = VALUES(upload_speed),
+ download_speed = VALUES(download_speed);
+""";
+
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement stmt = conn.prepareStatement(sql)) {
+
+ stmt.setString(1, infoHash); // info_hash
+ stmt.setString(2, ip); // ip
+ stmt.setInt(3, port); // port
+ stmt.setString(4, peerId); // peer_id
+ stmt.setLong(5, uploaded); // uploaded
+ stmt.setLong(6, downloaded); // downloaded
+ stmt.setLong(7, deltaUpload); // delta_upload
+ stmt.setLong(8, deltaDownload); // delta_download
+ stmt.setDouble(9, shareRatio); // share_ratio
+ stmt.setString(10, passkey); // passkey
+ stmt.setString(11, username); // username
+ stmt.setString(12, event); // last_event
+ // last_updated = CURRENT_TIMESTAMP (in SQL)
+ stmt.setString(13, client); // client
+ // created_at = CURRENT_TIMESTAMP (in SQL)
+ stmt.setTimestamp(14, isCompleted ? new java.sql.Timestamp(System.currentTimeMillis()) : null); // completed_time
+ stmt.setDouble(15, uploadSpeed); // upload_speed
+ stmt.setDouble(16, downloadSpeed); // download_speed
+//completetime还要再检查一下
+ stmt.executeUpdate();
+
+ // ✅ 成功写入后更新 user 表
+ userMapper.incrementUserTraffic(infoHash.toLowerCase(),passkey, deltaUpload, deltaDownload);
+
+ } catch (SQLException e) {
+ e.printStackTrace(); // 建议替换为日志系统
+ }
+ }
+ public static String parseClientFromPeerId(String peerId) {
+ String peerIdHex = peerId;
+ byte[] peerIdBytes = hexStringToByteArray(peerIdHex);
+ String decodedPeerId = new String(peerIdBytes, StandardCharsets.UTF_8);
+ System.out.println("Decoded Peer ID: " + decodedPeerId);
+ System.out.println("客户端:"+ peerId);
+ if (peerId == null || peerId.length() < 6) return "Unknown";
+
+ if (decodedPeerId.startsWith("-UT")) return "uTorrent";
+ if (decodedPeerId.startsWith("-TR")) return "Transmission";
+ if (decodedPeerId.startsWith("-AZ")) return "Azureus";
+ if (decodedPeerId.startsWith("-LT")) return "libtorrent";
+ if (decodedPeerId.startsWith("-qB")) return "qBittorrent";
+
+ return "Other";
+ }
+}
+
diff --git a/src/main/java/com/pt5/pthouduan/util/TorrentParser.java b/src/main/java/com/pt5/pthouduan/util/TorrentParser.java
new file mode 100644
index 0000000..d2b8f3f
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/util/TorrentParser.java
@@ -0,0 +1,62 @@
+package com.pt5.pthouduan.util;
+
+
+import com.pt5.pthouduan.entity.Torrent;
+import com.turn.ttorrent.bcodec.BDecoder;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+import java.util.Map;
+
+public class TorrentParser {
+public static Torrent parse(byte[] torrentBytes) throws Exception {
+ InputStream in = new ByteArrayInputStream(torrentBytes);
+ BEValue decoded = BDecoder.bdecode(in);
+ Map<String, BEValue> torrentMap = decoded.getMap();
+ BEValue infoValue = torrentMap.get("info");
+ if (infoValue == null) {
+ throw new IllegalArgumentException("Invalid torrent file: missing 'info' dictionary");
+ }
+
+ // 提取 info 的 Map 并编码
+ Map<String, BEValue> infoMap = infoValue.getMap();
+ ByteBuffer buffer = BEncoder.bencode(infoMap); // 返回 ByteBuffer
+ byte[] infoBencoded = new byte[buffer.remaining()];
+ buffer.get(infoBencoded); // 转换为 byte[]
+
+ // 计算 SHA-1 hash
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ byte[] hashBytes = digest.digest(infoBencoded);
+ StringBuilder sb = new StringBuilder();
+ for (byte b : hashBytes) {
+ sb.append(String.format("%02x", b));
+ }
+ String infoHash = sb.toString();
+
+ // 计算种子大小
+ long torrentSize = 0;
+ BEValue lengthValue = infoMap.get("length");
+ if (lengthValue != null) {
+ torrentSize = lengthValue.getLong();
+ } else {
+ BEValue filesValue = infoMap.get("files");
+ if (filesValue != null) {
+ for (BEValue fileValue : filesValue.getList()) {
+ BEValue fileLength = fileValue.getMap().get("length");
+ if (fileLength != null) {
+ torrentSize += fileLength.getLong();
+ }
+ }
+ }
+ }
+
+ Torrent torrent = new Torrent();
+ torrent.setInfoHash(infoHash);
+ torrent.setTorrentSize(torrentSize);
+ return torrent;
+}
+}
diff --git a/src/main/java/com/pt5/pthouduan/util/TorrentUtils.java b/src/main/java/com/pt5/pthouduan/util/TorrentUtils.java
new file mode 100644
index 0000000..6da7e73
--- /dev/null
+++ b/src/main/java/com/pt5/pthouduan/util/TorrentUtils.java
@@ -0,0 +1,40 @@
+package com.pt5.pthouduan.util;
+
+import java.util.HexFormat;
+
+public class TorrentUtils {
+ /**
+ * 16进制字符串转byte[]
+ * @param hexString 不带前缀的16进制字符串(如 "7a2b3c")
+ */
+ public static byte[] hexStringToByteArray(String hexString) {
+ // Java 17+ 推荐方式
+ return HexFormat.of().parseHex(hexString);
+
+ /* Java 8 兼容方案:
+ int len = hexString.length();
+ byte[] data = new byte[len / 2];
+ for (int i = 0; i < len; i += 2) {
+ data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4)
+ + Character.digit(hexString.charAt(i + 1), 16));
+ }
+ return data;
+ */
+ }
+
+ /**
+ * byte[] 转16进制字符串
+ */
+ public static String byteArrayToHexString(byte[] bytes) {
+ // Java 17+ 推荐方式
+ return HexFormat.of().formatHex(bytes);
+
+ /* Java 8 兼容方案:
+ StringBuilder sb = new StringBuilder();
+ for (byte b : bytes) {
+ sb.append(String.format("%02x", b));
+ }
+ return sb.toString();
+ */
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 34a3a7e..749faf7 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,14 +1,37 @@
spring.application.name=PT-houduan
spring.datasource.url=jdbc:mysql://localhost:3306/pt?useSSL=false&serverTimezone=Asia/Shanghai
+#spring.datasource.url=jdbc:mysql://host.docker.internal:3306/pt?useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.username=root
-spring.datasource.password=123456
+spring.datasource.password=12345
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
-
+# application.properties
+#spring.datasource.url=jdbc:mysql://202.205.102.121:3306/1group5?useSSL=false&serverTimezone=Asia/Shanghai
+#spring.datasource.username=team5
+#spring.datasource.password=Team5001#
+#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
+hikari.maximum-pool-size = 20
+hikari.minimum-idle = 5
+hikari.idle-timeout = 30000
# MyBatis-Plus ??
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.global-config.db-config.id-type=auto
mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0
-mybatis-plus.mapper-locations=classpath:/mapper/*.xml
+#logging.level.root=DEBUG
+#logging.level.org.springframework.web=DEBUG
+#logging.level.com.pt5=TRACE
+#logging.level.root=DEBUG
+mybatis-plus.mapper-locations=classpath:mapper/xml/*.xml
mybatis-plus.type-aliases-package=com.pt5.pthouduan.entity
+uploadDirectory= ./uploads/files # ????????
+torrent-dir= ./uploads/torrents # ????????
+# ??????
+pt.storage.torrent-path=/var/pt/torrents
+pt.storage.temp-path=/var/pt/temp
+# application.properties ??
+tracker.url=http://localhost:6969/announce
+
+
+
+
diff --git a/src/main/resources/mapper/xml/TorrentMapper.xml b/src/main/resources/mapper/xml/TorrentMapper.xml
new file mode 100644
index 0000000..609ec46
--- /dev/null
+++ b/src/main/resources/mapper/xml/TorrentMapper.xml
@@ -0,0 +1,228 @@
+<?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.pt5.pthouduan.mapper.TorrentMapper">
+
+ <!-- 按上传者查询 -->
+ <select id="selectByUploaderId" resultType="com.pt5.pthouduan.entity.Torrent">
+ SELECT * FROM torrent
+ WHERE userid = #{uploaderId}
+ </select>
+
+ <!-- 按名称模糊搜索 -->
+ <select id="searchByName" resultType="com.pt5.pthouduan.entity.Torrent">
+ SELECT * FROM torrent
+ WHERE torrent_title LIKE CONCAT('%', #{keyword}, '%')
+ </select>
+
+ <select id="searchByKeyword" resultType="com.pt5.pthouduan.entity.Torrent">
+ SELECT * FROM torrent
+ WHERE torrent_title LIKE CONCAT('%', #{keyword}, '%')
+ </select>
+
+ <!-- 获取热门种子 -->
+ <select id="selectTopPopular" resultType="com.pt5.pthouduan.entity.Torrent">
+ SELECT * FROM torrent
+ ORDER BY download_count DESC
+ LIMIT 10
+ </select>
+
+ <!-- 检查infoHash是否存在 -->
+ <select id="existsByInfoHash" resultType="int" parameterType="string">
+ SELECT COUNT(*) FROM torrent WHERE info_hash = #{infoHash}
+ </select>
+
+
+ <!-- 下载计数+1 -->
+ <update id="incrementDownloadCount">
+ UPDATE torrent
+ SET download_count = download_count + 1
+ WHERE torrentid = #{id}
+ </update>
+
+ <!-- 复杂查询示例(带动态SQL) -->
+ <select id="selectByCondition" resultType="com.pt5.pthouduan.entity.Torrent">
+ SELECT * FROM torrent
+ <where>
+ <if test="condition.categoryId != null">
+ AND categoryid = #{condition.categoryId}
+ </if>
+ <if test="condition.minSize != null">
+ AND torrent_size >= #{condition.minSize}
+ </if>
+ </where>
+ ORDER BY ${condition.orderBy} DESC
+ </select>
+
+<!-- <select id="selectById" resultType="com.pt5.pthouduan.entity.Torrent">-->
+<!-- SELECT-->
+<!-- t.torrentid,-->
+<!-- t.torrent_title,-->
+<!-- t.description,-->
+<!-- t.uploader_id,-->
+<!-- t.upload_time,-->
+<!-- t.download_count,-->
+<!-- t.torrent_size,-->
+<!-- t.filename,-->
+<!-- t.path,-->
+<!-- t.categoryid,-->
+<!-- t.caption,-->
+<!-- t.dpi-->
+<!-- FROM torrent t-->
+<!-- WHERE t.torrentid = #{torrentid}-->
+<!-- </select>-->
+ <select id="selectById" resultType="com.pt5.pthouduan.entity.Torrent">
+ SELECT
+ t.torrentid,
+ t.torrent_title,
+ t.description,
+ t.uploader_id,
+ t.upload_time,
+ t.download_count,
+ t.torrent_size,
+ t.filename,
+ t.path,
+ t.categoryid,
+ t.caption,
+ t.dpi,
+ t.last_seed,
+ t.info_hash
+ FROM torrent t
+ WHERE t.torrentid = #{torrentid}
+ </select>
+
+ <select id="getAllTorrents" resultType="com.pt5.pthouduan.entity.Torrent">
+ SELECT
+ torrentid,
+ torrent_title,
+ filename,
+ description,
+ path,
+ uploader_id,
+ upload_time,
+ download_count,
+ categoryid,
+ promotionid,
+ dpi,
+ caption
+ FROM
+ torrent
+ </select>
+
+ <insert id="save" parameterType="com.pt5.pthouduan.entity.Torrent"
+ useGeneratedKeys="true" keyProperty="torrentid" keyColumn="torrentid">
+ INSERT INTO torrent (
+ uploader_id,
+ promotionid,
+ categoryid,
+ info_hash,
+ torrent_title,
+ dpi,
+ caption,
+ torrent_size,
+ upload_time,
+ download_count,
+ description,
+ path,
+ filename
+ ) VALUES (
+ #{uploaderid},
+ #{promotionid},
+ #{categoryid},
+ #{infohash},
+ #{torrenttitle},
+ #{dpi},
+ #{caption},
+ #{torrentsize},
+ #{uploadtime},
+ #{downloadCount},
+ #{description},
+ #{path},
+ #{filename}
+ )
+ </insert>
+ <!-- 按分类查询种子 -->
+ <select id="selectTorrentsByCategory" resultType="com.pt5.pthouduan.entity.Torrent">
+ SELECT * FROM torrent WHERE categoryid = #{category}
+ </select>
+
+ <!-- 免费下载 -->
+ <update id="setFreePromotion">
+ <![CDATA[
+ UPDATE torrent t
+ LEFT JOIN (
+ SELECT info_hash, MAX(last_updated) AS last_time
+ FROM peer_stats
+ GROUP BY info_hash
+ ) ps ON t.info_hash = ps.info_hash
+ SET t.promotionid = 3
+ WHERE ps.last_time IS NULL OR ps.last_time < NOW() - INTERVAL 7 DAY
+ ]]>
+ </update>
+
+
+ <!-- 上传加倍 -->
+ <update id="setDoubleUpload">
+ <![CDATA[
+ UPDATE torrent t
+ LEFT JOIN (
+ SELECT info_hash, MAX(last_updated) AS last_time,
+ SUM(delta_upload) AS total_upload
+ FROM peer_stats
+ GROUP BY info_hash
+ ) ps ON t.info_hash = ps.info_hash
+ SET t.promotionid = 1
+ WHERE ps.last_time < NOW() - INTERVAL 5 DAY
+ AND (ps.total_upload IS NULL OR ps.total_upload = 0)
+ ]]>
+ </update>
+
+
+ <!-- 下载减半 -->
+ <update id="setHalfDownload">
+ <![CDATA[
+ UPDATE torrent t
+ LEFT JOIN (
+ SELECT info_hash, MAX(last_updated) AS last_time,
+ SUM(delta_download) AS total_download
+ FROM peer_stats
+ GROUP BY info_hash
+ ) ps ON t.info_hash = ps.info_hash
+ SET t.promotionid = 2
+ WHERE ps.last_time < NOW() - INTERVAL 5 DAY
+ AND (ps.total_download IS NULL OR ps.total_download = 0)
+ ]]>
+ </update>
+
+
+ <!-- 取消促销 -->
+ <update id="clearPromotion">
+ <![CDATA[
+ UPDATE torrent t
+ LEFT JOIN (
+ SELECT info_hash, MAX(last_updated) AS last_time
+ FROM peer_stats
+ GROUP BY info_hash
+ ) ps ON t.info_hash = ps.info_hash
+ SET t.promotionid = NULL
+ WHERE ps.last_time >= NOW() - INTERVAL 3 DAY
+ ]]>
+ </update>
+
+ <select id="listByCategoryWithFilters" resultType="com.pt5.pthouduan.entity.Torrent">
+ SELECT * FROM torrent t
+ WHERE t.categoryid = #{categoryid}
+ <if test="filters != null and !filters.isEmpty()">
+ AND t.torrentid IN (
+ SELECT e.torrentid FROM
+ ${extendTable} e
+ WHERE
+ <foreach collection="filters" index="key" item="value" separator=" AND ">
+ (e.${key} = #{value})
+ </foreach>
+ )
+ </if>
+ </select>
+
+</mapper>
\ No newline at end of file
diff --git a/src/main/resources/mapper/xml/UserMapper.xml b/src/main/resources/mapper/xml/UserMapper.xml
new file mode 100644
index 0000000..229f3c0
--- /dev/null
+++ b/src/main/resources/mapper/xml/UserMapper.xml
@@ -0,0 +1,91 @@
+<?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.pt5.pthouduan.mapper.UserMapper">
+
+ <!-- 通用查询映射结果 -->
+ <resultMap id="BaseResultMap" type="com.pt5.pthouduan.entity.User">
+ <id column="userid" property="userid" />
+ <result column="username" property="username" />
+ <result column="password" property="password" />
+ <result column="user_upload" property="userUpload" />
+ <result column="user_download" property="userDownload" />
+ <result column="credit" property="credit" />
+ <result column="image" property="image" />
+ <result column="sex" property="sex" />
+ <result column="grade_id" property="gradeId" />
+ <result column="passkey" property="passkey" />
+ <result column="user_created_time" property="userCreatedTime" />
+ <result column="ratio" property="ratio" />
+ <result column="age" property="age" />
+ <result column="privacy" property="privacy" />
+ </resultMap>
+
+ <!-- 通用查询结果列 -->
+ <sql id="Base_Column_List">
+ username, password, user_upload, user_download, credit, image, sex, userid, grade_id, passkey, user_created_time, ratio, age, privacy
+ </sql>
+ <!-- 根据 passkey 查询用户名 -->
+ <select id="getUsernameByPasskey" parameterType="java.lang.String" resultType="java.lang.String">
+ SELECT username
+ FROM user
+ WHERE passkey = #{passkey}
+ </select>
+
+ <select id="findSeedersByInfoHash" resultType="com.pt5.pthouduan.entity.PeerInfo">
+ SELECT
+ ps.ip,
+ ps.port,
+ ps.peer_id,
+ ps.uploaded,
+ ps.upload_speed,
+ ps.downloaded,
+ ps.download_speed,
+ ps.last_event,
+ ps.last_updated,
+ ps.created_at,
+ ps.client,
+ ps.username,
+ ps.created_at,
+ ps.completed_time,
+ ps.port
+ FROM peer_stats ps
+ WHERE ps.info_hash = #{infoHash}
+ </select>
+
+
+
+ <!-- 根据 passkey 检查用户是否存在 -->
+ <select id="existsByPasskey" resultType="boolean">
+ SELECT COUNT(*) > 0
+ FROM user
+ WHERE passkey = #{passkey}
+ </select>
+
+<!-- <update id="incrementUserTraffic">-->
+<!-- UPDATE user-->
+<!-- SET user_upload = user_upload + #{user_upload},-->
+<!-- user_download = user_download + #{user_download}-->
+<!-- WHERE passkey = #{passkey}-->
+<!-- </update>-->
+ <update id="incrementUserTraffic">
+ UPDATE user u
+ JOIN torrent t ON t.info_hash = #{info_hash} AND u.passkey = #{passkey}
+ SET
+ u.user_upload = u.user_upload + (
+ CASE
+ WHEN t.promotionid = 1 THEN #{user_upload} * 2 -- 上传加倍
+ ELSE #{user_upload}
+ END
+ ),
+ u.user_download = u.user_download + (
+ CASE
+ WHEN t.promotionid = 2 THEN #{user_download} / 2 -- 下载减半
+ WHEN t.promotionid = 3 THEN 0 -- 免费下载
+ ELSE #{user_download}
+ END
+ )
+ </update>
+
+
+
+</mapper>
diff --git a/src/test/java/com/pt5/pthouduan/ControllerTest/TorrentUploadControllerTest.java b/src/test/java/com/pt5/pthouduan/ControllerTest/TorrentUploadControllerTest.java
new file mode 100644
index 0000000..c0c9cbf
--- /dev/null
+++ b/src/test/java/com/pt5/pthouduan/ControllerTest/TorrentUploadControllerTest.java
@@ -0,0 +1,247 @@
+package com.pt5.pthouduan.ControllerTest;
+
+import com.pt5.pthouduan.controller.TorrentController;
+import com.pt5.pthouduan.entity.User;
+import com.pt5.pthouduan.service.TorrentService;
+import org.apache.tomcat.util.net.openssl.ciphers.Authentication;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+public class TorrentUploadControllerTest {
+
+ @Mock
+ private TorrentService torrentService;
+
+ @InjectMocks
+ private TorrentController torrentController;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ setupSecurityContext();
+ }
+
+ private void setupSecurityContext() {
+ // 设置模拟用户
+ User user = new User(1L, "testuser");
+ Authentication authentication = mock(Authentication.class);
+ }
+
+ @Test
+ void uploadTorrent_Success() throws Exception {
+ // 准备测试数据
+ MockMultipartFile mockFile = new MockMultipartFile(
+ "file", "test.torrent", "application/x-bittorrent", "mock torrent data".getBytes()
+ );
+
+ // 准备参数
+ String title = "Test Torrent";
+ String description = "Test Description";
+ Integer categoryId = 1;
+ String dpi = "1080";
+ String caption = "Test Caption";
+ String region = "CN";
+ Integer year = 2023;
+ String genre = "Action";
+
+ // 准备预期的extraParams
+ Map<String, String> expectedExtraParams = new HashMap<>();
+ expectedExtraParams.put("dpi", dpi);
+ expectedExtraParams.put("caption", caption);
+ expectedExtraParams.put("region", region);
+ expectedExtraParams.put("year", year.toString());
+ expectedExtraParams.put("genre", genre);
+
+ // 模拟Service行为
+ when(torrentService.uploadWithCategory(
+ any(MultipartFile.class), anyString(), anyString(),
+ anyInt(), any(User.class), anyMap())
+ ).thenReturn(ResponseEntity.ok().build());
+
+ // 执行测试
+ ResponseEntity<?> response = torrentController.uploadTorrent(
+ mockFile, title, description, categoryId,
+ dpi, caption,
+ region, year, genre,
+ null, null, null, null, null, null, null, null, null, null
+ );
+
+ // 验证结果
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+
+ // 使用ArgumentCaptor捕获参数
+ ArgumentCaptor<Map<String, String>> captor = ArgumentCaptor.forClass(Map.class);
+ verify(torrentService).uploadWithCategory(
+ eq(mockFile), eq(title), eq(description),
+ eq(categoryId), any(User.class), captor.capture()
+ );
+
+ // 验证Map内容
+ Map<String, String> actualParams = captor.getValue();
+ assertEquals(expectedExtraParams, actualParams);
+ }
+
+ @Test
+ void uploadTorrent_WithAllOptionalParams() throws Exception {
+ // 准备测试数据
+ MockMultipartFile mockFile = new MockMultipartFile(
+ "file", "test.torrent", "application/x-bittorrent", "mock torrent data".getBytes()
+ );
+
+ // 准备所有可能的参数
+ String title = "Complete Torrent";
+ String description = "Complete Description";
+ Integer categoryId = 2;
+
+ // 准备所有可选参数
+ String dpi = "4K";
+ String caption = "Complete Caption";
+ String region = "US";
+ Integer year = 2022;
+ String genre = "Sci-Fi";
+ String format = "MP4";
+ String resolution = "2160p";
+ String codecFormat = "H.265";
+ String platform = "PC";
+ String language = "English";
+ String eventType = "Championship";
+ String dataType = "RAW";
+ String source = "Blu-ray";
+ String style = "Rock";
+ Boolean isMainland = false;
+
+ // 准备预期的extraParams
+ Map<String, String> expectedExtraParams = new HashMap<>();
+ expectedExtraParams.put("dpi", dpi);
+ expectedExtraParams.put("caption", caption);
+ expectedExtraParams.put("region", region);
+ expectedExtraParams.put("year", year.toString());
+ expectedExtraParams.put("genre", genre);
+ expectedExtraParams.put("format", format);
+ expectedExtraParams.put("resolution", resolution);
+ expectedExtraParams.put("codecFormat", codecFormat);
+ expectedExtraParams.put("platform", platform);
+ expectedExtraParams.put("language", language);
+ expectedExtraParams.put("eventType", eventType);
+ expectedExtraParams.put("dataType", dataType);
+ expectedExtraParams.put("source", source);
+ expectedExtraParams.put("style", style);
+ expectedExtraParams.put("isMainland", isMainland.toString());
+
+ // 模拟Service行为
+ when(torrentService.uploadWithCategory(
+ any(MultipartFile.class), anyString(), anyString(),
+ anyInt(), any(User.class), anyMap())
+ ).thenReturn(ResponseEntity.ok().build());
+
+ // 执行测试
+ ResponseEntity<?> response = torrentController.uploadTorrent(
+ mockFile, title, description, categoryId,
+ dpi, caption,
+ region, year, genre,
+ format, resolution, codecFormat,
+ platform, language, eventType,
+ dataType, source, style, isMainland
+ );
+
+ // 验证结果
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+
+ // 验证参数传递
+ ArgumentCaptor<Map<String, String>> captor = ArgumentCaptor.forClass(Map.class);
+ verify(torrentService).uploadWithCategory(
+ eq(mockFile), eq(title), eq(description),
+ eq(categoryId), any(User.class), captor.capture()
+ );
+
+ // 验证所有参数都被正确传递
+ Map<String, String> actualParams = captor.getValue();
+ assertEquals(expectedExtraParams, actualParams);
+ }
+
+ @Test
+ void uploadTorrent_WithNullOptionalParams() throws Exception {
+ // 准备测试数据
+ MockMultipartFile mockFile = new MockMultipartFile(
+ "file", "test.torrent", "application/x-bittorrent", "mock torrent data".getBytes()
+ );
+
+ String title = "Minimal Torrent";
+ String description = "Minimal Description";
+ Integer categoryId = 3;
+
+ // 模拟Service行为
+ when(torrentService.uploadWithCategory(
+ any(MultipartFile.class), anyString(), anyString(),
+ anyInt(), any(User.class), anyMap())
+ ).thenReturn(ResponseEntity.ok().build());
+
+ // 执行测试 - 只传必填参数,可选参数都为null
+ ResponseEntity<?> response = torrentController.uploadTorrent(
+ mockFile, title, description, categoryId,
+ null, null, null, null, null,
+ null, null, null, null, null,
+ null, null, null, null, null
+ );
+
+ // 验证结果
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+
+ // 验证参数传递
+ ArgumentCaptor<Map<String, String>> captor = ArgumentCaptor.forClass(Map.class);
+ verify(torrentService).uploadWithCategory(
+ eq(mockFile), eq(title), eq(description),
+ eq(categoryId), any(User.class), captor.capture()
+ );
+
+ // 验证extraParams为空
+ Map<String, String> actualParams = captor.getValue();
+ assertTrue(actualParams.isEmpty());
+ }
+
+ @Test
+ void uploadTorrent_ServiceThrowsException() throws Exception {
+ // 准备测试数据
+ MockMultipartFile mockFile = new MockMultipartFile(
+ "file", "test.torrent", "application/x-bittorrent", "mock torrent data".getBytes()
+ );
+
+ String title = "Failing Torrent";
+ String description = "Failing Description";
+ Integer categoryId = 4;
+
+ // 模拟Service抛出异常
+ when(torrentService.uploadWithCategory(
+ any(MultipartFile.class), anyString(), anyString(),
+ anyInt(), any(User.class), anyMap())
+ ).thenThrow(new RuntimeException("Upload failed"));
+
+ // 执行测试
+ ResponseEntity<?> response = torrentController.uploadTorrent(
+ mockFile, title, description, categoryId,
+ null, null, null, null, null,
+ null, null, null, null, null,
+ null, null, null, null, null
+ );
+
+ // 验证结果
+ assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
+ assertTrue(response.getBody().toString().contains("Upload failed"));
+ }
+
+}
\ No newline at end of file