增加了种子统计的内容

Change-Id: I139c8cf149c05465d1170baa4c143210bd71c888
diff --git a/src/main/java/com/pt/entity/PeerInfoEntity.java b/src/main/java/com/pt/entity/PeerInfoEntity.java
index 081df7f..c19a269 100644
--- a/src/main/java/com/pt/entity/PeerInfoEntity.java
+++ b/src/main/java/com/pt/entity/PeerInfoEntity.java
@@ -16,6 +16,15 @@
     private String peerId;
     private LocalDateTime lastSeen;
 
+    // 新增状态字段
+    private String status; // "seeding", "downloading", "completed"
+    private boolean isActive; // 是否活跃
+
+    // 下载字段
+    private long uploaded;        // 已上传量
+    private long downloaded;      // 已下载量
+    private long left;            // 剩余下载量
+
     public Long getId() {
         return id;
     }
@@ -28,6 +37,30 @@
         return infoHash;
     }
 
+    public long getUploaded() {
+        return uploaded;
+    }
+
+    public void setUploaded(long uploaded) {
+        this.uploaded = uploaded;
+    }
+
+    public long getDownloaded() {
+        return downloaded;
+    }
+
+    public void setDownloaded(long downloaded) {
+        this.downloaded = downloaded;
+    }
+
+    public long getLeft() {
+        return left;
+    }
+
+    public void setLeft(long left) {
+        this.left = left;
+    }
+
     public void setInfoHash(String infoHash) {
         this.infoHash = infoHash;
     }
@@ -63,4 +96,21 @@
     public void setPeerId(String peerId) {
         this.peerId = peerId;
     }
+
+    // 新增状态字段的getter与setter
+    public String getStatus() {
+        return status;
+    }
+
+    public void setStatus(String status) {
+        this.status = status;
+    }
+
+    public boolean isActive() {
+        return isActive;
+    }
+
+    public void setActive(boolean active) {
+        isActive = active;
+    }
 }
