Merge remote-tracking branch 'origin/whx' into whx

Change-Id: I93cf2efb1997754277bddd982ae4ca5d3034dca9
diff --git a/src/main/java/com/example/g8backend/controller/PostController.java b/src/main/java/com/example/g8backend/controller/PostController.java
index 3557af2..f373539 100644
--- a/src/main/java/com/example/g8backend/controller/PostController.java
+++ b/src/main/java/com/example/g8backend/controller/PostController.java
@@ -1,5 +1,6 @@
 package com.example.g8backend.controller;
 
+import com.example.g8backend.dto.PostCreateDTO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.ResponseEntity;
 import org.springframework.security.core.Authentication;
@@ -17,11 +18,18 @@
     private IPostService postService;
 
     @PostMapping("")
-    public ResponseEntity<?> createPost(@RequestBody Post post) {
+    public ResponseEntity<?> createPost(@RequestBody PostCreateDTO postCreateDTO) {
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
         long userId = (long) authentication.getPrincipal();
+        Post post = postCreateDTO.getPost();
+        Long[] tagIds = postCreateDTO.getTagIds();
+
         post.setUserId(userId);
-        postService.save(post);
+        if (tagIds.length > 0){
+            postService.createPost(post, tagIds);
+        } else {
+            postService.createPost(post);
+        }
         return ResponseEntity.ok().build();
     }
 
diff --git a/src/main/java/com/example/g8backend/controller/TrackerController.java b/src/main/java/com/example/g8backend/controller/TrackerController.java
index 1511c76..4e26abb 100644
--- a/src/main/java/com/example/g8backend/controller/TrackerController.java
+++ b/src/main/java/com/example/g8backend/controller/TrackerController.java
@@ -1,7 +1,9 @@
 package com.example.g8backend.controller;
 
+import com.example.g8backend.dto.AnnounceRequestDTO;
+import com.example.g8backend.dto.AnnounceResponseDTO;
+import jakarta.servlet.http.HttpServletRequest;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 import com.example.g8backend.service.ITrackerService;
@@ -13,21 +15,41 @@
     @Autowired
     private ITrackerService trackerService;
 
-    @Autowired
-    private RedisTemplate<String, Object> redisTemplate;
-
     @GetMapping("/announce/{passkey}")
-    public ResponseEntity<?> getAnnouncements(
-            @RequestParam(name = "info_hash") String infoHash,
-            @RequestParam(name = "peer_id") String peerId,
-            @RequestParam(name = "port") int port,
-            @RequestParam(name = "uploaded") long uploaded,
-            @RequestParam(name = "downloaded") long downloaded,
-            @RequestParam(name = "left") long left,
-            @RequestParam(name = "compact", required = false) int compact,
-            @RequestParam(name = "event", required = false) String event,
+    public ResponseEntity<AnnounceResponseDTO> getAnnouncements(
+            HttpServletRequest request,
+            @RequestParam("info_hash") String infoHash,
+            @RequestParam("peer_id") String peerId,
+            @RequestParam("port") int port,
+            @RequestParam("uploaded") double uploaded,
+            @RequestParam("downloaded") double downloaded,
+            @RequestParam(value = "event", required = false) String event,
+            @RequestParam(value = "left", required = false) Double left,
+            @RequestParam(value = "compact", required = false) Integer compact,
             @PathVariable String passkey) {
 
-        return null;
+        AnnounceRequestDTO requestDTO = new AnnounceRequestDTO();
+        requestDTO.setPasskey(passkey);
+        requestDTO.setInfoHash(infoHash);
+        requestDTO.setPeerId(peerId);
+        requestDTO.setPort(port);
+        requestDTO.setUploaded(uploaded);
+        requestDTO.setDownloaded(downloaded);
+        requestDTO.setEvent(event);
+        requestDTO.setLeft(left);
+        requestDTO.setCompact(compact);
+
+        String ipAddress = request.getHeader("X-Forwarded-For");
+        if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getHeader("Proxy-Client-IP");
+        }
+        if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getRemoteAddr();
+        }
+        requestDTO.setIp(ipAddress.split(",")[0]);
+        return ResponseEntity.ok(trackerService.handleAnnounce(requestDTO));
     }
 }
