种子,促销
Change-Id: I0ce919ce4228dcefec26ef636bacd3298c0dc77a
diff --git a/src/main/java/com/example/myproject/MyProjectApplication.java b/src/main/java/com/example/myproject/MyProjectApplication.java
index 49cd5f0..eb58082 100644
--- a/src/main/java/com/example/myproject/MyProjectApplication.java
+++ b/src/main/java/com/example/myproject/MyProjectApplication.java
@@ -5,10 +5,14 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
@SpringBootApplication
@MapperScan("com.example.myproject.mapper") // 扫描 Mapper 接口
+
public class MyProjectApplication {
public static void main(String[] args) {
SpringApplication.run(MyProjectApplication.class, args);
}
}
+
diff --git a/src/main/java/com/example/myproject/common/CommonResultStatus.java b/src/main/java/com/example/myproject/common/CommonResultStatus.java
new file mode 100644
index 0000000..76e63a6
--- /dev/null
+++ b/src/main/java/com/example/myproject/common/CommonResultStatus.java
@@ -0,0 +1,37 @@
+package com.example.myproject.common;
+
+
+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/src/main/java/com/example/myproject/common/Constants.java b/src/main/java/com/example/myproject/common/Constants.java
new file mode 100644
index 0000000..4e3d864
--- /dev/null
+++ b/src/main/java/com/example/myproject/common/Constants.java
@@ -0,0 +1,54 @@
+package com.example.myproject.common;
+
+
+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 = "rocket pt";
+ }
+
+ interface Announce {
+
+ String PROTOCOL = "http";
+
+ String HOSTNAME = "192.168.6.112";
+
+ Integer PORT = 9966;
+
+ }
+}
diff --git a/src/main/java/com/example/myproject/common/ResultStatus.java b/src/main/java/com/example/myproject/common/ResultStatus.java
new file mode 100644
index 0000000..f6c7afe
--- /dev/null
+++ b/src/main/java/com/example/myproject/common/ResultStatus.java
@@ -0,0 +1,14 @@
+package com.example.myproject.common;
+
+
+public interface ResultStatus {
+ /**
+ * 错误码
+ */
+ int getCode();
+
+ /**
+ * 错误信息
+ */
+ String getMessage();
+}
diff --git a/src/main/java/com/example/myproject/common/base/I18nMessage.java b/src/main/java/com/example/myproject/common/base/I18nMessage.java
new file mode 100644
index 0000000..a725e9b
--- /dev/null
+++ b/src/main/java/com/example/myproject/common/base/I18nMessage.java
@@ -0,0 +1,61 @@
+package com.example.myproject.common.base;
+
+import org.springframework.context.MessageSource;
+import org.springframework.context.i18n.LocaleContextHolder;
+import org.springframework.context.support.ReloadableResourceBundleMessageSource;
+
+import java.util.Objects;
+
+import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
+
+@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/src/main/java/com/example/myproject/common/base/OrderPageParam.java b/src/main/java/com/example/myproject/common/base/OrderPageParam.java
new file mode 100644
index 0000000..21ff38b
--- /dev/null
+++ b/src/main/java/com/example/myproject/common/base/OrderPageParam.java
@@ -0,0 +1,99 @@
+package com.example.myproject.common.base;
+
+
+import com.example.myproject.common.CommonResultStatus;
+import com.example.myproject.common.Constants;
+import com.example.myproject.common.exception.RocketPTException;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.Setter;
+
+@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/src/main/java/com/example/myproject/common/base/PageParam.java b/src/main/java/com/example/myproject/common/base/PageParam.java
new file mode 100644
index 0000000..c59108d
--- /dev/null
+++ b/src/main/java/com/example/myproject/common/base/PageParam.java
@@ -0,0 +1,25 @@
+package com.example.myproject.common.base;
+
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.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/src/main/java/com/example/myproject/common/base/PageUtil.java b/src/main/java/com/example/myproject/common/base/PageUtil.java
new file mode 100644
index 0000000..a4b51ce
--- /dev/null
+++ b/src/main/java/com/example/myproject/common/base/PageUtil.java
@@ -0,0 +1,54 @@
+package com.example.myproject.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/src/main/java/com/example/myproject/common/base/ResPage.java b/src/main/java/com/example/myproject/common/base/ResPage.java
new file mode 100644
index 0000000..45a22dd
--- /dev/null
+++ b/src/main/java/com/example/myproject/common/base/ResPage.java
@@ -0,0 +1,31 @@
+package com.example.myproject.common.base;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+/**
+ * 分页的返回值
+ */
+@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/src/main/java/com/example/myproject/common/base/Result.java b/src/main/java/com/example/myproject/common/base/Result.java
new file mode 100644
index 0000000..6d20f4f
--- /dev/null
+++ b/src/main/java/com/example/myproject/common/base/Result.java
@@ -0,0 +1,178 @@
+package com.example.myproject.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;
+
+/**
+ * 返回值实体类
+ */
+@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(String 取消收藏成功) {
+ 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);
+ }
+
+}
diff --git a/src/main/java/com/example/myproject/common/base/Status.java b/src/main/java/com/example/myproject/common/base/Status.java
new file mode 100644
index 0000000..4949d0c
--- /dev/null
+++ b/src/main/java/com/example/myproject/common/base/Status.java
@@ -0,0 +1,57 @@
+package com.example.myproject.common.base;
+
+
+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/src/main/java/com/example/myproject/common/exception/RocketPTException.java b/src/main/java/com/example/myproject/common/exception/RocketPTException.java
new file mode 100644
index 0000000..7a475f2
--- /dev/null
+++ b/src/main/java/com/example/myproject/common/exception/RocketPTException.java
@@ -0,0 +1,29 @@
+package com.example.myproject.common.exception;
+
+import com.example.myproject.common.CommonResultStatus;
+import com.example.myproject.common.ResultStatus;
+
+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(String message) {
+ super(message);
+ this.status = CommonResultStatus.FAIL;
+ }
+
+ public ResultStatus getStatus() {
+ return status;
+ }
+
+
+}
diff --git a/src/main/java/com/example/myproject/config/MyMetaObjectHandler.java b/src/main/java/com/example/myproject/config/MyMetaObjectHandler.java
new file mode 100644
index 0000000..1031b93
--- /dev/null
+++ b/src/main/java/com/example/myproject/config/MyMetaObjectHandler.java
@@ -0,0 +1,24 @@
+package com.example.myproject.config;
+
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import org.apache.ibatis.reflection.MetaObject;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+
+@Component
+public class MyMetaObjectHandler implements MetaObjectHandler {
+
+
+ @Override
+ public void insertFill(MetaObject metaObject) {
+ this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
+ this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
+ }
+
+ // 更新时自动填充
+ @Override
+ public void updateFill(MetaObject metaObject) {
+ this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
+ }
+}
diff --git a/src/main/java/com/example/myproject/config/TrackerConfig.java b/src/main/java/com/example/myproject/config/TrackerConfig.java
new file mode 100644
index 0000000..67522b7
--- /dev/null
+++ b/src/main/java/com/example/myproject/config/TrackerConfig.java
@@ -0,0 +1,81 @@
+package com.example.myproject.config;
+
+import com.turn.ttorrent.tracker.TrackedTorrent;
+import com.turn.ttorrent.tracker.Tracker;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import javax.annotation.PreDestroy;
+import java.io.File;
+import java.io.IOException;
+import java.net.InetAddress;
+
+@Configuration
+public class TrackerConfig {
+ @Value("${pt.tracker.port}")
+ private int trackerPort;
+
+ @Value("${pt.tracker.torrent-dir}")
+ private String torrentDir;
+ @Value("${pt.tracker.announce-url}")
+ private String announceURL;
+
+ private Tracker tracker; // 存储 Tracker 实例
+
+ @Bean
+ public Tracker torrentTracker() throws IOException {
+ // 验证并创建目录
+ File dir = new File(torrentDir);
+ validateTorrentDirectory(dir);
+
+ // 初始化 Tracker
+ tracker = new Tracker(trackerPort, announceURL);
+ tracker.setAcceptForeignTorrents(false); // PT 站点必须关闭匿名模式
+ tracker.setAnnounceInterval(1800); // 30分钟 announce 间隔
+
+ // 加载种子文件
+ loadTorrents(tracker, dir);
+
+ // 启动 Tracker
+ tracker.start(true);
+ System.out.println("Tracker started on port " + trackerPort);
+ return tracker;
+ }
+
+ private void validateTorrentDirectory(File dir) throws IOException {
+ if (!dir.exists()) {
+ if (!dir.mkdirs()) {
+ throw new IOException("无法创建目录: " + dir.getAbsolutePath());
+ }
+ }
+ if (!dir.isDirectory()) {
+ throw new IllegalArgumentException("路径不是目录: " + dir.getAbsolutePath());
+ }
+ if (!dir.canRead() || !dir.canWrite()) {
+ throw new IOException("应用程序无权限访问目录: " + dir.getAbsolutePath());
+ }
+ }
+
+ private void loadTorrents(Tracker tracker, File dir) throws IOException {
+ if (!dir.exists() || !dir.isDirectory()) {
+ throw new IOException("无效的种子目录: " + dir.getAbsolutePath());
+ }
+
+ File[] torrentFiles = dir.listFiles((d, name) -> name.endsWith(".torrent"));
+ if (torrentFiles == null) {
+ throw new IOException("无法读取目录内容: " + dir.getAbsolutePath());
+ }
+
+ for (File f : torrentFiles) {
+ tracker.announce(TrackedTorrent.load(f));
+ }
+ }
+
+ @PreDestroy
+ public void stopTracker() {
+ if (tracker != null) {
+ tracker.stop();
+ System.out.println("Tracker stopped.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/myproject/controller/TorrentController.java b/src/main/java/com/example/myproject/controller/TorrentController.java
new file mode 100644
index 0000000..cea2ccf
--- /dev/null
+++ b/src/main/java/com/example/myproject/controller/TorrentController.java
@@ -0,0 +1,293 @@
+package com.example.myproject.controller;
+
+import com.example.myproject.common.base.PageUtil;
+import com.example.myproject.entity.TorrentEntity;
+import com.example.myproject.service.TorrentService;
+import com.example.myproject.service.PromotionService;
+import com.example.myproject.dto.param.TorrentParam;
+import com.example.myproject.dto.vo.TorrentVO;
+import com.example.myproject.common.base.Result;
+import com.example.myproject.dto.param.TorrentUploadParam;
+import com.example.myproject.dto.TorrentUpdateDTO;
+import com.example.myproject.dto.PromotionCreateDTO;
+import com.example.myproject.entity.Promotion;
+import com.example.myproject.service.UserService;
+
+import io.swagger.v3.oas.annotations.Operation;
+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 org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.List;
+
+import cn.dev33.satoken.annotation.SaCheckLogin;
+import cn.dev33.satoken.stp.StpUtil;
+
+@RestController
+@RequestMapping("/seeds")
+public class TorrentController {
+
+ @Autowired
+ private TorrentService torrentService;
+
+ @Autowired
+ private PromotionService promotionService;
+
+ @Autowired
+ private UserService userService;
+
+
+ @SaCheckLogin
+ @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.search(param);
+
+ // 返回分页结果
+ return Result.ok(list, PageUtil.getPage(list));
+ }
+
+ @SaCheckLogin
+ @Operation(summary = "种子详情查询")
+ @ApiResponse(responseCode = "0", description = "操作成功", content = {
+ @Content(mediaType = "application/json", schema = @Schema(implementation =
+ TorrentEntity.class))
+ })
+ @PostMapping("/info/{id}")
+ public Result info(@PathVariable("id")Long id) {
+
+ TorrentEntity entity = torrentService.selectBySeedId(id);
+ return Result.ok(entity);
+ }
+
+@Operation(summary = "上传种子")
+
+ @PostMapping("/upload")
+ public Result uploadTorrent(
+ @RequestParam("file") MultipartFile file,
+ @ModelAttribute @Validated TorrentUploadParam param) throws IOException {
+ try {
+ // 验证用户权限
+ // Long userId = StpUtil.getLoginIdAsLong();
+ String userId = String.valueOf(param.getUploader());
+ param.setUploader(userId);
+
+ // 验证文件大小和类型
+ if (file.isEmpty() || file.getSize() > 10 * 1024 * 1024) { // 10MB限制
+ return Result.error("文件大小不符合要求");
+ }
+
+ if (!file.getOriginalFilename().toLowerCase().endsWith(".torrent")) {
+ return Result.error("只支持.torrent文件");
+ }
+
+ torrentService.uploadTorrent(file, param);
+ return Result.ok();
+ } catch (Exception e) {
+ return Result.error("种子上传失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 获取种子文件
+ */
+ @GetMapping("/{seed_id}/download")
+ public void downloadTorrent(
+ @PathVariable("seed_id") Long seedId,
+ @RequestParam("passkey") String passkey,
+ HttpServletResponse response) throws IOException {
+
+ // 获取种子实体
+ TorrentEntity entity = torrentService.selectBySeedId(seedId);
+ if (entity == null) {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND, "种子不存在");
+ return;
+ }
+
+ // 获取并处理种子文件内容(需在service中实现passkey注入)
+ byte[] torrentBytes = torrentService.fetch(seedId, passkey);
+
+ // 设置下载文件名
+ String filename = entity.getFileName();
+ if (filename == null || filename.isBlank()) {
+ filename = seedId + ".torrent";
+ }
+ if (!filename.toLowerCase().endsWith(".torrent")) {
+ filename = filename + ".torrent";
+ }
+ filename = java.net.URLEncoder.encode(filename, java.nio.charset.StandardCharsets.UTF_8).replaceAll("\\+",
+ "%20");
+
+ // 设置响应头
+ response.setCharacterEncoding(java.nio.charset.StandardCharsets.UTF_8.name());
+ response.setContentLength(torrentBytes.length);
+ response.setContentType("application/x-bittorrent");
+ response.setHeader("Content-Disposition", "attachment;filename=" + filename);
+
+ // 写入文件内容
+ response.getOutputStream().write(torrentBytes);
+ response.getOutputStream().flush();
+ }
+
+ /**
+ * 收藏或者取消收藏
+ */
+ @PostMapping("/{seed_id}/favorite-toggle")
+ public Result favorite(
+ @PathVariable("seed_id") Long seedId,
+ @RequestParam("user_id") Long userId) {
+ try {
+
+ return torrentService.favorite(seedId, userId);
+ } catch (Exception e) {
+ return Result.error("失败: ");
+ }
+ }
+
+ @SaCheckLogin
+ @Operation(summary = "删除种子")
+ @DeleteMapping("/{torrentId}")
+ public Result deleteTorrent(@PathVariable Long torrentId) {
+ try {
+ // 验证用户权限
+ Long userId = StpUtil.getLoginIdAsLong();
+ if (!torrentService.canUserDeleteTorrent(torrentId, userId)) {
+ return Result.error("没有权限删除此种子");
+ }
+
+ torrentService.deleteTorrent(torrentId);
+ return Result.ok();
+ } catch (Exception e) {
+ return Result.error("删除失败: " + e.getMessage());
+ }
+ }
+
+ @SaCheckLogin
+ @Operation(summary = "修改种子信息")
+ @PutMapping("/{torrentId}")
+ public Result updateTorrent(
+ @PathVariable Long torrentId,
+ @RequestBody @Validated TorrentUpdateDTO updateDTO) {
+ try {
+ // 验证用户权限
+ Long userId = StpUtil.getLoginIdAsLong();
+ if (!torrentService.canUserUpdateTorrent(torrentId, userId)) {
+ return Result.error("没有权限修改此种子");
+ }
+
+ torrentService.updateTorrent(torrentId, updateDTO);
+ return Result.ok();
+ } catch (Exception e) {
+ return Result.error("更新失败: " + e.getMessage());
+ }
+ }
+
+ @SaCheckLogin
+ @Operation(summary = "创建促销活动")
+ @PostMapping("/promotions")
+ public Result createPromotion(@RequestBody @Validated PromotionCreateDTO promotionDTO) {
+ try {
+ // 验证用户权限(只有管理员可以创建促销)
+// if (!StpUtil.hasRole("admin")) {
+// return Result.error("没有权限创建促销活动");
+// }
+//
+ Promotion promotion = promotionService.createPromotion(promotionDTO);
+ return Result.ok(promotion);
+ } catch (Exception e) {
+ return Result.error("创建促销失败: " + e.getMessage());
+ }
+ }
+
+ @SaCheckLogin
+ @Operation(summary = "获取促销活动列表")
+ @GetMapping("/promotions")
+ public Result getPromotions() {
+ try {
+ List<Promotion> promotions = promotionService.getAllActivePromotions();
+ return Result.ok(promotions);
+ } catch (Exception e) {
+ return Result.error("获取促销列表失败: " + e.getMessage());
+ }
+ }
+
+ @SaCheckLogin
+ @Operation(summary = "获取促销详情")
+ @GetMapping("/promotions/{promotionId}")
+ public Result getPromotionDetails(@PathVariable Long promotionId) {
+ try {
+ Promotion promotion = promotionService.getPromotionById(promotionId);
+ if (promotion == null) {
+ return Result.error("促销活动不存在");
+ }
+ return Result.ok(promotion);
+ } catch (Exception e) {
+ return Result.error("获取促销详情失败: " + e.getMessage());
+ }
+ }
+
+ @SaCheckLogin
+ @Operation(summary = "删除促销活动")
+ @DeleteMapping("/promotions/{promotionId}")
+ public Result deletePromotion(@PathVariable Long promotionId) {
+ try {
+ // 验证用户权限(只有管理员可以删除促销)
+ if (!StpUtil.hasRole("admin")) {
+ return Result.error("没有权限删除促销活动");
+ }
+
+ promotionService.deletePromotion(promotionId);
+ return Result.ok();
+ } catch (Exception e) {
+ return Result.error("删除促销失败: " + e.getMessage());
+ }
+ }
+
+ // 下载种子(包含反作弊机制)
+ @PostMapping("/{torrentId}/download")
+ public ResponseEntity<?> downloadTorrent(@PathVariable Long torrentId,
+ @RequestParam Long userId) {
+// // 验证用户身份和权限
+// if (!userService.validateUser(userId)) {
+// return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
+// }
+
+ // 检查用户上传量是否足够
+ if (!torrentService.checkUserUploadRatio(userId)) {
+ return ResponseEntity.status(HttpStatus.FORBIDDEN)
+ .body("上传量不足,无法下载");
+ }
+
+ // 应用促销折扣(如果有)
+ double downloadSize = torrentService.calculateDownloadSize(torrentId, userId);
+
+ // 记录下载
+ torrentService.recordDownload(torrentId, userId, downloadSize);
+
+ return ResponseEntity.ok().build();
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/myproject/controller/UserController.java b/src/main/java/com/example/myproject/controller/UserController.java
index 4bf6adf..acda403 100644
--- a/src/main/java/com/example/myproject/controller/UserController.java
+++ b/src/main/java/com/example/myproject/controller/UserController.java
@@ -1,16 +1,25 @@
package com.example.myproject.controller;
+import cn.dev33.satoken.annotation.SaCheckLogin;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.example.myproject.common.base.PageUtil;
+import com.example.myproject.dto.param.TorrentParam;
+import com.example.myproject.dto.vo.TorrentVO;
+import com.example.myproject.entity.TorrentEntity;
import com.example.myproject.mapper.UserMapper;
import com.example.myproject.mapper.VerificationTokenMapper;
import com.example.myproject.entity.User;
import com.example.myproject.entity.VerificationToken;
import com.example.myproject.service.EmailService;
import com.example.myproject.service.UserService;
-import com.example.myproject.utils.Result;
+import com.example.myproject.common.base.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
+import io.swagger.v3.oas.annotations.Operation;
+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 org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -27,6 +36,7 @@
import javax.annotation.Resource;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
+import java.util.List;
@RestController
@RequestMapping("/user")
@@ -61,9 +71,9 @@
User user = userMapper.selectOne(new QueryWrapper<User>().eq("username", username));
System.out.println("Login successful for user: " + username);
- return Result.success(user);
+ return Result.ok(user);
} catch (AuthenticationException e) {
- return Result.error("401", "登录失败:" + e.getMessage());
+ return Result.error("登录失败");
}
}
@@ -71,15 +81,15 @@
@ApiOperation(value = "用户注册", notes = "使用用户信息进行注册")
public Result registerController(@RequestBody @ApiParam(value = "新用户信息", required = true) User newUser) {
if (userService.checkEmailExists(newUser.getEmail())) {
- return Result.error("邮箱冲突", "邮箱已被使用,请使用其他邮箱注册或找回密码!");
+ return Result.error( "邮箱已被使用,请使用其他邮箱注册或找回密码!");
}
boolean success = userService.preRegisterUser(newUser);
if (success) {
User responseUser = new User();
responseUser.setEmail(newUser.getEmail());
- return Result.success(responseUser, "验证邮件已发送,请检查您的邮箱。");
+ return Result.ok();
} else {
- return Result.error("注册失败", "账号已存在或注册失败!");
+ return Result.error("账号已存在或注册失败!");
}
}
@@ -100,9 +110,9 @@
String code = verificationRequest.getCode();
boolean isVerified = userService.verifyEmail(email, code);
if (isVerified) {
- return Result.success(null, "邮箱验证成功!");
+ return Result.ok();
} else {
- return Result.error("验证失败", "验证码错误或已过期!");
+ return Result.error( "验证码错误或已过期!");
}
}
@@ -123,7 +133,7 @@
if (user == null) {
logger.error("未找到与该邮箱地址相关联的用户: {}", email);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
- .body(Result.error("1","未找到与该邮箱地址相关联的用户"));
+ .body(Result.error("未找到与该邮箱地址相关联的用户"));
}
// 生成验证码
@@ -139,15 +149,42 @@
logger.info("验证令牌已保存,用户: {}", user.getUsername());
emailService.sendVerificationEmail(email, token);
- return ResponseEntity.ok(Result.success(200, "验证邮件已发送!"));
+ return ResponseEntity.ok(Result.ok());
}
@PostMapping("/checkPassword")
public Result<String> checkPassword(@RequestParam Long userId, @RequestParam String password) {
boolean isPasswordCorrect = userService.checkPassword(userId, password);
if (isPasswordCorrect) {
- return Result.success("200","原始密码输入正确");
+ return Result.ok();
} else {
- return Result.error("305","原始密码输入错误");
+ return Result.error("原始密码输入错误");
}
}
+
+
+// @SaCheckLogin
+// @Operation(summary = "用户收藏列表", description = "获取用户收藏的种子列表-分页-排序")
+// @ApiResponse(responseCode = "0", description = "操作成功",
+// content = {@Content(mediaType = "application/json",
+// schema = @Schema(implementation = TorrentVO.class))
+// })
+// @PostMapping("/favorite/list")
+// public Result listFavorites(@RequestBody FavoriteParam param) {
+// if (param.getUserId() == null) {
+// return Result.error("缺少 userId");
+// }
+//
+// // 校验排序字段是否合理(可选)
+// param.validOrder(param.getOrderKey(TorrentEntity.class));
+//
+// PageUtil.startPage(param);
+//
+// List<TorrentEntity> list = favoriteService.getUserFavoritesPaged(param.getUserId());
+//
+// return Result.ok(list, PageUtil.getPage(list));
+// }
+//
+
+
+
}
diff --git a/src/main/java/com/example/myproject/dto/PromotionCreateDTO.java b/src/main/java/com/example/myproject/dto/PromotionCreateDTO.java
new file mode 100644
index 0000000..211979b
--- /dev/null
+++ b/src/main/java/com/example/myproject/dto/PromotionCreateDTO.java
@@ -0,0 +1,35 @@
+package com.example.myproject.dto;
+
+import lombok.Data;
+import javax.validation.constraints.*;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+public class PromotionCreateDTO {
+ @NotBlank(message = "促销名称不能为空")
+ @Size(max = 100, message = "促销名称长度不能超过100个字符")
+ private String name;
+
+ @Size(max = 500, message = "描述长度不能超过500个字符")
+ private String description;
+
+ @NotNull(message = "开始时间不能为空")
+ private LocalDateTime startTime;
+
+ @NotNull(message = "结束时间不能为空")
+ private LocalDateTime endTime;
+
+ @NotNull(message = "折扣比例不能为空")
+ @Min(value = 0, message = "折扣比例不能小于0")
+ @Max(value = 100, message = "折扣比例不能大于100")
+ private double discountPercentage;
+
+ @NotEmpty(message = "适用种子列表不能为空")
+ private List<Long> applicableTorrentIds;
+
+ @AssertTrue(message = "结束时间必须晚于开始时间")
+ public boolean isEndTimeAfterStartTime() {
+ return endTime != null && startTime != null && endTime.isAfter(startTime);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/myproject/dto/TorrentUpdateDTO.java b/src/main/java/com/example/myproject/dto/TorrentUpdateDTO.java
new file mode 100644
index 0000000..4b09321
--- /dev/null
+++ b/src/main/java/com/example/myproject/dto/TorrentUpdateDTO.java
@@ -0,0 +1,24 @@
+package com.example.myproject.dto;
+
+import lombok.Data;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+
+@Data
+public class TorrentUpdateDTO {
+ @NotBlank(message = "标题不能为空")
+ @Size(max = 30, message = "名称长度不能超过30个字符")
+ private String title;
+
+ @Size(max = 1000, message = "描述长度不能超过1000个字符")
+ private String description;
+
+ @NotBlank(message = "分类不能为空")
+ private String category;
+
+
+ private String tags;
+ @NotBlank(message = "封面不能为空")
+
+ private String imageUrl;
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/myproject/dto/param/TorrentParam.java b/src/main/java/com/example/myproject/dto/param/TorrentParam.java
new file mode 100644
index 0000000..1ef832b
--- /dev/null
+++ b/src/main/java/com/example/myproject/dto/param/TorrentParam.java
@@ -0,0 +1,43 @@
+package com.example.myproject.dto.param;
+
+import com.example.myproject.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 = "关键字")
+ private String keyword;
+
+ @Schema(description = "分类")
+ private String category;
+
+ @Schema(description = "促销种子")
+ private 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();
+ if (!searchstrElement.isEmpty()) {
+ likeExpressions.add(searchstrElement);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/myproject/dto/param/TorrentUploadParam.java b/src/main/java/com/example/myproject/dto/param/TorrentUploadParam.java
new file mode 100644
index 0000000..6ede468
--- /dev/null
+++ b/src/main/java/com/example/myproject/dto/param/TorrentUploadParam.java
@@ -0,0 +1,28 @@
+package com.example.myproject.dto.param;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@Schema(description = "种子上传参数")
+public class TorrentUploadParam {
+ @Schema(description = "上传者")
+ private String uploader;
+
+ @Schema(description = "种子标题")
+ private String title;
+
+ @Schema(description = "种子描述")
+ private String description;
+
+ @Schema(description = "种子标签")
+ private String tags;
+
+ @Schema(description = "种子分类")
+ private String category;
+
+ @Schema(description = "种子封面图 URL")
+ private String imageUrl;
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/myproject/dto/vo/TorrentVO.java b/src/main/java/com/example/myproject/dto/vo/TorrentVO.java
new file mode 100644
index 0000000..e723cb3
--- /dev/null
+++ b/src/main/java/com/example/myproject/dto/vo/TorrentVO.java
@@ -0,0 +1,105 @@
+package com.example.myproject.dto.vo;
+
+import java.time.LocalDateTime;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class TorrentVO {
+ /**
+ * 种子ID
+ */
+ @Schema(description = "种子ID")
+ private Long id;
+
+ /**
+ * 标题
+ */
+ @NotEmpty
+ @Schema(description = "标题")
+ private String title;
+
+
+ /**
+ * 封面
+ */
+ @Schema(description = "封面")
+ private String imageUrl;
+ /**
+ * 描述
+ */
+ @NotEmpty
+ @Schema(description = "描述")
+ private String description;
+
+ /**
+ * 类别
+ */
+ @NotNull
+ @Schema(description = "类别")
+ private String category;
+
+
+ /**
+ * 添加日期
+ */
+ @Schema(description = "添加日期")
+ private LocalDateTime createTime;
+
+ /**
+ * 修改日期
+ */
+ @Schema(description = "修改日期")
+ private LocalDateTime updateTime;
+
+ /**
+ * 上传者
+ */
+ @Schema(description = "上传者")
+ private String uploader;
+ /**
+ * 文件大小
+ */
+ @Schema(description = "文件大小")
+ private Long size;
+
+
+
+ /**
+ * 评论数
+ */
+ @Schema(description = "评论数")
+ private Integer comments;
+ /**
+ * 浏览次数
+ */
+ @Schema(description = "浏览次数")
+ private Integer views;
+ /**
+ * 点击次数
+ */
+ @Schema(description = "点击次数")
+ private Integer hits;
+
+
+ /**
+ * 下载数
+ */
+ @Schema(description = "下载数")
+ private Integer leechers;
+ /**
+ * 做种数
+ */
+ @Schema(description = "做种数")
+ private Integer seeders;
+
+ /**
+ * 完成次数
+ */
+ @Schema(description = "完成次数")
+ private Integer completions;
+
+}
diff --git a/src/main/java/com/example/myproject/entity/EntityBase.java b/src/main/java/com/example/myproject/entity/EntityBase.java
new file mode 100644
index 0000000..48583cb
--- /dev/null
+++ b/src/main/java/com/example/myproject/entity/EntityBase.java
@@ -0,0 +1,61 @@
+package com.example.myproject.entity;
+
+
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+
+import java.util.Objects;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 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.
+
+ */
+@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());
+ }
+
+
+ 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/src/main/java/com/example/myproject/entity/FavoriteEntity.java b/src/main/java/com/example/myproject/entity/FavoriteEntity.java
new file mode 100644
index 0000000..3f76cfe
--- /dev/null
+++ b/src/main/java/com/example/myproject/entity/FavoriteEntity.java
@@ -0,0 +1,36 @@
+package com.example.myproject.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@TableName("favorite")
+@ApiModel("收藏实体类")
+public class FavoriteEntity {
+ @TableId(type = IdType.AUTO)
+ @ApiModelProperty(value = "收藏ID", example = "1")
+ private Long id;
+
+ @ApiModelProperty(value = "用户ID", example = "1001")
+ @JsonProperty("userId")
+ @TableField("user_id")
+ private Long userId;
+
+ @ApiModelProperty(value = "种子ID", example = "2001")
+ @JsonProperty("seedId")
+ @TableField("seed_id")
+ private Long seedId;
+
+ @ApiModelProperty(value = "收藏时间", example = "2024-05-13 12:00:00")
+ @JsonProperty("createTime")
+ @TableField("create_time")
+ private Date createTime;
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/myproject/entity/Promotion.java b/src/main/java/com/example/myproject/entity/Promotion.java
new file mode 100644
index 0000000..4ca846b
--- /dev/null
+++ b/src/main/java/com/example/myproject/entity/Promotion.java
@@ -0,0 +1,34 @@
+package com.example.myproject.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@TableName("promotion")
+public class Promotion {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ private String name;
+
+ private String description;
+
+ private LocalDateTime startTime;
+
+ private LocalDateTime endTime;
+
+ private double discountPercentage;
+
+ private String applicableTorrentIds;
+
+ private LocalDateTime createTime;
+
+ private LocalDateTime updateTime;
+
+ private Boolean isDeleted;
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/myproject/entity/TorrentEntity.java b/src/main/java/com/example/myproject/entity/TorrentEntity.java
new file mode 100644
index 0000000..f62fb76
--- /dev/null
+++ b/src/main/java/com/example/myproject/entity/TorrentEntity.java
@@ -0,0 +1,108 @@
+package com.example.myproject.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.Date;
+import java.util.List;
+
+@Data
+@TableName("torrent")
+@ApiModel("种子实体类")
+public class TorrentEntity {
+
+ @TableId(type = IdType.AUTO)
+ @ApiModelProperty(value = "种子ID", example = "1")
+ private Long id;
+
+ @JsonProperty("infoHash")
+ @ApiModelProperty(value = "种子信息哈希", example = "abcdef123456")
+ private String infoHash;
+
+ @JsonProperty("fileName")
+ @ApiModelProperty(value = "种子文件名", example = "movie_torrent_file.torrent")
+ private String fileName;
+
+ @JsonProperty("uploader")
+ @ApiModelProperty(value = "上传者", example = "user123")
+ private String uploader;
+
+ @JsonProperty("createdTime")
+ @ApiModelProperty(value = "上传时间", example = "2024-01-01 12:00:00")
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime createTime;
+
+ @JsonProperty("updateTime")
+ @ApiModelProperty(value = "更新时间", example = "2024-01-01 12:00:00")
+
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private LocalDateTime updateTime;
+
+ @JsonProperty("size")
+ @ApiModelProperty(value = "种子文件大小", example = "123456")
+ private Long size;
+
+ @JsonProperty("title")
+ @ApiModelProperty(value = "种子标题", example = "《星际穿越》")
+ private String title;
+
+ @JsonProperty("description")
+ @ApiModelProperty(value = "种子描述", example = "这是一部好看的科幻电影")
+ private String description;
+
+ @JsonProperty("tags")
+ @ApiModelProperty(value = "种子标签", example = "[\"科幻\",\"动作\"]")
+ private String tags;
+
+ @JsonProperty("category")
+ @ApiModelProperty(value = "种子分类", example = "movie")
+ private String category;
+
+ @JsonProperty("imageUrl")
+ @ApiModelProperty(value = "种子封面图URL", example = "http://example.com/images/cover.jpg")
+ private String imageUrl;
+
+ @JsonProperty("leechers")
+ @ApiModelProperty(value = "下载次数", example = "123")
+ private Integer leechers;
+
+ @JsonProperty("seeders")
+ @ApiModelProperty(value = "做种数", example = "10")
+ private Integer seeders;
+
+ @JsonProperty("comments")
+ @ApiModelProperty(value = "评论数", example = "5")
+ private Integer comments;
+
+ @JsonProperty("views")
+ @ApiModelProperty(value = "浏览次数", example = "1000")
+ private Integer views;
+
+ @JsonProperty("hits")
+ @ApiModelProperty(value = "点击次数", example = "2000")
+ private Integer hits;
+
+ @JsonProperty("promotionTimeType")
+ @ApiModelProperty(value = "促销时间类型", example = "1")
+ private Integer promotionTimeType;
+
+ @JsonProperty("promotionUntil")
+ @ApiModelProperty(value = "促销截止日期", example = "2024-12-31T23:59:59")
+ private LocalDateTime promotionUntil;
+
+
+ @JsonProperty("torrentFile")
+ @ApiModelProperty(value = "种子文件", example = "base64 encoded torrent file")
+ private byte[] torrentFile;
+
+ @JsonProperty("isDeleted")
+ @ApiModelProperty(value = "是否删除", example = "false")
+ private Boolean isDeleted;
+
+ public TorrentEntity() {
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/myproject/entity/User.java b/src/main/java/com/example/myproject/entity/User.java
index 20f7138..7574e67 100644
--- a/src/main/java/com/example/myproject/entity/User.java
+++ b/src/main/java/com/example/myproject/entity/User.java
@@ -9,6 +9,8 @@
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
+import java.time.LocalDateTime;
+
@Data
@TableName("user") // 指定数据库表名
@ApiModel("用户实体类") // 用于描述模型
@@ -50,6 +52,26 @@
@ApiModelProperty(value = "头像")
private String avatar;
+ @JsonProperty("uploaded")
+ @ApiModelProperty(value = "上传量", example = "1000")
+ private Long uploaded;
+
+ @JsonProperty("downloaded")
+ @ApiModelProperty(value = "下载量", example = "500")
+ private Long downloaded;
+
+ @JsonProperty("create_time")
+ @ApiModelProperty(value = "创建时间", example = "2024-04-01T12:00:00")
+ private LocalDateTime createTime;
+
+ @JsonProperty("update_time")
+ @ApiModelProperty(value = "更新时间", example = "2024-04-01T12:00:00")
+ private LocalDateTime updateTime;
+
+ @JsonProperty("is_deleted")
+ @ApiModelProperty(value = "是否删除", example = "false")
+ private Boolean isDeleted;
+
public User() {
}
}
diff --git a/src/main/java/com/example/myproject/mapper/FavoriteMapper.java b/src/main/java/com/example/myproject/mapper/FavoriteMapper.java
new file mode 100644
index 0000000..08b8151
--- /dev/null
+++ b/src/main/java/com/example/myproject/mapper/FavoriteMapper.java
@@ -0,0 +1,15 @@
+package com.example.myproject.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.myproject.entity.FavoriteEntity;
+import org.apache.ibatis.annotations.*;
+
+@Mapper
+public interface FavoriteMapper extends BaseMapper<FavoriteEntity> {
+
+ @Select("SELECT * FROM favorite WHERE user_id = #{userId} AND seed_id = #{seedId} LIMIT 1")
+ FavoriteEntity selectByUserIdAndSeedId(@Param("userId") Long userId, @Param("seedId") Long seedId);
+
+ @Delete("DELETE FROM favorite WHERE user_id = #{userId} AND seed_id = #{seedId}")
+ void deleteByUserIdAndSeedId(@Param("userId") Long userId, @Param("seedId") Long seedId);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/myproject/mapper/PromotionMapper.java b/src/main/java/com/example/myproject/mapper/PromotionMapper.java
new file mode 100644
index 0000000..a478835
--- /dev/null
+++ b/src/main/java/com/example/myproject/mapper/PromotionMapper.java
@@ -0,0 +1,52 @@
+package com.example.myproject.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.myproject.entity.Promotion;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Mapper
+public interface PromotionMapper extends BaseMapper<Promotion> {
+
+ @Select("SELECT * FROM promotion WHERE is_deleted = false " +
+ "AND start_time <= #{now} AND end_time >= #{now}")
+ List<Promotion> findActivePromotions(@Param("now") LocalDateTime now);
+
+ /**
+ * 查找某个torrentId是否在促销活动的applicable_torrent_ids字符串中
+ * 用MySQL的FIND_IN_SET判断
+ */
+ @Select("SELECT p.* FROM promotion p " +
+ "WHERE p.is_deleted = false " +
+ "AND p.start_time <= #{now} AND p.end_time >= #{now} " +
+ "AND FIND_IN_SET(#{torrentId}, p.applicable_torrent_ids) > 0")
+ List<Promotion> findActivePromotionsForTorrent(
+ @Param("torrentId") Long torrentId,
+ @Param("now") LocalDateTime now);
+
+ /**
+ * 校验种子id是否存在,返回计数,0代表不存在,>0代表存在
+ */
+ @Select("SELECT COUNT(*) FROM torrent WHERE id = #{torrentId}")
+ int checkTorrentExists(@Param("torrentId") Long torrentId);
+
+ /**
+ * 插入促销活动
+ */
+// int insert(Promotion promotion);
+
+ /**
+ * 根据ID更新促销活动(例如软删除)
+ */
+ int updateById(Promotion promotion);
+
+ /**
+ * 根据ID查询促销活动
+ */
+ Promotion selectById(@Param("id") Long id);
+}
diff --git a/src/main/java/com/example/myproject/mapper/TorrentMapper.java b/src/main/java/com/example/myproject/mapper/TorrentMapper.java
new file mode 100644
index 0000000..f9cdcdc
--- /dev/null
+++ b/src/main/java/com/example/myproject/mapper/TorrentMapper.java
@@ -0,0 +1,32 @@
+package com.example.myproject.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.example.myproject.dto.param.TorrentParam;
+import com.example.myproject.entity.TorrentEntity;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+
+import java.util.List;
+
+@Mapper
+public interface TorrentMapper extends BaseMapper<TorrentEntity> {
+
+ @Select("SELECT * FROM torrents WHERE info_hash = #{infoHash}")
+ TorrentEntity selectByInfoHash(String infoHash);
+
+ @Select("SELECT * FROM torrents WHERE seed_id = #{seedId}")
+ TorrentEntity selectBySeedId(Long seedId);
+
+ List<TorrentEntity> search(@Param("param") TorrentParam param);
+
+ @Update("UPDATE torrent SET downloads = downloads + 1 WHERE id = #{torrentId}")
+ void increaseDownloads(@Param("torrentId") Long torrentId);
+
+ boolean checkFavorite(@Param("seedId") Long seedId, @Param("userId") Long userId);
+
+ void addFavorite(@Param("seedId") Long seedId, @Param("userId") Long userId);
+
+ void removeFavorite(@Param("seedId") Long seedId, @Param("userId") Long userId);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/myproject/mapper/UserMapper.java b/src/main/java/com/example/myproject/mapper/UserMapper.java
index a070bb5..3a29b64 100644
--- a/src/main/java/com/example/myproject/mapper/UserMapper.java
+++ b/src/main/java/com/example/myproject/mapper/UserMapper.java
@@ -4,6 +4,7 @@
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
import org.springframework.data.repository.query.Param;
import java.util.List;
@@ -22,4 +23,9 @@
// 根据用户名包含查找用户
List<User> selectByUsernameContaining(@Param("name") String name);
+ @Update("UPDATE user SET downloaded = downloaded + #{size} WHERE id = #{userId}")
+ void increaseDownloaded(@Param("userId") Long userId, @Param("size") double size);
+
+ boolean hasRole(@Param("userId") Long userId, @Param("role") String role);
+
}
\ No newline at end of file
diff --git a/src/main/java/com/example/myproject/service/PromotionService.java b/src/main/java/com/example/myproject/service/PromotionService.java
new file mode 100644
index 0000000..087c13b
--- /dev/null
+++ b/src/main/java/com/example/myproject/service/PromotionService.java
@@ -0,0 +1,17 @@
+package com.example.myproject.service;
+
+import com.example.myproject.entity.Promotion;
+import com.example.myproject.dto.PromotionCreateDTO;
+import java.util.List;
+
+public interface PromotionService {
+ Promotion createPromotion(PromotionCreateDTO promotionDTO);
+
+ List<Promotion> getAllActivePromotions();
+
+ Promotion getPromotionById(Long promotionId);
+
+ void deletePromotion(Long promotionId);
+
+ double getCurrentDiscount(Long torrentId);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/myproject/service/TorrentService.java b/src/main/java/com/example/myproject/service/TorrentService.java
new file mode 100644
index 0000000..941804d
--- /dev/null
+++ b/src/main/java/com/example/myproject/service/TorrentService.java
@@ -0,0 +1,37 @@
+package com.example.myproject.service;
+
+import com.example.myproject.common.base.Result;
+import com.example.myproject.entity.TorrentEntity;
+import com.example.myproject.dto.param.TorrentParam;
+import com.example.myproject.dto.param.TorrentUploadParam;
+import com.example.myproject.dto.TorrentUpdateDTO;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.util.List;
+
+public interface TorrentService {
+ List<TorrentEntity> search(TorrentParam param);
+
+ TorrentEntity selectBySeedId(Long seedId);
+
+ void uploadTorrent(MultipartFile file, TorrentUploadParam param) throws IOException;
+
+ byte[] fetch(Long seedId, String passkey) throws IOException;
+
+ Result favorite(Long seedId, Long userId);
+
+ void deleteTorrent(Long seedId);
+
+ void updateTorrent(Long seedId, TorrentUpdateDTO updateDTO);
+
+ boolean canUserDeleteTorrent(Long seedId, Long userId);
+
+ boolean canUserUpdateTorrent(Long seedId, Long userId);
+
+ boolean checkUserUploadRatio(Long userId);
+
+ double calculateDownloadSize(Long torrentId, Long userId);
+
+ void recordDownload(Long torrentId, Long userId, double downloadSize);
+}
diff --git a/src/main/java/com/example/myproject/service/UserService.java b/src/main/java/com/example/myproject/service/UserService.java
index 535f635..71435c7 100644
--- a/src/main/java/com/example/myproject/service/UserService.java
+++ b/src/main/java/com/example/myproject/service/UserService.java
@@ -21,4 +21,5 @@
boolean checkPassword(Long userId, String rawPassword);
+// Integer getUserId();
}
\ No newline at end of file
diff --git a/src/main/java/com/example/myproject/service/serviceImpl/PromotionServiceImpl.java b/src/main/java/com/example/myproject/service/serviceImpl/PromotionServiceImpl.java
new file mode 100644
index 0000000..9d34cbc
--- /dev/null
+++ b/src/main/java/com/example/myproject/service/serviceImpl/PromotionServiceImpl.java
@@ -0,0 +1,114 @@
+package com.example.myproject.service.serviceImpl;
+
+import com.example.myproject.entity.Promotion;
+import com.example.myproject.mapper.PromotionMapper;
+import com.example.myproject.service.PromotionService;
+import com.example.myproject.dto.PromotionCreateDTO;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+public class PromotionServiceImpl implements PromotionService {
+
+ @Autowired
+ private PromotionMapper promotionMapper;
+
+ @Override
+ @Transactional
+ public Promotion createPromotion(PromotionCreateDTO promotionDTO) {
+ // 验证时间
+ LocalDateTime now = LocalDateTime.now();
+ if (promotionDTO.getEndTime().isBefore(now)) {
+ throw new RuntimeException("结束时间不能早于当前时间");
+ }
+
+ // 验证种子ID是否存在
+ validateTorrentIds(promotionDTO.getApplicableTorrentIds());
+
+ // 创建促销活动
+ Promotion promotion = new Promotion();
+ promotion.setName(promotionDTO.getName());
+ promotion.setDescription(promotionDTO.getDescription());
+ promotion.setStartTime(promotionDTO.getStartTime());
+ promotion.setEndTime(promotionDTO.getEndTime());
+ promotion.setDiscountPercentage(promotionDTO.getDiscountPercentage());
+
+ // 把List<Long>转换成逗号分隔字符串
+ String applicableTorrentIdsStr = promotionDTO.getApplicableTorrentIds().stream()
+ .map(String::valueOf)
+ .collect(Collectors.joining(","));
+ promotion.setApplicableTorrentIds(applicableTorrentIdsStr);
+
+ promotion.setCreateTime(now);
+ promotion.setUpdateTime(now);
+ promotion.setIsDeleted(false);
+
+ promotionMapper.insert(promotion);
+ return promotion;
+ }
+
+ @Override
+ public List<Promotion> getAllActivePromotions() {
+ LocalDateTime now = LocalDateTime.now();
+ return promotionMapper.findActivePromotions(now);
+ }
+
+ @Override
+ public Promotion getPromotionById(Long promotionId) {
+ Promotion promotion = promotionMapper.selectById(promotionId);
+ if (promotion == null || promotion.getIsDeleted()) {
+ return null;
+ }
+ return promotion;
+ }
+
+ @Override
+ @Transactional
+ public void deletePromotion(Long promotionId) {
+ Promotion promotion = getPromotionById(promotionId);
+ if (promotion == null) {
+ throw new RuntimeException("促销活动不存在");
+ }
+
+ // 软删除
+ promotion.setIsDeleted(true);
+ promotion.setUpdateTime(LocalDateTime.now());
+ promotionMapper.updateById(promotion);
+ }
+
+ @Override
+ public double getCurrentDiscount(Long torrentId) {
+ LocalDateTime now = LocalDateTime.now();
+ List<Promotion> activePromotions = promotionMapper.findActivePromotionsForTorrent(torrentId, now);
+
+ // 如果有多个促销活动,取折扣最大的
+ return activePromotions.stream()
+ .mapToDouble(Promotion::getDiscountPercentage)
+ .max()
+ .orElse(0.0);
+ }
+
+ /**
+ * 验证种子ID是否存在
+ */
+ private void validateTorrentIds(List<Long> torrentIds) {
+ if (torrentIds == null || torrentIds.isEmpty()) {
+ throw new RuntimeException("适用种子列表不能为空");
+ }
+
+ // 检查所有种子ID是否都存在
+ List<Long> invalidIds = torrentIds.stream()
+ .filter(id -> promotionMapper.checkTorrentExists(id) == 0) // 改成 == 0
+ .collect(Collectors.toList());
+
+ if (!invalidIds.isEmpty()) {
+ throw new RuntimeException("以下种子ID不存在: " + invalidIds);
+ }
+ }
+}
diff --git a/src/main/java/com/example/myproject/service/serviceImpl/TorrentServiceImpl.java b/src/main/java/com/example/myproject/service/serviceImpl/TorrentServiceImpl.java
new file mode 100644
index 0000000..884c65d
--- /dev/null
+++ b/src/main/java/com/example/myproject/service/serviceImpl/TorrentServiceImpl.java
@@ -0,0 +1,296 @@
+package com.example.myproject.service.serviceImpl;
+
+import com.example.myproject.entity.TorrentEntity;
+import com.example.myproject.entity.User;
+import com.example.myproject.mapper.TorrentMapper;
+import com.example.myproject.mapper.UserMapper;
+import com.example.myproject.service.TorrentService;
+import com.example.myproject.service.PromotionService;
+import com.example.myproject.dto.param.TorrentParam;
+import com.example.myproject.dto.param.TorrentUploadParam;
+import com.example.myproject.dto.TorrentUpdateDTO;
+import com.turn.ttorrent.bcodec.BDecoder;
+import com.turn.ttorrent.bcodec.BEValue;
+import com.turn.ttorrent.bcodec.BEncoder;
+import com.turn.ttorrent.client.SimpleClient;
+import com.turn.ttorrent.common.creation.MetadataBuilder;
+import com.turn.ttorrent.tracker.Tracker;
+import com.turn.ttorrent.tracker.TrackedTorrent;
+import com.example.myproject.common.base.Result;
+
+import com.turn.ttorrent.tracker.TrackedTorrent;
+import org.apache.commons.codec.binary.Hex;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.time.LocalTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+@Service
+public class TorrentServiceImpl implements TorrentService {
+ @Autowired
+ private Tracker tracker;
+
+ @Autowired
+ private TorrentMapper torrentMapper;
+
+ private final Map<String, TrackedTorrent> torrentRegistry = new HashMap<>();
+
+
+ @Autowired
+ private UserMapper userMapper;
+
+ @Autowired
+ private PromotionService promotionService;
+
+ private static final double MIN_UPLOAD_RATIO = 0.5; // 最小上传比例要求
+
+ @Override
+ public List<TorrentEntity> search(TorrentParam param) {
+ return torrentMapper.search(param);
+ }
+
+ @Override
+ public TorrentEntity selectBySeedId(Long seedId) {
+ return torrentMapper.selectById(seedId);
+ }
+ private final ExecutorService seederExecutor = Executors.newCachedThreadPool();
+
+ @Override
+ @Transactional
+ public void uploadTorrent(MultipartFile file, TorrentUploadParam param) throws IOException {
+ // 验证用户权限
+ User user = userMapper.selectById(param.getUploader());
+ if (user == null) {
+ throw new RuntimeException("用户不存在");
+ }
+ String workingDir = System.getProperty("user.dir");
+ Path originalDir = Paths.get(workingDir, "data", "files");
+ Files.createDirectories(originalDir);
+ Path originalFilePath = originalDir.resolve(file.getOriginalFilename());
+ Files.copy(file.getInputStream(), originalFilePath, StandardCopyOption.REPLACE_EXISTING);
+
+ MetadataBuilder builder = new MetadataBuilder()
+ .addFile(originalFilePath.toFile(), file.getOriginalFilename()) // 添加原始文件
+ .setTracker(" ") // 设置Tracker地址
+ .setPieceLength(512 * 1024) // 分片大小512KB
+ .setComment("Generated by PT站")
+ .setCreatedBy("PT-Server");
+
+ // 处理种子文件
+ byte[] torrentBytes = file.getBytes();
+ String infoHash = null;
+ try {
+ infoHash = calculateInfoHash(torrentBytes);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+
+ // 保存种子文件到data/torrents目录
+ Path torrentDir = Paths.get(workingDir, "data", "torrents");
+ Files.createDirectories(torrentDir);
+ Path torrentPath = torrentDir.resolve(infoHash + ".torrent");
+ Files.copy(new ByteArrayInputStream(torrentBytes), torrentPath, StandardCopyOption.REPLACE_EXISTING);
+
+ // 注册到Tracker
+ TrackedTorrent torrent = TrackedTorrent.load(torrentPath.toFile());
+ tracker.announce(torrent);
+
+
+ // 异步启动做种客户端
+ seederExecutor.submit(() -> {
+ try {
+ startSeeding(torrentPath, originalDir);
+ } catch (Exception e) {
+ Result.error("做种失败: " + e.getMessage());
+ }
+ });
+
+
+
+
+ // 保存种子信息
+ TorrentEntity entity= new TorrentEntity();
+ entity.setUploader(param.getUploader());
+ entity.setFileName(file.getOriginalFilename());
+ entity.setSize(file.getSize());
+ entity.setCategory(param.getCategory());
+ entity.setTags(param.getTags());
+ entity.setTitle(param.getTitle());
+ entity.setImageUrl(param.getImageUrl());
+ entity.setTorrentFile(torrentBytes);
+ entity.setInfoHash(infoHash);
+
+ torrentMapper.insert(entity);
+ }
+
+ @Override
+ public byte[] fetch(Long seedId, String passkey) {
+ TorrentEntity torrent = selectBySeedId(seedId);
+ if (torrent == null) {
+ throw new RuntimeException("种子不存在");
+ }
+
+ byte[] torrentBytes = torrent.getTorrentFile();
+
+ try {
+ // 1. 解码 .torrent 文件为 Map
+ Map<String, BEValue> decoded = BDecoder.bdecode(new ByteArrayInputStream(torrentBytes)).getMap();
+
+ // 2. 获取原始 announce 字段
+ String announce = decoded.get("announce").getString();
+
+ // 3. 注入 passkey 到 announce URL
+ if (!announce.contains("passkey=")) {
+ announce = announce + "?passkey=" + passkey;
+ decoded.put("announce", new BEValue(announce));
+ }
+
+ // 4. 编码成新的 .torrent 文件字节数组
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ BEncoder.bencode(decoded, out);
+ return out.toByteArray();
+
+ } catch (Exception e) {
+ throw new RuntimeException("处理 torrent 文件失败", e);
+ }
+ }
+
+ @Override
+ @Transactional
+ public Result favorite(Long seedId, Long userId) {
+ try {
+ boolean exists = torrentMapper.checkFavorite(seedId, userId);
+ if (exists) {
+ torrentMapper.removeFavorite(seedId, userId);
+ return Result.success("取消收藏成功");
+ } else {
+ torrentMapper.addFavorite(seedId, userId);
+ return Result.success("收藏成功");
+ }
+ } catch (Exception e) {
+ return Result.error("失败: " + e.getMessage());
+ }
+ }
+
+ @Override
+ @Transactional
+ public void deleteTorrent(Long seedId) {
+ torrentMapper.deleteById(seedId);
+ }
+
+ @Override
+ @Transactional
+ public void updateTorrent(Long seedId, TorrentUpdateDTO updateDTO) {
+ TorrentEntity torrent = selectBySeedId(seedId);
+ if (torrent == null) {
+ throw new RuntimeException("种子不存在");
+ }
+
+
+ torrent.setDescription(updateDTO.getDescription());
+ torrent.setCategory(updateDTO.getCategory());
+ torrent.setTitle(updateDTO.getTitle());
+ torrent.setTags(updateDTO.getTags());
+ torrent.setImageUrl(updateDTO.getImageUrl());
+
+ torrentMapper.updateById(torrent);
+ }
+
+ @Override
+ public boolean canUserDeleteTorrent(Long seedId, Long userId) {
+ TorrentEntity torrent = selectBySeedId(seedId);
+ if (torrent == null) {
+ return false;
+ }
+
+ // 检查是否是种子发布者或管理员
+ return torrent.getUploader().equals(userId) ||
+ userMapper.hasRole(userId, "admin");
+ }
+
+ @Override
+ public boolean canUserUpdateTorrent(Long seedId, Long userId) {
+ return canUserDeleteTorrent(seedId, userId);
+ }
+
+ @Override
+ public boolean checkUserUploadRatio(Long userId) {
+ User user = userMapper.selectById(userId);
+ if (user == null) {
+ return false;
+ }
+
+ // 防止除以零
+ if (user.getDownloaded() == 0) {
+ return true;
+ }
+
+ double uploadRatio = user.getUploaded() / (double) user.getDownloaded();
+ return uploadRatio >= MIN_UPLOAD_RATIO;
+ }
+ /**
+ * 启动做种客户端
+ */
+ private void startSeeding(Path torrentPath, Path dataDir) throws Exception {
+ SimpleClient seederClient = new SimpleClient();
+ seederClient.downloadTorrent(
+ torrentPath.toString(),
+ dataDir.toString(),
+ InetAddress.getLocalHost());
+ // 保持做种状态(阻塞线程)
+ while (true) {
+ Thread.sleep(60000); // 每60秒检查一次
+ }
+ }
+
+
+ @Override
+ public double calculateDownloadSize(Long torrentId, Long userId) {
+ TorrentEntity torrent = selectBySeedId(torrentId);
+ if (torrent == null) {
+ throw new RuntimeException("种子不存在");
+ }
+
+ // 获取当前有效的促销活动
+ double discount = promotionService.getCurrentDiscount(torrentId);
+
+ // 计算实际下载量
+ return torrent.getSize() * (1 - discount / 100.0);
+ }
+
+ @Override
+ @Transactional
+ public void recordDownload(Long torrentId, Long userId, double downloadSize) {
+ // 更新用户下载量
+ userMapper.increaseDownloaded(userId, downloadSize);
+
+ // 更新种子下载次数
+ torrentMapper.increaseDownloads(torrentId);
+ }
+ /**
+ * 计算种子文件的infoHash
+ */
+ private String calculateInfoHash(byte[] torrentData) throws NoSuchAlgorithmException {
+ MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+ sha1.update(torrentData);
+ byte[] hashBytes = sha1.digest();
+ return Hex.encodeHexString(hashBytes);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/myproject/utils/Result.java b/src/main/java/com/example/myproject/utils/Result.java
deleted file mode 100644
index e838a1c..0000000
--- a/src/main/java/com/example/myproject/utils/Result.java
+++ /dev/null
@@ -1,129 +0,0 @@
-package com.example.myproject.utils;
-
-
-public class Result<T> {
- private String code;
- private String msg;
- private T data;
-
- public String getCode() {
- return code;
- }
-
- public void setCode(String code) {
- this.code = code;
- }
-
- public String getMsg() {
- return msg;
- }
-
- public void setMsg(String msg) {
- this.msg = msg;
- }
-
- public T getData() {
- return data;
- }
-
- public void setData(T data) {
- this.data = data;
- }
-
- public Result() {
- }
-
- public Result(T data) {
- this.data = data;
- }
-
- public static Result success() {
- Result result = new Result<>();
- result.setCode("200");
- result.setMsg("成功");
- return result;
- }
-
- public static <T> Result<T> success(T data) {
- Result<T> result = new Result<>(data);
- result.setCode("200");
- result.setMsg("成功");
- return result;
- }
- public static <T> Result<T> success(String msg) {
- Result result = new Result();
- result.setCode("200");
- result.setMsg("成功");
- return result;
- }
-
- public static <T> Result<T> success(T data,String msg) {
- Result<T> result = new Result<>(data);
- result.setCode("200");
- result.setMsg(msg);
- return result;
- }
-
- public static Result error(String code, String msg) {
- Result result = new Result();
- result.setCode(code);
- result.setMsg(msg);
- return result;
- }
- /**
- * 创建一个表示文件大小超出限制的结果对象。
- *
- * @return 构造的文件过大错误结果对象
- */
- public static Result fileTooLarge() {
- Result result = new Result();
- result.setCode("413");
- result.setMsg("文件大小超出限制。");
- return result;
- }
-
- /**
- * 创建一个表示文件格式不支持的结果对象。
- *
- * @return 构造的文件格式错误结果对象
- */
- public static Result unsupportedFileType() {
- Result result = new Result();
- result.setCode("415");
- result.setMsg("不支持的文件格式。");
- return result;
- }
-
- /**
- * 创建一个表示文件未找到的结果对象。
- *
- * @return 构造的文件未找到错误结果对象
- */
- public static Result fileNotFound() {
- Result result = new Result();
- result.setCode("404");
- result.setMsg("文件未找到。");
- return result;
- }
-
- /**
- * 创建一个表示文件存储错误的结果对象。
- *
- * @param errorMsg 详细错误信息
- * @return 构造的文件存储错误结果对象
- */
- public static Result fileStorageError(String errorMsg) {
- Result result = new Result();
- result.setCode("500");
- result.setMsg("文件存储错误: " + errorMsg);
- return result;
- }
-
- public static Result permissionDenied() {
- Result result = new Result();
- result.setCode("401");
- result.setMsg("权限不足,无法执行该操作。");
- return result;
- }
-
-}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index b753da7..0590333 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -19,3 +19,13 @@
spring.jpa.enabled=false
spring.jpa.hibernate.ddl-auto=none
spring.jpa.open-in-view=false
+
+# tracker??
+pt.tracker.port=6969
+
+pt.tracker.torrent-dir=${user.dir}/data/torrents
+
+pt.tracker.allow-foreign=false
+pt.tracker.announce-url=/custom-announce
+
+mybatis-plus.mapper-locations=classpath:/mapper/**/*.xml
diff --git a/src/main/resources/files/files.torrent b/src/main/resources/files/files.torrent
new file mode 100644
index 0000000..e04974f
--- /dev/null
+++ b/src/main/resources/files/files.torrent
@@ -0,0 +1 @@
+d8:announce22:https://tracker.byr.pt10:created by21:qBittorrent v4.5.3.1013:creation datei1747717901e4:infod5:filesld6:lengthi173e4:pathl13:valid.torrenteee4:name5:files12:piece lengthi16384e6:pieces20:/ñíèEô5ã<òûìÕQ¡ûee
\ No newline at end of file
diff --git a/src/main/resources/mapper/FavoriteMapper.xml b/src/main/resources/mapper/FavoriteMapper.xml
new file mode 100644
index 0000000..1048ec2
--- /dev/null
+++ b/src/main/resources/mapper/FavoriteMapper.xml
@@ -0,0 +1,6 @@
+<?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.example.myproject.mapper.FavoriteMapper">
+
+</mapper>
diff --git a/src/main/resources/mapper/PromotionMapper.xml b/src/main/resources/mapper/PromotionMapper.xml
new file mode 100644
index 0000000..72ffd95
--- /dev/null
+++ b/src/main/resources/mapper/PromotionMapper.xml
@@ -0,0 +1,14 @@
+<?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.example.myproject.mapper.PromotionMapper">
+
+ <insert id="insert" parameterType="com.example.myproject.entity.Promotion">
+ INSERT INTO promotion (
+ name, description, start_time, end_time, discount_percentage, applicable_torrent_ids
+ ) VALUES (
+ #{name}, #{description}, #{startTime}, #{endTime}, #{discountPercentage}, #{applicableTorrentIds}
+ )
+ </insert>
+
+</mapper>
diff --git a/src/main/resources/mapper/TorrentMapper.xml b/src/main/resources/mapper/TorrentMapper.xml
new file mode 100644
index 0000000..d5f018e
--- /dev/null
+++ b/src/main/resources/mapper/TorrentMapper.xml
@@ -0,0 +1,104 @@
+<?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.example.myproject.mapper.TorrentMapper">
+ <resultMap id="BaseResultMap" type="com.example.myproject.entity.TorrentEntity">
+ <id column="info_hash" property="infoHash"/>
+ <result column="file_name" property="fileName"/>
+ <result column="uploader" property="uploader"/>
+ <result column="upload_time" property="uploadTime"/>
+ <result column="size" property="size"/>
+ <result column="title" property="title"/>
+ <result column="description" property="description"/>
+ <result column="category" property="category"/>
+ <result column="image_url" property="imageUrl"/>
+ </resultMap>
+
+ <select id="selectByInfoHash" resultMap="BaseResultMap">
+ SELECT * FROM torrent WHERE info_hash = #{infoHash}
+ </select>
+ <select id="selectBySeedId" resultMap="BaseResultMap">
+ SELECT * FROM torrent WHERE seed_id = #{seedId}
+ </select>
+
+
+
+
+ <update id="update" parameterType="com.example.myproject.entity.TorrentEntity">
+ UPDATE torrent
+ SET file_name = #{fileName},
+ uploader = #{uploader},
+ upload_time = #{uploadTime},
+ size = #{size},
+ title = #{title},
+ description = #{description},
+ category = #{category},
+ image_url = #{imageUrl}
+ WHERE info_hash = #{infoHash}
+ </update>
+ <select id="search" resultType="com.example.myproject.entity.TorrentEntity">
+ SELECT * FROM torrent
+ <where>
+ <if test="param.category != null">
+ AND category = #{param.category}
+ </if>
+
+ <!-- <if test="param.free != null and param.free != ''">-->
+ <!-- AND free = #{param.free}-->
+ <!-- </if>-->
+ <if test="param.free != null">
+ <choose>
+ <!-- 筛选“正在促销中”的种子 -->
+ <when test="param.free == true">
+ AND EXISTS (
+ SELECT 1 FROM promotion p
+ WHERE
+ JSON_CONTAINS(p.applicable_torrent_ids, JSON_ARRAY(t.id))
+ AND NOW() BETWEEN p.start_time AND p.end_time
+ AND p.is_deleted = 0
+ )
+ </when>
+ <!-- 筛选“未在促销中”的种子 -->
+ <otherwise>
+ AND NOT EXISTS (
+ SELECT 1 FROM promotion p
+ WHERE
+ JSON_CONTAINS(p.applicable_torrent_ids, JSON_ARRAY(t.id))
+ AND NOW() BETWEEN p.start_time AND p.end_time
+ AND p.is_deleted = 0
+ )
+ </otherwise>
+ </choose>
+ </if>
+
+ <if test="param.likeExpressions != null and param.likeExpressions.size > 0">
+ AND (
+ <foreach collection="param.likeExpressions" item="item" open="(" separator=" AND " close=")">
+
+ ( title LIKE CONCAT('%', #{item}, '%') ) or ( description LIKE CONCAT('%', #{item}, '%') ) or ( tags LIKE CONCAT('%', #{item}, '%') )
+ </foreach>
+ )
+ </if>
+ </where>
+
+ <if test="param.prop != null and param.sort != null">
+ ORDER BY ${param.prop} ${param.sort}
+ </if>
+ </select>
+ <select id="checkFavorite" resultType="boolean">
+ SELECT COUNT(*) > 0
+ FROM favorite
+ WHERE seed_id = #{seedId} AND user_id = #{userId}
+ </select>
+ <insert id="addFavorite">
+ INSERT INTO favorite (seed_id, user_id)
+ VALUES (#{seedId}, #{userId})
+ </insert>
+ <delete id="removeFavorite">
+ DELETE FROM favorite
+ WHERE seed_id = #{seedId} AND user_id = #{userId}
+ </delete>
+
+
+</mapper>
\ No newline at end of file
diff --git a/src/main/resources/output/valid.torrent b/src/main/resources/output/valid.torrent
new file mode 100644
index 0000000..6a90e52
--- /dev/null
+++ b/src/main/resources/output/valid.torrent
@@ -0,0 +1 @@
+d10:created by18:qBittorrent v5.1.013:creation datei1745948995e4:infod6:lengthi22e4:name15:example.torrent12:piece lengthi16384e6:pieces20:Fnð¶)ú<Ç æÂh£tl7:privatei1eee
\ No newline at end of file
diff --git a/src/test/java/com/example/myproject/controller/TorrentControllerTest.java b/src/test/java/com/example/myproject/controller/TorrentControllerTest.java
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/test/java/com/example/myproject/controller/TorrentControllerTest.java
diff --git a/src/test/java/com/example/myproject/controller/UserControllerTest.java b/src/test/java/com/example/myproject/controller/UserControllerTest.java
index 8a09e53..d332f31 100644
--- a/src/test/java/com/example/myproject/controller/UserControllerTest.java
+++ b/src/test/java/com/example/myproject/controller/UserControllerTest.java
@@ -6,13 +6,12 @@
import com.example.myproject.service.UserService;
import com.example.myproject.mapper.UserMapper;
import com.example.myproject.mapper.VerificationTokenMapper;
-import com.example.myproject.utils.Result;
+import com.example.myproject.common.base.Result;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.*;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;