add file

Change-Id: I49ce6d06ec46cbe679e4b8771031461047c704bb
diff --git a/Dockerfile b/Dockerfile
index 6b47fed..3dbfba9 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -22,7 +22,6 @@
 RUN apk add --no-cache openjdk21-jdk curl
 
 # 后台启动 Spring Boot + 前台运行 Nginx
-EXPOSE 8082
-EXPOSE 8080
+EXPOSE 5009
 
 CMD java -jar app.jar & nginx -g "daemon off;"
diff --git a/nginx.conf b/nginx.conf
index dd6cedc..d06c697 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -1,4 +1,5 @@
 worker_processes 1;
+
 events { worker_connections 1024; }
 
 http {
@@ -9,12 +10,47 @@
     keepalive_timeout  65;
 
     server {
-        listen 8082;
+        listen 5009;
+
+        # 支持 CORS 的全局选项处理(可选)
+        if ($request_method = OPTIONS) {
+            add_header Access-Control-Allow-Origin *;
+            add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS, PUT, DELETE';
+            add_header Access-Control-Allow-Headers 'Origin, Content-Type, Accept, Authorization';
+            add_header Access-Control-Max-Age 3600;
+            return 204;
+        }
 
         # 1. 代理静态资源:如 http://localhost/static/index.html
         location /upload/ {
             alias /app/upload/;
             autoindex on;
         }
+
+        # 反向代理 /api 到 localhost:8080
+        location /api/ {
+            proxy_pass http://localhost:8080/;
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+            # CORS headers
+            add_header Access-Control-Allow-Origin *;
+            add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS, PUT, DELETE';
+            add_header Access-Control-Allow-Headers 'Origin, Content-Type, Accept, Authorization';
+        }
+
+        # 反向代理 /tracker 到 localhost:6969/announce
+        location /tracker {
+            proxy_pass http://localhost:6969/announce;
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+            # CORS headers
+            add_header Access-Control-Allow-Origin *;
+            add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS, PUT, DELETE';
+            add_header Access-Control-Allow-Headers 'Origin, Content-Type, Accept, Authorization';
+        }
     }
 }
diff --git a/src/main/java/com/g9/g9backend/controller/FileController.java b/src/main/java/com/g9/g9backend/controller/FileController.java
index 1f9ccd2..7b4e339 100644
--- a/src/main/java/com/g9/g9backend/controller/FileController.java
+++ b/src/main/java/com/g9/g9backend/controller/FileController.java
@@ -1,18 +1,86 @@
 package com.g9.g9backend.controller;
 
+import com.g9.g9backend.pojo.TorrentRecord;
+import com.g9.g9backend.service.TorrentRecordService;
+import lombok.Getter;
+import lombok.Setter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+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.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.IOException;
 
 /**
  * FileController 文件控制器类,处理与文件相关的请求
  *
  * @author Seamher
  */
+@Getter
+@Setter
 @RestController
 @RequestMapping("/file")
 public class FileController {
 
+    private TorrentRecordService torrentRecordService;
+
     private final Logger logger = LoggerFactory.getLogger(FileController.class);
+
+    public FileController(TorrentRecordService torrentRecordService) {
+        this.torrentRecordService = torrentRecordService;
+    }
+
+    @PostMapping
+    public ResponseEntity<String> uploadFile(@RequestBody MultipartFile file) {
+        // 相对路径(可在资源根路径创建 upload 文件夹)
+        String UPLOAD_DIR = "upload";
+
+        // 获取原始文件名
+        String originalFilename = file.getOriginalFilename();
+        if (originalFilename.isEmpty()) {
+            return ResponseEntity.badRequest().body("文件名为空");
+        }
+
+        // 为文件名加时间戳,防止重名冲突
+        String filename = System.currentTimeMillis() + "_" + originalFilename;
+
+        // 构建文件保存目录(确保 upload 目录存在)
+        File uploadDir = new File(UPLOAD_DIR);
+        if (!uploadDir.exists()) {
+            boolean created = uploadDir.mkdirs(); // 创建目录
+            if (!created) {
+                logger.error("无法创建上传目录: " + uploadDir.getAbsolutePath());
+                return ResponseEntity.internalServerError().body("无法创建上传目录");
+            }
+        }
+
+        // 构建完整的文件路径
+        File dest = new File(uploadDir, filename);
+
+        try {
+            // 保存文件
+            file.transferTo(dest);
+        } catch (IOException e) {
+            logger.error("上传失败: " + e.getMessage(), e);
+            return ResponseEntity.internalServerError().body("上传失败");
+        }
+
+        // 返回访问 URL(你可能需要根据 Nginx 的静态资源映射来配置)
+        String url = "http://localhost:65/" + filename;
+
+        return ResponseEntity.ok(url);
+    }
+
+    @PostMapping(value = "bt")
+    public ResponseEntity<String> uploadBTFile(@RequestBody TorrentRecord torrentRecord) {
+        torrentRecordService.save(torrentRecord);
+
+        return ResponseEntity.ok("");
+    }
+
 }