diff --git a/src/main/java/com/example/g8backend/dto/AnnounceRequestDTO.java b/src/main/java/com/example/g8backend/dto/AnnounceRequestDTO.java
new file mode 100644
index 0000000..e54f06d
--- /dev/null
+++ b/src/main/java/com/example/g8backend/dto/AnnounceRequestDTO.java
@@ -0,0 +1,17 @@
+package com.example.g8backend.dto;
+
+import lombok.Data;
+
+@Data
+public class AnnounceRequestDTO {
+    private String passkey;
+    private String infoHash;
+    private String peerId;
+    private int port;
+    private double uploaded;
+    private double downloaded;
+    private double left;
+    private Integer compact; // 可选
+    private String event;    // 可选
+    private String ip;
+}
diff --git a/src/main/java/com/example/g8backend/dto/AnnounceResponseDTO.java b/src/main/java/com/example/g8backend/dto/AnnounceResponseDTO.java
new file mode 100644
index 0000000..bbb1dc8
--- /dev/null
+++ b/src/main/java/com/example/g8backend/dto/AnnounceResponseDTO.java
@@ -0,0 +1,16 @@
+package com.example.g8backend.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class AnnounceResponseDTO {
+    private int interval;
+    private List<Map<String, Object>> peers;
+}
diff --git a/src/main/java/com/example/g8backend/dto/PostCreateDTO.java b/src/main/java/com/example/g8backend/dto/PostCreateDTO.java
new file mode 100644
index 0000000..a14deea
--- /dev/null
+++ b/src/main/java/com/example/g8backend/dto/PostCreateDTO.java
@@ -0,0 +1,10 @@
+package com.example.g8backend.dto;
+
+import com.example.g8backend.entity.Post;
+import lombok.Data;
+
+@Data
+public class PostCreateDTO {
+    private Post post;
+    private Long[] tagIds;
+}
diff --git a/src/main/java/com/example/g8backend/entity/Peer.java b/src/main/java/com/example/g8backend/entity/Peer.java
index b59f7ee..e3e1342 100644
--- a/src/main/java/com/example/g8backend/entity/Peer.java
+++ b/src/main/java/com/example/g8backend/entity/Peer.java
@@ -6,7 +6,7 @@
 @Data
 @TableName("peers")
 public class Peer {
-    private Long peerId;
+    private String peerId;
     private String info_hash;
     private String passkey;
     private String ipAddress;
diff --git a/src/main/java/com/example/g8backend/entity/PostTag.java b/src/main/java/com/example/g8backend/entity/PostTag.java
new file mode 100644
index 0000000..804faaa
--- /dev/null
+++ b/src/main/java/com/example/g8backend/entity/PostTag.java
@@ -0,0 +1,18 @@
+package com.example.g8backend.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("post_tag")
+public class PostTag {
+    private Long postId;
+    private Long tagId;
+
+    public PostTag(Long postId, Long tagId) {
+        this.postId = postId;
+        this.tagId = tagId;
+    }
+}
diff --git a/src/main/java/com/example/g8backend/entity/Tag.java b/src/main/java/com/example/g8backend/entity/Tag.java
new file mode 100644
index 0000000..75eaa38
--- /dev/null
+++ b/src/main/java/com/example/g8backend/entity/Tag.java
@@ -0,0 +1,25 @@
+package com.example.g8backend.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+@Data
+@TableName("tags")
+public class Tag {
+    @TableId(type = IdType.AUTO)
+    private Long tagId;
+
+    private String name;
+    private Long parentId;
+
+    @Override
+    public String toString() {
+        return "Tag{" +
+                "tagId=" + tagId +
+                ", name='" + name + '\'' +
+                ", parentId=" + parentId +
+                '}';
+    }
+}
diff --git a/src/main/java/com/example/g8backend/mapper/PeerMapper.java b/src/main/java/com/example/g8backend/mapper/PeerMapper.java
index ce84bbe..f0b9b85 100644
--- a/src/main/java/com/example/g8backend/mapper/PeerMapper.java
+++ b/src/main/java/com/example/g8backend/mapper/PeerMapper.java
@@ -2,7 +2,14 @@
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.example.g8backend.entity.Peer;
+import org.apache.ibatis.annotations.Mapper;
 
+import java.util.List;
+
+@Mapper
 public interface PeerMapper extends BaseMapper<Peer> {
-
+    // get peer by primary key(peerId, infoHash, passkey)
+    Peer getPeerByPK(String peerId, String infoHash, String passkey);
+    List<Peer> getPeerByInfoHashAndPeerId(String infoHash, String peerId);
+    void updatePeer(String passkey, String peerId, String info_hash, double uploaded, double downloaded);
 }
diff --git a/src/main/java/com/example/g8backend/mapper/PostTagMapper.java b/src/main/java/com/example/g8backend/mapper/PostTagMapper.java
new file mode 100644
index 0000000..184feb2
--- /dev/null
+++ b/src/main/java/com/example/g8backend/mapper/PostTagMapper.java
@@ -0,0 +1,17 @@
+package com.example.g8backend.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.g8backend.entity.Post;
+import com.example.g8backend.entity.PostTag;
+import com.example.g8backend.entity.Tag;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+@Mapper
+public interface PostTagMapper extends BaseMapper<PostTag> {
+    List<Post> getPostsByTagIds(@Param("tagIds") Long[] tagIds);
+    List<Tag> getTagsByPostId(@Param("postId") Long postId);
+    int deleteByIds(@Param("postId") Long postId, @Param("tagId") Long tagId);
+}
diff --git a/src/main/java/com/example/g8backend/mapper/TagMapper.java b/src/main/java/com/example/g8backend/mapper/TagMapper.java
new file mode 100644
index 0000000..f959dd8
--- /dev/null
+++ b/src/main/java/com/example/g8backend/mapper/TagMapper.java
@@ -0,0 +1,9 @@
+package com.example.g8backend.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.g8backend.entity.Tag;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface TagMapper extends BaseMapper<Tag> {
+}
diff --git a/src/main/java/com/example/g8backend/service/IPostService.java b/src/main/java/com/example/g8backend/service/IPostService.java
index f81053e..f914946 100644
--- a/src/main/java/com/example/g8backend/service/IPostService.java
+++ b/src/main/java/com/example/g8backend/service/IPostService.java
@@ -2,11 +2,13 @@
 
 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);
