解决合并冲突,并实现种子junit单元测试
Change-Id: Ifff1a63aed76b380ea4767a1e6e1f31f3e9c0882
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java b/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java
index 8a5fdfe..ffda0e7 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java
@@ -11,6 +11,7 @@
*
* @author ruoyi
*/
+
@ComponentScan(basePackages = {
"com.ruoyi",
"bounty" // 新增您的包路径
@@ -18,12 +19,15 @@
// 关键添加:扩大MyBatis接口扫描范围
@MapperScan(basePackages = {
"com.ruoyi.**.mapper",
- "bounty.mapper" // 如果bounty下有Mapper接口
+ "bounty.mapper",
+ "com.ruoyi.web.dao.BT",
+ "com.ruoyi.web.dao.sys"
})
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class RuoYiApplication {
public static void main(String[] args) {
+
// System.setProperty("spring.devtools.restart.enabled", "false");
SpringApplication.run(RuoYiApplication.class, args);
// ANSI escape code for blue text
@@ -31,6 +35,6 @@
// ANSI escape code to reset text color
String reset = "\u001B[0m";
- System.out.println(blue + " ----贾仁翔 喵喵喵" + reset);
+ System.out.println(blue + " ----我爱雨滔身体好好 喵喵 喵 喵" + reset);
}
}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/WebConfig.java b/ruoyi-admin/src/main/java/com/ruoyi/WebConfig.java
index 55d2e34..30808c8 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/WebConfig.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/WebConfig.java
@@ -1,6 +1,8 @@
package com.ruoyi;
+import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@@ -11,4 +13,11 @@
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:uploads/");
}
+ @Bean
+ public CharacterEncodingFilter characterEncodingFilter() {
+ CharacterEncodingFilter filter = new CharacterEncodingFilter();
+ filter.setEncoding("UTF-8");
+ filter.setForceEncoding(true); // 强制覆盖响应编码
+ return filter;
+ }
}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/AnnounceService.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/AnnounceService.java
new file mode 100644
index 0000000..b983acd
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/AnnounceService.java
@@ -0,0 +1,248 @@
+package com.ruoyi.web.Server;
+
+import com.alibaba.fastjson.JSON;
+import com.ruoyi.web.dao.sys.UserDao;
+import com.ruoyi.web.domain.BT.TorrentEntity;
+import com.ruoyi.web.domain.BT.TorrentPeerEntity;
+import com.ruoyi.web.domain.sys.UserEntity;
+import com.ruoyi.web.dao.BT.AnnounceRequest;
+import com.ruoyi.web.dao.BT.TrackerResponse;
+import com.ruoyi.web.Server.sys.UserService;
+import com.ruoyi.web.Server.validator.ValidationManager;
+import com.ruoyi.web.Server.BT.TorrentPeerService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class AnnounceService {
+
+ final UserDao userDao;
+ final UserService userService;
+
+ final TorrentPeerService torrentPeerService;
+
+
+ final ValidationManager validationManager;
+
+ public Map<String, Object> announce(AnnounceRequest request) {
+
+ validationManager.validate(request);
+
+ //TODO 处理短时间内重复的请求
+
+ TorrentEntity torrent = request.getTorrent();
+
+ //省略peer id
+ boolean noPeerId = request.isNoPeerId();
+
+ List<Map<String, Object>> peerList = getPeerList(request, 200);
+
+ updatePeer(request, null);
+
+ updateUserInfo(request);
+ // 返回peer列表给客户端
+ //TODO 默认值是60,改为动态调整
+ Integer interval = getAnnounceInterval(request);
+ TrackerResponse trackerResponse = TrackerResponse.build(interval, 60, torrent.getSeeders(), torrent.getLeechers(), peerList);
+ return trackerResponse.toResultMap();
+ }
+
+ private void updateUserInfo(AnnounceRequest request) {
+ UserEntity user = request.getUser();
+ TorrentEntity torrent = request.getTorrent();
+ Integer userId = request.getUser().getUser_id().intValue();
+ Integer torrentId = request.getTorrent().getId();
+
+ TorrentPeerEntity peer = torrentPeerService.getPeer(userId, torrentId, request.getPeerId());
+ if (peer == null) {
+ //TODO
+ peer = tryInsertOrUpdatePeer(request);
+ }
+
+ long lastUploaded = peer.getUploaded();
+ long lastDownload = peer.getDownloaded();
+ long uploadedOffset = request.getUploaded() - lastUploaded;
+ long downloadedOffset = request.getDownloaded() - lastDownload;
+
+ LocalDateTime updateTime = torrent.getUpdateTime();
+ if (uploadedOffset < 0) {
+ uploadedOffset = request.getUploaded();
+ }
+ if (downloadedOffset < 0) {
+ downloadedOffset = request.getDownloaded();
+ }
+
+ user.setReal_downloaded(Objects.nonNull(user.getReal_downloaded())? user.getReal_downloaded()+lastDownload:lastDownload);
+ user.setReal_uploaded(Objects.nonNull(user.getReal_uploaded())?user.getReal_uploaded()+lastUploaded: lastUploaded);
+
+ //TODO 优惠
+ user.setUpload(user.getUpload() + uploadedOffset);
+ user.setDownload(user.getDownload() + downloadedOffset);
+ user.setSeedtime(user.getSeedtime() + (Instant.now().toEpochMilli() - updateTime.toInstant(ZoneOffset.UTC).toEpochMilli()));
+ userService.updateById(user);
+
+ //TODO 最后处理删除
+ //TODO 校正BEP标准的计算方式
+
+
+ }
+
+ public void updatePeer(AnnounceRequest request, TorrentPeerEntity peerSelf) {
+ String event = StringUtils.trimToEmpty(request.getEvent());
+ log.info("Peer event {}: {}", event, JSON.toJSONString(request, true));
+ Integer userId = request.getUser().getUser_id().intValue();
+ Integer torrentId = request.getTorrent().getId();
+ //TODO 加入分布式锁
+
+ // 任务停止 删除peer
+ if (AnnounceRequest.EventType.stopped.equalsIgnoreCase(event)) {
+
+ // 只有当有peer存在的时候才执行删除操作
+ if (torrentPeerService.peerExists(userId, torrentId, request.getPeerId())) {
+ torrentPeerService.delete(userId, torrentId, request.getPeerIdHex());
+ }
+
+ return;
+ } else if (AnnounceRequest.EventType.started.equalsIgnoreCase(event)) {
+ tryInsertOrUpdatePeer(request);
+
+ } else if (AnnounceRequest.EventType.completed.equalsIgnoreCase(event)) {
+ torrentPeerService.delete(userId, torrentId, request.getPeerIdHex());
+ tryInsertOrUpdatePeer(request);
+
+ } else {
+ torrentPeerService.delete(userId, torrentId, request.getPeerIdHex());
+ tryInsertOrUpdatePeer(request);
+ }
+ }
+
+ /**
+ * 获取 peer 列表
+ *
+ * @param peerNumWant 这个参数表明你希望从方法返回多少个 peers。如果当前的系统中现有的 peer 数量小于你想要的 peerNumWant,那么就返回所有的
+ * peers;否则,只返回你想要的 peerNumWant 数量的 peers。
+ * @return
+ */
+ private List<Map<String, Object>> getPeerList(AnnounceRequest request, Integer peerNumWant) {
+
+ Integer torrentId = request.getTorrent().getId();
+ Boolean seeder = request.getSeeder();
+ boolean noPeerId = request.isNoPeerId();
+ Integer userId = request.getUser().getUser_id().intValue();
+ String peerIdHex = request.getPeerIdHex();
+
+ //TODO 从数据库获取peer列表
+ //TODO 根据 seeder peerNumWant 参数限制peer
+ //如果当前用户是 seeder,那么这段代码将寻找 leecher;如果当前用户不是 seeder(或者不确定是否是 seeder),那么就不对 peer 的类型进行过滤。
+ List<TorrentPeerEntity> list = torrentPeerService.listByTorrent(torrentId, seeder, peerNumWant);
+
+ List<Map<String, Object>> result = list.stream()
+ .map(peer -> {
+
+ // 当前 Peer 自己不返回
+ if (peer.getUserId().equals(userId) && peer.getPeerId().equalsIgnoreCase(peerIdHex)) {
+ return null;
+ }
+
+ Map<String, Object> dataMap = new HashMap<>();
+ // 处理ipv4
+ if (StringUtils.isNotBlank(peer.getIp())) {
+ dataMap.put("ip", peer.getIp());
+ dataMap.put("port", peer.getPort());
+ if (!noPeerId) {
+ dataMap.put("peer id", peer.getPeerId());
+ }
+ }
+ //TODO 支持ipv6
+ //TODO 支持压缩
+ return dataMap.isEmpty() ? null : dataMap;
+ })
+ .filter(peer -> peer != null)
+ .collect(Collectors.toList());
+ return result;
+ }
+
+
+ private TorrentPeerEntity tryInsertOrUpdatePeer(AnnounceRequest request) {
+ try {
+
+ TorrentPeerEntity peerEntity = new TorrentPeerEntity();
+ peerEntity.setUserId(request.getUser().getUser_id().intValue());
+ peerEntity.setTorrentId(request.getTorrent().getId());
+ peerEntity.setPeerId(request.getPeer_id());
+ peerEntity.setPeerIdHex(request.getPeerIdHex());
+ peerEntity.setPort(request.getPort());
+ peerEntity.setDownloaded(request.getDownloaded());
+ peerEntity.setUploaded(request.getUploaded());
+ peerEntity.setRemaining(request.getLeft());
+ peerEntity.setSeeder(request.getSeeder());
+ peerEntity.setUserAgent(request.getUserAgent());
+ peerEntity.setPasskey(request.getPasskey());
+ peerEntity.setCreateTime(LocalDateTime.now());
+ peerEntity.setLastAnnounce(LocalDateTime.now());
+
+ peerEntity.setIp(request.getIp());
+ peerEntity.setIpv6(request.getIpv6());
+
+ if (StringUtils.isBlank(peerEntity.getIp())) {
+ peerEntity.setIp(request.getRemoteAddr());
+ }
+
+ torrentPeerService.save(peerEntity);
+ return peerEntity;
+
+ } catch (Exception exception) {
+ log.error("Peer update error update: ", exception);
+ }
+ return null;
+ }
+
+ /**
+ * 广播间隔
+ * 策略:
+ * 基于种子发布的时间长度来调整广播间隔:类似NP
+ * 如 种子发布7天内,广播间隔为 600s
+ * 种子发布大于7天,小于30天, 广播间隔1800s
+ * 种子发布30天以上,广播间隔3600s
+ * <p>
+ * <p>
+ * 基于活跃度调整广播间隔:你可以根据种子的活跃度(例如活跃peer的数量或者下载/上传速度)来调整广播间隔。
+ * 如果一个种子的活跃度高,说明它需要更频繁地更新和广播peer列表。
+ * 如果活跃度低,可以增加广播间隔以减少服务器负载。
+ * <p>
+ * 基于服务器负载调整广播间隔:如果你的服务器负载高(例如CPU
+ * 使用率高,内存使用量高,或者网络带宽使用高),可以增加广播间隔以减少负载。
+ * 如果服务器负载低,可以减小广播间隔以提高文件分享的效率。
+ * <p>
+ * 动态调整广播间隔:你可以实时监控你的网络状况、服务器状况、以及种子的活跃度,然后动态调整广播间隔。
+ * 例如,如果你发现某个时间段用户数量增多,可以临时减小广播间隔。如果发现某个时间段用户数量减少,可以增加广播间隔。
+ * <p>
+ * <p>
+ * 综合策略:
+ * <p>
+ * 种子发布7天内或活跃peer数大于1000,广播间隔为 600s
+ * 种子发布大于7天,小于30天或活跃peer数在100-1000之间,广播间隔为 1800s
+ * 种子发布30天以上或活跃peer数小于100,广播间隔为 3600s
+ *
+ * @param request
+ * @return
+ */
+ private Integer getAnnounceInterval(AnnounceRequest request) {
+ //TODO 广播间隔
+
+ return 60;
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/DefaultPasskeyManager.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/DefaultPasskeyManager.java
new file mode 100644
index 0000000..fdcf597
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/DefaultPasskeyManager.java
@@ -0,0 +1,20 @@
+package com.ruoyi.web.Server.BT;
+
+import cn.hutool.core.lang.UUID;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class DefaultPasskeyManager implements PasskeyManager {
+
+ @Override
+ public String generate(Integer userId) {
+ //32位UUID
+ String passkey = UUID.fastUUID().toString(true);
+
+ return passkey;
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/DefaultTorrentManager.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/DefaultTorrentManager.java
new file mode 100644
index 0000000..bfe370a
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/DefaultTorrentManager.java
@@ -0,0 +1,106 @@
+package com.ruoyi.web.Server.BT;
+
+import com.dampcake.bencode.Bencode;
+import com.dampcake.bencode.Type;
+import com.ruoyi.web.controller.common.CommonResultStatus;
+import com.ruoyi.web.controller.common.Constants;
+import com.ruoyi.web.controller.common.base.I18nMessage;
+import com.ruoyi.web.controller.common.exception.RocketPTException;
+import com.ruoyi.web.dao.BT.TorrentFileDao;
+import com.ruoyi.web.domain.BT.TorrentDto;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author Jrx
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class DefaultTorrentManager implements TorrentManager {
+
+ private final Bencode bencode = new Bencode(StandardCharsets.UTF_8);
+ private final Bencode infoBencode = new Bencode(StandardCharsets.ISO_8859_1);
+ private final TorrentFileDao torrentFileDao;
+
+ @Override
+ public TorrentDto parse(byte[] bytes) {
+ Map<String, Object> decodeDict = bencode.decode(bytes, Type.DICTIONARY);
+ if (decodeDict.containsKey("piece layers") || decodeDict.containsKey("files tree") ||
+ decodeDict.containsKey("meta version") && (int) decodeDict.get("meta version") == 2) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR, I18nMessage.getMessage(
+ "protocol_v2_not_support"));
+ }
+ Map<String, Object> infoDict = (Map<String, Object>) decodeDict.get("info");
+ Long size;
+ Long count;
+ if (infoDict.containsKey("length")) {
+ size = (Long) infoDict.get("length");
+ count = 1L;
+ } else {
+ List<Map<String, Object>> files = (List<Map<String, Object>>) infoDict.get("files");
+ size = files.stream().map(f -> (Long) f.get("length")).collect(Collectors.summingLong(i -> i));
+ count = (long) files.size();
+ }
+ TorrentDto dto = new TorrentDto();
+ dto.setTorrentSize(size);
+ dto.setTorrentCount(count);
+ dto.setDict(decodeDict);
+ return dto;
+ }
+
+ /**
+ * 修改torrent文件:
+ *
+ * 修改tracker
+ * 修改为私有种子 private=1
+ * http://bittorrent.org/beps/bep_0027.html
+ *
+ * @param bytes
+ * @return
+ */
+ @Override
+ public byte[] transform(byte[] bytes) {
+ Map<String, Object> map = infoBencode.decode(bytes, Type.DICTIONARY);
+ map.remove("announce-list");
+ map.remove("announce");
+ map.put("comment", "ThunderHub pt 赵总我们敬爱您啊!");
+ Map<String, Object> infoMap = (Map<String, Object>) map.get("info");
+ infoMap.put("private", 1);
+ //todo 配置
+ //todo 配置前缀
+ infoMap.put("source", Constants.Source.PREFIX + Constants.Source.NAME);
+ map.put("info", infoMap);
+
+ // 屏蔽Bittorrent v2种子上传
+ if (map.containsKey("piece layers") || infoMap.containsKey("files tree")
+ || (infoMap.containsKey("meta version") && infoMap.get("meta version").equals(2))) {
+
+ throw new RocketPTException("不支持使用 Bittorrent 协议 v2 创建的 Torrent 文件或混合 torrent");
+ }
+
+
+
+ return infoBencode.encode(map);
+ }
+
+ @Override
+ public byte[] infoHash(byte[] bytes) throws NoSuchAlgorithmException {
+ Map<String, Object> decodedMap = infoBencode.decode(bytes, Type.DICTIONARY);
+ Map<String, Object> infoDecodedMap = (Map<String, Object>) decodedMap.get("info");
+ byte[] encode = infoBencode.encode(infoDecodedMap);
+
+ MessageDigest md = MessageDigest.getInstance("SHA-1");
+ return md.digest(encode);
+ }
+
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/LocalTorrentStorageService.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/LocalTorrentStorageService.java
new file mode 100644
index 0000000..f49329c
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/LocalTorrentStorageService.java
@@ -0,0 +1,130 @@
+package com.ruoyi.web.Server.BT;
+
+import cn.hutool.core.io.FileUtil;
+import com.ruoyi.web.controller.common.exception.RocketPTException;
+import com.ruoyi.web.Tool.BT.TorrentUtils;
+import jakarta.annotation.PostConstruct;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.InputStream;
+
+
+/**
+ * 本地种子存储
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class LocalTorrentStorageService implements TorrentStorageService {
+
+ /**
+ * 使用绝对路径
+ */
+ @Getter
+ @Setter
+ boolean useAbsolutePath = false;
+
+ @Getter
+ @Setter
+ String absolutePath = "/torrent/";
+
+ @Getter
+ @Setter
+ String relativePath = "/torrent/";
+
+
+ String defaultDir = TorrentUtils.getDefaultDir();
+
+
+ @PostConstruct
+ @Override
+ @SneakyThrows
+ public void init() {
+ // 1. 初始化绝对路径(基于相对路径自动生成)
+ if (useAbsolutePath) {
+ // 若需绝对路径,可基于类路径或系统目录生成
+ absolutePath = new File(relativePath).getAbsolutePath();
+ } else {
+ absolutePath = relativePath; // 直接使用相对路径(由运行环境决定解析方式)
+ }
+
+ // 2. 确保路径存在(自动创建目录)
+ FileUtil.mkdir(absolutePath);
+ }
+
+
+ @Override
+ public void save(Integer id, byte[] torrent) {
+ String path = getFilePath(id);
+
+ FileUtil.writeBytes(torrent, path);
+ }
+
+ public String getPath() {
+ if (useAbsolutePath) {
+ return absolutePath;
+ }
+
+ return defaultDir + relativePath;
+ }
+
+ public String getFilePath(int id) {
+ String path = getPath() + id + ".torrent";
+
+ return path;
+ }
+
+ @Override
+ @SneakyThrows
+ public void store(Integer id, MultipartFile file) {
+ String filename = StringUtils.cleanPath(file.getOriginalFilename());
+ // validation here for .torrent file
+ if (!filename.endsWith(".torrent")) {
+ throw new RocketPTException("Invalid file type. Only .torrent files are allowed");
+ }
+
+ if (file.isEmpty()) {
+ throw new RocketPTException("Failed to store empty file " + filename);
+ }
+ if (filename.contains("..")) {
+ // This is a security check
+ throw new RocketPTException("Cannot store file with relative path outside current" +
+ " directory "
+ + filename);
+ }
+
+ try (InputStream inputStream = file.getInputStream()) {
+ FileUtil.writeFromStream(inputStream, getFilePath(id));
+ }
+
+ }
+
+ @Override
+ public byte[] read(Integer id) {
+ String path = getFilePath(id);
+
+ return FileUtil.readBytes(path);
+ }
+
+ @Override
+ public void delete(Integer id) {
+ String path = getFilePath(id);
+ FileUtil.del(path);
+ }
+
+ @Override
+ public InputStream load(Integer id) {
+ String path = getFilePath(id);
+ return FileUtil.getInputStream(path);
+
+ }
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/PasskeyManager.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/PasskeyManager.java
new file mode 100644
index 0000000..3ca02ff
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/PasskeyManager.java
@@ -0,0 +1,12 @@
+package com.ruoyi.web.Server.BT;
+
+import org.springframework.stereotype.Service;
+
+/**
+ * @author Jrx
+ */
+public interface PasskeyManager {
+
+ String generate(Integer userId);
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/TorrentCommentService.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/TorrentCommentService.java
new file mode 100644
index 0000000..fbc0045
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/TorrentCommentService.java
@@ -0,0 +1,88 @@
+package com.ruoyi.web.Server.BT;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import com.ruoyi.web.Server.sys.UserService;
+import com.ruoyi.web.dao.BT.TorrentCommentDao;
+import com.ruoyi.web.dao.BT.TorrentPeerDao;
+import com.ruoyi.web.domain.BT.TorrentCommentEntity;
+
+import com.ruoyi.web.domain.BT.TorrentCommentVO;
+import com.ruoyi.web.domain.sys.UserEntity;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class TorrentCommentService extends ServiceImpl<TorrentCommentDao, TorrentCommentEntity> {
+
+ private final TorrentCommentDao torrentCommentDao;
+ private final UserService userService;
+
+ public boolean saveComment(TorrentCommentEntity entity){
+ return save(entity);
+ }
+
+
+ public PageInfo<TorrentCommentVO> getCommentsByTorrentId(
+ Integer torrentId, Integer pageNum, Integer pageSize) {
+ // 分页查询
+ PageHelper.startPage(pageNum, pageSize);
+ List<TorrentCommentEntity> commentEntities =
+ torrentCommentDao.selectByTorrentId(torrentId);
+
+ // 转换为VO(包含用户信息)
+ List<TorrentCommentVO> commentVOs = commentEntities.stream()
+ .map(entity -> {
+ TorrentCommentVO vo = new TorrentCommentVO();
+ BeanUtils.copyProperties(entity, vo);
+
+ // 查询用户信息
+ UserEntity user = userService.getUserById(entity.getUserId());
+ if (user != null) {
+ vo.setUsername(user.getUser_name());
+ vo.setAvatar(user.getAvatar());
+ }
+
+ // 递归查询子评论(楼中楼)
+ List<TorrentCommentVO> children = getChildrenComments(entity.getId());
+ vo.setChildren(children);
+
+ return vo;
+ })
+ .collect(Collectors.toList());
+
+ return new PageInfo<>(commentVOs);
+ }
+
+ // 递归查询子评论
+ private List<TorrentCommentVO> getChildrenComments(Integer parentId) {
+ List<TorrentCommentEntity> children =
+ torrentCommentDao.selectByParentId(parentId);
+
+ return children.stream()
+ .map(entity -> {
+ TorrentCommentVO vo = new TorrentCommentVO();
+ BeanUtils.copyProperties(entity, vo);
+
+ // 设置用户信息
+ UserEntity user = userService.getUserById(entity.getUserId());
+ if (user != null) {
+ vo.setUsername(user.getUser_name());
+ vo.setAvatar(user.getAvatar());
+ }
+
+ return vo;
+ })
+ .collect(Collectors.toList());
+ }
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/TorrentManager.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/TorrentManager.java
new file mode 100644
index 0000000..5f19aac
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/TorrentManager.java
@@ -0,0 +1,18 @@
+package com.ruoyi.web.Server.BT;
+
+import com.ruoyi.web.domain.BT.TorrentDto;
+
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * @author Jrx
+ */
+public interface TorrentManager {
+
+ TorrentDto parse(byte[] bytes);
+
+ byte[] transform(byte[] bytes);
+
+ byte[] infoHash(byte[] bytes) throws NoSuchAlgorithmException;
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/TorrentPeerService.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/TorrentPeerService.java
new file mode 100644
index 0000000..af59128
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/TorrentPeerService.java
@@ -0,0 +1,72 @@
+package com.ruoyi.web.Server.BT;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.web.dao.BT.TorrentPeerDao;
+import com.ruoyi.web.domain.BT.TorrentPeerEntity;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 种子Peer表
+ */
+@Service
+@Slf4j
+public class TorrentPeerService extends ServiceImpl<TorrentPeerDao, TorrentPeerEntity> {
+
+
+ /**
+ * 从数据库获取peer列表
+ *
+ * @param torrentId
+ * @param seeder
+ * @param peerNumWant
+ * @return
+ */
+ public List<TorrentPeerEntity> listByTorrent(Integer torrentId,
+ Boolean seeder,
+ Integer peerNumWant) {
+
+ List<TorrentPeerEntity> list = list(new QueryWrapper<TorrentPeerEntity>()
+ .lambda()
+ .eq(TorrentPeerEntity::getTorrentId, torrentId)
+ // 如果下载完了,就不需要获取seeder用户了
+ // 如果当前用户是 seeder,那么这段代码将寻找 leecher;如果当前用户不是 seeder(或者不确定是否是 seeder),那么就不对 peer 的类型进行过滤
+ .eq(seeder, TorrentPeerEntity::getSeeder, 0)
+ .last("order by rand() limit " + peerNumWant)
+ );
+
+ return list;
+ }
+
+ public boolean peerExists(Integer userId, Integer torrentId, byte[] peerId) {
+ long count = count(new QueryWrapper<TorrentPeerEntity>()
+ .lambda()
+ .eq(TorrentPeerEntity::getTorrentId, torrentId)
+ .eq(TorrentPeerEntity::getUserId, userId)
+ .eq(TorrentPeerEntity::getPeerId, peerId)
+ );
+ return count > 0;
+ }
+
+ public TorrentPeerEntity getPeer(Integer userId, Integer torrentId, byte[] peerId) {
+ return getOne(new QueryWrapper<TorrentPeerEntity>()
+ .lambda()
+ .eq(TorrentPeerEntity::getTorrentId, torrentId)
+ .eq(TorrentPeerEntity::getUserId, userId)
+ .eq(TorrentPeerEntity::getPeerId, peerId), false
+ );
+ }
+
+ public void delete(Integer userId, Integer torrentId, String peerIdHex) {
+ remove(new QueryWrapper<TorrentPeerEntity>()
+ .lambda()
+ .eq(TorrentPeerEntity::getTorrentId, torrentId)
+ .eq(TorrentPeerEntity::getUserId, userId)
+ .eq(TorrentPeerEntity::getPeerIdHex, peerIdHex)
+ );
+ }
+}
+
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/TorrentService.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/TorrentService.java
new file mode 100644
index 0000000..b6f1b72
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/TorrentService.java
@@ -0,0 +1,273 @@
+package com.ruoyi.web.Server.BT;
+
+import cn.hutool.core.bean.BeanUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.dampcake.bencode.Bencode;
+import com.dampcake.bencode.Type;
+import com.ruoyi.system.mapper.SysConfigMapper;
+import com.ruoyi.web.Server.sys.UserCredentialService;
+import com.ruoyi.web.Tool.BT.IPUtils;
+import com.ruoyi.web.Tool.BT.TorrentUtils;
+import com.ruoyi.web.controller.common.CommonResultStatus;
+import com.ruoyi.web.controller.common.base.I18nMessage;
+import com.ruoyi.web.controller.common.exception.RocketPTException;
+import com.ruoyi.web.dao.BT.TorrentDao;
+import com.ruoyi.web.domain.BT.*;
+import com.ruoyi.web.Server.sys.UserService;
+import com.ruoyi.web.domain.sys.UserCredentialEntity;
+import com.ruoyi.web.domain.sys.UserEntity;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import cn.hutool.core.util.RandomUtil;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class TorrentService extends ServiceImpl<TorrentDao, TorrentEntity> {
+
+ final TorrentManager torrentManager;
+ final TorrentDao torrentDao;
+ final TrackerURLService trackerURLService;
+ private final UserCredentialService userCredentialService;
+ private final PasskeyManager passkeyManager;
+
+ final UserService userService;
+ final TorrentStorageService torrentStorageService;
+ final Bencode infoBencode = new Bencode(StandardCharsets.ISO_8859_1);
+
+ public void incrementLeechers(Integer id) {
+ // 使用乐观锁或数据库锁确保线程安全
+ // 示例使用数据库 update 语句原子性增加
+ int rows = torrentDao.incrementLeechers(id);
+ if (rows == 0) {
+ throw new RocketPTException(CommonResultStatus.FAIL, "Failed to update leechers count");
+ }
+ }
+
+ public Integer add(TorrentAddParam param) {
+ TorrentEntity entity = BeanUtil.copyProperties(param, TorrentEntity.class);
+ entity.setStatus(TorrentEntity.Status.CANDIDATE);
+ entity.setFileStatus(0);
+ entity.setOwner(userService.getUserId());
+ torrentDao.insert(entity);
+ return entity.getId();
+ }
+
+ @SneakyThrows
+ @Transactional(rollbackFor = Exception.class)
+ public void upload(Integer id, byte[] bytes, String filename) {
+ System.out.println(bytes);
+ TorrentEntity entity = getById(id);
+ if (entity == null) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR, "该种子不存在");
+ }
+ if (entity.getFileStatus() == 1) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR, "该种子已上传过,如需重传,请删除后再试");
+ }
+ if (!entity.getOwner().equals(userService.getUserId())) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR, "该种子不属于你");
+ }
+
+
+ byte[] transformedBytes = torrentManager.transform(bytes);
+ byte[] infoHash = torrentManager.infoHash(transformedBytes);
+ System.out.println(infoHash);
+
+ long count = count(Wrappers.<TorrentEntity>lambdaQuery()
+ .eq(TorrentEntity::getInfoHash, infoHash));
+
+ if (count != 0) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR, "该种子站内已存在。");
+ }
+ TorrentDto dto = torrentManager.parse(bytes);
+
+ entity.setInfoHash(infoHash);
+ entity.setFilename(filename);
+
+ entity.setSize(dto.getTorrentSize());
+ entity.setFileCount(dto.getTorrentCount().intValue());
+ entity.setType(dto.getTorrentCount() > 1 ? 2 : 1);
+ updateById(entity);
+ torrentStorageService.save(id, transformedBytes);
+
+ }
+
+ public byte[] fetch(Integer torrentId, String passkey) {
+ byte[] fileBytes = torrentStorageService.read(torrentId);
+
+ if (fileBytes == null) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR, I18nMessage.getMessage(
+ "torrent_not_exists"));
+ }
+
+ Map<String, Object> decodedMap = infoBencode.decode(fileBytes, Type.DICTIONARY);
+ if (StringUtils.isEmpty(passkey)) {
+ passkey = userService.getPasskey(userService.getUserId());
+ }
+
+ decodedMap.put("announce", trackerURLService.getAnnounce(passkey));
+
+ return infoBencode.encode(decodedMap);
+ }
+
+ public void audit(TorrentAuditParam param) {
+
+
+
+ Integer id = param.getId();
+ TorrentEntity entity = getById(id);
+ if (entity == null) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR, "该种子不存在");
+ }
+ if (entity.getStatus() != 0) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR, "该种子不是待审核状态");
+ }
+ entity.setReviewer(userService.getUserId());
+
+ entity.setStatus(param.getStatus());
+ if (StringUtils.isNotEmpty(param.getRemark())) {
+ entity.setRemark(param.getRemark());
+ }
+ updateById(entity);
+
+ }
+
+ public void update(TorrentEntity entity) {
+ Integer id = entity.getId();
+ TorrentEntity torrent = getById(id);
+
+ if (torrent == null) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR, I18nMessage.getMessage(
+ "torrent_not_exists"));
+ }
+ if (!entity.getOwner().equals(userService.getUserId())) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR, "该种子不属于你");
+ }
+
+ torrent.setDescription(entity.getDescription());
+ torrent.setTitle(entity.getTitle());
+ torrent.setName(entity.getName());
+ torrent.setSubheading(entity.getSubheading());
+ torrent.setStatus(TorrentEntity.Status.RETRIAL);
+
+ updateById(torrent);
+ }
+
+ public void remove(Integer[] ids) {
+ for (Integer id : ids) {
+ TorrentEntity entity = getById(id);
+ if (!entity.getOwner().equals(userService.getUserId())) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR, "该种子不属于你");
+ }
+ }
+
+ removeByIds(Arrays.asList(ids));
+ }
+
+ public TorrentEntity getByInfoHash(byte[] infoHash) {
+ TorrentEntity entity = getOne(new QueryWrapper<TorrentEntity>()
+ .lambda()
+ .eq(TorrentEntity::getInfoHash, infoHash)
+ , false
+ );
+ return entity;
+ }
+
+ /**
+ * 收藏或者取消收藏
+ */
+ public void favorite(Integer torrentId, Integer userId) {
+
+ }
+
+
+ /**
+ * 将普通文件转换为种子文件
+ * @param fileBytes 原始文件字节数据
+ * @param originalName 原始文件名(不含扩展名)
+ * @param announceUrl tracker地址
+ * @return 种子文件字节数据
+ */
+ public byte[] createTorrentFromFile(byte[] fileBytes, String originalName, String announceUrl)
+ throws IOException, RocketPTException {
+
+ try {
+ // 生成种子文件名(强制添加.torrent后缀)
+ String torrentName = originalName + ".torrent";
+
+ // 创建临时文件
+ Path tempFile = Files.createTempFile("upload-", ".tmp");
+ Files.write(tempFile, fileBytes);
+
+ // 调用工具类生成种子
+ File torrentFile = new File(tempFile.toUri());
+ TorrentUtils.createTorrent(
+ torrentFile,
+ new File(tempFile.toUri()),
+ announceUrl
+ );
+
+ // 读取生成的种子文件
+ byte[] torrentBytes = Files.readAllBytes(tempFile);
+
+ // 清理临时文件
+ Files.deleteIfExists(tempFile);
+
+ return torrentBytes;
+
+ } catch (IOException e) {
+ log.error("生成种子文件失败: {}", e.getMessage(), e);
+ throw new RocketPTException("文件转换失败,请重试");
+ }
+ }
+
+ /**
+ * The purpose of the method:
+ * 给当前用户创建passkey凭证信息
+ */
+ public void createCredential(int userId){
+
+ if (userCredentialService.getById(userId) == null){
+ UserEntity userEntity = userService.getUserById(userId);
+ UserCredentialEntity userCredentialEntity = new UserCredentialEntity();
+ userCredentialEntity.setUserid(userEntity.getUser_id().intValue());
+ userCredentialEntity.setUsername(userEntity.getUser_name());
+ userCredentialEntity.setRegIp(IPUtils.getIpAddr());
+ userCredentialEntity.setRegType(userEntity.getReg_type());
+ String checkCode = passkeyManager.generate(userEntity.getUser_id().intValue());
+ userCredentialEntity.setCheckCode(checkCode);
+
+ // 生成随机盐和密码
+ String salt = RandomUtil.randomString(8);
+ String passkey = passkeyManager.generate(userEntity.getUser_id().intValue());
+
+ userCredentialEntity.setSalt(salt);
+ userCredentialEntity.setPasskey(passkey);
+ String generatedPassword = userCredentialService.generate(userEntity.getPassword(), salt);
+ userCredentialEntity.setPassword(generatedPassword);
+ // 保存用户凭证实体
+ userCredentialService.save(userCredentialEntity);
+ }
+
+ }
+
+
+ public List<TorrentEntity> advancedSearch(AdvancedTorrentParam param){
+ return torrentDao.advancedSearch(param);
+ }
+}
+
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/TorrentStorageService.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/TorrentStorageService.java
new file mode 100644
index 0000000..147eb92
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/TorrentStorageService.java
@@ -0,0 +1,28 @@
+package com.ruoyi.web.Server.BT;
+
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.InputStream;
+
+/**
+ * 种子存储服务
+ *
+ */
+public interface TorrentStorageService {
+
+ default void init() {
+
+ }
+
+ void save(Integer id, byte[] torrent);
+
+ void store(Integer id, MultipartFile file);
+
+ InputStream load(Integer id);
+
+ byte[] read(Integer id);
+
+ void delete(Integer id);
+
+}
+
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/TrackerURLService.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/TrackerURLService.java
new file mode 100644
index 0000000..d28c7e2
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/BT/TrackerURLService.java
@@ -0,0 +1,38 @@
+package com.ruoyi.web.Server.BT;
+
+import com.ruoyi.web.controller.common.Constants;
+import com.ruoyi.web.controller.common.Constants;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * 系统配置
+ *
+ * @author Jrx
+ */
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class TrackerURLService {
+
+ /**
+ * 获取当前的tracker Announce 地址
+ */
+ public String getAnnounce(String passkey) {
+ try {
+ InetAddress localhost = InetAddress.getLocalHost();
+ String hostname = localhost.getHostName();
+
+ String string = Constants.Announce.PROTOCOL + "://" + hostname + ":" + Constants.Announce.PORT + "/tracker/announce?passkey=" + passkey;
+
+ return string;
+ }catch (UnknownHostException e){
+ return "Server error: can not get host";
+ }
+ }
+}
+
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/sys/UserCredentialService.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/sys/UserCredentialService.java
new file mode 100644
index 0000000..8839b9d
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/sys/UserCredentialService.java
@@ -0,0 +1,140 @@
+package com.ruoyi.web.Server.sys;
+
+import cn.dev33.satoken.secure.SaSecureUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.web.controller.common.CommonResultStatus;
+import com.ruoyi.web.controller.common.exception.RocketPTException;
+import com.ruoyi.web.dao.sys.UserCredentialDao;
+import com.ruoyi.web.domain.sys.UserCredentialEntity;
+import com.ruoyi.web.Server.BT.PasskeyManager;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+public class UserCredentialService extends ServiceImpl<UserCredentialDao, UserCredentialEntity> {
+
+ private final PasskeyManager passkeyManager;
+
+ private final UserCredentialDao userCredentialDao;
+
+ /**
+ * 生成加密的密码
+ *
+ * @param password
+ * @param salt
+ * @return
+ */
+ public String generate(String password, String salt) {
+ return SaSecureUtil.sha256(password + salt);
+ }
+
+ /**
+ * 更新密码
+ */
+ public void updatePassword(Integer userId, String newPassword) {
+ String salt = getById(userId).getSalt();
+ updatePassword(userId, newPassword, salt);
+ }
+
+ /**
+ * 更新totp
+ */
+ public void updateTotp(Integer userId, String newTotp) {
+ update(new LambdaUpdateWrapper<UserCredentialEntity>()
+ .eq(UserCredentialEntity::getUserid, userId)
+ .set(UserCredentialEntity::getTotp, newTotp)
+ );
+ }
+
+ /**
+ * 去掉remove
+ */
+ public void removeTotp(Integer userId) {
+ update(new LambdaUpdateWrapper<UserCredentialEntity>()
+ .eq(UserCredentialEntity::getUserid, userId)
+ .set(UserCredentialEntity::getTotp, null)
+ );
+ }
+
+
+ /**
+ * 更新密码
+ */
+ public void updatePassword(Integer userId, String newPassword, String salt) {
+ String generatedPassword = generate(newPassword, salt);
+
+ update(new LambdaUpdateWrapper<UserCredentialEntity>()
+ .eq(UserCredentialEntity::getUserid, userId)
+ .set(UserCredentialEntity::getPassword, generatedPassword)
+ );
+
+ }
+
+ /**
+ * 根据用户名获取
+ *
+ * @param username
+ * @return
+ */
+ public UserCredentialEntity getByUsername(String username) {
+ return getOne(new QueryWrapper<UserCredentialEntity>()
+ .lambda()
+ .eq(UserCredentialEntity::getUsername, username), false
+ );
+ }
+
+ public UserCredentialEntity getByUserId(Integer id) {
+ return userCredentialDao.getById(id);
+ }
+
+ /**
+ * 获取
+ *
+ * @return
+ */
+ public UserCredentialEntity getByCheckCode(String code) {
+ return getOne(new QueryWrapper<UserCredentialEntity>()
+ .lambda()
+ .eq(UserCredentialEntity::getCheckCode, code), false
+ );
+ }
+
+
+ /**
+ * 重置用户PASSKEY
+ *
+ * @param userId
+ */
+ public void refreshPasskey(Integer userId) {
+
+ UserCredentialEntity credentialEntity = getOne(
+ Wrappers.<UserCredentialEntity>lambdaQuery()
+ .eq(UserCredentialEntity::getUserid, userId)
+ );
+
+ UserCredentialEntity credential = Optional.ofNullable(credentialEntity)
+ .orElseThrow(() -> new RocketPTException(CommonResultStatus.PARAM_ERROR));
+
+ String key = passkeyManager.generate(userId);
+
+ credential.setPasskey(key);
+ updateById(credential);
+ }
+
+ public String resetCheckCode(Integer userId) {
+ String checkCode = passkeyManager.generate(userId);
+
+ update(new LambdaUpdateWrapper<UserCredentialEntity>()
+ .eq(UserCredentialEntity::getUserid, userId)
+ .set(UserCredentialEntity::getCheckCode, checkCode)
+ );
+
+ return checkCode;
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/sys/UserService.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/sys/UserService.java
new file mode 100644
index 0000000..f5b57ce
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/sys/UserService.java
@@ -0,0 +1,501 @@
+package com.ruoyi.web.Server.sys;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.web.Tool.BT.RedisUtil;
+import com.ruoyi.web.dao.sys.UserDao;
+import com.ruoyi.web.domain.sys.UserCredentialEntity;
+import com.ruoyi.web.domain.sys.UserEntity;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author Jrx
+ */
+@Service
+@RequiredArgsConstructor
+public class UserService extends ServiceImpl<UserDao,UserEntity> {
+
+ private final UserDao userDao;
+// private final InvitationService invitationService;
+ private final UserCredentialService userCredentialService;
+// private final CheckCodeManager checkCodeManager;
+// private final PasskeyManager passkeyManager;
+// private final CaptchaService captchaService;
+// private final MailService mailService;
+// private final EmailCodeService emailCodeService;
+// private final UserRoleService userRoleService;
+ private final RedisUtil redisUtil;
+//
+// private final GoogleAuthenticatorService googleAuthenticatorService;
+
+
+// @Transactional(rollbackFor = Exception.class)
+// public UserEntity createUser(String username,
+// String fullName,
+// String avatar,
+// UserEntity.Gender gender,
+// String email,
+// UserEntity.State state) {
+// UserEntity userEntity = new UserEntity();
+// userEntity.setUsername(username);
+// userEntity.setNickname(fullName);
+// userEntity.setAvatar(avatar);
+// userEntity.setGender(gender.getCode());
+// userEntity.setEmail(email);
+// userEntity.setState(state.getCode());
+// userEntity.setCreateTime(LocalDateTime.now());
+// save(userEntity);
+// DomainEventPublisher.instance().publish(new UserCreated(userEntity));
+// return userEntity;
+// }
+//
+// @Transactional(rollbackFor = Exception.class)
+// public UserEntity createUser(UserEntity entity) {
+// entity.setState(0);
+// entity.setCreateTime(LocalDateTime.now());
+// save(entity);
+// DomainEventPublisher.instance().publish(new UserCreated(entity));
+// return entity;
+// }
+//
+// public Set<UserEntity> findUserByIds(Set<Integer> userIds) {
+// List<UserEntity> userEntities = listByIds(userIds);
+// return new LinkedHashSet<>(userEntities);
+// }
+//
+// public List<UserEntity> findUserByIds(List<Integer> ids) {
+// List<UserEntity> list = listByIds(ids);
+// return list;
+// }
+//
+// public UserEntity findUserById(Integer userId) {
+// UserEntity userEntity = getById(userId);
+// if (userEntity == null) {
+// throw new RocketPTException(RECORD_NOT_EXIST);
+// }
+// return userEntity;
+// }
+//
+//
+// @Transactional(rollbackFor = Exception.class)
+// public UserEntity updateUser(Integer userId, String fullName, String avatar,
+// UserEntity.Gender gender,
+// UserEntity.State state, Long organization) {
+// UserEntity userEntity = findUserById(userId);
+// userEntity.setNickname(fullName);
+// userEntity.setAvatar(avatar);
+// userEntity.setGender(gender.getCode());
+// userEntity.setState(state.getCode());
+// updateById(userEntity);
+// DomainEventPublisher.instance().publish(new UserUpdated(userEntity));
+// return userEntity;
+// }
+//
+//
+// @Transactional(rollbackFor = Exception.class)
+// public UserEntity updateUser(UserEntity entity) {
+//
+// updateById(entity);
+// DomainEventPublisher.instance().publish(new UserUpdated(entity));
+// return entity;
+// }
+//
+// @Transactional(rollbackFor = Exception.class)
+// public UserEntity lockUser(Integer userId) {
+// UserEntity userEntity = findUserById(userId);
+// userEntity.setState(UserEntity.State.LOCKED.getCode());
+// updateById(userEntity);
+// return userEntity;
+// }
+//
+// @Transactional(rollbackFor = Exception.class)
+// public UserEntity unlockUser(Integer userId) {
+// UserEntity userEntity = findUserById(userId);
+// userEntity.setState(UserEntity.State.NORMAL.getCode());
+// updateById(userEntity);
+// return userEntity;
+// }
+//
+//// public PageDTO<UserEntity> findUsers(Pageable pageable, UserEntity userEntity) {
+//// PageHelper.startPage(pageable.getPageNumber(), pageable.getPageSize());
+//// Map<String, Object> map = JsonUtils.parseToMap(JsonUtils.stringify(userEntity));
+//// List<UserEntity> userEntities = listByMap(map);
+//// long total = new PageInfo(userEntities).getTotal();
+//// return new PageDTO<>(userEntities, total);
+//// }
+//
+// public Result findUsers(UserParam param) {
+// PageHelper.startPage(param.getPage(), param.getSize());
+// boolean usernameNotEmpty = StringUtils.isNotEmpty(param.getUsername());
+// List<UserEntity> list = list(new QueryWrapper<UserEntity>()
+// .lambda()
+// .like(usernameNotEmpty, UserEntity::getUsername, param.getUsername())
+// );
+//
+// ResPage page = PageUtil.getPage(list);
+// return Result.ok(list, page);
+// }
+//
+// public boolean isExists(String email, String username) {
+// long count = count(Wrappers.<UserEntity>lambdaQuery()
+// .eq(UserEntity::getEmail, email)
+// .or()
+// .eq(UserEntity::getUsername, username)
+// );
+//
+// return count > 0;
+// }
+//
+// @Transactional(rollbackFor = Exception.class)
+// public void delete(Integer userId) {
+// UserEntity userEntity = findUserById(userId);
+// removeById(userEntity);
+// DomainEventPublisher.instance().publish(new UserDeleted(userEntity));
+// }
+//
+// /**
+// * 用户注册方法
+// *
+// * @param param 注册参数
+// * @throws RocketPTException 注册过程中的异常
+// */
+// @Transactional(rollbackFor = Exception.class)
+// public void register(RegisterParam param) {
+// if (param.getType() != 1) {
+// // 检查邀请码是否有效
+// if (!invitationService.check(param.getEmail(), param.getInvitationCode())) {
+// throw new RocketPTException(CommonResultStatus.PARAM_ERROR, I18nMessage.getMessage(
+// "invitation_not_exists"));
+// }
+// }
+//
+// // 检查邮箱和用户名是否已存在
+// if (isExists(param.getEmail(), param.getUsername())) {
+// throw new RocketPTException(CommonResultStatus.PARAM_ERROR, I18nMessage.getMessage(
+// "email_exists"));
+// }
+// emailCodeService.checkEmailCode(param.getEmail(), param.getCode());
+//
+// // 校验通过,创建用户实体
+// UserEntity userEntity = createUser(
+// param.getUsername(),
+// param.getNickname(),
+// "/img.png",
+// UserEntity.Gender.valueof(param.getSex()),
+// param.getEmail(),
+// UserEntity.State.NORMAL
+// );
+//
+// // 设置用户属性
+// updateById(userEntity);
+//
+// // 创建用户凭证实体
+// UserCredentialEntity userCredentialEntity = new UserCredentialEntity();
+// userCredentialEntity.setId(userEntity.getId());
+// userCredentialEntity.setUsername(param.getUsername());
+// userCredentialEntity.setRegIp(IPUtils.getIpAddr());
+// userCredentialEntity.setRegType(param.getType());
+// String checkCode = passkeyManager.generate(userEntity.getId());
+// userCredentialEntity.setCheckCode(checkCode);
+//
+// // 生成随机盐和密码
+// String salt = RandomUtil.randomString(8);
+// String passkey = passkeyManager.generate(userEntity.getId());
+//
+// userCredentialEntity.setSalt(salt);
+// userCredentialEntity.setPasskey(passkey);
+// String generatedPassword = userCredentialService.generate(param.getPassword(), salt);
+// userCredentialEntity.setPassword(generatedPassword);
+//
+// // 保存用户凭证实体
+// userCredentialService.save(userCredentialEntity);
+// userRoleService.register(userEntity.getId());
+//
+// // 消费邀请码
+// if (param.getType() != 1) {
+// invitationService.consume(param.getEmail(), param.getInvitationCode(), userEntity);
+// }
+// }
+//
+//
+// /**
+// * 用户登录方法
+// *
+// * @param param 登录参数
+// * @return 登录成功返回用户ID,登录失败返回0
+// */
+// public Integer login(LoginParam param) {
+// String username = param.getUsername();
+// String password = param.getPassword();
+//
+// // 根据用户名获取用户凭证实体
+// UserCredentialEntity user = userCredentialService.getByUsername(username);
+//
+// if (user == null) {
+// return 0;
+// }
+//
+// // 对密码进行加密处理
+// String encryptedPassword = SaSecureUtil.sha256(password + user.getSalt());
+//
+// // 比较加密后的密码与数据库中存储的密码是否一致
+// if (!user.getPassword().equals(encryptedPassword)) {
+// return 0;
+// }
+//
+// // 获取用户的TOTP
+// String totp = user.getTotp();
+//
+// // 验证TOTP码是否有效
+// boolean codeValid = isTotpValid(param.getTotp(), totp);
+// if (!codeValid) {
+// return 0;
+// }
+//
+// if (isUserLocked(user.getId())) {
+// throw new UserException(CommonResultStatus.UNAUTHORIZED, "用户已经禁用,请与管理员联系");
+// }
+//
+// // 返回用户ID
+// return user.getId();
+// }
+//
+// /**
+// * @param userId
+// * @return 用户是否已经禁用
+// */
+// public boolean isUserLocked(Integer userId) {
+// UserEntity userEntity = getOne(new QueryWrapper<UserEntity>()
+// .lambda()
+// .select(UserEntity::getState)
+// .eq(UserEntity::getId, userId)
+// );
+// if (userEntity == null) {
+// throw new UserException(CommonResultStatus.UNAUTHORIZED, "用户不存在");
+// }
+//
+// return userEntity.getState() == 1;
+// }
+//
+// /**
+// * 验证TOTP码是否有效
+// *
+// * @param verificationCode 用户输入的验证码
+// * @param totp 用户的TOTP码
+// * @return 验证码有效返回true,无效返回false
+// */
+// public boolean isTotpValid(Integer verificationCode, String totp) {
+// // 如果用户的TOTP码为空,视为有效
+// if (StringUtils.isEmpty(totp)) {
+// return true;
+// }
+//
+// // 如果用户输入的验证码为空,视为无效
+// if (verificationCode == null) {
+// return false;
+// }
+//
+// // 使用Google Authenticator服务验证TOTP码的有效性
+// boolean codeValid = googleAuthenticatorService.isCodeValid(totp, verificationCode);
+//
+// // 返回验证结果
+// return codeValid;
+// }
+//
+//
+// /**
+// * 根据username获取用户信息
+// *
+// * @param username
+// * @return
+// */
+// private UserEntity getByUsername(String username) {
+// return getOne(new QueryWrapper<UserEntity>()
+// .lambda()
+// .eq(UserEntity::getUsername, username), false
+// );
+// }
+//
+// /**
+// * 根据email获取用户信息
+// *
+// * @return
+// */
+// private UserEntity getByEmail(String email) {
+// return getOne(new QueryWrapper<UserEntity>()
+// .lambda()
+// .eq(UserEntity::getEmail, email), false
+// );
+// }
+
+ /**
+ * 获取当前登录的用户ID
+ */
+ public Integer getUserId() {
+ // 获取当前认证信息
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+
+ if (authentication != null && authentication.getPrincipal() instanceof LoginUser) {
+ LoginUser loginUser = (LoginUser) authentication.getPrincipal();
+ return loginUser.getUserId().intValue(); // 从 LoginUser 中获取用户ID
+ }
+
+ return null; // 未登录或无法获取用户ID
+ }
+
+ /**
+ * The purpose of the method
+ * 获取当前用户
+ * @param id
+ * {@code @author} Jia
+ * {@code @date} 2025/5/27 18:08
+ */
+ public UserEntity getUserById(int id) {
+ UserEntity userEntity = userDao.findUserById(id);
+ return userEntity;
+ }
+
+// /**
+// * 获取用户信息
+// */
+// public UserinfoDTO getUserInfo() {
+// //TODO 校验用户信息,并只需要返回必要信息,隐藏敏感信息
+// //获取用户信息
+// UserEntity userEntity = findUserById(getUserId());
+// UserinfoDTO userinfoDTO = new UserinfoDTO();
+// BeanUtils.copyProperties(userEntity, userinfoDTO);
+// return userinfoDTO;
+// }
+//
+//
+// /**
+// * 确认邮箱
+// */
+// @Transactional(rollbackFor = Exception.class)
+// public void confirm(String code) {
+// UserCredentialEntity userCredential = userCredentialService.getByCheckCode(code);
+// if (userCredential == null) {
+// throw new RocketPTException("校验码不正确");
+// }
+// Integer id = userCredential.getId();
+// UserEntity entity = getById(id);
+// if (!entity.getState().equals(2)) {
+// throw new RocketPTException("用户状态不正确");
+// }
+//
+// entity.setState(0);
+// updateById(entity);
+// userCredentialService.resetCheckCode(id);
+//
+// }
+//
+//
+// /**
+// * 忘记密码
+// */
+// @Transactional(rollbackFor = Exception.class)
+// public void forgotPassword(ForgotPasswordParam param) {
+// UserEntity entity = getByEmail(param.getEmail());
+// if (entity == null) {
+// throw new RocketPTException("邮箱不正确");
+// }
+//
+// if (!entity.getState().equals(0)) {
+// throw new RocketPTException("用户状态不正确");
+// }
+// String checkCode = userCredentialService.resetCheckCode(entity.getId());
+// //发邮件
+// mailService.sendMail(param.getEmail(),
+// I18nMessage.getMessage("confirm_title"),
+// I18nMessage.getMessage("confirm_email") + checkCode,
+// null);
+// }
+//
+//
+// /**
+// * 更新密码
+// */
+// @Transactional(rollbackFor = Exception.class)
+// public void changePassword(ChangePasswordParam param) {
+// // 获取用户凭证实体
+// Integer userId = getUserId();
+// UserCredentialEntity credentialEntity = userCredentialService.getById(userId);
+//
+// // 生成旧密码的哈希值
+// String old = userCredentialService.generate(param.getOldPassword(),
+// credentialEntity.getSalt());
+//
+// // 获取数据库中保存的密码
+// String password = credentialEntity.getPassword();
+//
+// // 检查旧密码是否正确
+// if (!old.equals(password)) {
+// // 抛出用户异常,表示未授权访问
+// throw new UserException(CommonResultStatus.UNAUTHORIZED, "密码不正确");
+// }
+//
+// // 更新用户凭证实体中的密码
+// userCredentialService.updatePassword(userId, param.getNewPassword(),
+// credentialEntity.getSalt());
+//
+// }
+//
+//
+// /**
+// * 重置密码
+// */
+// @Transactional(rollbackFor = Exception.class)
+// public void resetPassword(ResetPasswordParam param) {
+// String code = param.getCheckCode();
+// UserCredentialEntity userCredential = userCredentialService.getByCheckCode(code);
+// if (userCredential == null) {
+// throw new RocketPTException("校验码不正确");
+// }
+// Integer userId = userCredential.getId();
+// UserEntity entity = getById(userId);
+// if (!entity.getState().equals(0)) {
+// throw new RocketPTException("用户状态不正确");
+// }
+//
+// // 更新用户凭证实体中的密码
+// userCredentialService.updatePassword(userId, param.getNewPassword(),
+// userCredential.getSalt());
+//
+// userCredentialService.resetCheckCode(userId);
+//
+// }
+//
+//
+ public String getPasskey(Integer id) {
+ UserCredentialEntity entity =
+ userCredentialService.getByUserId(id);
+ return entity.getPasskey();
+ }
+//
+// /**
+// * 验证图片验证码和邀请码正确后发送注册验证码邮件
+// *
+// * @param param
+// */
+// public void sendRegCode(RegisterCodeParam param) {
+// //TODO 判断处理注册类型
+// // 1.开放注册
+// // 2.受邀注册
+// // 3.自助答题注册
+// //验证图片验证码和邀请码正确后发送邮件
+// if (!captchaService.verifyCaptcha(param.getUuid(), param.getCode())) {
+// throw new RocketPTException("图片验证码错误");
+// }
+// if (param.getType() != 1) {
+// // 检查邀请码是否有效
+// if (!invitationService.check(param.getEmail(), param.getInvitationCode())) {
+// throw new RocketPTException("邀请码错误");
+// }
+// }
+// emailCodeService.setMailCode(param.getEmail());
+//
+// }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/CheaterValidator.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/CheaterValidator.java
new file mode 100644
index 0000000..72c1b6e
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/CheaterValidator.java
@@ -0,0 +1,22 @@
+package com.ruoyi.web.Server.validator;
+
+import com.ruoyi.web.dao.BT.AnnounceRequest;
+import org.springframework.stereotype.Component;
+
+
+@Component
+public class CheaterValidator implements TrackerValidator {
+
+
+ @Override
+ public void validate(AnnounceRequest request) {
+
+ //todo 作弊检查 该部分是否需要开源?
+
+ }
+
+ @Override
+ public int getOrder() {
+ return 1;
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/DuplicatedAnnounceValidator.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/DuplicatedAnnounceValidator.java
new file mode 100644
index 0000000..c95b9ee
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/DuplicatedAnnounceValidator.java
@@ -0,0 +1,22 @@
+package com.ruoyi.web.Server.validator;
+
+import com.ruoyi.web.dao.BT.AnnounceRequest;
+import org.springframework.stereotype.Component;
+
+
+@Component
+public class DuplicatedAnnounceValidator implements TrackerValidator {
+
+
+ @Override
+ public void validate(AnnounceRequest request) {
+
+ //todo 验证重复请求
+
+ }
+
+ @Override
+ public int getOrder() {
+ return 102;
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/IPValidator.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/IPValidator.java
new file mode 100644
index 0000000..f09f227
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/IPValidator.java
@@ -0,0 +1,22 @@
+package com.ruoyi.web.Server.validator;
+
+import com.ruoyi.web.dao.BT.AnnounceRequest;
+import org.springframework.stereotype.Component;
+
+
+@Component
+public class IPValidator implements TrackerValidator {
+
+
+ @Override
+ public void validate(AnnounceRequest request) {
+
+ //todo 验证IP
+
+ }
+
+ @Override
+ public int getOrder() {
+ return 1;
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/NumWantValidator.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/NumWantValidator.java
new file mode 100644
index 0000000..d668e43
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/NumWantValidator.java
@@ -0,0 +1,35 @@
+package com.ruoyi.web.Server.validator;
+
+
+import com.ruoyi.web.dao.BT.AnnounceRequest;
+import org.springframework.stereotype.Component;
+
+@Component
+public class NumWantValidator implements TrackerValidator {
+
+
+ final int defaultNumWant = 200;
+ final int maxNunWant = 2000;
+
+ @Override
+ public void validate(AnnounceRequest request) {
+ // 设置默认值
+ if (request.getNumwant() == null || request.getNumwant().equals(-1)) {
+ request.setNumwant(defaultNumWant);
+ return;
+ }
+
+ // 小于0
+ request.setNumwant(Math.max(0, request.getNumwant()));
+
+ // 大于最大值
+ request.setNumwant(Math.min(request.getNumwant(), maxNunWant));
+
+ }
+
+ @Override
+ public int getOrder() {
+ return 1;
+ }
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/PasskeyValidator.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/PasskeyValidator.java
new file mode 100644
index 0000000..419d6cb
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/PasskeyValidator.java
@@ -0,0 +1,26 @@
+package com.ruoyi.web.Server.validator;
+
+import com.ruoyi.web.controller.common.exception.TrackerNoRetryException;
+import com.ruoyi.web.dao.BT.AnnounceRequest;
+import org.springframework.stereotype.Component;
+
+
+@Component
+public class PasskeyValidator implements TrackerValidator {
+
+
+ @Override
+ public void validate(AnnounceRequest request) {
+
+ //todo 验证Passkey, 增加签名算法
+ int length = request.getPasskey().length();
+ if (length > 64 || length < 16) {
+ throw new TrackerNoRetryException("Invalid passkey. QAQ");
+ }
+ }
+
+ @Override
+ public int getOrder() {
+ return 2;
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/PortValidator.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/PortValidator.java
new file mode 100644
index 0000000..600493e
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/PortValidator.java
@@ -0,0 +1,43 @@
+package com.ruoyi.web.Server.validator;
+
+import com.ruoyi.web.controller.common.exception.TrackerException;
+import com.ruoyi.web.dao.BT.AnnounceRequest;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+
+@Component
+public class PortValidator implements TrackerValidator {
+
+
+ List<Integer> banned = List.of(411, 412, 413,
+ 6881, 6882, 6883, 6884, 6885, 6886, 6887, 6888, 6889, 1214, 6346, 6347, 4662, 6699);
+
+ @Override
+ public void validate(AnnounceRequest request) {
+
+ if (request.getPort() == null) {
+ throw new TrackerException("Port null");
+ }
+
+ if (request.getPort() <= 0 || request.getPort() >= 65535) {
+ throw new TrackerException("Port " + request.getPort() + " is invalid");
+ }
+
+
+ for (Integer port : banned) {
+
+ if (port.equals(request.getPort())) {
+ throw new TrackerException("Port " + request.getPort() + " is banned.");
+ }
+
+ }
+
+ }
+
+ @Override
+ public int getOrder() {
+ return 1;
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/TorrentValidator.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/TorrentValidator.java
new file mode 100644
index 0000000..7fcbaa0
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/TorrentValidator.java
@@ -0,0 +1,42 @@
+package com.ruoyi.web.Server.validator;
+
+import com.ruoyi.web.Server.BT.TorrentService;
+import com.ruoyi.web.controller.common.exception.TrackerException;
+import com.ruoyi.web.domain.BT.TorrentEntity;
+import com.ruoyi.web.dao.BT.AnnounceRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+
+@RequiredArgsConstructor
+@Component
+public class TorrentValidator implements TrackerValidator {
+
+
+ final TorrentService torrentService;
+
+ @Override
+ public void validate(AnnounceRequest request) {
+ TorrentEntity torrentEntity = torrentService.getByInfoHash(request.getInfoHash());
+
+ if (torrentEntity == null) {
+ throw new TrackerException("Torrent is not authorized for use on this tracker");
+ }
+
+
+ if (!torrentEntity.isStatusOK()) {
+ throw new TrackerException("Torrent status not ok, please keep seeding.");
+ }
+
+
+ request.setTorrent(torrentEntity);
+
+ }
+
+ @Override
+ public int getOrder() {
+ return 101;
+ }
+
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/TrackerValidator.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/TrackerValidator.java
new file mode 100644
index 0000000..8cae8b3
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/TrackerValidator.java
@@ -0,0 +1,22 @@
+package com.ruoyi.web.Server.validator;
+
+import com.ruoyi.web.dao.BT.AnnounceRequest;
+
+public interface TrackerValidator {
+
+ /**
+ * 验证
+ *
+ * @param object
+ */
+ void validate(AnnounceRequest object);
+
+ /**
+ * 优先校验非IO操作
+ *
+ * @return 执行顺序,小的先执行
+ */
+ default int getOrder() {
+ return 0;
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/UserAgentValidator.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/UserAgentValidator.java
new file mode 100644
index 0000000..79c632c
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/UserAgentValidator.java
@@ -0,0 +1,23 @@
+package com.ruoyi.web.Server.validator;
+
+import com.ruoyi.web.dao.BT.AnnounceRequest;
+import org.springframework.stereotype.Component;
+
+
+@Component
+public class UserAgentValidator implements TrackerValidator {
+
+
+ @Override
+ public void validate(AnnounceRequest request) {
+
+ //todo 验证客户端UserAgent 只允许白名单
+
+
+ }
+
+ @Override
+ public int getOrder() {
+ return 3;
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/UserValidator.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/UserValidator.java
new file mode 100644
index 0000000..86159a7
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/UserValidator.java
@@ -0,0 +1,43 @@
+package com.ruoyi.web.Server.validator;
+
+import com.ruoyi.web.controller.common.exception.TrackerException;
+import com.ruoyi.web.dao.sys.UserDao;
+import com.ruoyi.web.domain.sys.UserEntity;
+import com.ruoyi.web.dao.BT.AnnounceRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+
+@RequiredArgsConstructor
+@Component
+public class UserValidator implements TrackerValidator {
+
+
+ final UserDao userDao;
+
+ @Override
+ public void validate(AnnounceRequest request) {
+
+ UserEntity user = userDao.findUserByPasskey(request.getPasskey());
+
+ if (user == null) {
+ throw new TrackerException("passkey invalid, pls redownload the torrent file.");
+ }
+
+
+ if (!user.isUserOK()) {
+ throw new TrackerException("user invalid");
+ }
+
+
+ request.setUser(user);
+
+ }
+
+ @Override
+ public int getOrder() {
+ return 100;
+ }
+
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/ValidationManager.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/ValidationManager.java
new file mode 100644
index 0000000..51df926
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Server/validator/ValidationManager.java
@@ -0,0 +1,33 @@
+package com.ruoyi.web.Server.validator;
+
+
+import com.ruoyi.web.dao.BT.AnnounceRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class ValidationManager {
+
+ List<TrackerValidator> validators;
+
+ @Autowired
+ public ValidationManager(List<TrackerValidator> validatorList) {
+ this.validators = validatorList.stream()
+ .sorted(Comparator.comparing(TrackerValidator::getOrder))
+ .collect(Collectors.toList());
+ }
+
+
+ public void validate(AnnounceRequest object) {
+ for (TrackerValidator validator : validators) {
+ validator.validate(object);
+ }
+ }
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/BencodeUtil.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/BencodeUtil.java
new file mode 100644
index 0000000..588fa80
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/BencodeUtil.java
@@ -0,0 +1,127 @@
+package com.ruoyi.web.Tool.BT;
+
+import com.dampcake.bencode.BencodeOutputStream;
+import com.ruoyi.web.controller.common.exception.TrackerException;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 编解码
+ */
+public class BencodeUtil {
+
+ public static final String exceptionMsg = "Oops! Your request is like an alien language to us, we " +
+ "can't encode it!";
+
+ public interface Errors {
+ String CLIENT_ERROR = BencodeUtil.error("Oops! Your request is like an alien language to " +
+ "us, we can't decode it!");
+ String INTERNAL_ERROR_60 = BencodeUtil.error("Give us a moment, try again later!", 60);
+ String INTERNAL_ERROR_120 = BencodeUtil.error("Hold your horses! then try again later!",
+ 120);
+
+
+ }
+
+ public static String error(String reason) {
+ return encode(Map.of("failure reason", reason));
+ }
+
+ public static String errorNoRetry(String reason) {
+ return encode(Map.of("failure reason", reason,
+ "retry in", "never"
+ ));
+ }
+
+ public static String warning(String reason) {
+ return encode(Map.of("warning message", reason));
+ }
+
+ public static String error() {
+ return Errors.CLIENT_ERROR;
+ }
+
+
+ public static String error(String reason, Integer retry) {
+ return encode(Map.of("failure reason", reason, "retry in", retry));
+ }
+
+ public static <K, V> String encode(Map<K, V> data) {
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ try (BencodeOutputStream bencoder = new BencodeOutputStream(out)) {
+ bencoder.writeDictionary(data);
+ return out.toString();
+ }
+ } catch (IOException e) {
+ throw new TrackerException(exceptionMsg, e);
+ }
+ }
+
+ public static String encode(Integer data) {
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ try (BencodeOutputStream bencoder = new BencodeOutputStream(out)) {
+ bencoder.write(data);
+ return out.toString();
+ }
+ } catch (IOException e) {
+ throw new TrackerException(exceptionMsg, e);
+ }
+ }
+
+ public static String encode(Number data) {
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ try (BencodeOutputStream bencoder = new BencodeOutputStream(out)) {
+ bencoder.writeNumber(data);
+ return out.toString();
+ }
+ } catch (IOException e) {
+ throw new TrackerException(exceptionMsg, e);
+ }
+ }
+
+ public static String encode(byte[] data) {
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ try (BencodeOutputStream bencoder = new BencodeOutputStream(out)) {
+ bencoder.write(data);
+ return out.toString();
+ }
+ } catch (IOException e) {
+ throw new TrackerException(exceptionMsg, e);
+ }
+ }
+
+ public static String encode(byte[] data, int offset, int length) {
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ try (BencodeOutputStream bencoder = new BencodeOutputStream(out)) {
+ bencoder.write(data, offset, length);
+ return out.toString();
+ }
+ } catch (IOException e) {
+ throw new TrackerException(exceptionMsg, e);
+ }
+ }
+
+ public static String encode(String data) {
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ try (BencodeOutputStream bencoder = new BencodeOutputStream(out)) {
+ bencoder.writeString(data);
+ return out.toString();
+ }
+ } catch (IOException e) {
+ throw new TrackerException(exceptionMsg, e);
+ }
+ }
+
+ public static <E> String encode(List<E> data) {
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ try (BencodeOutputStream bencoder = new BencodeOutputStream(out)) {
+ bencoder.writeList(data);
+ return out.toString();
+ }
+ } catch (IOException e) {
+ throw new TrackerException(exceptionMsg, e);
+ }
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/BinaryFieldUtil.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/BinaryFieldUtil.java
new file mode 100644
index 0000000..b0927b6
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/BinaryFieldUtil.java
@@ -0,0 +1,51 @@
+package com.ruoyi.web.Tool.BT;
+
+
+import com.ruoyi.web.controller.common.exception.TrackerException;
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.net.URLCodec;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class BinaryFieldUtil {
+ interface RegexPattern {
+ Pattern INFO_HASH = Pattern.compile("(?)info_hash=([^&]*)");
+ Pattern PEER_ID = Pattern.compile("(?)peer_id=([^&]*)");
+ }
+
+ static private final URLCodec CODEC = new URLCodec();
+
+ static public List<byte[]> matchInfoHashesHex(String queryStrings) {
+ return RegexPattern.INFO_HASH.matcher(queryStrings)
+ .results()
+ .map(x -> x.group(1))
+ .map(BinaryFieldUtil::decodeUrl)
+ .toList();
+ }
+
+ static public byte[] matchInfoHash(String queryStrings) {
+ return RegexPattern.INFO_HASH.matcher(queryStrings)
+ .results()
+ .map(x -> x.group(1))
+ .map(BinaryFieldUtil::decodeUrl)
+ .findFirst().orElseThrow(() -> new TrackerException("missing field info_hash"));
+ }
+
+ static public byte[] matchPeerId(String queryStrings) {
+ return RegexPattern.PEER_ID.matcher(queryStrings)
+ .results()
+ .map(x -> x.group(1))
+ .map(BinaryFieldUtil::decodeUrl)
+ .findFirst()
+ .orElseThrow(() -> new TrackerException("missing field peer_id"));
+ }
+
+ static private byte[] decodeUrl(String url) {
+ try {
+ return CODEC.decode(url.getBytes());
+ } catch (DecoderException e) {
+ throw new TrackerException("unable to decode input data " + url, e);
+ }
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/CollectionUtils.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/CollectionUtils.java
new file mode 100644
index 0000000..2b9cafb
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/CollectionUtils.java
@@ -0,0 +1,104 @@
+package com.ruoyi.web.Tool.BT;
+
+import java.util.*;
+
+/**
+ * Static helper class for use dealing with Collections.
+ *
+ * @since 0.9
+ */
+public class CollectionUtils {
+
+ //TODO - complete JavaDoc
+
+ public static <E> Set<E> asSet(E... elements) {
+ if (elements == null || elements.length == 0) {
+ return Collections.emptySet();
+ }
+
+ if (elements.length == 1) {
+ return Collections.singleton(elements[0]);
+ }
+
+ LinkedHashSet<E> set = new LinkedHashSet<E>(elements.length * 4 / 3 + 1);
+ Collections.addAll(set, elements);
+ return set;
+ }
+
+ /**
+ * Returns {@code true} if the specified {@code Collection} is {@code null} or
+ * {@link Collection#isEmpty empty},
+ * {@code false} otherwise.
+ *
+ * @param c the collection to check
+ * @return {@code true} if the specified {@code Collection} is {@code null} or
+ * {@link Collection#isEmpty empty},
+ * {@code false} otherwise.
+ * @since 1.0
+ */
+ public static boolean isEmpty(Collection c) {
+ return c == null || c.isEmpty();
+ }
+
+ /**
+ * Returns {@code true} if the specified {@code Map} is {@code null} or {@link Map#isEmpty
+ * empty},
+ * {@code false} otherwise.
+ *
+ * @param m the {@code Map} to check
+ * @return {@code true} if the specified {@code Map} is {@code null} or {@link Map#isEmpty
+ * empty},
+ * {@code false} otherwise.
+ * @since 1.0
+ */
+ public static boolean isEmpty(Map m) {
+ return m == null || m.isEmpty();
+ }
+
+ /**
+ * Returns the size of the specified collection or {@code 0} if the collection is {@code null}.
+ *
+ * @param c the collection to check
+ * @return the size of the specified collection or {@code 0} if the collection is {@code null}.
+ * @since 1.2
+ */
+ public static int size(Collection c) {
+ return c != null ? c.size() : 0;
+ }
+
+ /**
+ * Returns the size of the specified map or {@code 0} if the map is {@code null}.
+ *
+ * @param m the map to check
+ * @return the size of the specified map or {@code 0} if the map is {@code null}.
+ * @since 1.2
+ */
+ public static int size(Map m) {
+ return m != null ? m.size() : 0;
+ }
+
+ public static <E> List<E> asList(E... elements) {
+ if (elements == null || elements.length == 0) {
+ return Collections.emptyList();
+ }
+
+ // Integer overflow does not occur when a large array is passed in because the list array
+ // already exists
+ return Arrays.asList(elements);
+ }
+
+ /*public static <E> Deque<E> asDeque(E... elements) {
+ if (elements == null || elements.length == 0) {
+ return new ArrayDeque<E>();
+ }
+ // Avoid integer overflow when a large array is passed in
+ int capacity = computeListCapacity(elements.length);
+ ArrayDeque<E> deque = new ArrayDeque<E>(capacity);
+ Collections.addAll(deque, elements);
+ return deque;
+ }*/
+
+ static int computeListCapacity(int arraySize) {
+ return (int) Math.min(5L + arraySize + (arraySize / 10), Integer.MAX_VALUE);
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/FileUtil.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/FileUtil.java
new file mode 100644
index 0000000..a2470e8
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/FileUtil.java
@@ -0,0 +1,12 @@
+package com.ruoyi.web.Tool.BT;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+public class FileUtil {
+ public static void writeBytesToFile(byte[] data, String filePath) throws IOException {
+ try (FileOutputStream fos = new FileOutputStream(filePath)) {
+ fos.write(data);
+ }
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/IPUtils.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/IPUtils.java
new file mode 100644
index 0000000..16c27ae
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/IPUtils.java
@@ -0,0 +1,96 @@
+package com.ruoyi.web.Tool.BT;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+/**
+ * 获取请求主机IP地址工具
+ *
+ * @author pt
+ */
+@Slf4j
+public class IPUtils {
+
+ /**
+ * 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址;
+ * <p>
+ * x-real-ip:171.111
+ * x-forwarded-for:171.1.157.11
+ * remote-host:171.111.157.11
+ * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
+ * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
+ */
+ public static String getIpAddr() {
+
+ HttpServletRequest request =
+ ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
+
+
+ //打印头信息
+// Enumeration enums = request.getHeaderNames();
+// while (enums.hasMoreElements()) {
+// String headerName = (String) enums.nextElement();
+// String headerValue = request.getHeader(headerName);
+// log.info(headerName + ":" + headerValue);
+// }
+
+
+ return getIpAddr(request);
+ }
+
+ /**
+ * 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址;
+ * <p>
+ * x-real-ip:171.111
+ * x-forwarded-for:171.1.157.11
+ * remote-host:171.111.157.11
+ * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址
+ * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址
+ */
+ public static String getIpAddr(HttpServletRequest request) {
+
+ String ip = null;
+ try {
+ ip = request.getHeader("x-real-ip");
+
+
+ if (isUnknownIp(ip)) {
+ ip = request.getHeader("x-forwarded-for");
+
+ }
+ if (isUnknownIp(ip)) {
+ ip = request.getHeader("remote-host");
+ }
+ if (isUnknownIp(ip)) {
+ ip = request.getHeader("Proxy-Client-IP");
+
+ }
+ if (isUnknownIp(ip)) {
+ ip = request.getHeader("HTTP_X_FORWARDED_FOR");
+ }
+ if (isUnknownIp(ip)) {
+ ip = request.getRemoteAddr();
+ }
+ } catch (Exception e) {
+ log.error("IPUtils ERROR ", e);
+ }
+
+ //使用代理,则获取第一个IP地址
+ if (StringUtils.isNotBlank(ip) && ip.length() > 15) {
+ if (ip.indexOf(",") > 0) {
+ ip = ip.substring(0, ip.indexOf(","));
+ }
+ }
+
+ return ip;
+ }
+
+
+ private static boolean isUnknownIp(String ip) {
+ return StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip);
+ }
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/JsonTypeHandler.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/JsonTypeHandler.java
new file mode 100644
index 0000000..83f0623
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/JsonTypeHandler.java
@@ -0,0 +1,70 @@
+package com.ruoyi.web.Tool.BT;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.apache.ibatis.type.BaseTypeHandler;
+import org.apache.ibatis.type.JdbcType;
+
+import java.io.IOException;
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class JsonTypeHandler extends BaseTypeHandler<Map<String, Object>> {
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ static {
+ objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ objectMapper.registerModule(new JavaTimeModule());
+ }
+
+ @Override
+ public void setNonNullParameter(PreparedStatement ps, int i,
+ Map<String, Object> parameter, JdbcType jdbcType)
+ throws SQLException {
+ try {
+ ps.setString(i, objectMapper.writeValueAsString(parameter));
+ } catch (JsonProcessingException e) {
+ throw new SQLException("Error converting map to JSON", e);
+ }
+ }
+
+ @Override
+ public Map<String, Object> getNullableResult(ResultSet rs, String columnName)
+ throws SQLException {
+ String json = rs.getString(columnName);
+ return parseJsonToMap(json);
+ }
+
+ @Override
+ public Map<String, Object> getNullableResult(ResultSet rs, int columnIndex)
+ throws SQLException {
+ String json = rs.getString(columnIndex);
+ return parseJsonToMap(json);
+ }
+
+ @Override
+ public Map<String, Object> getNullableResult(CallableStatement cs, int columnIndex)
+ throws SQLException {
+ String json = cs.getString(columnIndex);
+ return parseJsonToMap(json);
+ }
+
+ private Map<String, Object> parseJsonToMap(String json) {
+ if (json == null || json.isEmpty()) {
+ return new HashMap<>();
+ }
+ try {
+ return objectMapper.readValue(json,
+ new TypeReference<Map<String, Object>>() {});
+ } catch (IOException e) {
+ throw new RuntimeException("Error parsing JSON to map", e);
+ }
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/JsonUtils.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/JsonUtils.java
new file mode 100644
index 0000000..f4e1a59
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/JsonUtils.java
@@ -0,0 +1,93 @@
+package com.ruoyi.web.Tool.BT;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+
+import java.io.IOException;
+import java.util.*;
+
+
+/**
+ * @author Jrx
+ */
+public class JsonUtils {
+
+ private static final JsonMapper JSON;
+
+ static {
+ JsonMapper.Builder builder = new JsonMapper().rebuild();
+ builder.serializationInclusion(JsonInclude.Include.NON_NULL);
+// JSON.configure(SerializationFeature.INDENT_OUTPUT, false);
+ //不显示为null的字段
+ builder.serializationInclusion(JsonInclude.Include.NON_NULL);
+ //序列化枚举是以ordinal()来输出
+ builder.addModule(new JavaTimeModule());
+ builder.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+ builder.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+ builder.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
+// SimpleModule module = new SimpleModule();
+// module.addSerializer(EntityBaseSerializer.instance);
+// builder.addModule(module);
+ JSON = builder.build();
+ }
+
+ public static String stringify(Object obj) {
+ try {
+ return JSON.writeValueAsString(obj);
+ } catch (JsonProcessingException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public static <T> T parseToObject(String json, Class<T> clz) {
+ try {
+ return JSON.readValue(json, clz);
+ } catch (IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public static Map<String, Object> parseToMap(String json) {
+ try {
+ return JSON.readValue(json, new TypeReference<>() {
+ });
+ } catch (IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public static <T> List<T> parseToList(String json, Class<T> clz) {
+ try {
+ List<T> result = new ArrayList<>();
+ JsonNode jsonNode = JSON.readTree(json);
+ for (JsonNode itemNode : jsonNode) {
+ result.add(JSON.readValue(itemNode.toString(), clz));
+ }
+ return result;
+ } catch (IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public static <T> Set<T> parseToSet(String json, Class<T> clz) {
+ try {
+ Set<T> result = new HashSet<>();
+ JsonNode jsonNode = JSON.readTree(json);
+ for (JsonNode itemNode : jsonNode) {
+ result.add(JSON.readValue(itemNode.toString(), clz));
+ }
+ return result;
+ } catch (IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/RedisUtil.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/RedisUtil.java
new file mode 100644
index 0000000..5ca505a
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/RedisUtil.java
@@ -0,0 +1,1332 @@
+package com.ruoyi.web.Tool.BT;
+
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.connection.DataType;
+import org.springframework.data.redis.core.Cursor;
+import org.springframework.data.redis.core.ScanOptions;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.ZSetOperations.TypedTuple;
+import org.springframework.stereotype.Component;
+
+import java.util.*;
+import java.util.Map.Entry;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Redis工具类
+ * RedisTemplate和StringRedisTemplate
+ * 二者主要区别是他们使用的序列化类不一样,RedisTemplate使用的是JdkSerializationRedisSerializer,
+ * StringRedisTemplate使用的是StringRedisSerializer,两者的数据是不共通的。
+ */
+@Component
+@RequiredArgsConstructor
+public class RedisUtil {
+
+ @Getter
+ final StringRedisTemplate redisTemplate;
+
+ /** -------------------key相关操作--------------------- */
+
+ /**
+ * 删除key
+ *
+ * @param key
+ */
+ public void delete(String key) {
+ redisTemplate.delete(key);
+ }
+
+ /**
+ * 批量删除key
+ *
+ * @param keys
+ */
+ public void delete(Collection<String> keys) {
+ redisTemplate.delete(keys);
+ }
+
+ /**
+ * 序列化key
+ *
+ * @param key
+ * @return
+ */
+ public byte[] dump(String key) {
+ return redisTemplate.dump(key);
+ }
+
+ /**
+ * 是否存在key
+ *
+ * @param key
+ * @return
+ */
+ public Boolean hasKey(String key) {
+ return redisTemplate.hasKey(key);
+ }
+
+ /**
+ * 设置过期时间
+ *
+ * @param key
+ * @param timeout
+ * @param unit
+ * @return
+ */
+ public Boolean expire(String key, long timeout, TimeUnit unit) {
+ return redisTemplate.expire(key, timeout, unit);
+ }
+
+ /**
+ * 设置过期时间
+ *
+ * @param key
+ * @param date
+ * @return
+ */
+ public Boolean expireAt(String key, Date date) {
+ return redisTemplate.expireAt(key, date);
+ }
+
+ /**
+ * 查找匹配的key
+ *
+ * @param pattern
+ * @return
+ */
+ public Set<String> keys(String pattern) {
+ return redisTemplate.keys(pattern);
+ }
+
+ /**
+ * 将当前数据库的 key 移动到给定的数据库 db 当中
+ *
+ * @param key
+ * @param dbIndex
+ * @return
+ */
+ public Boolean move(String key, int dbIndex) {
+ return redisTemplate.move(key, dbIndex);
+ }
+
+ /**
+ * 移除 key 的过期时间,key 将持久保持
+ *
+ * @param key
+ * @return
+ */
+ public Boolean persist(String key) {
+ return redisTemplate.persist(key);
+ }
+
+ /**
+ * 返回 key 的剩余的过期时间
+ *
+ * @param key
+ * @param unit
+ * @return
+ */
+ public Long getExpire(String key, TimeUnit unit) {
+ return redisTemplate.getExpire(key, unit);
+ }
+
+ /**
+ * 返回 key 的剩余的过期时间
+ *
+ * @param key
+ * @return
+ */
+ public Long getExpire(String key) {
+ return redisTemplate.getExpire(key);
+ }
+
+ /**
+ * 从当前数据库中随机返回一个 key
+ *
+ * @return
+ */
+ public String randomKey() {
+ return redisTemplate.randomKey();
+ }
+
+ /**
+ * 修改 key 的名称
+ *
+ * @param oldKey
+ * @param newKey
+ */
+ public void rename(String oldKey, String newKey) {
+ redisTemplate.rename(oldKey, newKey);
+ }
+
+ /**
+ * 仅当 newkey 不存在时,将 oldKey 改名为 newkey
+ *
+ * @param oldKey
+ * @param newKey
+ * @return
+ */
+ public Boolean renameIfAbsent(String oldKey, String newKey) {
+ return redisTemplate.renameIfAbsent(oldKey, newKey);
+ }
+
+ /**
+ * 返回 key 所储存的值的类型
+ *
+ * @param key
+ * @return
+ */
+ public DataType type(String key) {
+ return redisTemplate.type(key);
+ }
+
+ /** -------------------string相关操作--------------------- */
+
+ /**
+ * 设置指定 key 的值
+ *
+ * @param key
+ * @param value
+ */
+ public void set(String key, String value) {
+ redisTemplate.opsForValue().set(key, value);
+ }
+
+ /**
+ * 获取指定 key 的值
+ *
+ * @param key
+ * @return
+ */
+ public String get(String key) {
+ return redisTemplate.opsForValue().get(key);
+ }
+
+ /**
+ * 返回 key 中字符串值的子字符
+ *
+ * @param key
+ * @param start
+ * @param end
+ * @return
+ */
+ public String getRange(String key, long start, long end) {
+ return redisTemplate.opsForValue().get(key, start, end);
+ }
+
+ /**
+ * 将给定 key 的值设为 value ,并返回 key 的旧值(old value)
+ *
+ * @param key
+ * @param value
+ * @return
+ */
+ public String getAndSet(String key, String value) {
+ return redisTemplate.opsForValue().getAndSet(key, value);
+ }
+
+ /**
+ * 对 key 所储存的字符串值,获取指定偏移量上的位(bit)
+ *
+ * @param key
+ * @param offset
+ * @return
+ */
+ public Boolean getBit(String key, long offset) {
+ return redisTemplate.opsForValue().getBit(key, offset);
+ }
+
+ /**
+ * 批量获取
+ *
+ * @param keys
+ * @return
+ */
+ public List<String> multiGet(Collection<String> keys) {
+ return redisTemplate.opsForValue().multiGet(keys);
+ }
+
+ /**
+ * 设置ASCII码, 字符串'a'的ASCII码是97, 转为二进制是'01100001', 此方法是将二进制第offset位值变为value
+ *
+ * @param key
+ * @param postion 位置
+ * @param value 值,true为1, false为0
+ * @return
+ */
+ public boolean setBit(String key, long offset, boolean value) {
+ return redisTemplate.opsForValue().setBit(key, offset, value);
+ }
+
+ /**
+ * 将值 value 关联到 key ,并将 key 的过期时间设为 timeout
+ *
+ * @param key
+ * @param value
+ * @param timeout 过期时间
+ * @param unit 时间单位, 天:TimeUnit.DAYS 小时:TimeUnit.HOURS 分钟:TimeUnit.MINUTES
+ * 秒:TimeUnit.SECONDS 毫秒:TimeUnit.MILLISECONDS
+ */
+ public void setEx(String key, String value, long timeout, TimeUnit unit) {
+ redisTemplate.opsForValue().set(key, value, timeout, unit);
+ }
+
+ /**
+ * 只有在 key 不存在时设置 key 的值
+ *
+ * @param key
+ * @param value
+ * @return 之前已经存在返回false, 不存在返回true
+ */
+ public boolean setIfAbsent(String key, String value) {
+ return redisTemplate.opsForValue().setIfAbsent(key, value);
+ }
+
+ /**
+ * 用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始
+ *
+ * @param key
+ * @param value
+ * @param offset 从指定位置开始覆写
+ */
+ public void setRange(String key, String value, long offset) {
+ redisTemplate.opsForValue().set(key, value, offset);
+ }
+
+ /**
+ * 获取字符串的长度
+ *
+ * @param key
+ * @return
+ */
+ public Long size(String key) {
+ return redisTemplate.opsForValue().size(key);
+ }
+
+ /**
+ * 批量添加
+ *
+ * @param maps
+ */
+ public void multiSet(Map<String, String> maps) {
+ redisTemplate.opsForValue().multiSet(maps);
+ }
+
+ /**
+ * 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在
+ *
+ * @param maps
+ * @return 之前已经存在返回false, 不存在返回true
+ */
+ public boolean multiSetIfAbsent(Map<String, String> maps) {
+ return redisTemplate.opsForValue().multiSetIfAbsent(maps);
+ }
+
+ /**
+ * 增加(自增长), 负数则为自减
+ *
+ * @param key
+ * @param value
+ * @return
+ */
+ public Long incrBy(String key, long increment) {
+ return redisTemplate.opsForValue().increment(key, increment);
+ }
+
+ /**
+ * @param key
+ * @param value
+ * @return
+ */
+ public Double incrByFloat(String key, double increment) {
+ return redisTemplate.opsForValue().increment(key, increment);
+ }
+
+ /**
+ * 追加到末尾
+ *
+ * @param key
+ * @param value
+ * @return
+ */
+ public Integer append(String key, String value) {
+ return redisTemplate.opsForValue().append(key, value);
+ }
+
+ /** -------------------hash相关操作------------------------- */
+
+ /**
+ * 获取存储在哈希表中指定字段的值
+ *
+ * @param key
+ * @param field
+ * @return
+ */
+ public Object hGet(String key, String field) {
+ return redisTemplate.opsForHash().get(key, field);
+ }
+
+ /**
+ * 获取所有给定字段的值
+ *
+ * @param key
+ * @return
+ */
+ public Map<Object, Object> hGetAll(String key) {
+ return redisTemplate.opsForHash().entries(key);
+ }
+
+ /**
+ * 获取所有给定字段的值
+ *
+ * @param key
+ * @param fields
+ * @return
+ */
+ public List<Object> hMultiGet(String key, Collection<Object> fields) {
+ return redisTemplate.opsForHash().multiGet(key, fields);
+ }
+
+ public void hPut(String key, String hashKey, String value) {
+ redisTemplate.opsForHash().put(key, hashKey, value);
+ }
+
+ public void hPutAll(String key, Map<String, String> maps) {
+ redisTemplate.opsForHash().putAll(key, maps);
+ }
+
+ /**
+ * 仅当hashKey不存在时才设置
+ *
+ * @param key
+ * @param hashKey
+ * @param value
+ * @return
+ */
+ public Boolean hPutIfAbsent(String key, String hashKey, String value) {
+ return redisTemplate.opsForHash().putIfAbsent(key, hashKey, value);
+ }
+
+ /**
+ * 删除一个或多个哈希表字段
+ *
+ * @param key
+ * @param fields
+ * @return
+ */
+ public Long hDelete(String key, Object... fields) {
+ return redisTemplate.opsForHash().delete(key, fields);
+ }
+
+ /**
+ * 查看哈希表 key 中,指定的字段是否存在
+ *
+ * @param key
+ * @param field
+ * @return
+ */
+ public boolean hExists(String key, String field) {
+ return redisTemplate.opsForHash().hasKey(key, field);
+ }
+
+ /**
+ * 为哈希表 key 中的指定字段的整数值加上增量 increment
+ *
+ * @param key
+ * @param field
+ * @param increment
+ * @return
+ */
+ public Long hIncrBy(String key, Object field, long increment) {
+ return redisTemplate.opsForHash().increment(key, field, increment);
+ }
+
+ /**
+ * 为哈希表 key 中的指定字段的整数值加上增量 increment
+ *
+ * @param key
+ * @param field
+ * @param delta
+ * @return
+ */
+ public Double hIncrByFloat(String key, Object field, double delta) {
+ return redisTemplate.opsForHash().increment(key, field, delta);
+ }
+
+ /**
+ * 获取所有哈希表中的字段
+ *
+ * @param key
+ * @return
+ */
+ public Set<Object> hKeys(String key) {
+ return redisTemplate.opsForHash().keys(key);
+ }
+
+ /**
+ * 获取哈希表中字段的数量
+ *
+ * @param key
+ * @return
+ */
+ public Long hSize(String key) {
+ return redisTemplate.opsForHash().size(key);
+ }
+
+ /**
+ * 获取哈希表中所有值
+ *
+ * @param key
+ * @return
+ */
+ public List<Object> hValues(String key) {
+ return redisTemplate.opsForHash().values(key);
+ }
+
+ /**
+ * 迭代哈希表中的键值对
+ *
+ * @param key
+ * @param options
+ * @return
+ */
+ public Cursor<Entry<Object, Object>> hScan(String key, ScanOptions options) {
+ return redisTemplate.opsForHash().scan(key, options);
+ }
+
+ /** ------------------------list相关操作---------------------------- */
+
+ /**
+ * 通过索引获取列表中的元素
+ *
+ * @param key
+ * @param index
+ * @return
+ */
+ public String lIndex(String key, long index) {
+ return redisTemplate.opsForList().index(key, index);
+ }
+
+ /**
+ * 获取列表指定范围内的元素
+ *
+ * @param key
+ * @param start 开始位置, 0是开始位置
+ * @param end 结束位置, -1返回所有
+ * @return
+ */
+ public List<String> lRange(String key, long start, long end) {
+ return redisTemplate.opsForList().range(key, start, end);
+ }
+
+ /**
+ * 存储在list头部
+ *
+ * @param key
+ * @param value
+ * @return
+ */
+ public Long lLeftPush(String key, String value) {
+ return redisTemplate.opsForList().leftPush(key, value);
+ }
+
+ /**
+ * @param key
+ * @param value
+ * @return
+ */
+ public Long lLeftPushAll(String key, String... value) {
+ return redisTemplate.opsForList().leftPushAll(key, value);
+ }
+
+ /**
+ * @param key
+ * @param value
+ * @return
+ */
+ public Long lLeftPushAll(String key, Collection<String> value) {
+ return redisTemplate.opsForList().leftPushAll(key, value);
+ }
+
+ /**
+ * 当list存在的时候才加入
+ *
+ * @param key
+ * @param value
+ * @return
+ */
+ public Long lLeftPushIfPresent(String key, String value) {
+ return redisTemplate.opsForList().leftPushIfPresent(key, value);
+ }
+
+ /**
+ * 如果pivot存在,再pivot前面添加
+ *
+ * @param key
+ * @param pivot
+ * @param value
+ * @return
+ */
+ public Long lLeftPush(String key, String pivot, String value) {
+ return redisTemplate.opsForList().leftPush(key, pivot, value);
+ }
+
+ /**
+ * @param key
+ * @param value
+ * @return
+ */
+ public Long lRightPush(String key, String value) {
+ return redisTemplate.opsForList().rightPush(key, value);
+ }
+
+ /**
+ * @param key
+ * @param value
+ * @return
+ */
+ public Long lRightPushAll(String key, String... value) {
+ return redisTemplate.opsForList().rightPushAll(key, value);
+ }
+
+ /**
+ * @param key
+ * @param value
+ * @return
+ */
+ public Long lRightPushAll(String key, Collection<String> value) {
+ return redisTemplate.opsForList().rightPushAll(key, value);
+ }
+
+ /**
+ * 为已存在的列表添加值
+ *
+ * @param key
+ * @param value
+ * @return
+ */
+ public Long lRightPushIfPresent(String key, String value) {
+ return redisTemplate.opsForList().rightPushIfPresent(key, value);
+ }
+
+ /**
+ * 在pivot元素的右边添加值
+ *
+ * @param key
+ * @param pivot
+ * @param value
+ * @return
+ */
+ public Long lRightPush(String key, String pivot, String value) {
+ return redisTemplate.opsForList().rightPush(key, pivot, value);
+ }
+
+ /**
+ * 通过索引设置列表元素的值
+ *
+ * @param key
+ * @param index 位置
+ * @param value
+ */
+ public void lSet(String key, long index, String value) {
+ redisTemplate.opsForList().set(key, index, value);
+ }
+
+ /**
+ * 移出并获取列表的第一个元素
+ *
+ * @param key
+ * @return 删除的元素
+ */
+ public String lLeftPop(String key) {
+ return redisTemplate.opsForList().leftPop(key);
+ }
+
+ /**
+ * 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
+ *
+ * @param key
+ * @param timeout 等待时间
+ * @param unit 时间单位
+ * @return
+ */
+ public String lBLeftPop(String key, long timeout, TimeUnit unit) {
+ return redisTemplate.opsForList().leftPop(key, timeout, unit);
+ }
+
+ /**
+ * 移除并获取列表最后一个元素
+ *
+ * @param key
+ * @return 删除的元素
+ */
+ public String lRightPop(String key) {
+ return redisTemplate.opsForList().rightPop(key);
+ }
+
+ /**
+ * 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
+ *
+ * @param key
+ * @param timeout 等待时间
+ * @param unit 时间单位
+ * @return
+ */
+ public String lBRightPop(String key, long timeout, TimeUnit unit) {
+ return redisTemplate.opsForList().rightPop(key, timeout, unit);
+ }
+
+ /**
+ * 移除列表的最后一个元素,并将该元素添加到另一个列表并返回
+ *
+ * @param sourceKey
+ * @param destinationKey
+ * @return
+ */
+ public String lRightPopAndLeftPush(String sourceKey, String destinationKey) {
+ return redisTemplate.opsForList().rightPopAndLeftPush(sourceKey,
+ destinationKey);
+ }
+
+ /**
+ * 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
+ *
+ * @param sourceKey
+ * @param destinationKey
+ * @param timeout
+ * @param unit
+ * @return
+ */
+ public String lBRightPopAndLeftPush(String sourceKey, String destinationKey,
+ long timeout, TimeUnit unit) {
+ return redisTemplate.opsForList().rightPopAndLeftPush(sourceKey,
+ destinationKey, timeout, unit);
+ }
+
+ /**
+ * 删除集合中值等于value得元素
+ *
+ * @param key
+ * @param index index=0, 删除所有值等于value的元素; index>0, 从头部开始删除第一个值等于value的元素;
+ * index<0, 从尾部开始删除第一个值等于value的元素;
+ * @param value
+ * @return
+ */
+ public Long lRemove(String key, long index, String value) {
+ return redisTemplate.opsForList().remove(key, index, value);
+ }
+
+ /**
+ * 裁剪list
+ *
+ * @param key
+ * @param start
+ * @param end
+ */
+ public void lTrim(String key, long start, long end) {
+ redisTemplate.opsForList().trim(key, start, end);
+ }
+
+ /**
+ * 获取列表长度
+ *
+ * @param key
+ * @return
+ */
+ public Long lLen(String key) {
+ return redisTemplate.opsForList().size(key);
+ }
+
+ /** --------------------set相关操作-------------------------- */
+
+ /**
+ * set添加元素
+ *
+ * @param key
+ * @param values
+ * @return
+ */
+ public Long sAdd(String key, String... values) {
+ return redisTemplate.opsForSet().add(key, values);
+ }
+
+ /**
+ * set移除元素
+ *
+ * @param key
+ * @param values
+ * @return
+ */
+ public Long sRemove(String key, Object... values) {
+ return redisTemplate.opsForSet().remove(key, values);
+ }
+
+ /**
+ * 移除并返回集合的一个随机元素
+ *
+ * @param key
+ * @return
+ */
+ public String sPop(String key) {
+ return redisTemplate.opsForSet().pop(key);
+ }
+
+ /**
+ * 将元素value从一个集合移到另一个集合
+ *
+ * @param key
+ * @param value
+ * @param destKey
+ * @return
+ */
+ public Boolean sMove(String key, String value, String destKey) {
+ return redisTemplate.opsForSet().move(key, value, destKey);
+ }
+
+ /**
+ * 获取集合的大小
+ *
+ * @param key
+ * @return
+ */
+ public Long sSize(String key) {
+ return redisTemplate.opsForSet().size(key);
+ }
+
+ /**
+ * 判断集合是否包含value
+ *
+ * @param key
+ * @param value
+ * @return
+ */
+ public Boolean sIsMember(String key, Object value) {
+ return redisTemplate.opsForSet().isMember(key, value);
+ }
+
+ /**
+ * 获取两个集合的交集
+ *
+ * @param key
+ * @param otherKey
+ * @return
+ */
+ public Set<String> sIntersect(String key, String otherKey) {
+ return redisTemplate.opsForSet().intersect(key, otherKey);
+ }
+
+ /**
+ * 获取key集合与多个集合的交集
+ *
+ * @param key
+ * @param otherKeys
+ * @return
+ */
+ public Set<String> sIntersect(String key, Collection<String> otherKeys) {
+ return redisTemplate.opsForSet().intersect(key, otherKeys);
+ }
+
+ /**
+ * key集合与otherKey集合的交集存储到destKey集合中
+ *
+ * @param key
+ * @param otherKey
+ * @param destKey
+ * @return
+ */
+ public Long sIntersectAndStore(String key, String otherKey, String destKey) {
+ return redisTemplate.opsForSet().intersectAndStore(key, otherKey,
+ destKey);
+ }
+
+ /**
+ * key集合与多个集合的交集存储到destKey集合中
+ *
+ * @param key
+ * @param otherKeys
+ * @param destKey
+ * @return
+ */
+ public Long sIntersectAndStore(String key, Collection<String> otherKeys,
+ String destKey) {
+ return redisTemplate.opsForSet().intersectAndStore(key, otherKeys,
+ destKey);
+ }
+
+ /**
+ * 获取两个集合的并集
+ *
+ * @param key
+ * @param otherKeys
+ * @return
+ */
+ public Set<String> sUnion(String key, String otherKeys) {
+ return redisTemplate.opsForSet().union(key, otherKeys);
+ }
+
+ /**
+ * 获取key集合与多个集合的并集
+ *
+ * @param key
+ * @param otherKeys
+ * @return
+ */
+ public Set<String> sUnion(String key, Collection<String> otherKeys) {
+ return redisTemplate.opsForSet().union(key, otherKeys);
+ }
+
+ /**
+ * key集合与otherKey集合的并集存储到destKey中
+ *
+ * @param key
+ * @param otherKey
+ * @param destKey
+ * @return
+ */
+ public Long sUnionAndStore(String key, String otherKey, String destKey) {
+ return redisTemplate.opsForSet().unionAndStore(key, otherKey, destKey);
+ }
+
+ /**
+ * key集合与多个集合的并集存储到destKey中
+ *
+ * @param key
+ * @param otherKeys
+ * @param destKey
+ * @return
+ */
+ public Long sUnionAndStore(String key, Collection<String> otherKeys,
+ String destKey) {
+ return redisTemplate.opsForSet().unionAndStore(key, otherKeys, destKey);
+ }
+
+ /**
+ * 获取两个集合的差集
+ *
+ * @param key
+ * @param otherKey
+ * @return
+ */
+ public Set<String> sDifference(String key, String otherKey) {
+ return redisTemplate.opsForSet().difference(key, otherKey);
+ }
+
+ /**
+ * 获取key集合与多个集合的差集
+ *
+ * @param key
+ * @param otherKeys
+ * @return
+ */
+ public Set<String> sDifference(String key, Collection<String> otherKeys) {
+ return redisTemplate.opsForSet().difference(key, otherKeys);
+ }
+
+ /**
+ * key集合与otherKey集合的差集存储到destKey中
+ *
+ * @param key
+ * @param otherKey
+ * @param destKey
+ * @return
+ */
+ public Long sDifference(String key, String otherKey, String destKey) {
+ return redisTemplate.opsForSet().differenceAndStore(key, otherKey,
+ destKey);
+ }
+
+ /**
+ * key集合与多个集合的差集存储到destKey中
+ *
+ * @param key
+ * @param otherKeys
+ * @param destKey
+ * @return
+ */
+ public Long sDifference(String key, Collection<String> otherKeys,
+ String destKey) {
+ return redisTemplate.opsForSet().differenceAndStore(key, otherKeys,
+ destKey);
+ }
+
+ /**
+ * 获取集合所有元素
+ *
+ * @param key
+ * @param otherKeys
+ * @param destKey
+ * @return
+ */
+ public Set<String> setMembers(String key) {
+ return redisTemplate.opsForSet().members(key);
+ }
+
+ /**
+ * 随机获取集合中的一个元素
+ *
+ * @param key
+ * @return
+ */
+ public String sRandomMember(String key) {
+ return redisTemplate.opsForSet().randomMember(key);
+ }
+
+ /**
+ * 随机获取集合中count个元素
+ *
+ * @param key
+ * @param count
+ * @return
+ */
+ public List<String> sRandomMembers(String key, long count) {
+ return redisTemplate.opsForSet().randomMembers(key, count);
+ }
+
+ /**
+ * 随机获取集合中count个元素并且去除重复的
+ *
+ * @param key
+ * @param count
+ * @return
+ */
+ public Set<String> sDistinctRandomMembers(String key, long count) {
+ return redisTemplate.opsForSet().distinctRandomMembers(key, count);
+ }
+
+ /**
+ * @param key
+ * @param options
+ * @return
+ */
+ public Cursor<String> sScan(String key, ScanOptions options) {
+ return redisTemplate.opsForSet().scan(key, options);
+ }
+
+ /**------------------zSet相关操作--------------------------------*/
+
+ /**
+ * 添加元素,有序集合是按照元素的score值由小到大排列
+ *
+ * @param key
+ * @param value
+ * @param score
+ * @return
+ */
+ public Boolean zAdd(String key, String value, double score) {
+ return redisTemplate.opsForZSet().add(key, value, score);
+ }
+
+ /**
+ * @param key
+ * @param values
+ * @return
+ */
+ public Long zAdd(String key, Set<TypedTuple<String>> values) {
+ return redisTemplate.opsForZSet().add(key, values);
+ }
+
+ /**
+ * @param key
+ * @param values
+ * @return
+ */
+ public Long zRemove(String key, Object... values) {
+ return redisTemplate.opsForZSet().remove(key, values);
+ }
+
+ /**
+ * 增加元素的score值,并返回增加后的值
+ *
+ * @param key
+ * @param value
+ * @param delta
+ * @return
+ */
+ public Double zIncrementScore(String key, String value, double delta) {
+ return redisTemplate.opsForZSet().incrementScore(key, value, delta);
+ }
+
+ /**
+ * 返回元素在集合的排名,有序集合是按照元素的score值由小到大排列
+ *
+ * @param key
+ * @param value
+ * @return 0表示第一位
+ */
+ public Long zRank(String key, Object value) {
+ return redisTemplate.opsForZSet().rank(key, value);
+ }
+
+ /**
+ * 返回元素在集合的排名,按元素的score值由大到小排列
+ *
+ * @param key
+ * @param value
+ * @return
+ */
+ public Long zReverseRank(String key, Object value) {
+ return redisTemplate.opsForZSet().reverseRank(key, value);
+ }
+
+ /**
+ * 获取集合的元素, 从小到大排序
+ *
+ * @param key
+ * @param start 开始位置
+ * @param end 结束位置, -1查询所有
+ * @return
+ */
+ public Set<String> zRange(String key, long start, long end) {
+ return redisTemplate.opsForZSet().range(key, start, end);
+ }
+
+ /**
+ * 获取集合元素, 并且把score值也获取
+ *
+ * @param key
+ * @param start
+ * @param end
+ * @return
+ */
+ public Set<TypedTuple<String>> zRangeWithScores(String key, long start,
+ long end) {
+ return redisTemplate.opsForZSet().rangeWithScores(key, start, end);
+ }
+
+ /**
+ * 根据Score值查询集合元素
+ *
+ * @param key
+ * @param min 最小值
+ * @param max 最大值
+ * @return
+ */
+ public Set<String> zRangeByScore(String key, double min, double max) {
+ return redisTemplate.opsForZSet().rangeByScore(key, min, max);
+ }
+
+ /**
+ * 根据Score值查询集合元素, 从小到大排序
+ *
+ * @param key
+ * @param min 最小值
+ * @param max 最大值
+ * @return
+ */
+ public Set<TypedTuple<String>> zRangeByScoreWithScores(String key,
+ double min, double max) {
+ return redisTemplate.opsForZSet().rangeByScoreWithScores(key, min, max);
+ }
+
+ /**
+ * @param key
+ * @param min
+ * @param max
+ * @param start
+ * @param end
+ * @return
+ */
+ public Set<TypedTuple<String>> zRangeByScoreWithScores(String key,
+ double min, double max, long start,
+ long end) {
+ return redisTemplate.opsForZSet().rangeByScoreWithScores(key, min, max,
+ start, end);
+ }
+
+ /**
+ * 获取集合的元素, 从大到小排序
+ *
+ * @param key
+ * @param start
+ * @param end
+ * @return
+ */
+ public Set<String> zReverseRange(String key, long start, long end) {
+ return redisTemplate.opsForZSet().reverseRange(key, start, end);
+ }
+
+ /**
+ * 获取集合的元素, 从大到小排序, 并返回score值
+ *
+ * @param key
+ * @param start
+ * @param end
+ * @return
+ */
+ public Set<TypedTuple<String>> zReverseRangeWithScores(String key,
+ long start, long end) {
+ return redisTemplate.opsForZSet().reverseRangeWithScores(key, start,
+ end);
+ }
+
+ /**
+ * 根据Score值查询集合元素, 从大到小排序
+ *
+ * @param key
+ * @param min
+ * @param max
+ * @return
+ */
+ public Set<String> zReverseRangeByScore(String key, double min,
+ double max) {
+ return redisTemplate.opsForZSet().reverseRangeByScore(key, min, max);
+ }
+
+ /**
+ * 根据Score值查询集合元素, 从大到小排序
+ *
+ * @param key
+ * @param min
+ * @param max
+ * @return
+ */
+ public Set<TypedTuple<String>> zReverseRangeByScoreWithScores(
+ String key, double min, double max) {
+ return redisTemplate.opsForZSet().reverseRangeByScoreWithScores(key,
+ min, max);
+ }
+
+ /**
+ * @param key
+ * @param min
+ * @param max
+ * @param start
+ * @param end
+ * @return
+ */
+ public Set<String> zReverseRangeByScore(String key, double min,
+ double max, long start, long end) {
+ return redisTemplate.opsForZSet().reverseRangeByScore(key, min, max,
+ start, end);
+ }
+
+ /**
+ * 根据score值获取集合元素数量
+ *
+ * @param key
+ * @param min
+ * @param max
+ * @return
+ */
+ public Long zCount(String key, double min, double max) {
+ return redisTemplate.opsForZSet().count(key, min, max);
+ }
+
+ /**
+ * 获取集合大小
+ *
+ * @param key
+ * @return
+ */
+ public Long zSize(String key) {
+ return redisTemplate.opsForZSet().size(key);
+ }
+
+ /**
+ * 获取集合大小
+ *
+ * @param key
+ * @return
+ */
+ public Long zZCard(String key) {
+ return redisTemplate.opsForZSet().zCard(key);
+ }
+
+ /**
+ * 获取集合中value元素的score值
+ *
+ * @param key
+ * @param value
+ * @return
+ */
+ public Double zScore(String key, Object value) {
+ return redisTemplate.opsForZSet().score(key, value);
+ }
+
+ /**
+ * 移除指定索引位置的成员
+ *
+ * @param key
+ * @param start
+ * @param end
+ * @return
+ */
+ public Long zRemoveRange(String key, long start, long end) {
+ return redisTemplate.opsForZSet().removeRange(key, start, end);
+ }
+
+ /**
+ * 根据指定的score值的范围来移除成员
+ *
+ * @param key
+ * @param min
+ * @param max
+ * @return
+ */
+ public Long zRemoveRangeByScore(String key, double min, double max) {
+ return redisTemplate.opsForZSet().removeRangeByScore(key, min, max);
+ }
+
+ /**
+ * 获取key和otherKey的并集并存储在destKey中
+ *
+ * @param key
+ * @param otherKey
+ * @param destKey
+ * @return
+ */
+ public Long zUnionAndStore(String key, String otherKey, String destKey) {
+ return redisTemplate.opsForZSet().unionAndStore(key, otherKey, destKey);
+ }
+
+ /**
+ * @param key
+ * @param otherKeys
+ * @param destKey
+ * @return
+ */
+ public Long zUnionAndStore(String key, Collection<String> otherKeys,
+ String destKey) {
+ return redisTemplate.opsForZSet()
+ .unionAndStore(key, otherKeys, destKey);
+ }
+
+ /**
+ * 交集
+ *
+ * @param key
+ * @param otherKey
+ * @param destKey
+ * @return
+ */
+ public Long zIntersectAndStore(String key, String otherKey,
+ String destKey) {
+ return redisTemplate.opsForZSet().intersectAndStore(key, otherKey,
+ destKey);
+ }
+
+ /**
+ * 交集
+ *
+ * @param key
+ * @param otherKeys
+ * @param destKey
+ * @return
+ */
+ public Long zIntersectAndStore(String key, Collection<String> otherKeys,
+ String destKey) {
+ return redisTemplate.opsForZSet().intersectAndStore(key, otherKeys,
+ destKey);
+ }
+
+ /**
+ * @param key
+ * @param options
+ * @return
+ */
+ public Cursor<TypedTuple<String>> zScan(String key, ScanOptions options) {
+ return redisTemplate.opsForZSet().scan(key, options);
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/StringListTypeHandler.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/StringListTypeHandler.java
new file mode 100644
index 0000000..40d472e
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/StringListTypeHandler.java
@@ -0,0 +1,57 @@
+package com.ruoyi.web.Tool.BT;
+
+import org.apache.ibatis.type.BaseTypeHandler;
+import org.apache.ibatis.type.JdbcType;
+
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class StringListTypeHandler extends BaseTypeHandler<List<String>> {
+ private static final String DELIMITER = ",";
+
+ @Override
+ public void setNonNullParameter(PreparedStatement ps, int i,
+ List<String> parameter, JdbcType jdbcType)
+ throws SQLException {
+ if (parameter == null || parameter.isEmpty()) {
+ ps.setString(i, "");
+ } else {
+ ps.setString(i, String.join(DELIMITER, parameter));
+ }
+ }
+
+ @Override
+ public List<String> getNullableResult(ResultSet rs, String columnName)
+ throws SQLException {
+ String value = rs.getString(columnName);
+ return parseStringToList(value);
+ }
+
+ @Override
+ public List<String> getNullableResult(ResultSet rs, int columnIndex)
+ throws SQLException {
+ String value = rs.getString(columnIndex);
+ return parseStringToList(value);
+ }
+
+ @Override
+ public List<String> getNullableResult(CallableStatement cs, int columnIndex)
+ throws SQLException {
+ String value = cs.getString(columnIndex);
+ return parseStringToList(value);
+ }
+
+ private List<String> parseStringToList(String value) {
+ if (value == null || value.isEmpty()) {
+ return new ArrayList<>();
+ }
+ return Arrays.asList(value.split(DELIMITER));
+ }
+}
+
+
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/TorrentUtils.java b/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/TorrentUtils.java
new file mode 100644
index 0000000..140d047
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/Tool/BT/TorrentUtils.java
@@ -0,0 +1,212 @@
+package com.ruoyi.web.Tool.BT;
+
+import cn.hutool.core.io.FileUtil;
+import com.dampcake.bencode.Bencode;
+import com.dampcake.bencode.Type;
+import com.ruoyi.web.domain.BT.TorrentDto;
+import com.ruoyi.web.domain.BT.TorrentFileVo;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.system.ApplicationHome;
+
+import java.io.*;
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.*;
+
+/**
+ * @author Jrx
+ */
+@Slf4j
+public class TorrentUtils {
+ private static final char[] HEX_SYMBOLS = "0123456789ABCDEF".toCharArray();
+
+ /**
+ * 只获取种子info的hash并返回
+ *
+ * @param data
+ * @return
+ * @throws NoSuchAlgorithmException
+ */
+ public static TorrentDto getTorrentHash(byte[] data) throws NoSuchAlgorithmException {
+ Bencode bencode = new Bencode(true);
+ Map<String, Object> torrentDataMap = bencode.decode(data, Type.DICTIONARY);
+ Map<String, Object> infoMap = (Map<String, Object>) torrentDataMap.get("info");
+ List<Object> files = (List<Object>) infoMap.get("files");
+ long len = 0;
+ long count = 0;
+ if (files != null) {
+ for (Object o : files) {
+ Map<String, Object> pace = (Map<String, Object>) o;
+ len += (long) pace.get("length");
+ List<Object> path = (List<Object>) pace.get("path");
+ for (Object value : path) {
+ ByteBuffer name = (ByteBuffer) value;
+ String s = new String(name.array());
+ if (s.contains(".")) {
+ count++;
+ }
+ }
+ }
+ } else {
+ len = (long) infoMap.get("length");
+ count = 1;
+ }
+ TorrentDto torrent = new TorrentDto();
+ torrent.setUkInfoHash(hash(bencode.encode(infoMap)));
+ torrent.setTorrentSize(len);
+ torrent.setTorrentCount(count);
+ return torrent;
+ }
+
+ public static TorrentFileVo getTorrentFile(File file) throws NoSuchAlgorithmException {
+ TorrentFileVo torrent = new TorrentFileVo();
+ torrent.setFillName(file.getName());
+ Bencode bencode = new Bencode(true);
+ byte[] fileBytes = FileUtil.readBytes(file);
+ Map<String, Object> torrentMap = bencode.decode(fileBytes, Type.DICTIONARY);
+ ByteBuffer announceByteBuffer = (ByteBuffer) torrentMap.get("announce");
+ torrent.setAnnounce(new String(announceByteBuffer.array()));
+ Map<String, Object> infoMap = (Map<String, Object>) torrentMap.get("info");
+ // info
+ List<Object> files = (List<Object>) infoMap.get("files");
+ // hash
+ torrent.setHash(hash(bencode.encode(infoMap)));
+ // length
+ long len = 0;
+ if (files != null) {
+ for (Object o : files) {
+ Map<String, Object> pace = (Map<String, Object>) o;
+ len += (long) pace.get("length");
+ }
+ } else {
+ len = (long) infoMap.get("length");
+ }
+ torrent.setLength(len);
+ return torrent;
+ }
+
+
+ /**
+ * hash
+ *
+ * @param data
+ * @return
+ * @throws NoSuchAlgorithmException
+ */
+ public static byte[] hash(byte[] data) throws NoSuchAlgorithmException {
+ MessageDigest crypt;
+ crypt = MessageDigest.getInstance("SHA-1");
+ crypt.reset();
+ crypt.update(data);
+ return crypt.digest();
+ }
+
+ /**
+ * 获取默认种子目录: jar所在目录/torrent
+ *
+ * @return
+ */
+ public static String getDefaultDir() {
+ ApplicationHome home = new ApplicationHome();
+ return home.getDir().getAbsolutePath();
+ }
+
+ /**
+ * 获取默认种子目录: jar所在目录/torrent
+ *
+ * @return
+ */
+ public static String getDefaultTorrentDir() {
+ return getDefaultDir() + "/torrent/";
+ }
+
+
+ /**
+ * 生成种子文件
+ * {@code @author} Jia
+ * {@code @date} 2025/5/27 17:20
+ */
+
+ private static void encodeObject(Object o, OutputStream out) throws IOException {
+ if (o instanceof String)
+ encodeString((String) o, out);
+ else if (o instanceof Map)
+ encodeMap((Map) o, out);
+ else if (o instanceof byte[])
+ encodeBytes((byte[]) o, out);
+ else if (o instanceof Number)
+ encodeLong(((Number) o).longValue(), out);
+ else
+ throw new Error("Unencodable type");
+ }
+
+ private static void encodeLong(long value, OutputStream out) throws IOException {
+ out.write('i');
+ out.write(Long.toString(value).getBytes("US-ASCII"));
+ out.write('e');
+ }
+
+ private static void encodeBytes(byte[] bytes, OutputStream out) throws IOException {
+ out.write(Integer.toString(bytes.length).getBytes("US-ASCII"));
+ out.write(':');
+ out.write(bytes);
+ }
+
+ private static void encodeString(String str, OutputStream out) throws IOException {
+ encodeBytes(str.getBytes("UTF-8"), out);
+ }
+
+ private static void encodeMap(Map<String, Object> map, OutputStream out) throws IOException {
+ // Sort the map. A generic encoder should sort by key bytes
+ SortedMap<String, Object> sortedMap = new TreeMap<String, Object>(map);
+ out.write('d');
+ for (Map.Entry<String, Object> e : sortedMap.entrySet()) {
+ encodeString(e.getKey(), out);
+ encodeObject(e.getValue(), out);
+ }
+ out.write('e');
+ }
+
+ private static byte[] hashPieces(File file, int pieceLength) throws IOException {
+ MessageDigest sha1;
+ try {
+ sha1 = MessageDigest.getInstance("SHA");
+ } catch (NoSuchAlgorithmException e) {
+ throw new Error("SHA1 not supported");
+ }
+ InputStream in = new FileInputStream(file);
+ ByteArrayOutputStream pieces = new ByteArrayOutputStream();
+ byte[] bytes = new byte[pieceLength];
+ int pieceByteCount = 0, readCount = in.read(bytes, 0, pieceLength);
+ while (readCount != -1) {
+ pieceByteCount += readCount;
+ sha1.update(bytes, 0, readCount);
+ if (pieceByteCount == pieceLength) {
+ pieceByteCount = 0;
+ pieces.write(sha1.digest());
+ }
+ readCount = in.read(bytes, 0, pieceLength - pieceByteCount);
+ }
+ in.close();
+ if (pieceByteCount > 0)
+ pieces.write(sha1.digest());
+ return pieces.toByteArray();
+ }
+
+ public static void createTorrent(File file, File sharedFile, String announceURL) throws IOException {
+ final int pieceLength = 512 * 1024;
+ Map<String, Object> info = new HashMap<>();
+ info.put("name", sharedFile.getName());
+ info.put("length", sharedFile.length());
+ info.put("piece length", pieceLength);
+ info.put("pieces", hashPieces(sharedFile, pieceLength));
+ Map<String, Object> metainfo = new HashMap<String, Object>();
+ metainfo.put("announce", announceURL);
+ metainfo.put("info", info);
+ OutputStream out = new FileOutputStream(file);
+ encodeMap(metainfo, out);
+ out.close();
+ }
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/BT/TagController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/BT/TagController.java
new file mode 100644
index 0000000..3cedd5d
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/BT/TagController.java
@@ -0,0 +1,44 @@
+package com.ruoyi.web.controller.BT;
+
+import com.ruoyi.web.Server.sys.UserService;
+import com.ruoyi.web.controller.common.Constants;
+import com.ruoyi.web.controller.common.base.Result;
+import com.ruoyi.web.dao.BT.TagDao;
+import com.ruoyi.web.domain.BT.TagCatEntity;
+import com.ruoyi.web.domain.BT.TagEntity;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+
+@RestController
+@Tag(name = "种子标签相关", description = Constants.FinishStatus.UNFINISHED)
+@RequiredArgsConstructor
+@RequestMapping("/tag")
+@Validated
+public class TagController {
+
+ private final TagDao tagDao;
+ private final UserService userService;
+
+
+ @Operation(summary = "所有标签", description = "返回所有标签,自打上的标签")
+ @GetMapping("/all_tag")
+ public Result allTag() {
+ List<TagEntity> tags = tagDao.all_tag();
+ return Result.ok(tags);
+ }
+
+ @Operation(summary = "所有分类", description = "返回所有分类,电影之类的")
+ @GetMapping("/all_cat")
+ public Result allCat() {
+ List<TagCatEntity> tags = tagDao.all_cat();
+ return Result.ok(tags);
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/BT/TorrentController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/BT/TorrentController.java
new file mode 100644
index 0000000..7a84cd5
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/BT/TorrentController.java
@@ -0,0 +1,341 @@
+package com.ruoyi.web.controller.BT;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import com.github.pagehelper.PageInfo;
+import com.ruoyi.web.Server.BT.TorrentCommentService;
+import com.ruoyi.web.Server.BT.TorrentService;
+import com.ruoyi.web.Server.BT.TrackerURLService;
+import com.ruoyi.web.controller.common.CommonResultStatus;
+import com.ruoyi.web.controller.common.Constants;
+import com.ruoyi.web.controller.common.base.I18nMessage;
+import com.ruoyi.web.controller.common.base.PageUtil;
+import com.ruoyi.web.controller.common.base.Result;
+import com.ruoyi.web.controller.common.exception.RocketPTException;
+import com.ruoyi.web.dao.BT.SuggestDao;
+import com.ruoyi.web.domain.BT.*;
+import com.ruoyi.web.Server.sys.UserService;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Positive;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * @author Jrx
+ */
+@RestController
+@Tag(name = "torrent种子相关", description = Constants.FinishStatus.FINISHED)
+@RequiredArgsConstructor
+@RequestMapping("/torrent")
+@Validated
+public class TorrentController {
+
+ private final TorrentService torrentService;
+ private final TrackerURLService trackerURLService;
+ private final SuggestDao suggestDao;
+ private final UserService userService;
+ private final TorrentCommentService torrentCommentService;
+
+ /**
+ * 种子列表查询
+ */
+
+ @Operation(summary = "种子列表查询", description = "种子列表条件查询-分页-排序")
+ @ApiResponse(responseCode = "0", description = "操作成功",
+ content = {@Content(mediaType = "application/json",
+ schema = @Schema(implementation = TorrentVO.class))
+ })
+ @PostMapping("/list")
+ public Result list(@RequestBody TorrentParam param) {
+ param.validOrder(param.getOrderKey(TorrentEntity.class));
+ param.buildLike();
+ PageUtil.startPage(param);
+
+ List<TorrentEntity> list = torrentService.getBaseMapper().search(param);
+
+ return Result.ok(list, PageUtil.getPage(list));
+ }
+
+ @Operation(summary = "种子搜索建议")
+ @GetMapping("/suggest")
+ @Parameter(name = "q", description = "关键字", required = true, in = ParameterIn.QUERY)
+ @ApiResponse(responseCode = "0", description = "操作成功", content = {
+ @Content(mediaType = "application/json", schema = @Schema(implementation =
+ SuggestVo.class))
+ })
+ public Result getSuggestions(@RequestParam(required = false) String q) {
+
+ if (StringUtils.isEmpty(q)) {
+ return Result.ok(new ArrayList<>());
+ }
+
+ List<SuggestVo> suggests = suggestDao.getSuggestions(q.trim() + "%");
+ List<SuggestVo> result = new ArrayList<>();
+ int i = 0;
+ for (SuggestVo suggest : suggests) {
+ if (suggest.getSuggest().length() > 25) {
+ continue;
+ }
+ result.add(suggest);
+
+ i++;
+ if (i >= 5) {
+ break;
+ }
+ }
+
+ return Result.ok(result);
+ }
+
+
+ @Operation(summary = "种子详情查询")
+ @ApiResponse(responseCode = "0", description = "操作成功", content = {
+ @Content(mediaType = "application/json", schema = @Schema(implementation =
+ TorrentEntity.class))
+ })
+ @PostMapping("/info/{id}")
+ public Result info(@PathVariable("id") Integer id) {
+ TorrentEntity entity = torrentService.getById(id);
+ return Result.ok(entity);
+ }
+
+
+ @Operation(summary = "新增种子")
+ @PostMapping("/add")
+ public Result add(TorrentAddParam param) {
+ Integer id = torrentService.add(param);
+ return Result.ok(id);
+ }
+
+
+
+ @Operation(summary = "审核种子")
+ @PostMapping("/audit")
+ public Result audit(@RequestBody @Validated TorrentAuditParam param) {
+ torrentService.audit(param);
+ return Result.ok();
+ }
+
+ /**
+ * 收藏或者取消收藏
+ */
+ @Operation(summary = "收藏或者取消收藏种子")
+ @Parameter(name = "id", description = "种子ID", required = true, in = ParameterIn.QUERY)
+ @PostMapping("/favorite")
+ public Result favorite(Integer id) {
+ torrentService.favorite(id, userService.getUserId());
+ return Result.ok();
+ }
+
+
+ @PostMapping("/create_credential")
+ public Result createCredential(){
+ torrentService.createCredential(userService.getUserId());
+ return Result.ok();
+ }
+
+
+ @Operation(summary = "上传文件并生成种子")
+ @PostMapping("/upload")
+ public ResponseEntity<byte[]> upload(@RequestPart("file") MultipartFile file,
+ @RequestParam Integer id) {
+ try {
+ if (file.isEmpty()) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR,
+ I18nMessage.getMessage("torrent_empty"));
+ }
+
+ String originalFilename = org.springframework.util.StringUtils.cleanPath(file.getOriginalFilename());
+ boolean isTorrentFile = originalFilename.endsWith(".torrent");
+ String filename = originalFilename; // 原始文件名(可能包含中文)
+ byte[] fileBytes = file.getBytes(); // 获取文件字节
+
+ // 非种子文件需要先转换为种子
+ if (!isTorrentFile) {
+ // 获取tracker地址
+ String passkey = userService.getPasskey(userService.getUserId());
+ String announceUrl = trackerURLService.getAnnounce(passkey);
+
+ // 生成种子文件
+ fileBytes = torrentService.createTorrentFromFile(
+ fileBytes,
+ originalFilename, // 使用原始文件名生成种子
+ announceUrl
+ );
+
+ filename = originalFilename + ".torrent"; // 强制添加.torrent后缀
+ }
+
+ // 验证文件路径安全(处理编码前的原始文件名,避免绕过校验)
+ if (originalFilename.contains("..")) { // 检查原始文件名,而非编码后的名称
+ throw new RocketPTException("文件路径包含非法字符");
+ }
+
+ // 调用原有上传逻辑
+ torrentService.upload(id, fileBytes, filename);
+
+ // ------------------- 关键修改:对响应头中的文件名进行编码 -------------------
+ // 1. 对文件名进行 UTF-8 URL 编码
+ String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8)
+ .replaceAll("\\+", "%20"); // 替换 + 为 %20,符合 URL 编码规范
+
+ // 2. 使用 RFC 5987 标准的 filename* 语法设置响应头
+ String contentDisposition = "attachment; filename*=UTF-8''" + encodedFilename;
+
+ return ResponseEntity.ok()
+ .contentType(MediaType.parseMediaType("application/x-bittorrent")) // 明确类型
+ .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition) // 使用 RFC 5987
+ .body(fileBytes);
+
+ } catch (IOException | RocketPTException e) {
+ return ResponseEntity.status(501)
+ .body(e.getMessage().getBytes());
+ }
+ }
+ @Operation(summary = "修改种子")
+ @PostMapping("/update")
+ public Result update(@RequestBody TorrentEntity entity) {
+ torrentService.update(entity);
+ return Result.ok();
+ }
+
+
+
+ @Operation(summary = "删除种子")
+ @PostMapping("/delete")
+ public Result delete(@RequestBody Integer[] ids) {
+ torrentService.remove(ids);
+
+ return Result.ok();
+ }
+
+ @Operation(summary = "获取tracker服务器")
+ @PostMapping("/tracker")
+ public Result tracker() {
+ String announce = trackerURLService.getAnnounce(userService.getPasskey(userService.getUserId()));
+ return Result.ok(announce);
+ }
+
+
+ @SaIgnore
+ @SneakyThrows
+ @Operation(summary = "下载种子")
+ @Parameter(name = "id", description = "种子ID", required = true, in = ParameterIn.QUERY)
+ @Parameter(name = "passkey", description = "passkey", in = ParameterIn.QUERY)
+ @GetMapping("/download")
+ public void download(@RequestParam("id") @Positive Integer id,
+ @RequestParam(value = "passkey", required = false) @Positive String passkey,
+ HttpServletResponse response) {
+
+ TorrentEntity entity = torrentService.getById(id);
+ if (entity == null) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR, I18nMessage.getMessage(
+ "torrent_not_exists"));
+ }
+ byte[] torrentBytes = torrentService.fetch(id, passkey);
+ String filename = entity.getFilename();
+ if (StringUtils.isBlank(filename)) {
+ filename = entity.getId() + ".torrent";
+ }
+ if (!StringUtils.endsWithIgnoreCase(filename, ".torrent")) {
+ filename = filename + ".torrent";
+ }
+
+ filename = URLEncoder.encode(filename, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
+ response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+ response.setContentLength(torrentBytes.length);
+ response.setContentType("application/x-bittorrent");
+ response.setHeader("Content-Disposition", "attachment;filename=" + filename);
+ if (response.isCommitted()) {
+ return;
+ }
+ response.getOutputStream().write(torrentBytes);
+ response.getOutputStream().flush();
+ // 成功下载后更新 leechers 字段
+ torrentService.incrementLeechers(id);
+ }
+
+ @Operation(summary = "高级种子列表查询", description = "支持分页、多字段排序和复合条件筛选")
+ @ApiResponse(responseCode = "0", description = "操作成功",
+ content = {@Content(mediaType = "application/json",
+ schema = @Schema(implementation = TorrentVO.class))
+ })
+ @PostMapping("/torrentList")
+ public Result advancedList(@RequestBody AdvancedTorrentParam param) {
+ // 校验排序字段合法性
+ param.validOrder(param.getOrderKey(TorrentEntity.class));
+ // 构建模糊查询条件
+ param.buildLike();
+ // 开启分页
+ PageUtil.startPage(param);
+
+ // 查询数据(实际业务逻辑需在Service中实现)
+ List<TorrentEntity> list = torrentService.advancedSearch(param);
+
+ // 返回分页结果
+ return Result.ok(list, PageUtil.getPage(list));
+ }
+
+ /**
+ * The purpose of the method:
+ * 种子的评论
+ * {@code @author} Jia
+ */
+
+ @PostMapping("/addComment")
+ public Result comment(@RequestBody @Valid CommentForm form,
+ HttpServletRequest request) {
+ // 获取当前登录用户ID
+ Integer userId = userService.getUserId();
+
+ // 构建评论实体
+ TorrentCommentEntity comment = new TorrentCommentEntity();
+ comment.setTorrentId(form.getTorrentId());
+ comment.setUserId(userId);
+ comment.setPid(form.getPid());
+ comment.setComment(form.getComment());
+ comment.setCreateTime(LocalDateTime.now());
+
+ // 保存评论
+ torrentCommentService.saveComment(comment);
+
+ return Result.ok();
+ }
+
+ @GetMapping("/comments") // 使用GET方法更符合语义
+ public Result getComments(
+ @RequestParam("torrentId") Integer torrentId,
+ @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
+ @RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize
+ ) {
+ // 分页查询种子评论(按时间倒序)
+ PageInfo<TorrentCommentVO> pageInfo = torrentCommentService.getCommentsByTorrentId(
+ torrentId, pageNum, pageSize
+ );
+
+ return Result.ok(pageInfo);
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/BT/TrackerController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/BT/TrackerController.java
new file mode 100644
index 0000000..45813ae
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/BT/TrackerController.java
@@ -0,0 +1,94 @@
+package com.ruoyi.web.controller.BT;
+
+
+import cn.hutool.core.util.HexUtil;
+import com.ruoyi.web.Server.AnnounceService;
+import com.ruoyi.web.dao.BT.AnnounceRequest;
+import com.ruoyi.web.Tool.BT.BencodeUtil;
+import com.ruoyi.web.Tool.BT.BinaryFieldUtil;
+import com.ruoyi.web.Tool.BT.IPUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/tracker")
+public class TrackerController {
+ final AnnounceService announceService;
+
+ /**
+ * <p>
+ * want-digest 是为了校验aria2
+ * <p>
+ * 1. bep建议接口路径是 /announce
+ * 2. 成功响应
+ * 返回一个经过 Bencode 编码的 Dictionary (也就是 Map),包含以下 key:
+ * <p>
+ * interval – 代表间隔时间,单位是秒,表示 BT 客户端应该指定时间之后再与 Tracker 联系更新状态
+ * peers – 这是个 List,每个 List 存储一个 Dictionary(也就是 Map),每个 Dictionary 包含以下 key:
+ * peer id 客户端随机唯一 ID
+ * ip 客户端 IP 地址,既可是 IPV4,也可以是 IPV6,以常规字符串表示即可,如 `127.0.0.11 或者 ::1,也支持 DNS 名称
+ * port 客户端端口号
+ */
+ @GetMapping("/announce")
+ public String announce(HttpServletRequest request,
+ @ModelAttribute AnnounceRequest announceRequest,
+ @RequestHeader(name = "User-Agent") String ua,
+ @RequestHeader(name = "want-digest", required = false) String wantDigest) {
+
+ String queryStrings = request.getQueryString();
+ log.info("收到announce汇报:" + queryStrings);
+ String ipAddr = IPUtils.getIpAddr();
+ byte[] peerId = BinaryFieldUtil.matchPeerId(queryStrings);
+
+ String peerIdHex = HexUtil.encodeHexStr(peerId);
+
+ announceRequest.setSeeder(announceRequest.getLeft().equals(0L));
+ announceRequest.setInfoHash(BinaryFieldUtil.matchInfoHash(queryStrings));
+ announceRequest.setPeerId(peerId);
+ announceRequest.setPeerIdHex(peerIdHex);
+ announceRequest.setRemoteAddr(ipAddr);
+ announceRequest.setWantDigest(wantDigest);
+ announceRequest.setUserAgent(ua);
+
+ Map<String, Object> response = announceService.announce(announceRequest);
+
+ String responseStr = BencodeUtil.encode(response);
+ return responseStr;
+ }
+
+ /**
+ * 从请求中获取所有info_hash信息,
+ * 从数据库中匹配出来对应的做种内容,
+ * <p>
+ * 一个特殊的请求,它允许用户获取关于一个或多个 torrent 的基本统计信息,而不需要完全加入 swarm(即 torrent 的下载/上传群体)。
+ * 通常,这些信息包括:完成下载的次数(即有多少 peers 拥有了文件的全部内容)、正在下载的用户数量(leechers)、拥有完整文件并且正在分发的用户数量(seeders)。
+ * 通过 scrape 请求,用户可以快速了解 torrent 的“健康状况”,而无需加入 swarm。例如,一个拥有很多 seeders 的 torrent 可能下载速度更快,表明它是一个活跃的 torrent。
+ * 另一方面,如果一个 torrent 没有 seeders,那么新用户可能无法下载到完整的文件。
+ * 但是,对于PT来说,scrape 请求对于一般用户而言并不是必需,用户可以从网站的页面上获得关于 torrent 健康状况的足够信息,无需直接进行 scrape 请求。
+ * <p>
+ */
+ @GetMapping("/scrape")
+ public String processingScrape(Optional<String> passkey,
+ HttpServletRequest request) {
+ String queryString = request.getQueryString();
+ //收到scrape汇报:passkey=xxx&info_hash=X0%BE%xxx7%A0
+ log.info("收到scrape汇报:" + queryString);
+
+ if (StringUtils.isEmpty(queryString)) {
+ return BencodeUtil.error();
+ }
+
+ List<byte[]> infoHashesHex = BinaryFieldUtil.matchInfoHashesHex(queryString);
+
+ return BencodeUtil.errorNoRetry("dont touch server");
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonResultStatus.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonResultStatus.java
new file mode 100644
index 0000000..9628bc3
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonResultStatus.java
@@ -0,0 +1,39 @@
+package com.ruoyi.web.controller.common;
+
+/**
+ * @author plexpt
+ */
+public enum CommonResultStatus implements ResultStatus {
+
+ OK(0, "成功"),
+
+ FAIL(500, "失败"),
+
+ PARAM_ERROR(400, "参数非法"),
+
+ RECORD_NOT_EXIST(404, "记录不存在"),
+
+ UNAUTHORIZED(401, "未授权"),
+
+ FORBIDDEN(403, "无权限"),
+
+ SERVER_ERROR(500, "服务器内部错误");
+
+ private final int code;
+ private final String message;
+
+ CommonResultStatus(int code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+
+ @Override
+ public int getCode() {
+ return code;
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/Constants.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/Constants.java
new file mode 100644
index 0000000..e3fa91b
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/Constants.java
@@ -0,0 +1,56 @@
+package com.ruoyi.web.controller.common;
+
+import org.springframework.beans.factory.annotation.Value;
+
+/**
+ * @author plexpt
+ */
+public interface Constants {
+
+ String TOKEN_HEADER_NAME = "Authorization";
+ String SESSION_CURRENT_USER = "currentUser";
+
+ /**
+ * 菜单根id
+ */
+ Integer RESOURCE_ROOT_ID = 0;
+
+ interface Order {
+ String DEFAULT_ORDER_TYPE = "desc";
+
+ String[] ORDER_TYPE = new String[]{"desc", "asc", "DESC", "ASC"};
+ }
+
+ interface FinishStatus {
+
+ /**
+ * 已完成并测试
+ */
+ String FINISHED = " (已完成并测试通过)";
+
+ /**
+ * 未完成未测试
+ */
+ String UNFINISHED = " (未完成未测试)";
+
+ /**
+ * 已完成但未测试
+ */
+ String FINISHED_NOT_TEST = " (已完成但未测试)";
+
+ }
+
+ interface Source {
+ String PREFIX = "[RKT] ";
+
+ String NAME = "thunder pt";
+ }
+
+ interface Announce {
+
+ String PROTOCOL = "http";
+
+ Integer PORT = 8080;
+
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/DomainEvent.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/DomainEvent.java
new file mode 100644
index 0000000..ff9f5c5
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/DomainEvent.java
@@ -0,0 +1,32 @@
+// Copyright 2012,2013 Vaughn Vernon
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.ruoyi.web.controller.common;
+
+import java.time.LocalDateTime;
+
+public interface DomainEvent {
+
+ default int eventVersion() {
+ return 1;
+ }
+
+ default LocalDateTime occurredOn() {
+ return LocalDateTime.now();
+ }
+
+ default boolean needSave() {
+ return false;
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/DomainEventPublisher.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/DomainEventPublisher.java
new file mode 100644
index 0000000..314c04b
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/DomainEventPublisher.java
@@ -0,0 +1,146 @@
+// Copyright 2012,2013 Vaughn Vernon
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.ruoyi.web.controller.common;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+
+public class DomainEventPublisher {
+
+ private static final ThreadLocal<DomainEventPublisher> instance =
+ ThreadLocal.withInitial(() -> new DomainEventPublisher());
+
+ private boolean publishing;
+
+ @SuppressWarnings("rawtypes")
+ private List subscribers;
+
+ private DomainEventPublisher() {
+ super();
+
+ this.setPublishing(false);
+ this.ensureSubscribersList();
+ }
+
+ public static DomainEventPublisher instance() {
+ return instance.get();
+ }
+
+ public <T> void publish(final T aDomainEvent) {
+ if (!this.isPublishing() && this.hasSubscribers()) {
+
+ try {
+ this.setPublishing(true);
+
+ Class<?> eventType = aDomainEvent.getClass();
+
+ @SuppressWarnings("unchecked")
+ List<DomainEventSubscriber<T>> allSubscribers = this.subscribers();
+
+ for (DomainEventSubscriber<T> subscriber : allSubscribers) {
+ Class<?> subscribedToType = subscriber.subscribedToEventType();
+
+ if (eventType == subscribedToType || subscribedToType == DomainEvent.class) {
+ subscriber.handleEvent(aDomainEvent);
+ }
+ }
+
+ } finally {
+ this.setPublishing(false);
+ }
+ }
+ }
+
+ public void publishAll(Collection<DomainEvent> aDomainEvents) {
+ for (DomainEvent domainEvent : aDomainEvents) {
+ this.publish(domainEvent);
+ }
+ }
+
+ public void reset() {
+ if (!this.isPublishing()) {
+ this.setSubscribers(null);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public <T> void subscribe(DomainEventSubscriber<T> aSubscriber) {
+ if (!this.isPublishing()) {
+ this.ensureSubscribersList();
+
+ this.subscribers().add(aSubscriber);
+ }
+ }
+
+ public <T> void asyncSubscribe(Class<T> subscribedToEventType, Consumer<T> event) {
+ subscribe(new DomainEventSubscriber<T>() {
+ @Override
+ public void handleEvent(T aDomainEvent) {
+ CompletableFuture.runAsync(() -> event.accept(aDomainEvent));
+ }
+
+ @Override
+ public Class<T> subscribedToEventType() {
+ return subscribedToEventType;
+ }
+ });
+ }
+
+ public <T> void subscribe(Class<T> subscribedToEventType, Consumer<T> event) {
+ subscribe(new DomainEventSubscriber<T>() {
+ @Override
+ public void handleEvent(T aDomainEvent) {
+ event.accept(aDomainEvent);
+ }
+
+ @Override
+ public Class<T> subscribedToEventType() {
+ return subscribedToEventType;
+ }
+ });
+ }
+
+ @SuppressWarnings("rawtypes")
+ private void ensureSubscribersList() {
+ if (!this.hasSubscribers()) {
+ this.setSubscribers(new ArrayList());
+ }
+ }
+
+ private boolean isPublishing() {
+ return this.publishing;
+ }
+
+ private void setPublishing(boolean aFlag) {
+ this.publishing = aFlag;
+ }
+
+ private boolean hasSubscribers() {
+ return this.subscribers() != null;
+ }
+
+ @SuppressWarnings("rawtypes")
+ private List subscribers() {
+ return this.subscribers;
+ }
+
+ @SuppressWarnings("rawtypes")
+ private void setSubscribers(List aSubscriberList) {
+ this.subscribers = aSubscriberList;
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/DomainEventSubscriber.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/DomainEventSubscriber.java
new file mode 100644
index 0000000..0ce21ba
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/DomainEventSubscriber.java
@@ -0,0 +1,22 @@
+// Copyright 2012,2013 Vaughn Vernon
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.ruoyi.web.controller.common;
+
+public interface DomainEventSubscriber<T> {
+
+ void handleEvent(final T aDomainEvent);
+
+ Class<T> subscribedToEventType();
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/EventStore.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/EventStore.java
new file mode 100644
index 0000000..8842e87
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/EventStore.java
@@ -0,0 +1,12 @@
+package com.ruoyi.web.controller.common;
+
+
+/**
+ * @author plexpt
+ * @date 2022/7/22
+ */
+public interface EventStore {
+
+ void append(DomainEvent aDomainEvent);
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/ResultStatus.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/ResultStatus.java
new file mode 100644
index 0000000..165c00a
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/ResultStatus.java
@@ -0,0 +1,16 @@
+package com.ruoyi.web.controller.common;
+
+/**
+ * @author plexpt
+ */
+public interface ResultStatus {
+ /**
+ * 错误码
+ */
+ int getCode();
+
+ /**
+ * 错误信息
+ */
+ String getMessage();
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/SessionItemHolder.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/SessionItemHolder.java
new file mode 100644
index 0000000..60a69e2
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/SessionItemHolder.java
@@ -0,0 +1,29 @@
+package com.ruoyi.web.controller.common;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author plexpt
+ */
+public class SessionItemHolder {
+
+ private SessionItemHolder() {
+ }
+
+ private static final ThreadLocal<Map<String, Object>> store =
+ InheritableThreadLocal.withInitial(HashMap::new);
+
+ public static void setItem(String key, Object obj) {
+ store.get().put(key, obj);
+ }
+
+ public static final Object getItem(String key) {
+ return store.get().get(key);
+ }
+
+ public static final void clear() {
+ store.remove();
+ }
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/I18nMessage.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/I18nMessage.java
new file mode 100644
index 0000000..cc35378
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/I18nMessage.java
@@ -0,0 +1,60 @@
+package com.ruoyi.web.controller.common.base;
+
+import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.MessageSource;
+import org.springframework.context.i18n.LocaleContextHolder;
+import org.springframework.context.support.ReloadableResourceBundleMessageSource;
+
+import java.util.Objects;
+
+@Slf4j
+@UtilityClass
+public class I18nMessage {
+
+ static {
+ ReloadableResourceBundleMessageSource messageSource =
+ new ReloadableResourceBundleMessageSource();
+ messageSource.setCacheSeconds(5);
+ messageSource.setBasenames("classpath:i18n/message");
+ I18nMessage.init(messageSource);
+ }
+
+ private static MessageSource messageSource;
+
+ public static void init(MessageSource messageSource) {
+ Objects.requireNonNull(messageSource, "MessageSource can't be null");
+ I18nMessage.messageSource = messageSource;
+ }
+
+ /**
+ * 读取国际化消息
+ *
+ * @param msgCode 消息码
+ * @param args 消息参数 例: new String[]{"1","2","3"}
+ * @return
+ */
+ public static String getMessage(String msgCode, Object[] args) {
+ try {
+ return I18nMessage.messageSource.getMessage(msgCode, args,
+ LocaleContextHolder.getLocale());
+ } catch (Exception e) {
+ if (log.isDebugEnabled()) {
+ e.printStackTrace();
+ }
+ log.error("===> 读取国际化消息失败, code:{}, args:{}, ex:{}", msgCode, args,
+ e.getMessage() == null ? e.toString() : e.getMessage());
+ }
+ return "-Unknown-";
+ }
+
+ /**
+ * 获取一条语言配置信息
+ *
+ * @param msgCode 消息码
+ * @return 对应配置的信息
+ */
+ public static String getMessage(String msgCode) {
+ return I18nMessage.getMessage(msgCode, null);
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/OrderPageParam.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/OrderPageParam.java
new file mode 100644
index 0000000..ba3e666
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/OrderPageParam.java
@@ -0,0 +1,97 @@
+package com.ruoyi.web.controller.common.base;
+
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.ArrayUtil;
+import com.ruoyi.web.controller.common.CommonResultStatus;
+import com.ruoyi.web.controller.common.Constants;
+import com.ruoyi.web.controller.common.exception.RocketPTException;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.commons.lang3.StringUtils;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@Getter
+@Setter
+public class OrderPageParam extends PageParam {
+
+ /**
+ * 排序字段
+ */
+ @Schema(description = "排序字段")
+ protected String prop;
+
+ /**
+ * 排序规则
+ */
+ @Schema(description = "排序规则")
+ protected String sort;
+
+ public void validOrder(List<String> orderKey) throws RocketPTException {
+ prop = StringUtils.isBlank(prop) ? null : StrUtil.toUnderlineCase(prop);
+ sort = StringUtils.isBlank(sort) ? Constants.Order.DEFAULT_ORDER_TYPE : sort;
+
+ if (Arrays.asList(Constants.Order.ORDER_TYPE).indexOf(sort) < 0) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR, "排序方式錯誤");
+ }
+
+ if (StringUtils.isNotBlank(prop) && Arrays.asList(orderKey).indexOf(prop) < 0) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR, "排序欄位錯誤");
+ }
+ }
+
+ public void validOrder() throws RocketPTException {
+ List<String> orderKey = getOrderKey();
+ prop = StringUtils.isBlank(prop) ? null : StrUtil.toUnderlineCase(prop);
+ sort = StringUtils.isBlank(sort) ? Constants.Order.DEFAULT_ORDER_TYPE : sort;
+
+ if (!ArrayUtil.contains(Constants.Order.ORDER_TYPE, sort)) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR, "排序方式錯誤");
+ }
+
+ if (StringUtils.isNotBlank(prop) && !orderKey.contains(prop)) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR, "排序欄位錯誤");
+ }
+ }
+
+ /**
+ * @return 反射获取字段列表
+ */
+ public List<String> getOrderKey() {
+ List<String> list = new ArrayList<>();
+
+ Field[] fields = getClass().getDeclaredFields();
+ if (fields != null) {
+ for (Field field : fields) {
+ field.setAccessible(true);
+ String name = field.getName();
+
+ list.add(StrUtil.toUnderlineCase(name));
+ }
+ }
+ return list;
+ }
+ /**
+ * @return 反射获取字段列表
+ */
+ public List<String> getOrderKey(Class clazz) {
+ List<String> list = new ArrayList<>();
+
+ Field[] fields = clazz.getDeclaredFields();
+ if (fields != null) {
+ for (Field field : fields) {
+ field.setAccessible(true);
+ String name = field.getName();
+
+ list.add(StrUtil.toUnderlineCase(name));
+ }
+ }
+ return list;
+ }
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/OrderParam.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/OrderParam.java
new file mode 100644
index 0000000..01dc381
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/OrderParam.java
@@ -0,0 +1,74 @@
+package com.ruoyi.web.controller.common.base;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.ArrayUtil;
+import com.ruoyi.web.controller.common.CommonResultStatus;
+import com.ruoyi.web.controller.common.Constants;
+import com.ruoyi.web.controller.common.exception.RocketPTException;
+import lombok.Data;
+import org.apache.commons.lang3.StringUtils;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@Data
+public class OrderParam {
+
+ /**
+ * 排序字段
+ */
+ protected String prop;
+
+ /**
+ * 排序规则
+ */
+ protected String sort;
+
+ public void validOrder(String[] orderKey) throws RocketPTException {
+ this.prop = StringUtils.isBlank(this.prop) ? null : this.prop;
+ this.sort = StringUtils.isBlank(this.sort) ? Constants.Order.DEFAULT_ORDER_TYPE
+ : this.sort;
+
+ if (Arrays.asList(Constants.Order.ORDER_TYPE).indexOf(this.sort) < 0) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR, "排序方式錯誤");
+ }
+
+ if (!StringUtils.isBlank(this.prop) && Arrays.asList(orderKey).indexOf(this.prop) < 0) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR, "排序欄位錯誤");
+ }
+ }
+
+ public void validOrder() throws RocketPTException {
+ List<String> orderKey = getOrderKey();
+ prop = StringUtils.isBlank(prop) ? null : StrUtil.toUnderlineCase(prop);
+ sort = StringUtils.isBlank(sort) ? Constants.Order.DEFAULT_ORDER_TYPE : sort;
+
+ if (!ArrayUtil.contains(Constants.Order.ORDER_TYPE, sort)) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR, "排序方式錯誤");
+ }
+
+ if (StringUtils.isNotBlank(prop) && !orderKey.contains(prop)) {
+ throw new RocketPTException(CommonResultStatus.PARAM_ERROR, "排序欄位錯誤");
+ }
+ }
+
+ /**
+ * @return 反射获取字段列表
+ */
+ private List<String> getOrderKey() {
+ List<String> list = new ArrayList<>();
+
+ Field[] fields = getClass().getDeclaredFields();
+ if (fields != null) {
+ for (Field field : fields) {
+ field.setAccessible(true);
+ String name = field.getName();
+
+ list.add(StrUtil.toUnderlineCase(name));
+ }
+ }
+ return list;
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/PageParam.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/PageParam.java
new file mode 100644
index 0000000..3283f5b
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/PageParam.java
@@ -0,0 +1,25 @@
+package com.ruoyi.web.controller.common.base;
+
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotNull;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+
+@Getter
+@Setter
+@ToString
+public class PageParam {
+ @NotNull(message = "page参数不能为空")
+ @Min(value = 1L, message = "page参数必须是数字或数值小于限制")
+ protected Integer page;
+
+ @NotNull(message = "参数不能为空")
+ @Min(value = 1L, message = "参数必须是数字或数值小于限制")
+ @Max(value = 200L, message = "参数必须是数字或数值大于限制")
+ protected Integer size;
+
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/PageUtil.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/PageUtil.java
new file mode 100644
index 0000000..b23ccd4
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/PageUtil.java
@@ -0,0 +1,54 @@
+package com.ruoyi.web.controller.common.base;
+
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+
+import java.util.List;
+
+public class PageUtil {
+
+
+ public static int DEFAULT_PAGE_SIZE = 20;
+
+ /**
+ * 开始分页
+ *
+ * @param param
+ */
+ public static void startPage(OrderPageParam param) {
+ Integer page = param.getPage();
+ if (page == null) {
+ param.setPage(1);
+ param.setSize(DEFAULT_PAGE_SIZE);
+ }
+
+ PageHelper.startPage(param.getPage(), param.getSize());
+
+ }
+
+ /**
+ * 开始分页
+ *
+ * @param param
+ */
+ public static void startPage(PageParam param) {
+ Integer page = param.getPage();
+ if (page == null) {
+ param.setPage(1);
+ param.setSize(DEFAULT_PAGE_SIZE);
+ }
+
+ PageHelper.startPage(param.getPage(), param.getSize());
+ }
+
+ /**
+ * 分页结果
+ *
+ * @param list
+ */
+ public static ResPage getPage(List list) {
+ PageInfo pageInfo = new PageInfo(list);
+ return new ResPage(pageInfo.getTotal(), pageInfo.getPageNum(), pageInfo.getSize());
+
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/ResPage.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/ResPage.java
new file mode 100644
index 0000000..55e2c27
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/ResPage.java
@@ -0,0 +1,28 @@
+package com.ruoyi.web.controller.common.base;
+
+import lombok.*;
+
+/**
+ * 分页的返回值
+ */
+@Getter
+@Setter
+@ToString(callSuper = false)
+@NoArgsConstructor
+@AllArgsConstructor
+public class ResPage {
+
+ private long total;
+
+ private int page;
+
+ private int size;
+
+ public static ResPage getPage(long total, int page, int size) {
+ return new ResPage(total, page, size);
+ }
+
+ public static ResPage defaultPage() {
+ return new ResPage(10, 1, 10);
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/Result.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/Result.java
new file mode 100644
index 0000000..5769c5f
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/Result.java
@@ -0,0 +1,180 @@
+package com.ruoyi.web.controller.common.base;
+
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+import com.ruoyi.web.controller.common.base.Status;
+
+/**
+ * 返回值实体类
+ */
+@Getter
+@Setter
+@ToString
+@NoArgsConstructor
+public class Result<T> {
+
+ private int code;
+
+ private String msg;
+
+ @JsonProperty
+ private T data;
+
+ private ResPage page;
+
+
+ public Result(Status status) {
+ this.code = status.getCode();
+ this.msg = status.getMsg();
+ this.data = null;
+ }
+
+ public Result(Status status, T data) {
+ this.code = status.getCode();
+ this.msg = status.getMsg();
+ this.data = data;
+ }
+
+ public Result(Status status, String msg) {
+ this.code = status.getCode();
+ this.msg = msg;
+ this.data = null;
+ }
+
+ public Result(Status status, int msgCode) {
+ this.code = status.getCode();
+ this.msg = I18nMessage.getMessage(String.valueOf(msgCode));
+ this.data = null;
+ }
+
+ public Result(Status status, String msg, T data) {
+ this.code = status.getCode();
+ this.msg = msg;
+ this.data = data;
+ }
+
+ public Result(Status status, int msgCode, T data) {
+ this.code = status.getCode();
+ this.msg = I18nMessage.getMessage(String.valueOf(msgCode));
+ this.data = data;
+ }
+
+ public Result(Status status, T data, ResPage page) {
+ this.code = status.getCode();
+ this.msg = status.getMsg();
+ this.data = data;
+ this.page = page;
+ }
+
+ public Result(Status status, String msg, T data, ResPage page) {
+ this.code = status.getCode();
+ this.msg = msg;
+ this.data = data;
+ this.page = page;
+ }
+
+ public Result(Status status, int msgCode, T data, ResPage page) {
+ this.code = status.getCode();
+ this.msg = I18nMessage.getMessage(String.valueOf(msgCode));
+ this.data = data;
+ this.page = page;
+ }
+
+ @JsonIgnore
+ public boolean isSuccess() {
+ return this.code == Status.SUCCESS.getCode();
+ }
+
+ @JsonIgnore
+ public boolean nonSuccess() {
+ return this.code != Status.SUCCESS.getCode();
+ }
+
+ public static <T> Result<T> success() {
+ return new Result<T>(Status.SUCCESS);
+ }
+
+ public static <T> Result<T> ok() {
+ return new Result<T>(Status.SUCCESS);
+ }
+
+ public static <T> Result<T> ok(T data) {
+ return new Result<T>(Status.SUCCESS, data);
+ }
+
+ public static <T> Result<T> illegal() {
+ return new Result<T>(Status.BAD_REQUEST);
+ }
+
+ public static <T> Result<T> unauthorized() {
+ return new Result<T>(Status.UNAUTHORIZED);
+ }
+
+ public static <T> Result<T> forbidden() {
+ return new Result<T>(Status.FORBIDDEN);
+ }
+
+ public static <T> Result<T> notFound() {
+ return new Result<T>(Status.NOT_FOUND);
+ }
+
+ public static <T> Result<T> failure() {
+ return new Result<T>(Status.FAILURE);
+ }
+
+ public static <T> Result<T> failure(String msg) {
+ return new Result<T>(Status.FAILURE, msg);
+ }
+
+ public static <T> Result<T> error(String msg) {
+ return new Result<T>(Status.FAILURE, msg);
+ }
+
+ public static <T> Result<T> conflict() {
+ return new Result<T>(Status.CONFLICT);
+ }
+
+ public static <T> Result<T> build(Status status, T data) {
+ return new Result<T>(status, data);
+ }
+
+ public static <T> Result<T> build(Status status, String msg) {
+ return new Result<T>(status, msg);
+ }
+
+ public static <T> Result<T> build(Status status, int msgCode) {
+ return new Result<T>(status, msgCode);
+ }
+
+ public static <T> Result<T> build(Status status, String msg, T data) {
+ return new Result<T>(status, msg, data);
+ }
+
+ public static <T> Result<T> build(Status status, int msgCode, T data) {
+ return new Result<T>(status, msgCode, data);
+ }
+
+ public static Result ok(Object data, ResPage page) {
+ return new Result(Status.SUCCESS, data, page);
+ }
+
+ public static Result build(Status status, Object data, ResPage page) {
+ return new Result(status, data, page);
+ }
+
+ public static Result build(Status status, String msg, Object data, ResPage page) {
+ return new Result(status, msg, data, page);
+ }
+
+ public static Result build(Status status, int msgCode, Object data, ResPage page) {
+ return new Result(status, msgCode, data, page);
+ }
+
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/Status.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/Status.java
new file mode 100644
index 0000000..3cb73ad
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/base/Status.java
@@ -0,0 +1,64 @@
+package com.ruoyi.web.controller.common.base;
+
+/**
+ * 返回值状态码
+ *
+ * @author liyue
+ * @version v1
+ * @create 2019-03-07 16:40:52
+ * @copyright www.liderong.cn
+ */
+public enum Status {
+
+ /**
+ * 请求执行成功
+ */
+ SUCCESS(0, "操作成功"),
+
+ /**
+ * 请求验证失败
+ */
+ BAD_REQUEST(400, "操作验证失败"),
+
+ /**
+ * 权限不足
+ */
+ UNAUTHORIZED(401, "操作未授权"),
+
+ /**
+ * 请求拒绝
+ */
+ FORBIDDEN(403, "操作被拒绝"),
+
+ /**
+ * 未知请求
+ */
+ NOT_FOUND(404, "未知操作"),
+
+ /**
+ * 未知请求
+ */
+ CONFLICT(409, "请求发生冲突"),
+
+ /**
+ * 请求执行失败
+ */
+ FAILURE(500, "操作失败");
+
+ private int code;
+
+ private String msg;
+
+ Status(int code, String msg) {
+ this.code = code;
+ this.msg = msg;
+ }
+
+ public int getCode() {
+ return this.code;
+ }
+
+ public String getMsg() {
+ return this.msg;
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/exception/ExceptionControllerAdvice.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/exception/ExceptionControllerAdvice.java
new file mode 100644
index 0000000..85c7389
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/exception/ExceptionControllerAdvice.java
@@ -0,0 +1,99 @@
+package com.ruoyi.web.controller.common.exception;
+
+import com.ruoyi.web.Tool.BT.BencodeUtil;
+import com.ruoyi.web.controller.common.CommonResultStatus;
+import com.ruoyi.web.controller.common.ResultStatus;
+
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.ConstraintViolationException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author plexpt
+ */
+@Slf4j
+@SecurityRequirement(name = "bearerAuth")
+@RestControllerAdvice
+public class ExceptionControllerAdvice {
+
+ private final Map<ResultStatus, HttpStatus> codeMap = new HashMap<>() {{
+ put(CommonResultStatus.FAIL, HttpStatus.BAD_REQUEST);
+ put(CommonResultStatus.PARAM_ERROR, HttpStatus.BAD_REQUEST);
+ put(CommonResultStatus.RECORD_NOT_EXIST, HttpStatus.BAD_REQUEST);
+ put(CommonResultStatus.UNAUTHORIZED, HttpStatus.UNAUTHORIZED);
+ put(CommonResultStatus.FORBIDDEN, HttpStatus.FORBIDDEN);
+ }};
+
+
+ @ExceptionHandler(TrackerException.class)
+ public String invalidLengthExceptionHandler(TrackerException exception) {
+ log.error("tracker exception, message={}", exception.getMessage());
+ if (exception instanceof TrackerNoRetryException) {
+ return BencodeUtil.errorNoRetry(exception.getMessage());
+ }
+ return BencodeUtil.error(exception.getMessage());
+ }
+
+
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity<Map<String, Object>> handleDefaultErrorView(Exception ex,
+ HttpServletRequest request) {
+ log.error("Handle exception, message={}, requestUrl={}", ex.getMessage(),
+ request.getRequestURI(), ex);
+ Map<String, Object> body = new HashMap<>();
+ body.put("code", CommonResultStatus.SERVER_ERROR.getCode());
+ body.put("message", ex.getMessage());
+ body.put("success", false);
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
+ }
+
+ @ExceptionHandler(RocketPTException.class)
+ public ResponseEntity<Map<String, Object>> handleBusinessException(RocketPTException ex) {
+ Map<String, Object> body = Map.of("code", ex.getStatus().getCode(), "message",
+ ex.getMessage(), "success", false);
+ return ResponseEntity.status(codeMap.getOrDefault(ex.getStatus(), HttpStatus.BAD_REQUEST)).body(body);
+ }
+
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ResponseEntity<Map<String, Object>> handleMethodArgumentNotValidException(
+ MethodArgumentNotValidException e) {
+ List<String> fieldErrors = e.getBindingResult().getFieldErrors().stream()
+ .map(fieldError -> fieldError.getField() + " " + fieldError.getDefaultMessage())
+ .collect(Collectors.toList());
+ Map<String, Object> body = getErrorsMap(fieldErrors);
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
+ }
+
+ @ExceptionHandler(ConstraintViolationException.class)
+ public ResponseEntity<Map<String, Object>> handleConstraintViolationException(
+ ConstraintViolationException e) {
+ List<String> errors = e.getConstraintViolations().stream()
+ .map(constraintViolation -> constraintViolation.getPropertyPath() + constraintViolation.getMessage())
+ .collect(Collectors.toList());
+ Map<String, Object> body = getErrorsMap(errors);
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
+ }
+
+ private Map<String, Object> getErrorsMap(List<String> fieldErrors) {
+ Map<String, Object> body = new HashMap<>();
+ body.put("code", CommonResultStatus.PARAM_ERROR.getCode());
+ body.put("message", fieldErrors.stream().collect(Collectors.joining(", ")));
+ body.put("errors", fieldErrors);
+ body.put("success", false);
+ return body;
+ }
+
+
+}
+
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/exception/ExceptionControllerAdviceTracker.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/exception/ExceptionControllerAdviceTracker.java
new file mode 100644
index 0000000..2c5fbfb
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/exception/ExceptionControllerAdviceTracker.java
@@ -0,0 +1,30 @@
+package com.ruoyi.web.controller.common.exception;
+
+import com.ruoyi.web.Tool.BT.BencodeUtil;
+import com.ruoyi.web.controller.BT.TrackerController;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+/**
+ * @author plexpt
+ */
+@Slf4j
+@RestControllerAdvice(assignableTypes = TrackerController.class)
+public class ExceptionControllerAdviceTracker {
+
+
+ @ExceptionHandler(TrackerException.class)
+ public String invalidLengthExceptionHandler(TrackerException exception) {
+
+ return BencodeUtil.error(exception.getMessage());
+ }
+
+ @ExceptionHandler(Exception.class)
+ public String e(Exception exception) {
+ return BencodeUtil.error(exception.getMessage());
+ }
+
+
+}
+
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/exception/RocketPTException.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/exception/RocketPTException.java
new file mode 100644
index 0000000..0d47d7d
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/exception/RocketPTException.java
@@ -0,0 +1,36 @@
+package com.ruoyi.web.controller.common.exception;
+
+import com.ruoyi.web.controller.common.CommonResultStatus;
+import com.ruoyi.web.controller.common.ResultStatus;
+
+/**
+ * @author Jrx
+ */
+public class RocketPTException extends RuntimeException {
+ private final ResultStatus status;
+
+ public RocketPTException(ResultStatus status) {
+ super(status.getMessage());
+ this.status = status;
+ }
+
+ public RocketPTException(ResultStatus status, String message) {
+ super(message);
+ this.status = status;
+ }
+
+ public RocketPTException(com.ruoyi.web.controller.common.CommonResultStatus paramError, String message) {
+ super(message);
+ this.status = CommonResultStatus.FAIL;
+ }
+ public RocketPTException(String message) {
+ super(message);
+ this.status = CommonResultStatus.FAIL;
+ }
+
+ public ResultStatus getStatus() {
+ return status;
+ }
+
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/exception/TrackerException.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/exception/TrackerException.java
new file mode 100644
index 0000000..08c49cc
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/exception/TrackerException.java
@@ -0,0 +1,26 @@
+package com.ruoyi.web.controller.common.exception;
+
+
+public class TrackerException extends RuntimeException {
+
+ public TrackerException() {
+ super();
+ }
+
+ public TrackerException(String message) {
+ super(message);
+ }
+
+ public TrackerException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public TrackerException(Throwable cause) {
+ super(cause);
+ }
+
+ protected TrackerException(String message, Throwable cause, boolean enableSuppression,
+ boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/exception/TrackerNoRetryException.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/exception/TrackerNoRetryException.java
new file mode 100644
index 0000000..d3b90cc
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/exception/TrackerNoRetryException.java
@@ -0,0 +1,26 @@
+package com.ruoyi.web.controller.common.exception;
+
+
+public class TrackerNoRetryException extends TrackerException {
+
+ public TrackerNoRetryException() {
+ super();
+ }
+
+ public TrackerNoRetryException(String message) {
+ super(message);
+ }
+
+ public TrackerNoRetryException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public TrackerNoRetryException(Throwable cause) {
+ super(cause);
+ }
+
+ protected TrackerNoRetryException(String message, Throwable cause, boolean enableSuppression,
+ boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/exception/UserException.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/exception/UserException.java
new file mode 100644
index 0000000..6fe7142
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/exception/UserException.java
@@ -0,0 +1,17 @@
+package com.ruoyi.web.controller.common.exception;
+
+import com.ruoyi.web.controller.common.ResultStatus;
+
+/**
+ * @author plexpt
+ */
+public class UserException extends RocketPTException {
+
+ public UserException(ResultStatus status) {
+ super(status);
+ }
+
+ public UserException(ResultStatus status, String message) {
+ super(status, message);
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java
index fe19249..b4b017b 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRegisterController.java
@@ -34,5 +34,31 @@
}
String msg = registerService.register(user);
return StringUtils.isEmpty(msg) ? success() : error(msg);
+ // 创建用户凭证实体
+// UserCredentialEntity userCredentialEntity = new UserCredentialEntity();
+// userCredentialEntity.setId(userEntity.getId());
+// userCredentialEntity.setUsername(param.getUsername());
+// userCredentialEntity.setRegIp(IPUtils.getIpAddr());
+// userCredentialEntity.setRegType(param.getType());
+// String checkCode = passkeyManager.generate(userEntity.getId());
+// userCredentialEntity.setCheckCode(checkCode);
+//
+// // 生成随机盐和密码
+// String salt = RandomUtil.randomString(8);
+// String passkey = passkeyManager.generate(userEntity.getId());
+//
+// userCredentialEntity.setSalt(salt);
+// userCredentialEntity.setPasskey(passkey);
+// String generatedPassword = userCredentialService.generate(param.getPassword(), salt);
+// userCredentialEntity.setPassword(generatedPassword);
+//
+// // 保存用户凭证实体
+// userCredentialService.save(userCredentialEntity);
+// userRoleService.register(userEntity.getId());
+
+
+
+
+
}
}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/AnnounceRequest.java b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/AnnounceRequest.java
new file mode 100644
index 0000000..819e68a
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/AnnounceRequest.java
@@ -0,0 +1,309 @@
+package com.ruoyi.web.dao.BT;
+
+import com.ruoyi.web.domain.BT.TorrentEntity;
+import com.ruoyi.web.domain.sys.UserEntity;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Announce请求参数
+ * <p>
+ * 参考
+ * http://bittorrent.org/beps/bep_0003.html
+ * https://wiki.theory.org/BitTorrentSpecification
+ * https://wiki.theory.org/BitTorrent_Tracker_Protocol
+ */
+@Data
+@Slf4j
+public class AnnounceRequest {
+ //来自客户端的请求如下
+ // qBittorrent v4.5.5
+ //{
+ // "passkey": "xxxxxxxx",
+ // "info_hash": "%5d%df%19%a2%85%03",
+ // "peer_id": "-qB4550-NJ4pxxxkx)za",
+ // "port": 14042,
+ // "uploaded": 0,
+ // "downloaded": 0,
+ // "left": 135621375,
+ // "corrupt": 0,
+ // "key": "B99DE91E",
+ // "event": "started",
+ // "numwant": 200,
+ // "compact": 1,
+ // "no_peer_id": 1,
+ // "supportcrypto": 1,
+ // "redundant": 0,
+ // "ipv6": [
+ // "1111:222",
+ // "1111:333"
+ // ]
+ //}
+
+
+ //Transmission 4.0.3
+ //{
+ // "passkey": "xxxxx",
+ // "info_hash": "xxx",
+ // "peer_id": "-TR4030-a2cdxxxxf42m",
+ // "port": 51413,
+ // "uploaded": 0,
+ // "downloaded": 0,
+ // "left": 135621375,
+ // "numwant": 80,
+ // "key": 2116472635,
+ // "compact": 1,
+ // "supportcrypto": 1,
+ // "event": "started"
+ //}
+
+ //uTorrent 3.6.0
+ //{
+ // "passkey": "xxxxx",
+ // "info_hash": "xxx",
+ // "peer_id": "-UT360S-%24%b7%c1%5eAct%e6sy%2f%e9",
+ // "port": 16623,
+ // "uploaded": 0,
+ // "downloaded": 0,
+ // "left": 135621375,
+ // "corrupt": 0,
+ // "key": "D39D733C",
+ // "event": "started",
+ // "numwant": 200,
+ // "compact": 1,
+ // "no_peer_id": 1
+ //}
+
+ //µTorrent-2.2.1
+ //{
+ // "passkey": "xxxx",
+ // "info_hash": "xxx",
+ // "peer_id": "-UT2210-%d6b2%b50%c1%1d%9b%0a%e7%82%c0",
+ // "port": 63636,
+ // "uploaded": 0,
+ // "downloaded": 0,
+ // "left": 135621375,
+ // "corrupt": 0,
+ // "key": "014E25CF",
+ // "event": "started",
+ // "numwant": 200,
+ // "compact": 1,
+ // "no_peer_id": 1,
+ // "ipv6": "210e%1a3"
+ //}
+
+ //{
+ // "passkey": "xxx",
+ // "info_hash": "xxx",
+ // "peer_id": "-UT355S-%d8%b5g%1cf%a5%80%c8%16%f4%12%0b",
+ // "port": 24295,
+ // "uploaded": 0,
+ // "downloaded": 0,
+ // "left": 135621375,
+ // "corrupt": 0,
+ // "key": "C394F4CA",
+ // "event": "started",
+ // "numwant": 200,
+ // "compact": 1,
+ // "no_peer_id": 1
+ //}
+
+ /**
+ * passkey
+ */
+ private String passkey;
+
+ /**
+ * hash
+ * 20字节SHA1哈希。请注意,该值将是一个b编码的字典
+ */
+ private byte[] infoHash;
+
+ /**
+ * 客户端ID
+ * 客户端在启动时生成的用作客户端唯一ID的url编码的20字节字符串。
+ * 它可以是任何值,也可以是二进制数据。它至少必须对您的本地机器是唯一的,因此应该包括诸如进程ID和可能在启动时记录的时间戳之类的东西。
+ * 有关此字段的常见客户端编码,请参见下面的peer_id。
+ */
+ private String peer_id;
+ private byte[] peerId;
+
+ /**
+ * 客户端ID(16进制)
+ * 2d5452343033302d6264637a71111118386a3170
+ */
+ private String peerIdHex;
+
+ /**
+ * 事件
+ * started, completed, stopped (或为空,这与未指定相同).
+ * 这是一个可选的键,它对应于 started(已开始)、completed(已完成)或 stopped(已停止) (或为空,这等同于不出现)。
+ * 如果未指定,则此请求是定期执行的请求之一。
+ * 当下载首次开始时,会发送使用 started 的公告,当下载完成时,会发送使用 completed 的公告。
+ * 如果文件在开始时就已经完成,那么不会发送 completed。
+ * 当下载器停止下载时,会发送使用 stopped 的公告。
+ * <p>
+ * started:对跟踪器的第一个请求必须包含具有此值的事件键。
+ * stopped:如果客户端正常关闭,则必须发送到跟踪器。
+ * completed:当下载完成时,必须发送给跟踪器。但是,如果客户端启动时下载已经100%完成,则不得发送。大概是为了允许跟踪器仅根据此事件递增“已完成下载”的度量标准。
+ *
+ * @see EventType
+ */
+ private String event = "";
+
+ /**
+ * IPv6地址
+ */
+ private String ipv6;
+
+ /**
+ * IPv4地址
+ */
+ private String ipv4;
+
+ /**
+ * IP地址
+ * <p>
+ * 可选。客户端机器的真实IP地址,以点分四组格式或rfc3513定义的十六进制IPv6地址。
+ * 注意:通常不需要此参数,因为可以从发出HTTP请求的IP地址确定客户端的地址。
+ * 只有在请求到达的IP地址不是客户端的IP地址的情况下才需要此参数。
+ * 这发生在客户端通过代理(或透明Web代理/缓存)与跟踪器通信的情况下。
+ * 当客户端和跟踪器都位于NAT网关的同一本地侧时,也是必需的。
+ * 这样做的原因是,否则跟踪器将给出客户端的内部(RFC1918)地址,这不是可路由的。
+ * 因此,客户端必须显式地声明其(外部,可路由的)IP地址,以便分发给外部对等方。
+ * 各种跟踪器对此参数的处理各不相同。有些只有在请求到达的IP地址位于RFC1918空间时才接受它。
+ * 其他人无条件地尊重它,而另一些人则完全忽略它。对于IPv6地址(例如:2001:db8:1:2::100),它只表明客户端可以通过IPv6进行通信。
+ */
+ private String ip;
+
+ /**
+ * 密钥
+ * <p>
+ * 可选。一个不与任何其他客户端共享的附加标识。它旨在允许客户端在IP地址更改时证明其身份。
+ */
+ private String key;
+
+
+ /**
+ * 端口号
+ * 客户端正在侦听的端口号。为BitTorrent保留的端口通常是6881-6889。
+ */
+ private Integer port;
+
+ /**
+ * 需要的同伴数
+ * http://bittorrent.org/beps/bep_0008.html
+ */
+ private Integer numwant = 200;
+
+ /**
+ * 已下载量
+ * 客户端向跟踪器发送'started'事件后下载的总量(以基数十的ASCII表示)。虽然官方规范中没有明确说明,但共识是这应该是下载的总字节数。
+ */
+ private Long downloaded = -1L;
+
+ /**
+ * 已上传量
+ * 客户端向tracker发送'started'事件后上传的总量(以基数十的ASCII表示)。
+ * 虽然官方规范中没有明确说明,但共识是这应该是上传的总字节数。
+ */
+ private Long uploaded = -1L;
+
+ /**
+ * 剩余量
+ * 此客户端仍需下载的字节数(以基数十的ASCII表示)。澄清:为了达到100%的完成度并获得种子中包含的所有文件,需要下载的字节数。
+ */
+ private Long left = -1L;
+
+ /**
+ * redundant
+ * <p>
+ * http://bittorrent.org/beps/bep_0016.html
+ * <p>
+ * 研究或观察表明,通过超级播种模式,初始播种者创建新种子所需的上传数据量仅为总文件大小的大约105%,而使用标准方法通常需要150-200%。
+ * 一般用途不建议使用超级种子模式。虽然它确实有助于更广泛地分发稀有数据,
+ * 但因为它限制了客户端可以下载的片段的选择,所以它也限制了这些客户端下载他们已经部分检索的片段的数据的能力。
+ * 因此,仅建议初始种子服务器使用超级种子模式。
+ */
+ private Long redundant = 0L;
+
+ /**
+ * 压缩响应
+ * 将此设置为1表示客户端接受紧凑的响应。
+ * <p>
+ * 相关文档
+ * http://bittorrent.org/beps/bep_0007.html
+ * http://bittorrent.org/beps/bep_0010.html
+ * http://bittorrent.org/beps/bep_0015.html
+ * http://bittorrent.org/beps/bep_0052.html
+ * http://bittorrent.org/beps/bep_0023.html
+ */
+ private Integer compact;
+
+ /**
+ * "no_peer_id":表示tracker可以在peers字典中省略peer id字段。如果启用了"compact"选项,则会忽略此选项。
+ */
+ private Integer no_peer_id;
+
+ /**
+ * 做种
+ */
+ private Boolean seeder;
+
+ /**
+ * 远程地址
+ */
+ private String remoteAddr;
+
+ /**
+ * 客户端信息
+ */
+ private String userAgent;
+
+ /**
+ * 想要的摘要信息
+ */
+ private String wantDigest;
+
+ /**
+ * 用户实体
+ */
+ private UserEntity user;
+
+ /**
+ * 用户实体
+ */
+ private TorrentEntity torrent;
+
+
+ public boolean isNoPeerId() {
+ boolean noPeerId = Integer.valueOf(1).equals(getNo_peer_id());
+
+ return noPeerId;
+ }
+
+ /**
+ * 事件
+ * started, completed, stopped (或为空,这与未指定相同).
+ * 这是一个可选的键,它对应于 started(已开始)、completed(已完成)或 stopped(已停止) (或为空,这等同于不出现)。
+ * 如果未指定,则此请求是定期执行的请求之一。
+ * 当下载首次开始时,会发送使用 started 的公告,当下载完成时,会发送使用 completed 的公告。
+ * 如果文件在开始时就已经完成,那么不会发送 completed。
+ * 当下载器停止下载时,会发送使用 stopped 的公告。
+ * <p>
+ * started:对跟踪器的第一个请求必须包含具有此值的事件键。
+ * stopped:如果客户端正常关闭,则必须发送到跟踪器。
+ * completed:当下载完成时,必须发送给跟踪器。但是,如果客户端启动时下载已经100%完成,则不得发送。大概是为了允许跟踪器仅根据此事件递增“已完成下载”的度量标准。
+ * paused: http://bittorrent.org/beps/bep_0021.html
+ */
+ public interface EventType {
+ // started, completed, stopped (或为空,这与未指定相同).
+ //
+ String started = "started";
+ String completed = "completed";
+ String stopped = "stopped";
+ String paused = "paused";
+ String empty = "";
+ }
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/SuggestDao.java b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/SuggestDao.java
new file mode 100644
index 0000000..ac6f92b
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/SuggestDao.java
@@ -0,0 +1,25 @@
+package com.ruoyi.web.dao.BT;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.web.domain.BT.SuggestEntity;
+import com.ruoyi.web.domain.BT.SuggestVo;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * @author yzr
+ */
+@Mapper
+public interface SuggestDao extends BaseMapper<SuggestEntity> {
+
+
+ @Select("SELECT keyword AS suggest, COUNT(*) AS count " +
+ "FROM t_suggest WHERE keyword LIKE #{searchStr} " +
+ "GROUP BY keyword " +
+ "ORDER BY count DESC, keyword DESC LIMIT 10")
+ List<SuggestVo> getSuggestions(@Param("searchStr") String searchStr);
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TagDao.java b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TagDao.java
new file mode 100644
index 0000000..2d3ce55
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TagDao.java
@@ -0,0 +1,18 @@
+package com.ruoyi.web.dao.BT;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.web.domain.BT.TagCatEntity;
+import com.ruoyi.web.domain.BT.TagEntity;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+
+@Mapper
+public interface TagDao extends BaseMapper<TagEntity> {
+ @Select("select * from bt_tag")
+ List<TagEntity> all_tag();
+ @Select("select * from bt_cat")
+ List<TagCatEntity> all_cat();
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TorrentCommentDao.java b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TorrentCommentDao.java
new file mode 100644
index 0000000..2f50a7b
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TorrentCommentDao.java
@@ -0,0 +1,18 @@
+package com.ruoyi.web.dao.BT;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.web.domain.BT.TorrentCommentEntity;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+@Mapper
+public interface TorrentCommentDao extends BaseMapper<TorrentCommentEntity> {
+
+ @Select("select * from bt_torrent_comment where torrent_id=#{torrentId}")
+ List<TorrentCommentEntity> selectByTorrentId(Integer torrentId);
+
+ @Select("select * from bt_torrent_comment where pid=#{pid}")
+ List<TorrentCommentEntity> selectByParentId(Integer pid);
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TorrentDao.java b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TorrentDao.java
new file mode 100644
index 0000000..0ccc066
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TorrentDao.java
@@ -0,0 +1,88 @@
+package com.ruoyi.web.dao.BT;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.ruoyi.web.domain.BT.AdvancedTorrentParam;
+import com.ruoyi.web.domain.BT.TorrentEntity;
+import com.ruoyi.web.domain.BT.TorrentParam;
+import org.apache.ibatis.annotations.*;
+import org.apache.poi.ss.formula.functions.T;
+
+import java.util.List;
+
+@Mapper
+public interface TorrentDao extends BaseMapper<TorrentEntity> {
+ List<TorrentEntity> search(TorrentParam param);
+ @Insert("INSERT INTO bt_torrent (\n" +
+ " info_hash, name, filename, title, subheading, \n" +
+ " cover, description, category, status, file_status, \n" +
+ " reviewer, create_time, update_time, owner, size, \n" +
+ " type, file_count, comments, views, hits, \n" +
+ " visible, anonymous, leechers, seeders, completions, \n" +
+ " remark\n" +
+ ") VALUES (\n" +
+ " #{infoHash}, #{name}, #{filename}, #{title}, #{subheading}, \n" +
+ " #{cover}, #{description}, #{category}, #{status}, #{fileStatus}, \n" +
+ " #{reviewer}, #{createTime}, #{updateTime}, #{owner}, #{size}, \n" +
+ " #{type}, #{fileCount}, #{comments}, #{views}, #{hits}, \n" +
+ " #{visible}, #{anonymous}, #{leechers}, #{seeders}, #{completions}, \n" +
+ " #{remark}\n" +
+ ")")
+ int insert(TorrentEntity entity);
+
+ @Select("select * from bt_torrent where id=#{id}")
+ TorrentEntity selectById(int id);
+
+ @Select("SELECT COUNT(*) FROM bt_torrent WHERE info_hash = #{infoHash}")
+ long selectCountByInfoHash(byte[] infoHash);
+
+
+ default List<TorrentEntity> advancedSearch(AdvancedTorrentParam param) {
+ QueryWrapper<TorrentEntity> wrapper = new QueryWrapper<>();
+
+ // 构建基础查询条件
+ if (param.getCategory() != null) {
+ wrapper.eq("category", param.getCategory());
+ }
+ if (param.getStatus() != null) {
+ wrapper.eq("status", param.getStatus());
+ }
+
+ // 处理排序
+ if (StringUtils.isNotBlank(param.getSortField())) {
+ // 安全映射:防止SQL注入
+ String orderByField = mapSortField(param.getSortField());
+ if (orderByField != null) {
+ boolean isAsc = "asc".equalsIgnoreCase(param.getSortDirection());
+ wrapper.orderBy(true, isAsc, orderByField);
+ }
+ }
+
+ return this.selectList(wrapper);
+ }
+
+ /**
+ * 安全映射排序字段(防止SQL注入)
+ */
+ default String mapSortField(String sortField) {
+ switch (sortField) {
+ case "createTime":
+ return "create_time";
+ case "updateTime":
+ return "update_time";
+ case "completions":
+ return "completions";
+ case "leechers":
+ return "leechers";
+ case "seeders":
+ return "seeders";
+ default:
+ return null; // 忽略非法字段
+ }
+ }
+
+ @Update("UPDATE bt_torrent SET leechers = leechers + 1 WHERE id = #{id}")
+ int incrementLeechers(@Param("id") Integer id);
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TorrentFileDao.java b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TorrentFileDao.java
new file mode 100644
index 0000000..4cda06e
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TorrentFileDao.java
@@ -0,0 +1,12 @@
+package com.ruoyi.web.dao.BT;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.web.domain.BT.TorrentFilesEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * @author yzr
+ */
+@Mapper
+public interface TorrentFileDao extends BaseMapper<TorrentFilesEntity> {
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TorrentPeerDao.java b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TorrentPeerDao.java
new file mode 100644
index 0000000..163471a
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TorrentPeerDao.java
@@ -0,0 +1,14 @@
+package com.ruoyi.web.dao.BT;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.web.domain.BT.TorrentPeerEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 种子Peer
+ *
+ */
+@Mapper
+public interface TorrentPeerDao extends BaseMapper<TorrentPeerEntity> {
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TorrentTagDao.java b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TorrentTagDao.java
new file mode 100644
index 0000000..102ab64
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TorrentTagDao.java
@@ -0,0 +1,10 @@
+package com.ruoyi.web.dao.BT;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.web.domain.BT.TorrentTagEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+
+@Mapper
+public interface TorrentTagDao extends BaseMapper<TorrentTagEntity> {
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TrackerResponse.java b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TrackerResponse.java
new file mode 100644
index 0000000..a5c70b5
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/BT/TrackerResponse.java
@@ -0,0 +1,91 @@
+package com.ruoyi.web.dao.BT;
+
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class TrackerResponse {
+
+
+ String failureReason;
+
+ String warningMessage;
+
+ Integer interval;
+
+ Integer minInterval;
+
+ String trackerId;
+
+ Integer complete;
+ Integer incomplete;
+
+ /**
+ * 下载次数
+ * qbit可以看到
+ */
+ Integer downloaded;
+
+ List<Map<String, Object>> peers;
+
+ public static TrackerResponse build(Integer interval,
+ Integer minInterval,
+ Integer complete,
+ Integer incomplete,
+ List<Map<String, Object>> peers) {
+ TrackerResponse response = new TrackerResponse();
+ response.setInterval(interval);
+ response.setMinInterval(minInterval);
+ response.setComplete(complete);
+ response.setIncomplete(incomplete);
+ response.setPeers(peers);
+
+ return response;
+ }
+
+ /**
+ * Tracker 响应
+ * <p>
+ * Tracker 返回一个 "text/plain" 文档,该文档包含一个 bencoded 字典,其中包含以下键值:
+ * <p>
+ * failure reason(失败原因):如果此键存在,则可能没有其他键。该值是一个人类可读的错误消息,说明请求为何失败(字符串)。
+ * warning message(警告信息):(新的,可选的)类似于 failure reason,但响应仍会被正常处理。警告信息就像错误信息一样显示。
+ * interval(间隔):客户端应在向 tracker 发送常规请求之间等待的秒数。
+ * min interval(最小间隔):(可选的)最小公告间隔。如果此键存在,客户端不得在此时间间隔内重复汇报。
+ * tracker id(跟踪器 ID):客户端应在其下一次汇报中发送回的字符串。如果没有并且先前的公告发送了一个追踪器ID,请不要丢弃旧值;继续使用它。
+ * complete(完成):拥有整个文件的 peer(对等方)数量,即种子seeders (整数类型)。
+ * incomplete(未完成):非种子 peer(对等方)的数量,又名 "leechers"(整数类型)。
+ * peers :(字典模型)该键的值是一个字典列表,每个字典都包含以下键:
+ * ---- peer id (字符串)
+ * ---- ip:peer的IP地址,可以是IPv6(十六进制),IPv4(点状四元组)或DNS名称(字符串)
+ * ---- port(端口):peer的端口号(整数)
+ * peers :(二进制模型)不使用上述的字典模型,peers 键的值可以是一个字符串,由多个6字节组成。前4字节是 IP 地址,后2字节是端口号。所有这些都采用网络字节序(大端序)。
+ * <p>
+ * 如上所述,默认情况下,peers列表长度为 50。如果 torrent 中的peers较少,则列表会更小。
+ * 否则,tracker 会随机选择包含在响应中的peers。
+ * tracker 可能会选择实施更智能的peers选择机制来响应请求。
+ * 例如,可以避免向其他种子报告种子。
+ * <p>
+ * 在发生特定事件(如 stopped 或 completed)时,或者客户端需要了解更多peers时,客户端可以比指定的间隔更频繁地向 tracker 发送请求。但是,频繁地“轰炸” tracker 以获取多个peers是一种不良行为。如果客户端希望在响应中获得大量peer列表,则应指定 numwant 参数。
+ * <p>
+ * 实施者注意:即使30个peer已经足够,官方客户端版本3实际上只有在peer少于30个时才主动形成新连接,并且如果有55个peer,它将拒绝连接。这个值对性能非常重要。
+ * 当新的片段下载完成后,需要向大多数活跃的peer发送 HAVE 消息(见下文)。结果是广播流量的成本与peer的数量成正比。
+ * 超过25个peer后,新的peer不太可能增加下载速度。
+ *
+ * @return
+ */
+ public Map<String, Object> toResultMap() {
+ return Map.of(
+ "interval", interval,
+ "min interval", minInterval,
+ "complete", complete,
+ "incomplete", incomplete,
+ "downloaded", 60,
+ "downloaders", 70,
+ "peers", peers
+ );
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/dao/sys/UserCredentialDao.java b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/sys/UserCredentialDao.java
new file mode 100644
index 0000000..23c4123
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/sys/UserCredentialDao.java
@@ -0,0 +1,27 @@
+package com.ruoyi.web.dao.sys;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.web.domain.sys.UserCredentialEntity;
+import org.apache.ibatis.annotations.Insert;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+
+/**
+ * @author Jrx
+ */
+@Mapper
+public interface UserCredentialDao extends BaseMapper<UserCredentialEntity> {
+
+ @Select("select * from sys_user_credential where id=#{id}")
+ public UserCredentialEntity selectById(int id);
+
+ @Insert(" INSERT INTO sys_user_credential \n" +
+ " (userid,username, password, salt, passkey, check_code, totp, reg_ip, reg_type)\n" +
+ " VALUES \n" +
+ " (#{userid},#{username}, #{password}, #{salt}, #{passkey}, #{checkCode}, #{totp}, #{regIp}, #{regType})")
+ int insert(UserCredentialEntity entity);
+
+
+ @Select("select * from sys_user_credential where userid=#{id}")
+ UserCredentialEntity getById(Integer id);
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/dao/sys/UserDao.java b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/sys/UserDao.java
new file mode 100644
index 0000000..ad641c0
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/dao/sys/UserDao.java
@@ -0,0 +1,38 @@
+package com.ruoyi.web.dao.sys;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.web.domain.sys.UserEntity;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author Jrx
+ */
+
+@Mapper
+public interface UserDao extends BaseMapper<UserEntity> {
+
+
+ /**
+ * 查询用户的所有权限
+ *
+ * @param userId 用户ID
+ */
+ List<String> queryAllPerms(Integer userId);
+
+ @Select("select * from sys_user where id in (#{userIds})")
+ Set<UserEntity> findByIds(Set<Long> userIds);
+
+ Set<String> listUserPermissions(Integer userId);
+
+ @Select("SELECT * FROM sys_user u LEFT JOIN sys_user_credential uc on uc.id = u.user_id WHERE uc.passkey=#{passkey}")
+ UserEntity findUserByPasskey(String passkey);
+
+ @Select("SELECT * FROM sys_user WHERE user_id=#{id}")
+ UserEntity findUserById(Integer id);
+
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/AdvancedTorrentParam.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/AdvancedTorrentParam.java
new file mode 100644
index 0000000..5e10935
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/AdvancedTorrentParam.java
@@ -0,0 +1,19 @@
+package com.ruoyi.web.domain.BT;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class AdvancedTorrentParam extends TorrentParam {
+
+ /**
+ * 排序字段(支持:createTime-创建时间、updateTime-更新时间、completions-下载量、seeders-做种数)
+ */
+ private String sortField;
+
+ /**
+ * 排序方向(asc/desc)
+ */
+ private String sortDirection;
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/CommentForm.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/CommentForm.java
new file mode 100644
index 0000000..e1e4083
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/CommentForm.java
@@ -0,0 +1,18 @@
+package com.ruoyi.web.domain.BT;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Data
+public class CommentForm {
+ @NotNull(message = "种子ID不能为空")
+ private Integer torrentId;
+
+ private Integer pid; // 可选,楼中楼评论
+
+ @NotBlank(message = "评论内容不能为空")
+ @Size(max = 1000, message = "评论内容不能超过1000个字符")
+ private String comment;
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/EntityBase.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/EntityBase.java
new file mode 100644
index 0000000..f009d32
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/EntityBase.java
@@ -0,0 +1,73 @@
+package com.ruoyi.web.domain.BT;
+
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Objects;
+
+/**
+ * Base class for an entity, as explained in the book "Domain Driven Design".
+ * All entities in this project have an identity attribute with type Long and
+ * name id. Inspired by the DDD Sample project.
+ *
+ * @author Christoph Knabe
+ * @author plexpt
+ * @see <a href=
+ * "https://github.com/citerus/dddsample-core/blob/master/src/main/java/se/citerus/dddsample
+ * /domain/shared/Entity.java">Entity
+ * in the DDD Sample</a>
+ * @since 2017-03-06
+ */
+@Setter
+@Getter
+public abstract class EntityBase {
+
+ /**
+ * This identity field has the wrapper class type Long so that an entity which
+ * has not been saved is recognizable by a null identity.
+ */
+ @TableId(type = IdType.AUTO)
+ private Integer id;
+
+ @Override
+ public boolean equals(final Object object) {
+ if (!(object instanceof EntityBase)) {
+ return false;
+ }
+ if (!getClass().equals(object.getClass())) {
+ return false;
+ }
+ final EntityBase that = (EntityBase) object;
+ _checkIdentity(this);
+ _checkIdentity(that);
+ return this.id.equals(that.getId());
+ }
+
+ /**
+ * Checks the passed entity, if it has an identity. It gets an identity only by
+ * saving.
+ *
+ * @param entity the entity to be checked
+ * @throws IllegalStateException the passed entity does not have the identity
+ * attribute set.
+ */
+ private void _checkIdentity(final EntityBase entity) {
+ if (entity.getId() == null) {
+ throw new IllegalStateException("Comparison identity missing in entity: " + entity);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.getId());
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName() + "<" + getId() + ">";
+ }
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/SuggestEntity.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/SuggestEntity.java
new file mode 100644
index 0000000..1d72cdd
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/SuggestEntity.java
@@ -0,0 +1,38 @@
+package com.ruoyi.web.domain.BT;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ *
+ *
+ * @author Jrx
+ */
+@Data
+@TableName("bt_suggest")
+public class SuggestEntity extends EntityBase {
+
+
+ @TableId
+ private Integer id;
+ /**
+ * 角色名称
+ */
+ @Schema(description = "keyword")
+ private String keyword;
+ /**
+ * 创建者ID
+ */
+ @Schema(description = "创建者ID")
+ private Integer createBy;
+ /**
+ * 创建时间
+ */
+ @Schema(description = "创建时间")
+ private LocalDateTime createTime;
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/SuggestVo.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/SuggestVo.java
new file mode 100644
index 0000000..15b41e9
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/SuggestVo.java
@@ -0,0 +1,12 @@
+package com.ruoyi.web.domain.BT;
+
+
+import lombok.Data;
+
+@Data
+public class SuggestVo {
+
+ private String suggest;
+
+ private int count;
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TagCatEntity.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TagCatEntity.java
new file mode 100644
index 0000000..1e3fdbc
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TagCatEntity.java
@@ -0,0 +1,27 @@
+package com.ruoyi.web.domain.BT;
+
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 分类
+ * {@code @author} Jia
+ * {@code @date} 2025/5/26 20:38
+ */
+@Data
+@TableName("bt_cat")
+public class TagCatEntity extends EntityBase{
+ @TableId(type = IdType.AUTO)
+ private Integer id;
+ @Schema(description = "名称")
+ private String name;
+ @Schema(description = "备注")
+ private String remark;
+
+ @Schema(description = "分类类型 可以是 多选、单选 或 必选一个")
+ private Integer type;
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TagEntity.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TagEntity.java
new file mode 100644
index 0000000..ba80ff4
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TagEntity.java
@@ -0,0 +1,45 @@
+package com.ruoyi.web.domain.BT;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 标签
+ *
+ * @author Jrx
+ */
+@Data
+@TableName("bt_tag")
+public class TagEntity extends EntityBase {
+
+
+ @TableId
+ private Integer id;
+ /**
+ * 名称
+ */
+ @Schema(description = "名称")
+ private String name;
+
+ @Schema(description = "备注")
+ private String remark;
+
+ @Schema(description = "标签分类")
+ private Integer cat;
+ /**
+ * 创建者ID
+ */
+ @Schema(description = "创建者ID")
+ private Integer createBy;
+ /**
+ * 创建时间
+ */
+ @Schema(description = "创建时间")
+ private LocalDateTime createTime;
+
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentAddParam.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentAddParam.java
new file mode 100644
index 0000000..a72125f
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentAddParam.java
@@ -0,0 +1,67 @@
+package com.ruoyi.web.domain.BT;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class TorrentAddParam {
+
+
+ /**
+ * 名称
+ */
+ @NotEmpty
+ @Schema(description = "简称")
+ private String name;
+
+ /**
+ * 标题
+ */
+ @NotEmpty
+ @Schema(description = "标题")
+ private String title;
+ /**
+ * 简介副标题
+ */
+ @NotEmpty
+ @Schema(description = "副标题")
+ private String subheading;
+ /**
+ * 封面
+ */
+ @Schema(description = "封面")
+ private String cover;
+ /**
+ * 描述
+ */
+ @NotEmpty
+ @Schema(description = "描述")
+ private String description;
+
+ /**
+ * 类别
+ */
+ @NotNull
+ @Schema(description = "类别")
+ private Integer category;
+
+ /**
+ * 状态 0 候选中 1 已发布 2 已下架
+ */
+ @Schema(description = "状态 0 候选中 1 已发布 2 已下架")
+ private Integer status;
+
+ /**
+ * 是否匿名 0 否 1 是
+ */
+ @Schema(description = "是否匿名 0 否 1 是")
+ private Integer anonymous;
+
+
+ @Schema(description = "备注")
+ private String remark;
+
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentAuditParam.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentAuditParam.java
new file mode 100644
index 0000000..d4b9c67
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentAuditParam.java
@@ -0,0 +1,26 @@
+package com.ruoyi.web.domain.BT;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class TorrentAuditParam {
+
+ @NotNull
+ Integer id;
+
+
+ /**
+ * 审核状态 1 通过 2 不通过
+ */
+ @Schema(description = "状态 0 候选中 1 已发布 2 已下架")
+ @NotNull
+ Integer status;
+
+
+ @Schema(description = "备注")
+ String remark;
+
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentCommentEntity.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentCommentEntity.java
new file mode 100644
index 0000000..237ea38
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentCommentEntity.java
@@ -0,0 +1,51 @@
+package com.ruoyi.web.domain.BT;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * 种子文件列表
+ *
+ * @author Jrx
+ */
+@Data
+@NoArgsConstructor
+@TableName("bt_torrent_comment")
+public class TorrentCommentEntity extends EntityBase {
+
+ /**
+ * ID
+ */
+ Integer id;
+
+ /**
+ * 关联的种子ID
+ */
+ Integer torrentId;
+
+ /**
+ * 用户ID
+ */
+ Integer userId;
+
+ /**
+ * 父级ID 楼中楼才有
+ */
+ Integer pid;
+
+ /**
+ * comment内容
+ */
+ String comment;
+
+
+ /**
+ * 添加日期
+ */
+ private LocalDateTime createTime;
+
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentCommentVO.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentCommentVO.java
new file mode 100644
index 0000000..822a8e8
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentCommentVO.java
@@ -0,0 +1,21 @@
+package com.ruoyi.web.domain.BT;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+public class TorrentCommentVO {
+ private Integer id;
+ private Integer torrentId;
+ private Integer userId;
+ private String username; // 用户昵称
+ private String avatar; // 用户头像
+ private Integer pid;
+ private String comment;
+ private LocalDateTime createTime;
+ private Boolean isLiked; // 是否已点赞(可选)
+ // 可能包含子评论列表(楼中楼)
+ private List<TorrentCommentVO> children;
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentDto.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentDto.java
new file mode 100644
index 0000000..2e632a8
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentDto.java
@@ -0,0 +1,34 @@
+package com.ruoyi.web.domain.BT;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+/**
+ * torrent基本信息
+ */
+@Data
+@TableName(value = "bt_torrent_info")
+public class TorrentDto {
+
+ @TableId(type = IdType.AUTO)
+ private Integer id;
+ // 创建时间
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime created;
+ // 更新时间
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private LocalDateTime modified;
+ private Long fkUserAuthId;
+ private Long fkTorrentDiscountId;
+ private byte[] ukInfoHash;
+ private String fileName;
+ private Long fileSize;
+ private String filePath;
+ private Long torrentSize;
+ private Long torrentCount;
+ private Boolean isDelete;
+ private Map<String, Object> dict;
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentEntity.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentEntity.java
new file mode 100644
index 0000000..0592102
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentEntity.java
@@ -0,0 +1,177 @@
+package com.ruoyi.web.domain.BT;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("bt_torrent")
+public class TorrentEntity {
+
+ /**
+ *
+ */
+ @TableId
+ private Integer id;
+ /**
+ * 种子哈希
+ */
+ private byte[] infoHash;
+ /**
+ * 名称
+ */
+ private String name;
+ /**
+ * 上传文件名
+ */
+ private String filename;
+ /**
+ * 标题
+ */
+ private String title;
+ /**
+ * 简介副标题
+ */
+ private String subheading;
+ /**
+ * 封面
+ */
+ private String cover;
+ /**
+ * 描述
+ */
+ private String description;
+
+ /**
+ * 类别
+ */
+ private Integer category;
+
+ /**
+ * 状态
+ *
+ * @see Status
+ */
+ private Integer status;
+
+ /**
+ * 文件状态 0 未上传 1 已上传
+ */
+ private Integer fileStatus;
+ /**
+ * 审核人
+ */
+ private Integer reviewer;
+
+
+ /**
+ * 添加日期
+ */
+ private LocalDateTime createTime;
+
+
+ /**
+ * 修改日期
+ */
+ private LocalDateTime updateTime;
+
+ /**
+ * 拥有者
+ */
+ private Integer owner;
+ /**
+ * 文件大小
+ */
+ private Long size;
+ /**
+ * 类型
+ * single(1)
+ * multi(2)
+ */
+ private Integer type;
+ /**
+ * 文件数量
+ */
+ private Integer fileCount;
+
+ /**
+ * 评论数
+ */
+ private Integer comments;
+ /**
+ * 浏览次数
+ */
+ private Integer views;
+ /**
+ * 点击次数
+ */
+ private Integer hits;
+
+ /**
+ * 可见性
+ */
+ private Integer visible;
+
+ /**
+ * 是否匿名
+ */
+ private Integer anonymous;
+
+
+ /**
+ * 下载数
+ */
+ private Integer leechers;
+ /**
+ * 做种数
+ */
+ private Integer seeders;
+
+ /**
+ * 做种完成次数
+ */
+ private Integer completions;
+
+ /**
+ *
+ */
+ private String remark;
+
+ /**
+ * 种子状态
+ * 0 候选中 1 已发布 2 审核不通过 3 已上架修改重审中 10 已下架
+ */
+ public interface Status {
+
+ int CANDIDATE = 0;
+
+ int PUBLISHED = 1;
+
+ int AUDIT_NOT_PASSED = 2;
+
+ int RETRIAL = 3;
+
+ int REMOVED = 10;
+
+ }
+
+ @RequiredArgsConstructor
+ public enum Type {
+ single(1),
+ multi(2);
+
+ @Getter
+ @EnumValue
+ private final int code;
+ }
+
+ public boolean isStatusOK() {
+ return status == 1;
+ }
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentFileVo.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentFileVo.java
new file mode 100644
index 0000000..d61f148
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentFileVo.java
@@ -0,0 +1,15 @@
+package com.ruoyi.web.domain.BT;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+@Getter
+@Setter
+@ToString
+public class TorrentFileVo {
+ private String fillName;
+ private String announce;
+ private byte[] hash;
+ private long length;
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentFilesEntity.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentFilesEntity.java
new file mode 100644
index 0000000..719ba17
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentFilesEntity.java
@@ -0,0 +1,41 @@
+package com.ruoyi.web.domain.BT;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 种子文件列表
+ *
+ * @author Jrx
+ */
+@Data
+@NoArgsConstructor
+@TableName("bt_torrent_files")
+public class TorrentFilesEntity extends EntityBase {
+
+ /**
+ * ID
+ */
+ Integer id;
+
+ /**
+ * 关联的种子ID
+ */
+ Integer torrentId;
+ /**
+ * 父级ID
+ */
+ Integer pid;
+
+ /**
+ * 文件的名称
+ */
+ String filename;
+
+ /**
+ * 文件的大小,单位为字节。
+ */
+ Long size;
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentParam.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentParam.java
new file mode 100644
index 0000000..410ad51
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentParam.java
@@ -0,0 +1,56 @@
+package com.ruoyi.web.domain.BT;
+
+import com.ruoyi.web.controller.common.base.OrderPageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * 种子查询
+ */
+@Data
+@Schema(description = "种子查询")
+public class TorrentParam extends OrderPageParam {
+
+
+ @Schema(description = "关键字")
+ String keyword;
+
+
+ @Schema(description = "分类")
+ Integer category;
+
+ @Schema(description = "状态")
+ Integer status;
+
+ @Schema(description = "审核状态")
+ Integer reviewStatus;
+
+ //促销种子?
+ @Schema(description = "促销种子?")
+ String free;
+
+
+ private Set<String> likeExpressions;
+
+ public void buildLike() {
+ likeExpressions = new LinkedHashSet<>();
+ if (StringUtils.isEmpty(keyword)) {
+ return;
+ }
+
+ keyword = keyword.replace(".", " ");
+ String[] searchstrExploded = keyword.split(" ");
+
+
+ for (int i = 0; i < searchstrExploded.length && i < 10; i++) {
+ String searchstrElement = searchstrExploded[i].trim();
+ likeExpressions.add(searchstrElement);
+ }
+
+
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentPeerEntity.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentPeerEntity.java
new file mode 100644
index 0000000..ecaf6c3
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentPeerEntity.java
@@ -0,0 +1,131 @@
+package com.ruoyi.web.domain.BT;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * 种子文件列表
+ *
+ * @author plexpt
+ */
+@Data
+@NoArgsConstructor
+@TableName("bt_torrent_peer")
+public class TorrentPeerEntity extends EntityBase {
+
+ /**
+ * ID
+ */
+ Integer id;
+
+ /**
+ * 关联的种子ID
+ */
+ Integer torrentId;
+
+ /**
+ * 用户ID
+ */
+ Integer userId;
+
+ /**
+ * Peer的客户端ID,表示在P2P网络中的唯一标识,如 "-qB4520-Lw~GW1WMV5ZO"
+ */
+ String peerId;
+ String peerIdHex;
+
+ /**
+ * 用户IP地址
+ */
+ String ip;
+
+ /**
+ * 用户IPV6地址
+ */
+ String ipv6;
+
+ /**
+ * 用户的网络端口号
+ */
+ Integer port;
+
+ /**
+ * 用户已经上传的数据量(单位:字节)
+ */
+ Long uploaded;
+
+ /**
+ * 用户已经下载的数据量(单位:字节)
+ */
+ Long downloaded;
+
+ /**
+ * 用户剩余需要下载的数据量(单位:字节)
+ */
+ Long remaining;
+
+ /**
+ * 本次会话的下载量偏移(单位:字节)
+ */
+ Long downloadoffset;
+
+ /**
+ * 本次会话的上传量偏移(单位:字节)
+ */
+ Long uploadoffset;
+
+ /**
+ * 是否是做种者,如果是,则值为true
+ */
+ Boolean seeder;
+
+ /**
+ * 是否可连接,如果是,则值为true
+ */
+ Boolean connectable;
+
+ /**
+ * 是否使用种子盒,如果是,则值为true
+ */
+ Boolean seedBox;
+
+ /**
+ * 用户使用的客户端的用户代理,例如 "qBittorrent/4.5.2"
+ */
+ String userAgent;
+
+ /**
+ * 用户的秘钥,用于身份验证
+ */
+ String passkey;
+
+ /**
+ * 用户添加种子的时间
+ */
+ LocalDateTime createTime;
+
+ /**
+ * 用户开始下载或者上传种子的时间
+ */
+ LocalDateTime startTime;
+
+ /**
+ * 用户完成下载或者上传种子的时间
+ */
+ LocalDateTime finishTime;
+
+ /**
+ * 用户最近一次连接到tracker的时间
+ */
+ LocalDateTime lastAnnounce;
+
+ /**
+ * 用户上一次连接到tracker的时间
+ */
+ LocalDateTime preAnnounce;
+
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentTagEntity.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentTagEntity.java
new file mode 100644
index 0000000..399525f
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentTagEntity.java
@@ -0,0 +1,27 @@
+package com.ruoyi.web.domain.BT;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 种子标签
+ *
+ * @author Jrx
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@TableName("bt_torrent_tag")
+public class TorrentTagEntity {
+
+ @TableId
+ private Integer id;
+
+ private Integer torrentId;
+
+ private Integer tagId;
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentVO.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentVO.java
new file mode 100644
index 0000000..d8570e5
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TorrentVO.java
@@ -0,0 +1,155 @@
+package com.ruoyi.web.domain.BT;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+public class TorrentVO {
+
+ private Integer id;
+
+ /**
+ * 名称
+ */
+ @NotEmpty
+ @Schema(description = "简称")
+ private String name;
+
+ /**
+ * 标题
+ */
+ @NotEmpty
+ @Schema(description = "标题")
+ private String title;
+ /**
+ * 简介副标题
+ */
+ @NotEmpty
+ @Schema(description = "副标题")
+ private String subheading;
+ /**
+ * 封面
+ */
+ @Schema(description = "封面")
+ private String cover;
+ /**
+ * 描述
+ */
+ @NotEmpty
+ @Schema(description = "描述")
+ private String description;
+
+ /**
+ * 类别
+ */
+ @NotNull
+ @Schema(description = "类别")
+ private Integer category;
+
+ /**
+ * 状态 0 候选中 1 已发布 2 已下架
+ */
+ @Schema(description = "状态 0 候选中 1 已发布 2 已下架")
+ private Integer status;
+
+ /**
+ * 是否匿名 0 否 1 是
+ */
+ @Schema(description = "是否匿名 0 否 1 是")
+ private Integer anonymous;
+
+
+ @Schema(description = "备注")
+ private String remark;
+
+
+ /**
+ * 文件状态 0 未上传 1 已上传
+ */
+ private Integer fileStatus;
+ /**
+ * 审核人
+ */
+ @Schema(description = "审核人")
+ private Integer reviewer;
+
+
+ /**
+ * 添加日期
+ */
+ @Schema(description = "添加日期")
+ private LocalDateTime createTime;
+
+
+ /**
+ * 修改日期
+ */
+ @Schema(description = "修改日期")
+ private LocalDateTime updateTime;
+
+ /**
+ * 拥有者
+ */
+ @Schema(description = "拥有者")
+ private Integer owner;
+ /**
+ * 文件大小
+ */
+ @Schema(description = "文件大小")
+ private Long size;
+ /**
+ * 类型
+ * single(1)
+ * multi(2)
+ */
+ private Integer type;
+ /**
+ * 文件数量
+ */
+ @Schema(description = "文件数量")
+ private Integer fileCount;
+
+ /**
+ * 评论数
+ */
+ @Schema(description = "评论数")
+ private Integer comments;
+ /**
+ * 浏览次数
+ */
+ @Schema(description = "浏览次数")
+ private Integer views;
+ /**
+ * 点击次数
+ */
+ @Schema(description = "点击次数")
+ private Integer hits;
+
+ /**
+ * 可见性
+ */
+ @Schema(description = "可见性")
+ private Integer visible;
+
+ /**
+ * 下载数
+ */
+ @Schema(description = "下载数")
+ private Integer leechers;
+ /**
+ * 做种数
+ */
+ @Schema(description = "做种数")
+ private Integer seeders;
+
+ /**
+ * 完成次数
+ */
+ @Schema(description = "完成次数")
+ private Integer completions;
+
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TotpVo.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TotpVo.java
new file mode 100644
index 0000000..9c37619
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TotpVo.java
@@ -0,0 +1,26 @@
+package com.ruoyi.web.domain.BT;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import org.hibernate.validator.constraints.Length;
+
+@Data
+@Schema(description = "TOTP")
+public class TotpVo {
+
+ @NotEmpty
+ @Schema(description = "密钥")
+ String key;
+
+ @Schema(description = "二维码内容")
+ String uri;
+
+
+ @NotNull
+ @Length(min = 6, max = 6)
+ @Schema(description = "6位动态验证码")
+ Integer code;
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TrackerResponse.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TrackerResponse.java
new file mode 100644
index 0000000..4215dc3
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/BT/TrackerResponse.java
@@ -0,0 +1,91 @@
+package com.ruoyi.web.domain.BT;
+
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class TrackerResponse {
+
+
+ String failureReason;
+
+ String warningMessage;
+
+ Integer interval;
+
+ Integer minInterval;
+
+ String trackerId;
+
+ Integer complete;
+ Integer incomplete;
+
+ /**
+ * 下载次数
+ * qbit可以看到
+ */
+ Integer downloaded;
+
+ List<Map<String, Object>> peers;
+
+ public static TrackerResponse build(Integer interval,
+ Integer minInterval,
+ Integer complete,
+ Integer incomplete,
+ List<Map<String, Object>> peers) {
+ TrackerResponse response = new TrackerResponse();
+ response.setInterval(interval);
+ response.setMinInterval(minInterval);
+ response.setComplete(complete);
+ response.setIncomplete(incomplete);
+ response.setPeers(peers);
+
+ return response;
+ }
+
+ /**
+ * Tracker 响应
+ * <p>
+ * Tracker 返回一个 "text/plain" 文档,该文档包含一个 bencoded 字典,其中包含以下键值:
+ * <p>
+ * failure reason(失败原因):如果此键存在,则可能没有其他键。该值是一个人类可读的错误消息,说明请求为何失败(字符串)。
+ * warning message(警告信息):(新的,可选的)类似于 failure reason,但响应仍会被正常处理。警告信息就像错误信息一样显示。
+ * interval(间隔):客户端应在向 tracker 发送常规请求之间等待的秒数。
+ * min interval(最小间隔):(可选的)最小公告间隔。如果此键存在,客户端不得在此时间间隔内重复汇报。
+ * tracker id(跟踪器 ID):客户端应在其下一次汇报中发送回的字符串。如果没有并且先前的公告发送了一个追踪器ID,请不要丢弃旧值;继续使用它。
+ * complete(完成):拥有整个文件的 peer(对等方)数量,即种子seeders (整数类型)。
+ * incomplete(未完成):非种子 peer(对等方)的数量,又名 "leechers"(整数类型)。
+ * peers :(字典模型)该键的值是一个字典列表,每个字典都包含以下键:
+ * ---- peer id (字符串)
+ * ---- ip:peer的IP地址,可以是IPv6(十六进制),IPv4(点状四元组)或DNS名称(字符串)
+ * ---- port(端口):peer的端口号(整数)
+ * peers :(二进制模型)不使用上述的字典模型,peers 键的值可以是一个字符串,由多个6字节组成。前4字节是 IP 地址,后2字节是端口号。所有这些都采用网络字节序(大端序)。
+ * <p>
+ * 如上所述,默认情况下,peers列表长度为 50。如果 torrent 中的peers较少,则列表会更小。
+ * 否则,tracker 会随机选择包含在响应中的peers。
+ * tracker 可能会选择实施更智能的peers选择机制来响应请求。
+ * 例如,可以避免向其他种子报告种子。
+ * <p>
+ * 在发生特定事件(如 stopped 或 completed)时,或者客户端需要了解更多peers时,客户端可以比指定的间隔更频繁地向 tracker 发送请求。但是,频繁地“轰炸” tracker 以获取多个peers是一种不良行为。如果客户端希望在响应中获得大量peer列表,则应指定 numwant 参数。
+ * <p>
+ * 实施者注意:即使30个peer已经足够,官方客户端版本3实际上只有在peer少于30个时才主动形成新连接,并且如果有55个peer,它将拒绝连接。这个值对性能非常重要。
+ * 当新的片段下载完成后,需要向大多数活跃的peer发送 HAVE 消息(见下文)。结果是广播流量的成本与peer的数量成正比。
+ * 超过25个peer后,新的peer不太可能增加下载速度。
+ *
+ * @return
+ */
+ public Map<String, Object> toResultMap() {
+ return Map.of(
+ "interval", interval,
+ "min interval", minInterval,
+ "complete", complete,
+ "incomplete", incomplete,
+ "downloaded", 60,
+ "downloaders", 70,
+ "peers", peers
+ );
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/sys/SysUser.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/sys/SysUser.java
new file mode 100644
index 0000000..345e77d
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/sys/SysUser.java
@@ -0,0 +1,32 @@
+//package com.ruoyi.web.domain.sys;
+//
+//import com.baomidou.mybatisplus.annotation.TableId;
+//import com.baomidou.mybatisplus.annotation.TableName;
+//import lombok.Data;
+//
+//@Data
+//@TableName("sys_user")
+//public class SysUser {
+// @TableId("user_id")
+// private Long userId;
+// private String userName;
+// private String nickName;
+// private String userType;
+// private String email;
+// private String phonenumber;
+// private String sex;
+// private String avatar;
+// private String password;
+// private Long downloadCount;
+// private Long uploadCount;
+// private int status;
+// private String loginIp;
+// private Data loginDate;
+// private String createBy;
+// private String updateTime;
+// private String remark;
+//
+// public boolean isUserOK() {
+// return this.status == 1;
+// }
+//}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/sys/UserCredentialEntity.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/sys/UserCredentialEntity.java
new file mode 100644
index 0000000..59f5de6
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/sys/UserCredentialEntity.java
@@ -0,0 +1,74 @@
+package com.ruoyi.web.domain.sys;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.ruoyi.web.domain.BT.EntityBase;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 用户凭证 敏感信息 user_credential
+ *
+ * @author Jrx
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@TableName("sys_user_credential")
+public class UserCredentialEntity extends EntityBase {
+ /**
+ * ID
+ */
+ @TableId(type = IdType.INPUT)
+ private Integer id;
+
+ private Integer userid;
+
+
+ /**
+ * 用户名
+ */
+ private String username;
+
+ /**
+ * password
+ */
+ private String password;
+
+ /**
+ * salt
+ */
+ private String salt;
+
+ /**
+ * passkey
+ */
+ private String passkey;
+ /**
+ * 邮箱校验码
+ */
+ private String checkCode;
+
+ /**
+ * 二步验证 totp
+ */
+ private String totp;
+
+ /**
+ * 注册IP
+ */
+ @Schema(description = "注册IP")
+ private String regIp;
+
+ /**
+ * 注册类型
+ * 0.手动添加
+ * 1.开放注册
+ * 2.受邀注册
+ * 3.自助答题注册
+ */
+ private Integer regType;
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/domain/sys/UserEntity.java b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/sys/UserEntity.java
new file mode 100644
index 0000000..aa85880
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/domain/sys/UserEntity.java
@@ -0,0 +1,111 @@
+package com.ruoyi.web.domain.sys;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+
+/**
+ * 用户
+ * @author Jrx
+ */
+@Data
+@TableName("sys_user")
+public class UserEntity implements Serializable {
+
+ @TableId(value = "user_id", type = IdType.AUTO)
+ private Long user_id;
+ private String user_name;
+ private String nick_name;
+ private String user_type;
+ private String email;
+ private String phonenumber;
+
+ // 数据库字段为sex,实体类使用gender更符合语义
+ @TableField("sex")
+ private String gender;
+
+ private String avatar;
+ private String password;
+
+ // 状态字段映射
+ @TableField("status")
+ private Integer status;
+
+ private String login_ip;
+
+ // 日期类型统一使用LocalDateTime
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime login_date;
+
+ private String create_by;
+
+ // 修正字段名与数据库一致
+ @TableField("create_time")
+ private LocalDateTime create_time;
+
+ private String update_by;
+
+ // 修正字段名与数据库一致
+ @TableField("update_time")
+ private LocalDateTime update_time;
+
+ private String remark;
+ private String full_name;
+
+ // 使用Integer而非自定义枚举,与数据库字段类型一致
+ private Integer state;
+
+ private LocalDateTime added;
+ private LocalDateTime last_login;
+ private LocalDateTime last_access;
+ private LocalDateTime last_home;
+ private LocalDateTime last_offer;
+ private LocalDateTime forum_access;
+ private LocalDateTime last_staffmsg;
+ private LocalDateTime last_pm;
+ private LocalDateTime last_comment;
+ private LocalDateTime last_post;
+ private LocalDateTime last_active;
+
+ private Integer privacy;
+ private String reg_ip;
+ private Integer level;
+ private Long seedtime;
+ private Long leechtime;
+
+ @Schema(description = "真实上传量")
+ private Long real_uploaded;
+
+ @Schema(description = "真实下载量")
+ private Long real_downloaded;
+
+ private String modcomment;
+ private Long warning_by;
+ private Integer warning_times;
+ private Boolean warning;
+
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime warning_until;
+
+ private Long download;
+ private Long upload;
+ private Integer invited_by;
+ private Long bonus;
+ private Long exp;
+ private String check_code;
+ private Integer reg_type;
+
+ // 状态辅助方法
+ public boolean isUserLocked() {
+ return this.state != null && this.state == 1;
+ }
+
+ public boolean isUserOK() {
+ return this.state != null && this.state == 0;
+ }
+}