冷门种子推荐,冷门种子下载不计入下载量,冷门种子自动化判断
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();