Merge "modif"
diff --git a/.gitignore b/.gitignore
index d9e9af4..235af44 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,4 +33,4 @@
 .vscode/
 
 ### Upload ###
-./uploaded-torrents/
\ No newline at end of file
+uploaded-torrents/
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..4fac612
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,13 @@
+version: '3.8'
+
+services:
+  redis:
+    image: "redis:alpine"
+    container_name: "redis-server"
+    ports:
+      - "6379:6379"
+    volumes:
+      - redis-data:/data
+
+volumes:
+  redis-data:
\ No newline at end of file
diff --git a/src/main/java/com/example/g8backend/controller/AuthController.java b/src/main/java/com/example/g8backend/controller/AuthController.java
index 2f36500..e824b63 100644
--- a/src/main/java/com/example/g8backend/controller/AuthController.java
+++ b/src/main/java/com/example/g8backend/controller/AuthController.java
@@ -66,7 +66,7 @@
 
         // passkey 用于在客户端发送announce请求时获取用户信息
         user.setPasskey(UUID.randomUUID().toString().replace("-", ""));
-        userService.registerUser(user);
+        userService.save(user);
 
         return ResponseEntity.ok("注册成功");
     }
diff --git a/src/main/java/com/example/g8backend/controller/PostController.java b/src/main/java/com/example/g8backend/controller/PostController.java
new file mode 100644
index 0000000..3557af2
--- /dev/null
+++ b/src/main/java/com/example/g8backend/controller/PostController.java
@@ -0,0 +1,104 @@
+package com.example.g8backend.controller;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.bind.annotation.*;
+import com.example.g8backend.entity.Post;
+import com.example.g8backend.service.IPostService;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/post")
+public class PostController {
+    @Autowired
+    private IPostService postService;
+
+    @PostMapping("")
+    public ResponseEntity<?> createPost(@RequestBody Post post) {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        long userId = (long) authentication.getPrincipal();
+        post.setUserId(userId);
+        postService.save(post);
+        return ResponseEntity.ok().build();
+    }
+
+    @GetMapping("/{postId}")
+    public Post getPost(@PathVariable("postId") Long postId) {
+        return postService.getById(postId);
+    }
+
+    @DeleteMapping("/{postId}")
+    public ResponseEntity<?> deletePost(@PathVariable("postId") Long postId) {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        long userId = (long) authentication.getPrincipal();
+        Post post = postService.getById(postId);
+        if (post == null) {
+            return ResponseEntity.status(500).body("Post not found.");
+        }
+        if (post.getUserId()!= userId) {
+            return ResponseEntity.status(403).body("You are not authorized to delete this post.");
+        }
+        postService.removeById(postId);
+        return ResponseEntity.ok().body("Post deleted successfully.");
+    }
+
+    @GetMapping("/getAll")
+    public List<Post> getAllPosts() {
+        return postService.list();
+    }
+
+    @GetMapping("/getByUserId/{userId}")
+    public List<Post> getPostsByUserId(@PathVariable("userId") Long userId) {
+        return postService.getPostsByUserId(userId);
+    }
+
+    @PutMapping("/{postId}")
+    public ResponseEntity<?> updatePost(@PathVariable("postId") Long postId, @RequestBody Post post) {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        long userId = (long) authentication.getPrincipal();
+        Post existingPost = postService.getById(postId);
+        
+        if (existingPost == null) {
+            return ResponseEntity.status(500).body("Post not found.");
+        }
+        if (existingPost.getUserId() != userId) {
+            return ResponseEntity.status(403).body("You are not authorized to update this post.");
+        }
+        
+        post.setPostId(postId);
+        post.setUserId(userId);
+        postService.updateById(post);
+        return ResponseEntity.ok().body("Post updated successfully.");
+    }
+
+    @GetMapping("/type/{postType}")
+    public ResponseEntity<?> getPostsByType(@PathVariable String postType) {
+        List<Post> posts = postService.getPostsByType(postType);
+        return ResponseEntity.ok().body(posts);
+    }
+
+    @PostMapping("/{postId}/like")
+    public ResponseEntity<?> likePost(@PathVariable Long postId) {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        long userId = (long) authentication.getPrincipal();
+        postService.likePost(userId, postId);
+        return ResponseEntity.ok().body("Post liked successfully.");
+    }
+
+    @DeleteMapping("/{postId}/like")
+    public ResponseEntity<?> unlikePost(@PathVariable Long postId) {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        long userId = (long) authentication.getPrincipal();
+        postService.unlikePost(userId, postId);
+        return ResponseEntity.ok().body("Post unliked successfully.");
+    }
+
+    @GetMapping("/{postId}/likes")
+    public ResponseEntity<?> getPostLikeCount(@PathVariable Long postId) {
+        Long likeCount = postService.getPostLikeCount(postId);
+        return ResponseEntity.ok().body(likeCount);
+    }
+}
diff --git a/src/main/java/com/example/g8backend/controller/TorrentController.java b/src/main/java/com/example/g8backend/controller/TorrentController.java
index bec187e..0c016f4 100644
--- a/src/main/java/com/example/g8backend/controller/TorrentController.java
+++ b/src/main/java/com/example/g8backend/controller/TorrentController.java
@@ -1,19 +1,21 @@
 package com.example.g8backend.controller;
 
 import com.example.g8backend.entity.User;
