tracker服务器以及torrent上传下载逻辑&依赖&种子文件

Change-Id: I8cb04f663faf1f4d0fadb0c4585ba12bc0dd929c
diff --git a/src/main/java/edu/bjtu/groupone/backend/api/TorrentController.java b/src/main/java/edu/bjtu/groupone/backend/api/TorrentController.java
new file mode 100644
index 0000000..e5f1aa4
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/api/TorrentController.java
@@ -0,0 +1,53 @@
+package edu.bjtu.groupone.backend.api;
+
+import edu.bjtu.groupone.backend.domain.entity.Torrent;
+import edu.bjtu.groupone.backend.service.TorrentService;
+import edu.bjtu.groupone.backend.utils.GetTokenUserId;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.Resource;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+@RestController
+@RequestMapping({"/api/torrents"})
+public class TorrentController {
+    @Autowired
+    private TorrentService torrentService;
+
+    public TorrentController() {
+    }
+
+    @PostMapping({"/upload"})
+    public ResponseEntity<?> uploadTorrent(@RequestParam("file") MultipartFile file, HttpServletRequest request) {
+        String uidStr = GetTokenUserId.getUserId(request);
+        if (uidStr == null) {
+            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Token无效或缺失");
+        } else {
+            try {
+                Long userId = Long.parseLong(uidStr);
+                Torrent saved = this.torrentService.uploadTorrent(file, userId);
+                return ResponseEntity.ok(saved);
+            } catch (Exception var6) {
+                return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("上传失败:" + var6.getMessage());
+            }
+        }
+    }
+
+    @GetMapping({"/download/{infoHash}"})
+    public ResponseEntity<Resource> downloadTorrent(@PathVariable String infoHash) {
+        try {
+            Resource resource = this.torrentService.downloadTorrent(infoHash);
+            return ((ResponseEntity.BodyBuilder)ResponseEntity.ok().header("Content-Disposition", new String[]{"attachment; filename=\"" + infoHash + ".torrent\""})).body(resource);
+        } catch (Exception var3) {
+            return ResponseEntity.notFound().build();
+        }
+    }
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/config/TrackerStarter.java b/src/main/java/edu/bjtu/groupone/backend/config/TrackerStarter.java
new file mode 100644
index 0000000..6ae8be7
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/config/TrackerStarter.java
@@ -0,0 +1,50 @@
+package edu.bjtu.groupone.backend.config;
+
+import com.turn.ttorrent.tracker.TrackedTorrent;
+import com.turn.ttorrent.tracker.Tracker;
+import java.io.File;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.security.NoSuchAlgorithmException;
+import java.util.Objects;
+import javax.annotation.PostConstruct;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+@Component
+public class TrackerStarter {
+    private Tracker tracker;
+
+    public TrackerStarter() {
+    }
+
+    @PostConstruct
+    public void startTracker() throws Exception {
+        InetSocketAddress address = new InetSocketAddress("0.0.0.0", 6969);
+        this.tracker = new Tracker(address);
+        this.tracker.start();
+        System.out.println("Tracker started on http://localhost:6969/announce");
+    }
+
+    @Scheduled(
+            fixedRate = 60000L
+    )
+    public void scanTorrentDirectory() throws IOException, NoSuchAlgorithmException {
+        File torrentDir = new File("C:\\Users\\wangy\\Desktop\\GroupOne-Back-End(2)\\GroupOne-Back-End\\torrents");
+        if (torrentDir.exists() && torrentDir.isDirectory()) {
+            File[] var2 = (File[])Objects.requireNonNull(torrentDir.listFiles());
+            int var3 = var2.length;
+
+            for(int var4 = 0; var4 < var3; ++var4) {
+                File file = var2[var4];
+                if (file.getName().endsWith(".torrent")) {
+                    this.tracker.announce(TrackedTorrent.load(file));
+                    System.out.println("Loaded torrent: " + file.getName());
+                }
+            }
+        } else {
+            System.out.println("Torrent directory not found: " + torrentDir.getAbsolutePath());
+        }
+
+    }
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/domain/entity/Torrent.java b/src/main/java/edu/bjtu/groupone/backend/domain/entity/Torrent.java
new file mode 100644
index 0000000..0564b50
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/domain/entity/Torrent.java
@@ -0,0 +1,258 @@
+package edu.bjtu.groupone.backend.domain.entity;
+
+public class Torrent {
+    private Long id;
+    private String name;
+    private String infoHash;
+    private String filePath;
+    private Long size;
+    private Long uploaderId;
+    private Boolean isFreeleech;
+    private Double uploadMultiplier;
+    private String uploadedAt;
+
+    public Long getId() {
+        return this.id;
+    }
+
+    public String getName() {
+        return this.name;
+    }
+
+    public String getInfoHash() {
+        return this.infoHash;
+    }
+
+    public String getFilePath() {
+        return this.filePath;
+    }
+
+    public Long getSize() {
+        return this.size;
+    }
+
+    public Long getUploaderId() {
+        return this.uploaderId;
+    }
+
+    public Boolean getIsFreeleech() {
+        return this.isFreeleech;
+    }
+
+    public Double getUploadMultiplier() {
+        return this.uploadMultiplier;
+    }
+
+    public String getUploadedAt() {
+        return this.uploadedAt;
+    }
+
+    public void setId(final Long id) {
+        this.id = id;
+    }
+
+    public void setName(final String name) {
+        this.name = name;
+    }
+
+    public void setInfoHash(final String infoHash) {
+        this.infoHash = infoHash;
+    }
+
+    public void setFilePath(final String filePath) {
+        this.filePath = filePath;
+    }
+
+    public void setSize(final Long size) {
+        this.size = size;
+    }
+
+    public void setUploaderId(final Long uploaderId) {
+        this.uploaderId = uploaderId;
+    }
+
+    public void setIsFreeleech(final Boolean isFreeleech) {
+        this.isFreeleech = isFreeleech;
+    }
+
+    public void setUploadMultiplier(final Double uploadMultiplier) {
+        this.uploadMultiplier = uploadMultiplier;
+    }
+
+    public void setUploadedAt(final String uploadedAt) {
+        this.uploadedAt = uploadedAt;
+    }
+
+    public boolean equals(final Object o) {
+        if (o == this) {
+            return true;
+        } else if (!(o instanceof Torrent)) {
+            return false;
+        } else {
+            Torrent other = (Torrent)o;
+            if (!other.canEqual(this)) {
+                return false;
+            } else {
+                label119: {
+                    Object this$id = this.getId();
+                    Object other$id = other.getId();
+                    if (this$id == null) {
+                        if (other$id == null) {
+                            break label119;
+                        }
+                    } else if (this$id.equals(other$id)) {
+                        break label119;
+                    }
+
+                    return false;
+                }
+
+                Object this$size = this.getSize();
+                Object other$size = other.getSize();
+                if (this$size == null) {
+                    if (other$size != null) {
+                        return false;
+                    }
+                } else if (!this$size.equals(other$size)) {
+                    return false;
+                }
+
+                label105: {
+                    Object this$uploaderId = this.getUploaderId();
+                    Object other$uploaderId = other.getUploaderId();
+                    if (this$uploaderId == null) {
+                        if (other$uploaderId == null) {
+                            break label105;
+                        }
+                    } else if (this$uploaderId.equals(other$uploaderId)) {
+                        break label105;
+                    }
+
+                    return false;
+                }
+
+                Object this$isFreeleech = this.getIsFreeleech();
+                Object other$isFreeleech = other.getIsFreeleech();
+                if (this$isFreeleech == null) {
+                    if (other$isFreeleech != null) {
+                        return false;
+                    }
+                } else if (!this$isFreeleech.equals(other$isFreeleech)) {
+                    return false;
+                }
+
+                label91: {
+                    Object this$uploadMultiplier = this.getUploadMultiplier();
+                    Object other$uploadMultiplier = other.getUploadMultiplier();
+                    if (this$uploadMultiplier == null) {
+                        if (other$uploadMultiplier == null) {
+                            break label91;
+                        }
+                    } else if (this$uploadMultiplier.equals(other$uploadMultiplier)) {
+                        break label91;
+                    }
+
+                    return false;
+                }
+
+                Object this$name = this.getName();
+                Object other$name = other.getName();
+                if (this$name == null) {
+                    if (other$name != null) {
+                        return false;
+                    }
+                } else if (!this$name.equals(other$name)) {
+                    return false;
+                }
+
+                label77: {
+                    Object this$infoHash = this.getInfoHash();
+                    Object other$infoHash = other.getInfoHash();
+                    if (this$infoHash == null) {
+                        if (other$infoHash == null) {
+                            break label77;
+                        }
+                    } else if (this$infoHash.equals(other$infoHash)) {
+                        break label77;
+                    }
+
+                    return false;
+                }
+
+                label70: {
+                    Object this$filePath = this.getFilePath();
+                    Object other$filePath = other.getFilePath();
+                    if (this$filePath == null) {
+                        if (other$filePath == null) {
+                            break label70;
+                        }
+                    } else if (this$filePath.equals(other$filePath)) {
+                        break label70;
+                    }
+
+                    return false;
+                }
+
+                Object this$uploadedAt = this.getUploadedAt();
+                Object other$uploadedAt = other.getUploadedAt();
+                if (this$uploadedAt == null) {
+                    if (other$uploadedAt != null) {
+                        return false;
+                    }
+                } else if (!this$uploadedAt.equals(other$uploadedAt)) {
+                    return false;
+                }
+
+                return true;
+            }
+        }
+    }
+
+    protected boolean canEqual(final Object other) {
+        return other instanceof Torrent;
+    }
+
+    public int hashCode() {
+        boolean PRIME = true;
+        int result = 1;
+        Object $id = this.getId();
+        result = result * 59 + ($id == null ? 43 : $id.hashCode());
+        Object $size = this.getSize();
+        result = result * 59 + ($size == null ? 43 : $size.hashCode());
+        Object $uploaderId = this.getUploaderId();
+        result = result * 59 + ($uploaderId == null ? 43 : $uploaderId.hashCode());
+        Object $isFreeleech = this.getIsFreeleech();
+        result = result * 59 + ($isFreeleech == null ? 43 : $isFreeleech.hashCode());
+        Object $uploadMultiplier = this.getUploadMultiplier();
+        result = result * 59 + ($uploadMultiplier == null ? 43 : $uploadMultiplier.hashCode());
+        Object $name = this.getName();
+        result = result * 59 + ($name == null ? 43 : $name.hashCode());
+        Object $infoHash = this.getInfoHash();
+        result = result * 59 + ($infoHash == null ? 43 : $infoHash.hashCode());
+        Object $filePath = this.getFilePath();
+        result = result * 59 + ($filePath == null ? 43 : $filePath.hashCode());
+        Object $uploadedAt = this.getUploadedAt();
+        result = result * 59 + ($uploadedAt == null ? 43 : $uploadedAt.hashCode());
+        return result;
+    }
+
+    public String toString() {
+        Long var10000 = this.getId();
+        return "Torrent(id=" + var10000 + ", name=" + this.getName() + ", infoHash=" + this.getInfoHash() + ", filePath=" + this.getFilePath() + ", size=" + this.getSize() + ", uploaderId=" + this.getUploaderId() + ", isFreeleech=" + this.getIsFreeleech() + ", uploadMultiplier=" + this.getUploadMultiplier() + ", uploadedAt=" + this.getUploadedAt() + ")";
+    }
+
+    public Torrent(final Long id, final String name, final String infoHash, final String filePath, final Long size, final Long uploaderId, final Boolean isFreeleech, final Double uploadMultiplier, final String uploadedAt) {
+        this.id = id;
+        this.name = name;
+        this.infoHash = infoHash;
+        this.filePath = filePath;
+        this.size = size;
+        this.uploaderId = uploaderId;
+        this.isFreeleech = isFreeleech;
+        this.uploadMultiplier = uploadMultiplier;
+        this.uploadedAt = uploadedAt;
+    }
+
+    public Torrent() {
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/edu/bjtu/groupone/backend/mapper/TorrentMapper.java b/src/main/java/edu/bjtu/groupone/backend/mapper/TorrentMapper.java
new file mode 100644
index 0000000..a00d3cf
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/mapper/TorrentMapper.java
@@ -0,0 +1,9 @@
+package edu.bjtu.groupone.backend.mapper;
+
+import edu.bjtu.groupone.backend.domain.entity.Torrent;
+
+public interface TorrentMapper {
+    void insertTorrent(Torrent torrent);
+
+    Torrent selectByInfoHash(String infoHash);
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/mapper/UserMapper.java b/src/main/java/edu/bjtu/groupone/backend/mapper/UserMapper.java
index 3e14ee4..ba0763e 100644
--- a/src/main/java/edu/bjtu/groupone/backend/mapper/UserMapper.java
+++ b/src/main/java/edu/bjtu/groupone/backend/mapper/UserMapper.java
@@ -42,4 +42,7 @@
 
     @Select("SELECT * FROM user")
     List<User> selectAllUsers();
+
+    @Select({"SELECT * FROM user WHERE userid = #{id}"})
+    User selectById(@Param("id") Long id);
 }
diff --git a/src/main/java/edu/bjtu/groupone/backend/service/TorrentService.java b/src/main/java/edu/bjtu/groupone/backend/service/TorrentService.java
new file mode 100644
index 0000000..5a00de5
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/service/TorrentService.java
@@ -0,0 +1,11 @@
+package edu.bjtu.groupone.backend.service;
+
+import edu.bjtu.groupone.backend.domain.entity.Torrent;
+import org.springframework.core.io.Resource;
+import org.springframework.web.multipart.MultipartFile;
+
+public interface TorrentService {
+    Torrent uploadTorrent(MultipartFile file, Long userId) throws Exception;
+
+    Resource downloadTorrent(String infoHash) throws Exception;
+}
\ No newline at end of file
diff --git a/src/main/java/edu/bjtu/groupone/backend/service/impl/TorrentServiceImpl.java b/src/main/java/edu/bjtu/groupone/backend/service/impl/TorrentServiceImpl.java
new file mode 100644
index 0000000..a1dac1f
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/service/impl/TorrentServiceImpl.java
@@ -0,0 +1,68 @@
+package edu.bjtu.groupone.backend.service.impl;
+
+import edu.bjtu.groupone.backend.mapper.TorrentMapper;
+import edu.bjtu.groupone.backend.mapper.UserMapper;
+import edu.bjtu.groupone.backend.domain.entity.Torrent;
+import edu.bjtu.groupone.backend.domain.entity.User;
+import edu.bjtu.groupone.backend.service.TorrentService;
+import edu.bjtu.groupone.backend.utils.TorrentParserUtil;
+import java.io.File;
+import java.util.Map;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.core.io.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+@Service
+public class TorrentServiceImpl implements TorrentService {
+    @Value("${torrent.storage.path}")
+    private String storagePath;
+    @Autowired
+    private TorrentMapper torrentMapper;
+    @Autowired
+    private UserMapper userMapper;
+
+    public TorrentServiceImpl() {
+    }
+
+    public Torrent uploadTorrent(MultipartFile file, Long userId) throws Exception {
+        Map<String, Object> meta = TorrentParserUtil.parseTorrent(file.getInputStream());
+        String infoHash = (String)meta.get("infoHash");
+        String name = (String)meta.get("name");
+        Long size = (Long)meta.get("length");
+        String pathToUse = this.storagePath != null && !this.storagePath.isBlank() ? this.storagePath : System.getProperty("java.io.tmpdir");
+        File dir = new File(pathToUse);
+        if (!dir.exists()) {
+            dir.mkdirs();
+        }
+
+        String savePath = pathToUse + File.separator + infoHash + ".torrent";
+        File dest = new File(savePath);
+        file.transferTo(dest);
+        User uploader = this.userMapper.selectById(userId);
+        Torrent torrent = new Torrent();
+        torrent.setInfoHash(infoHash);
+        torrent.setName(name);
+        torrent.setFilePath(savePath);
+        torrent.setSize(size);
+        torrent.setUploaderId((long)uploader.getUserId());
+        this.torrentMapper.insertTorrent(torrent);
+        return torrent;
+    }
+
+    public Resource downloadTorrent(String infoHash) throws Exception {
+        Torrent torrent = this.torrentMapper.selectByInfoHash(infoHash);
+        if (torrent == null) {
+            throw new IllegalArgumentException("种子不存在");
+        } else {
+            File file = new File(torrent.getFilePath());
+            if (!file.exists()) {
+                throw new IllegalStateException("种子文件不存在");
+            } else {
+                return new FileSystemResource(file);
+            }
+        }
+    }
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/utils/TorrentParserUtil.java b/src/main/java/edu/bjtu/groupone/backend/utils/TorrentParserUtil.java
new file mode 100644
index 0000000..fdc14be
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/utils/TorrentParserUtil.java
@@ -0,0 +1,32 @@
+package edu.bjtu.groupone.backend.utils;
+
+import com.turn.ttorrent.bcodec.BDecoder;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.commons.codec.digest.DigestUtils;
+
+public class TorrentParserUtil {
+    public TorrentParserUtil() {
+    }
+
+    public static Map<String, Object> parseTorrent(InputStream inputStream) throws Exception {
+        Map<String, Object> result = new HashMap();
+        BEValue rootBev = BDecoder.bdecode(inputStream);
+        Map<String, BEValue> root = rootBev.getMap();
+        Map<String, BEValue> info = ((BEValue)root.get("info")).getMap();
+        String name = ((BEValue)info.get("name")).getString();
+        long length = info.containsKey("length") ? ((BEValue)info.get("length")).getLong() : -1L;
+        ByteBuffer buffer = BEncoder.bencode(info);
+        byte[] encodedInfo = new byte[buffer.remaining()];
+        buffer.get(encodedInfo);
+        String infoHash = DigestUtils.sha1Hex(encodedInfo);
+        result.put("infoHash", infoHash);
+        result.put("name", name);
+        result.put("length", length);
+        return result;
+    }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 668bc3b..165698e 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,15 +1,14 @@
 # ??MySQL??
 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
-spring.datasource.url=jdbc:mysql://localhost:3306/llksh\
+spring.datasource.url=jdbc:mysql://rm-cn-qzy4ah2qt0008vfo.rwlb.rds.aliyuncs.com:3306/smart_course_platform\
 ?useSSL=false\
 &serverTimezone=Asia/Shanghai\
 &characterEncoding=utf8\
 &allowPublicKeyRetrieval=true
 
-spring.datasource.username=root
-spring.datasource.password=wuxiaorui123
+spring.datasource.username=wangy
+spring.datasource.password=Wyt2005011600
 # src/main/resources/application.properties
-spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
 
 # ???????????
 spring.datasource.hikari.maximum-pool-size=10
@@ -26,4 +25,6 @@
 mybatis.type-aliases-package=edu.bjtu.groupone.backend.domain.entity
 
 # ????????
-mybatis.mapper-locations=classpath:mapper/*.xml
\ No newline at end of file
+mybatis.mapper-locations=classpath:mapper/*.xml
+
+torrent.storage.path=./torrent
\ No newline at end of file
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 2d79bf4..ae77276 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -15,9 +15,9 @@
 
   datasource:
     driver-class-name: com.mysql.cj.jdbc.Driver
-    url: jdbc:mysql://localhost:3306/llksh?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
-    username: root
-    password: "wuxiaorui123"
+    url: jdbc:mysql://rm-cn-qzy4ah2qt0008vfo.rwlb.rds.aliyuncs.com:3306/smart_course_platform?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
+    username: wangy
+    password: "Wyt2005011600"
     hikari:
       maximum-pool-size: 10
       minimum-idle: 5
diff --git a/src/test/java/edu/bjtu/groupone/backend/TorrentControllerTest.java b/src/test/java/edu/bjtu/groupone/backend/TorrentControllerTest.java
new file mode 100644
index 0000000..86573d8
--- /dev/null
+++ b/src/test/java/edu/bjtu/groupone/backend/TorrentControllerTest.java
@@ -0,0 +1,125 @@
+package edu.bjtu.groupone.backend;
+
+import edu.bjtu.groupone.backend.api.TorrentController;
+import edu.bjtu.groupone.backend.domain.entity.Torrent;
+import edu.bjtu.groupone.backend.service.TorrentService;
+import edu.bjtu.groupone.backend.utils.GetTokenUserId;
+import jakarta.servlet.http.HttpServletRequest;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.core.io.Resource;
+import org.springframework.http.ResponseEntity;
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.web.multipart.MultipartFile;
+
+@ExtendWith({MockitoExtension.class})
+public class TorrentControllerTest {
+    @Mock
+    private TorrentService torrentService;
+    @InjectMocks
+    private TorrentController torrentController;
+    @Mock
+    private HttpServletRequest request;
+
+    public TorrentControllerTest() {
+    }
+
+    @Test
+    public void uploadTorrent_shouldReturnTorrent_whenUserIdValid() throws Exception {
+        MultipartFile file = new MockMultipartFile("file", "test.torrent", "application/x-bittorrent", "dummy data".getBytes());
+        MockedStatic<GetTokenUserId> utilities = Mockito.mockStatic(GetTokenUserId.class);
+
+        try {
+            utilities.when(() -> {
+                GetTokenUserId.getUserId(this.request);
+            }).thenReturn("123");
+            Torrent expectedTorrent = new Torrent();
+            expectedTorrent.setId(1L);
+            expectedTorrent.setName("testfile");
+            expectedTorrent.setInfoHash("fakehash");
+            expectedTorrent.setSize(12345L);
+            expectedTorrent.setUploaderId(123L);
+            Mockito.when(this.torrentService.uploadTorrent(file, 123L)).thenReturn(expectedTorrent);
+            ResponseEntity<?> response = this.torrentController.uploadTorrent(file, this.request);
+            Assertions.assertThat(response.getStatusCodeValue()).isEqualTo(200);
+            Assertions.assertThat(response.getBody()).isEqualTo(expectedTorrent);
+            ((TorrentService)Mockito.verify(this.torrentService, Mockito.times(1))).uploadTorrent(file, 123L);
+        } catch (Throwable var6) {
+            if (utilities != null) {
+                try {
+                    utilities.close();
+                } catch (Throwable var5) {
+                    var6.addSuppressed(var5);
+                }
+            }
+
+            throw var6;
+        }
+
+        if (utilities != null) {
+            utilities.close();
+        }
+
+    }
+
+    @Test
+    public void uploadTorrent_shouldReturnUnauthorized_whenUserIdNull() throws Exception {
+        MockedStatic<GetTokenUserId> utilities = Mockito.mockStatic(GetTokenUserId.class);
+
+        try {
+            utilities.when(() -> {
+                GetTokenUserId.getUserId(this.request);
+            }).thenReturn((Object)null);
+            MultipartFile file = new MockMultipartFile("file", "test.torrent", "application/x-bittorrent", "dummy data".getBytes());
+            ResponseEntity<?> response = this.torrentController.uploadTorrent(file, this.request);
+            Assertions.assertThat(response.getStatusCodeValue()).isEqualTo(401);
+            Assertions.assertThat(response.getBody()).isEqualTo("Token无效或缺失");
+            Mockito.verifyNoInteractions(new Object[]{this.torrentService});
+        } catch (Throwable var5) {
+            if (utilities != null) {
+                try {
+                    utilities.close();
+                } catch (Throwable var4) {
+                    var5.addSuppressed(var4);
+                }
+            }
+
+            throw var5;
+        }
+
+        if (utilities != null) {
+            utilities.close();
+        }
+
+    }
+
+    @Test
+    public void downloadTorrent_shouldReturnResource_whenFound() throws Exception {
+        String infoHash = "fakehash";
+        byte[] data = "torrent data".getBytes();
+        Resource resource = new ByteArrayResource(data);
+        Mockito.when(this.torrentService.downloadTorrent(infoHash)).thenReturn(resource);
+        ResponseEntity<Resource> response = this.torrentController.downloadTorrent(infoHash);
+        Assertions.assertThat(response.getStatusCodeValue()).isEqualTo(200);
+        Assertions.assertThat(response.getHeaders().getFirst("Content-Disposition")).isEqualTo("attachment; filename=\"" + infoHash + ".torrent\"");
+        Assertions.assertThat((Resource)response.getBody()).isEqualTo(resource);
+        ((TorrentService)Mockito.verify(this.torrentService, Mockito.times(1))).downloadTorrent(infoHash);
+    }
+
+    @Test
+    public void downloadTorrent_shouldReturnNotFound_whenException() throws Exception {
+        String infoHash = "notexist";
+        Mockito.when(this.torrentService.downloadTorrent(infoHash)).thenThrow(new Throwable[]{new RuntimeException("Not found")});
+        ResponseEntity<Resource> response = this.torrentController.downloadTorrent(infoHash);
+        Assertions.assertThat(response.getStatusCodeValue()).isEqualTo(404);
+        Assertions.assertThat((Resource)response.getBody()).isNull();
+        ((TorrentService)Mockito.verify(this.torrentService, Mockito.times(1))).downloadTorrent(infoHash);
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/edu/bjtu/groupone/backend/TorrentServiceImplTest.java b/src/test/java/edu/bjtu/groupone/backend/TorrentServiceImplTest.java
new file mode 100644
index 0000000..f25f7cb
--- /dev/null
+++ b/src/test/java/edu/bjtu/groupone/backend/TorrentServiceImplTest.java
@@ -0,0 +1,68 @@
+package edu.bjtu.groupone.backend;
+
+import edu.bjtu.groupone.backend.mapper.TorrentMapper;
+import edu.bjtu.groupone.backend.mapper.UserMapper;
+import edu.bjtu.groupone.backend.domain.entity.Torrent;
+import edu.bjtu.groupone.backend.domain.entity.User;
+import edu.bjtu.groupone.backend.service.impl.TorrentServiceImpl;
+import java.io.File;
+import java.io.FileInputStream;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.core.io.Resource;
+import org.springframework.mock.web.MockMultipartFile;
+
+@ExtendWith({MockitoExtension.class})
+public class TorrentServiceImplTest {
+    @Mock
+    private TorrentMapper torrentMapper;
+    @Mock
+    private UserMapper userMapper;
+    @InjectMocks
+    private TorrentServiceImpl torrentService;
+
+    public TorrentServiceImplTest() {
+    }
+
+    @BeforeEach
+    void setup() {
+    }
+
+    @Test
+    public void uploadTorrent_shouldSaveAndReturnTorrent() throws Exception {
+        File file = new File("torrents/22301024-王玉涛.doc.torrent");
+        Assertions.assertThat(file.exists()).isTrue();
+        FileInputStream inputStream = new FileInputStream(file);
+        MockMultipartFile mockFile = new MockMultipartFile("file", "22301024-王玉涛.doc.torrent", "application/x-bittorrent", inputStream);
+        ((TorrentMapper)Mockito.doNothing().when(this.torrentMapper)).insertTorrent((Torrent)Mockito.any(Torrent.class));
+        User fakeUser = new User();
+        fakeUser.setUserId(100);
+        Mockito.when(this.userMapper.selectById(100L)).thenReturn(fakeUser);
+        Torrent result = this.torrentService.uploadTorrent(mockFile, 100L);
+        Assertions.assertThat(result).isNotNull();
+        Assertions.assertThat(result.getInfoHash()).isNotBlank();
+        Assertions.assertThat(result.getName()).isNotBlank();
+        Assertions.assertThat(result.getUploaderId()).isEqualTo(100L);
+        ((TorrentMapper)Mockito.verify(this.torrentMapper, Mockito.times(1))).insertTorrent((Torrent)Mockito.any(Torrent.class));
+    }
+
+    @Test
+    public void downloadTorrent_shouldReturnResource_whenTorrentExists() throws Exception {
+        File tempFile = File.createTempFile("test-torrent-", ".torrent");
+        tempFile.deleteOnExit();
+        Torrent fakeTorrent = new Torrent();
+        fakeTorrent.setInfoHash("fakeinfohash123");
+        fakeTorrent.setFilePath(tempFile.getAbsolutePath());
+        Mockito.when(this.torrentMapper.selectByInfoHash("fakeinfohash123")).thenReturn(fakeTorrent);
+        Resource resource = this.torrentService.downloadTorrent("fakeinfohash123");
+        Assertions.assertThat(resource).isNotNull();
+        Assertions.assertThat(resource.exists()).isTrue();
+        Assertions.assertThat(resource.getFile().getAbsolutePath()).isEqualTo(tempFile.getAbsolutePath());
+    }
+}