diff --git a/src/main/java/com/pt/entity/TorrentStats.java b/src/main/java/com/pt/entity/TorrentStats.java
new file mode 100644
index 0000000..0c04142
--- /dev/null
+++ b/src/main/java/com/pt/entity/TorrentStats.java
@@ -0,0 +1,71 @@
+package com.pt.entity;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+
+import java.time.LocalDateTime;
+
+@Entity
+public class TorrentStats {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    private Long torrentId;       // 关联的种子ID
+    private int seederCount;     // 当前做种人数
+    private int leecherCount;    // 当前下载人数
+    private int completedCount;  // 历史完成下载次数
+    private LocalDateTime lastUpdated;
+
+    // Getters and Setters
+
+    public int getSeederCount() {
+        return seederCount;
+    }
+
+    public void setSeederCount(int seederCount) {
+        this.seederCount = seederCount;
+    }
+
+    public int getLeecherCount() {
+        return leecherCount;
+    }
+
+    public void setLeecherCount(int leecherCount) {
+        this.leecherCount = leecherCount;
+    }
+
+    public int getCompletedCount() {
+        return completedCount;
+    }
+
+    public void setCompletedCount(int completedCount) {
+        this.completedCount = completedCount;
+    }
+
+    public LocalDateTime getLastUpdated() {
+        return lastUpdated;
+    }
+
+    public void setLastUpdated(LocalDateTime lastUpdated) {
+        this.lastUpdated = lastUpdated;
+    }
+
+    public Long getTorrentId() {
+        return torrentId;
+    }
+
+    public void setTorrentId(Long torrentId) {
+        this.torrentId = torrentId;
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+}
diff --git a/src/main/java/com/pt/exception/ResourceNotFoundException.java b/src/main/java/com/pt/exception/ResourceNotFoundException.java
new file mode 100644
index 0000000..07a3d3c
--- /dev/null
+++ b/src/main/java/com/pt/exception/ResourceNotFoundException.java
@@ -0,0 +1,19 @@
+package com.pt.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(HttpStatus.NOT_FOUND)
+public class ResourceNotFoundException extends RuntimeException {
+    public ResourceNotFoundException() {
+        super();
+    }
+
+    public ResourceNotFoundException(String message) {
+        super(message);
+    }
+
+    public ResourceNotFoundException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pt/repository/PeerInfoRepository.java b/src/main/java/com/pt/repository/PeerInfoRepository.java
index 565204c..b096a35 100644
--- a/src/main/java/com/pt/repository/PeerInfoRepository.java
+++ b/src/main/java/com/pt/repository/PeerInfoRepository.java
@@ -2,8 +2,23 @@
 
 import com.pt.entity.PeerInfoEntity;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+
+import java.time.LocalDateTime;
 import java.util.List;
+import java.util.Optional;
 
 public interface PeerInfoRepository extends JpaRepository<PeerInfoEntity, Long> {
     List<PeerInfoEntity> findByInfoHash(String infoHash);
+
+    // 根据 peerId 和 infoHash 查找
+    Optional<PeerInfoEntity> findByPeerIdAndInfoHash(String peerId, String infoHash);
+
+    // 获取活跃的 peer
+    @Query("SELECT p FROM PeerInfoEntity p WHERE p.infoHash = :infoHash AND p.isActive = true")
+    List<PeerInfoEntity> findActivePeersByInfoHash(String infoHash);
+
+    // 获取需要清理的非活跃 peer
+    @Query("SELECT p FROM PeerInfoEntity p WHERE p.lastSeen < :threshold")
+    List<PeerInfoEntity> findInactivePeers(LocalDateTime threshold);
 }
diff --git a/src/main/java/com/pt/repository/TorrentStatsRepository.java b/src/main/java/com/pt/repository/TorrentStatsRepository.java
new file mode 100644
index 0000000..70b5c7d
--- /dev/null
+++ b/src/main/java/com/pt/repository/TorrentStatsRepository.java
@@ -0,0 +1,58 @@
+package com.pt.repository;
+
+import com.pt.entity.TorrentStats;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+public interface TorrentStatsRepository extends JpaRepository<TorrentStats, Long> {
+
+    // 1. 根据种子ID查找统计信息
+    Optional<TorrentStats> findByTorrentId(Long torrentId);
+
+    // 2. 获取做种人数最多的种子(用于热门种子)
+    @Query("SELECT s FROM TorrentStats s ORDER BY s.seederCount DESC")
+    List<TorrentStats> findTopBySeederCount(int limit);
+
+    // 3. 获取最近活跃的种子
+    @Query("SELECT s FROM TorrentStats s ORDER BY s.lastUpdated DESC")
+    List<TorrentStats> findRecentActiveTorrents(int limit);
+
+    // 4. 根据多个种子ID批量获取统计信息
+    @Query("SELECT s FROM TorrentStats s WHERE s.torrentId IN :torrentIds")
+    List<TorrentStats> findByTorrentIds(@Param("torrentIds") List<Long> torrentIds);
+
+    // 5. 获取所有需要更新的种子统计(用于定时任务)
+    @Query("SELECT s.torrentId FROM TorrentStats s WHERE s.lastUpdated < :threshold")
+    List<Long> findStaleStatsIds(@Param("threshold") LocalDateTime threshold);
+
+    // 6. 增加完成次数(当有用户完成下载时调用)
+    @Modifying
+    @Query("UPDATE TorrentStats s SET s.completedCount = s.completedCount + 1 WHERE s.torrentId = :torrentId")
+    void incrementCompletedCount(@Param("torrentId") Long torrentId);
+
+    // 7. 更新做种者和下载者数量
+    @Modifying
+    @Query("UPDATE TorrentStats s SET s.seederCount = :seeders, s.leecherCount = :leechers, s.lastUpdated = CURRENT_TIMESTAMP WHERE s.torrentId = :torrentId")
+    void updatePeerCounts(@Param("torrentId") Long torrentId,
+                          @Param("seeders") int seeders,
+                          @Param("leechers") int leechers);
+
+    // 8. 添加缺失的方法:更新最后更新时间
+    @Modifying
+    @Query("UPDATE TorrentStats s SET s.lastUpdated = :lastUpdated WHERE s.torrentId = :torrentId")
+    void updateLastUpdated(@Param("torrentId") Long torrentId,
+                           @Param("lastUpdated") LocalDateTime lastUpdated);
+
+    // 9. 添加缺失的方法:使用当前时间戳更新最后更新时间
+    @Modifying
+    @Query("UPDATE TorrentStats s SET s.lastUpdated = CURRENT_TIMESTAMP WHERE s.torrentId = :torrentId")
+    void updateLastUpdatedToNow(@Param("torrentId") Long torrentId);
+}
\ No newline at end of file
diff --git a/src/main/java/com/pt/service/TorrentStatsService.java b/src/main/java/com/pt/service/TorrentStatsService.java
new file mode 100644
index 0000000..ee11689
--- /dev/null
+++ b/src/main/java/com/pt/service/TorrentStatsService.java
@@ -0,0 +1,112 @@
+package com.pt.service;
+
+import com.pt.entity.PeerInfoEntity;
+import com.pt.entity.TorrentStats;
+import com.pt.exception.ResourceNotFoundException;
+import com.pt.repository.PeerInfoRepository;
+import com.pt.repository.TorrentMetaRepository;
+import com.pt.repository.TorrentStatsRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Service
+public class TorrentStatsService {
+
+    @Autowired
+    private PeerInfoRepository peerInfoRepository;
+
+    @Autowired
+    private TorrentStatsRepository statsRepository;
+
+    @Autowired
+    private TorrentMetaRepository torrentMetaRepository;
+
+    @Autowired
+    public TorrentStatsService(TorrentStatsRepository statsRepository) {
+        this.statsRepository = statsRepository;
+    }
+
+    /**
+     * 增加种子完成次数
+     *
+     * @param torrentId 种子ID
+     */
+    @Transactional
+    public void incrementCompletedCount(Long torrentId) {
+        // 1. 检查统计记录是否存在
+        if (!statsRepository.findByTorrentId(torrentId).isPresent()) {
+            // 创建新的统计记录
+            TorrentStats newStats = new TorrentStats();
+            newStats.setTorrentId(torrentId);
+            newStats.setCompletedCount(1);
+            newStats.setLastUpdated(LocalDateTime.now());
+            statsRepository.save(newStats);
+            return;
+        }
+
+        // 2. 原子操作增加完成次数
+        statsRepository.incrementCompletedCount(torrentId);
+
+        // 3. 更新最后更新时间
+        statsRepository.updateLastUpdatedToNow(torrentId);
+    }
+    /**
+     * 创建新的统计记录
+     */
+    private TorrentStats createNewStats(Long torrentId) {
+        TorrentStats stats = new TorrentStats();
+        stats.setTorrentId(torrentId);
+        stats.setSeederCount(0);
+        stats.setLeecherCount(0);
+        stats.setCompletedCount(0);
+        return statsRepository.save(stats);
+    }
+
+    // 每次客户端上报状态时调用
+    @Transactional
+    public void updateTorrentStats(String infoHash) {
+        // 1. 获取当前种子的peer信息
+        List<PeerInfoEntity> peers = peerInfoRepository.findByInfoHash(infoHash);
+
+        // 2. 统计各类人数
+        int seeders = 0;
+        int leechers = 0;
+        int completed = 0;
+
+        for (PeerInfoEntity peer : peers) {
+            if ("seeding".equals(peer.getStatus()) && peer.isActive()) {
+                seeders++;
+            } else if ("downloading".equals(peer.getStatus()) && peer.isActive()) {
+                leechers++;
+            }
+
+            if ("completed".equals(peer.getStatus())) {
+                completed++;
+            }
+        }
+
+        // 3. 更新统计记录
+        TorrentStats stats = statsRepository.findByTorrentId(
+                torrentMetaRepository.findByInfoHash(infoHash).getId()
+        ).orElse(new TorrentStats());
+
+        stats.setTorrentId(torrentMetaRepository.findByInfoHash(infoHash).getId());
+        stats.setSeederCount(seeders);
+        stats.setLeecherCount(leechers);
+        stats.setCompletedCount(completed);
+        stats.setLastUpdated(LocalDateTime.now());
+
+        statsRepository.save(stats);
+    }
+
+    // 获取种子统计信息
+    public TorrentStats getTorrentStats(Long torrentId) {
+        return statsRepository.findByTorrentId(torrentId)
+                .orElseThrow(() -> new ResourceNotFoundException("Stats not found"));
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pt/service/TrackerService.java b/src/main/java/com/pt/service/TrackerService.java
index 7167ff3..191479b 100644
--- a/src/main/java/com/pt/service/TrackerService.java
+++ b/src/main/java/com/pt/service/TrackerService.java
@@ -1,76 +1,180 @@
 package com.pt.service;
 
+import com.pt.entity.PeerInfoEntity;
 import com.pt.entity.TorrentMeta;
+import com.pt.exception.ResourceNotFoundException;
+import com.pt.repository.PeerInfoRepository;
 import com.pt.repository.TorrentMetaRepository;
 import com.pt.utils.BencodeCodec;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.HashMap;
+import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArrayList;
 
 @Service
 public class TrackerService {
 
-    private final Map<String, List<PeerInfo>> torrentPeers = new ConcurrentHashMap<>();
-
     @Autowired
     private TorrentMetaRepository torrentMetaRepository;
 
+    @Autowired
+    private PeerInfoRepository peerInfoRepository;
+
+    @Autowired
+    private TorrentStatsService statsService;
+
+    @Transactional
     public byte[] handleAnnounce(Map<String, String[]> params, String ipAddress) {
         try {
-            if (!params.containsKey("info_hash") || !params.containsKey("peer_id") || !params.containsKey("port")) {
-                return BencodeCodec.encode(Map.of("failure reason", "Missing required parameters"));
+            // 验证必要参数
+            if (!params.containsKey("info_hash") || !params.containsKey("peer_id")
+                    || !params.containsKey("port")) {
+                return errorResponse("Missing required parameters");
             }
 
+            // 解析参数
             String infoHash = decodeParam(params.get("info_hash")[0]);
-            TorrentMeta meta = torrentMetaRepository.findByInfoHash(infoHash);
-            if (meta == null) {
-                return BencodeCodec.encode(Map.of("failure reason", "Invalid info_hash"));
-            }
-
             String peerId = decodeParam(params.get("peer_id")[0]);
             int port = Integer.parseInt(params.get("port")[0]);
 
-            PeerInfo peer = new PeerInfo(ipAddress, port, peerId);
+            // 获取事件类型
+            String event = getEventParam(params);
 
-            torrentPeers.computeIfAbsent(infoHash, k -> new CopyOnWriteArrayList<>());
-            List<PeerInfo> peers = torrentPeers.get(infoHash);
+            // 获取流量数据
+            long uploaded = getLongParam(params, "uploaded", 0);
+            long downloaded = getLongParam(params, "downloaded", 0);
+            long left = getLongParam(params, "left", 0);
 
-            boolean exists = peers.stream().anyMatch(p -> p.peerId.equals(peerId));
-            if (!exists) {
-                peers.add(peer);
+            // 验证种子是否存在
+            TorrentMeta meta = torrentMetaRepository.findByInfoHash(infoHash);
+            if (meta == null) {
+                return errorResponse("Torrent not found: " + infoHash);
             }
 
-            List<String> ips = peers.stream().map(p -> p.ip).toList();
-            List<Integer> ports = peers.stream().map(p -> p.port).toList();
-            byte[] peerBytes = BencodeCodec.buildCompactPeers(ips, ports);
+            // 创建或更新 peer 信息
+            PeerInfoEntity peer = findOrCreatePeer(peerId, infoHash);
 
+            // 设置 peer 属性
+            setPeerProperties(peer, ipAddress, port, uploaded, downloaded, left);
+
+            // 处理事件类型
+            handlePeerEvent(event, peer, left);
+
+            // 保存 peer
+            peerInfoRepository.save(peer);
+
+            // 更新种子统计信息
+            statsService.updateTorrentStats(infoHash);
+
+            // 获取 peer 列表响应
+            List<PeerInfoEntity> activePeers = peerInfoRepository.findActivePeersByInfoHash(infoHash);
+            byte[] peerBytes = buildPeerResponse(activePeers);
+
+            // 返回成功响应
             return BencodeCodec.buildTrackerResponse(1800, peerBytes);
         } catch (Exception e) {
-            return BencodeCodec.encode(Map.of("failure reason", "Internal server error"));
+            return errorResponse("Internal server error");
         }
     }
 
+    // 辅助方法:获取事件参数
+    private String getEventParam(Map<String, String[]> params) {
+        return params.containsKey("event") ?
+                decodeParam(params.get("event")[0]) : "update";
+    }
+
+    // 辅助方法:获取长整型参数
+    private long getLongParam(Map<String, String[]> params, String key, long defaultValue) {
+        return params.containsKey(key) ?
+                Long.parseLong(params.get(key)[0]) : defaultValue;
+    }
+
+    // 辅助方法:查找或创建 peer
+    private PeerInfoEntity findOrCreatePeer(String peerId, String infoHash) {
+        return peerInfoRepository.findByPeerIdAndInfoHash(peerId, infoHash)
+                .orElseGet(PeerInfoEntity::new);
+    }
+
+    // 辅助方法:设置 peer 属性
+    private void setPeerProperties(PeerInfoEntity peer, String ip, int port,
+                                   long uploaded, long downloaded, long left) {
+        peer.setIp(ip);
+        peer.setPort(port);
+        peer.setPeerId(peer.getPeerId() != null ? peer.getPeerId() : ""); // 防止 NPE
+        peer.setInfoHash(peer.getInfoHash() != null ? peer.getInfoHash() : "");
+        peer.setUploaded(uploaded);
+        peer.setDownloaded(downloaded);
+        peer.setLeft(left);
+        peer.setLastSeen(LocalDateTime.now());
+    }
+
+    // 辅助方法:处理 peer 事件
+    private void handlePeerEvent(String event, PeerInfoEntity peer, long left) {
+        switch (event) {
+            case "started":
+                peer.setStatus("downloading");
+                peer.setActive(true);
+                break;
+
+            case "stopped":
+                peer.setActive(false);
+                break;
+
+            case "completed":
+                handleCompletedEvent(peer);
+                break;
+
+            case "update":
+            default:
+                handleUpdateEvent(peer, left);
+                break;
+        }
+    }
+
+    // 处理完成事件
+    private void handleCompletedEvent(PeerInfoEntity peer) {
+        peer.setStatus("completed");
+        peer.setActive(true);
+        peer.setLeft(0);
+        incrementCompletedCount(peer.getInfoHash());
+    }
+
+    // 处理更新事件
+    private void handleUpdateEvent(PeerInfoEntity peer, long left) {
+        if (left == 0 && "downloading".equals(peer.getStatus())) {
+            // 检测到下载完成
+            peer.setStatus("completed");
+            incrementCompletedCount(peer.getInfoHash());
+        }
+        peer.setActive(true);
+    }
+
+    // 增加完成次数
+    private void incrementCompletedCount(String infoHash) {
+        TorrentMeta meta = torrentMetaRepository.findByInfoHash(infoHash);
+        if (meta != null) {
+            statsService.incrementCompletedCount(meta.getId());
+        }
+    }
+
+    // 构建 peer 响应
+    private byte[] buildPeerResponse(List<PeerInfoEntity> peers) {
+        List<String> ips = peers.stream().map(PeerInfoEntity::getIp).toList();
+        List<Integer> ports = peers.stream().map(PeerInfoEntity::getPort).toList();
+        return BencodeCodec.buildCompactPeers(ips, ports);
+    }
+
+    // 构建错误响应
+    private byte[] errorResponse(String reason) {
+        return BencodeCodec.encode(Map.of("failure reason", reason));
+    }
+
+    // 解码参数
     private String decodeParam(String raw) {
         return new String(raw.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
     }
-
-    private static class PeerInfo {
-        String ip;
-        int port;
-        String peerId;
-
-        public PeerInfo(String ip, int port, String peerId) {
-            this.ip = ip;
-            this.port = port;
-            this.peerId = peerId;
-        }
-    }
-}
-
+}
\ No newline at end of file