种子,促销
Change-Id: I0ce919ce4228dcefec26ef636bacd3298c0dc77a
diff --git a/src/main/java/com/example/myproject/service/PromotionService.java b/src/main/java/com/example/myproject/service/PromotionService.java
new file mode 100644
index 0000000..087c13b
--- /dev/null
+++ b/src/main/java/com/example/myproject/service/PromotionService.java
@@ -0,0 +1,17 @@
+package com.example.myproject.service;
+
+import com.example.myproject.entity.Promotion;
+import com.example.myproject.dto.PromotionCreateDTO;
+import java.util.List;
+
+public interface PromotionService {
+ Promotion createPromotion(PromotionCreateDTO promotionDTO);
+
+ List<Promotion> getAllActivePromotions();
+
+ Promotion getPromotionById(Long promotionId);
+
+ void deletePromotion(Long promotionId);
+
+ double getCurrentDiscount(Long torrentId);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/myproject/service/TorrentService.java b/src/main/java/com/example/myproject/service/TorrentService.java
new file mode 100644
index 0000000..941804d
--- /dev/null
+++ b/src/main/java/com/example/myproject/service/TorrentService.java
@@ -0,0 +1,37 @@
+package com.example.myproject.service;
+
+import com.example.myproject.common.base.Result;
+import com.example.myproject.entity.TorrentEntity;
+import com.example.myproject.dto.param.TorrentParam;
+import com.example.myproject.dto.param.TorrentUploadParam;
+import com.example.myproject.dto.TorrentUpdateDTO;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.util.List;
+
+public interface TorrentService {
+ List<TorrentEntity> search(TorrentParam param);
+
+ TorrentEntity selectBySeedId(Long seedId);
+
+ void uploadTorrent(MultipartFile file, TorrentUploadParam param) throws IOException;
+
+ byte[] fetch(Long seedId, String passkey) throws IOException;
+
+ Result favorite(Long seedId, Long userId);
+
+ void deleteTorrent(Long seedId);
+
+ void updateTorrent(Long seedId, TorrentUpdateDTO updateDTO);
+
+ boolean canUserDeleteTorrent(Long seedId, Long userId);
+
+ boolean canUserUpdateTorrent(Long seedId, Long userId);
+
+ boolean checkUserUploadRatio(Long userId);
+
+ double calculateDownloadSize(Long torrentId, Long userId);
+
+ void recordDownload(Long torrentId, Long userId, double downloadSize);
+}
diff --git a/src/main/java/com/example/myproject/service/UserService.java b/src/main/java/com/example/myproject/service/UserService.java
index 535f635..71435c7 100644
--- a/src/main/java/com/example/myproject/service/UserService.java
+++ b/src/main/java/com/example/myproject/service/UserService.java
@@ -21,4 +21,5 @@
boolean checkPassword(Long userId, String rawPassword);
+// Integer getUserId();
}
\ No newline at end of file
diff --git a/src/main/java/com/example/myproject/service/serviceImpl/PromotionServiceImpl.java b/src/main/java/com/example/myproject/service/serviceImpl/PromotionServiceImpl.java
new file mode 100644
index 0000000..9d34cbc
--- /dev/null
+++ b/src/main/java/com/example/myproject/service/serviceImpl/PromotionServiceImpl.java
@@ -0,0 +1,114 @@
+package com.example.myproject.service.serviceImpl;
+
+import com.example.myproject.entity.Promotion;
+import com.example.myproject.mapper.PromotionMapper;
+import com.example.myproject.service.PromotionService;
+import com.example.myproject.dto.PromotionCreateDTO;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+public class PromotionServiceImpl implements PromotionService {
+
+ @Autowired
+ private PromotionMapper promotionMapper;
+
+ @Override
+ @Transactional
+ public Promotion createPromotion(PromotionCreateDTO promotionDTO) {
+ // 验证时间
+ LocalDateTime now = LocalDateTime.now();
+ if (promotionDTO.getEndTime().isBefore(now)) {
+ throw new RuntimeException("结束时间不能早于当前时间");
+ }
+
+ // 验证种子ID是否存在
+ validateTorrentIds(promotionDTO.getApplicableTorrentIds());
+
+ // 创建促销活动
+ Promotion promotion = new Promotion();
+ promotion.setName(promotionDTO.getName());
+ promotion.setDescription(promotionDTO.getDescription());
+ promotion.setStartTime(promotionDTO.getStartTime());
+ promotion.setEndTime(promotionDTO.getEndTime());
+ promotion.setDiscountPercentage(promotionDTO.getDiscountPercentage());
+
+ // 把List<Long>转换成逗号分隔字符串
+ String applicableTorrentIdsStr = promotionDTO.getApplicableTorrentIds().stream()
+ .map(String::valueOf)
+ .collect(Collectors.joining(","));
+ promotion.setApplicableTorrentIds(applicableTorrentIdsStr);
+
+ promotion.setCreateTime(now);
+ promotion.setUpdateTime(now);
+ promotion.setIsDeleted(false);
+
+ promotionMapper.insert(promotion);
+ return promotion;
+ }
+
+ @Override
+ public List<Promotion> getAllActivePromotions() {
+ LocalDateTime now = LocalDateTime.now();
+ return promotionMapper.findActivePromotions(now);
+ }
+
+ @Override
+ public Promotion getPromotionById(Long promotionId) {
+ Promotion promotion = promotionMapper.selectById(promotionId);
+ if (promotion == null || promotion.getIsDeleted()) {
+ return null;
+ }
+ return promotion;
+ }
+
+ @Override
+ @Transactional
+ public void deletePromotion(Long promotionId) {
+ Promotion promotion = getPromotionById(promotionId);
+ if (promotion == null) {
+ throw new RuntimeException("促销活动不存在");
+ }
+
+ // 软删除
+ promotion.setIsDeleted(true);
+ promotion.setUpdateTime(LocalDateTime.now());
+ promotionMapper.updateById(promotion);
+ }
+
+ @Override
+ public double getCurrentDiscount(Long torrentId) {
+ LocalDateTime now = LocalDateTime.now();
+ List<Promotion> activePromotions = promotionMapper.findActivePromotionsForTorrent(torrentId, now);
+
+ // 如果有多个促销活动,取折扣最大的
+ return activePromotions.stream()
+ .mapToDouble(Promotion::getDiscountPercentage)
+ .max()
+ .orElse(0.0);
+ }
+
+ /**
+ * 验证种子ID是否存在
+ */
+ private void validateTorrentIds(List<Long> torrentIds) {
+ if (torrentIds == null || torrentIds.isEmpty()) {
+ throw new RuntimeException("适用种子列表不能为空");
+ }
+
+ // 检查所有种子ID是否都存在
+ List<Long> invalidIds = torrentIds.stream()
+ .filter(id -> promotionMapper.checkTorrentExists(id) == 0) // 改成 == 0
+ .collect(Collectors.toList());
+
+ if (!invalidIds.isEmpty()) {
+ throw new RuntimeException("以下种子ID不存在: " + invalidIds);
+ }
+ }
+}
diff --git a/src/main/java/com/example/myproject/service/serviceImpl/TorrentServiceImpl.java b/src/main/java/com/example/myproject/service/serviceImpl/TorrentServiceImpl.java
new file mode 100644
index 0000000..884c65d
--- /dev/null
+++ b/src/main/java/com/example/myproject/service/serviceImpl/TorrentServiceImpl.java
@@ -0,0 +1,296 @@
+package com.example.myproject.service.serviceImpl;
+
+import com.example.myproject.entity.TorrentEntity;
+import com.example.myproject.entity.User;
+import com.example.myproject.mapper.TorrentMapper;
+import com.example.myproject.mapper.UserMapper;
+import com.example.myproject.service.TorrentService;
+import com.example.myproject.service.PromotionService;
+import com.example.myproject.dto.param.TorrentParam;
+import com.example.myproject.dto.param.TorrentUploadParam;
+import com.example.myproject.dto.TorrentUpdateDTO;
+import com.turn.ttorrent.bcodec.BDecoder;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import com.turn.ttorrent.client.SimpleClient;
+import com.turn.ttorrent.common.creation.MetadataBuilder;
+import com.turn.ttorrent.tracker.Tracker;
+import com.turn.ttorrent.tracker.TrackedTorrent;
+import com.example.myproject.common.base.Result;
+
+import com.turn.ttorrent.tracker.TrackedTorrent;
+import org.apache.commons.codec.binary.Hex;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.time.LocalTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+@Service
+public class TorrentServiceImpl implements TorrentService {
+ @Autowired
+ private Tracker tracker;
+
+ @Autowired
+ private TorrentMapper torrentMapper;
+
+ private final Map<String, TrackedTorrent> torrentRegistry = new HashMap<>();
+
+
+ @Autowired
+ private UserMapper userMapper;
+
+ @Autowired
+ private PromotionService promotionService;
+
+ private static final double MIN_UPLOAD_RATIO = 0.5; // 最小上传比例要求
+
+ @Override
+ public List<TorrentEntity> search(TorrentParam param) {
+ return torrentMapper.search(param);
+ }
+
+ @Override
+ public TorrentEntity selectBySeedId(Long seedId) {
+ return torrentMapper.selectById(seedId);
+ }
+ private final ExecutorService seederExecutor = Executors.newCachedThreadPool();
+
+ @Override
+ @Transactional
+ public void uploadTorrent(MultipartFile file, TorrentUploadParam param) throws IOException {
+ // 验证用户权限
+ User user = userMapper.selectById(param.getUploader());
+ if (user == null) {
+ throw new RuntimeException("用户不存在");
+ }
+ String workingDir = System.getProperty("user.dir");
+ Path originalDir = Paths.get(workingDir, "data", "files");
+ Files.createDirectories(originalDir);
+ Path originalFilePath = originalDir.resolve(file.getOriginalFilename());
+ Files.copy(file.getInputStream(), originalFilePath, StandardCopyOption.REPLACE_EXISTING);
+
+ MetadataBuilder builder = new MetadataBuilder()
+ .addFile(originalFilePath.toFile(), file.getOriginalFilename()) // 添加原始文件
+ .setTracker(" ") // 设置Tracker地址
+ .setPieceLength(512 * 1024) // 分片大小512KB
+ .setComment("Generated by PT站")
+ .setCreatedBy("PT-Server");
+
+ // 处理种子文件
+ byte[] torrentBytes = file.getBytes();
+ String infoHash = null;
+ try {
+ infoHash = calculateInfoHash(torrentBytes);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+
+ // 保存种子文件到data/torrents目录
+ Path torrentDir = Paths.get(workingDir, "data", "torrents");
+ Files.createDirectories(torrentDir);
+ Path torrentPath = torrentDir.resolve(infoHash + ".torrent");
+ Files.copy(new ByteArrayInputStream(torrentBytes), torrentPath, StandardCopyOption.REPLACE_EXISTING);
+
+ // 注册到Tracker
+ TrackedTorrent torrent = TrackedTorrent.load(torrentPath.toFile());
+ tracker.announce(torrent);
+
+
+ // 异步启动做种客户端
+ seederExecutor.submit(() -> {
+ try {
+ startSeeding(torrentPath, originalDir);
+ } catch (Exception e) {
+ Result.error("做种失败: " + e.getMessage());
+ }
+ });
+
+
+
+
+ // 保存种子信息
+ TorrentEntity entity= new TorrentEntity();
+ entity.setUploader(param.getUploader());
+ entity.setFileName(file.getOriginalFilename());
+ entity.setSize(file.getSize());
+ entity.setCategory(param.getCategory());
+ entity.setTags(param.getTags());
+ entity.setTitle(param.getTitle());
+ entity.setImageUrl(param.getImageUrl());
+ entity.setTorrentFile(torrentBytes);
+ entity.setInfoHash(infoHash);
+
+ torrentMapper.insert(entity);
+ }
+
+ @Override
+ public byte[] fetch(Long seedId, String passkey) {
+ TorrentEntity torrent = selectBySeedId(seedId);
+ if (torrent == null) {
+ throw new RuntimeException("种子不存在");
+ }
+
+ byte[] torrentBytes = torrent.getTorrentFile();
+
+ try {
+ // 1. 解码 .torrent 文件为 Map
+ Map<String, BEValue> decoded = BDecoder.bdecode(new ByteArrayInputStream(torrentBytes)).getMap();
+
+ // 2. 获取原始 announce 字段
+ String announce = decoded.get("announce").getString();
+
+ // 3. 注入 passkey 到 announce URL
+ if (!announce.contains("passkey=")) {
+ announce = announce + "?passkey=" + passkey;
+ decoded.put("announce", new BEValue(announce));
+ }
+
+ // 4. 编码成新的 .torrent 文件字节数组
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ BEncoder.bencode(decoded, out);
+ return out.toByteArray();
+
+ } catch (Exception e) {
+ throw new RuntimeException("处理 torrent 文件失败", e);
+ }
+ }
+
+ @Override
+ @Transactional
+ public Result favorite(Long seedId, Long userId) {
+ try {
+ boolean exists = torrentMapper.checkFavorite(seedId, userId);
+ if (exists) {
+ torrentMapper.removeFavorite(seedId, userId);
+ return Result.success("取消收藏成功");
+ } else {
+ torrentMapper.addFavorite(seedId, userId);
+ return Result.success("收藏成功");
+ }
+ } catch (Exception e) {
+ return Result.error("失败: " + e.getMessage());
+ }
+ }
+
+ @Override
+ @Transactional
+ public void deleteTorrent(Long seedId) {
+ torrentMapper.deleteById(seedId);
+ }
+
+ @Override
+ @Transactional
+ public void updateTorrent(Long seedId, TorrentUpdateDTO updateDTO) {
+ TorrentEntity torrent = selectBySeedId(seedId);
+ if (torrent == null) {
+ throw new RuntimeException("种子不存在");
+ }
+
+
+ torrent.setDescription(updateDTO.getDescription());
+ torrent.setCategory(updateDTO.getCategory());
+ torrent.setTitle(updateDTO.getTitle());
+ torrent.setTags(updateDTO.getTags());
+ torrent.setImageUrl(updateDTO.getImageUrl());
+
+ torrentMapper.updateById(torrent);
+ }
+
+ @Override
+ public boolean canUserDeleteTorrent(Long seedId, Long userId) {
+ TorrentEntity torrent = selectBySeedId(seedId);
+ if (torrent == null) {
+ return false;
+ }
+
+ // 检查是否是种子发布者或管理员
+ return torrent.getUploader().equals(userId) ||
+ userMapper.hasRole(userId, "admin");
+ }
+
+ @Override
+ public boolean canUserUpdateTorrent(Long seedId, Long userId) {
+ return canUserDeleteTorrent(seedId, userId);
+ }
+
+ @Override
+ public boolean checkUserUploadRatio(Long userId) {
+ User user = userMapper.selectById(userId);
+ if (user == null) {
+ return false;
+ }
+
+ // 防止除以零
+ if (user.getDownloaded() == 0) {
+ return true;
+ }
+
+ double uploadRatio = user.getUploaded() / (double) user.getDownloaded();
+ return uploadRatio >= MIN_UPLOAD_RATIO;
+ }
+ /**
+ * 启动做种客户端
+ */
+ private void startSeeding(Path torrentPath, Path dataDir) throws Exception {
+ SimpleClient seederClient = new SimpleClient();
+ seederClient.downloadTorrent(
+ torrentPath.toString(),
+ dataDir.toString(),
+ InetAddress.getLocalHost());
+ // 保持做种状态(阻塞线程)
+ while (true) {
+ Thread.sleep(60000); // 每60秒检查一次
+ }
+ }
+
+
+ @Override
+ public double calculateDownloadSize(Long torrentId, Long userId) {
+ TorrentEntity torrent = selectBySeedId(torrentId);
+ if (torrent == null) {
+ throw new RuntimeException("种子不存在");
+ }
+
+ // 获取当前有效的促销活动
+ double discount = promotionService.getCurrentDiscount(torrentId);
+
+ // 计算实际下载量
+ return torrent.getSize() * (1 - discount / 100.0);
+ }
+
+ @Override
+ @Transactional
+ public void recordDownload(Long torrentId, Long userId, double downloadSize) {
+ // 更新用户下载量
+ userMapper.increaseDownloaded(userId, downloadSize);
+
+ // 更新种子下载次数
+ torrentMapper.increaseDownloads(torrentId);
+ }
+ /**
+ * 计算种子文件的infoHash
+ */
+ private String calculateInfoHash(byte[] torrentData) throws NoSuchAlgorithmException {
+ MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+ sha1.update(torrentData);
+ byte[] hashBytes = sha1.digest();
+ return Hex.encodeHexString(hashBytes);
+ }
+}
\ No newline at end of file