feat: 支持 Torrent 解析与数据持久化(v1/v2/Hybrid)

- 实现对 Torrent v1、v2 以及 Hybrid 模式的解析
- 完成 Torrent 元信息的数据库持久化逻辑
- 支持 announce 节点、文件信息的提取与存储
- 构建 BtTorrent 表结构与相关插入逻辑
- 预留 OSS 上传接口,明日补充实际上传实现
Change-Id: I4438da0ab364bc8e0d299e1d57474c190584052a
diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml
index fa5532d..4fe1dc0 100644
--- a/ruoyi-admin/pom.xml
+++ b/ruoyi-admin/pom.xml
@@ -17,6 +17,16 @@
 
     <dependencies>
 
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+
+
+
+
+
+
         <!-- spring-boot-devtools -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
@@ -24,6 +34,8 @@
             <optional>true</optional> <!-- 表示依赖不会传递 -->
         </dependency>
 
+
+
         <!-- spring-doc -->
         <dependency>
             <groupId>org.springdoc</groupId>
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/torrent/controller/BtTorrentController.java b/ruoyi-admin/src/main/java/com/ruoyi/torrent/controller/BtTorrentController.java
index 2dab95a..6d7626b 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/torrent/controller/BtTorrentController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/torrent/controller/BtTorrentController.java
@@ -1,7 +1,20 @@
 package com.ruoyi.torrent.controller;
 
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.torrent.domain.BtTorrentAnnounce;
+import com.ruoyi.torrent.domain.BtTorrentFile;
+import com.ruoyi.torrent.service.IBtTorrentAnnounceService;
+import com.ruoyi.torrent.service.IBtTorrentFileService;
 import com.ruoyi.torrent.service.IBtTorrentService;
 import jakarta.servlet.http.HttpServletResponse;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -14,6 +27,9 @@
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.multipart.MultipartFile;
+
 import com.ruoyi.common.annotation.Log;
 import com.ruoyi.common.core.controller.BaseController;
 import com.ruoyi.common.core.domain.AjaxResult;