+import com.example.g8backend.entity.Torrent;
 import com.example.g8backend.service.IUserService;
+import jakarta.servlet.http.HttpServletResponse;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.ResponseEntity;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
-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.bind.annotation.*;
 import com.example.g8backend.service.ITorrentService;
 import org.springframework.web.multipart.MultipartFile;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 
 @RestController
 @RequestMapping("/torrent")
@@ -35,7 +37,11 @@
         File tempFile = File.createTempFile("upload-", ".torrent");
         multipartFile.transferTo(tempFile);
 
-        torrentService.handleTorrentUpload(tempFile, userId, passkey);
+        try {
+            torrentService.handleTorrentUpload(tempFile, userId, passkey);
+        } catch (IllegalArgumentException e) {
+            return ResponseEntity.badRequest().body(e.getMessage());
+        }
 
         // 删除临时文件
         if(!tempFile.delete()){
@@ -43,4 +49,25 @@
         }
         return ResponseEntity.ok("种子上传成功");
     }
+
+    @GetMapping("/download/{torrentId}")
+    public void downloadTorrent(@PathVariable String torrentId, HttpServletResponse response) throws IOException {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        long userId = (long) authentication.getPrincipal();
+
+        User user = userService.getById(userId);
+        String passkey = user.getPasskey();
+        Torrent torrent = torrentService.findByTorrentId(Long.parseLong(torrentId));
+        File tempFile = torrentService.handleTorrentDownload(torrent, passkey);
+
+        response.setContentType("application/x-bittorrent");
+        response.setHeader("Content-Disposition", "attachment; filename=\"" + torrent.getTorrentName() + "\"");
+
+        try (InputStream inputStream = new FileInputStream(tempFile)) {
+            inputStream.transferTo(response.getOutputStream());
+        }
+        if (!tempFile.delete()) {
+            throw new IOException("Failed to delete temporary file: " + tempFile.getAbsolutePath());
+        }
+    }
 }
