feat(core): 实现用户积分计算逻辑
- 新增积分规则处理模块
- 集成积分计算到相关业务流程中
fix(file): 修复上传工具中路径乱码和非法字符问题
- 使用 UTF-8 编码统一路径解析
- 增加路径合法性校验
fix(user): 修正用户 Passkey 生成与验证流程
- 修复部分场景下 Passkey 未生成的问题
- 调整 Passkey 校验逻辑,确保唯一性和安全性
Change-Id: Id5eacb20f354a07de01413474095c2b2b9b49231
diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml
index 0f7fa42..1fbe9d6 100644
--- a/ruoyi-admin/pom.xml
+++ b/ruoyi-admin/pom.xml
@@ -22,8 +22,11 @@
<artifactId>jackson-databind</artifactId>
</dependency>
-
-
+ <dependency>
+ <groupId>com.aliyun.oss</groupId>
+ <artifactId>aliyun-sdk-oss</artifactId>
+ <version>3.18.2</version>
+ </dependency>
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/announce/controller/AnnounceController.java b/ruoyi-admin/src/main/java/com/ruoyi/announce/controller/AnnounceController.java
index f3bd624..93c3f50 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/announce/controller/AnnounceController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/announce/controller/AnnounceController.java
@@ -1,8 +1,17 @@
package com.ruoyi.announce.controller;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ruoyi.announce.util.BencodeEncoder;
+import com.ruoyi.authentication.domain.SysUserPasskey;
+import com.ruoyi.authentication.service.ISysUserPasskeyService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.announce.service.IAnnounceService;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.utils.SecurityUtils;
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.net.URLCodec;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@@ -10,52 +19,182 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
@RestController
public class AnnounceController extends BaseController {
+ // 在你的 Controller 中:
@Autowired
private IAnnounceService announceService;
+ @Autowired
+ private BencodeEncoder bencodeEncoder;
+
+
+
+
+
+
+ private byte[] decodeInfoHashAndPeerId(String param) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ for (int i = 0; i < param.length(); ) {
+ char c = param.charAt(i);
+ if (c == '%' && i + 2 < param.length()) {
+ char hi = param.charAt(i + 1);
+ char lo = param.charAt(i + 2);
+ if (isHexDigit(hi) && isHexDigit(lo)) {
+ int value = Character.digit(hi, 16) << 4 | Character.digit(lo, 16);
+ bos.write(value);
+ i += 3;
+ continue;
+ }
+ }
+ // 不是合法的 %xx,直接跳过或处理为异常字符
+ bos.write((byte) c); // 或者 throw new IllegalArgumentException(...)
+ i++;
+ }
+ return bos.toByteArray();
+ }
+
+ private boolean isHexDigit(char c) {
+ return (c >= '0' && c <= '9') ||
+ (c >= 'A' && c <= 'F') ||
+ (c >= 'a' && c <= 'f');
+ }
+
+
+
+
/**
* BT Tracker /announce 接口
* 接收客户端(qBittorrent 等)发来的 announce 请求,返回 bencoded peers 列表
*/
- @GetMapping(value = "/announce", produces = "application/x-bittorrent")
- public void announce(
- HttpServletRequest request,
- HttpServletResponse response,
- @RequestParam("info_hash") String infoHashParam,
- @RequestParam("peer_id") String peerIdParam,
- @RequestParam("port") int port,
- @RequestParam("uploaded") long uploaded,
- @RequestParam("downloaded") long downloaded,
- @RequestParam("left") long left,
- @RequestParam(value = "event", required = false) String event,
- @RequestParam("passkey") String passkey
- ) throws Exception {
- // 1. URL Decode 得到原始二进制
- byte[] infoHash = URLDecoder.decode(infoHashParam, StandardCharsets.ISO_8859_1.name())
- .getBytes(StandardCharsets.ISO_8859_1);
- byte[] peerId = URLDecoder.decode(peerIdParam, StandardCharsets.ISO_8859_1.name())
- .getBytes(StandardCharsets.ISO_8859_1);
- // 2. 处理 announce 请求(验证 passkey,更新 peer 列表,获取 peers 信息)
+ @GetMapping(value = "/announce", produces = "application/x-bittorrent")
+ public void announce(HttpServletRequest request, HttpServletResponse response) throws Exception {
+ // —— 打印所有参数 ——
+ Map<String, String[]> parameterMap = request.getParameterMap();
+ System.out.println("—— 请求参数列表 ——");
+ for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
+ String paramName = entry.getKey();
+ String[] paramValues = entry.getValue();
+ System.out.println(paramName + " = " + Arrays.toString(paramValues));
+ }
+
+
+ // —— 4. 获取参数(不使用 @RequestParam) ——
+ String infoHashParam = request.getParameter("info_hash");
+ String peerIdParam = request.getParameter("peer_id");
+ String portStr = request.getParameter("port");
+ String uploadedStr = request.getParameter("uploaded");
+ String downloadedStr = request.getParameter("downloaded");
+ String leftStr = request.getParameter("left");
+ String event = request.getParameter("event");
+ String passkey = request.getParameter("passkey");
+ String ip = request.getRemoteAddr();
+
+ System.out.println("Received request with parameters:");
+ System.out.println("info_hash: " + infoHashParam);
+ System.out.println("peer_id: " + peerIdParam);
+ System.out.println("port: " + portStr);
+ System.out.println("uploaded: " + uploadedStr);
+ System.out.println("downloaded: " + downloadedStr);
+ System.out.println("left: " + leftStr);
+ System.out.println("event: " + event);
+ System.out.println("passkey: " + passkey);
+ System.out.println("client IP: " + ip);
+
+
+
+
+
+
+
+
+ if ((infoHashParam == null && peerIdParam == null && portStr == null &&
+ uploadedStr == null && downloadedStr == null && leftStr == null)) {
+
+ // 只要 passkey 和 ip 不为空,则认为是 heartbeat
+ if (passkey != null && ip != null) {
+ // 处理为心跳请求
+ Map<String, Object> reply = announceService.handleHeartbeat(ip);
+ byte[] bencodeReply = bencodeEncoder.encodeBencode(reply);
+
+ response.setContentType("text/plain");
+ response.getOutputStream().write(bencodeReply);
+ return;
+ }
+
+ // 如果不是心跳,就是错误请求
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ response.setContentType("text/plain");
+ response.getWriter().write("Missing required announce parameters.");
+ return;
+ }
+
+ // —— 5. 转换参数类型 ——
+ assert portStr != null;
+ int port = Integer.parseInt(portStr);
+ long uploaded = Long.parseLong(uploadedStr);
+ long downloaded = Long.parseLong(downloadedStr);
+ long left = Long.parseLong(leftStr);
+
+ // 确保 URL 解码不会失败,使用更健壮的方法
+ assert infoHashParam != null;
+ byte[] infoHash = decodeInfoHashAndPeerId(infoHashParam);
+ byte[] peerId = decodeInfoHashAndPeerId(peerIdParam);
+
+ // —— 7. 调用业务逻辑处理 ——
Map<String, Object> reply = announceService.handleAnnounce(
infoHash, peerId, port, uploaded, downloaded, left, event, passkey,
request.getRemoteAddr()
);
- // 3. bencode 编码并返回给客户端
+ // —— 8. 返回 Bencode 编码的 tracker 响应 ——
response.setStatus(HttpServletResponse.SC_OK);
response.setHeader("Content-Type", "application/x-bittorrent");
try (var out = response.getOutputStream()) {
- byte[] bencoded = announceService.encodeBencode(reply);
+ byte[] bencoded = bencodeEncoder.encodeBencode(reply);
+
out.write(bencoded);
out.flush();
}
}
+ // Scrape 接口
+ @GetMapping(value = "/scrape", produces = "application/x-bittorrent")
+ public void scrape(@RequestParam("info_hash") String[] infoHashes, HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ // 收集所有 info_hash 对应的统计数据
+ Map<String, Object> scrapeStats = announceService.handleScrape(infoHashes);
+
+ // 将统计数据转换为 Bencode 格式并返回
+ byte[] bencodedReply = bencodeEncoder.encodeBencode(scrapeStats);
+
+ response.setStatus(HttpServletResponse.SC_OK);
+ response.setHeader("Content-Type", "application/x-bittorrent");
+ try (var out = response.getOutputStream()) {
+ out.write(bencodedReply);
+ out.flush();
+ }
+ }
+
+ /**
+ * 查询当前所有活跃的 Peer 节点
+ */
+ @GetMapping(value = "/peers", produces = "application/json")
+ public TableDataInfo getAllPeers() {
+ List<Map<String, Object>> peers = announceService.getAllPeers();
+ System.out.println(peers);
+ return getDataTable(peers);
+ }
}
+
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/announce/service/IAnnounceService.java b/ruoyi-admin/src/main/java/com/ruoyi/announce/service/IAnnounceService.java
index e6e7aa7..c005cc8 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/announce/service/IAnnounceService.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/announce/service/IAnnounceService.java
@@ -1,6 +1,7 @@
package com.ruoyi.announce.service;
import java.io.IOException;
+import java.util.List;
import java.util.Map;
/**
@@ -38,9 +39,11 @@
String passkey,
String ip
) throws Exception;
+ List<Map<String, Object>> getAllPeers();
- /**
- * 将一个 Map<String,Object> 编码成 bencode 二进制
- */
- byte[] encodeBencode(Map<String, Object> reply) throws IOException;
+ // 处理 scrape 请求
+ Map<String, Object> handleScrape(String[] infoHashes);
+
+ Map<String, Object> handleHeartbeat(String ip) throws IOException;
+
}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/announce/service/impl/AnnounceServiceImpl.java b/ruoyi-admin/src/main/java/com/ruoyi/announce/service/impl/AnnounceServiceImpl.java
index f80050f..e9cdce8 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/announce/service/impl/AnnounceServiceImpl.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/announce/service/impl/AnnounceServiceImpl.java
@@ -1,132 +1,332 @@
package com.ruoyi.announce.service.impl;
import com.ruoyi.announce.service.IAnnounceService;
-import com.ruoyi.announce.util.BencodeEncoder;
+import com.ruoyi.authentication.domain.SysUserPasskey;
+import com.ruoyi.authentication.domain.UserScore;
+import com.ruoyi.authentication.service.ISysUserPasskeyService;
+import com.ruoyi.authentication.service.IUserScoreService;
import com.ruoyi.common.core.redis.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
+import static java.lang.Long.parseLong;
+
@Service
public class AnnounceServiceImpl implements IAnnounceService {
- private static final int ANNOUNCE_INTERVAL = 1800; // 秒
+ private static final int ANNOUNCE_INTERVAL = 1800;
+ private static final int PEER_CACHE_DURATION_MIN = 60;
+ private static final int MAX_SEEDERS = 25;
+ private static final int MAX_LEECHERS = 50;
+ private static final int UPLOAD_WEIGHT = 70;
+ private static final int DOWNLOAD_WEIGHT = 30;
@Autowired
private RedisCache redisCache;
+ @Autowired
+ private ISysUserPasskeyService passkeyService;
+ @Autowired
+ private IUserScoreService userScoreService;
@Override
- public Map<String, Object> handleAnnounce(
- byte[] infoHash,
- byte[] peerId,
- int port,
- long uploaded,
- long downloaded,
- long left,
- String event,
- String passkey,
- String ip
- ) throws Exception {
- // 1. 转 hex 作为 Redis key 前缀
- String infoHashHex = bytesToHex(infoHash);
- String peerIdStr = new String(peerId, StandardCharsets.ISO_8859_1);
-
- // 2. 校验 passkey(可根据业务到 MySQL 查 userId,此处略过)
-
- // 3. 在 Redis 中记录/刷新此 peer 的信息
- // 使用 Hash 存储详情,TTL 120 秒
- String peerKey = "peer:" + infoHashHex + ":" + peerIdStr;
- Map<String, Object> peerData = new HashMap<>();
- peerData.put("ip", ip);
- peerData.put("port", port);
- peerData.put("uploaded", uploaded);
- peerData.put("downloaded", downloaded);
- peerData.put("left", left);
- peerData.put("lastSeen", System.currentTimeMillis());
- redisCache.setCacheMap(peerKey, peerData);
- redisCache.expire(peerKey, 120, TimeUnit.SECONDS);
-
- // 4. 从 Redis 中扫描所有同 info_hash 的 peer
- Collection<String> keys = redisCache.keys("peer:" + infoHashHex + ":*");
- List<byte[]> peersBin = new ArrayList<>(keys.size());
- for (String key : keys) {
- @SuppressWarnings("unchecked")
- Map<String, Object> data = (Map<String, Object>) redisCache.getCacheMap(key);
- String peerIp = (String) data.get("ip");
- int peerPort = ((Number) data.get("port")).intValue();
- peersBin.add(encodePeer(peerIp, peerPort));
- if (peersBin.size() >= 50) break; // 最多返回 50 个 peers
+ public Map<String, Object> handleAnnounce(byte[] infoHash,
+ byte[] peerId,
+ int port,
+ long uploaded,
+ long downloaded,
+ long left,
+ String event,
+ String passkey,
+ String ip) throws IOException {
+ if (passkey == null) {
+ return Collections.emptyMap();
}
- // 5. 构造返回数据 Map(有序)
- Map<String, Object> reply = new LinkedHashMap<>();
- reply.put("interval", ANNOUNCE_INTERVAL);
- reply.put("min interval", ANNOUNCE_INTERVAL / 2);
- reply.put("complete", countSeeders(infoHashHex));
- reply.put("incomplete", countLeechers(infoHashHex));
- reply.put("peers", peersBin);
+ String infoHashHex = toHex(infoHash);
+ String peerKey = buildPeerKey(peerId);
+ String seederSet = buildZSetKey(infoHashHex, "seeders");
+ String leecherSet = buildZSetKey(infoHashHex, "leechers");
- return reply;
- }
-
- @Override
- public byte[] encodeBencode(Map<String, Object> reply) throws IOException {
- BencodeEncoder encoder = new BencodeEncoder();
- return encoder.encodeBencode(reply);
- }
-
- // —— 辅助方法 —— //
-
- /** 统计 left == 0 的 Seeder 数 */
- private int countSeeders(String infoHashHex) {
- int count = 0;
- for (String key : redisCache.keys("peer:" + infoHashHex + ":*")) {
- @SuppressWarnings("unchecked")
- Map<String, Object> data = (Map<String, Object>) redisCache.getCacheMap(key);
- long left = ((Number) data.get("left")).longValue();
- if (left == 0) count++;
+ if ("stopped".equals(event)) {
+ return handleStopEvent(peerKey, seederSet, leecherSet, passkey, uploaded, downloaded);
}
- return count;
+
+ cachePeerState(peerKey, uploaded, downloaded, left, ip, port);
+ updatePeerRanking(seederSet, leecherSet, peerKey, left == 0);
+
+ List<byte[]> peerList = selectPeers(seederSet, leecherSet, peerKey, left == 0);
+ byte[] peersBinary = flattenPeers(peerList);
+
+ return buildResponse(seederSet, leecherSet, peersBinary);
}
- /** 统计 left > 0 的 Leecher 数 */
- private int countLeechers(String infoHashHex) {
- int count = 0;
- for (String key : redisCache.keys("peer:" + infoHashHex + ":*")) {
- @SuppressWarnings("unchecked")
- Map<String, Object> data = (Map<String, Object>) redisCache.getCacheMap(key);
- long left = ((Number) data.get("left")).longValue();
- if (left > 0) count++;
+ private Map<String, Object> handleStopEvent(String peerKey,
+ String seederSet,
+ String leecherSet,
+ String passkey,
+ long uploaded,
+ long downloaded) {
+ redisCache.deleteObject(peerKey);
+ redisCache.zRemove(seederSet, peerKey);
+ redisCache.zRemove(leecherSet, peerKey);
+
+ Optional<Long> userId = resolveUserId(passkey);
+ userId.ifPresent(id -> updateUserScore(id, uploaded, downloaded));
+ return Collections.emptyMap();
+ }
+
+ private void cachePeerState(String peerKey,
+ long uploaded,
+ long downloaded,
+ long left,
+ String ip,
+ int port) {
+ Map<String, String> data = new HashMap<>();
+ data.put("ip", ip);
+ data.put("port", String.valueOf(port));
+ data.put("uploaded", String.valueOf(uploaded));
+ data.put("downloaded", String.valueOf(downloaded));
+ data.put("left", String.valueOf(left));
+ data.put("lastSeen", String.valueOf(System.currentTimeMillis()));
+
+ redisCache.setCacheMap(peerKey, data);
+ redisCache.expire(peerKey, PEER_CACHE_DURATION_MIN, TimeUnit.MINUTES);
+ }
+
+ private void updatePeerRanking(String seederSet,
+ String leecherSet,
+ String peerKey,
+ boolean isSeeder) {
+ String sourceSet = isSeeder ? leecherSet : seederSet;
+ String targetSet = isSeeder ? seederSet : leecherSet;
+
+ redisCache.zRemove(sourceSet, peerKey);
+ redisCache.zAdd(targetSet, peerKey, System.currentTimeMillis());
+ }
+
+ private List<byte[]> selectPeers(String seederSet,
+ String leecherSet,
+ String selfKey,
+ boolean isSeeder) throws IOException {
+ Set<String> candidates = new LinkedHashSet<>();
+ if (isSeeder) {
+ candidates.addAll(redisCache.zRange(leecherSet, 0, MAX_LEECHERS));
+ } else {
+ candidates.addAll(Optional.ofNullable(redisCache.zRange(seederSet, 0, MAX_SEEDERS)).orElse(Collections.emptySet()));
+ candidates.addAll(Optional.ofNullable(redisCache.zRange(leecherSet, 0, MAX_LEECHERS)).orElse(Collections.emptySet()));
}
- return count;
+
+ List<byte[]> peers = new ArrayList<>();
+ for (String candidateKey : candidates) {
+ if (candidateKey.equals(selfKey)) continue;
+ cachePeerLevel:
+ {
+ Map<String, String> info = redisCache.getCacheMap(candidateKey);
+ String peerIp = info.get("ip");
+ String portStr = info.get("port");
+ if (peerIp == null || peerIp.contains(":") || portStr == null) break cachePeerLevel;
+ peers.add(encodePeer(peerIp, Integer.parseInt(portStr)));
+ }
+ }
+ return peers;
}
- /** 将 IPv4 + port 编码成 6 字节:4 字节 IP + 2 字节 port */
- private byte[] encodePeer(String ip, int port) throws Exception {
+ private byte[] flattenPeers(List<byte[]> peerList) throws IOException {
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ for (byte[] peer : peerList) {
+ out.write(peer);
+ }
+ return out.toByteArray();
+ }
+ }
+
+ private Map<String, Object> buildResponse(String seederSet,
+ String leecherSet,
+ byte[] peersBinary) {
+ Map<String, Object> response = new LinkedHashMap<>();
+ response.put("interval", ANNOUNCE_INTERVAL);
+ response.put("min interval", ANNOUNCE_INTERVAL / 2);
+ response.put("complete", Optional.ofNullable(redisCache.zCard(seederSet)).orElse(0L).intValue());
+ response.put("incomplete", Optional.ofNullable(redisCache.zCard(leecherSet)).orElse(0L).intValue());
+ response.put("peers", peersBinary);
+ return response;
+ }
+
+ private Optional<Long> resolveUserId(String passkey) {
+ SysUserPasskey criteria = new SysUserPasskey(passkey);
+ List<SysUserPasskey> records = passkeyService.selectSysUserPasskeyList(criteria);
+ return records.isEmpty() ? Optional.empty() : Optional.of(records.get(0).getUserId());
+ }
+
+ private void updateUserScore(Long userId, long uploaded, long downloaded) {
+ try {
+ UserScore existing = userScoreService.selectUserScoreByUserId(userId);
+ boolean isNew = existing.getUserId() == null;
+ long totalUploaded = isNew ? uploaded : parseLong(existing.getUploaded()) + uploaded;
+ long totalDownloaded = isNew ? downloaded : parseLong(existing.getDownloaded()) + downloaded;
+ long combinedScore = calculateWeightedScore(totalUploaded, totalDownloaded);
+
+ existing.setUserId(userId);
+ existing.setUploaded(String.valueOf(totalUploaded));
+ existing.setDownloaded(String.valueOf(totalDownloaded));
+ existing.setScore(combinedScore);
+ existing.setUpdatedAt(new Date());
+
+ if (isNew) {
+ userScoreService.insertUserScore(existing);
+ } else {
+ userScoreService.updateUserScore(existing);
+ }
+ } catch (Exception e) {
+ // Log exception properly in production code
+ System.err.println("Failed to update user score: " + e.getMessage());
+ }
+ }
+
+ private long calculateWeightedScore(long uploaded, long downloaded) {
+ return (uploaded * UPLOAD_WEIGHT + downloaded * DOWNLOAD_WEIGHT) / (UPLOAD_WEIGHT + DOWNLOAD_WEIGHT);
+ }
+
+ private String buildPeerKey(byte[] peerId) {
+ return "peer:" + new String(peerId, StandardCharsets.ISO_8859_1);
+ }
+
+ private String buildZSetKey(String infoHashHex, String type) {
+ return String.format("torrent:%s:%s", infoHashHex, type);
+ }
+
+ private String toHex(byte[] data) {
+ char[] hex = "0123456789abcdef".toCharArray();
+ StringBuilder sb = new StringBuilder(data.length * 2);
+ for (byte b : data) {
+ sb.append(hex[(b >> 4) & 0xF]).append(hex[b & 0xF]);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Encode peer address into 6-byte compact representation as per BEP 23
+ */
+ private byte[] encodePeer(String ip, int port) {
String[] parts = ip.split("\\.");
- ByteBuffer buf = ByteBuffer.allocate(6);
- for (int i = 0; i < 4; i++) {
- buf.put((byte) Integer.parseInt(parts[i]));
+ if (parts.length != 4) return null;
+ ByteBuffer buffer = ByteBuffer.allocate(6);
+ try {
+ for (String part : parts) {
+ int segment = Integer.parseInt(part);
+ if (segment < 0 || segment > 255) return null;
+ buffer.put((byte) segment);
+ }
+ buffer.putShort((short) port);
+ return buffer.array();
+ } catch (NumberFormatException e) {
+ return null;
}
- buf.putShort((short) port);
- return buf.array();
}
- /** 将字节数组转成十六进制字符串 */
- private static final char[] HEX = "0123456789abcdef".toCharArray();
- private String bytesToHex(byte[] bytes) {
- char[] cs = new char[bytes.length * 2];
- for (int i = 0; i < bytes.length; i++) {
- int v = bytes[i] & 0xFF;
- cs[i * 2] = HEX[v >>> 4];
- cs[i * 2 + 1] = HEX[v & 0x0F];
+ @Override
+ public Map<String, Object> handleScrape(String[] infoHashes) {
+ Map<String, Object> stats = new HashMap<>();
+ for (String hash : infoHashes) {
+ String key = hash.toLowerCase();
+ Map<String, Integer> countMap = Map.of(
+ "complete", getZCardAsInt(buildZSetKey(key, "seeders")),
+ "incomplete", getZCardAsInt(buildZSetKey(key, "leechers")),
+ "downloaded", 0
+ );
+ stats.put(hash, countMap);
}
- return new String(cs);
+ return stats;
}
-}
+
+ /**
+ * 查询当前所有活跃的 Peer 节点,包含详细字段
+ */
+ @Override
+ public Map<String, Object> handleHeartbeat( String ip) {
+ // 延长该 IP 下所有 peer 的缓存有效期
+ Collection<String> keys = redisCache.keys("peer:*");
+ for (String key : keys) {
+ Map<String, String> data = redisCache.getCacheMap(key);
+ if (data == null) continue;
+ String peerIp = data.get("ip");
+ if (ip.equals(peerIp)) {
+ redisCache.expire(key, PEER_CACHE_DURATION_MIN, TimeUnit.MINUTES);
+ }
+ }
+
+ // 构造基本响应(可根据需要扩展)
+ return Map.of(
+ "interval", ANNOUNCE_INTERVAL,
+ "min interval", ANNOUNCE_INTERVAL / 2,
+ "complete", Optional.ofNullable(redisCache.zCard("some:default:torrent:seeders")).orElse(0L).intValue(),
+ "incomplete", Optional.ofNullable(redisCache.zCard("some:default:torrent:leechers")).orElse(0L).intValue(),
+ "peers", new byte[0]
+ );
+ }
+
+ /**
+ * 查询当前所有活跃的 Peer 节点,包含详细字段
+ */
+ @Override
+ public List<Map<String, Object>> getAllPeers() {
+ // RedisCache.keys 返回 Collection<String>
+ Collection<String> keys = redisCache.keys("peer:*");
+ List<Map<String, Object>> peers = new ArrayList<>();
+ for (String key : keys) {
+ Map<String, String> data = redisCache.getCacheMap(key);
+ if (data == null || data.isEmpty()) continue;
+ Map<String, Object> info = new LinkedHashMap<>();
+ info.put("peerKey", key);
+ info.put("ip", data.get("ip"));
+ info.put("port", data.get("port"));
+ info.put("uploaded", data.get("uploaded"));
+ info.put("downloaded", data.get("downloaded"));
+ info.put("left", data.get("left"));
+ info.put("lastSeen", data.get("lastSeen"));
+ peers.add(info);
+ }
+ return peers;
+ }
+
+ private void refreshPeersForIp(String ip) {
+ redisCache.keys("peer:*").stream()
+ .filter(key -> ip.equals(redisCache.getCacheMap(key).get("ip")))
+ .forEach(key -> redisCache.expire(key, PEER_CACHE_DURATION_MIN, TimeUnit.MINUTES));
+ }
+
+ private List<byte[]> selectPeerBytes(Set<String> peers) {
+ List<byte[]> list = new ArrayList<>();
+ for (String peerId : peers) {
+ Map<String, String> data = redisCache.getCacheMap("peer:" + peerId);
+ if (data == null) continue;
+ String ip = data.get("ip");
+ String port = data.get("port");
+ if (ip == null || ip.contains(":")) continue;
+ list.add(encodePeer(ip, Integer.parseInt(port)));
+ }
+ return list;
+ }
+
+ private Map<String, Object> buildResponse(int incomplete, byte[] peers) {
+ Map<String, Object> response = new LinkedHashMap<>();
+ response.put("interval", ANNOUNCE_INTERVAL);
+ response.put("min interval", ANNOUNCE_INTERVAL / 2);
+ response.put("complete", 0);
+ response.put("incomplete", incomplete);
+ response.put("peers", peers);
+ return response;
+ }
+
+ private int getZCardAsInt(String zsetKey) {
+ Long count = redisCache.zCard(zsetKey);
+ return Optional.ofNullable(count).orElse(0L).intValue();
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/announce/util/BencodeEncoder.java b/ruoyi-admin/src/main/java/com/ruoyi/announce/util/BencodeEncoder.java
index a2cdb30..44dcf6a 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/announce/util/BencodeEncoder.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/announce/util/BencodeEncoder.java
@@ -1,9 +1,12 @@
package com.ruoyi.announce.util;
+import org.springframework.stereotype.Service;
+
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.util.Map;
-
+@Service
public class BencodeEncoder {
public byte[] encodeBencode(Map<String, Object> reply) throws IOException {
@@ -32,11 +35,20 @@
encodeMap((Map<String, Object>) obj, outputStream);
} else if (obj instanceof Iterable) {
encodeList((Iterable<Object>) obj, outputStream);
+ } else if (obj instanceof byte[]) {
+ encodeByteArray((byte[]) obj, outputStream);
} else {
throw new IllegalArgumentException("Unsupported object type: " + obj.getClass());
}
}
+ private void encodeByteArray(byte[] bytes, ByteArrayOutputStream outputStream) throws IOException {
+ outputStream.write(Integer.toString(bytes.length).getBytes());
+ outputStream.write(':');
+ outputStream.write(bytes);
+ }
+
+
private void encodeInteger(Integer value, ByteArrayOutputStream outputStream) throws IOException {
outputStream.write('i');
outputStream.write(value.toString().getBytes());
@@ -44,11 +56,13 @@
}
private void encodeString(String value, ByteArrayOutputStream outputStream) throws IOException {
- outputStream.write(Integer.toString(value.length()).getBytes());
+ byte[] bytes = value.getBytes(StandardCharsets.UTF_8); // Assuming UTF-8 encoding
+ outputStream.write(Integer.toString(bytes.length).getBytes());
outputStream.write(':');
- outputStream.write(value.getBytes());
+ outputStream.write(bytes);
}
+
private void encodeList(Iterable<Object> list, ByteArrayOutputStream outputStream) throws IOException {
outputStream.write('l'); // Start of a list
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/authentication/controller/SysUserPasskeyController.java b/ruoyi-admin/src/main/java/com/ruoyi/authentication/controller/SysUserPasskeyController.java
new file mode 100644
index 0000000..d3ea497
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/authentication/controller/SysUserPasskeyController.java
@@ -0,0 +1,105 @@
+package com.ruoyi.authentication.controller;
+
+import java.util.List;
+
+import com.ruoyi.authentication.domain.SysUserPasskey;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.authentication.service.ISysUserPasskeyService;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.common.core.page.TableDataInfo;
+
+/**
+ * 【请填写功能名称】Controller
+ *
+ * @author ruoyi
+ * @date 2025-04-25
+ */
+@RestController
+@RequestMapping("/system/passkey")
+public class SysUserPasskeyController extends BaseController
+{
+ @Autowired
+ private ISysUserPasskeyService sysUserPasskeyService;
+
+ /**
+ * 查询【请填写功能名称】列表
+ */
+ @PreAuthorize("@ss.hasPermi('system:passkey:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(SysUserPasskey sysUserPasskey)
+ {
+ startPage();
+ List<SysUserPasskey> list = sysUserPasskeyService.selectSysUserPasskeyList(sysUserPasskey);
+ return getDataTable(list);
+ }
+
+ /**
+ * 导出【请填写功能名称】列表
+ */
+ @PreAuthorize("@ss.hasPermi('system:passkey:export')")
+ @Log(title = "【请填写功能名称】", businessType = BusinessType.EXPORT)
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, SysUserPasskey sysUserPasskey)
+ {
+ List<SysUserPasskey> list = sysUserPasskeyService.selectSysUserPasskeyList(sysUserPasskey);
+ ExcelUtil<SysUserPasskey> util = new ExcelUtil<SysUserPasskey>(SysUserPasskey.class);
+ util.exportExcel(response, list, "【请填写功能名称】数据");
+ }
+
+ /**
+ * 获取【请填写功能名称】详细信息
+ */
+ @PreAuthorize("@ss.hasPermi('system:passkey:query')")
+ @GetMapping(value = "/{userId}")
+ public AjaxResult getInfo(@PathVariable("userId") Long userId)
+ {
+ return success(sysUserPasskeyService.selectSysUserPasskeyByUserId(userId));
+ }
+
+ /**
+ * 新增【请填写功能名称】
+ */
+ @PreAuthorize("@ss.hasPermi('system:passkey:add')")
+ @Log(title = "【请填写功能名称】", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@RequestBody SysUserPasskey sysUserPasskey)
+ {
+ return toAjax(sysUserPasskeyService.insertSysUserPasskey(sysUserPasskey));
+ }
+
+ /**
+ * 修改【请填写功能名称】
+ */
+ @PreAuthorize("@ss.hasPermi('system:passkey:edit')")
+ @Log(title = "【请填写功能名称】", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@RequestBody SysUserPasskey sysUserPasskey)
+ {
+ return toAjax(sysUserPasskeyService.updateSysUserPasskey(sysUserPasskey));
+ }
+
+ /**
+ * 删除【请填写功能名称】
+ */
+ @PreAuthorize("@ss.hasPermi('system:passkey:remove')")
+ @Log(title = "【请填写功能名称】", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{userIds}")
+ public AjaxResult remove(@PathVariable Long[] userIds)
+ {
+ return toAjax(sysUserPasskeyService.deleteSysUserPasskeyByUserIds(userIds));
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/authentication/controller/UserScoreController.java b/ruoyi-admin/src/main/java/com/ruoyi/authentication/controller/UserScoreController.java
new file mode 100644
index 0000000..93b9119
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/authentication/controller/UserScoreController.java
@@ -0,0 +1,105 @@
+package com.ruoyi.authentication.controller;
+
+import java.util.List;
+
+import com.ruoyi.authentication.domain.UserScore;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.authentication.service.IUserScoreService;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.common.core.page.TableDataInfo;
+
+/**
+ * 【请填写功能名称】Controller
+ *
+ * @author ruoyi
+ * @date 2025-04-25
+ */
+@RestController
+@RequestMapping("/system/score")
+public class UserScoreController extends BaseController
+{
+ @Autowired
+ private IUserScoreService userScoreService;
+
+ /**
+ * 查询【请填写功能名称】列表
+ */
+ @PreAuthorize("@ss.hasPermi('system:score:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(UserScore userScore)
+ {
+ startPage();
+ List<UserScore> list = userScoreService.selectUserScoreList(userScore);
+ return getDataTable(list);
+ }
+
+ /**
+ * 导出【请填写功能名称】列表
+ */
+ @PreAuthorize("@ss.hasPermi('system:score:export')")
+ @Log(title = "【请填写功能名称】", businessType = BusinessType.EXPORT)
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, UserScore userScore)
+ {
+ List<UserScore> list = userScoreService.selectUserScoreList(userScore);
+ ExcelUtil<UserScore> util = new ExcelUtil<UserScore>(UserScore.class);
+ util.exportExcel(response, list, "【请填写功能名称】数据");
+ }
+
+ /**
+ * 获取【请填写功能名称】详细信息
+ */
+ @PreAuthorize("@ss.hasPermi('system:score:query')")
+ @GetMapping(value = "/{userId}")
+ public AjaxResult getInfo(@PathVariable("userId") Long userId)
+ {
+ return success(userScoreService.selectUserScoreByUserId(userId));
+ }
+
+ /**
+ * 新增【请填写功能名称】
+ */
+ @PreAuthorize("@ss.hasPermi('system:score:add')")
+ @Log(title = "【请填写功能名称】", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@RequestBody UserScore userScore)
+ {
+ return toAjax(userScoreService.insertUserScore(userScore));
+ }
+
+ /**
+ * 修改【请填写功能名称】
+ */
+ @PreAuthorize("@ss.hasPermi('system:score:edit')")
+ @Log(title = "【请填写功能名称】", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@RequestBody UserScore userScore)
+ {
+ return toAjax(userScoreService.updateUserScore(userScore));
+ }
+
+ /**
+ * 删除【请填写功能名称】
+ */
+ @PreAuthorize("@ss.hasPermi('system:score:remove')")
+ @Log(title = "【请填写功能名称】", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{userIds}")
+ public AjaxResult remove(@PathVariable Long[] userIds)
+ {
+ return toAjax(userScoreService.deleteUserScoreByUserIds(userIds));
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/authentication/domain/SysUserPasskey.java b/ruoyi-admin/src/main/java/com/ruoyi/authentication/domain/SysUserPasskey.java
new file mode 100644
index 0000000..7370c77
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/authentication/domain/SysUserPasskey.java
@@ -0,0 +1,71 @@
+package com.ruoyi.authentication.domain;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 【请填写功能名称】对象 sys_user_passkey
+ *
+ * @author ruoyi
+ * @date 2025-04-25
+ */
+public class SysUserPasskey extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ public SysUserPasskey(String passkey){
+ this.passkey = passkey;
+ }
+
+ /** $column.columnComment */
+ private Long userId;
+
+ /** $column.columnComment */
+ @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
+ private String passkey;
+
+ /** $column.columnComment */
+ @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
+ private Date createdAt;
+
+ public void setUserId(Long userId)
+ {
+ this.userId = userId;
+ }
+
+ public Long getUserId()
+ {
+ return userId;
+ }
+ public void setPasskey(String passkey)
+ {
+ this.passkey = passkey;
+ }
+
+ public String getPasskey()
+ {
+ return passkey;
+ }
+ public void setCreatedAt(Date createdAt)
+ {
+ this.createdAt = createdAt;
+ }
+
+ public Date getCreatedAt()
+ {
+ return createdAt;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("userId", getUserId())
+ .append("passkey", getPasskey())
+ .append("createdAt", getCreatedAt())
+ .toString();
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/authentication/domain/UserScore.java b/ruoyi-admin/src/main/java/com/ruoyi/authentication/domain/UserScore.java
new file mode 100644
index 0000000..f7049d1
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/authentication/domain/UserScore.java
@@ -0,0 +1,109 @@
+package com.ruoyi.authentication.domain;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 【请填写功能名称】对象 user_score
+ *
+ * @author ruoyi
+ * @date 2025-04-25
+ */
+public class UserScore extends BaseEntity
+{
+ private static final long serialVersionUID = 1L;
+
+ /** $column.columnComment */
+ private Long userId;
+
+ /** $column.columnComment */
+ @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
+ private String uploaded;
+
+ /** $column.columnComment */
+ @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
+ private String downloaded;
+
+ /** $column.columnComment */
+ @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
+ private Long bonus;
+
+ /** $column.columnComment */
+ @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
+ private Long score;
+
+ /** $column.columnComment */
+ @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
+ private Date updatedAt;
+
+ public void setUserId(Long userId)
+ {
+ this.userId = userId;
+ }
+
+ public Long getUserId()
+ {
+ return userId;
+ }
+ public void setUploaded(String uploaded)
+ {
+ this.uploaded = uploaded;
+ }
+
+ public String getUploaded()
+ {
+ return uploaded;
+ }
+ public void setDownloaded(String downloaded)
+ {
+ this.downloaded = downloaded;
+ }
+
+ public String getDownloaded()
+ {
+ return downloaded;
+ }
+ public void setBonus(Long bonus)
+ {
+ this.bonus = bonus;
+ }
+
+ public Long getBonus()
+ {
+ return bonus;
+ }
+ public void setScore(Long score)
+ {
+ this.score = score;
+ }
+
+ public Long getScore()
+ {
+ return score;
+ }
+ public void setUpdatedAt(Date updatedAt)
+ {
+ this.updatedAt = updatedAt;
+ }
+
+ public Date getUpdatedAt()
+ {
+ return updatedAt;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+ .append("userId", getUserId())
+ .append("uploaded", getUploaded())
+ .append("downloaded", getDownloaded())
+ .append("bonus", getBonus())
+ .append("score", getScore())
+ .append("updatedAt", getUpdatedAt())
+ .toString();
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/authentication/mapper/SysUserPasskeyMapper.java b/ruoyi-admin/src/main/java/com/ruoyi/authentication/mapper/SysUserPasskeyMapper.java
new file mode 100644
index 0000000..7607c63
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/authentication/mapper/SysUserPasskeyMapper.java
@@ -0,0 +1,61 @@
+package com.ruoyi.authentication.mapper;
+
+import java.util.List;
+import com.ruoyi.authentication.domain.SysUserPasskey;
+
+/**
+ * 【请填写功能名称】Mapper接口
+ *
+ * @author ruoyi
+ * @date 2025-04-25
+ */
+public interface SysUserPasskeyMapper
+{
+ /**
+ * 查询【请填写功能名称】
+ *
+ * @param userId 【请填写功能名称】主键
+ * @return 【请填写功能名称】
+ */
+ public SysUserPasskey selectSysUserPasskeyByUserId(Long userId);
+
+ /**
+ * 查询【请填写功能名称】列表
+ *
+ * @param sysUserPasskey 【请填写功能名称】
+ * @return 【请填写功能名称】集合
+ */
+ public List<SysUserPasskey> selectSysUserPasskeyList(SysUserPasskey sysUserPasskey);
+
+ /**
+ * 新增【请填写功能名称】
+ *
+ * @param sysUserPasskey 【请填写功能名称】
+ * @return 结果
+ */
+ public int insertSysUserPasskey(SysUserPasskey sysUserPasskey);
+
+ /**
+ * 修改【请填写功能名称】
+ *
+ * @param sysUserPasskey 【请填写功能名称】
+ * @return 结果
+ */
+ public int updateSysUserPasskey(SysUserPasskey sysUserPasskey);
+
+ /**
+ * 删除【请填写功能名称】
+ *
+ * @param userId 【请填写功能名称】主键
+ * @return 结果
+ */
+ public int deleteSysUserPasskeyByUserId(Long userId);
+
+ /**
+ * 批量删除【请填写功能名称】
+ *
+ * @param userIds 需要删除的数据主键集合
+ * @return 结果
+ */
+ public int deleteSysUserPasskeyByUserIds(Long[] userIds);
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/authentication/mapper/UserScoreMapper.java b/ruoyi-admin/src/main/java/com/ruoyi/authentication/mapper/UserScoreMapper.java
new file mode 100644
index 0000000..4c15e5e
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/authentication/mapper/UserScoreMapper.java
@@ -0,0 +1,61 @@
+package com.ruoyi.authentication.mapper;
+
+import java.util.List;
+import com.ruoyi.authentication.domain.UserScore;
+
+/**
+ * 【请填写功能名称】Mapper接口
+ *
+ * @author ruoyi
+ * @date 2025-04-25
+ */
+public interface UserScoreMapper
+{
+ /**
+ * 查询【请填写功能名称】
+ *
+ * @param userId 【请填写功能名称】主键
+ * @return 【请填写功能名称】
+ */
+ public UserScore selectUserScoreByUserId(Long userId);
+
+ /**
+ * 查询【请填写功能名称】列表
+ *
+ * @param userScore 【请填写功能名称】
+ * @return 【请填写功能名称】集合
+ */
+ public List<UserScore> selectUserScoreList(UserScore userScore);
+
+ /**
+ * 新增【请填写功能名称】
+ *
+ * @param userScore 【请填写功能名称】
+ * @return 结果
+ */
+ public int insertUserScore(UserScore userScore);
+
+ /**
+ * 修改【请填写功能名称】
+ *
+ * @param userScore 【请填写功能名称】
+ * @return 结果
+ */
+ public int updateUserScore(UserScore userScore);
+
+ /**
+ * 删除【请填写功能名称】
+ *
+ * @param userId 【请填写功能名称】主键
+ * @return 结果
+ */
+ public int deleteUserScoreByUserId(Long userId);
+
+ /**
+ * 批量删除【请填写功能名称】
+ *
+ * @param userIds 需要删除的数据主键集合
+ * @return 结果
+ */
+ public int deleteUserScoreByUserIds(Long[] userIds);
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/authentication/service/ISysUserPasskeyService.java b/ruoyi-admin/src/main/java/com/ruoyi/authentication/service/ISysUserPasskeyService.java
new file mode 100644
index 0000000..33e0ec6
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/authentication/service/ISysUserPasskeyService.java
@@ -0,0 +1,62 @@
+package com.ruoyi.authentication.service;
+
+import java.util.List;
+
+import com.ruoyi.authentication.domain.SysUserPasskey;
+
+/**
+ * 【请填写功能名称】Service接口
+ *
+ * @author ruoyi
+ * @date 2025-04-25
+ */
+public interface ISysUserPasskeyService
+{
+ /**
+ * 查询【请填写功能名称】
+ *
+ * @param userId 【请填写功能名称】主键
+ * @return 【请填写功能名称】
+ */
+ public SysUserPasskey selectSysUserPasskeyByUserId(Long userId);
+
+ /**
+ * 查询【请填写功能名称】列表
+ *
+ * @param sysUserPasskey 【请填写功能名称】
+ * @return 【请填写功能名称】集合
+ */
+ public List<SysUserPasskey> selectSysUserPasskeyList(SysUserPasskey sysUserPasskey);
+
+ /**
+ * 新增【请填写功能名称】
+ *
+ * @param sysUserPasskey 【请填写功能名称】
+ * @return 结果
+ */
+ public int insertSysUserPasskey(SysUserPasskey sysUserPasskey);
+
+ /**
+ * 修改【请填写功能名称】
+ *
+ * @param sysUserPasskey 【请填写功能名称】
+ * @return 结果
+ */
+ public int updateSysUserPasskey(SysUserPasskey sysUserPasskey);
+
+ /**
+ * 批量删除【请填写功能名称】
+ *
+ * @param userIds 需要删除的【请填写功能名称】主键集合
+ * @return 结果
+ */
+ public int deleteSysUserPasskeyByUserIds(Long[] userIds);
+
+ /**
+ * 删除【请填写功能名称】信息
+ *
+ * @param userId 【请填写功能名称】主键
+ * @return 结果
+ */
+ public int deleteSysUserPasskeyByUserId(Long userId);
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/authentication/service/IUserScoreService.java b/ruoyi-admin/src/main/java/com/ruoyi/authentication/service/IUserScoreService.java
new file mode 100644
index 0000000..b93e41e
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/authentication/service/IUserScoreService.java
@@ -0,0 +1,62 @@
+package com.ruoyi.authentication.service;
+
+import java.util.List;
+
+import com.ruoyi.authentication.domain.UserScore;
+
+/**
+ * 【请填写功能名称】Service接口
+ *
+ * @author ruoyi
+ * @date 2025-04-25
+ */
+public interface IUserScoreService
+{
+ /**
+ * 查询【请填写功能名称】
+ *
+ * @param userId 【请填写功能名称】主键
+ * @return 【请填写功能名称】
+ */
+ public UserScore selectUserScoreByUserId(Long userId);
+
+ /**
+ * 查询【请填写功能名称】列表
+ *
+ * @param userScore 【请填写功能名称】
+ * @return 【请填写功能名称】集合
+ */
+ public List<UserScore> selectUserScoreList(UserScore userScore);
+
+ /**
+ * 新增【请填写功能名称】
+ *
+ * @param userScore 【请填写功能名称】
+ * @return 结果
+ */
+ public int insertUserScore(UserScore userScore);
+
+ /**
+ * 修改【请填写功能名称】
+ *
+ * @param userScore 【请填写功能名称】
+ * @return 结果
+ */
+ public int updateUserScore(UserScore userScore);
+
+ /**
+ * 批量删除【请填写功能名称】
+ *
+ * @param userIds 需要删除的【请填写功能名称】主键集合
+ * @return 结果
+ */
+ public int deleteUserScoreByUserIds(Long[] userIds);
+
+ /**
+ * 删除【请填写功能名称】信息
+ *
+ * @param userId 【请填写功能名称】主键
+ * @return 结果
+ */
+ public int deleteUserScoreByUserId(Long userId);
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/authentication/service/impl/SysUserPasskeyServiceImpl.java b/ruoyi-admin/src/main/java/com/ruoyi/authentication/service/impl/SysUserPasskeyServiceImpl.java
new file mode 100644
index 0000000..c2091e5
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/authentication/service/impl/SysUserPasskeyServiceImpl.java
@@ -0,0 +1,94 @@
+package com.ruoyi.authentication.service.impl;
+
+import java.util.List;
+
+import com.ruoyi.authentication.domain.SysUserPasskey;
+import com.ruoyi.authentication.mapper.SysUserPasskeyMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.authentication.service.ISysUserPasskeyService;
+
+/**
+ * 【请填写功能名称】Service业务层处理
+ *
+ * @author ruoyi
+ * @date 2025-04-25
+ */
+@Service
+public class SysUserPasskeyServiceImpl implements ISysUserPasskeyService
+{
+ @Autowired
+ private SysUserPasskeyMapper sysUserPasskeyMapper;
+
+ /**
+ * 查询【请填写功能名称】
+ *
+ * @param userId 【请填写功能名称】主键
+ * @return 【请填写功能名称】
+ */
+ @Override
+ public SysUserPasskey selectSysUserPasskeyByUserId(Long userId)
+ {
+ return sysUserPasskeyMapper.selectSysUserPasskeyByUserId(userId);
+ }
+
+ /**
+ * 查询【请填写功能名称】列表
+ *
+ * @param sysUserPasskey 【请填写功能名称】
+ * @return 【请填写功能名称】
+ */
+ @Override
+ public List<SysUserPasskey> selectSysUserPasskeyList(SysUserPasskey sysUserPasskey)
+ {
+ return sysUserPasskeyMapper.selectSysUserPasskeyList(sysUserPasskey);
+ }
+
+ /**
+ * 新增【请填写功能名称】
+ *
+ * @param sysUserPasskey 【请填写功能名称】
+ * @return 结果
+ */
+ @Override
+ public int insertSysUserPasskey(SysUserPasskey sysUserPasskey)
+ {
+ return sysUserPasskeyMapper.insertSysUserPasskey(sysUserPasskey);
+ }
+
+ /**
+ * 修改【请填写功能名称】
+ *
+ * @param sysUserPasskey 【请填写功能名称】
+ * @return 结果
+ */
+ @Override
+ public int updateSysUserPasskey(SysUserPasskey sysUserPasskey)
+ {
+ return sysUserPasskeyMapper.updateSysUserPasskey(sysUserPasskey);
+ }
+
+ /**
+ * 批量删除【请填写功能名称】
+ *
+ * @param userIds 需要删除的【请填写功能名称】主键
+ * @return 结果
+ */
+ @Override
+ public int deleteSysUserPasskeyByUserIds(Long[] userIds)
+ {
+ return sysUserPasskeyMapper.deleteSysUserPasskeyByUserIds(userIds);
+ }
+
+ /**
+ * 删除【请填写功能名称】信息
+ *
+ * @param userId 【请填写功能名称】主键
+ * @return 结果
+ */
+ @Override
+ public int deleteSysUserPasskeyByUserId(Long userId)
+ {
+ return sysUserPasskeyMapper.deleteSysUserPasskeyByUserId(userId);
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/authentication/service/impl/UserScoreServiceImpl.java b/ruoyi-admin/src/main/java/com/ruoyi/authentication/service/impl/UserScoreServiceImpl.java
new file mode 100644
index 0000000..4a41923
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/authentication/service/impl/UserScoreServiceImpl.java
@@ -0,0 +1,94 @@
+package com.ruoyi.authentication.service.impl;
+
+import java.util.List;
+
+import com.ruoyi.authentication.domain.UserScore;
+import com.ruoyi.authentication.mapper.UserScoreMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.authentication.service.IUserScoreService;
+
+/**
+ * 【请填写功能名称】Service业务层处理
+ *
+ * @author ruoyi
+ * @date 2025-04-25
+ */
+@Service
+public class UserScoreServiceImpl implements IUserScoreService
+{
+ @Autowired
+ private UserScoreMapper userScoreMapper;
+
+ /**
+ * 查询【请填写功能名称】
+ *
+ * @param userId 【请填写功能名称】主键
+ * @return 【请填写功能名称】
+ */
+ @Override
+ public UserScore selectUserScoreByUserId(Long userId)
+ {
+ return userScoreMapper.selectUserScoreByUserId(userId);
+ }
+
+ /**
+ * 查询【请填写功能名称】列表
+ *
+ * @param userScore 【请填写功能名称】
+ * @return 【请填写功能名称】
+ */
+ @Override
+ public List<UserScore> selectUserScoreList(UserScore userScore)
+ {
+ return userScoreMapper.selectUserScoreList(userScore);
+ }
+
+ /**
+ * 新增【请填写功能名称】
+ *
+ * @param userScore 【请填写功能名称】
+ * @return 结果
+ */
+ @Override
+ public int insertUserScore(UserScore userScore)
+ {
+ return userScoreMapper.insertUserScore(userScore);
+ }
+
+ /**
+ * 修改【请填写功能名称】
+ *
+ * @param userScore 【请填写功能名称】
+ * @return 结果
+ */
+ @Override
+ public int updateUserScore(UserScore userScore)
+ {
+ return userScoreMapper.updateUserScore(userScore);
+ }
+
+ /**
+ * 批量删除【请填写功能名称】
+ *
+ * @param userIds 需要删除的【请填写功能名称】主键
+ * @return 结果
+ */
+ @Override
+ public int deleteUserScoreByUserIds(Long[] userIds)
+ {
+ return userScoreMapper.deleteUserScoreByUserIds(userIds);
+ }
+
+ /**
+ * 删除【请填写功能名称】信息
+ *
+ * @param userId 【请填写功能名称】主键
+ * @return 结果
+ */
+ @Override
+ public int deleteUserScoreByUserId(Long userId)
+ {
+ return userScoreMapper.deleteUserScoreByUserId(userId);
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/torrent/controller/BtTorrentController.java b/ruoyi-admin/src/main/java/com/ruoyi/torrent/controller/BtTorrentController.java
index c80db77..15dc9f3 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/torrent/controller/BtTorrentController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/torrent/controller/BtTorrentController.java
@@ -1,11 +1,11 @@
package com.ruoyi.torrent.controller;
+import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
+import java.util.*;
+
import com.ruoyi.common.utils.http.HttpUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
@@ -16,6 +16,7 @@
import com.ruoyi.torrent.service.IBtTorrentAnnounceService;
import com.ruoyi.torrent.service.IBtTorrentFileService;
import com.ruoyi.torrent.service.IBtTorrentService;
+import com.ruoyi.torrent.util.TorrentFileUtil;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
@@ -41,7 +42,6 @@
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
-import java.util.UUID;
/**
* 种子主Controller
@@ -62,6 +62,9 @@
private IBtTorrentAnnounceService btTorrentAnnounceService;
private static final String RECOMMEND_API = "http://127.0.0.1:5000/recommend_torrents";
+ private String torrentPath= "torrents";
+
+
private static final String boundary = "----WebKitFormBoundary" + UUID.randomUUID().toString().replace("-", "");
@PreAuthorize("@ss.hasPermi('system:torrent:add')")
@@ -69,8 +72,9 @@
@PostMapping("/uploadTorrent")
public AjaxResult uploadTorrent(@RequestParam("file") MultipartFile file) {
try {
+
// Create URL connection to Flask server
- String flaskUrl = "http://localhost:5000/parse_torrent"; // Flask server URL
+ String flaskUrl = "http://49.233.215.144:5000/parse_torrent"; // Flask server URL
HttpURLConnection connection = (HttpURLConnection) new URL(flaskUrl).openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
@@ -86,6 +90,10 @@
if (responseCode != 200) {
return AjaxResult.error("Failed to communicate with Flask server, response code: " + responseCode);
}
+ TorrentFileUtil.uploadFile(file,torrentPath+"/"+SecurityUtils.getUserId());
+
+
+
// Assuming the Flask server responds with JSON, parse the response
String responseBody = new String(connection.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
@@ -133,9 +141,11 @@
BtTorrent btTorrent = objectMapper.readValue(btTorrentNode.toString(), BtTorrent.class);
btTorrent.setCreatedBy(SecurityUtils.getUsername());
btTorrent.setUploaderId(SecurityUtils.getUserId());
+ btTorrent.setFilePath(torrentPath+"/"+SecurityUtils.getUserId()+"/"+btTorrent.getName());
btTorrentService.insertBtTorrent(btTorrent);
Long torrentId=btTorrent.getTorrentId();
+
// Convert btTorrentFilesNode to List<BtTorrentFile> using TypeReference
List<BtTorrentFile> btTorrentFiles = objectMapper.readValue(
btTorrentFilesNode.toString(),
@@ -175,6 +185,43 @@
}
}
+ @GetMapping("/download/{id}")
+ public void downloadTorrent(@PathVariable("id") Long id, HttpServletResponse response) {
+ try {
+ // 1. 调用 getTorrentInfo(id) 获取文件路径
+ BtTorrent btTorrent= btTorrentService.selectBtTorrentByTorrentId(id);
+ String filePath=btTorrent.getFilePath();
+ String fileName=btTorrent.getName();
+
+ // 2. 使用工具方法下载文件(不删除源文件)
+ TorrentFileUtil.downloadFile(response, filePath, fileName, false);
+
+ } catch (FileNotFoundException e) {
+
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ System.out.println(e.getMessage());
+
+ } catch (Exception e) {
+
+ response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ System.out.println(e.getMessage());
+
+ }
+
+ }
+
+ /**
+ * 查询种子主列表
+ */
+ @PreAuthorize("@ss.hasPermi('system:torrent:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(BtTorrent btTorrent)
+ {
+ startPage();
+ List<BtTorrent> list = btTorrentService.selectBtTorrentList(btTorrent);
+ return getDataTable(list);
+ }
+
/**
* 查询种子主列表
*/
@@ -265,6 +312,10 @@
@DeleteMapping("/{torrentIds}")
public AjaxResult remove(@PathVariable Long[] torrentIds)
{
+ for (Long torrentId : torrentIds){
+ String filePath = btTorrentService.selectBtTorrentByTorrentId(torrentId).getFilePath();
+ TorrentFileUtil.deleteFileOrFolder(filePath);
+ }
return toAjax(btTorrentService.deleteBtTorrentByTorrentIds(torrentIds));
}
}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/torrent/util/OssMultipartUploader.java b/ruoyi-admin/src/main/java/com/ruoyi/torrent/util/OssMultipartUploader.java
new file mode 100644
index 0000000..6a0323f
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/torrent/util/OssMultipartUploader.java
@@ -0,0 +1,96 @@
+package com.ruoyi.torrent.util;
+
+import com.aliyun.oss.*;
+import com.aliyun.oss.common.auth.CredentialsProviderFactory;
+import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider;
+import com.aliyun.oss.common.comm.SignVersion;
+import com.aliyun.oss.internal.Mimetypes;
+import com.aliyun.oss.model.*;
+import com.aliyuncs.exceptions.ClientException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+public class OssMultipartUploader {
+
+ private final OSS ossClient;
+ private final String bucketName;
+
+ public OssMultipartUploader(String endpoint, String region, String bucketName) throws ClientException {
+ EnvironmentVariableCredentialsProvider credentialsProvider =
+ CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
+ ClientBuilderConfiguration config = new ClientBuilderConfiguration();
+ config.setSignatureVersion(SignVersion.V4);
+
+ this.ossClient = OSSClientBuilder.create()
+ .endpoint(endpoint)
+ .region(region)
+ .credentialsProvider(credentialsProvider)
+ .clientConfiguration(config)
+ .build();
+
+ this.bucketName = bucketName;
+ }
+
+ public void uploadFile(String objectName, String filePath) throws Exception {
+ File file = new File(filePath);
+ long partSize = 1 * 1024 * 1024L; // 1MB
+ long fileLength = file.length();
+ int partCount = (int) (fileLength / partSize);
+ if (fileLength % partSize != 0) {
+ partCount++;
+ }
+
+ // 设置元数据(如 content-type)
+ ObjectMetadata metadata = new ObjectMetadata();
+ metadata.setContentType(Mimetypes.getInstance().getMimetype(file, objectName));
+
+ InitiateMultipartUploadRequest initiateRequest = new InitiateMultipartUploadRequest(bucketName, objectName);
+ initiateRequest.setObjectMetadata(metadata);
+ InitiateMultipartUploadResult initResult = ossClient.initiateMultipartUpload(initiateRequest);
+ String uploadId = initResult.getUploadId();
+
+ List<PartETag> partETags = new ArrayList<>();
+
+ try {
+ for (int i = 0; i < partCount; i++) {
+ long startPos = i * partSize;
+ long curPartSize = (i + 1 == partCount) ? (fileLength - startPos) : partSize;
+
+ try (InputStream inputStream = new FileInputStream(file)) {
+ inputStream.skip(startPos);
+
+ UploadPartRequest uploadPartRequest = new UploadPartRequest();
+ uploadPartRequest.setBucketName(bucketName);
+ uploadPartRequest.setKey(objectName);
+ uploadPartRequest.setUploadId(uploadId);
+ uploadPartRequest.setInputStream(inputStream);
+ uploadPartRequest.setPartSize(curPartSize);
+ uploadPartRequest.setPartNumber(i + 1);
+
+ UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
+ partETags.add(uploadPartResult.getPartETag());
+ }
+ }
+
+ CompleteMultipartUploadRequest completeRequest =
+ new CompleteMultipartUploadRequest(bucketName, objectName, uploadId, partETags);
+
+ CompleteMultipartUploadResult result = ossClient.completeMultipartUpload(completeRequest);
+ System.out.println("上传成功,ETag: " + result.getETag());
+
+ } catch (Exception e) {
+ ossClient.abortMultipartUpload(new AbortMultipartUploadRequest(bucketName, objectName, uploadId));
+ throw new RuntimeException("上传失败,已中止上传任务", e);
+ }
+ }
+
+ public void shutdown() {
+ if (ossClient != null) {
+ ossClient.shutdown();
+ }
+ }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/torrent/util/TorrentFileUtil.java b/ruoyi-admin/src/main/java/com/ruoyi/torrent/util/TorrentFileUtil.java
index a1cd422..03f2c18 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/torrent/util/TorrentFileUtil.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/torrent/util/TorrentFileUtil.java
@@ -5,10 +5,8 @@
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
+import java.nio.file.*;
+import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.stream.Collectors;
@@ -33,28 +31,30 @@
Files.createDirectories(saveDir);
}
- // 获取文件信息
- String originalFilename = Objects.requireNonNull(file.getOriginalFilename());
- String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
- String storedFilename = System.currentTimeMillis() + "_" + UUID.randomUUID() + fileExtension;
+ // 获取原始文件名
+ String originalFilename = Objects.requireNonNull(file.getOriginalFilename()).trim();
long fileSize = file.getSize();
+ String fileExtension = originalFilename.contains(".") ?
+ originalFilename.substring(originalFilename.lastIndexOf(".")) : "";
- // 保存文件
- Path targetPath = saveDir.resolve(storedFilename);
+ // 使用原始文件名保存(防止路径穿越漏洞)
+ String safeFileName = originalFilename.replaceAll("[\\\\/:*?\"<>|]", "_");
+ Path targetPath = saveDir.resolve(safeFileName);
Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);
// 返回文件信息
Map<String, Object> fileInfo = new HashMap<>();
fileInfo.put("originalName", originalFilename);
- fileInfo.put("storedName", storedFilename);
+ fileInfo.put("storedName", safeFileName);
fileInfo.put("filePath", targetPath.toString());
fileInfo.put("fileSize", fileSize);
- fileInfo.put("fileType", fileExtension.substring(1));
+ fileInfo.put("fileType", fileExtension.isEmpty() ? "unknown" : fileExtension.substring(1));
fileInfo.put("uploadTime", new Date());
return fileInfo;
}
+
/**
* 文件下载工具方法
*
@@ -114,6 +114,40 @@
return false;
}
}
+ /**
+ * 删除文件或文件夹
+ *
+ * @param path 文件或文件夹路径
+ * @return 是否删除成功
+ */
+ public static boolean deleteFileOrFolder(String path) {
+ try {
+ Path filePath = Paths.get(path);
+ if (Files.isDirectory(filePath)) {
+ // 如果是文件夹,递归删除文件夹中的内容
+ Files.walkFileTree(filePath, new SimpleFileVisitor<Path>() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ Files.delete(file); // 删除文件
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+ Files.delete(dir); // 删除文件夹
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ } else {
+ // 如果是文件,直接删除
+ Files.deleteIfExists(filePath);
+ }
+ return true;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
/**
* 重命名文件
diff --git a/ruoyi-admin/src/main/resources/application-druid.yml b/ruoyi-admin/src/main/resources/application-druid.yml
index 12ed4d4..ca1fce9 100644
--- a/ruoyi-admin/src/main/resources/application-druid.yml
+++ b/ruoyi-admin/src/main/resources/application-druid.yml
@@ -7,8 +7,8 @@
# 主库数据源
master:
url: jdbc:mysql://49.233.215.144:3306/pt_station?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
- username: sy
- password: sy_password
+ username: root
+ password: 22301058Zsy*
# 从库数据源
slave:
# 从数据源开关/默认关闭
diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml
index cb1a87c..01b71fe 100644
--- a/ruoyi-admin/src/main/resources/application.yml
+++ b/ruoyi-admin/src/main/resources/application.yml
@@ -9,7 +9,7 @@
# 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
profile: D:/ruoyi/uploadPath
# 获取ip地址开关
- addressEnabled: false
+ addressEnabled: true
# 验证码类型 math 数字计算 char 字符验证
captchaType: math
@@ -75,7 +75,7 @@
# 数据库索引
database: 0
# 密码
- password:
+ password: 22301058Zsy*
# 连接超时时间
timeout: 10s
lettuce:
diff --git a/ruoyi-admin/src/main/resources/mapper/system/SysUserPasskeyMapper.xml b/ruoyi-admin/src/main/resources/mapper/system/SysUserPasskeyMapper.xml
new file mode 100644
index 0000000..949ac4f
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/mapper/system/SysUserPasskeyMapper.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.authentication.mapper.SysUserPasskeyMapper">
+
+ <resultMap type="SysUserPasskey" id="SysUserPasskeyResult">
+ <result property="userId" column="user_id" />
+ <result property="passkey" column="passkey" />
+ <result property="createdAt" column="created_at" />
+ </resultMap>
+
+ <sql id="selectSysUserPasskeyVo">
+ select user_id, passkey, created_at from sys_user_passkey
+ </sql>
+
+ <select id="selectSysUserPasskeyList" parameterType="SysUserPasskey" resultMap="SysUserPasskeyResult">
+ <include refid="selectSysUserPasskeyVo"/>
+ <where>
+ <if test="passkey != null and passkey != ''"> and passkey = #{passkey}</if>
+ </where>
+ </select>
+
+ <select id="selectSysUserPasskeyByUserId" parameterType="Long" resultMap="SysUserPasskeyResult">
+ <include refid="selectSysUserPasskeyVo"/>
+ where user_id = #{userId}
+ </select>
+
+ <insert id="insertSysUserPasskey" parameterType="SysUserPasskey">
+ insert into sys_user_passkey
+ <trim prefix="(" suffix=")" suffixOverrides=",">
+ <if test="userId != null">user_id,</if>
+ <if test="passkey != null and passkey != ''">passkey,</if>
+ <if test="createdAt != null">created_at,</if>
+ </trim>
+ <trim prefix="values (" suffix=")" suffixOverrides=",">
+ <if test="userId != null">#{userId},</if>
+ <if test="passkey != null and passkey != ''">#{passkey},</if>
+ <if test="createdAt != null">#{createdAt},</if>
+ </trim>
+ </insert>
+
+ <update id="updateSysUserPasskey" parameterType="SysUserPasskey">
+ update sys_user_passkey
+ <trim prefix="SET" suffixOverrides=",">
+ <if test="passkey != null and passkey != ''">passkey = #{passkey},</if>
+ <if test="createdAt != null">created_at = #{createdAt},</if>
+ </trim>
+ where user_id = #{userId}
+ </update>
+
+ <delete id="deleteSysUserPasskeyByUserId" parameterType="Long">
+ delete from sys_user_passkey where user_id = #{userId}
+ </delete>
+
+ <delete id="deleteSysUserPasskeyByUserIds" parameterType="String">
+ delete from sys_user_passkey where user_id in
+ <foreach item="userId" collection="array" open="(" separator="," close=")">
+ #{userId}
+ </foreach>
+ </delete>
+</mapper>
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/resources/mapper/system/UserScoreMapper.xml b/ruoyi-admin/src/main/resources/mapper/system/UserScoreMapper.xml
new file mode 100644
index 0000000..2f0f7fc
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/mapper/system/UserScoreMapper.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.authentication.mapper.UserScoreMapper">
+
+ <resultMap type="UserScore" id="UserScoreResult">
+ <result property="userId" column="user_id" />
+ <result property="uploaded" column="uploaded" />
+ <result property="downloaded" column="downloaded" />
+ <result property="bonus" column="bonus" />
+ <result property="score" column="score" />
+ <result property="updatedAt" column="updated_at" />
+ </resultMap>
+
+ <sql id="selectUserScoreVo">
+ select user_id, uploaded, downloaded, bonus, score, updated_at from user_score
+ </sql>
+
+ <select id="selectUserScoreList" parameterType="UserScore" resultMap="UserScoreResult">
+ <include refid="selectUserScoreVo"/>
+ <where>
+ <if test="uploaded != null and uploaded != ''"> and uploaded = #{uploaded}</if>
+ <if test="downloaded != null and downloaded != ''"> and downloaded = #{downloaded}</if>
+ <if test="bonus != null "> and bonus = #{bonus}</if>
+ <if test="score != null "> and score = #{score}</if>
+ <if test="updatedAt != null "> and updated_at = #{updatedAt}</if>
+ </where>
+ </select>
+
+ <select id="selectUserScoreByUserId" parameterType="Long" resultMap="UserScoreResult">
+ <include refid="selectUserScoreVo"/>
+ where user_id = #{userId}
+ </select>
+
+ <insert id="insertUserScore" parameterType="UserScore">
+ insert into user_score
+ <trim prefix="(" suffix=")" suffixOverrides=",">
+ <if test="userId != null">user_id,</if>
+ <if test="uploaded != null">uploaded,</if>
+ <if test="downloaded != null">downloaded,</if>
+ <if test="bonus != null">bonus,</if>
+
+ <if test="updatedAt != null">updated_at,</if>
+ </trim>
+ <trim prefix="values (" suffix=")" suffixOverrides=",">
+ <if test="userId != null">#{userId},</if>
+ <if test="uploaded != null">#{uploaded},</if>
+ <if test="downloaded != null">#{downloaded},</if>
+ <if test="bonus != null">#{bonus},</if>
+
+ <if test="updatedAt != null">#{updatedAt},</if>
+ </trim>
+ </insert>
+
+ <update id="updateUserScore" parameterType="UserScore">
+ update user_score
+ <trim prefix="SET" suffixOverrides=",">
+ <if test="uploaded != null">uploaded = #{uploaded},</if>
+ <if test="downloaded != null">downloaded = #{downloaded},</if>
+ <if test="bonus != null">bonus = #{bonus},</if>
+
+ <if test="updatedAt != null">updated_at = #{updatedAt},</if>
+ </trim>
+ where user_id = #{userId}
+ </update>
+
+ <delete id="deleteUserScoreByUserId" parameterType="Long">
+ delete from user_score where user_id = #{userId}
+ </delete>
+
+ <delete id="deleteUserScoreByUserIds" parameterType="String">
+ delete from user_score where user_id in
+ <foreach item="userId" collection="array" open="(" separator="," close=")">
+ #{userId}
+ </foreach>
+ </delete>
+</mapper>
\ No newline at end of file