冷门种子推荐,冷门种子下载不计入下载量,冷门种子自动化判断

Change-Id: Iba4d232d8408195c5af01d2f1686d171e3d5eeac
diff --git a/src/main/java/com/example/g8backend/G8BackendApplication.java b/src/main/java/com/example/g8backend/G8BackendApplication.java
index e380bb9..2cef205 100644
--- a/src/main/java/com/example/g8backend/G8BackendApplication.java
+++ b/src/main/java/com/example/g8backend/G8BackendApplication.java
@@ -2,8 +2,10 @@
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
 
 @SpringBootApplication()
+@EnableScheduling
 public class G8BackendApplication {
 
     public static void main(String[] args) {
diff --git a/src/main/java/com/example/g8backend/controller/TorrentRecommendationController.java b/src/main/java/com/example/g8backend/controller/TorrentRecommendationController.java
new file mode 100644
index 0000000..ed2896b
--- /dev/null
+++ b/src/main/java/com/example/g8backend/controller/TorrentRecommendationController.java
@@ -0,0 +1,22 @@
+package com.example.g8backend.controller;
+import com.example.g8backend.dto.TorrentRecommendationDTO;
+import com.example.g8backend.dto.ApiResponse;
+import com.example.g8backend.service.ITorrentRecommendationService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/torrent")
+public class TorrentRecommendationController {
+
+    @Autowired
+    private ITorrentRecommendationService recommendationService;
+    @GetMapping("/recommend-cold")
+    public ApiResponse<List<TorrentRecommendationDTO>> recommendColdTorrents(
+            @RequestParam(name = "limit", defaultValue = "10") int limit) {
+        List<TorrentRecommendationDTO> result = recommendationService.getColdTorrentRecommendations(limit);
+        return ApiResponse.success(result);
+    }
+}
diff --git a/src/main/java/com/example/g8backend/dto/TorrentRecommendationDTO.java b/src/main/java/com/example/g8backend/dto/TorrentRecommendationDTO.java
new file mode 100644
index 0000000..bf38a27
--- /dev/null
+++ b/src/main/java/com/example/g8backend/dto/TorrentRecommendationDTO.java
@@ -0,0 +1,19 @@
+// 文件:src/main/java/com/example/g8backend/dto/TorrentRecommendationDTO.java
+package com.example.g8backend.dto;
+
+import lombok.Data;
+import java.time.LocalDateTime;
+
+@Data
+public class TorrentRecommendationDTO {
+    private Long torrentId;             // 种子ID
+    private String torrentName;         // 种子名称
+    private String infoHash;            // info_hash(十六进制字符串)
+    private Double fileSize;            // 文件大小(MB)
+
+    private Double coldnessScore;       // 冷度分 = ageHours / (recentActivityCount + 1)
+    private Long recentActivityCount;   // 最近7天内 peer 活跃数
+    private Long ageHours;              // 种子发布距今小时数
+
+    private LocalDateTime uploadTime;   // 上传时间
+}
diff --git a/src/main/java/com/example/g8backend/entity/Torrent.java b/src/main/java/com/example/g8backend/entity/Torrent.java
index e52df8a..9e83c42 100644
--- a/src/main/java/com/example/g8backend/entity/Torrent.java
+++ b/src/main/java/com/example/g8backend/entity/Torrent.java
@@ -4,9 +4,8 @@
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
-
-import java.sql.Timestamp;
 import lombok.Data;
+import java.time.LocalDateTime;
 
 @Data
 @TableName("torrents")
@@ -18,6 +17,14 @@
     private String filePath;
     private String infoHash;
     private Double fileSize;
+    private Long downloadCount;
+    private Long viewCount;
+
+    @TableField("is_rare")
+    private Boolean isRare;
+
+    @TableField("upload_time")
+    private LocalDateTime uploadTime;
 
     @Override
     public String toString() {
@@ -27,6 +34,8 @@
                 ", torrentName='" + torrentName + '\'' +
                 ", infoHash='" + infoHash + '\'' +
                 ", fileSize=" + fileSize +
+                ", uploadTime=" + uploadTime +
+                ", isRare=" + isRare +
                 '}';
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/example/g8backend/mapper/PeerMapper.java b/src/main/java/com/example/g8backend/mapper/PeerMapper.java
index f0b9b85..bd96863 100644
--- a/src/main/java/com/example/g8backend/mapper/PeerMapper.java
+++ b/src/main/java/com/example/g8backend/mapper/PeerMapper.java
@@ -8,8 +8,10 @@
 
 @Mapper
 public interface PeerMapper extends BaseMapper<Peer> {
-    // get peer by primary key(peerId, infoHash, passkey)
+    List<String> selectAllInfoHashesWithPeers();
+    Long countRecentActivity(String infoHash);
     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);
+    void updatePeer(String passkey, String peerId, String infoHash, double uploaded, double downloaded);
 }
+
diff --git a/src/main/java/com/example/g8backend/mapper/TorrentMapper.java b/src/main/java/com/example/g8backend/mapper/TorrentMapper.java
index 6c3d215..15d592c 100644
--- a/src/main/java/com/example/g8backend/mapper/TorrentMapper.java
+++ b/src/main/java/com/example/g8backend/mapper/TorrentMapper.java
@@ -4,6 +4,8 @@
 import com.example.g8backend.entity.Torrent;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
+import java.util.List;
+
 
 @Mapper
 public interface TorrentMapper extends BaseMapper<Torrent> {
@@ -14,4 +16,8 @@
                        @Param("fileSize") Double fileSize);
     Torrent getTorrentByInfoHash (@Param("infoHash") String infoHash);
     Torrent getTorrentByTorrentId (@Param("torrentId") Long torrentId);
+    List<Torrent> selectByInfoHashList(@Param("infoHashes") List<String> infoHashes);
+    // Mapper接口,MyBatis注解或XML配置均可
+    int updateIsRareByInfoHash(@Param("infoHash") String infoHash, @Param("israre") int israre);
+
 }
diff --git a/src/main/java/com/example/g8backend/scheduler/ColdTorrentRefreshScheduler.java b/src/main/java/com/example/g8backend/scheduler/ColdTorrentRefreshScheduler.java
new file mode 100644
index 0000000..bf88d07
--- /dev/null
+++ b/src/main/java/com/example/g8backend/scheduler/ColdTorrentRefreshScheduler.java
@@ -0,0 +1,32 @@
+package com.example.g8backend.scheduler;
+
+import com.example.g8backend.dto.TorrentRecommendationDTO;
+import com.example.g8backend.service.ITorrentRecommendationService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+public class ColdTorrentRefreshScheduler {
+
+    private final ITorrentRecommendationService recommendationService;
+
+    @Autowired
+    public ColdTorrentRefreshScheduler(ITorrentRecommendationService recommendationService) {
+        this.recommendationService = recommendationService;
+    }
+
+    @Scheduled(fixedRate = 5000)
+    public void refreshColdTorrents() {
+        // 自动刷新数据库中的israre字段,保持前10个冷门标记
+        recommendationService.refreshIsRareField(10);
+
+        // 获取刷新后的冷门数据返回(可选打印日志)
+        List<TorrentRecommendationDTO> coldTorrents = recommendationService.getColdTorrentRecommendations(10);
+        System.out.println("【定时任务】刷新冷门种子列表,条数:" + coldTorrents.size());
+        coldTorrents.forEach(System.out::println);
+    }
+}
+
diff --git a/src/main/java/com/example/g8backend/service/ITorrentRecommendationService.java b/src/main/java/com/example/g8backend/service/ITorrentRecommendationService.java
new file mode 100644
index 0000000..0f0c69b
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/ITorrentRecommendationService.java
@@ -0,0 +1,9 @@
+package com.example.g8backend.service;
+import com.example.g8backend.dto.TorrentRecommendationDTO;
+import java.util.List;
+
+public interface ITorrentRecommendationService {
+
+    List<TorrentRecommendationDTO> getColdTorrentRecommendations(int limit);
+    void refreshIsRareField(int limit);
+}
diff --git a/src/main/java/com/example/g8backend/service/IUserStatsService.java b/src/main/java/com/example/g8backend/service/IUserStatsService.java
index 4771829..bc329b9 100644
--- a/src/main/java/com/example/g8backend/service/IUserStatsService.java
+++ b/src/main/java/com/example/g8backend/service/IUserStatsService.java
@@ -8,5 +8,5 @@
  * 后面弄作弊检测的时候再补充相关的方法。
  */
 public interface IUserStatsService extends IService<UserStats> {
-
+    void increaseDownloadedBytes(Long userId, double downloadedBytes);
 }
diff --git a/src/main/java/com/example/g8backend/service/PeerService.java b/src/main/java/com/example/g8backend/service/PeerService.java
new file mode 100644
index 0000000..5ae8651
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/PeerService.java
@@ -0,0 +1,13 @@
+package com.example.g8backend.service;
+
+import com.example.g8backend.entity.Peer;
+
+import java.util.List;
+
+public interface PeerService {
+    Peer getPeerByPK(String peerId, String infoHash, String passkey);
+
+    List<Peer> getPeerByInfoHashAndPeerId(String infoHash, String peerId);
+
+    void updatePeer(String passkey, String peerId, String infoHash, double uploaded, double downloaded);
+}
diff --git a/src/main/java/com/example/g8backend/service/impl/PeerServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/PeerServiceImpl.java
new file mode 100644
index 0000000..ab24566
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/PeerServiceImpl.java
@@ -0,0 +1,35 @@
+package com.example.g8backend.service.impl;
+
+import com.example.g8backend.entity.Peer;
+import com.example.g8backend.mapper.PeerMapper;
+import com.example.g8backend.service.PeerService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class PeerServiceImpl implements PeerService {
+
+    private final PeerMapper peerMapper;
+
+    @Autowired
+    public PeerServiceImpl(PeerMapper peerMapper) {
+        this.peerMapper = peerMapper;
+    }
+
+    @Override
+    public Peer getPeerByPK(String peerId, String infoHash, String passkey) {
+        return peerMapper.getPeerByPK(peerId, infoHash, passkey);
+    }
+
+    @Override
+    public List<Peer> getPeerByInfoHashAndPeerId(String infoHash, String peerId) {
+        return peerMapper.getPeerByInfoHashAndPeerId(infoHash, peerId);
+    }
+
+    @Override
+    public void updatePeer(String passkey, String peerId, String infoHash, double uploaded, double downloaded) {
+        peerMapper.updatePeer(passkey, peerId, infoHash, uploaded, downloaded);
+    }
+}
diff --git a/src/main/java/com/example/g8backend/service/impl/TorrentRecommendationServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/TorrentRecommendationServiceImpl.java
new file mode 100644
index 0000000..f93c2e4
--- /dev/null
+++ b/src/main/java/com/example/g8backend/service/impl/TorrentRecommendationServiceImpl.java
@@ -0,0 +1,108 @@
+package com.example.g8backend.service.impl;
+
+import com.example.g8backend.dto.TorrentRecommendationDTO;
+import com.example.g8backend.entity.Torrent;
+import com.example.g8backend.mapper.PeerMapper;
+import com.example.g8backend.mapper.TorrentMapper;
+import com.example.g8backend.service.ITorrentRecommendationService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+@Service
+public class TorrentRecommendationServiceImpl implements ITorrentRecommendationService {
+
+    private final PeerMapper peerMapper;
+    private final TorrentMapper torrentMapper;
+
+    @Autowired
+    public TorrentRecommendationServiceImpl(PeerMapper peerMapper, TorrentMapper torrentMapper) {
+        this.peerMapper = peerMapper;
+        this.torrentMapper = torrentMapper;
+    }
+
+    /**
+     * 获取冷门种子推荐列表,冷门定义为“种子存在时间越长,最近7天内peer活跃数越少”
+     *
+     * @param limit 返回结果条数限制
+     * @return 冷门种子推荐DTO列表
+     */
+    @Override
+    @Transactional(readOnly = true)
+    public List<TorrentRecommendationDTO> getColdTorrentRecommendations(int limit) {
+        // 1. 查询所有有peer活动的种子info_hash列表
+        List<String> infoHashes = peerMapper.selectAllInfoHashesWithPeers();
+        if (infoHashes == null || infoHashes.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        // 2. 根据info_hash列表查询对应的种子详细信息
+        List<Torrent> allTorrents = torrentMapper.selectByInfoHashList(infoHashes);
+
+        LocalDateTime now = LocalDateTime.now();
+        List<TorrentRecommendationDTO> dtos = new ArrayList<>();
+
+        // 3. 计算每个种子的冷度指标
+        for (Torrent torrent : allTorrents) {
+            LocalDateTime uploadTime = torrent.getUploadTime();
+            if (uploadTime == null) {
+                // 如果没有上传时间,跳过
+                continue;
+            }
+
+            // 计算种子发布距今的小时数
+            long ageHours = Duration.between(uploadTime, now).toHours();
+
+            // 查询最近7天内该infoHash的peer活跃数
+            Long recentActivityCount = peerMapper.countRecentActivity(torrent.getInfoHash());
+            if (recentActivityCount == null) {
+                recentActivityCount = 0L;
+            }
+
+            // 计算冷度分,防止除0,加1
+            double coldnessScore = (double) ageHours / (recentActivityCount + 1);
+
+            // 组装DTO
+            TorrentRecommendationDTO dto = new TorrentRecommendationDTO();
+            dto.setTorrentId(torrent.getTorrentId());
+            dto.setTorrentName(torrent.getTorrentName());
+            dto.setInfoHash(torrent.getInfoHash());
+            dto.setFileSize(torrent.getFileSize());
+            dto.setUploadTime(torrent.getUploadTime());
+            dto.setAgeHours(ageHours);
+            dto.setRecentActivityCount(recentActivityCount);
+            dto.setColdnessScore(coldnessScore);
+
+            dtos.add(dto);
+        }
+
+        // 4. 按冷度分倒序排序,取前limit条
+        dtos.sort(Comparator.comparingDouble(TorrentRecommendationDTO::getColdnessScore).reversed());
+        if (dtos.size() > limit) {
+            return dtos.subList(0, limit);
+        }
+        return dtos;
+    }
+
+    @Override
+    @Transactional
+    public void refreshIsRareField(int limit) {
+        // 1. 获取所有种子的冷门推荐(冷门度排序)
+        List<TorrentRecommendationDTO> coldTorrents = getColdTorrentRecommendations(Integer.MAX_VALUE);
+
+        // 2. 前limit条设为 israre = 1,其他设为0
+        for (int i = 0; i < coldTorrents.size(); i++) {
+            TorrentRecommendationDTO dto = coldTorrents.get(i);
+            int israre = (i < limit) ? 1 : 0;
+
+            // 直接调用mapper更新对应torrent的israre字段
+            torrentMapper.updateIsRareByInfoHash(dto.getInfoHash(), israre);
+        }
+    }
+}
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 43855a4..d540528 100644
--- a/src/main/java/com/example/g8backend/service/impl/TorrentServiceImpl.java
+++ b/src/main/java/com/example/g8backend/service/impl/TorrentServiceImpl.java
@@ -4,8 +4,11 @@
 import com.example.g8backend.entity.Torrent;
 import com.example.g8backend.mapper.TorrentMapper;
 import com.example.g8backend.service.ITorrentService;
+import com.example.g8backend.service.IUserStatsService;
 import com.example.g8backend.util.TorrentUtil;
 import jakarta.annotation.Resource;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.stereotype.Service;
 
 import java.io.File;
@@ -13,29 +16,31 @@
 import java.io.IOException;
 
 @Service
-public class TorrentServiceImpl  extends ServiceImpl<TorrentMapper, Torrent> implements ITorrentService {
+public class TorrentServiceImpl extends ServiceImpl<TorrentMapper, Torrent> implements ITorrentService {
+
     @Resource
     private TorrentMapper torrentMapper;
 
-    String tracker = "http://127.0.0.1:8080/tracker/announce/";
+    @Resource
+    private IUserStatsService userStatsService;
+
+    private final String tracker = "http://127.0.0.1:8080/tracker/announce/";
 
     @Override
-    public Torrent handleTorrentUpload(File file, String fileName, Long userId, String passkey) throws IOException, IllegalArgumentException {
-        // 修改 announce 字段
+    public Torrent handleTorrentUpload(File file, String fileName, Long userId, String passkey) throws IOException {
+        // 注入 tracker
         byte[] modifiedBytes = TorrentUtil.injectTracker(file, tracker + passkey);
 
-        // 计算 info_hash
+        // 获取 infoHash
         String infoHash = TorrentUtil.getInfoHash(file);
 
-        // 文件大小(以MB为单位)
+        // 计算大小(MB)
         double fileSize = file.length() / 1024.0 / 1024.0;
 
-        // 保存新的种子文件(可选)
+        // 保存修改后的文件
         File outputDir = new File("uploaded-torrents");
-        if (!outputDir.exists()) {
-            if (!outputDir.mkdirs()){
-                throw new IOException("Failed to create directory: " + outputDir.getAbsolutePath());
-            }
+        if (!outputDir.exists() && !outputDir.mkdirs()) {
+            throw new IOException("Failed to create directory: " + outputDir.getAbsolutePath());
         }
 
         File savedFile = new File(outputDir, file.getName());
@@ -44,36 +49,48 @@
         }
 
         // 插入数据库
-        torrentMapper.insertTorrent(userId, fileName, file.getName(), infoHash, fileSize);
+        Torrent newTorrent = new Torrent();
+        newTorrent.setUserId(userId);
+        newTorrent.setTorrentName(fileName);
+        newTorrent.setFilePath(file.getName());
+        newTorrent.setInfoHash(infoHash);
+        newTorrent.setFileSize(fileSize);
+        newTorrent.setIsRare(false);  // 默认不是冷门
+        torrentMapper.insert(newTorrent);
 
-        // 构建返回实体
-        Torrent torrent = new Torrent();
-        torrent.setUserId(userId);
-        torrent.setTorrentName(file.getName());
-        torrent.setInfoHash(infoHash);
-        torrent.setFileSize(fileSize);
-        return torrent;
+        return newTorrent;
     }
 
     @Override
     public File handleTorrentDownload(Torrent torrent, String passkey) throws IOException {
+        // 原始种子文件读取
         File torrentFile = new File("uploaded-torrents/" + torrent.getFilePath());
         byte[] modifiedBytes = TorrentUtil.injectTracker(torrentFile, tracker + passkey);
 
+        // 写入到临时文件
         File tempFile = File.createTempFile("user_torrent_", ".torrent");
         try (FileOutputStream fos = new FileOutputStream(tempFile)) {
             fos.write(modifiedBytes);
         }
+
+        // 下载统计判断
+        if (Boolean.FALSE.equals(torrent.getIsRare())) {
+            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+            if (auth != null && auth.getPrincipal() instanceof Long currentUserId) {
+                userStatsService.increaseDownloadedBytes(currentUserId, torrent.getFileSize());
+            }
+        }
+
         return tempFile;
     }
 
     @Override
-    public Torrent findByInfoHash(String infoHash){
+    public Torrent findByInfoHash(String infoHash) {
         return torrentMapper.getTorrentByInfoHash(infoHash);
     }
 
     @Override
-    public Torrent findByTorrentId(Long torrentId){
+    public Torrent findByTorrentId(Long torrentId) {
         return torrentMapper.getTorrentByTorrentId(torrentId);
     }
 }
diff --git a/src/main/java/com/example/g8backend/service/impl/UserStatsServiceImpl.java b/src/main/java/com/example/g8backend/service/impl/UserStatsServiceImpl.java
index 9110d16..5854a6c 100644
--- a/src/main/java/com/example/g8backend/service/impl/UserStatsServiceImpl.java
+++ b/src/main/java/com/example/g8backend/service/impl/UserStatsServiceImpl.java
@@ -6,6 +6,27 @@
 import com.example.g8backend.service.IUserStatsService;
 import org.springframework.stereotype.Service;
 
+import java.time.LocalDateTime;
+
 @Service
 public class UserStatsServiceImpl extends ServiceImpl<UserStatsMapper, UserStats> implements IUserStatsService {
+    @Override
+    public void increaseDownloadedBytes(Long userId, double downloadedBytes) {
+        UserStats userStats = this.getById(userId);
+        if (userStats != null) {
+            userStats.setTotal_download(userStats.getTotal_download() + downloadedBytes);
+            userStats.setLast_update_time(LocalDateTime.now());
+            this.updateById(userStats);
+        } else {
+            // 如果记录不存在,可选:创建新记录(视项目逻辑而定)
+            UserStats newStats = new UserStats();
+            newStats.setUserId(userId);
+            newStats.setTotal_download(downloadedBytes);
+            newStats.setTotal_upload(0.0);
+            newStats.setLast_update_time(LocalDateTime.now());
+            this.save(newStats);
+        }
+    }
+
+
 }
diff --git a/src/main/resources/mapper/PeerMapper.xml b/src/main/resources/mapper/PeerMapper.xml
index b3d4780..4c90eae 100644
--- a/src/main/resources/mapper/PeerMapper.xml
+++ b/src/main/resources/mapper/PeerMapper.xml
@@ -10,6 +10,22 @@
         SELECT * FROM peers
         WHERE info_hash = #{infoHash} and peer_id = #{peerId}
     </select>
+    <select id="selectAllInfoHashesWithPeers" resultType="String">
+        SELECT DISTINCT info_hash FROM peers
+    </select>
+
+    <select id="countRecentActivity" resultType="Long" parameterType="String">
+        SELECT COUNT(*) FROM peers
+        WHERE info_hash = #{infoHash}
+        AND last_activity_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
+    </select>
+    <select id="selectByInfoHashList" parameterType="list" resultType="com.example.g8backend.entity.Torrent">
+        SELECT * FROM torrents
+        WHERE info_hash IN
+        <foreach item="item" index="index" collection="infoHashes" open="(" separator="," close=")">
+            #{item}
+        </foreach>
+    </select>
     <update id="updatePeer">
         UPDATE peers
         SET uploaded = #{uploaded}, downloaded = #{downloaded}
diff --git a/src/main/resources/mapper/TorrentMapper.xml b/src/main/resources/mapper/TorrentMapper.xml
index 243fd92..cec75eb 100644
--- a/src/main/resources/mapper/TorrentMapper.xml
+++ b/src/main/resources/mapper/TorrentMapper.xml
@@ -31,4 +31,26 @@
         FROM torrents
         WHERE torrent_id = #{torrentId}
     </select>
+
+    <select id="selectByInfoHashList" resultType="com.example.g8backend.entity.Torrent" parameterType="list">
+        SELECT
+        torrent_id,
+        user_id,
+        torrent_name,
+        file_path,
+        HEX(info_hash) AS infoHash,
+        file_size
+        FROM torrents
+        WHERE info_hash IN
+        <foreach collection="infoHashes" item="hash" open="(" separator="," close=")">
+            UNHEX(#{hash})
+        </foreach>
+    </select>
+
+    <update id="updateIsRareByInfoHash">
+        UPDATE torrent
+        SET israre = #{israre}
+        WHERE info_hash = #{infoHash}
+    </update>
+
 </mapper>
\ No newline at end of file
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
index 8a2387c..0ace8eb 100644
--- a/src/main/resources/schema.sql
+++ b/src/main/resources/schema.sql
@@ -33,6 +33,8 @@
   `file_path` VARCHAR(255) NOT NULL,
   `info_hash` BINARY(20) NOT NULL,
   `file_size` FLOAT NOT NULL,
+  `upload_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '种子上传时间(第一次插入时自动填充)',
+  `is_rare` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否为冷门种子',
   FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`)
 );
 -- Peer表(保持不变)
@@ -163,7 +165,7 @@
     `post_id` INT NOT NULL COMMENT '被举报的帖子ID',
     `user_id` INT NOT NULL COMMENT '举报人ID',
     `reason` TEXT NOT NULL COMMENT '举报原因',
-    `status` ENUM('pending', 'resolved', 'rejected') DEFAULT 'pending' COMMENT '处理状态(待处理/已解决/已驳回)',
+        `status` ENUM('pending', 'resolved', 'rejected') DEFAULT 'pending' COMMENT '处理状态(待处理/已解决/已驳回)',
     `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '举报时间',
     `resolved_by` INT DEFAULT NULL COMMENT '处理人ID(管理员)',
     `resolved_at` TIMESTAMP DEFAULT NULL COMMENT '处理时间',
diff --git a/src/test/java/com/example/g8backend/service/TorrentRecommendationServiceImplTest.java b/src/test/java/com/example/g8backend/service/TorrentRecommendationServiceImplTest.java
new file mode 100644
index 0000000..c2dd84c
--- /dev/null
+++ b/src/test/java/com/example/g8backend/service/TorrentRecommendationServiceImplTest.java
@@ -0,0 +1,114 @@
+package com.example.g8backend.service;
+
+import com.example.g8backend.dto.TorrentRecommendationDTO;
+import com.example.g8backend.entity.Torrent;
+import com.example.g8backend.mapper.PeerMapper;
+import com.example.g8backend.mapper.TorrentMapper;
+import com.example.g8backend.service.impl.TorrentRecommendationServiceImpl;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class TorrentRecommendationServiceImplTest {
+
+    @Mock
+    private PeerMapper peerMapper;
+
+    @Mock
+    private TorrentMapper torrentMapper;
+
+    @InjectMocks
+    private TorrentRecommendationServiceImpl service;
+
+    @Test
+    void testGetColdTorrentRecommendations_withValidData() {
+        List<String> infoHashes = Arrays.asList("hash1", "hash2");
+        when(peerMapper.selectAllInfoHashesWithPeers()).thenReturn(infoHashes);
+
+        LocalDateTime now = LocalDateTime.now();
+
+        Torrent torrent1 = new Torrent();
+        torrent1.setTorrentId(1L);
+        torrent1.setTorrentName("Torrent1");
+        torrent1.setInfoHash("hash1");
+        torrent1.setFileSize(100.0);
+        torrent1.setUploadTime(now.minusHours(48));
+
+        Torrent torrent2 = new Torrent();
+        torrent2.setTorrentId(2L);
+        torrent2.setTorrentName("Torrent2");
+        torrent2.setInfoHash("hash2");
+        torrent2.setFileSize(200.0);
+        torrent2.setUploadTime(now.minusHours(72));
+
+        when(torrentMapper.selectByInfoHashList(infoHashes))
+                .thenReturn(Arrays.asList(torrent1, torrent2));
+        when(peerMapper.countRecentActivity("hash1")).thenReturn(4L);
+        when(peerMapper.countRecentActivity("hash2")).thenReturn(2L);
+
+        List<TorrentRecommendationDTO> recommendations = service.getColdTorrentRecommendations(2);
+
+        assertNotNull(recommendations);
+        assertEquals(2, recommendations.size());
+        // 根据冷度得分,torrent2 排第一
+        assertEquals("Torrent2", recommendations.get(0).getTorrentName());
+        assertEquals("Torrent1", recommendations.get(1).getTorrentName());
+
+        assertEquals(72.0 / 3, recommendations.get(0).getColdnessScore(), 0.01);
+        assertEquals(48.0 / 5, recommendations.get(1).getColdnessScore(), 0.01);
+
+        verify(peerMapper).selectAllInfoHashesWithPeers();
+        verify(torrentMapper).selectByInfoHashList(infoHashes);
+        verify(peerMapper).countRecentActivity("hash1");
+        verify(peerMapper).countRecentActivity("hash2");
+    }
+
+    @Test
+    void testGetColdTorrentRecommendations_noPeers() {
+        when(peerMapper.selectAllInfoHashesWithPeers()).thenReturn(Collections.emptyList());
+
+        List<TorrentRecommendationDTO> recommendations = service.getColdTorrentRecommendations(5);
+
+        assertNotNull(recommendations);
+        assertTrue(recommendations.isEmpty());
+
+        verify(peerMapper).selectAllInfoHashesWithPeers();
+        verifyNoInteractions(torrentMapper);
+    }
+
+    @Test
+    void testGetColdTorrentRecommendations_uploadTimeNull_skips() {
+        when(peerMapper.selectAllInfoHashesWithPeers()).thenReturn(Collections.singletonList("hash1"));
+
+        Torrent torrent = new Torrent();
+        torrent.setTorrentId(1L);
+        torrent.setTorrentName("Torrent1");
+        torrent.setInfoHash("hash1");
+        torrent.setUploadTime(null);
+
+        when(torrentMapper.selectByInfoHashList(anyList()))
+                .thenReturn(Collections.singletonList(torrent));
+
+        List<TorrentRecommendationDTO> recommendations = service.getColdTorrentRecommendations(10);
+
+        assertNotNull(recommendations);
+        assertTrue(recommendations.isEmpty());
+
+        verify(peerMapper).selectAllInfoHashesWithPeers();
+        verify(torrentMapper).selectByInfoHashList(anyList());
+        verify(peerMapper, never()).countRecentActivity(anyString());
+    }
+
+}
diff --git a/src/test/java/com/example/g8backend/service/TorrentServiceTest.java b/src/test/java/com/example/g8backend/service/TorrentServiceTest.java
index fd6d88a..4529881 100644
--- a/src/test/java/com/example/g8backend/service/TorrentServiceTest.java
+++ b/src/test/java/com/example/g8backend/service/TorrentServiceTest.java
@@ -10,10 +10,7 @@
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.api.io.TempDir;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.MockedStatic;
-import org.mockito.Mockito;
+import org.mockito.*;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 import java.io.File;
@@ -70,32 +67,34 @@
 
     @Test
     public void testHandleTorrentUpload() throws Exception {
-        Map<String, Object> info = Map.of( "name", "test.txt", "length", 1024);
+        Map<String, Object> info = Map.of("name", "test.txt", "length", 1024);
         File torrentFile = createTestTorrentFile(info);
         Long userId = 1L;
         String passkey = "123456";
         byte[] mockedBytes = "modified".getBytes();
         String expectedInfoHash = "modified-info-hash";
 
-        // 注入 Mock 对象(静态方法)
-        try (MockedStatic<TorrentUtil> mockedStatic = Mockito.mockStatic(TorrentUtil.class)){
+        try (MockedStatic<TorrentUtil> mockedStatic = Mockito.mockStatic(TorrentUtil.class)) {
             mockedStatic.when(() -> TorrentUtil.injectTracker(any(File.class), any(String.class)))
                     .thenReturn(mockedBytes);
             mockedStatic.when(() -> TorrentUtil.getInfoHash(any(File.class)))
                     .thenReturn(expectedInfoHash);
-            Torrent torrent = torrentService.handleTorrentUpload(torrentFile,"test.torrent", userId, passkey);
 
-            // 验证调用
-            verify(torrentMapper, times(1))
-                    .insertTorrent(eq(1L),
-                            anyString(),
-                            anyString(),
-                            eq("modified-info-hash"),
-                            eq(torrentFile.length()/1024.0/1024.0));
+            Torrent torrent = torrentService.handleTorrentUpload(torrentFile, "test.torrent", userId, passkey);
+
+            // 使用 ArgumentCaptor 捕获 insert 方法的参数
+            ArgumentCaptor<Torrent> torrentCaptor = ArgumentCaptor.forClass(Torrent.class);
+            verify(torrentMapper, times(1)).insert(torrentCaptor.capture());
+            Torrent captured = torrentCaptor.getValue();
+
+            assertEquals(userId, captured.getUserId());
+            assertEquals("test.torrent", captured.getTorrentName());
+            assertEquals(expectedInfoHash, captured.getInfoHash());
+            assertEquals(torrentFile.length() / 1024.0 / 1024.0, captured.getFileSize());
 
             assertEquals(expectedInfoHash, torrent.getInfoHash());
             assertEquals(userId, torrent.getUserId());
-            assertEquals(torrentFile.length()/1024.0/1024.0, torrent.getFileSize());
+            assertEquals(torrentFile.length() / 1024.0 / 1024.0, torrent.getFileSize());
         } finally {
             if (!torrentFile.delete()) {
                 System.err.println("Failed to delete temporary file: " + torrentFile.getAbsolutePath());
@@ -103,6 +102,7 @@
         }
     }
 
+
     @Test
     public void testHandleTorrentDownload() throws Exception {
         Torrent torrent = new Torrent();