diff --git a/src/main/java/com/example/g8backend/entity/Post.java b/src/main/java/com/example/g8backend/entity/Post.java
new file mode 100644
index 0000000..ac7ff20
--- /dev/null
+++ b/src/main/java/com/example/g8backend/entity/Post.java
@@ -0,0 +1,33 @@
+package com.example.g8backend.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+
+import java.sql.Timestamp;
+import lombok.Data;
+
+@Data
+@TableName("posts")
+public class Post {
+    @TableId(type = IdType.AUTO)
+    private Long postId;
+
+    private Long userId;
+    private String postTitle;
+    private String postContent;
+    private Timestamp createdAt;
+    private String postType;
+
+    @Override
+    public String toString() {
+        return "Post{" +
+                "postId=" + postId +
+                ", userId=" + userId +
+                ", postTitle='" + postTitle + '\'' +
+                ", postContent='" + postContent + '\'' +
+                ", createdAt=" + createdAt +
+                ", postType='" + postType + '\'' +
+                '}';
+    }
+}
diff --git a/src/main/java/com/example/g8backend/mapper/PostMapper.java b/src/main/java/com/example/g8backend/mapper/PostMapper.java
new file mode 100644
index 0000000..6db4e67
--- /dev/null
+++ b/src/main/java/com/example/g8backend/mapper/PostMapper.java
@@ -0,0 +1,13 @@
+package com.example.g8backend.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.g8backend.entity.Post;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+@Mapper
+public interface PostMapper extends BaseMapper<Post> {
+    List<Post> getPostsByUserId(@Param("userId") Long userId);
+}
diff --git a/src/main/java/com/example/g8backend/service/IPostService.java b/src/main/java/com/example/g8backend/service/IPostService.java
new file mode 100644
index 0000000..f81053e
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/IPostService.java
@@ -0,0 +1,15 @@
+package com.example.g8backend.service;
+
+import com.example.g8backend.entity.Post;
+import com.baomidou.mybatisplus.extension.service.IService;
+import java.util.List;
+
+public interface IPostService extends IService<Post> {
+    List<Post> getPostsByUserId(Long userId);
+    Post createPost(Post post);
+    Post updatePost(Post post);
+    List<Post> getPostsByType(String postType);
+    Long getPostLikeCount(Long postId);
+    void likePost(Long userId, Long postId);
+    void unlikePost(Long userId, Long postId);
+}
diff --git a/src/main/java/com/example/g8backend/service/ITorrentService.java b/src/main/java/com/example/g8backend/service/ITorrentService.java
index 5e48dc4..87622c8 100644
--- a/src/main/java/com/example/g8backend/service/ITorrentService.java
+++ b/src/main/java/com/example/g8backend/service/ITorrentService.java
@@ -8,6 +8,7 @@
 
 public interface ITorrentService extends IService<Torrent> {
     Torrent handleTorrentUpload(File file, Long userId, String passkey) throws IOException;
+    File handleTorrentDownload(Torrent torrent, String passkey) throws IOException;
     Torrent findByInfoHash(String infoHash);
     Torrent findByTorrentId(Long torrentId);
 }
diff --git a/src/main/java/com/example/g8backend/service/IUserService.java b/src/main/java/com/example/g8backend/service/IUserService.java
index f407c37..af94d27 100644
--- a/src/main/java/com/example/g8backend/service/IUserService.java
+++ b/src/main/java/com/example/g8backend/service/IUserService.java
@@ -8,5 +8,4 @@
     User getUserByName(@Param("name") String name);
     User getUserByEmail(@Param("email") String email);
     User getUserByPasskey(@Param("passkey") String passkey);
-    void registerUser(User user);;
 }