@@ -22,6 +38,11 @@
 import com.ruoyi.common.utils.poi.ExcelUtil;
 import com.ruoyi.common.core.page.TableDataInfo;
 
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.UUID;
+
 /**
  * 种子主Controller
  * 
@@ -35,6 +56,125 @@
     @Autowired
     private IBtTorrentService btTorrentService;
 
+    @Autowired
+    private IBtTorrentFileService btTorrentFileService;
+    @Autowired
+    private IBtTorrentAnnounceService btTorrentAnnounceService;
+
+    private static final String boundary = "----WebKitFormBoundary" + UUID.randomUUID().toString().replace("-", "");
+
+    @PreAuthorize("@ss.hasPermi('system:torrent:add')")
+    @Log(title = "种子主", businessType = BusinessType.INSERT)
+    @PostMapping("/uploadTorrent")
+    public AjaxResult uploadTorrent(@RequestParam("file") MultipartFile file) {
+        try {
+
+            // Create URL connection to Flask server
+            String flaskUrl = "http://localhost:5000/parse_torrent"; // Flask server URL
+            HttpURLConnection connection = (HttpURLConnection) new URL(flaskUrl).openConnection();
+            connection.setRequestMethod("POST");
+            connection.setDoOutput(true);
+            connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
+
+            // Open output stream
+            try (OutputStream outputStream = connection.getOutputStream()) {
+                // Write the multipart form data boundary and file data
+                writeFormData(outputStream, file);
+
+                // Handle Flask response
+                int responseCode = connection.getResponseCode();
+                if (responseCode != 200) {
+                    return AjaxResult.error("Failed to communicate with Flask server, response code: " + responseCode);
+                }
+
+                // Assuming the Flask server responds with JSON, parse the response
+                String responseBody = new String(connection.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
+                parseTorrentData(responseBody);
+                return AjaxResult.success("Torrent uploaded and processed successfully");
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+            return AjaxResult.error("Error during file upload or communication with Flask service");
+        }
+    }
+
+    // Write multipart form data to the output stream
+    private void writeFormData(OutputStream outputStream, MultipartFile file) throws IOException {
+        // Write the start boundary
+        outputStream.write(("--" + boundary + "\r\n").getBytes(StandardCharsets.UTF_8));
+
+        // Write content-disposition header
+        outputStream.write(("Content-Disposition: form-data; name=\"torrent\"; filename=\"" + file.getOriginalFilename() + "\"\r\n").getBytes(StandardCharsets.UTF_8));
+        outputStream.write(("Content-Type: application/octet-stream\r\n\r\n").getBytes(StandardCharsets.UTF_8));
+
+        // Write the file content
+        file.getInputStream().transferTo(outputStream);
+        outputStream.write("\r\n".getBytes(StandardCharsets.UTF_8));
+
+        // Write the closing boundary
+        outputStream.write(("--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
+    }
+
+
+    public void parseTorrentData(String responseData) {
+        try {
+
+            // Create ObjectMapper to handle JSON parsing
+            ObjectMapper objectMapper = new ObjectMapper();
+
+            // Parse the JSON response
+            JsonNode jsonNode = objectMapper.readTree(responseData);
+            JsonNode btTorrentNode = jsonNode.get("btTorrent");
+            JsonNode btTorrentAnnounceNode = jsonNode.get("btTorrentAnnounce");
+            JsonNode btTorrentFilesNode = jsonNode.get("btTorrentFiles");
+
+
+            // Convert btTorrentNode to BtTorrent object
+            BtTorrent btTorrent = objectMapper.readValue(btTorrentNode.toString(), BtTorrent.class);
+            btTorrent.setCreatedBy(SecurityUtils.getUsername());
+            btTorrent.setUploaderId(SecurityUtils.getUserId());
+            btTorrentService.insertBtTorrent(btTorrent);
+            Long torrentId=btTorrent.getTorrentId();
+
+            // Convert btTorrentFilesNode to List<BtTorrentFile> using TypeReference
+            List<BtTorrentFile> btTorrentFiles = objectMapper.readValue(
+                    btTorrentFilesNode.toString(),
+                    new TypeReference<List<BtTorrentFile>>() {});
+            btTorrentFiles.forEach(btFile -> {
+                btFile.setTorrentId(torrentId);
+                btTorrentFileService.insertBtTorrentFile(btFile);
+            }
+            );
+
+            // Convert btTorrentAnnounceNode to List<BtTorrentAnnounce>
+            List<BtTorrentAnnounce> btTorrentAnnounceList = new ArrayList<>();
+            if (btTorrentAnnounceNode.isArray()) {
+                for (JsonNode announceNode : btTorrentAnnounceNode) {
+                    // You may need to adjust this depending on your BtTorrentAnnounce class structure
+                    BtTorrentAnnounce announce = objectMapper.readValue(announceNode.toString(), BtTorrentAnnounce.class);
+                    btTorrentAnnounceList.add(announce);
+                }
+            }
+            btTorrentAnnounceList.forEach(btTorrentAnnounce -> {
+                        btTorrentAnnounce.setTorrentId(torrentId);
+                        btTorrentAnnounceService.insertBtTorrentAnnounce(btTorrentAnnounce);
+                    }
+            );
+
+
+
+
+
+
+
+
+        } catch (Exception e) {
+            e.printStackTrace();
+            // Handle the error (e.g., return null or an error message)
+
+        }
+    }
+
     /**
      * 查询种子主列表
      */
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrent.java b/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrent.java
index 9a85f2f..30fe780 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrent.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrent.java
@@ -2,6 +2,7 @@
 
 import java.util.Date;
 import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringStyle;
 import com.ruoyi.common.annotation.Excel;
@@ -13,6 +14,7 @@
  * @author ruoyi
  * @date 2025-04-21
  */
+@JsonIgnoreProperties(ignoreUnknown = true)
 public class BtTorrent extends BaseEntity
 {
     private static final long serialVersionUID = 1L;
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentAnnounce.java b/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentAnnounce.java
index 967cc5d..9736a4f 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentAnnounce.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentAnnounce.java
@@ -1,5 +1,6 @@
 package com.ruoyi.torrent.domain;
 
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringStyle;
 import com.ruoyi.common.annotation.Excel;
@@ -11,6 +12,7 @@
  * @author ruoyi
  * @date 2025-04-21
  */
+@JsonIgnoreProperties(ignoreUnknown = true)
 public class BtTorrentAnnounce extends BaseEntity
 {
     private static final long serialVersionUID = 1L;
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentFile.java b/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentFile.java
index 920d871..485b8dd 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentFile.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentFile.java
@@ -1,5 +1,6 @@
 package com.ruoyi.torrent.domain;
 
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringStyle;
 import com.ruoyi.common.annotation.Excel;
@@ -11,6 +12,7 @@
  * @author ruoyi
  * @date 2025-04-21
  */
+@JsonIgnoreProperties(ignoreUnknown = true)
 public class BtTorrentFile extends BaseEntity
 {
     private static final long serialVersionUID = 1L;
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentTags.java b/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentTags.java
index 8f19b72..ccb81ea 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentTags.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/torrent/domain/BtTorrentTags.java
@@ -1,5 +1,6 @@
 package com.ruoyi.torrent.domain;
 
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringStyle;
 import com.ruoyi.common.annotation.Excel;
@@ -11,6 +12,7 @@
  * @author ruoyi
  * @date 2025-04-21
  */
+@JsonIgnoreProperties(ignoreUnknown = true)
 public class BtTorrentTags extends BaseEntity
 {
     private static final long serialVersionUID = 1L;