Merge "docker文件分开执行 不再使用compose"
diff --git a/Dockerfile b/Dockerfile
index 2d4db7a..b646295 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,41 +1,14 @@
-# 构建阶段
-FROM maven:3.9.6-amazoncorretto-17 AS build
-
-# 设置工作目录
-WORKDIR /build
-
-# 复制 pom.xml
-COPY pom.xml .
-
-# 下载依赖
-RUN mvn dependency:go-offline
-
-# 复制源代码
-COPY src ./src
-
-# 构建项目
-RUN mvn clean package -DskipTests
-
-# 运行阶段
-FROM amazoncorretto:17 AS final
+# 使用官方 OpenJDK 镜像
+FROM eclipse-temurin:17-jdk-jammy
 
 # 设置工作目录
 WORKDIR /team12
 
-# 设置时区
-ENV TZ=Asia/Shanghai
-RUN yum update -y && \
-    yum install -y tzdata && \
-    ln -fs /usr/share/zoneinfo/$TZ /etc/localtime && \
-    echo $TZ > /etc/timezone && \
-    yum clean all && \
-    rm -rf /var/cache/yum
+# 复制 JAR 文件到容器
+COPY target/*.jar team12.jar
 
-# 从构建阶段复制构建好的 jar 文件
-COPY --from=build /build/target/*.jar team12.jar
-
-# 暴露端口
+# 暴露端口(与 application.yml/server.port 一致)
 EXPOSE 8080
 
-# 启动命令
+# 启动应用
 ENTRYPOINT ["java", "-jar", "team12.jar"]
\ No newline at end of file
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..7057dbd
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,20 @@
+server {
+    listen 80;
+    server_name team12.10813352.xyz;  # 替换为你的域名
+
+    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;
+
+         # 长连接支持
+         proxy_http_version 1.1;
+         proxy_set_header Connection "";
+
+         # 超时设置
+         proxy_connect_timeout 60s;
+         proxy_send_timeout 60s;
+         proxy_read_timeout 60s;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pt/controller/AdminController.java b/src/main/java/com/pt/controller/AdminController.java
index 1ddfc92..22e2b86 100644
--- a/src/main/java/com/pt/controller/AdminController.java
+++ b/src/main/java/com/pt/controller/AdminController.java
@@ -31,7 +31,9 @@
         Admin admin = adminService.findByUsernameAndPassword(username, password);
         if (admin != null) {
             ans.put("result", "Login successful");
-            ans.put("token", JWTUtils.generateToken(username, Constants.UserRole.ADMIN, (int)1.2e8));
+            ans.put("data", Map.of(
+                    "token", JWTUtils.generateToken(username, Constants.UserRole.ADMIN, (int)1.2e8)
+            ));
             return ResponseEntity.ok().body(ans);
         } else {
             ans.put("result", "Invalid username or password");
diff --git a/src/main/java/com/pt/controller/CommentController.java b/src/main/java/com/pt/controller/CommentController.java
index a85d131..17dabb3 100644
--- a/src/main/java/com/pt/controller/CommentController.java
+++ b/src/main/java/com/pt/controller/CommentController.java
@@ -72,7 +72,9 @@
 
         List<Comment> comments = commentService.getCommentsByPostId(postId);
         ans.put("result", "Comments retrieved successfully");
-        ans.put("comments", comments);
+        ans.put("data", Map.of(
+           "comments", comments
+        ));
         return ResponseEntity.ok(ans);
     }
 }
diff --git a/src/main/java/com/pt/controller/PostController.java b/src/main/java/com/pt/controller/PostController.java
index 387cc2d..0ab86ed 100644
--- a/src/main/java/com/pt/controller/PostController.java
+++ b/src/main/java/com/pt/controller/PostController.java
@@ -92,7 +92,9 @@
             posts.removeIf(post -> !post.getPublishDate().toString().equals(date));
         }
         ans.put("result", "Post retrieved successfully");
-        ans.put("post", posts);
+        ans.put("data", Map.of(
+                "post", posts
+        ));
         return ResponseEntity.ok(ans);
     }
 
diff --git a/src/main/java/com/pt/controller/ResourceController.java b/src/main/java/com/pt/controller/ResourceController.java
index 8e08e16..411e7bc 100644
--- a/src/main/java/com/pt/controller/ResourceController.java
+++ b/src/main/java/com/pt/controller/ResourceController.java
@@ -9,6 +9,7 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.io.IOException;
 import java.util.HashMap;
@@ -60,11 +61,14 @@
     }
 
     @PostMapping("/publish")
-    public ResponseEntity<?> publishResource(@RequestHeader("token") String token,
-                                             @RequestParam("username") String username,
-                                             @RequestParam("size") double size,
-                                             @RequestParam("name") String name,
-                                             @RequestParam("description") String description) {
+    public ResponseEntity<?> publishResource(
+            @RequestHeader("token") String token,
+            @RequestParam("username") String username,
+            @RequestParam("size") double size,
+            @RequestParam("name") String name,
+            @RequestParam("description") String description,
+            @RequestParam("torrent") MultipartFile torrentFile) {
+
         Map<String, Object> ans = new HashMap<>();
 
         if (!JWTUtils.checkToken(token, username, Constants.UserRole.USER)) {
@@ -79,7 +83,8 @@
         }
 
         try {
-            resourceService.publishResource(name, description, username, size);
+            // 传入种子文件字节,同时传入资源其他信息
+            resourceService.publishResource(name, description, username, size, torrentFile.getBytes());
         } catch (Exception e) {
             ans.put("result", "Failed to publish resource: " + e.getMessage());
             return ResponseEntity.status(500).body(ans);
@@ -91,6 +96,7 @@
 
 
 
+
     @GetMapping("/get/{resourceId}")
     public ResponseEntity<?> getResourceById(@PathVariable("resourceId") int resourceId,
                                               @RequestHeader("token") String token,
diff --git a/src/main/java/com/pt/controller/TrackerController.java b/src/main/java/com/pt/controller/TrackerController.java
new file mode 100644
index 0000000..1e0302d
--- /dev/null
+++ b/src/main/java/com/pt/controller/TrackerController.java
@@ -0,0 +1,36 @@
+package com.pt.controller;
+
+import com.pt.service.TrackerService;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.util.Map;
+
+@RestController
+public class TrackerController {
+
+    @Autowired
+    private TrackerService trackerService;
+
+    @GetMapping("/announce")
+    public void announce(HttpServletRequest request, HttpServletResponse response) throws IOException {
+        try {
+            String ip = request.getRemoteAddr();
+            Map<String, String[]> params = request.getParameterMap();
+
+            byte[] bencodedResponse = trackerService.handleAnnounce(params, ip);
+
+            response.setContentType("application/x-bittorrent");
+            response.getOutputStream().write(bencodedResponse);
+        } catch (Exception e) {
+            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+            response.setContentType("text/plain");
+            response.getWriter().write("Tracker internal error: " + e.getMessage());
+        }
+    }
+
+}
+
diff --git a/src/main/java/com/pt/controller/UserController.java b/src/main/java/com/pt/controller/UserController.java
index 3978831..7603f9d 100644
--- a/src/main/java/com/pt/controller/UserController.java
+++ b/src/main/java/com/pt/controller/UserController.java
@@ -43,8 +43,7 @@
             userService.save(newUser);
 
             Map<String, Object> ans = new HashMap<>();
-            ans.put("success", true);
-            ans.put("message", "User registered successfully");
+            ans.put("result", "User registered successfully");
             ans.put("data", newUser);
 
             return ResponseEntity.ok().body(ans);
@@ -52,9 +51,9 @@
     }
 
     @PostMapping("/login")
-    public ResponseEntity<?> loginUser(@RequestBody Map<String, String> request) {
-        String username = request.get("username");
-        String password = request.get("password");
+    public ResponseEntity<?> loginUser(@RequestParam("username") String username,
+                                       @RequestParam("password") String password) {
+
 
         if (username == null || password == null) {
             return ResponseEntity.badRequest().body("Missing username or password");
@@ -64,16 +63,11 @@
         Map<String, Object> ans = new HashMap<>();
         if (user != null) {
             String token = JWTUtils.generateToken(username, Constants.UserRole.USER, Constants.DEFAULT_EXPIRE_TIME);
-            ans.put("success", true);
-            ans.put("message", "Login successful");
-            ans.put("data", Map.of(
-                "token", token,
-                "user", user
-            ));
+            ans.put("result", "Login successful");
+            ans.put("data", token);
             return ResponseEntity.ok().body(ans);
         } else {
-            ans.put("success", false);
-            ans.put("message", "Invalid username or password");
+            ans.put("result", "Invalid username or password");
             return ResponseEntity.badRequest().body(ans);
         }
     }
@@ -82,17 +76,21 @@
     public ResponseEntity<?> updateUsername(@RequestHeader("token") String token,
                                             @RequestParam("username") String oldUsername,
                                             @RequestParam("newUsername") String newUsername) {
+        Map<String, Object> ans = new HashMap<>();
         if(!JWTUtils.checkToken(token, oldUsername, Constants.UserRole.USER)) {
-            return ResponseEntity.badRequest().body("Invalid token");
+            ans.put("result", "Invalid token");
+            return ResponseEntity.badRequest().body(ans);
         }
 
         User user = userService.findByUsername(oldUsername);
         if (user != null) {
             user.setUsername(newUsername);
             userService.save(user);
-            return ResponseEntity.ok("Username updated successfully");
+            ans.put("result", "Username updated successfully");
+            return ResponseEntity.ok(ans);
         } else {
-            return ResponseEntity.badRequest().body("User not found");
+            ans.put("result", "User not found");
+            return ResponseEntity.badRequest().body(ans);
         }
     }
 
@@ -100,17 +98,21 @@
     public ResponseEntity<?> updatePassword(@RequestHeader("token") String token,
                                             @RequestParam("username") String username,
                                             @RequestParam("newPassword") String newPassword) {
+        Map<String, Object> ans = new HashMap<>();
         if(!JWTUtils.checkToken(token, username, Constants.UserRole.USER)) {
-            return ResponseEntity.badRequest().body("Invalid token");
+            ans.put("result", "Invalid token");
+            return ResponseEntity.badRequest().body(ans);
         }
 
         User user = userService.findByUsername(username);
         if (user != null) {
             user.setPassword(newPassword);
             userService.save(user);
-            return ResponseEntity.ok("Password updated successfully");
+            ans.put("result", "Password updated successfully");
+            return ResponseEntity.ok(ans);
         } else {
-            return ResponseEntity.badRequest().body("Invalid username or password");
+            ans.put("result", "Invalid username or password");
+            return ResponseEntity.badRequest().body(ans);
         }
     }
 
@@ -118,17 +120,22 @@
     public ResponseEntity<?> updateEmail(@RequestHeader("token") String token,
                                          @RequestParam("username") String username,
                                          @RequestParam("newEmail") String newEmail) {
+
+        Map<String, Object> ans = new HashMap<>();
         if(!JWTUtils.checkToken(token, username, Constants.UserRole.USER)) {
-            return ResponseEntity.badRequest().body("Invalid token");
+            ans.put("result", "Invalid token");
+            return ResponseEntity.badRequest().body(ans);
         }
 
         User user = userService.findByUsername(username);
         if (user != null) {
             user.setEmail(newEmail);
             userService.save(user);
-            return ResponseEntity.ok("Email updated successfully");
+            ans.put("result", "Email updated successfully");
+            return ResponseEntity.ok(ans);
         } else {
-            return ResponseEntity.badRequest().body("User not found");
+            ans.put("result", "User not found");
+            return ResponseEntity.badRequest().body(ans);
         }
     }
 
@@ -137,16 +144,20 @@
                                         @RequestParam("username") String username,
                                         @RequestParam("targetUsername") String targetUsername
     ) {
+        Map<String, Object> ans = new HashMap<>();
         if(!JWTUtils.checkToken(token, username, Constants.UserRole.ADMIN)) {
-            return ResponseEntity.badRequest().body("Invalid token");
+            ans.put("result", "Invalid token");
+            return ResponseEntity.badRequest().body(ans);
         }
 
         User user = userService.findByUsername(targetUsername);
         if (user != null) {
             userService.deleteById(user.getUid());
-            return ResponseEntity.ok("User deleted successfully");
+            ans.put("result", "User deleted successfully");
+            return ResponseEntity.ok(ans);
         } else {
-            return ResponseEntity.badRequest().body("User not found");
+            ans.put("result", "User not found");
+            return ResponseEntity.badRequest().body(ans);
         }
     }
 
@@ -159,26 +170,30 @@
 
         Map<String, Object> ans = new HashMap<>();
         ans.put("result", "User list retrieved successfully");
-        ans.put("amount", userService.listAll().size());
-        ans.put("users", userService.listAll());
+        ans.put("data", Map.of(
+                "amount", userService.listAll().size(),
+                "users", userService.listAll()
+        ));
         return ResponseEntity.ok(ans);
     }
 
     @GetMapping("/get/info")
     public ResponseEntity<?> getUserInfo(@RequestHeader("token") String token,
                                          @RequestParam("username") String username) {
+        Map<String, Object> ans = new HashMap<>();
         if(!JWTUtils.checkToken(token, username, Constants.UserRole.USER)) {
-            return ResponseEntity.badRequest().body("Invalid token");
+            ans.put("result", "Invalid token");
+            return ResponseEntity.badRequest().body(ans);
         }
 
         User user = userService.findByUsername(username);
         if (user != null) {
-            Map<String, Object> ans = new HashMap<>();
             ans.put("result", "User info retrieved successfully");
-            ans.put("user", user);
+            ans.put("data", user);
             return ResponseEntity.ok(ans);
         } else {
-            return ResponseEntity.badRequest().body("User not found");
+            ans.put("result", "User not found");
+            return ResponseEntity.badRequest().body(ans);
         }
     }
 }
diff --git a/src/main/java/com/pt/entity/PeerInfoEntity.java b/src/main/java/com/pt/entity/PeerInfoEntity.java
new file mode 100644
index 0000000..081df7f
--- /dev/null
+++ b/src/main/java/com/pt/entity/PeerInfoEntity.java
@@ -0,0 +1,66 @@
+package com.pt.entity;
+
+import jakarta.persistence.*;
+import java.time.LocalDateTime;
+
+@Entity
+public class PeerInfoEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    private String infoHash;
+    private String ip;
+    private int port;
+    private String peerId;
+    private LocalDateTime lastSeen;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getInfoHash() {
+        return infoHash;
+    }
+
+    public void setInfoHash(String infoHash) {
+        this.infoHash = infoHash;
+    }
+
+    public String getIp() {
+        return ip;
+    }
+
+    public void setIp(String ip) {
+        this.ip = ip;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public void setPort(int port) {
+        this.port = port;
+    }
+
+    public LocalDateTime getLastSeen() {
+        return lastSeen;
+    }
+
+    public void setLastSeen(LocalDateTime lastSeen) {
+        this.lastSeen = lastSeen;
+    }
+
+    public String getPeerId() {
+        return peerId;
+    }
+
+    public void setPeerId(String peerId) {
+        this.peerId = peerId;
+    }
+}
diff --git a/src/main/java/com/pt/entity/TorrentMeta.java b/src/main/java/com/pt/entity/TorrentMeta.java
index 3823181..2c690f7 100644
--- a/src/main/java/com/pt/entity/TorrentMeta.java
+++ b/src/main/java/com/pt/entity/TorrentMeta.java
@@ -17,6 +17,7 @@
     private String infoHash;
     private Long size;
     private LocalDateTime uploadTime;
+    private byte[] torrentData;
 
     public Long getId() {
         return id;
@@ -57,4 +58,12 @@
     public void setUploadTime(LocalDateTime uploadTime) {
         this.uploadTime = uploadTime;
     }
+
+    public byte[] getTorrentData() {
+        return torrentData;
+    }
+
+    public void setTorrentData(byte[] torrentData) {
+        this.torrentData = torrentData;
+    }
 }
\ No newline at end of file
diff --git a/src/main/java/com/pt/repository/PeerInfoRepository.java b/src/main/java/com/pt/repository/PeerInfoRepository.java
new file mode 100644
index 0000000..565204c
--- /dev/null
+++ b/src/main/java/com/pt/repository/PeerInfoRepository.java
@@ -0,0 +1,9 @@
+package com.pt.repository;
+
+import com.pt.entity.PeerInfoEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import java.util.List;
+
+public interface PeerInfoRepository extends JpaRepository<PeerInfoEntity, Long> {
+    List<PeerInfoEntity> findByInfoHash(String infoHash);
+}
diff --git a/src/main/java/com/pt/service/ResourceService.java b/src/main/java/com/pt/service/ResourceService.java
index 1d0e797..d85315e 100644
--- a/src/main/java/com/pt/service/ResourceService.java
+++ b/src/main/java/com/pt/service/ResourceService.java
@@ -6,16 +6,11 @@
 import com.pt.repository.DownloadRepository;
 import com.pt.repository.ResourceRepository;
 import com.pt.repository.TorrentMetaRepository;
-import com.pt.utils.BencodeUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
 import java.time.LocalDateTime;
 import java.util.List;
-import java.util.Map;
 
 @Service
 public class ResourceService {
@@ -40,9 +35,11 @@
         return resourceRepository.findByAuthor(username);
     }
 
-    // 发布资源时,将种子文件二进制数据保存到数据库
-    public void publishResource(String name, String description, String author, double size) throws Exception {
-        // 保存资源信息到数据库,包括种子文件的二进制数据
+    public void publishResource(String name, String description, String author, double size, byte[] torrentBytes) throws Exception {
+        // 解析并保存torrent元信息
+        TorrentMeta meta = torrentService.parseAndSaveTorrent(torrentBytes);
+
+        // 保存资源信息,并关联torrent信息
         Resource resource = new Resource();
         resource.setName(name);
         resource.setDescription(description);
@@ -50,10 +47,8 @@
         resource.setSize(size);
         resource.setPublishTime(LocalDateTime.now());
 
-        // 生成种子数据 byte[]
-        byte[] torrentData = torrentService.generateTorrentBytes("uploads/" + name);
-        resource.setTorrentData(torrentData);
-
+        resource.setTorrentData(torrentBytes);
+        // 这里可以保存torrent文件路径,或直接存数据库,依据你的设计
         resourceRepository.save(resource);
     }
 
diff --git a/src/main/java/com/pt/service/TorrentService.java b/src/main/java/com/pt/service/TorrentService.java
index dcbee69..1fdb700 100644
--- a/src/main/java/com/pt/service/TorrentService.java
+++ b/src/main/java/com/pt/service/TorrentService.java
@@ -2,77 +2,62 @@
 
 import com.pt.entity.TorrentMeta;
 import com.pt.repository.TorrentMetaRepository;
-import com.pt.utils.BencodeUtils;
+import com.pt.utils.BencodeCodec;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-import java.io.*;
 import java.security.MessageDigest;
 import java.time.LocalDateTime;
-import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 
 @Service
 public class TorrentService {
 
-    //  Tracker 服务器的 announce 地址
-    private static final String ANNOUNCE_URL = "http://localhost:8080/announce";
-    private static final int PIECE_LENGTH = 256 * 1024;
-
     @Autowired
     private TorrentMetaRepository torrentMetaRepository;
 
-    public byte[] generateTorrentBytes(String sourceFilePath) throws Exception {
-        File sourceFile = new File(sourceFilePath);
-
-        // 构造 info 字典
-        Map<String, Object> infoDict = new LinkedHashMap<>();
-        infoDict.put("name", sourceFile.getName());
-        infoDict.put("length", sourceFile.length());
-        infoDict.put("piece length", PIECE_LENGTH);
-        infoDict.put("pieces", calcPiecesHashes(sourceFile));
-
-        // 构造完整 torrent 字典
-        Map<String, Object> torrentDict = new LinkedHashMap<>();
-        torrentDict.put("announce", ANNOUNCE_URL);
-        torrentDict.put("info", infoDict);
-
-        // 编码成种子数据字节数组
-        byte[] bencodedTorrent = BencodeUtils.encode(torrentDict);
-
-        // 计算 info_hash 并保存到数据库(如果需要的话)
-        MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
-        byte[] infoEncoded = BencodeUtils.encode(infoDict);
-        sha1.update(infoEncoded);
-        String infoHash = bytesToHex(sha1.digest());
-
-        TorrentMeta meta = new TorrentMeta();
-        meta.setFilename(sourceFile.getName());
-        meta.setInfoHash(infoHash);
-        meta.setSize(sourceFile.length());
-        meta.setUploadTime(LocalDateTime.now());
-        torrentMetaRepository.save(meta);
-
-        return bencodedTorrent;
-    }
-
-
-    private byte[] calcPiecesHashes(File file) throws Exception {
-        MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
-        try (InputStream fis = new FileInputStream(file)) {
-            byte[] buffer = new byte[PIECE_LENGTH];
-            ByteArrayOutputStream piecesBuffer = new ByteArrayOutputStream();
-
-            int read;
-            while ((read = fis.read(buffer)) > 0) {
-                sha1.reset();
-                sha1.update(buffer, 0, read);
-                piecesBuffer.write(sha1.digest());
-            }
-            return piecesBuffer.toByteArray();
+    public TorrentMeta parseAndSaveTorrent(byte[] torrentBytes) throws Exception {
+        Map<String, Object> torrentDict = (Map<String, Object>) BencodeCodec.decode(torrentBytes);
+        if (!torrentDict.containsKey("info")) {
+            throw new IllegalArgumentException("Invalid torrent file: missing 'info' dictionary");
         }
+
+        Map<String, Object> infoDict = (Map<String, Object>) torrentDict.get("info");
+
+        // 计算 info_hash
+        byte[] infoEncoded = BencodeCodec.encode(infoDict);
+        MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+        byte[] infoHashBytes = sha1.digest(infoEncoded);
+        String infoHash = bytesToHex(infoHashBytes);
+
+        // 获取文件名,大小等
+        String name = (String) infoDict.get("name");
+        long length = 0;
+        if (infoDict.containsKey("length")) {
+            length = ((Number) infoDict.get("length")).longValue();
+        } else if (infoDict.containsKey("files")) {
+            long totalLength = 0;
+            List<Map<String, Object>> files = (List<Map<String, Object>>) infoDict.get("files");
+            for (Map<String, Object> file : files) {
+                totalLength += ((Number) file.get("length")).longValue();
+            }
+            length = totalLength;
+        }
+
+        // 保存到数据库
+        TorrentMeta meta = new TorrentMeta();
+        meta.setFilename(name);
+        meta.setInfoHash(infoHash);
+        meta.setSize(length);
+        meta.setUploadTime(LocalDateTime.now());
+        meta.setTorrentData(torrentBytes);
+
+        torrentMetaRepository.save(meta);
+        return meta;
     }
 
+
     private String bytesToHex(byte[] bytes) {
         StringBuilder sb = new StringBuilder();
         for (byte b : bytes) {
@@ -80,4 +65,4 @@
         }
         return sb.toString();
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/pt/service/TrackerService.java b/src/main/java/com/pt/service/TrackerService.java
new file mode 100644
index 0000000..7167ff3
--- /dev/null
+++ b/src/main/java/com/pt/service/TrackerService.java
@@ -0,0 +1,76 @@
+package com.pt.service;
+
+import com.pt.entity.TorrentMeta;
+import com.pt.repository.TorrentMetaRepository;
+import com.pt.utils.BencodeCodec;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+@Service
+public class TrackerService {
+
+    private final Map<String, List<PeerInfo>> torrentPeers = new ConcurrentHashMap<>();
+
+    @Autowired
+    private TorrentMetaRepository torrentMetaRepository;
+
+    public byte[] handleAnnounce(Map<String, String[]> params, String ipAddress) {
+        try {
+            if (!params.containsKey("info_hash") || !params.containsKey("peer_id") || !params.containsKey("port")) {
+                return BencodeCodec.encode(Map.of("failure reason", "Missing required parameters"));
+            }
+
+            String infoHash = decodeParam(params.get("info_hash")[0]);
+            TorrentMeta meta = torrentMetaRepository.findByInfoHash(infoHash);
+            if (meta == null) {
+                return BencodeCodec.encode(Map.of("failure reason", "Invalid info_hash"));
+            }
+
+            String peerId = decodeParam(params.get("peer_id")[0]);
+            int port = Integer.parseInt(params.get("port")[0]);
+
+            PeerInfo peer = new PeerInfo(ipAddress, port, peerId);
+
+            torrentPeers.computeIfAbsent(infoHash, k -> new CopyOnWriteArrayList<>());
+            List<PeerInfo> peers = torrentPeers.get(infoHash);
+
+            boolean exists = peers.stream().anyMatch(p -> p.peerId.equals(peerId));
+            if (!exists) {
+                peers.add(peer);
+            }
+
+            List<String> ips = peers.stream().map(p -> p.ip).toList();
+            List<Integer> ports = peers.stream().map(p -> p.port).toList();
+            byte[] peerBytes = BencodeCodec.buildCompactPeers(ips, ports);
+
+            return BencodeCodec.buildTrackerResponse(1800, peerBytes);
+        } catch (Exception e) {
+            return BencodeCodec.encode(Map.of("failure reason", "Internal server error"));
+        }
+    }
+
+    private String decodeParam(String raw) {
+        return new String(raw.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
+    }
+
+    private static class PeerInfo {
+        String ip;
+        int port;
+        String peerId;
+
+        public PeerInfo(String ip, int port, String peerId) {
+            this.ip = ip;
+            this.port = port;
+            this.peerId = peerId;
+        }
+    }
+}
+
diff --git a/src/main/java/com/pt/testResponse/TestController.java b/src/main/java/com/pt/testResponse/TestController.java
new file mode 100644
index 0000000..a0dbae9
--- /dev/null
+++ b/src/main/java/com/pt/testResponse/TestController.java
@@ -0,0 +1,15 @@
+package com.pt.testResponse;
+
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@CrossOrigin(origins = "*")
+public class TestController {
+
+    @GetMapping("/api/test")
+    public String testResponse() {
+        return "Hello, world!";
+    }
+}
diff --git a/src/main/java/com/pt/utils/BencodeCodec.java b/src/main/java/com/pt/utils/BencodeCodec.java
new file mode 100644
index 0000000..2117d43
--- /dev/null
+++ b/src/main/java/com/pt/utils/BencodeCodec.java
@@ -0,0 +1,216 @@
+package com.pt.utils;
+
+import java.io.*;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+public class BencodeCodec {
+
+    /* ------------- 编码部分 ------------- */
+
+    public static void encode(Object obj, OutputStream out) throws IOException {
+        if (obj instanceof String) {
+            encodeString((String) obj, out);
+        } else if (obj instanceof Number) {
+            encodeInteger(((Number) obj).longValue(), out);
+        } else if (obj instanceof byte[]) {
+            encodeBytes((byte[]) obj, out);
+        } else if (obj instanceof List) {
+            encodeList((List<?>) obj, out);
+        } else if (obj instanceof Map) {
+            encodeMap((Map<String, Object>) obj, out);
+        } else {
+            throw new IllegalArgumentException("Unsupported type: " + obj.getClass());
+        }
+    }
+
+    public static byte[] encode(Object obj) {
+        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+            encode(obj, baos);
+            return baos.toByteArray();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static void encodeString(String s, OutputStream out) throws IOException {
+        byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
+        out.write(String.valueOf(bytes.length).getBytes(StandardCharsets.US_ASCII));
+        out.write(':');
+        out.write(bytes);
+    }
+
+    private static void encodeBytes(byte[] bytes, OutputStream out) throws IOException {
+        out.write(String.valueOf(bytes.length).getBytes(StandardCharsets.US_ASCII));
+        out.write(':');
+        out.write(bytes);
+    }
+
+    private static void encodeInteger(long value, OutputStream out) throws IOException {
+        out.write('i');
+        out.write(Long.toString(value).getBytes(StandardCharsets.US_ASCII));
+        out.write('e');
+    }
+
+    private static void encodeList(List<?> list, OutputStream out) throws IOException {
+        out.write('l');
+        for (Object item : list) {
+            encode(item, out);
+        }
+        out.write('e');
+    }
+
+    private static void encodeMap(Map<String, Object> map, OutputStream out) throws IOException {
+        out.write('d');
+        List<String> keys = new ArrayList<>(map.keySet());
+        Collections.sort(keys);  // bencode字典必须按key排序
+        for (String key : keys) {
+            encodeString(key, out);
+            encode(map.get(key), out);
+        }
+        out.write('e');
+    }
+
+    /* ------------- 解码部分 ------------- */
+
+    public static Object decode(byte[] data) throws IOException {
+        try (ByteArrayInputStream in = new ByteArrayInputStream(data)) {
+            in.mark(data.length);
+            return decodeNext(in);
+        }
+    }
+
+    private static Object decodeNext(InputStream in) throws IOException {
+        int prefix = in.read();
+        if (prefix == -1) {
+            throw new IOException("Unexpected end of stream");
+        }
+
+        in.mark(1024);
+
+        if (prefix >= '0' && prefix <= '9') {
+            in.reset();
+            return parseString(in);
+        } else if (prefix == 'i') {
+            return parseInteger(in);
+        } else if (prefix == 'l') {
+            return parseList(in);
+        } else if (prefix == 'd') {
+            return parseDict(in);
+        } else {
+            throw new IOException("Invalid bencode prefix: " + (char) prefix);
+        }
+    }
+
+    private static String parseString(InputStream in) throws IOException {
+        StringBuilder lenStr = new StringBuilder();
+        int b;
+        while ((b = in.read()) != -1 && b != ':') {
+            if (b < '0' || b > '9') {
+                throw new IOException("Invalid string length character: " + (char) b);
+            }
+            lenStr.append((char) b);
+        }
+        if (b == -1) {
+            throw new IOException("Unexpected end of stream reading string length");
+        }
+        int length = Integer.parseInt(lenStr.toString());
+
+        byte[] buf = new byte[length];
+        int offset = 0;
+        while (offset < length) {
+            int read = in.read(buf, offset, length - offset);
+            if (read == -1) {
+                throw new IOException("Unexpected end of stream reading string data");
+            }
+            offset += read;
+        }
+
+        return new String(buf, StandardCharsets.UTF_8);
+    }
+
+    private static long parseInteger(InputStream in) throws IOException {
+        StringBuilder intStr = new StringBuilder();
+        int b;
+        while ((b = in.read()) != -1 && b != 'e') {
+            intStr.append((char) b);
+        }
+        if (b == -1) {
+            throw new IOException("Unexpected end of stream reading integer");
+        }
+        return Long.parseLong(intStr.toString());
+    }
+
+    private static List<Object> parseList(InputStream in) throws IOException {
+        List<Object> list = new ArrayList<>();
+        int b;
+        while (true) {
+            in.mark(1);
+            b = in.read();
+            if (b == -1) {
+                throw new IOException("Unexpected end of stream reading list");
+            }
+            if (b == 'e') {
+                break;
+            }
+            in.reset();
+            list.add(decodeNext(in));
+        }
+        return list;
+    }
+
+    private static Map<String, Object> parseDict(InputStream in) throws IOException {
+        Map<String, Object> map = new LinkedHashMap<>();
+        int b;
+        while (true) {
+            in.mark(1);
+            b = in.read();
+            if (b == -1) {
+                throw new IOException("Unexpected end of stream reading dictionary");
+            }
+            if (b == 'e') {
+                break;
+            }
+            in.reset();
+            String key = (String) decodeNext(in);
+            Object value = decodeNext(in);
+            map.put(key, value);
+        }
+        return map;
+    }
+
+    /* ------------- 其他辅助方法 ------------- */
+
+    // 构造单个compact peer的二进制格式 (4字节IP + 2字节端口)
+    public static byte[] buildCompactPeer(String ip, int port) {
+        try {
+            InetAddress addr = InetAddress.getByName(ip);
+            ByteBuffer buffer = ByteBuffer.allocate(6);
+            buffer.put(addr.getAddress());
+            buffer.putShort((short) port);
+            return buffer.array();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    // 构造多个compact peer的二进制拼接
+    public static byte[] buildCompactPeers(List<String> ips, List<Integer> ports) {
+        if (ips.size() != ports.size()) throw new IllegalArgumentException("IPs and ports list size mismatch");
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        for (int i = 0; i < ips.size(); i++) {
+            out.write(buildCompactPeer(ips.get(i), ports.get(i)), 0, 6);
+        }
+        return out.toByteArray();
+    }
+
+    // 构造tracker响应字典,至少包含interval和peers
+    public static byte[] buildTrackerResponse(int interval, byte[] peersCompact) {
+        Map<String, Object> dict = new LinkedHashMap<>();
+        dict.put("interval", interval);
+        dict.put("peers", peersCompact);
+        return encode(dict);
+    }
+}