diff --git a/src/main/java/com/example/g8backend/service/impl/PostServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/PostServiceImpl.java
new file mode 100644
index 0000000..09a471e
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/PostServiceImpl.java
@@ -0,0 +1,63 @@
+package com.example.g8backend.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.example.g8backend.entity.Post;
+import com.example.g8backend.mapper.PostMapper;
+import com.example.g8backend.service.IPostService;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import java.sql.Timestamp;
+import java.util.List;
+
+@Service
+public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements IPostService {
+    
+    private final PostMapper postMapper;
+
+    public PostServiceImpl(PostMapper postMapper) {
+        this.postMapper = postMapper;
+        this.baseMapper = postMapper; // 重要:设置 baseMapper
+    }
+
+    @Override
+    public List<Post> getPostsByUserId(Long userId) {
+        return postMapper.getPostsByUserId(userId);
+    }
+
+    @Override
+    public Post createPost(Post post) {
+        post.setCreatedAt(new Timestamp(System.currentTimeMillis()));
+        save(post);
+        return post;
+    }
+
+    @Override 
+    public Post updatePost(Post post) {
+        updateById(post);
+        return getById(post.getPostId());
+    }
+
+    @Override
+    public List<Post> getPostsByType(String postType) {
+        QueryWrapper<Post> wrapper = new QueryWrapper<>();
+        wrapper.eq("post_type", postType);
+        return list(wrapper);
+    }
+
+    @Override
+    public Long getPostLikeCount(Long postId) {
+        // TODO: 需要实现post_likes表的查询
+        return 0L;
+    }
+
+    @Override
+    public void likePost(Long userId, Long postId) {
+        // TODO: 需要实现post_likes表的插入
+    }
+
+    @Override
+    public void unlikePost(Long userId, Long postId) {
+        // TODO: 需要实现post_likes表的删除
+    }
+}
diff --git a/src/main/java/com/example/g8backend/service/impl/TorrentServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/TorrentServiceImpl.java
index 03185ba..bedb94a 100644
--- a/src/main/java/com/example/g8backend/service/impl/TorrentServiceImpl.java
+++ b/src/main/java/com/example/g8backend/service/impl/TorrentServiceImpl.java
@@ -17,12 +17,12 @@
     @Resource
     private TorrentMapper torrentMapper;
 
-    @Override
-    public Torrent handleTorrentUpload(File file, Long userId, String passkey) throws IOException{
-        String tracker = "http://127.0.0.1:8080/announce/" + passkey;
+    String tracker = "http://127.0.0.1:8080/announce/";
 
+    @Override
+    public Torrent handleTorrentUpload(File file, Long userId, String passkey) throws IOException, IllegalArgumentException {
         // 修改 announce 字段
-        byte[] modifiedBytes = TorrentUtil.injectTracker(file, tracker);
+        byte[] modifiedBytes = TorrentUtil.injectTracker(file, tracker + passkey);
 
         // 计算 info_hash
         String infoHash = TorrentUtil.getInfoHash(file);
@@ -56,6 +56,18 @@
     }
 
     @Override
+    public File handleTorrentDownload(Torrent torrent, String passkey) throws IOException {
+        File torrentFile = new File("uploaded-torrents/" + torrent.getTorrentName());
+        byte[] modifiedBytes = TorrentUtil.injectTracker(torrentFile, tracker + passkey);
+
+        File tempFile = File.createTempFile("user_torrent_", ".torrent");
+        try (FileOutputStream fos = new FileOutputStream(tempFile)) {
+            fos.write(modifiedBytes);
+        }
+        return tempFile;
+    }
+
+    @Override
     public Torrent findByInfoHash(String infoHash){
         return torrentMapper.getTorrentByInfoHash(infoHash);
     }
diff --git a/src/main/java/com/example/g8backend/service/impl/UserServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/UserServiceImpl.java
index 3f3357c..e8bcf20 100644
--- a/src/main/java/com/example/g8backend/service/impl/UserServiceImpl.java
+++ b/src/main/java/com/example/g8backend/service/impl/UserServiceImpl.java
@@ -22,6 +22,4 @@
     @Override
     public User getUserByPasskey(String passkey) { return userMapper.getUserByPasskey(passkey);}
 
-    @Override
-    public void registerUser(User user) {userMapper.insert(user);}
 }
diff --git a/src/main/java/com/example/g8backend/util/TorrentUtil.java b/src/main/java/com/example/g8backend/util/TorrentUtil.java
index f644fff..e6c1f3b 100644
--- a/src/main/java/com/example/g8backend/util/TorrentUtil.java
+++ b/src/main/java/com/example/g8backend/util/TorrentUtil.java
@@ -22,10 +22,14 @@
         return bencode.encode(torrentMap);
     }
 