+    void createPost(Post post);
+    void createPost(Post post, Long[] tagIds);
     Post updatePost(Post post);
     List<Post> getPostsByType(String postType);
     Long getPostLikeCount(Long postId);
diff --git a/src/main/java/com/example/g8backend/service/IPostTagService.java b/src/main/java/com/example/g8backend/service/IPostTagService.java
new file mode 100644
index 0000000..03b16fa
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/IPostTagService.java
@@ -0,0 +1,16 @@
+package com.example.g8backend.service;
+
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.example.g8backend.entity.Tag;
+import com.example.g8backend.entity.Post;
+import com.example.g8backend.entity.PostTag;
+
+import java.util.List;
+
+public interface IPostTagService extends IService<PostTag> {
+    boolean save(PostTag postTag);
+    List<Post> getPostsByTagIds(Long[] tagIds);
+    List<Tag> getTagsByPostId(Long postId);
+    boolean removeByIds(PostTag postTag);
+}
diff --git a/src/main/java/com/example/g8backend/service/ITagService.java b/src/main/java/com/example/g8backend/service/ITagService.java
new file mode 100644
index 0000000..0fc589f
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/ITagService.java
@@ -0,0 +1,7 @@
+package com.example.g8backend.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.example.g8backend.entity.Tag;
+
+public interface ITagService extends IService<Tag> {
+}
diff --git a/src/main/java/com/example/g8backend/service/ITrackerService.java b/src/main/java/com/example/g8backend/service/ITrackerService.java
index e522d77..055e317 100644
--- a/src/main/java/com/example/g8backend/service/ITrackerService.java
+++ b/src/main/java/com/example/g8backend/service/ITrackerService.java
@@ -1,6 +1,9 @@
 package com.example.g8backend.service;
 
 