diff --git a/src/main/java/com/g9/g9backend/service/TrackerService.java b/src/main/java/com/g9/g9backend/service/TrackerService.java
new file mode 100644
index 0000000..3d30d6c
--- /dev/null
+++ b/src/main/java/com/g9/g9backend/service/TrackerService.java
@@ -0,0 +1,26 @@
+package com.g9.g9backend.service;
+
+import com.turn.ttorrent.tracker.Tracker;
+import jakarta.annotation.PostConstruct;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+@Service
+public class TrackerService {
+
+    private final Logger logger = LoggerFactory.getLogger(TrackerService.class);
+
+    @PostConstruct
+    public void initTracker() {
+        try {
+            Tracker tracker = new Tracker(6969, "http://localhost:6969/announce");
+            tracker.setAcceptForeignTorrents(true);
+            tracker.start(false);
+            logger.info("Tracker服务器已启动,监听端口 6969");
+        } catch (Exception e) {
+            logger.error("Tracker启动失败:", e);
+        }
+    }
+
+}
diff --git a/src/main/java/com/g9/g9backend/service/impl/TorrentRecordServiceImpl.java b/src/main/java/com/g9/g9backend/service/impl/TorrentRecordServiceImpl.java
index 3739fb0..a783761 100644
--- a/src/main/java/com/g9/g9backend/service/impl/TorrentRecordServiceImpl.java
+++ b/src/main/java/com/g9/g9backend/service/impl/TorrentRecordServiceImpl.java
@@ -4,6 +4,8 @@
 import com.g9.g9backend.mapper.TorrentRecordMapper;
 import com.g9.g9backend.pojo.TorrentRecord;
 import com.g9.g9backend.service.TorrentRecordService;
+import org.springframework.stereotype.Service;
 
+@Service
 public class TorrentRecordServiceImpl extends ServiceImpl<TorrentRecordMapper, TorrentRecord> implements TorrentRecordService {
 }
diff --git a/src/test/java/com/g9/g9backend/controller/FileControllerTest.java b/src/test/java/com/g9/g9backend/controller/FileControllerTest.java
new file mode 100644
index 0000000..a8b3d01
--- /dev/null
+++ b/src/test/java/com/g9/g9backend/controller/FileControllerTest.java
@@ -0,0 +1,82 @@
+package com.g9.g9backend.controller;
+
+import com.g9.g9backend.pojo.TorrentRecord;
+import com.g9.g9backend.service.TorrentRecordService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+
+import static org.mockito.Mockito.verify;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.hamcrest.Matchers.containsString;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+
+public class FileControllerTest {
+
+    private MockMvc mockMvc;
+
+    @InjectMocks
+    private FileController fileController;
+
+    @Mock
+    private TorrentRecordService torrentRecordService;
+
+    @BeforeEach
+    public void setup() {
+        MockitoAnnotations.openMocks(this);
+        mockMvc = MockMvcBuilders.standaloneSetup(fileController).build();
+    }
+
+    // 测试上传文件接口(模拟 multipart/form-data)
+    @Test
+    public void testUploadFile_success() throws Exception {
+        MockMultipartFile mockFile = new MockMultipartFile(
+                "file",                // 参数名,必须为 file
+                "test.txt",            // 文件名
+                "text/plain",          // MIME 类型
+                "test content".getBytes(StandardCharsets.UTF_8) // 内容
+        );
+
+        mockMvc.perform(multipart("/file").file(mockFile))
+                .andExpect(status().isOk())
+                .andExpect(content().string(containsString("http://localhost:65/")));
+    }
+
+    // 测试上传 BT 文件接口
+    @Test
+    public void testUploadBTFile_success() throws Exception {
+        TorrentRecord record = new TorrentRecord();
+        record.setTorrentRecordId(1);
+        record.setTorrentUrl("test.torrent");
+        record.setInfoHash("abc123");
+        record.setUploaderUserId(1);
+        record.setUploadTime(new Date());
+
+        // 模拟 post 请求,传 json 数据
+        mockMvc.perform(post("/file/bt")
+                        .contentType("application/json")
+                        .content("""
+                                {
+                                  "torrentRecordId": 1,
+                                  "torrentUrl": "test.torrent",
+                                  "infoHash": "abc123",
+                                  "uploaderUserId": 1,
+                                  "uploadTime": "2025-06-09T10:00:00"
+                                }
+                                """))
+                .andExpect(status().isOk());
+
+        // 验证 service 是否调用
+        verify(torrentRecordService).save(org.mockito.ArgumentMatchers.any(TorrentRecord.class));
+    }
+}