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

- 实现对 Torrent v1、v2 以及 Hybrid 模式的解析
- 完成 Torrent 元信息的数据库持久化逻辑
- 支持 announce 节点、文件信息的提取与存储
- 构建 BtTorrent 表结构与相关插入逻辑
- 预留 OSS 上传接口,明日补充实际上传实现
Change-Id: I4438da0ab364bc8e0d299e1d57474c190584052a
diff --git a/react-ui/src/pages/Torrent/index.tsx b/react-ui/src/pages/Torrent/index.tsx
index 6990da7..bb058ae 100644
--- a/react-ui/src/pages/Torrent/index.tsx
+++ b/react-ui/src/pages/Torrent/index.tsx
@@ -1,5 +1,5 @@
 import React, { useRef, useState } from 'react';
-import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
+import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, UploadOutlined } from '@ant-design/icons';
 import {
   Button,
   Modal,
@@ -11,14 +11,10 @@
   DatePicker,
   Card,
   Layout,
+  Upload,
+  UploadProps,
 } from 'antd';
-import {
-  ProTable,
-  ActionType,
-  ProColumns,
-  ProDescriptions,
-  ProDescriptionsItemProps,
-} from '@ant-design/pro-components';
+import { ProTable, ActionType, ProColumns, ProDescriptions, ProDescriptionsItemProps } from '@ant-design/pro-components';
 import type { BtTorrent } from './data';
 import {
   listBtTorrent,
@@ -26,6 +22,7 @@
   addBtTorrent,
   updateBtTorrent,
   removeBtTorrent,
+  uploadTorrent, // Function to handle torrent upload
 } from './service';
 
 const { Content } = Layout;
@@ -36,7 +33,10 @@
   const [modalVisible, setModalVisible] = useState(false);
   const [drawerVisible, setDrawerVisible] = useState(false);
   const [current, setCurrent] = useState<Partial<BtTorrent>>({});
+  const [uploadModalVisible, setUploadModalVisible] = useState(false); // State for upload modal
+  const [uploadFile, setUploadFile] = useState<File | null>(null); // State to store selected file
 
+  // Columns for the ProTable (the table displaying torrents)
   const columns: ProColumns<BtTorrent>[] = [
     {
       title: '种子ID',
@@ -89,18 +89,47 @@
     },
   ];
 
+  // Handle the submit for adding or updating a torrent
   const handleSubmit = async () => {
     const values = await form.validateFields();
-    if (current?.torrentId) {
-      await updateBtTorrent({ ...current, ...values });
-      message.success('更新成功');
-    } else {
-      await addBtTorrent(values as BtTorrent);
-      message.success('新增成功');
+    try {
+      if (current?.torrentId) {
+        await updateBtTorrent({ ...current, ...values });
+        message.success('更新成功');
+      } else {
+        await addBtTorrent(values as BtTorrent);
+        message.success('新增成功');
+      }
+      setModalVisible(false);
+      form.resetFields();
+      actionRef.current?.reload();
+    } catch (error) {
+      message.error('操作失败');
     }
-    setModalVisible(false);
-    form.resetFields();
-    actionRef.current?.reload();
+  };
+
+  // Handle file upload
+  const handleFileUpload = async (file: File) => {
+    try {
+      if (!file) {
+        throw new Error('请选择一个文件');
+      }
+
+      // Call the uploadTorrent function to upload the file
+      await uploadTorrent(file);
+
+      // Show a success message
+      message.success('文件上传成功');
+
+      // Close the upload modal
+      setUploadModalVisible(false);
+
+      // Optionally reload the table or perform other actions (e.g., refresh list)
+      actionRef.current?.reload();
+
+    } catch (error) {
+      message.error(error.message || '文件上传失败');
+    }
   };
 
   return (
@@ -124,6 +153,14 @@
             >
               新增
             </Button>,
+            <Button
+              key="upload"
+              type="primary"
+              icon={<UploadOutlined />}
+              onClick={() => setUploadModalVisible(true)} // Show the upload modal
+            >
+              上传种子文件
+            </Button>
           ]}
           request={async (params) => {
             const res = await listBtTorrent(params);
@@ -162,6 +199,26 @@
           </Form>
         </Modal>
 
+        {/* 上传种子文件的Modal */}
+        <Modal
+          title="上传种子文件"
+          visible={uploadModalVisible}
+          onCancel={() => setUploadModalVisible(false)}
+          footer={null}
+        >
+          <Upload
+            customRequest={({ file, onSuccess, onError }) => {
+              setUploadFile(file);
+              handleFileUpload(file);
+              onSuccess?.();
+            }}
+            showUploadList={false}
+            accept=".torrent"
+          >
+            <Button icon={<UploadOutlined />}>点击上传 .torrent 文件</Button>
+          </Upload>
+        </Modal>
+
         {/* 详情抽屉 */}
         <Drawer
           width={500}
diff --git a/react-ui/src/pages/Torrent/service.ts b/react-ui/src/pages/Torrent/service.ts
index c1d8e26..8e15618 100644
--- a/react-ui/src/pages/Torrent/service.ts
+++ b/react-ui/src/pages/Torrent/service.ts
@@ -166,7 +166,23 @@
     data,
   });
 }
+/**
+ * 上传并解析种子文件
+ * @param file The .torrent file to upload
+ * @returns The parsed torrent data or error message
+ */
+export async function uploadTorrent(file: File) {
+  const formData = new FormData();
+  formData.append('file', file);
 
+  return request('/api/system/torrent/uploadTorrent', {
+    method: 'POST',
+    data: formData,
+    headers: {
+      'Content-Type': 'multipart/form-data',
+    },
+  });
+}
 /** 删除标签(支持批量) */
 export async function removeBtTorrentTag(ids: number[]) {
   return request(`/api/system/tags/${ids.join(',')}`, {
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;