-public interface ITrackerService {
+import com.example.g8backend.dto.AnnounceRequestDTO;
+import com.example.g8backend.dto.AnnounceResponseDTO;
 
+public interface ITrackerService {
+    AnnounceResponseDTO handleAnnounce(AnnounceRequestDTO requestDTO);
 }
diff --git a/src/main/java/com/example/g8backend/service/impl/PostServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/PostServiceImpl.java
index 09a471e..d40ddd6 100644
--- a/src/main/java/com/example/g8backend/service/impl/PostServiceImpl.java
+++ b/src/main/java/com/example/g8backend/service/impl/PostServiceImpl.java
@@ -3,9 +3,11 @@
 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.entity.PostTag;
 import com.example.g8backend.mapper.PostMapper;
 import com.example.g8backend.service.IPostService;
-import jakarta.annotation.Resource;
+import com.example.g8backend.service.IPostTagService;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import java.sql.Timestamp;
 import java.util.List;
@@ -15,6 +17,9 @@
     
     private final PostMapper postMapper;
 
+    @Autowired
+    private IPostTagService postTagService;
+
     public PostServiceImpl(PostMapper postMapper) {
         this.postMapper = postMapper;
         this.baseMapper = postMapper; // 重要:设置 baseMapper
@@ -26,10 +31,18 @@
     }
 
     @Override
-    public Post createPost(Post post) {
+    public void createPost(Post post) {
         post.setCreatedAt(new Timestamp(System.currentTimeMillis()));
         save(post);
-        return post;
+    }
+
+    @Override
+    public void createPost(Post post, Long[] tagIds) {
+        post.setCreatedAt(new Timestamp(System.currentTimeMillis()));
+        save(post);
+        for (long tagId : tagIds) {
+            postTagService.save(new PostTag(post.getPostId(), tagId));
+        }
     }
 
     @Override 
diff --git a/src/main/java/com/example/g8backend/service/impl/PostTagServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/PostTagServiceImpl.java
new file mode 100644
index 0000000..0aa0647
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/PostTagServiceImpl.java
@@ -0,0 +1,42 @@
+package com.example.g8backend.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.example.g8backend.entity.Post;
+import com.example.g8backend.entity.PostTag;
+import com.example.g8backend.entity.Tag;
+import com.example.g8backend.mapper.PostTagMapper;
+import com.example.g8backend.service.IPostTagService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+public class PostTagServiceImpl extends ServiceImpl<PostTagMapper, PostTag> implements IPostTagService {
+
+    @Autowired
+    private PostTagMapper postTagMapper;
+
+    @Override
+    public boolean save(PostTag postTag) {
+        return postTagMapper.insert(postTag) > 0;
+    }
+
+    @Override
+    public List<Post> getPostsByTagIds(Long[] tagIds) {
+        if (tagIds == null || tagIds.length == 0) return new ArrayList<>();
+        return postTagMapper.getPostsByTagIds(tagIds);
+    }
+
+    @Override
+    public List<Tag> getTagsByPostId(Long postId) {
+        if (postId == null) return new ArrayList<>();
+        return postTagMapper.getTagsByPostId(postId);
+    }
+
+    @Override
+    public boolean removeByIds(PostTag postTag) {
+        return postTagMapper.deleteByIds(postTag.getPostId(), postTag.getTagId()) > 0;
+    }
+}
diff --git a/src/main/java/com/example/g8backend/service/impl/TagServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/TagServiceImpl.java
new file mode 100644
index 0000000..41b95a3
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/TagServiceImpl.java
@@ -0,0 +1,12 @@
+package com.example.g8backend.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.example.g8backend.entity.Tag;
+import com.example.g8backend.mapper.TagMapper;
+import com.example.g8backend.service.ITagService;
+import jakarta.annotation.Resource;
+
+public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements ITagService {
+    @Resource
+    private TagMapper tagMapper;
+}
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 bedb94a..199ff2a 100644
--- a/src/main/java/com/example/g8backend/service/impl/TorrentServiceImpl.java
+++ b/src/main/java/com/example/g8backend/service/impl/TorrentServiceImpl.java
@@ -17,7 +17,7 @@
     @Resource
     private TorrentMapper torrentMapper;
 
-    String tracker = "http://127.0.0.1:8080/announce/";
+    String tracker = "http://127.0.0.1:8080/tracker/announce/";
 
     @Override
     public Torrent handleTorrentUpload(File file, Long userId, String passkey) throws IOException, IllegalArgumentException {
diff --git a/src/main/java/com/example/g8backend/service/impl/TrackerServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/TrackerServiceImpl.java
index c0dba70..ffb5e65 100644
--- a/src/main/java/com/example/g8backend/service/impl/TrackerServiceImpl.java
+++ b/src/main/java/com/example/g8backend/service/impl/TrackerServiceImpl.java
@@ -1,8 +1,88 @@
 package com.example.g8backend.service.impl;
 
+import com.example.g8backend.dto.AnnounceRequestDTO;
+import com.example.g8backend.dto.AnnounceResponseDTO;
+import com.example.g8backend.entity.Peer;
+import com.example.g8backend.mapper.PeerMapper;
 import com.example.g8backend.service.ITrackerService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 
+import java.util.*;
+
 @Service
 public class TrackerServiceImpl implements ITrackerService {
+    final int interval = 30;
+
+    @Autowired
+    private PeerMapper peerMapper;
+
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
+    @Override
+    public AnnounceResponseDTO handleAnnounce(AnnounceRequestDTO requestDTO) {
+        String passkey = requestDTO.getPasskey();
+        String infoHash = requestDTO.getInfoHash();
+        String peerId = requestDTO.getPeerId();
+        int port = requestDTO.getPort();
+        String ip = requestDTO.getIp();
+        double uploaded = requestDTO.getUploaded();
+        double downloaded = requestDTO.getDownloaded();
+        String event = requestDTO.getEvent();
+
+        String redisKey = "peers:" + infoHash;
+
+        if ("stopped".equalsIgnoreCase(event)) {
+            // 从 Redis 中移除 peer
+            redisTemplate.opsForSet().remove(redisKey, peerId);
+            return new AnnounceResponseDTO(interval, Collections.emptyList());
+        }
+
+        // 插入或更新 peer 信息
+        Peer existingPeer = peerMapper.getPeerByPK(peerId, infoHash, passkey);
+        if (existingPeer == null) {
+            Peer newPeer = new Peer();
+            newPeer.setPeerId(peerId);
+            newPeer.setInfo_hash(infoHash);
+            newPeer.setPasskey(passkey);
+            newPeer.setIpAddress(ip); // TODO: 从 request 获取真实 IP
+            newPeer.setPort(port);
+            newPeer.setUploaded(uploaded);
+            newPeer.setDownloaded(downloaded);
+            peerMapper.insert(newPeer);
+        } else {
+            existingPeer.setUploaded(uploaded);
+            existingPeer.setDownloaded(downloaded);
+            peerMapper.updatePeer(existingPeer.getPasskey(), existingPeer.getPeerId(), existingPeer.getInfo_hash(),
+                    existingPeer.getUploaded(), existingPeer.getDownloaded());
+        }
+
+        // 缓存 peer 到 Redis
+        redisTemplate.opsForSet().add(redisKey, peerId);
+
+        // 构造返回 peer 列表
+        List<Map<String, Object>> peerList = new ArrayList<>();
+        Set<Object> peerIds = redisTemplate.opsForSet().members(redisKey);
+        try {
+            assert peerIds != null;
+            for (Object pid : peerIds) {
+                List<Peer> peers = peerMapper.getPeerByInfoHashAndPeerId(infoHash, (String) pid);
+                for (Peer peer : peers) {
+                    if (peer != null) {
+                        Map<String, Object> peerMap = new HashMap<>();
+                        peerMap.put("ip", peer.getIpAddress());
+                        peerMap.put("port", peer.getPort());
+                        peerList.add(peerMap);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            return new AnnounceResponseDTO(interval, Collections.emptyList());
+        }
+
+        return new AnnounceResponseDTO(interval, peerList);
+    }
+
 }
diff --git a/src/main/java/com/example/g8backend/util/TorrentUtil.java b/src/main/java/com/example/g8backend/util/TorrentUtil.java
index e8360bc..b7390d6 100644
--- a/src/main/java/com/example/g8backend/util/TorrentUtil.java
+++ b/src/main/java/com/example/g8backend/util/TorrentUtil.java
@@ -4,12 +4,13 @@
 import com.dampcake.bencode.Type;
 
 import java.io.*;
+import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.util.Map;
 
 public class TorrentUtil {
 
-    private static final Bencode bencode = new Bencode();
+    private static final Bencode bencode = new Bencode(StandardCharsets.ISO_8859_1);
 
     public static byte[] injectTracker(File torrentFile, String trackerUrl) throws IOException {
         byte[] fileBytes = readBytes(torrentFile);
@@ -17,7 +18,9 @@
         Map<String, Object> torrentMap = bencode.decode(fileBytes, Type.DICTIONARY);
 
         // trackerUrl: ip:port + /announce / {passkey}
-        torrentMap.put("announce", trackerUrl);
+        byte[] ISO_trackerUrlByte = trackerUrl.getBytes(StandardCharsets.ISO_8859_1);
+        String ISO_trackerUrl = new String(ISO_trackerUrlByte, StandardCharsets.ISO_8859_1);
+        torrentMap.put("announce", ISO_trackerUrl); // ISO-8859-1
 
         return bencode.encode(torrentMap);
     }
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 7f85f87..22f3728 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -18,3 +18,5 @@
 spring.mail.properties.mail.smtp.auth=true
 spring.mail.properties.mail.smtp.ssl.enable=true
 spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
+
+logging.level.org.springframework.data.redis= DEBUG
diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql
index 0907033..607d353 100644
--- a/src/main/resources/data.sql
+++ b/src/main/resources/data.sql
@@ -1 +1,11 @@
-# 后面统一数据库数据用
\ No newline at end of file
+# # 后面统一数据库数据用
+#
+# # tags表父标签
+# INSERT INTO `tags` (tag_id, tag_name, parent_id) VALUES (1, '电影', NULL);
+# INSERT INTO `tags` (tag_id, tag_name, parent_id) VALUES (2, '游戏', NULL);
+# INSERT INTO `tags` (tag_id, tag_name, parent_id) VALUES (3, '音乐', NULL);
+#
+# # tags表子标签
+# INSERT INTO `tags` (tag_id, tag_name, parent_id) VALUES (4, '动作', 1);
+# INSERT INTO `tags` (tag_id, tag_name, parent_id) VALUES (5, '喜剧', 1);
+# INSERT INTO `tags` (tag_id, tag_name, parent_id) VALUES (6, '科幻', 1);
\ No newline at end of file
diff --git a/src/main/resources/mapper/PeerMapper.xml b/src/main/resources/mapper/PeerMapper.xml
new file mode 100644
index 0000000..b3d4780
--- /dev/null
+++ b/src/main/resources/mapper/PeerMapper.xml
@@ -0,0 +1,18 @@
+<?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.PeerMapper">
+    <select id="getPeerByPK">
+        SELECT * FROM peers
+        WHERE peer_id = #{peerId} and info_hash = #{infoHash} and passkey = #{passkey}
+    </select>
+    <select id="getPeerByInfoHashAndPeerId">
+        SELECT * FROM peers
+        WHERE info_hash = #{infoHash} and peer_id = #{peerId}
+    </select>
+    <update id="updatePeer">
+        UPDATE peers
+        SET uploaded = #{uploaded}, downloaded = #{downloaded}
+        WHERE peer_id = #{peerId} and info_hash = #{info_hash} and passkey = #{passkey}
+    </update>
+</mapper>
diff --git a/src/main/resources/mapper/PostTagMapper.xml b/src/main/resources/mapper/PostTagMapper.xml
new file mode 100644
index 0000000..b2cfc69
--- /dev/null
+++ b/src/main/resources/mapper/PostTagMapper.xml
@@ -0,0 +1,22 @@
+<?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.entity.PostTag">
+    <select id="getPostsByTagIds" resultType="com.example.g8backend.entity.Post">
+        SELECT DISTINCT p.* FROM posts p JOIN post_tag pt ON p.post_id = pt.post_id
+        WHERE pt.tag_id IN
+        <foreach collection="tagIds" item="tagId" open="(" separator="," close=")">
+            #{tagId}
+        </foreach>
+    </select>
+
+    <select id="getTagsByPostId">
+        SELECT DISTINCT t.* FROM tags t JOIN post_tag pt ON t.tag_id = pt.tag_id
+    </select>
+
+    <delete id="deleteByIds">
+        DELETE FROM post_tag
+        WHERE post_id = #{postId} AND tag_id = #{tagId}
+    </delete>
+</mapper>
\ No newline at end of file
diff --git a/src/test/java/com/example/g8backend/service/PostTagServiceTest.java b/src/test/java/com/example/g8backend/service/PostTagServiceTest.java
new file mode 100644
index 0000000..6ec9055
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/PostTagServiceTest.java
@@ -0,0 +1,96 @@
+package com.example.g8backend.service;
+
+import com.example.g8backend.entity.Post;
+import com.example.g8backend.entity.PostTag;
+import com.example.g8backend.entity.Tag;
+import com.example.g8backend.mapper.PostTagMapper;
+import com.example.g8backend.service.impl.PostTagServiceImpl;
+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.junit.jupiter.MockitoExtension;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+public class PostTagServiceTest {
+
+    @InjectMocks
+    private PostTagServiceImpl postTagService;
+
+    @Mock
+    private PostTagMapper postTagMapper;
+
+    private PostTag samplePostTag;
+
+    @BeforeEach
+    public void setUp() {
+        samplePostTag = new PostTag(1L, 2L);
+    }
+
+    @Test
+    public void testSave() {
+        when(postTagMapper.insert(any(PostTag.class))).thenReturn(1);
+
+        boolean result = postTagService.save(samplePostTag);
+
+        assertTrue(result);
+        verify(postTagMapper, times(1)).insert(samplePostTag);
+    }
+
+    @Test
+    public void testGetPostsByTagIds() {
+        Long[] tagIds = {1L, 2L};
+        List<Post> expectedPosts = List.of(new Post());
+        when(postTagMapper.getPostsByTagIds(tagIds)).thenReturn(expectedPosts);
+
+        List<Post> result = postTagService.getPostsByTagIds(tagIds);
+
+        assertEquals(expectedPosts, result);
+        verify(postTagMapper, times(1)).getPostsByTagIds(tagIds);
+    }
+
+    @Test
+    public void testGetPostsByTagIds_EmptyInput() {
+        List<Post> result = postTagService.getPostsByTagIds(new Long[]{});
+
+        assertTrue(result.isEmpty());
+        verify(postTagMapper, never()).getPostsByTagIds(any());
+    }
+
+    @Test
+    public void testGetTagsByPostId() {
+        Long postId = 1L;
+        List<Tag> expectedTags = List.of(new Tag());
+        when(postTagMapper.getTagsByPostId(postId)).thenReturn(expectedTags);
+
+        List<Tag> result = postTagService.getTagsByPostId(postId);
+
+        assertEquals(expectedTags, result);
+        verify(postTagMapper, times(1)).getTagsByPostId(postId);
+    }
+
+    @Test
+    public void testGetTagsByPostId_Null() {
+        List<Tag> result = postTagService.getTagsByPostId(null);
+
+        assertTrue(result.isEmpty());
+        verify(postTagMapper, never()).getTagsByPostId(any());
+    }
+
+    @Test
+    public void testRemoveByIds() {
+        when(postTagMapper.deleteByIds(1L, 2L)).thenReturn(1);
+
+        boolean result = postTagService.removeByIds(samplePostTag);
+
+        assertTrue(result);
+        verify(postTagMapper, times(1)).deleteByIds(1L, 2L);
+    }
+}
diff --git a/src/test/java/com/example/g8backend/service/TrackerServiceTest.java b/src/test/java/com/example/g8backend/service/TrackerServiceTest.java
new file mode 100644
index 0000000..829fe15
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/TrackerServiceTest.java
@@ -0,0 +1,138 @@
+package com.example.g8backend.service;
+
+import com.example.g8backend.dto.AnnounceRequestDTO;
+import com.example.g8backend.dto.AnnounceResponseDTO;
+import com.example.g8backend.entity.Peer;
+import com.example.g8backend.mapper.PeerMapper;
+import com.example.g8backend.service.impl.TrackerServiceImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.*;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.SetOperations;
+
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+public class TrackerServiceTest {
+
+    @InjectMocks
+    private TrackerServiceImpl trackerService;
+
+    @Mock
+    private PeerMapper peerMapper;
+
+    @Mock
+    private RedisTemplate<String, Object> redisTemplate;
+
+    @Mock
+    private SetOperations<String, Object> setOperations;
+
+    @BeforeEach
+    public void setUp() {
+        when(redisTemplate.opsForSet()).thenReturn(setOperations);
+    }
+
+    @Test
+    public void testHandleAnnounceStoppedEvent() {
+        AnnounceRequestDTO requestDTO = new AnnounceRequestDTO();
+        requestDTO.setEvent("stopped");
+        requestDTO.setInfoHash("infohash");
+        requestDTO.setPeerId("peer1");
+
+        AnnounceResponseDTO response = trackerService.handleAnnounce(requestDTO);
+
+        verify(setOperations).remove("peers:infohash", "peer1");
+        assertEquals(30, response.getInterval());
+        assertTrue(response.getPeers().isEmpty());
+    }
+
+    @Test
+    public void testHandleAnnounceWithNewPeer() {
+        AnnounceRequestDTO requestDTO = new AnnounceRequestDTO();
+        requestDTO.setPasskey("key123");
+        requestDTO.setInfoHash("infohash");
+        requestDTO.setPeerId("peer1");
+        requestDTO.setIp("192.168.0.1");
+        requestDTO.setPort(6881);
+        requestDTO.setUploaded(1000);
+        requestDTO.setDownloaded(500);
+        requestDTO.setEvent("started");
+
+        when(peerMapper.getPeerByPK("peer1", "infohash", "key123")).thenReturn(null);
+        when(setOperations.members("peers:infohash")).thenReturn(Set.of("peer1"));
+        when(peerMapper.getPeerByInfoHashAndPeerId("infohash", "peer1")).thenReturn(List.of(new Peer() {{
+            setIpAddress("192.168.0.1");
+            setPort(6881);
+        }}));
+
+        AnnounceResponseDTO response = trackerService.handleAnnounce(requestDTO);
+
+        verify(peerMapper).insert(any(Peer.class));
+        verify(setOperations).add("peers:infohash", "peer1");
+        assertEquals(30, response.getInterval());
+        assertEquals(1, response.getPeers().size());
+        assertEquals("192.168.0.1", response.getPeers().get(0).get("ip"));
+    }
+
+    @Test
+    public void testHandleAnnounceWithExistingPeer() {
+        AnnounceRequestDTO requestDTO = new AnnounceRequestDTO();
+        requestDTO.setPasskey("key123");
+        requestDTO.setInfoHash("infohash");
+        requestDTO.setPeerId("peer2");
+        requestDTO.setIp("192.168.1.1");
+        requestDTO.setPort(6882);
+        requestDTO.setUploaded(2000);
+        requestDTO.setDownloaded(1000);
+        requestDTO.setEvent("update");
+
+        Peer existing = new Peer();
+        existing.setPeerId("peer2");
+        existing.setPasskey("key123");
+        existing.setInfo_hash("infohash");
+        existing.setUploaded(1000.0);
+        existing.setDownloaded(500.0);
+
+        when(peerMapper.getPeerByPK("peer2", "infohash", "key123")).thenReturn(existing);
+        when(setOperations.members("peers:infohash")).thenReturn(Set.of("peer2"));
+        when(peerMapper.getPeerByInfoHashAndPeerId("infohash", "peer2")).thenReturn(List.of(new Peer() {{
+            setIpAddress("192.168.1.1");
+            setPort(6882);
+        }}));
+
+        AnnounceResponseDTO response = trackerService.handleAnnounce(requestDTO);
+
+        verify(peerMapper).updatePeer(eq("key123"), eq("peer2"), eq("infohash"), eq(2000.0), eq(1000.0));
+        verify(setOperations).add("peers:infohash", "peer2");
+        assertEquals(1, response.getPeers().size());
+        assertEquals("192.168.1.1", response.getPeers().get(0).get("ip"));
+    }
+
+    @Test
+    public void testHandleAnnounceExceptionInRedis() {
+        AnnounceRequestDTO requestDTO = new AnnounceRequestDTO();
+        requestDTO.setPasskey("keyX");
+        requestDTO.setInfoHash("hashX");
+        requestDTO.setPeerId("peerX");
+        requestDTO.setIp("10.0.0.1");
+        requestDTO.setPort(6883);
+        requestDTO.setUploaded(100);
+        requestDTO.setDownloaded(200);
+        requestDTO.setEvent("started");
+
+        when(peerMapper.getPeerByPK(any(), any(), any())).thenReturn(null);
+        when(setOperations.members(anyString())).thenReturn(new HashSet<>() );
+
+        AnnounceResponseDTO response = trackerService.handleAnnounce(requestDTO);
+
+        assertEquals(30, response.getInterval());
+        assertTrue(response.getPeers().isEmpty());
+    }
+}
diff --git a/uploaded-torrents/upload-1263176031835183570.torrent b/uploaded-torrents/upload-1263176031835183570.torrent
deleted file mode 100644
index 8634e79..0000000
--- a/uploaded-torrents/upload-1263176031835183570.torrent
+++ /dev/null
Binary files differ