-    public static String getInfoHash(File torrentFile) throws IOException {
+    public static String getInfoHash(File torrentFile) throws IOException, IllegalArgumentException {
         byte[] fileBytes = readBytes(torrentFile);
         Map<String, Object> torrentMap = bencode.decode(fileBytes, Type.DICTIONARY);
 
+        if (!torrentMap.containsKey("info")) {
+            throw new IllegalArgumentException("Invalid torrent file: missing 'info' dictionary.");
+        }
+
         @SuppressWarnings("unchecked")
         Map<String, Object> info = (Map<String, Object>) torrentMap.get("info");
 
@@ -56,7 +60,7 @@
         }
     }
 
-    private static String bytesToHex(byte[] hash) {
+    public static String bytesToHex(byte[] hash) {
         StringBuilder hex = new StringBuilder();
         for (byte b : hash) {
             hex.append(String.format("%02x", b));
diff --git a/src/main/resources/mapper/PostMapper.xml b/src/main/resources/mapper/PostMapper.xml
new file mode 100644
index 0000000..c1dbda7
--- /dev/null
+++ b/src/main/resources/mapper/PostMapper.xml
@@ -0,0 +1,24 @@
+.xml
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+
+<mapper namespace="com.example.g8backend.mapper.PostMapper">
+    <select id="getPostsByUserId" resultType="com.example.g8backend.entity.Post">
+        SELECT * FROM posts WHERE user_id = #{userId}
+    </select>
+
+    <select id="getPostLikeCount" resultType="java.lang.Long">
+        SELECT COUNT(*) FROM post_likes WHERE post_id = #{postId}
+    </select>
+
+    <insert id="likePost">
+        INSERT INTO post_likes (user_id, post_id)
+        VALUES (#{userId}, #{postId})
+    </insert>
+
+    <delete id="unlikePost">
+        DELETE FROM post_likes 
+        WHERE user_id = #{userId} AND post_id = #{postId}
+    </delete>
+</mapper>
\ No newline at end of file
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
index 365f9e7..e88ff07 100644
--- a/src/main/resources/schema.sql
+++ b/src/main/resources/schema.sql
@@ -1,3 +1,6 @@
+CREATE DATABASE IF NOT EXISTS g8backend;
+USE g8backend;
+
 CREATE TABLE IF NOT EXISTS `users` (
   user_id INT AUTO_INCREMENT PRIMARY KEY,
   user_name VARCHAR(255) NOT NULL,
@@ -27,3 +30,21 @@
   FOREIGN KEY (passkey) REFERENCES users(passkey),
   PRIMARY KEY (passkey, info_hash, peer_id)
 );
+
+CREATE TABLE IF NOT EXISTS `posts` (
+    post_id INT AUTO_INCREMENT PRIMARY KEY,
+    user_id INT NOT NULL,
+    post_title VARCHAR(255) NOT NULL,
+    post_content TEXT NOT NULL,
+    post_type ENUM('resource', 'discussion') NOT NULL,
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    FOREIGN KEY (user_id) REFERENCES users(user_id)
+);
+
+CREATE TABLE IF NOT EXISTS `post_likes` (
+    user_id INT NOT NULL,
+    post_id INT NOT NULL,
+    FOREIGN KEY (user_id) REFERENCES users(user_id),
+    FOREIGN KEY (post_id) REFERENCES posts(post_id),
+    PRIMARY KEY (user_id, post_id)
+);
diff --git a/src/test/java/com/example/g8backend/service/PostServiceTest.java b/src/test/java/com/example/g8backend/service/PostServiceTest.java
new file mode 100644
index 0000000..6f54177
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/PostServiceTest.java
@@ -0,0 +1,152 @@
+package com.example.g8backend.service;
+
+import com.example.g8backend.entity.Post;
+import com.example.g8backend.mapper.PostMapper;
+import com.example.g8backend.service.impl.PostServiceImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import java.sql.Timestamp;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(SpringExtension.class)
+@DisplayName("帖子服务测试")
+class PostServiceTest {
+
+    @Mock
+    private PostMapper postMapper;
+
+    private PostServiceImpl postService;
+
+    private Post testPost;
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+        postService = new PostServiceImpl(postMapper);
+        testPost = createTestPost();
+    }
+
+    private Post createTestPost() {
+        Post post = new Post();
+        post.setPostId(1L);
+        post.setUserId(1L);
+        post.setPostTitle("测试标题");
+        post.setPostContent("测试内容");
+        post.setCreatedAt(new Timestamp(System.currentTimeMillis()));
+        return post;
+    }
+
+    @Test
+    @DisplayName("创建帖子-成功")
+    void save_ShouldSucceed() {
+        // Arrange
+        when(postMapper.insert(any(Post.class))).thenReturn(1);
+
+        // Act
+        boolean result = postService.save(testPost);
+
+        // Assert
+        assertTrue(result);
+        verify(postMapper).insert(testPost);
+    }
+
+    @Test
+    @DisplayName("获取帖子-通过ID存在")
+    void getById_WhenExists_ShouldReturnPost() {
+        // Arrange
+        when(postMapper.selectById(1L)).thenReturn(testPost);
+
+        // Act
+        Post result = postService.getById(1L);
+
+        // Assert
+        assertNotNull(result);
+        assertEquals(testPost.getPostId(), result.getPostId());
+        verify(postMapper).selectById(1L);
+    }
+
+    @Test
+    @DisplayName("获取帖子-通过ID不存在")
+    void getById_WhenNotExists_ShouldReturnNull() {
+        // Arrange
+        when(postMapper.selectById(999L)).thenReturn(null);
+
+        // Act
+        Post result = postService.getById(999L);
+
+        // Assert
+        assertNull(result);
+        verify(postMapper).selectById(999L);
+    }
+
+    @Test
+    @DisplayName("更新帖子-成功")
+    void updateById_ShouldSucceed() {
+        // Arrange
+        when(postMapper.updateById(any(Post.class))).thenReturn(1);
+
+        // Act
+        boolean result = postService.updateById(testPost);
+
+        // Assert
+        assertTrue(result);
+        verify(postMapper).updateById(testPost);
+    }
+
+    @Test
+    @DisplayName("删除帖子-成功")
+    void removeById_ShouldSucceed() {
+        // Arrange
+        when(postMapper.deleteById(1L)).thenReturn(1);
+
+        // Act
+        boolean result = postService.removeById(1L);
+
+        // Assert
+        assertTrue(result);
+        verify(postMapper).deleteById(1L);
+    }
+
+    @Test
+    @DisplayName("获取用户帖子列表")
+    void getPostsByUserId_ShouldReturnPosts() {
+        // Arrange
+        List<Post> expectedPosts = Arrays.asList(testPost);
+        when(postMapper.getPostsByUserId(1L)).thenReturn(expectedPosts);
+
+        // Act
+        List<Post> result = postService.getPostsByUserId(1L);
+
+        // Assert
+        assertNotNull(result);
+        assertFalse(result.isEmpty());
+        assertEquals(testPost.getPostId(), result.get(0).getPostId());
+        verify(postMapper).getPostsByUserId(1L);
+    }
+
+    @Test
+    @DisplayName("获取用户帖子-空列表")
+    void getPostsByUserId_WhenNoPosts_ShouldReturnEmptyList() {
+        // Arrange
+        when(postMapper.getPostsByUserId(999L)).thenReturn(Arrays.asList());
+
+        // Act
+        List<Post> result = postService.getPostsByUserId(999L);
+
+        // Assert
+        assertNotNull(result);
+        assertTrue(result.isEmpty());
+        verify(postMapper).getPostsByUserId(999L);
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/com/example/g8backend/util/TorrentUtilTest.java b/src/test/java/com/example/g8backend/util/TorrentUtilTest.java
new file mode 100644
index 0000000..c42803b
--- /dev/null
+++ b/src/test/java/com/example/g8backend/util/TorrentUtilTest.java
@@ -0,0 +1,105 @@
+package com.example.g8backend.util;
+
+import com.dampcake.bencode.Bencode;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import java.security.MessageDigest;
+import static org.junit.jupiter.api.Assertions.*;
+
+class TorrentUtilTest {
+
+    private static final String TEST_TRACKER_URL = "http://test.tracker/announce";
+
+    @TempDir
+    Path tempDir; // 临时目录
+
+    //---------------- 动态生成 .torrent 文件 -----------------
+    private File createTestTorrentFile(String announceUrl, Map<String, Object> info) throws Exception {
+        // 1. 构造 Bencode 数据结构
+        Map<String, Object> torrentMap = new HashMap<>();
+        torrentMap.put("announce", announceUrl);
+        torrentMap.put("info", info);
+
+        // 2. 编码为 Bencode 字节流
+        Bencode bencode = new Bencode();
+        byte[] bencodeData = bencode.encode(torrentMap);
+
+        // 3. 写入临时文件
+        Path torrentPath = tempDir.resolve("dynamic.torrent");
+        Files.write(torrentPath, bencodeData);
+        return torrentPath.toFile();
+    }
+
+    //---------------- 测试 injectTracker() -----------------
+    @Test
+    void testInjectTracker() throws Exception {
+        // 动态生成测试文件
+        Map<String, Object> info = Map.of("name", "test.txt", "length", 1024);
+        File originalFile = createTestTorrentFile("http://old.tracker", info);
+
+        // 调用方法修改 Tracker
+        byte[] modifiedData = TorrentUtil.injectTracker(originalFile, TEST_TRACKER_URL);
+
+        // 验证新 Tracker 是否生效
+        String modifiedBencode = new String(modifiedData);
+        assertTrue(modifiedBencode.contains(TEST_TRACKER_URL));
+    }
+
+    //---------------- 测试 getInfoHash() -----------------
+    @Test
+    void testGetInfoHash_ShouldCalculateCorrectHash() throws Exception {
+        // 动态生成测试文件
+        Map<String, Object> info = Map.of(
+                "name", "test.txt",
+                "length", 1024,
+                "piece length", 16384,
+                "pieces", "1234567890abcdef1234"
+        );
+        File torrentFile = createTestTorrentFile(TEST_TRACKER_URL, info);
+
+        // 计算预期哈希(直接通过 Bencode 库生成 info 的哈希)
+        Bencode bencode = new Bencode();
+        byte[] infoBytes = bencode.encode(info);
+        String expectedHash = TorrentUtil.bytesToHex(
+                MessageDigest.getInstance("SHA-1").digest(infoBytes)
+        );
+
+        // 调用方法并验证
+        String actualHash = TorrentUtil.getInfoHash(torrentFile);
+        assertEquals(expectedHash, actualHash);
+    }
+
+    //---------------- 测试 saveToFile() -----------------
+    @Test
+    void testSaveToFile() throws Exception {
+        // 生成测试数据
+        byte[] testData = {0x01, 0x02, 0x03};
+        File outputFile = tempDir.resolve("output.torrent").toFile();
+
+        // 调用方法保存文件
+        TorrentUtil.saveToFile(testData, outputFile);
+
+        // 验证文件内容
+        byte[] savedData = Files.readAllBytes(outputFile.toPath());
+        assertArrayEquals(testData, savedData);
+    }
+      //---------------- 异常场景测试 -----------------
+    @Test
+    void getInfoHash_ShouldThrow_WhenInfoFieldMissing() throws Exception {
+        // 构造一个缺少 "info" 字段的无效 .torrent 文件
+        Map<String, Object> invalidTorrent = Map.of("announce", TEST_TRACKER_URL);
+        Bencode bencode = new Bencode();
+        byte[] bencodeData = bencode.encode(invalidTorrent);
+
+        Path invalidTorrentPath = tempDir.resolve("dynamic.torrent");
+        Files.write(invalidTorrentPath, bencodeData);
+
+        // 验证抛出异常
+        assertThrows(IllegalArgumentException.class, () -> TorrentUtil.getInfoHash(invalidTorrentPath.toFile()));
+    }
+}
\ No newline at end of file