完整后端
Change-Id: I7a2b336225df8d477741a47c0dd2d2668daebed2
diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
new file mode 100644
index 0000000..f5e6591
--- /dev/null
+++ b/.github/workflows/gradle.yml
@@ -0,0 +1,34 @@
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle
+
+name: Java CI with Gradle
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ - name: Build with Gradle
+ uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
+ with:
+ arguments: build
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8a57676
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,48 @@
+HELP.md
+.gradle
+build/
+!gradle/wrapper/gradle-wrapper.jar
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+out/
+!**/src/main/**/out/
+!**/src/test/**/out/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
+
+/public/torrents
+/config
+/src/main/resources/application-db-tencentcloud.yml
+/src/main/resources/application-db-vultr.yml
+/public/
+
+application*.yml
+application.yml
+application-caching.yml
+application-test.yml
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..726ba8b
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,209 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-parent</artifactId>
+ <version>3.0.2</version>
+ <relativePath/> <!-- lookup parent from repository -->
+ </parent>
+ <groupId>com.github.bitsapling</groupId>
+ <artifactId>Sapling</artifactId>
+ <version>0.0.1-SNAPSHOT</version>
+ <name>Sapling</name>
+ <description>Sapling project</description>
+
+ <properties>
+ <java.version>17</java.version>
+ <maven.compiler.source>17</maven.compiler.source>
+ <maven.compiler.target>17</maven.compiler.target>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-cache</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-data-redis</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-data-jpa</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-mail</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-quartz</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-validation</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>cn.dev33</groupId>
+ <artifactId>sa-token-spring-boot3-starter</artifactId>
+ <version>1.34.0</version>
+ </dependency>
+ <dependency>
+ <groupId>cn.dev33</groupId>
+ <artifactId>sa-token-dao-redis-jackson</artifactId>
+ <version>1.34.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-json</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-tomcat</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <version>2.11.0</version>
+ </dependency>
+ <dependency>
+ <groupId>commons-codec</groupId>
+ <artifactId>commons-codec</artifactId>
+ <version>1.15</version>
+ </dependency>
+ <dependency>
+ <groupId>commons-validator</groupId>
+ <artifactId>commons-validator</artifactId>
+ <version>1.7</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ <version>3.12.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-text</artifactId>
+ <version>1.10.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-compress</artifactId>
+ <version>1.22</version>
+ </dependency>
+ <dependency>
+ <groupId>org.jetbrains</groupId>
+ <artifactId>annotations</artifactId>
+ <version>23.1.0</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <version>31.1-jre</version>
+ </dependency>
+ <dependency>
+ <groupId>at.favre.lib</groupId>
+ <artifactId>bcrypt</artifactId>
+ <version>0.9.0</version>
+ </dependency>
+ <dependency>
+ <groupId>com.konghq</groupId>
+ <artifactId>unirest-java</artifactId>
+ <version>3.14.1</version>
+ </dependency>
+ <dependency>
+ <groupId>me.tongfei</groupId>
+ <artifactId>progressbar</artifactId>
+ <version>0.9.5</version>
+ </dependency>
+ <dependency>
+ <groupId>com.dampcake</groupId>
+ <artifactId>bencode</artifactId>
+ <version>1.4</version>
+ </dependency>
+ <dependency>
+ <groupId>com.mysql</groupId>
+ <artifactId>mysql-connector-j</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.h2database</groupId>
+ <artifactId>h2</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-devtools</artifactId>
+ <scope>development</scope>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.datatype</groupId>
+ <artifactId>jackson-datatype-jsr310</artifactId>
+ <version>2.14.1</version>
+ </dependency>
+ <dependency>
+ <groupId>org.redisson</groupId>
+ <artifactId>redisson-hibernate-6</artifactId>
+ <version>3.19.3</version>
+ </dependency>
+ <dependency>
+ <groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
+ <artifactId>owasp-java-html-sanitizer</artifactId>
+ <version>20220608.1</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-pool2</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.rometools</groupId>
+ <artifactId>rome</artifactId>
+ <version>1.18.0</version>
+ </dependency>
+ <dependency>
+ <groupId>com.alicp.jetcache</groupId>
+ <artifactId>jetcache-starter-redis</artifactId>
+ <version>2.7.3</version>
+ </dependency>
+ <dependency>
+ <groupId>org.greenrobot</groupId>
+ <artifactId>eventbus-java</artifactId>
+ <version>3.3.1</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-maven-plugin</artifactId>
+ <configuration>
+ <excludes>
+ <exclude>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ </exclude>
+ </excludes>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
\ No newline at end of file
diff --git a/src/main/java/com/github/example/pt/autoconfig/JacksonConfig.java b/src/main/java/com/github/example/pt/autoconfig/JacksonConfig.java
new file mode 100644
index 0000000..51562bb
--- /dev/null
+++ b/src/main/java/com/github/example/pt/autoconfig/JacksonConfig.java
@@ -0,0 +1,22 @@
+package com.github.example.pt.autoconfig;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+@Configuration
+public class JacksonConfig {
+ @Bean
+ @Primary
+ public ObjectMapper primaryObjectMapper() {
+ return JsonMapper.builder()
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+ .findAndAddModules()
+ .addModule(new JavaTimeModule())
+ .build();
+ }
+}
diff --git a/src/main/java/com/github/example/pt/autoconfig/JetcacheConfig.java b/src/main/java/com/github/example/pt/autoconfig/JetcacheConfig.java
new file mode 100644
index 0000000..e614f29
--- /dev/null
+++ b/src/main/java/com/github/example/pt/autoconfig/JetcacheConfig.java
@@ -0,0 +1,10 @@
+package com.github.example.pt.autoconfig;
+
+import com.alicp.jetcache.anno.config.EnableCreateCacheAnnotation;
+import com.alicp.jetcache.anno.config.EnableMethodCache;
+
+@EnableMethodCache(basePackages = "com.github.bitsapling.sapling")
+@EnableCreateCacheAnnotation
+public class JetcacheConfig {
+
+}
diff --git a/src/main/java/com/github/example/pt/autoconfig/QuartzConfig.java b/src/main/java/com/github/example/pt/autoconfig/QuartzConfig.java
new file mode 100644
index 0000000..d355496
--- /dev/null
+++ b/src/main/java/com/github/example/pt/autoconfig/QuartzConfig.java
@@ -0,0 +1,59 @@
+package com.github.example.pt.autoconfig;
+
+import com.github.example.pt.crontask.PeersCleanup;
+import org.jetbrains.annotations.NotNull;
+import org.quartz.*;
+import org.quartz.spi.JobFactory;
+import org.quartz.spi.TriggerFiredBundle;
+import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.quartz.SpringBeanJobFactory;
+
+@Configuration
+@EnableScheduling
+public class QuartzConfig {
+ @Bean
+ public JobDetail jobDetail() {
+ return JobBuilder.newJob(PeersCleanup.class)
+ .withIdentity("peers_cleanup")
+ .withDescription("Peers Cleanup")
+ .storeDurably()
+ .build();
+ }
+
+ @Bean
+ public Trigger trigger() {
+ return TriggerBuilder.newTrigger()
+ .forJob(jobDetail())
+ .withSchedule(SimpleScheduleBuilder.repeatMinutelyForever(30))
+ .startNow()
+ .build();
+ }
+
+ @Bean
+ public JobFactory jobFactory(ApplicationContext applicationContext) {
+ AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
+ jobFactory.setApplicationContext(applicationContext);
+ return jobFactory;
+ }
+
+ public static class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {
+ private transient AutowireCapableBeanFactory beanFactory;
+
+ @Override
+ public void setApplicationContext(final ApplicationContext context) {
+ beanFactory = context.getAutowireCapableBeanFactory();
+ }
+
+ @Override
+ protected @NotNull Object createJobInstance(@NotNull TriggerFiredBundle bundle) throws Exception {
+ Object job = super.createJobInstance(bundle);
+ beanFactory.autowireBean(job);
+ return job;
+ }
+ }
+}
diff --git a/src/main/java/com/github/example/pt/autoconfig/RedisConfig.java b/src/main/java/com/github/example/pt/autoconfig/RedisConfig.java
new file mode 100644
index 0000000..1f11e87
--- /dev/null
+++ b/src/main/java/com/github/example/pt/autoconfig/RedisConfig.java
@@ -0,0 +1,72 @@
+package com.github.example.pt.autoconfig;
+
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.cache.interceptor.KeyGenerator;
+import org.springframework.cache.interceptor.SimpleKeyGenerator;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+import java.time.Duration;
+
+@Configuration
+@EnableCaching
+@EnableRedisRepositories
+public class RedisConfig {
+
+ @Bean
+ public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
+ RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
+ redisCacheConfiguration = redisCacheConfiguration
+ .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer.UTF_8))
+ .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer()))
+ .prefixCacheNameWith("sapling:")
+ .entryTtl(Duration.ofMinutes(3L));
+ return RedisCacheManager
+ .builder(redisConnectionFactory)
+ .cacheDefaults(redisCacheConfiguration)
+ //.withInitialCacheConfigurations(map)
+ .build();
+ }
+
+ @Bean
+ public RedisConnectionFactory connectionFactory() {
+ return new LettuceConnectionFactory();
+ }
+
+ @Bean
+ public KeyGenerator keyGenerator() {
+ return (target, method, params) -> {
+ StringBuilder sb = new StringBuilder();
+ sb.append(target.getClass().getName()).append(":");
+ sb.append(method.getName()).append(":");
+ Object key = SimpleKeyGenerator.generateKey(params);
+ return sb.append(key);
+ };
+ }
+
+ @Bean
+ public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
+ GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = serializer();
+ RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
+ redisTemplate.setKeySerializer(StringRedisSerializer.UTF_8);
+ redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
+ redisTemplate.setHashKeySerializer(StringRedisSerializer.UTF_8);
+ redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
+ redisTemplate.setConnectionFactory(redisConnectionFactory);
+ return redisTemplate;
+ }
+
+ public GenericJackson2JsonRedisSerializer serializer() {
+ return new GenericJackson2JsonRedisSerializer();
+ }
+}
diff --git a/src/main/java/com/github/example/pt/autoconfig/SaTokenConfig.java b/src/main/java/com/github/example/pt/autoconfig/SaTokenConfig.java
new file mode 100644
index 0000000..7259e1e
--- /dev/null
+++ b/src/main/java/com/github/example/pt/autoconfig/SaTokenConfig.java
@@ -0,0 +1,17 @@
+package com.github.example.pt.autoconfig;
+
+import cn.dev33.satoken.interceptor.SaInterceptor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+@Configuration
+public class SaTokenConfig implements WebMvcConfigurer {
+ @Override
+ public void addInterceptors(InterceptorRegistry registry) {
+ // 注册 Sa-Token 拦截器,打开注解式鉴权功能
+ registry.addInterceptor(new SaInterceptor())
+ .addPathPatterns("/**")
+ .excludePathPatterns("/auth/**")
+ .excludePathPatterns("/torrent/download/**");
+ }
+}
diff --git a/src/main/java/com/github/example/pt/autoconfig/SafeHTMLConfig.java b/src/main/java/com/github/example/pt/autoconfig/SafeHTMLConfig.java
new file mode 100644
index 0000000..3ef8fc9
--- /dev/null
+++ b/src/main/java/com/github/example/pt/autoconfig/SafeHTMLConfig.java
@@ -0,0 +1,19 @@
+package com.github.example.pt.autoconfig;
+
+import org.owasp.html.PolicyFactory;
+import org.owasp.html.Sanitizers;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class SafeHTMLConfig {
+ @Bean
+ public PolicyFactory safeHTML(){
+ return Sanitizers.LINKS
+ .and(Sanitizers.TABLES)
+ .and(Sanitizers.IMAGES)
+ .and(Sanitizers.BLOCKS)
+ .and(Sanitizers.FORMATTING)
+ .and(Sanitizers.STYLES);
+ }
+}
diff --git a/src/main/java/com/github/example/pt/autoconfig/WorkDirectoryConfig.java b/src/main/java/com/github/example/pt/autoconfig/WorkDirectoryConfig.java
new file mode 100644
index 0000000..a190e19
--- /dev/null
+++ b/src/main/java/com/github/example/pt/autoconfig/WorkDirectoryConfig.java
@@ -0,0 +1,27 @@
+package com.github.example.pt.autoconfig;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.io.File;
+
+@Configuration
+public class WorkDirectoryConfig {
+ @Bean(name = "publicDirectory")
+ public File publicDirectory() {
+ File file = new File("public");
+ if (!file.exists()) {
+ file.mkdirs();
+ }
+ return file;
+ }
+
+ @Bean(name = "torrentsDirectory")
+ public File torrentsDirectory() {
+ File file = new File(publicDirectory(), "torrents");
+ if (!file.exists()) {
+ file.mkdirs();
+ }
+ return file;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/config/ApiPrinter.java b/src/main/java/com/github/example/pt/config/ApiPrinter.java
new file mode 100644
index 0000000..6019575
--- /dev/null
+++ b/src/main/java/com/github/example/pt/config/ApiPrinter.java
@@ -0,0 +1,24 @@
+package com.github.example.pt.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.ApplicationListener;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+
+@Component
+public class ApiPrinter implements ApplicationListener<ApplicationReadyEvent> {
+
+ @Autowired
+ private RequestMappingHandlerMapping handlerMapping;
+
+ @Override
+ public void onApplicationEvent(ApplicationReadyEvent event) {
+ System.out.println("=== 项目启动,已注册的 API 接口如下: ===");
+ handlerMapping.getHandlerMethods().forEach((mapping, method) -> {
+ System.out.println("接口路径: " + mapping + " -> 方法: " + method.getMethod().getDeclaringClass().getSimpleName()
+ + "." + method.getMethod().getName());
+ });
+ System.out.println("========================================");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/github/example/pt/config/MailConfig.java b/src/main/java/com/github/example/pt/config/MailConfig.java
new file mode 100644
index 0000000..a95276c
--- /dev/null
+++ b/src/main/java/com/github/example/pt/config/MailConfig.java
@@ -0,0 +1,25 @@
+package com.github.example.pt.config;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+@Data
+@AllArgsConstructor
+public class MailConfig {
+ private String host;
+ private int port;
+ private boolean login;
+ private String username;
+ private String password;
+ private String sender;
+ private boolean emailSuffixWhitelistMode;
+ private List<String> emailSuffix;
+ private String smtpEncryption;
+
+ @NotNull
+ public static String getConfigKey(){
+ return "mail";
+ }
+}
diff --git a/src/main/java/com/github/example/pt/config/SecurityConfig.java b/src/main/java/com/github/example/pt/config/SecurityConfig.java
new file mode 100644
index 0000000..dcb896f
--- /dev/null
+++ b/src/main/java/com/github/example/pt/config/SecurityConfig.java
@@ -0,0 +1,34 @@
+package com.github.example.pt.config;
+
+import com.github.example.pt.type.GuestAccessBlocker;
+import com.github.example.pt.type.GuestAccessRequirement;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collections;
+import java.util.List;
+@Data
+@AllArgsConstructor
+public class SecurityConfig {
+ private int maxIp;
+ private int maxAuthenticationAttempts;
+ private int maxPasskeyAuthenticationAttempts;
+ private GuestAccessBlocker guestAccessBlocker;
+ private boolean guestAccessRequirementAnyMode;
+ private List<GuestAccessRequirement> guestAccessRequirement;
+ private List<String> guestAccessSecret;
+ private List<String> guestAccessReferer;
+ private List<String> guestAccessUserAgentKeyword;
+ private List<String> guestAccessIp;
+
+ @NotNull
+ public static String getConfigKey(){
+ return "security";
+ }
+ @NotNull
+ public static SecurityConfig spawnDefault(){
+ return new SecurityConfig(10, 5,150, GuestAccessBlocker.NORMAL, false, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
+ }
+
+}
diff --git a/src/main/java/com/github/example/pt/config/SiteBasicConfig.java b/src/main/java/com/github/example/pt/config/SiteBasicConfig.java
new file mode 100644
index 0000000..ceb82c5
--- /dev/null
+++ b/src/main/java/com/github/example/pt/config/SiteBasicConfig.java
@@ -0,0 +1,49 @@
+package com.github.example.pt.config;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.jetbrains.annotations.NotNull;
+
+import java.net.InetAddress;
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+public class SiteBasicConfig {
+ private String siteName;
+ private String siteSubName;
+ private String siteBaseURL;
+ private String siteDescription;
+ private List<String> siteKeywords;
+ private boolean openRegistration;
+ private boolean maintenanceMode;
+
+ @NotNull
+ public static String getConfigKey() {
+ return "site_basic";
+ }
+
+ @NotNull
+ public static SiteBasicConfig spawnDefault() {
+ String localIp = getLocalIp(); // 获取本机IP地址
+ return new SiteBasicConfig(
+ "Another Sapling Site",
+ "未配置的站点",
+ "http://" + localIp + ":8081", // 动态生成 siteBaseURL
+ "又一个由 Sapling 驱动的站点!",
+ List.of("BitTorrent", "Torrent", "File Sharing", "Private Tracker"),
+ false,
+ false
+ );
+ }
+
+ private static String getLocalIp() {
+ try {
+ InetAddress inetAddress = InetAddress.getLocalHost();
+ return inetAddress.getHostAddress(); // 获取本机的IP地址
+ } catch (Exception e) {
+ e.printStackTrace();
+ return "127.0.0.1"; // 如果无法获取本机IP,返回默认值
+ }
+ }
+}
diff --git a/src/main/java/com/github/example/pt/config/TrackerConfig.java b/src/main/java/com/github/example/pt/config/TrackerConfig.java
new file mode 100644
index 0000000..61855b3
--- /dev/null
+++ b/src/main/java/com/github/example/pt/config/TrackerConfig.java
@@ -0,0 +1,53 @@
+package com.github.example.pt.config;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.jetbrains.annotations.NotNull;
+
+import java.net.InetAddress;
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+public class TrackerConfig {
+ private List<String> trackerURL;
+ private long maxTorrentSize;
+ private int torrentIntervalMin;
+ private int torrentIntervalMax;
+ private boolean ipAddressWhitelistMode;
+ private List<String> controlIps;
+ private boolean portWhiteListMode;
+ private List<Integer> controlPorts;
+ private String torrentPrefix;
+
+ @NotNull
+ public static String getConfigKey() {
+ return "tracker";
+ }
+
+ @NotNull
+ public static TrackerConfig spawnDefault() {
+ String localIp = getLocalIp(); // 获取本机IP地址
+ return new TrackerConfig(
+ List.of("http://" + localIp + ":8081/api/announce"), // 动态生成 URL
+ -1,
+ 60 * 60 * 15,
+ 60 * 60 * 45,
+ false,
+ List.of(),
+ false,
+ List.of(20, 21, 22, 23, 25, 80, 110, 119, 161, 162, 443, 445, 1433, 1521, 2049, 3306, 3389, 8080, 8081),
+ "Sapling"
+ );
+ }
+
+ private static String getLocalIp() {
+ try {
+ InetAddress inetAddress = InetAddress.getLocalHost();
+ return inetAddress.getHostAddress(); // 获取本机的IP地址
+ } catch (Exception e) {
+ e.printStackTrace();
+ return "127.0.0.1"; // 如果无法获取本机IP,返回默认值
+ }
+ }
+}
diff --git a/src/main/java/com/github/example/pt/controller/DebugController.java b/src/main/java/com/github/example/pt/controller/DebugController.java
new file mode 100644
index 0000000..8ef53fd
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/DebugController.java
@@ -0,0 +1,268 @@
+package com.github.example.pt.controller;
+
+import com.github.example.pt.entity.Category;
+import com.github.example.pt.entity.Peer;
+import com.github.example.pt.entity.Permission;
+import com.github.example.pt.entity.PromotionPolicy;
+import com.github.example.pt.entity.Torrent;
+import com.github.example.pt.entity.User;
+import com.github.example.pt.entity.UserGroup;
+import com.github.example.pt.exception.TorrentException;
+import com.github.example.pt.repository.CategoryRepository;
+import com.github.example.pt.repository.PeersRepository;
+import com.github.example.pt.repository.TorrentRepository;
+import com.github.example.pt.service.AnnouncePerformanceMonitorService;
+import com.github.example.pt.service.CategoryService;
+import com.github.example.pt.service.PermissionService;
+import com.github.example.pt.service.PromotionService;
+import com.github.example.pt.service.UserGroupService;
+import com.github.example.pt.service.UserService;
+import com.github.example.pt.type.PrivacyLevel;
+import com.github.example.pt.util.TorrentParser;
+import lombok.Getter;
+import lombok.ToString;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.io.File;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.nio.file.Files;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/debug")
+@Slf4j
+public class DebugController {
+ @Autowired
+ private PermissionService permissionService;
+ @Autowired
+ private PromotionService promotionService;
+ @Autowired
+ private UserGroupService userGroupService;
+ @Autowired
+ private UserService userService;
+ @Autowired
+ private PeersRepository peersRepository;
+ @Autowired
+ private TorrentRepository torrentRepository;
+ @Autowired
+ private CategoryRepository categoryRepository;
+ @Autowired
+ private CategoryService categoryService;
+ @Autowired
+ private AnnouncePerformanceMonitorService announcePerformanceMonitorService;
+
+ @GetMapping("/")
+ public String torrents() throws IOException {
+ String page = Files.readString(new File("landing-debug.html").toPath());
+ long startTime = System.currentTimeMillis();
+ StringJoiner peersJoiner = new StringJoiner("\n\n");
+ StringJoiner torrentsJoiner = new StringJoiner("\n\n");
+ int debugPeers = 0;
+ int debugTorrents = 0;
+ long dbTimeStart = System.currentTimeMillis();
+ for (Torrent entity : torrentRepository.findAll()) {
+ debugTorrents++;
+ torrentsJoiner.add(new DebugTorrent(entity).toString());
+ }
+ for (Peer peer : peersRepository.findAll()) {
+ debugPeers++;
+ peersJoiner.add(new DebugPeer(peer).toString());
+ }
+ long dbTimeEnd = System.currentTimeMillis() - dbTimeStart;
+ String resp = page.replace("%%torrents_amount%%", String.valueOf(debugTorrents));
+ resp = resp.replace("%%peers_amount%%", String.valueOf(debugPeers));
+ resp = resp.replace("%%announce_reqs%%", String.valueOf(announcePerformanceMonitorService.getAnnounceTimes().size()));
+ resp = resp.replace("%%announce_ms%%", String.valueOf(announcePerformanceMonitorService.avgMs()));
+ resp = resp.replace("%%startup_date%%", announcePerformanceMonitorService.getStartTime().toString());
+ resp = resp.replace("%%announce_count%%", String.valueOf(announcePerformanceMonitorService.getHandled()));
+ resp = resp.replace("%%peers_list%%", peersJoiner.toString());
+ resp = resp.replace("%%torrents_list%%", torrentsJoiner.toString());
+ resp = resp.replace("%%debug_page_db_consumed%%", String.valueOf(dbTimeEnd));
+ resp = resp.replace("%%debug_page_consumed%%", String.valueOf(System.currentTimeMillis() - startTime));
+ resp = resp.replace("%%announce_job_avg%%", String.valueOf(announcePerformanceMonitorService.avgJobMs()));
+ return resp;
+ }
+
+ @GetMapping("/parseTorrents")
+ public String parseTorrent() throws IOException, TorrentException {
+ StringJoiner joiner = new StringJoiner("\n");
+ TorrentParser parser = new TorrentParser(new File("1.torrent"), true);
+ joiner.add("File Size: " + parser.getTorrentFilesSize());
+ joiner.add("Pieces Length: " + parser.getTorrentFilesSize());
+ Map<String, Long> fileList = parser.getFileList();
+ fileList.forEach((key, value) -> joiner.add("File -> " + key + ", Size -> " + value));
+ return joiner.toString();
+ }
+
+ @GetMapping("/initTables")
+ @Transactional
+ public String initTables() {
+ try {
+ List<Permission> permissions = new ArrayList<>();
+ permissions.add(new Permission(0, "torrent:announce", true));
+ permissions.add(new Permission(0, "torrent:upload", false));
+ permissions.add(new Permission(0, "torrent:scrape", true));
+ permissions.add(new Permission(0, "torrent:list", false));
+ permissions.add(new Permission(0, "torrent:view", false));
+ permissions.add(new Permission(0, "torrent:download", false));
+ permissions.add(new Permission(0, "torrent:download_review", false));
+ permissions.add(new Permission(0, "torrent:search", false));
+ permissions.add(new Permission(0, "torrent:thanks", false));
+ permissions.add(new Permission(0, "torrent:publish_anonymous", false));
+ permissions.add(new Permission(0, "torrent:bypass_review", false));
+ permissions.add(new Permission(0, "promotion:list", false));
+ permissions.add(new Permission(0, "category:list", false));
+ permissions.add(new Permission(0, "feed:subscribe", false));
+
+ permissions = permissions.stream().map(p -> permissionService.save(p)).toList();
+ PromotionPolicy promotionPolicy = promotionService.save(new PromotionPolicy(0, "normal", "无促销", 1.0d, 1.0d));
+ UserGroup userGroup = userGroupService.save(new UserGroup(0, "default", "Lv.1 青铜", permissions, promotionPolicy));
+ User user = userService.save(new User(0,
+ "test@test.com",
+ "$2a$06$r6QixzXG/Y8mUtmCV7b70.Jp7qjOL2nONUJolzGmQPzVn2acoKLf6",
+ "TestUser1",
+ userGroup,
+ new UUID(0, 0).toString(),
+ Timestamp.from(Instant.now()),
+ "https://www.baidu.com/favicon.ico",
+ "这是自定义头衔",
+ "这是测试签名",
+ "zh-CN",
+ "1000mbps",
+ "1000mbps",
+ 0,
+ 0,
+ 0,
+ 0,
+ "中国移不动",
+ BigDecimal.ZERO,
+ 0,
+ 0,
+ new UUID(1, 0).toString().replace("_", ""),
+ PrivacyLevel.LOW));
+ log.info("创建测试用户 1 成功");
+ User user2 = userService.save(new User(0,
+ "test2@test.com",
+ "$2a$06$r6QixzXG/Y8mUtmCV7b70.Jp7qjOL2nONUJolzGmQPzVn2acoKLf6",
+ "TestUser2",
+ userGroup,
+ new UUID(0, 1).toString(),
+ Timestamp.from(Instant.now()),
+ "https://weibo.com/favicon.ico",
+ "这是自定义头衔2",
+ "这是测试签名2",
+ "en-US",
+ "10000mbps",
+ "10000mbps",
+ 0,
+ 0,
+ 0,
+ 0,
+ "中国联不通",
+ BigDecimal.ZERO,
+ 5,
+ 0,
+ new UUID(2, 0).toString().replace("_", ""),
+ PrivacyLevel.LOW));
+ log.info("创建测试用户 2 成功");
+ categoryService.save(new Category(0, "test-category", "这是一个测试分类", TODO_CATEGORY_IMG));
+ categoryService.save(new Category(0, "movie-sd", "电影/标清", TODO_CATEGORY_IMG));
+ categoryService.save(new Category(0, "movie-hd", "电影/高清", TODO_CATEGORY_IMG));
+ categoryService.save(new Category(0, "movie-dvd", "电影/DVDISO", TODO_CATEGORY_IMG));
+ categoryService.save(new Category(0, "movie-bluray", "电影/Blu-Ray", TODO_CATEGORY_IMG));
+ categoryService.save(new Category(0, "movie-remux", "电影/Remux", TODO_CATEGORY_IMG));
+ categoryService.save(new Category(0, "tv-sd", "影视剧/标清", TODO_CATEGORY_IMG));
+ categoryService.save(new Category(0, "tv-hd", "影视剧/高清", TODO_CATEGORY_IMG));
+ categoryService.save(new Category(0, "tv-dvd", "影视剧/DVDISO", TODO_CATEGORY_IMG));
+ categoryService.save(new Category(0, "tv-bluray", "影视剧/Blu-Ray", TODO_CATEGORY_IMG));
+ categoryService.save(new Category(0, "documentary-edu", "纪录片/教育", TODO_CATEGORY_IMG));
+ categoryService.save(new Category(0, "anime", "动画", TODO_CATEGORY_IMG));
+ categoryService.save(new Category(0, "sports", "体育", TODO_CATEGORY_IMG));
+ categoryService.save(new Category(0, "software", "软件", TODO_CATEGORY_IMG));
+ categoryService.save(new Category(0, "game", "游戏", TODO_CATEGORY_IMG));
+ categoryService.save(new Category(0, "ebook", "电子书", TODO_CATEGORY_IMG));
+ categoryService.save(new Category(0, "other", "其它", TODO_CATEGORY_IMG));
+ return "初始化基本数据库测试内容成功";
+ } catch (Exception e) {
+ log.error("Error: ", e);
+ throw e;
+ }
+ }
+
+ @Getter
+ @ToString
+ static class DebugTorrent {
+ private final long id;
+ private final String infoHash;
+ private final String title;
+ private final String subTitle;
+ private final long size;
+ private final Timestamp createdAt;
+ private final Timestamp updatedAt;
+ private final boolean underReview;
+ private final boolean anonymous;
+ private final Category category;
+ private final PromotionPolicy promotionPolicy;
+ private final String description;
+
+ public DebugTorrent(Torrent torrent) {
+ this.id = torrent.getId();
+ this.infoHash = torrent.getInfoHash();
+ this.title = torrent.getTitle();
+ this.subTitle = torrent.getSubTitle();
+ this.size = torrent.getSize();
+ this.createdAt = torrent.getCreatedAt();
+ this.updatedAt = torrent.getUpdatedAt();
+ this.underReview = torrent.isUnderReview();
+ this.anonymous = torrent.isAnonymous();
+ this.category = torrent.getCategory();
+ this.promotionPolicy = torrent.getPromotionPolicy();
+ this.description = torrent.getDescription();
+
+ }
+ }
+
+ @Getter
+ @ToString
+ static class DebugPeer {
+ private final long id;
+ private final String ip;
+ private final int port;
+ private final String infoHash;
+ private final String userAgent;
+ private final long uploaded;
+ private final long downloaded;
+ private final long left;
+ private final boolean seeder;
+ private final Timestamp updateAt;
+ private final long seedingTime;
+
+ public DebugPeer(Peer peer) {
+ this.id = peer.getId();
+ this.ip = peer.getIp();
+ this.port = peer.getPort();
+ this.infoHash = peer.getInfoHash();
+ this.userAgent = peer.getUserAgent();
+ this.uploaded = peer.getUploaded();
+ this.downloaded = peer.getDownloaded();
+ this.left = peer.getLeft();
+ this.seeder = peer.isSeeder();
+ this.updateAt = peer.getUpdateAt();
+ this.seedingTime = peer.getSeedingTime();
+ }
+ }
+
+ private static final String TODO_CATEGORY_IMG = "https://img1.imgtp.com/2023/02/13/M09Eg8vR.jpg";
+}
diff --git a/src/main/java/com/github/example/pt/controller/advice/GlobalControllerAdvice.java b/src/main/java/com/github/example/pt/controller/advice/GlobalControllerAdvice.java
new file mode 100644
index 0000000..da96b9f
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/advice/GlobalControllerAdvice.java
@@ -0,0 +1,126 @@
+package com.github.example.pt.controller.advice;
+
+import cn.dev33.satoken.exception.NotLoginException;
+import cn.dev33.satoken.exception.NotPermissionException;
+import com.github.example.pt.exception.APIGenericException;
+import com.github.example.pt.exception.FixedAnnounceException;
+import com.github.example.pt.exception.RetryableAnnounceException;
+import com.github.example.pt.util.BencodeUtil;
+import com.github.example.pt.util.ClassUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.multipart.MaxUploadSizeExceededException;
+import org.springframework.web.multipart.MultipartException;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.github.example.pt.exception.APIErrorCode.MAX_UPLOAD_SIZE_EXCEEDED;
+
+@ControllerAdvice
+@Slf4j
+public class GlobalControllerAdvice {
+ @Autowired
+ private ClassUtil classUtil;
+
+ @ExceptionHandler(value = FixedAnnounceException.class)
+ @ResponseBody
+ public ResponseEntity<String> announceExceptionHandler(FixedAnnounceException exception) {
+ Map<String, String> dict = new HashMap<>();
+ dict.put("failure reason", classUtil.getClassSimpleName(exception.getClass()) + ": " + exception.getMessage());
+ dict.put("retry in", "never");
+ return ResponseEntity.ok()
+ .body(BencodeUtil.convertToString(BencodeUtil.bittorrent().encode(dict)));
+ }
+
+ @ExceptionHandler(value = RetryableAnnounceException.class)
+ @ResponseBody
+ public ResponseEntity<String> announceExceptionHandler(RetryableAnnounceException exception) {
+ Map<String, String> dict = new HashMap<>();
+ dict.put("failure reason", classUtil.getClassSimpleName(exception.getClass()) + ": " + exception.getMessage());
+ dict.put("retry in", String.valueOf(exception.getRetryIn()));
+ return ResponseEntity.ok()
+ .body(BencodeUtil.convertToString(BencodeUtil.bittorrent().encode(dict)));
+ }
+
+ @ExceptionHandler(value = Exception.class)
+ @ResponseBody
+ public ResponseEntity<Map<String, Object>> apiExceptionHandler(Exception exception) {
+ log.error("Catch an API exception", exception);
+ return ResponseEntity.internalServerError()
+ .body(
+ Map.of("status", "error",
+ "type", classUtil.getClassSimpleName(exception.getClass()),
+ "message", exception.getMessage())
+ );
+ }
+
+ @ExceptionHandler(value = APIGenericException.class)
+ @ResponseBody
+ public ResponseEntity<Map<String, Object>> apiExceptionHandler(APIGenericException exception) {
+ return ResponseEntity
+ .status(exception.getStatusCode())
+ .body(
+ Map.of("status", "error",
+ "code", exception.getError(),
+ "type", exception.getErrorText(),
+ "message", exception.getMessage())
+ );
+ }
+
+ @ExceptionHandler(value = IllegalArgumentException.class)
+ @ResponseBody
+ public ResponseEntity<Map<String, Object>> argumentExceptionHandler(IllegalArgumentException exception) {
+ log.error("Catch an argument exception", exception);
+ return ResponseEntity.badRequest()
+ .body(
+ Map.of("status", "error",
+ "type", classUtil.getClassSimpleName(exception.getClass()),
+ "message", exception.getMessage())
+ );
+ }
+
+ @ExceptionHandler(value = NotLoginException.class)
+ @ResponseBody
+ public ResponseEntity<Map<String, Object>> loginExceptionHandler(NotLoginException exception) {
+ return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
+ .body(Map.of("status", "error",
+ "type", classUtil.getClassSimpleName(exception.getClass()),
+ "message", exception.getMessage())
+ );
+ }
+
+ @ExceptionHandler(value = NotPermissionException.class)
+ @ResponseBody
+ public ResponseEntity<Map<String, Object>> loginExceptionHandler(NotPermissionException exception) {
+ return ResponseEntity.status(HttpStatus.FORBIDDEN)
+ .body(Map.of("status", "error",
+ "type", classUtil.getClassSimpleName(exception.getClass()),
+ "message", exception.getMessage())
+ );
+ }
+
+
+ @ExceptionHandler(value = MultipartException.class)
+ @ResponseBody
+ public Object fileUploadExceptionHandler(MultipartException exception) {
+ Map<String, Object> map = new HashMap<>();
+ Throwable rootCause = exception.getRootCause();
+ if (rootCause instanceof MaxUploadSizeExceededException ex) {
+ return ResponseEntity
+ .status(MAX_UPLOAD_SIZE_EXCEEDED.getStatusCode())
+ .body(
+ Map.of("status", "error",
+ "code", MAX_UPLOAD_SIZE_EXCEEDED.getCode(),
+ "type", classUtil.getClassSimpleName(exception.getClass()),
+ "message", exception.getMessage())
+ );
+ }
+ return map;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/controller/announce/AnnounceController.java b/src/main/java/com/github/example/pt/controller/announce/AnnounceController.java
new file mode 100644
index 0000000..3782b26
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/announce/AnnounceController.java
@@ -0,0 +1,375 @@
+package com.github.example.pt.controller.announce;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.github.example.pt.config.TrackerConfig;
+import com.github.example.pt.entity.Peer;
+import com.github.example.pt.entity.Torrent;
+import com.github.example.pt.entity.User;
+import com.github.example.pt.exception.APIGenericException;
+import com.github.example.pt.exception.FixedAnnounceException;
+import com.github.example.pt.exception.InvalidAnnounceException;
+import com.github.example.pt.exception.RetryableAnnounceException;
+import com.github.example.pt.service.AnnouncePerformanceMonitorService;
+import com.github.example.pt.service.AnnounceService;
+import com.github.example.pt.service.AuthenticationService;
+import com.github.example.pt.service.BlacklistClientService;
+import com.github.example.pt.service.CategoryService;
+import com.github.example.pt.service.PeerService;
+import com.github.example.pt.service.PromotionService;
+import com.github.example.pt.service.SettingService;
+import com.github.example.pt.service.TorrentService;
+import com.github.example.pt.service.TransferHistoryService;
+import com.github.example.pt.service.UserService;
+import com.github.example.pt.type.AnnounceEventType;
+import com.github.example.pt.util.BencodeUtil;
+import com.github.example.pt.util.BooleanUtil;
+import com.github.example.pt.util.IPUtil;
+import com.github.example.pt.util.InfoHashUtil;
+import com.github.example.pt.util.MiscUtil;
+import com.github.example.pt.util.SafeUUID;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.validator.routines.InetAddressValidator;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+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;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Random;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+@RestController
+@Slf4j
+public class AnnounceController {
+ private static final Random random = new Random();
+ private static final Pattern infoHashPattern = Pattern.compile("info_hash=(.*?)($|&)");
+ private static final Pattern peerIdPattern = Pattern.compile("peer_id=(.*?)&");
+ private static final InetAddressValidator ipValidator = InetAddressValidator.getInstance();
+ private static final int MIN_INTERVAL = 60 * 60 * 15;
+ private static final int MAX_INTERVAL = 60 * 60 * 45;
+ @Autowired
+ private HttpServletRequest request;
+ @Autowired
+ private BlacklistClientService blacklistClientService;
+ @Autowired
+ private PeerService peerService;
+ @Autowired
+ private AnnounceService announceBackgroundJob;
+ @Autowired
+ private UserService userService;
+ @Autowired
+ private TorrentService torrentService;
+ @Autowired
+ private PromotionService promotionService;
+ @Autowired
+ private AnnouncePerformanceMonitorService performanceMonitorService;
+ @Autowired
+ private CategoryService categoryService;
+ @Autowired
+ private SettingService settingService;
+ @Autowired
+ private AuthenticationService authenticationService;
+ @Autowired
+ private TransferHistoryService transferHistoryService;
+
+
+ @GetMapping("/scrape")
+ public ResponseEntity<String> scrape(@RequestParam Map<String, String> gets) throws FixedAnnounceException {
+ // https://wiki.vuze.com/w/Scrape
+ String passkey = gets.get("passkey");
+ if (StringUtils.isEmpty(passkey)) {
+ throw new InvalidAnnounceException("You must re-download the torrent from tracker for seeding.");
+ }
+ if (!SafeUUID.isUUID(passkey)) {
+ throw new InvalidAnnounceException("Invalid passkey.");
+ }
+ checkClient();
+ checkScrapeFields(gets);
+ User user = safeParseUser(passkey);
+ if (!StpUtil.hasPermission(user.getId(), "torrent:scrape")) {
+ throw new InvalidAnnounceException("Permission Denied");
+ }
+ Map<String, Object> dict = new LinkedHashMap<>();
+ Map<String, Integer> flags = new LinkedHashMap<>();
+ flags.put("min_request_interval", randomInterval());
+ dict.put("flags", flags);
+ Map<String, Map<String, Object>> files = new LinkedHashMap<>();
+ for (String infoHash : readAllInfoHash(request.getQueryString())) {
+ Torrent torrent = torrentService.getTorrent(infoHash);
+ if (torrent == null) {
+ continue;
+ }
+ Map<String, Object> meta = new LinkedHashMap<>();
+ TransferHistoryService.PeerStatus peerStatus = transferHistoryService.getPeerStatus(torrent);
+ meta.put("downloaded", peerStatus.downloaded());
+ meta.put("complete", peerStatus.complete());
+ meta.put("incomplete", peerStatus.incomplete());
+ meta.put("downloaders", peerStatus.downloaders());
+ files.put(infoHash, meta);
+ }
+ dict.put("files", files);
+ String resp = BencodeUtil.convertToString(BencodeUtil.bittorrent().encode(dict));
+ return ResponseEntity.ok()
+ .header("Content-Type", "text/plain; charset=iso-8859-1")
+ .body(resp);
+ }
+
+ @NotNull
+ private User safeParseUser(@NotNull String passkey) throws InvalidAnnounceException {
+ User user;
+ try {
+ user = authenticationService.authenticate(passkey, IPUtil.getRequestIp(request));
+ if (user == null) {
+ throw new InvalidAnnounceException("Unauthorized");
+ }
+ } catch (APIGenericException e) {
+ throw new InvalidAnnounceException("APIError: " + e.getErrorText() + " -> " + e.getMessage());
+ }
+ return user;
+ }
+
+
+ @GetMapping("/announce")
+ public ResponseEntity<String> announce(@RequestParam Map<String, String> gets) throws FixedAnnounceException, RetryableAnnounceException {
+ long ns = System.nanoTime();
+ String[] ipv4 = request.getParameterValues("ipv4");
+ String[] ipv6 = request.getParameterValues("ipv6");
+ String passkey = gets.get("passkey");
+ log.info("sfmierngitnriugnbearjgjhb9rjgmb");
+ if (StringUtils.isEmpty(passkey)) {
+ throw new InvalidAnnounceException("You must re-download the torrent from tracker for seeding.");
+ }
+ if (!SafeUUID.isUUID(passkey)) {
+ throw new InvalidAnnounceException("Invalid passkey.");
+ }
+ checkClient();
+ checkAnnounceFields(gets);
+ String peerId = gets.get("peer_id");
+ long left = Long.parseLong(gets.get("left"));
+ int port = Integer.parseInt(gets.get("port"));
+ AnnounceEventType event = AnnounceEventType.fromName(gets.get("event"));
+ int numWant = Integer.parseInt(Optional.ofNullable(MiscUtil.anyNotNull(gets.get("numwant"), gets.get("num want"), gets.get("num_want"))).orElse("150"));
+ numWant = Math.min(numWant, 300);
+ boolean noPeerId = BooleanUtil.parseBoolean(Optional.ofNullable(MiscUtil.anyNotNull(gets.get("nopeerid"), gets.get("no_peerid"), gets.get("no_peer_id"))).orElse("0"));
+ boolean supportCrypto = BooleanUtil.parseBoolean(Optional.ofNullable(MiscUtil.anyNotNull(gets.get("supportcrypto"), gets.get("support crypto"), gets.get("support_crypto"))).orElse("0"));
+ boolean compact = BooleanUtil.parseBoolean(gets.get("compact"));
+ List<String> peerIp = cutIps(Optional.ofNullable(MiscUtil.anyNotNull(gets.get("ip"), gets.get("address"), gets.get("ipaddress"), gets.get("ip_address"), gets.get("ip address"))).orElse(IPUtil.getRequestIp(request)));
+ String infoHash = InfoHashUtil.parseInfoHash(readInfoHash(request.getQueryString()));
+ long downloaded = Math.max(0, Long.parseLong(gets.get("downloaded")));
+ long uploaded = Math.max(0, Long.parseLong(gets.get("uploaded")));
+ int redundant = Integer.parseInt(Optional.ofNullable(MiscUtil.anyNotNull(gets.get("redundant"), gets.get("redundant_peers"), gets.get("redundant peers"), gets.get("redundant_peers"))).orElse("0"));
+ // User permission checks
+ User user = safeParseUser(passkey);
+// if (!StpUtil.hasPermission(user.getId(), "torrent:announce")) {
+// throw new InvalidAnnounceException("Permission Denied");
+// }
+ Torrent torrent = torrentService.getTorrent(infoHash);
+ if (torrent == null) {
+ throw new InvalidAnnounceException("Torrent not registered on this tracker");
+ }
+ // User had permission to announce torrents
+ // Create an announce tasks and drop into background, end this request as fast as possible
+ Set<String> peerIps = new HashSet<>(peerIp);
+
+ if (ipv4 != null) {
+ peerIps.addAll(List.of(ipv4));
+ }
+ if (ipv6 != null) {
+ peerIps.addAll(List.of(ipv6));
+ }
+ List<String> filteredIps = peerIps.stream().filter(this::checkValidIp).toList();
+ if (filteredIps.isEmpty()) {
+ log.info("Client {} announced invalid ips.", user.getUsername());
+ throw new InvalidAnnounceException("Invalid IP address");
+ }
+ for (String filteredIp : filteredIps) {
+ announceBackgroundJob.schedule(new AnnounceService.AnnounceTask(filteredIp, port, infoHash, peerId, uploaded, downloaded, left, event, numWant, user.getId(), compact, noPeerId, supportCrypto, redundant, request.getHeader("User-Agent"), passkey, torrent.getId()));
+ }
+ String peers = BencodeUtil.convertToString(BencodeUtil.bittorrent().encode(generatePeersResponse(torrent, numWant, compact)));
+ performanceMonitorService.recordStats(System.nanoTime() - ns);
+ log.debug("adiuaherfwehf9whg");
+ return ResponseEntity.ok()
+ .header("Content-Type", "text/plain; charset=iso-8859-1")
+ .body(peers);
+ }
+
+ private List<String> cutIps(String str) {
+ List<String> ips = new ArrayList<>();
+ if (str.contains(",")) {
+ ips.addAll(Arrays.stream(str.split(",")).map(String::trim).toList());
+ } else {
+ ips.add(str);
+ }
+ return ips;
+ }
+
+ @Nullable
+ private String readInfoHash(@NotNull String queryString) {
+ // This is a workaround for binary encoded info_hash data.
+ String[] queryStrings = queryString.split("&");
+ for (String string : queryStrings) {
+ String[] args = string.split("=");
+ if (args.length != 2) {
+ continue;
+ }
+ String key = args[0];
+ if (key.equals("info_hash")) {
+ return args[1];
+ }
+ }
+ return null;
+ }
+
+ private @NotNull List<String> readAllInfoHash(@NotNull String queryString) {
+ // This is a workaround for binary encoded info_hash data.
+ List<String> infoHash = new ArrayList<>();
+ String[] queryStrings = queryString.split("&");
+ for (String string : queryStrings) {
+ String[] args = string.split("=");
+ if (args.length != 2) {
+ continue;
+ }
+ String key = args[0];
+ if (key.equals("info_hash")) {
+ infoHash.add(args[1]);
+ }
+ }
+ return infoHash;
+ }
+
+ @SneakyThrows(UnknownHostException.class)
+ private boolean checkValidIp(@NotNull String ip) {
+ InetAddress address = InetAddress.getByName(ip);
+ return true;
+// return !address.isAnyLocalAddress()
+// && !address.isLinkLocalAddress()
+// && !address.isLoopbackAddress()
+// && !address.isSiteLocalAddress();
+ }
+
+ private void checkClient() throws FixedAnnounceException {
+ String method = request.getMethod();
+ if (!method.equals("GET")) {
+ throw new InvalidAnnounceException("Invalid request method: " + method);
+ }
+ if (request.getHeader("User-Agent") == null) {
+ throw new InvalidAnnounceException("Bad client: User-Agent cannot be empty");
+ }
+ }
+
+ private void checkScrapeFields(@NotNull Map<String, String> gets) throws InvalidAnnounceException {
+ if (StringUtils.isEmpty(gets.get("info_hash"))) throw new InvalidAnnounceException("Missing param: info_hash");
+ }
+
+
+ private void checkAnnounceFields(@NotNull Map<String, String> gets) throws InvalidAnnounceException {
+ if (StringUtils.isEmpty(gets.get("info_hash"))) throw new InvalidAnnounceException("Missing param: info_hash");
+ if (StringUtils.isEmpty(gets.get("peer_id"))) throw new InvalidAnnounceException("Missing param: peer_id");
+ if (StringUtils.isEmpty(gets.get("port")) || !StringUtils.isNumeric(gets.get("port")))
+ throw new InvalidAnnounceException("Missing/Invalid param: port");
+ if (StringUtils.isEmpty(gets.get("uploaded")) || !StringUtils.isNumeric(gets.get("uploaded")))
+ throw new InvalidAnnounceException("Missing/Invalid param: uploaded");
+ if (StringUtils.isEmpty(gets.get("downloaded")) || !StringUtils.isNumeric(gets.get("downloaded")))
+ throw new InvalidAnnounceException("Missing/Invalid param: downloaded");
+ if (StringUtils.isEmpty(gets.get("left")) || !StringUtils.isNumeric(gets.get("left")))
+ throw new InvalidAnnounceException("Missing/Invalid param: left");
+ String numwant = MiscUtil.anyNotNull(gets.get("numwant"), gets.get("num want"), gets.get("num_want"));
+ if (!StringUtils.isEmpty(numwant) && !StringUtils.isNumeric(numwant))
+ throw new InvalidAnnounceException("Invalid optional param: numwant");
+ if (!StringUtils.isEmpty(gets.get("compact")) && !StringUtils.isNumeric(gets.get("compact")))
+ throw new InvalidAnnounceException("Invalid optional param: compact");
+ if (!StringUtils.isEmpty(gets.get("corrupt")) && !StringUtils.isNumeric(gets.get("corrupt")))
+ throw new InvalidAnnounceException("Invalid optional param: corrupt");
+ }
+
+ @NotNull
+ private Map<String, Object> generatePeersResponse(Torrent torrent, int numWant, boolean compact) throws RetryableAnnounceException {
+ Map<String, Object> resp;
+ if (compact) {
+ resp = generatePeersResponseCompat(torrent, numWant);
+ } else {
+ resp = generatePeersResponseNonCompat(torrent, numWant, compact);
+ }
+ return resp;
+ }
+
+ @NotNull
+ private Map<String, Object> generatePeersResponseCompat(@NotNull Torrent torrent, int numWant) throws RetryableAnnounceException {
+ PeerResult peers = gatherPeers(torrent.getInfoHash(), numWant);
+ TransferHistoryService.PeerStatus peerStatus = transferHistoryService.getPeerStatus(torrent);
+ Map<String, Object> dict = new HashMap<>();
+ dict.put("interval", randomInterval());
+ dict.put("complete", peerStatus.complete());
+ dict.put("incomplete", peerStatus.incomplete());
+ dict.put("downloaded", peerStatus.downloaded());
+ dict.put("downloaders", peerStatus.downloaders());
+ dict.put("peers", BencodeUtil.compactPeers(peers.peers(), false));
+ if (peers.peers6().size() > 0) {
+ dict.put("peers6", BencodeUtil.compactPeers(peers.peers6(), true));
+ }
+ return dict;
+ }
+
+ @NotNull
+ private Map<String, Object> generatePeersResponseNonCompat(@NotNull Torrent torrent, int numWant, boolean noPeerId) {
+ PeerResult peers = gatherPeers(torrent.getInfoHash(), numWant);
+ TransferHistoryService.PeerStatus peerStatus = transferHistoryService.getPeerStatus(torrent);
+ List<Map<String, Object>> peerList = new ArrayList<>();
+ List<Peer> allPeers = new ArrayList<>(peers.peers());
+ allPeers.addAll(peers.peers6());
+ for (Peer peer : allPeers) {
+ Map<String, Object> peerMap = new LinkedHashMap<>();
+ if (!noPeerId) {
+ peerMap.put("peer id", peer.getPeerId());
+ }
+ peerMap.put("ip", peer.getIp());
+ peerMap.put("port", peer.getPort());
+ peerList.add(peerMap);
+ }
+ Map<String, Object> dict = new HashMap<>();
+ dict.put("interval", randomInterval());
+ dict.put("complete", peerStatus.complete());
+ dict.put("incomplete", peerStatus.incomplete());
+ dict.put("downloaded", peerStatus.downloaded());
+ dict.put("downloaders", peerStatus.downloaders());
+ dict.put("peers", peerList);
+ return dict;
+ }
+
+ @NotNull
+ private PeerResult gatherPeers(@NotNull String infoHash, int numWant) {
+ List<Peer> torrentPeers = peerService.getPeers(infoHash, numWant);
+ //List<Peer> torrentPeers = RandomUtil.getRandomElements(allPeers, numWant);
+ List<Peer> v4 = torrentPeers.stream().filter(peer -> ipValidator.isValidInet4Address(peer.getIp())).toList();
+ List<Peer> v6 = torrentPeers.stream().filter(peer -> ipValidator.isValidInet6Address(peer.getIp())).toList();
+ int downloaders = (int) torrentPeers.stream().filter(Peer::isPartialSeeder).count();
+ long completed = torrentPeers.stream().filter(Peer::isSeeder).count();
+ long incompleted = torrentPeers.size() - completed;
+ return new PeerResult(v4, v6, completed, incompleted, downloaders);
+ }
+
+ private int randomInterval() {
+ TrackerConfig trackerConfig = settingService.get(TrackerConfig.getConfigKey(), TrackerConfig.class);
+ return random.nextInt(trackerConfig.getTorrentIntervalMin(), trackerConfig.getTorrentIntervalMax());
+ }
+
+ record PeerResult(@NotNull List<Peer> peers, List<Peer> peers6, long complete, long incomplete, int downloaders) {
+ }
+
+}
diff --git a/src/main/java/com/github/example/pt/controller/auth/AuthController.java b/src/main/java/com/github/example/pt/controller/auth/AuthController.java
new file mode 100644
index 0000000..72a2e45
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/auth/AuthController.java
@@ -0,0 +1,164 @@
+package com.github.example.pt.controller.auth;
+
+import cn.dev33.satoken.exception.NotLoginException;
+import cn.dev33.satoken.stp.SaTokenInfo;
+import cn.dev33.satoken.stp.StpUtil;
+import com.github.example.pt.controller.auth.dto.request.LoginRequestDTO;
+import com.github.example.pt.controller.auth.dto.request.RegisterRequestDTO;
+import com.github.example.pt.controller.dto.response.LoginStatusResponseDTO;
+import com.github.example.pt.controller.dto.response.UserResponseDTO;
+import com.github.example.pt.controller.dto.response.UserSessionResponseDTO;
+import com.github.example.pt.entity.User;
+import com.github.example.pt.exception.APIErrorCode;
+import com.github.example.pt.exception.APIGenericException;
+import com.github.example.pt.service.AuthenticationService;
+import com.github.example.pt.service.UserGroupService;
+import com.github.example.pt.service.UserService;
+import com.github.example.pt.type.PrivacyLevel;
+import com.github.example.pt.util.IPUtil;
+import com.github.example.pt.util.PasswordHash;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.math.BigDecimal;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import static com.github.example.pt.exception.APIErrorCode.AUTHENTICATION_FAILED;
+import static com.github.example.pt.exception.APIErrorCode.MISSING_PARAMETERS;
+
+@RestController
+@RequestMapping("/auth")
+@Slf4j
+public class AuthController {
+ @Autowired
+ private HttpServletRequest request;
+ @Autowired
+ private UserService userService;
+ @Autowired
+ private UserGroupService userGroupService;
+ @Autowired
+ private AuthenticationService authenticationService;
+
+ @PostMapping("/login")
+ public UserSessionResponseDTO login(@RequestBody LoginRequestDTO login) {
+ String ip = IPUtil.getRequestIp(request);
+ if (StringUtils.isEmpty(login.getUser())) {
+ throw new APIGenericException(MISSING_PARAMETERS, "User parameter is required");
+ }
+ if (StringUtils.isEmpty(login.getPassword())) {
+ throw new APIGenericException(MISSING_PARAMETERS, "Password parameter is required");
+ }
+ User user = userService.getUserByUsername(login.getUser());
+ if (user == null) user = userService.getUserByEmail(login.getUser());
+ if (user == null) {
+ log.info("IP {} tried to login with not exists username {}.",ip, login.getUser());
+ authenticationService.markUserLoginFail(ip); // Mark fail because it not use authenticate
+ authenticationService.checkAccountLoginAttempts(ip);
+ throw new APIGenericException(AUTHENTICATION_FAILED);
+ }
+ if(!authenticationService.authenticate(user,login.getPassword(),ip)){
+ log.info("IP {} tried to login to user {} with bad password.",ip, login.getUser());
+ throw new APIGenericException(AUTHENTICATION_FAILED);
+ }
+
+ StpUtil.login(user.getId());
+ return getUserBasicInformation(user);
+ }
+
+ @PostMapping("/logout")
+ public Map<String, Object> logout() {
+ if (StpUtil.isLogin()) {
+ StpUtil.logout();
+ Map<String, Object> logoutResponse = new LinkedHashMap<>();
+ logoutResponse.put("status", "ok");
+ return logoutResponse;
+ } else {
+ throw new APIGenericException(APIErrorCode.REQUIRED_AUTHENTICATION);
+ }
+ }
+
+ @GetMapping("/status")
+ public LoginStatusResponseDTO status() {
+ try {
+ User user = userService.getUser(StpUtil.getLoginIdAsLong());
+ if (user == null) {
+ return new LoginStatusResponseDTO(false, false, false, null);
+ } else {
+ return new LoginStatusResponseDTO(true, true, false, getUserBasicInformation(user));
+ }
+ } catch (NotLoginException e) {
+ return new LoginStatusResponseDTO(false, false, false, null);
+ }
+ }
+
+ @PostMapping("/register")
+ @Transactional
+ public UserSessionResponseDTO register(@RequestBody RegisterRequestDTO register) {
+ log.info("Received register request: {}", register.getEmail());
+ log.info("这里111111111111111111111111111");
+ if (StringUtils.isEmpty(register.getEmail())) {
+ throw new APIGenericException(MISSING_PARAMETERS, "Email parameter is required");
+ }
+ if (StringUtils.isEmpty(register.getUsername())) {
+ throw new APIGenericException(MISSING_PARAMETERS, "Username parameter is required");
+ }
+ if (StringUtils.isEmpty(register.getPassword())) {
+ throw new APIGenericException(MISSING_PARAMETERS, "Password parameter is required");
+ }
+ User user = userService.getUserByUsername(register.getUsername());
+ if (user != null) {
+ throw new APIGenericException(APIErrorCode.USERNAME_ALREADY_IN_USAGE);
+ }
+ user = userService.getUserByEmail(register.getEmail());
+ if (user != null) {
+ throw new APIGenericException(APIErrorCode.EMAIL_ALREADY_IN_USAGE);
+ }
+ user = userService.save(new User(
+ 0,
+ register.getEmail(),
+ PasswordHash.hash(register.getPassword()),
+ register.getUsername(),
+ userGroupService.getDefaultUserGroup(),
+ UUID.randomUUID().toString(),
+ Timestamp.from(Instant.now()),
+ "https://www.baidu.com/facivon.ico",
+ "测试用户",
+ "这个用户很懒,还没有个性签名",
+ "zh-CN",
+ "100mbps",
+ "100mbps",
+ 0L, 0L, 0L, 0L,
+ "未知",
+ BigDecimal.ZERO,
+ 0,
+ 0L,
+ UUID.randomUUID().toString(), PrivacyLevel.LOW));
+ StpUtil.login(user.getId());
+ return getUserBasicInformation(user);
+ }
+
+ @NotNull
+ private UserSessionResponseDTO getUserBasicInformation(User user) {
+ SaTokenInfo tokenInfo = null;
+ try {
+ tokenInfo = StpUtil.getTokenInfo();
+ } catch (NotLoginException ignored) {
+
+ }
+ return new UserSessionResponseDTO(tokenInfo, new UserResponseDTO(user));
+ }
+
+}
diff --git a/src/main/java/com/github/example/pt/controller/auth/dto/request/LoginRequestDTO.java b/src/main/java/com/github/example/pt/controller/auth/dto/request/LoginRequestDTO.java
new file mode 100644
index 0000000..60b7b4e
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/auth/dto/request/LoginRequestDTO.java
@@ -0,0 +1,18 @@
+package com.github.example.pt.controller.auth.dto.request;
+
+import jakarta.validation.constraints.NotEmpty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@Validated
+public class LoginRequestDTO {
+ @NotEmpty
+ private String user;
+ @NotEmpty
+ private String password;
+}
diff --git a/src/main/java/com/github/example/pt/controller/auth/dto/request/RegisterRequestDTO.java b/src/main/java/com/github/example/pt/controller/auth/dto/request/RegisterRequestDTO.java
new file mode 100644
index 0000000..3e6ad68
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/auth/dto/request/RegisterRequestDTO.java
@@ -0,0 +1,22 @@
+package com.github.example.pt.controller.auth.dto.request;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@Validated
+public class RegisterRequestDTO {
+ @NotEmpty
+ private String username;
+ @NotEmpty
+ private String password;
+ @NotEmpty
+ @Email
+ private String email;
+}
diff --git a/src/main/java/com/github/example/pt/controller/category/CategoryController.java b/src/main/java/com/github/example/pt/controller/category/CategoryController.java
new file mode 100644
index 0000000..348b3a4
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/category/CategoryController.java
@@ -0,0 +1,26 @@
+package com.github.example.pt.controller.category;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.github.example.pt.controller.dto.response.CategoryResponseDTO;
+import com.github.example.pt.service.CategoryService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/category")
+@Slf4j
+public class CategoryController {
+ @Autowired
+ private CategoryService categoryService;
+
+ @GetMapping("/list")
+ @SaCheckPermission("category:list")
+ public List<CategoryResponseDTO> listCategory(){
+ return categoryService.getAllCategories().stream().map(CategoryResponseDTO::new).toList();
+ }
+}
diff --git a/src/main/java/com/github/example/pt/controller/dto/response/CategoryResponseDTO.java b/src/main/java/com/github/example/pt/controller/dto/response/CategoryResponseDTO.java
new file mode 100644
index 0000000..ef2fbfe
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/dto/response/CategoryResponseDTO.java
@@ -0,0 +1,25 @@
+package com.github.example.pt.controller.dto.response;
+
+import com.github.example.pt.entity.Category;
+import com.github.example.pt.objects.ResponsePojo;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.validation.annotation.Validated;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+@Validated
+public class CategoryResponseDTO extends ResponsePojo {
+ private long id;
+ private String slug;
+ private String name;
+ private String icon;
+
+ public CategoryResponseDTO(@NotNull Category category) {
+ this.id = category.getId();
+ this.slug = category.getSlug();
+ this.name = category.getName();
+ this.icon = category.getIcon();
+ }
+}
diff --git a/src/main/java/com/github/example/pt/controller/dto/response/LoginStatusResponseDTO.java b/src/main/java/com/github/example/pt/controller/dto/response/LoginStatusResponseDTO.java
new file mode 100644
index 0000000..0b20850
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/dto/response/LoginStatusResponseDTO.java
@@ -0,0 +1,19 @@
+package com.github.example.pt.controller.dto.response;
+
+import com.github.example.pt.objects.ResponsePojo;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.validation.annotation.Validated;
+
+@EqualsAndHashCode(callSuper = true)
+@AllArgsConstructor
+@Data
+@Validated
+public class LoginStatusResponseDTO extends ResponsePojo {
+ private boolean isLoggedIn;
+ private boolean isSafe;
+ private boolean isSwitch;
+ private UserSessionResponseDTO user;
+
+}
diff --git a/src/main/java/com/github/example/pt/controller/dto/response/PeerInfoResponseDTO.java b/src/main/java/com/github/example/pt/controller/dto/response/PeerInfoResponseDTO.java
new file mode 100644
index 0000000..41ea978
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/dto/response/PeerInfoResponseDTO.java
@@ -0,0 +1,51 @@
+package com.github.example.pt.controller.dto.response;
+
+import com.github.example.pt.entity.Peer;
+import com.github.example.pt.objects.ResponsePojo;
+import com.github.example.pt.type.PrivacyLevel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.validation.annotation.Validated;
+
+import java.sql.Timestamp;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+@Validated
+public class PeerInfoResponseDTO extends ResponsePojo {
+ private long id;
+ private UserBasicResponseDTO user;
+ private String infoHash;
+ private String peerId;
+ private String userAgent;
+ private long uploaded;
+ private long downloaded;
+ private long left;
+ private boolean seeder;
+ private boolean partialSeeder;
+ private Timestamp updateAt;
+ private long seedingTime;
+ private long uploadSpeed;
+ private long downloadSpeed;
+
+ public PeerInfoResponseDTO(Peer peer) {
+ this.id = peer.getId();
+ if (peer.getUser().getPrivacyLevel().ordinal() > PrivacyLevel.MEDIUM.ordinal()) {
+ this.user = null;
+ } else {
+ this.user = new UserBasicResponseDTO(peer.getUser());
+ }
+ this.infoHash = peer.getInfoHash();
+ this.peerId = peer.getPeerId();
+ this.userAgent = peer.getUserAgent();
+ this.uploaded = peer.getUploaded();
+ this.downloaded = peer.getDownloaded();
+ this.left = peer.getLeft();
+ this.seeder = peer.isSeeder();
+ this.partialSeeder = peer.isPartialSeeder();
+ this.updateAt = peer.getUpdateAt();
+ this.seedingTime = peer.getSeedingTime();
+ this.uploadSpeed = peer.getUploadSpeed();
+ this.downloadSpeed = peer.getDownloadSpeed();
+ }
+}
diff --git a/src/main/java/com/github/example/pt/controller/dto/response/PromotionResponseDTO.java b/src/main/java/com/github/example/pt/controller/dto/response/PromotionResponseDTO.java
new file mode 100644
index 0000000..eccd69e
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/dto/response/PromotionResponseDTO.java
@@ -0,0 +1,26 @@
+package com.github.example.pt.controller.dto.response;
+
+import com.github.example.pt.entity.PromotionPolicy;
+import com.github.example.pt.objects.ResponsePojo;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.validation.annotation.Validated;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+@Validated
+public class PromotionResponseDTO extends ResponsePojo {
+ private long id;
+ private String slug;
+ private String displayName;
+ private double uploadRatio;
+ private double downloadRatio;
+
+ public PromotionResponseDTO(PromotionPolicy policy){
+ this.id = policy.getId();
+ this.slug = policy.getSlug();
+ this.displayName = policy.getDisplayName();
+ this.uploadRatio = policy.getUploadRatio();
+ this.downloadRatio = policy.getDownloadRatio();
+ }
+}
diff --git a/src/main/java/com/github/example/pt/controller/dto/response/ScrapeContainerDTO.java b/src/main/java/com/github/example/pt/controller/dto/response/ScrapeContainerDTO.java
new file mode 100644
index 0000000..7f68d3a
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/dto/response/ScrapeContainerDTO.java
@@ -0,0 +1,15 @@
+package com.github.example.pt.controller.dto.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.springframework.validation.annotation.Validated;
+
+@Data
+@AllArgsConstructor
+@Validated
+public class ScrapeContainerDTO {
+ private long downloaded;
+ private long complete;
+ private long incomplete;
+ private long downloaders;
+}
diff --git a/src/main/java/com/github/example/pt/controller/dto/response/TorrentBasicResponseDTO.java b/src/main/java/com/github/example/pt/controller/dto/response/TorrentBasicResponseDTO.java
new file mode 100644
index 0000000..fbe728d
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/dto/response/TorrentBasicResponseDTO.java
@@ -0,0 +1,48 @@
+package com.github.example.pt.controller.dto.response;
+
+import com.github.example.pt.entity.PromotionPolicy;
+import com.github.example.pt.entity.Tag;
+import com.github.example.pt.entity.Torrent;
+import com.github.example.pt.objects.ResponsePojo;
+import lombok.Getter;
+import org.springframework.validation.annotation.Validated;
+
+import java.sql.Timestamp;
+import java.util.List;
+
+@Getter
+@Validated
+public class TorrentBasicResponseDTO extends ResponsePojo {
+ private final long id;
+ private final String infoHash;
+ private final UserTinyResponseDTO user;
+ private final String title;
+ private final String subTitle;
+ private final long size;
+ private final Timestamp createdAt;
+ private final boolean underReview;
+ private final boolean anonymous;
+ private final CategoryResponseDTO category;
+ private final PromotionPolicy promotionPolicy;
+ private final List<String> tag;
+
+ public TorrentBasicResponseDTO(Torrent torrent){
+ super(0);
+ this.id = torrent.getId();
+ this.infoHash = torrent.getInfoHash();
+ if(torrent.isAnonymous()){
+ this.user = null;
+ }else{
+ this.user = new UserTinyResponseDTO(torrent.getUser());
+ }
+ this.title = torrent.getTitle();
+ this.subTitle = torrent.getSubTitle();
+ this.size = torrent.getSize();
+ this.createdAt = torrent.getCreatedAt();
+ this.underReview = torrent.isUnderReview();
+ this.anonymous = torrent.isAnonymous();
+ this.category = new CategoryResponseDTO(torrent.getCategory());
+ this.promotionPolicy = torrent.getPromotionPolicy();
+ this.tag = torrent.getTag().stream().map(Tag::getName).toList();
+ }
+}
diff --git a/src/main/java/com/github/example/pt/controller/dto/response/TorrentInfoResponseDTO.java b/src/main/java/com/github/example/pt/controller/dto/response/TorrentInfoResponseDTO.java
new file mode 100644
index 0000000..bb2bf52
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/dto/response/TorrentInfoResponseDTO.java
@@ -0,0 +1,53 @@
+package com.github.example.pt.controller.dto.response;
+
+import com.github.example.pt.entity.PromotionPolicy;
+import com.github.example.pt.entity.Tag;
+import com.github.example.pt.entity.Torrent;
+import com.github.example.pt.objects.ResponsePojo;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.validation.annotation.Validated;
+
+import java.sql.Timestamp;
+import java.util.List;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+@Validated
+public class TorrentInfoResponseDTO extends ResponsePojo {
+ private long id;
+ private String infoHash;
+ private UserResponseDTO user;
+ private String title;
+ private String subTitle;
+ private long size;
+ private long finishes;
+ private Timestamp createdAt;
+ private Timestamp updatedAt;
+ private boolean underReview;
+ private CategoryResponseDTO category;
+ private PromotionPolicy promotionPolicy;
+ private String description;
+ private List<String> tag;
+
+ public TorrentInfoResponseDTO(Torrent torrent){
+ super(0);
+ this.id = torrent.getId();
+ this.infoHash = torrent.getInfoHash();
+ if(torrent.isAnonymous()){
+ this.user = null;
+ }else{
+ this.user = new UserResponseDTO(torrent.getUser());
+ }
+ this.title = torrent.getTitle();
+ this.subTitle = torrent.getSubTitle();
+ this.size = torrent.getSize();
+ this.createdAt = torrent.getCreatedAt();
+ this.updatedAt = torrent.getUpdatedAt();
+ this.underReview = torrent.isUnderReview();
+ this.category = new CategoryResponseDTO(torrent.getCategory());
+ this.promotionPolicy = torrent.getPromotionPolicy();
+ this.description = torrent.getDescription();
+ this.tag = torrent.getTag().stream().map(Tag::getName).toList();
+ }
+}
diff --git a/src/main/java/com/github/example/pt/controller/dto/response/TransferHistoryDTO.java b/src/main/java/com/github/example/pt/controller/dto/response/TransferHistoryDTO.java
new file mode 100644
index 0000000..451fc1d
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/dto/response/TransferHistoryDTO.java
@@ -0,0 +1,45 @@
+package com.github.example.pt.controller.dto.response;
+
+import com.github.example.pt.entity.TransferHistory;
+import com.github.example.pt.type.PrivacyLevel;
+import lombok.Data;
+import org.springframework.validation.annotation.Validated;
+
+import java.sql.Timestamp;
+
+@Data
+@Validated
+public class TransferHistoryDTO {
+ private long id;
+ private UserBasicResponseDTO user;
+ private TorrentBasicResponseDTO torrent;
+ private long left;
+ private Timestamp startedAt;
+ private Timestamp updatedAt;
+ private long uploaded;
+ private long downloaded;
+ private long actualUploaded;
+ private long actualDownloaded;
+ private long uploadSpeed;
+ private long downloadSpeed;
+
+ public TransferHistoryDTO(TransferHistory transferHistory) {
+ this.id = transferHistory.getId();
+ if (transferHistory.getUser().getPrivacyLevel().ordinal() > PrivacyLevel.MEDIUM.ordinal()) {
+ this.user = null;
+ } else {
+ this.user = new UserBasicResponseDTO(transferHistory.getUser());
+ }
+ this.torrent = new TorrentBasicResponseDTO(transferHistory.getTorrent());
+ this.left = transferHistory.getLeft();
+ this.startedAt = transferHistory.getStartedAt();
+ this.updatedAt = transferHistory.getUpdatedAt();
+ this.uploaded = transferHistory.getUploaded();
+ this.downloaded = transferHistory.getDownloaded();
+ this.actualUploaded = transferHistory.getActualUploaded();
+ this.actualDownloaded = transferHistory.getActualDownloaded();
+ this.uploadSpeed = transferHistory.getUploadSpeed();
+ this.downloadSpeed = transferHistory.getDownloadSpeed();
+
+ }
+}
diff --git a/src/main/java/com/github/example/pt/controller/dto/response/UserBasicResponseDTO.java b/src/main/java/com/github/example/pt/controller/dto/response/UserBasicResponseDTO.java
new file mode 100644
index 0000000..3176f24
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/dto/response/UserBasicResponseDTO.java
@@ -0,0 +1,47 @@
+package com.github.example.pt.controller.dto.response;
+
+import com.github.example.pt.entity.User;
+import com.github.example.pt.objects.ResponsePojo;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.validation.annotation.Validated;
+
+import java.math.BigDecimal;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+@Validated
+public class UserBasicResponseDTO extends ResponsePojo {
+ private long id;
+ private String username;
+ private UserGroupResponseDTO group;
+ private long createdAt;
+ private String avatar;
+ private String customTitle;
+ private String signature;
+ private String downloadBandwidth;
+ private String uploadBandwidth;
+ private long downloaded;
+ private long uploaded;
+ private String isp;
+ private BigDecimal karma;
+ private long seedingTime;
+
+ public UserBasicResponseDTO(@NotNull User user) {
+ this.id = user.getId();
+ this.username = user.getUsername();
+ this.group = new UserGroupResponseDTO(user.getGroup());
+ this.createdAt = user.getCreatedAt().getTime();
+ this.avatar = user.getAvatar();
+ this.customTitle = user.getCustomTitle();
+ this.signature = user.getSignature();
+ this.downloadBandwidth = user.getDownloadBandwidth();
+ this.uploadBandwidth = user.getUploadBandwidth();
+ this.downloaded = user.getDownloaded();
+ this.uploaded = user.getUploaded();
+ this.isp = user.getIsp();
+ this.karma = user.getKarma();
+ this.seedingTime = user.getSeedingTime();
+ }
+}
diff --git a/src/main/java/com/github/example/pt/controller/dto/response/UserGroupResponseDTO.java b/src/main/java/com/github/example/pt/controller/dto/response/UserGroupResponseDTO.java
new file mode 100644
index 0000000..f94995e
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/dto/response/UserGroupResponseDTO.java
@@ -0,0 +1,22 @@
+package com.github.example.pt.controller.dto.response;
+
+import com.github.example.pt.entity.UserGroup;
+import com.github.example.pt.objects.ResponsePojo;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.validation.annotation.Validated;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+@Validated
+public class UserGroupResponseDTO extends ResponsePojo {
+ private long id;
+ private String slug;
+ private String displayName;
+ public UserGroupResponseDTO(@NotNull UserGroup userGroup){
+ this.id = userGroup.getId();
+ this.slug = userGroup.getSlug();
+ this.displayName = userGroup.getDisplayName();
+ }
+}
diff --git a/src/main/java/com/github/example/pt/controller/dto/response/UserResponseDTO.java b/src/main/java/com/github/example/pt/controller/dto/response/UserResponseDTO.java
new file mode 100644
index 0000000..93b9866
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/dto/response/UserResponseDTO.java
@@ -0,0 +1,57 @@
+package com.github.example.pt.controller.dto.response;
+
+import com.github.example.pt.entity.User;
+import com.github.example.pt.objects.ResponsePojo;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.validation.annotation.Validated;
+
+import java.math.BigDecimal;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+@Validated
+public class UserResponseDTO extends ResponsePojo {
+ private long id;
+ private String email;
+ private String username;
+ private UserGroupResponseDTO group;
+ private long createdAt;
+ private String avatar;
+ private String customTitle;
+ private String signature;
+ private String language;
+ private String downloadBandwidth;
+ private String uploadBandwidth;
+ private long downloaded;
+ private long uploaded;
+ private long realDownloaded;
+ private long realUploaded;
+ private String isp;
+ private BigDecimal karma;
+ private int inviteSlot;
+ private long seedingTime;
+
+ public UserResponseDTO(@NotNull User user) {
+ this.id = user.getId();
+ this.email = user.getEmail();
+ this.username = user.getUsername();
+ this.group = new UserGroupResponseDTO(user.getGroup());
+ this.createdAt = user.getCreatedAt().getTime();
+ this.avatar = user.getAvatar();
+ this.customTitle = user.getCustomTitle();
+ this.signature = user.getSignature();
+ this.language = user.getLanguage();
+ this.downloadBandwidth = user.getDownloadBandwidth();
+ this.uploadBandwidth = user.getUploadBandwidth();
+ this.downloaded = user.getDownloaded();
+ this.uploaded = user.getUploaded();
+ this.realDownloaded = user.getRealDownloaded();
+ this.realUploaded = user.getRealUploaded();
+ this.isp = user.getIsp();
+ this.karma = user.getKarma();
+ this.inviteSlot = user.getInviteSlot();
+ this.seedingTime = user.getSeedingTime();
+ }
+}
diff --git a/src/main/java/com/github/example/pt/controller/dto/response/UserSessionResponseDTO.java b/src/main/java/com/github/example/pt/controller/dto/response/UserSessionResponseDTO.java
new file mode 100644
index 0000000..b31d937
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/dto/response/UserSessionResponseDTO.java
@@ -0,0 +1,20 @@
+package com.github.example.pt.controller.dto.response;
+
+import cn.dev33.satoken.stp.SaTokenInfo;
+import com.github.example.pt.objects.ResponsePojo;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.validation.annotation.Validated;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+@Validated
+public class UserSessionResponseDTO extends ResponsePojo {
+ private SaTokenInfo token;
+ private UserResponseDTO user;
+
+ public UserSessionResponseDTO(SaTokenInfo token, UserResponseDTO user) {
+ this.token = token;
+ this.user = user;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/controller/dto/response/UserTinyResponseDTO.java b/src/main/java/com/github/example/pt/controller/dto/response/UserTinyResponseDTO.java
new file mode 100644
index 0000000..bb00414
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/dto/response/UserTinyResponseDTO.java
@@ -0,0 +1,25 @@
+package com.github.example.pt.controller.dto.response;
+
+import com.github.example.pt.entity.User;
+import com.github.example.pt.objects.ResponsePojo;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.validation.annotation.Validated;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+@Validated
+public class UserTinyResponseDTO extends ResponsePojo {
+ private long id;
+ private String username;
+ private UserGroupResponseDTO group;
+ private String avatar;
+
+ public UserTinyResponseDTO(@NotNull User user) {
+ this.id = user.getId();
+ this.username = user.getUsername();
+ this.group = new UserGroupResponseDTO(user.getGroup());
+ this.avatar = user.getAvatar();
+ }
+}
diff --git a/src/main/java/com/github/example/pt/controller/feed/FeedController.java b/src/main/java/com/github/example/pt/controller/feed/FeedController.java
new file mode 100644
index 0000000..3b9efb2
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/feed/FeedController.java
@@ -0,0 +1,131 @@
+package com.github.example.pt.controller.feed;
+
+import cn.dev33.satoken.exception.NotPermissionException;
+import cn.dev33.satoken.stp.StpUtil;
+import com.github.example.pt.config.SiteBasicConfig;
+import com.github.example.pt.entity.Torrent;
+import com.github.example.pt.entity.User;
+import com.github.example.pt.exception.APIErrorCode;
+import com.github.example.pt.exception.APIGenericException;
+import com.github.example.pt.service.AuthenticationService;
+import com.github.example.pt.service.SettingService;
+import com.github.example.pt.service.TorrentService;
+import com.github.example.pt.service.UserService;
+import com.github.example.pt.util.IPUtil;
+import com.rometools.rome.feed.rss.Category;
+import com.rometools.rome.feed.rss.Channel;
+import com.rometools.rome.feed.rss.Description;
+import com.rometools.rome.feed.rss.Enclosure;
+import com.rometools.rome.feed.rss.Guid;
+import com.rometools.rome.feed.rss.Item;
+import com.rometools.rome.io.FeedException;
+import com.rometools.rome.io.WireFeedOutput;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Pageable;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/feed")
+@Slf4j
+public class FeedController {
+ @Autowired
+ private TorrentService torrentService;
+ @Autowired
+ private UserService userService;
+ @Autowired
+ private SettingService settingService;
+ @Autowired
+ private HttpServletRequest request;
+ @Autowired
+ private AuthenticationService authenticationService;
+
+ @GetMapping("/subscribe")
+ public String feed(@RequestParam Map<String, String> params) throws FeedException {
+ String passkey = params.get("passkey");
+ if (StringUtils.isEmpty(passkey)) {
+ throw new APIGenericException(APIErrorCode.MISSING_PARAMETERS, "Passkey is required");
+ }
+ User user = authenticationService.authenticate(passkey, IPUtil.getRequestIp(request));
+ if (user == null) {
+ throw new APIGenericException(APIErrorCode.USER_NOT_FOUND, "Unauthorized");
+ }
+ if (!StpUtil.hasPermission(user.getId(), "feed:subscribe")) {
+ throw new NotPermissionException("feed:subscribe");
+ }
+ int entries = Math.min(Integer.parseInt(params.getOrDefault("entries", "50")), 300);
+ String categorySlug = params.get("category");
+ String[] categorySlugs = categorySlug != null ? categorySlug.split(",") : new String[0];
+ String promotionSlug = params.get("promotion");
+ String[] promotionSlugs = promotionSlug != null ? promotionSlug.split(",") : new String[0];
+ String tag = params.get("tag");
+ String[] tags = tag != null ? tag.split(",") : new String[0];
+ List<Torrent> torrentList = torrentService
+ .search("", Arrays.stream(categorySlugs).toList()
+ , Arrays.stream(promotionSlugs).toList(),
+ Arrays.stream(tags).toList(), Pageable.ofSize(entries))
+ .getContent();
+ return makeFeed(passkey, torrentList, false);
+ }
+
+ private String makeFeed(String passkey, List<Torrent> torrentList, boolean canSeeAnonymous) throws FeedException {
+ SiteBasicConfig basicConfig = settingService.get(SiteBasicConfig.getConfigKey(), SiteBasicConfig.class);
+ Channel channel = new Channel("rss_2.0");
+ channel.setTitle(basicConfig.getSiteName() + " - " + basicConfig.getSiteSubName());
+ channel.setGenerator("Sapling RSS Generator - v1.0");
+ channel.setEncoding("UTF-8");
+ channel.setPubDate(new Date());
+ channel.setDescription(basicConfig.getSiteDescription());
+ channel.setLink(basicConfig.getSiteBaseURL());
+ List<Item> items = new ArrayList<>();
+ for (Torrent torrent : torrentList) {
+ try {
+ Item item = new Item();
+ item.setAuthor(torrent.getUsernameWithAnonymous(canSeeAnonymous));
+ StringBuilder titleBuilder = new StringBuilder(torrent.getTitle());
+ appendTorrentTitle(torrent, titleBuilder);
+ item.setTitle(titleBuilder.toString());
+ item.setPubDate(torrent.getCreatedAt());
+ item.setLink(basicConfig.getSiteBaseURL() + "/torrent/" + torrent.getInfoHash());
+ Guid guid = new Guid();
+ guid.setValue(torrent.getInfoHash());
+ item.setGuid(guid);
+ Category category = new Category();
+ category.setValue(torrent.getCategory().getName());
+ item.setCategories(List.of(category));
+ Enclosure torrentClosure = new Enclosure();
+ torrentClosure.setType("application/x-bittorrent");
+ torrentClosure.setUrl(basicConfig.getSiteBaseURL() + "/torrent/download/" + torrent.getInfoHash() + "?passkey=" + passkey);
+ item.setEnclosures(List.of(torrentClosure));
+ Description description = new Description();
+ item.setDescription(description);
+ items.add(item);
+ } catch (Exception e) {
+ log.error("Error when generating RSS item for torrent: {}", torrent, e);
+ }
+ }
+ channel.setItems(items);
+ WireFeedOutput out = new WireFeedOutput();
+ return out.outputString(channel);
+ }
+
+ private void appendTorrentTitle(Torrent torrent, StringBuilder titleBuilder) {
+ titleBuilder.append(" ");
+ titleBuilder.append("[");
+ titleBuilder.append(torrent.getPromotionPolicy().getDisplayName());
+ titleBuilder.append("]");
+ titleBuilder.append(" ");
+ }
+
+}
diff --git a/src/main/java/com/github/example/pt/controller/promotion/PromotionController.java b/src/main/java/com/github/example/pt/controller/promotion/PromotionController.java
new file mode 100644
index 0000000..2a82ca0
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/promotion/PromotionController.java
@@ -0,0 +1,28 @@
+package com.github.example.pt.controller.promotion;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.github.example.pt.controller.dto.response.PromotionResponseDTO;
+import com.github.example.pt.service.PromotionService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/promotion")
+@Slf4j
+public class PromotionController {
+
+ @Autowired
+ private PromotionService promotionService;
+
+ @GetMapping("/list")
+ @SaCheckPermission("promotion:list")
+ public List<PromotionResponseDTO> listPromotions(){
+ return promotionService.getAllPromotionPolicies().stream().map(PromotionResponseDTO::new).toList();
+ }
+
+}
diff --git a/src/main/java/com/github/example/pt/controller/torrent/TorrentController.java b/src/main/java/com/github/example/pt/controller/torrent/TorrentController.java
new file mode 100644
index 0000000..4ff12f0
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/torrent/TorrentController.java
@@ -0,0 +1,276 @@
+package com.github.example.pt.controller.torrent;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.dev33.satoken.exception.NotPermissionException;
+import cn.dev33.satoken.stp.StpUtil;
+import com.github.example.pt.config.SiteBasicConfig;
+import com.github.example.pt.config.TrackerConfig;
+import com.github.example.pt.controller.dto.response.ScrapeContainerDTO;
+import com.github.example.pt.controller.dto.response.TorrentInfoResponseDTO;
+import com.github.example.pt.controller.dto.response.TransferHistoryDTO;
+import com.github.example.pt.controller.dto.response.UserTinyResponseDTO;
+import com.github.example.pt.controller.torrent.dto.request.SearchTorrentRequestDTO;
+import com.github.example.pt.controller.torrent.dto.request.ThanksResponseDTO;
+import com.github.example.pt.controller.torrent.dto.request.TorrentScrapeRequestDTO;
+import com.github.example.pt.controller.torrent.dto.response.TorrentScrapeResponseDTO;
+import com.github.example.pt.controller.torrent.dto.response.TorrentSearchResultResponseDTO;
+import com.github.example.pt.controller.torrent.dto.response.TorrentUploadSuccessResponseDTO;
+import com.github.example.pt.controller.torrent.form.TorrentUploadForm;
+import com.github.example.pt.entity.Category;
+import com.github.example.pt.entity.PromotionPolicy;
+import com.github.example.pt.entity.Tag;
+import com.github.example.pt.entity.Thanks;
+import com.github.example.pt.entity.Torrent;
+import com.github.example.pt.entity.User;
+import com.github.example.pt.exception.APIGenericException;
+import com.github.example.pt.exception.EmptyTorrentFileException;
+import com.github.example.pt.exception.InvalidTorrentVersionException;
+import com.github.example.pt.exception.TorrentException;
+import com.github.example.pt.objects.ResponsePojo;
+import com.github.example.pt.service.AuthenticationService;
+import com.github.example.pt.service.CategoryService;
+import com.github.example.pt.service.PeerService;
+import com.github.example.pt.service.PromotionService;
+import com.github.example.pt.service.SettingService;
+import com.github.example.pt.service.TagService;
+import com.github.example.pt.service.ThanksService;
+import com.github.example.pt.service.TorrentService;
+import com.github.example.pt.service.TransferHistoryService;
+import com.github.example.pt.service.UserService;
+import com.github.example.pt.util.IPUtil;
+import com.github.example.pt.util.TorrentParser;
+import com.github.example.pt.util.URLEncodeUtil;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.NotNull;
+import org.owasp.html.PolicyFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.data.domain.Page;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import static com.github.example.pt.exception.APIErrorCode.*;
+
+@RestController
+@RequestMapping("/torrent")
+@Slf4j
+public class TorrentController {
+ @Autowired
+ private TorrentService torrentService;
+ @Autowired
+ private CategoryService categoryService;
+ @Autowired
+ private PromotionService promotionService;
+ @Autowired
+ private UserService userService;
+ @Autowired
+ @Qualifier("torrentsDirectory")
+ private File torrentsDirectory;
+ @Autowired
+ private HttpServletRequest request;
+ @Autowired
+ private TransferHistoryService transferHistoryService;
+ @Autowired
+ private SettingService settingService;
+ @Autowired
+ private PolicyFactory sanitizeFactory;
+ @Autowired
+ private TagService tagService;
+ @Autowired
+ private AuthenticationService authenticationService;
+ @Autowired
+ private PeerService peerService;
+ @Autowired
+ private ThanksService thanksService;
+
+ @PostMapping("/upload")
+ //@SaCheckPermission("torrent:upload")
+ @Transactional
+ public ResponseEntity<ResponsePojo> upload(TorrentUploadForm form) throws IOException {
+ if (StringUtils.isEmpty(form.getTitle())) {
+ throw new APIGenericException(MISSING_PARAMETERS, "You must provide a title.");
+ }
+ if (StringUtils.isEmpty(form.getDescription())) {
+ throw new APIGenericException(MISSING_PARAMETERS, "You must provide a description.");
+ }
+ if (form.getFile() == null || form.getFile().isEmpty()) {
+ throw new APIGenericException(INVALID_TORRENT_FILE, "You must provide a valid torrent file.");
+ }
+ form.setDescription(sanitizeFactory.sanitize(form.getDescription()));
+ User user = userService.getUser(StpUtil.getLoginIdAsLong());
+ Category category = categoryService.getCategory(form.getCategory());
+ PromotionPolicy promotionPolicy = promotionService.getDefaultPromotionPolicy();
+ SiteBasicConfig siteBasicConfig = settingService.get(SiteBasicConfig.getConfigKey(), SiteBasicConfig.class);
+ if (category == null) {
+ throw new APIGenericException(INVALID_CATEGORY, "Specified category not exists.");
+ }
+ if (user == null) {
+ throw new IllegalStateException("User cannot be null at this time");
+ }
+ String publisher = user.getUsername();
+ String publisherUrl = siteBasicConfig.getSiteBaseURL() + "/user/" + user.getId();
+ if (form.isAnonymous()) {
+ StpUtil.checkPermission("torrent:publish_anonymous");
+ publisher = "Anonymous";
+ publisherUrl = siteBasicConfig.getSiteBaseURL();
+ }
+ List<Tag> tags = new ArrayList<>();
+ for (String tag : form.getTag()) {
+ Tag t = tagService.getTag(tag);
+ tags.add(Objects.requireNonNullElseGet(t, () -> tagService.save(new Tag(0, tag))));
+ }
+ try {
+ TorrentParser parser = new TorrentParser(form.getFile().getBytes(), true);
+ parser.rewriteForTracker(siteBasicConfig.getSiteName(), publisher, publisherUrl);
+ String infoHash = parser.getInfoHash();
+ if (torrentService.getTorrent(infoHash) != null) {
+ throw new APIGenericException(TORRENT_ALREADY_EXISTS, "The torrent's info_hash has been exists on this tracker.");
+ }
+ Files.write(new File(torrentsDirectory, infoHash + ".torrent").toPath(), parser.save());
+ Torrent torrent = new Torrent(0, infoHash, user, form.getTitle(),
+ form.getSubtitle(), parser.getTorrentFilesSize(),
+ Timestamp.from(Instant.now()), Timestamp.from(Instant.now()),
+ StpUtil.hasPermission("torrent:bypass_review"), form.isAnonymous(), category,
+ promotionPolicy, form.getDescription(), tags);
+ torrent = torrentService.save(torrent);
+ return ResponseEntity.ok().body(new TorrentUploadSuccessResponseDTO(torrent.getId(), parser.getInfoHash(), form.getFile()));
+ } catch (EmptyTorrentFileException e) {
+ throw new APIGenericException(INVALID_TORRENT_FILE, "This torrent is empty.");
+ } catch (InvalidTorrentVersionException e) {
+ throw new APIGenericException(INVALID_TORRENT_FILE, "V2 Torrent are not supported.");
+ } catch (TorrentException e) {
+ throw new APIGenericException(INVALID_TORRENT_FILE, e.getClass().getSimpleName() + ":" + e.getMessage());
+ }
+ }
+
+ @PostMapping("/search")
+ // @SaCheckPermission("torrent:search")
+ public TorrentSearchResultResponseDTO search(@RequestBody SearchTorrentRequestDTO searchRequestDTO) {
+ searchRequestDTO.setEntriesPerPage(Math.min(searchRequestDTO.getEntriesPerPage(), 300));
+ Page<Torrent> torrents = torrentService.search(searchRequestDTO);
+ return new TorrentSearchResultResponseDTO(torrents.getTotalElements(), torrents.getTotalPages(), torrents.getContent());
+ }
+
+ @GetMapping("/view/{info_hash}")
+ @SaCheckPermission("torrent:view")
+ public TorrentInfoResponseDTO view(@PathVariable("info_hash") String infoHash) {
+ Torrent torrent = torrentService.getTorrent(infoHash);
+ if (torrent == null) {
+ throw new APIGenericException(TORRENT_NOT_EXISTS, "This torrent not registered on this tracker");
+ }
+ return new TorrentInfoResponseDTO(torrent);
+ }
+
+ @PostMapping("/scrape")
+ // @SaCheckPermission("torrent:scrape")
+ public TorrentScrapeResponseDTO scrape(@RequestBody TorrentScrapeRequestDTO scrapeRequestDTO) {
+ if (scrapeRequestDTO.getTorrents() == null) {
+ throw new APIGenericException(MISSING_PARAMETERS, "You must provide a list of info_hash");
+ }
+ Map<String, ScrapeContainerDTO> scrapes = new HashMap<>();
+ Map<String, List<TransferHistoryDTO>> details = new HashMap<>();
+ for (String infoHash : scrapeRequestDTO.getTorrents()) {
+ Torrent torrent = torrentService.getTorrent(infoHash);
+ if (torrent == null) {
+ continue;
+ }
+ TransferHistoryService.PeerStatus peerStatus = transferHistoryService.getPeerStatus(torrent);
+ scrapes.put(infoHash, new ScrapeContainerDTO(peerStatus.downloaded(), peerStatus.complete(), peerStatus.incomplete(), peerStatus.downloaders()));
+ details.put(infoHash, transferHistoryService.getTransferHistory(torrent).stream().map(TransferHistoryDTO::new).toList());
+ }
+ return new TorrentScrapeResponseDTO(scrapes, details);
+ }
+
+ @PutMapping("/thanks/{info_hash}")
+ @SaCheckPermission("torrent:thanks")
+ @Transactional
+ public HttpEntity<?> sayThanks(@PathVariable("info_hash") String infoHash) {
+ User user = userService.getUser(StpUtil.getLoginIdAsLong());
+ Torrent torrent = torrentService.getTorrent(infoHash);
+ if (torrent == null) {
+ throw new APIGenericException(TORRENT_NOT_EXISTS, "This torrent not registered on this tracker");
+ }
+ if (thanksService.sayThanks(torrent, user)) {
+ return ResponseEntity.ok().build();
+ } else {
+ throw new APIGenericException(YOU_ALREADY_THANKED_THIS_TORRENT, "You have already expressed your thanks for the current Torrent");
+ }
+ }
+
+ @GetMapping("/thanks/{info_hash}")
+ @SaCheckPermission("torrent:view")
+ public ThanksResponseDTO queryThanks(@PathVariable("info_hash") String infoHash) {
+ Torrent torrent = torrentService.getTorrent(infoHash);
+ if (torrent == null) {
+ throw new APIGenericException(TORRENT_NOT_EXISTS, "This torrent not registered on this tracker");
+ }
+ List<Thanks> thanks = thanksService.getLast25ThanksByTorrent(torrent);
+ long thanksAmount = thanksService.countThanksForTorrent(torrent);
+ return new ThanksResponseDTO(thanksAmount, thanks.stream().map(t -> new UserTinyResponseDTO(t.getUser())).toList());
+ }
+
+ @GetMapping("/download/{info_hash}")
+ public HttpEntity<?> download(@PathVariable("info_hash") String infoHash, @RequestParam @NotNull Map<String, String> params) throws IOException, TorrentException {
+ User user;
+ if (params.containsKey("passkey")) {
+ user = authenticationService.authenticate(params.get("passkey"), IPUtil.getRequestIp(request));
+ } else {
+ user = userService.getUser(StpUtil.getLoginIdAsLong());
+ }
+ if (user == null) {
+ throw new APIGenericException(AUTHENTICATION_FAILED, "Neither passkey or session provided.");
+ }
+// if (!StpUtil.hasPermission(user.getId(), "torrent:download")) {
+// throw new NotPermissionException("torrent:download");
+// }
+ TrackerConfig trackerConfig = settingService.get(TrackerConfig.getConfigKey(), TrackerConfig.class);
+ if (StringUtils.isEmpty(infoHash)) {
+ throw new APIGenericException(MISSING_PARAMETERS, "You must provide a info_hash.");
+ }
+ Torrent torrent = torrentService.getTorrent(infoHash);
+ if (torrent == null) {
+ throw new APIGenericException(TORRENT_NOT_EXISTS, "This torrent not registered on this tracker");
+ }
+ if (torrent.isUnderReview()) {
+ if (!StpUtil.hasPermission(user.getId(), "torrent:download_review")) {
+ throw new NotPermissionException("torrent:download_review");
+ }
+ }
+ File torrentFile = new File(torrentsDirectory, infoHash + ".torrent");
+ if (!torrentFile.exists()) {
+ throw new APIGenericException(TORRENT_FILE_MISSING, "This torrent's file are missing on this tracker, please contact with system administrator.");
+ }
+ TorrentParser parser = new TorrentParser(Files.readAllBytes(torrentFile.toPath()), false);
+ parser.rewriteForUser(trackerConfig.getTrackerURL(), user.getPasskey()/*torrent.getUser().getPasskey()*/, user);
+ log.info("passkey: {}", torrent.getUser().getPasskey());
+ String fileName = "[" + trackerConfig.getTorrentPrefix() + "] " + torrent.getTitle() + ".torrent";
+ HttpHeaders header = new HttpHeaders();
+ log.info(fileName);
+ header.set(HttpHeaders.CONTENT_TYPE, "application/x-bittorrent");
+ header.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + URLEncodeUtil.urlEncode(fileName, false));
+ return new HttpEntity<>(parser.save(), header);
+ }
+}
diff --git a/src/main/java/com/github/example/pt/controller/torrent/dto/request/SearchTorrentRequestDTO.java b/src/main/java/com/github/example/pt/controller/torrent/dto/request/SearchTorrentRequestDTO.java
new file mode 100644
index 0000000..c3a65f9
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/torrent/dto/request/SearchTorrentRequestDTO.java
@@ -0,0 +1,29 @@
+package com.github.example.pt.controller.torrent.dto.request;
+
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.PositiveOrZero;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Validated
+public class SearchTorrentRequestDTO {
+ private String keyword;
+ private List<String> promotion;
+ private List<String> category;
+
+ private List<String> tag;
+ private boolean includeDeadTorrent;
+ @PositiveOrZero
+ private int page;
+ @Min(1)
+ @Max(300)
+ private int entriesPerPage;
+}
diff --git a/src/main/java/com/github/example/pt/controller/torrent/dto/request/ThanksResponseDTO.java b/src/main/java/com/github/example/pt/controller/torrent/dto/request/ThanksResponseDTO.java
new file mode 100644
index 0000000..edd3ef6
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/torrent/dto/request/ThanksResponseDTO.java
@@ -0,0 +1,16 @@
+package com.github.example.pt.controller.torrent.dto.request;
+
+import com.github.example.pt.controller.dto.response.UserTinyResponseDTO;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+@Validated
+public class ThanksResponseDTO {
+ private long thanks;
+ private List<UserTinyResponseDTO> users;
+}
diff --git a/src/main/java/com/github/example/pt/controller/torrent/dto/request/TorrentScrapeRequestDTO.java b/src/main/java/com/github/example/pt/controller/torrent/dto/request/TorrentScrapeRequestDTO.java
new file mode 100644
index 0000000..62f8488
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/torrent/dto/request/TorrentScrapeRequestDTO.java
@@ -0,0 +1,18 @@
+package com.github.example.pt.controller.torrent.dto.request;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+@Validated
+public class TorrentScrapeRequestDTO {
+ @NotNull
+ private List<String> torrents;
+ @NotNull
+ private boolean details;
+}
diff --git a/src/main/java/com/github/example/pt/controller/torrent/dto/response/TorrentScrapeResponseDTO.java b/src/main/java/com/github/example/pt/controller/torrent/dto/response/TorrentScrapeResponseDTO.java
new file mode 100644
index 0000000..ed17e9e
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/torrent/dto/response/TorrentScrapeResponseDTO.java
@@ -0,0 +1,18 @@
+package com.github.example.pt.controller.torrent.dto.response;
+
+import com.github.example.pt.controller.dto.response.ScrapeContainerDTO;
+import com.github.example.pt.controller.dto.response.TransferHistoryDTO;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.List;
+import java.util.Map;
+
+@AllArgsConstructor
+@Data
+@Validated
+public class TorrentScrapeResponseDTO {
+ private Map<String, ScrapeContainerDTO> scrapes;
+ private Map<String, List<TransferHistoryDTO>> details;
+}
diff --git a/src/main/java/com/github/example/pt/controller/torrent/dto/response/TorrentSearchResultResponseDTO.java b/src/main/java/com/github/example/pt/controller/torrent/dto/response/TorrentSearchResultResponseDTO.java
new file mode 100644
index 0000000..4d7ddfc
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/torrent/dto/response/TorrentSearchResultResponseDTO.java
@@ -0,0 +1,25 @@
+package com.github.example.pt.controller.torrent.dto.response;
+
+import com.github.example.pt.controller.dto.response.TorrentBasicResponseDTO;
+import com.github.example.pt.entity.Torrent;
+import com.github.example.pt.objects.ResponsePojo;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.List;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+@Validated
+public class TorrentSearchResultResponseDTO extends ResponsePojo {
+ private long totalElements;
+ private int totalPages;
+ private List<TorrentBasicResponseDTO> torrents;
+
+ public TorrentSearchResultResponseDTO(long totalElements, int totalPages, List<Torrent> torrents) {
+ this.totalElements = totalElements;
+ this.totalPages = totalPages;
+ this.torrents = torrents.stream().map(TorrentBasicResponseDTO::new).toList();
+ }
+}
diff --git a/src/main/java/com/github/example/pt/controller/torrent/dto/response/TorrentUploadSuccessResponseDTO.java b/src/main/java/com/github/example/pt/controller/torrent/dto/response/TorrentUploadSuccessResponseDTO.java
new file mode 100644
index 0000000..146e0e7
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/torrent/dto/response/TorrentUploadSuccessResponseDTO.java
@@ -0,0 +1,35 @@
+package com.github.example.pt.controller.torrent.dto.response;
+
+import com.github.example.pt.objects.ResponsePojo;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.multipart.MultipartFile;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+@Validated
+public class TorrentUploadSuccessResponseDTO extends ResponsePojo {
+ private final String originalName;
+ private final String name;
+ private final long size;
+ private final String infoHash;
+ private final long id;
+
+ public TorrentUploadSuccessResponseDTO(long id, @NotNull String infoHash, @Nullable MultipartFile multipartFile) {
+ super(0);
+ this.id = id;
+ this.infoHash = infoHash;
+ if (multipartFile != null) {
+ this.originalName = multipartFile.getOriginalFilename();
+ this.name = multipartFile.getName();
+ this.size = multipartFile.getSize();
+ } else {
+ this.originalName = "null";
+ this.name = "null";
+ this.size = 0;
+ }
+ }
+}
diff --git a/src/main/java/com/github/example/pt/controller/torrent/form/TorrentUploadForm.java b/src/main/java/com/github/example/pt/controller/torrent/form/TorrentUploadForm.java
new file mode 100644
index 0000000..5aa3bc8
--- /dev/null
+++ b/src/main/java/com/github/example/pt/controller/torrent/form/TorrentUploadForm.java
@@ -0,0 +1,29 @@
+package com.github.example.pt.controller.torrent.form;
+
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@Validated
+public class TorrentUploadForm {
+ @NotEmpty
+ private String title;
+ private String subtitle;
+ @NotEmpty
+ private String description;
+ @NotEmpty
+ private String category;
+ private List<String> tag;
+ private boolean anonymous;
+ @NotNull
+ private MultipartFile file;
+}
diff --git a/src/main/java/com/github/example/pt/crontask/PeersCleanup.java b/src/main/java/com/github/example/pt/crontask/PeersCleanup.java
new file mode 100644
index 0000000..198691a
--- /dev/null
+++ b/src/main/java/com/github/example/pt/crontask/PeersCleanup.java
@@ -0,0 +1,23 @@
+package com.github.example.pt.crontask;
+
+import com.github.example.pt.service.PeerService;
+import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
+import org.quartz.JobExecutionContext;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.quartz.QuartzJobBean;
+import org.springframework.stereotype.Component;
+
+@Component
+@Slf4j
+public class PeersCleanup extends QuartzJobBean {
+ @Autowired
+ private PeerService peerService;
+
+ @Override
+ public void executeInternal(@NotNull JobExecutionContext context) {
+ log.info("Executing the peers cleanup...");
+ int count = peerService.cleanup();
+ log.info("Peers cleanup complete! Purged {} peers.", count);
+ }
+}
diff --git a/src/main/java/com/github/example/pt/entity/Category.java b/src/main/java/com/github/example/pt/entity/Category.java
new file mode 100644
index 0000000..470964d
--- /dev/null
+++ b/src/main/java/com/github/example/pt/entity/Category.java
@@ -0,0 +1,38 @@
+package com.github.example.pt.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Index;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "categories",
+ uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"slug"})
+ },
+ indexes = {
+ @Index(columnList = "slug")
+ }
+)
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Category {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", nullable = false, updatable = false)
+ private long id;
+ @Column(name = "slug", nullable = false, updatable = false)
+ private String slug;
+ @Column(name = "name", nullable = false)
+ private String name;
+ @Column(name = "icon", nullable = false)
+ private String icon;
+}
diff --git a/src/main/java/com/github/example/pt/entity/Exam.java b/src/main/java/com/github/example/pt/entity/Exam.java
new file mode 100644
index 0000000..9db7de4
--- /dev/null
+++ b/src/main/java/com/github/example/pt/entity/Exam.java
@@ -0,0 +1,47 @@
+package com.github.example.pt.entity;
+
+import com.fasterxml.jackson.annotation.JsonBackReference;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Index;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.PrimaryKeyJoinColumn;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.sql.Timestamp;
+
+@Entity
+@Table(name = "exams",
+ uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"user_id"})
+ },
+ indexes = {
+ @Index(columnList = "user_id")
+ }
+)
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Exam {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", nullable = false, updatable = false)
+ private long id;
+ @PrimaryKeyJoinColumn
+ @ManyToOne
+ @JsonBackReference
+ private ExamPlan examPlan;
+ @PrimaryKeyJoinColumn
+ @ManyToOne
+ @JsonBackReference
+ private User user;
+ @Column(name = "end_at", nullable = false)
+ private Timestamp endAt;
+}
diff --git a/src/main/java/com/github/example/pt/entity/ExamPlan.java b/src/main/java/com/github/example/pt/entity/ExamPlan.java
new file mode 100644
index 0000000..b91c82c
--- /dev/null
+++ b/src/main/java/com/github/example/pt/entity/ExamPlan.java
@@ -0,0 +1,50 @@
+package com.github.example.pt.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Index;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "exam_plans",
+ uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"slug"})
+ },
+ indexes = {
+ @Index(columnList = "slug")
+ }
+)
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ExamPlan {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", nullable = false, updatable = false)
+ private long id;
+ @Column(name = "slug", nullable = false, updatable = false)
+ private String slug;
+ @Column(name = "displayName", nullable = false)
+ private String displayName;
+ @Column(name = "uploaded", nullable = false)
+ private long uploaded;
+ @Column(name = "downloaded", nullable = false)
+ private long downloaded;
+ @Column(name = "karma", nullable = false)
+ private double karma;
+ @Column(name = "seeds", nullable = false)
+ private long seeds;
+ @Column(name = "seedingTime", nullable = false)
+ private long seedingTime;
+ @Column(name = "shareRatio", nullable = false)
+ private double shareRatio;
+ @Column(name = "duration", nullable = false)
+ private long duration;
+}
diff --git a/src/main/java/com/github/example/pt/entity/LoginHistory.java b/src/main/java/com/github/example/pt/entity/LoginHistory.java
new file mode 100644
index 0000000..5d97cc4
--- /dev/null
+++ b/src/main/java/com/github/example/pt/entity/LoginHistory.java
@@ -0,0 +1,41 @@
+package com.github.example.pt.entity;
+
+import com.fasterxml.jackson.annotation.JsonBackReference;
+import com.github.example.pt.type.LoginType;
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.sql.Timestamp;
+
+@Entity
+@Table(name = "login_history",
+ indexes = {
+ @Index(columnList = "time")
+ }
+)
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class LoginHistory {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", nullable = false, updatable = false)
+ private long id;
+ @PrimaryKeyJoinColumn(name = "user_id", referencedColumnName = "id")
+ @ManyToOne
+ @JsonBackReference
+ private User user;
+ @Column(name = "time", nullable = false, updatable = false)
+ private Timestamp loginTime;
+ @Enumerated(EnumType.STRING)
+ @Column(name = "type", nullable = false, updatable = false)
+ private LoginType loginType;
+ @Column(name = "ip_address", updatable = false)
+ private String ipAddress;
+ @Column(name = "user_agent", updatable = false)
+ private String userAgent;
+ @Column(name = "location", updatable = false)
+ private String location;
+}
diff --git a/src/main/java/com/github/example/pt/entity/Peer.java b/src/main/java/com/github/example/pt/entity/Peer.java
new file mode 100644
index 0000000..f2099c7
--- /dev/null
+++ b/src/main/java/com/github/example/pt/entity/Peer.java
@@ -0,0 +1,74 @@
+package com.github.example.pt.entity;
+
+import com.fasterxml.jackson.annotation.JsonBackReference;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Index;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.PrimaryKeyJoinColumn;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hibernate.annotations.DynamicUpdate;
+
+import java.sql.Timestamp;
+
+@Entity
+@Table(name = "peers",
+ uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"ip", "port", "info_hash"})
+ },
+ indexes = {
+ @Index(columnList = "update_at")
+ }
+)
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@DynamicUpdate
+public class Peer {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", nullable = false, updatable = false)
+ private long id;
+ @Column(name = "ip", nullable = false, updatable = false)
+ private String ip;
+ @Column(name = "port", nullable = false, updatable = false)
+ private int port;
+ @Column(name = "info_hash", nullable = false, updatable = false)
+ private String infoHash;
+ @Column(name = "peer_id", nullable = false)
+ private String peerId;
+ @Column(name = "user_agent", nullable = false)
+ private String userAgent;
+ @Column(name = "uploaded", nullable = false)
+ private long uploaded;
+ @Column(name = "downloaded", nullable = false)
+ private long downloaded;
+ @Column(name = "to_go", nullable = false)
+ private long left;
+ @Column(name = "seeder", nullable = false)
+ private boolean seeder;
+ @Column(name = "partial_seeder", nullable = false)
+ private boolean partialSeeder;
+ @Column(name = "passkey", nullable = false)
+ private String passKey;
+ @Column(name = "update_at", nullable = false)
+ private Timestamp updateAt;
+ @Column(name = "seeding_time", nullable = false)
+ private long seedingTime;
+ @Column(name = "upload_speed", nullable = false)
+ private long uploadSpeed;
+ @Column(name = "download_speed", nullable = false)
+ private long downloadSpeed;
+ @PrimaryKeyJoinColumn
+ @ManyToOne
+ @JsonBackReference
+ private User user;
+
+}
diff --git a/src/main/java/com/github/example/pt/entity/Permission.java b/src/main/java/com/github/example/pt/entity/Permission.java
new file mode 100644
index 0000000..06278a2
--- /dev/null
+++ b/src/main/java/com/github/example/pt/entity/Permission.java
@@ -0,0 +1,33 @@
+package com.github.example.pt.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "permissions",
+ uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"slug"})
+ }
+)
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Permission {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", nullable = false, updatable = false)
+ private long id;
+ @Column(name = "slug", nullable = false, updatable = false)
+ private String slug;
+ @Column(name = "def", nullable = false)
+ private boolean def;
+}
diff --git a/src/main/java/com/github/example/pt/entity/PromotionPolicy.java b/src/main/java/com/github/example/pt/entity/PromotionPolicy.java
new file mode 100644
index 0000000..e129c4d
--- /dev/null
+++ b/src/main/java/com/github/example/pt/entity/PromotionPolicy.java
@@ -0,0 +1,48 @@
+package com.github.example.pt.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "promotion_policies",
+ uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"slug"}),
+ }
+)
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class PromotionPolicy {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", nullable = false, updatable = false)
+ private long id;
+
+ @Column(name = "slug", nullable = false)
+ private String slug;
+
+ @Column(name = "display_name", nullable = false)
+ private String displayName;
+
+ @Column(name = "upload_ratio")
+ private double uploadRatio;
+ @Column(name = "download_ratio")
+ private double downloadRatio;
+
+ public double applyUploadRatio(double upload) {
+ return upload * uploadRatio;
+ }
+
+ public double applyDownloadRatio(double download) {
+ return download * downloadRatio;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/entity/SeedBox.java b/src/main/java/com/github/example/pt/entity/SeedBox.java
new file mode 100644
index 0000000..7da5104
--- /dev/null
+++ b/src/main/java/com/github/example/pt/entity/SeedBox.java
@@ -0,0 +1,37 @@
+package com.github.example.pt.entity;
+
+import com.fasterxml.jackson.annotation.JsonBackReference;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.PrimaryKeyJoinColumn;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import lombok.Data;
+
+@Entity
+@Table(name = "seedbox",
+ uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"address"})
+ }
+)
+
+@Data
+public class SeedBox {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", nullable = false, updatable = false)
+ private long id;
+ @Column(name = "address", nullable = false)
+ private String address;
+ @ManyToOne
+ @PrimaryKeyJoinColumn
+ @JsonBackReference
+ private PromotionPolicy downloadMultiplier;
+ @ManyToOne
+ @JsonBackReference
+ private PromotionPolicy uploadMultiplier;
+}
diff --git a/src/main/java/com/github/example/pt/entity/SettingEntity.java b/src/main/java/com/github/example/pt/entity/SettingEntity.java
new file mode 100644
index 0000000..27983da
--- /dev/null
+++ b/src/main/java/com/github/example/pt/entity/SettingEntity.java
@@ -0,0 +1,33 @@
+package com.github.example.pt.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "settings",
+ uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"setting_key"})
+ }
+)
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class SettingEntity {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", nullable = false, updatable = false)
+ private long id;
+ @Column(name = "setting_key", nullable = false)
+ private String key;
+ @Column(name = "setting_value", nullable = false, columnDefinition = "mediumtext")
+ private String value;
+}
diff --git a/src/main/java/com/github/example/pt/entity/Tag.java b/src/main/java/com/github/example/pt/entity/Tag.java
new file mode 100644
index 0000000..304cd8e
--- /dev/null
+++ b/src/main/java/com/github/example/pt/entity/Tag.java
@@ -0,0 +1,30 @@
+package com.github.example.pt.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "tags",
+ uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"name"})
+ }
+)
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Tag {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", nullable = false, updatable = false)
+ private long id;
+ @Column(name = "name", nullable = false, updatable = false)
+ private String name;
+}
diff --git a/src/main/java/com/github/example/pt/entity/Thanks.java b/src/main/java/com/github/example/pt/entity/Thanks.java
new file mode 100644
index 0000000..9baf6a8
--- /dev/null
+++ b/src/main/java/com/github/example/pt/entity/Thanks.java
@@ -0,0 +1,39 @@
+package com.github.example.pt.entity;
+
+import com.fasterxml.jackson.annotation.JsonBackReference;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.PrimaryKeyJoinColumn;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "thanks",
+ uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"user_id", "torrent_id"})
+ }
+)
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Thanks {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", nullable = false, updatable = false)
+ private long id;
+ @PrimaryKeyJoinColumn
+ @ManyToOne
+ @JsonBackReference
+ private User user;
+ @PrimaryKeyJoinColumn
+ @ManyToOne
+ @JsonBackReference
+ private Torrent torrent;
+}
diff --git a/src/main/java/com/github/example/pt/entity/Torrent.java b/src/main/java/com/github/example/pt/entity/Torrent.java
new file mode 100644
index 0000000..4820401
--- /dev/null
+++ b/src/main/java/com/github/example/pt/entity/Torrent.java
@@ -0,0 +1,83 @@
+package com.github.example.pt.entity;
+
+import com.fasterxml.jackson.annotation.JsonBackReference;
+import com.fasterxml.jackson.annotation.JsonManagedReference;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Index;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.PrimaryKeyJoinColumn;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hibernate.annotations.DynamicUpdate;
+
+import java.sql.Timestamp;
+import java.util.List;
+
+@Entity
+@Table(name = "torrents",
+ uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"info_hash"})
+ },
+ indexes = {
+ @Index(columnList = "title"),
+ @Index(columnList = "sub_title"),
+ @Index(columnList = "promotion_policy_id")
+ }
+)
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@DynamicUpdate
+public class Torrent {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", nullable = false, updatable = false)
+ private long id;
+ @Column(name = "info_hash", nullable = false, updatable = false)
+ private String infoHash;
+ @PrimaryKeyJoinColumn
+ @ManyToOne
+ @JsonBackReference
+ private User user;
+ @Column(name = "title", nullable = false)
+ private String title;
+ @Column(name = "sub_title", nullable = false)
+ private String subTitle;
+ @Column(name = "size", nullable = false, updatable = false)
+ private long size;
+ @Column(name = "created_at", nullable = false, updatable = false)
+ private Timestamp createdAt;
+ @Column(name = "updated_at", nullable = false)
+ private Timestamp updatedAt;
+ @Column(name = "under_review", nullable = false)
+ private boolean underReview;
+ @Column(name = "anonymous", nullable = false)
+ private boolean anonymous;
+ @ManyToOne
+ @JsonBackReference
+ @PrimaryKeyJoinColumn
+ private Category category;
+ @ManyToOne
+ @PrimaryKeyJoinColumn
+ @JsonBackReference
+ private PromotionPolicy promotionPolicy;
+ @Column(name = "description", nullable = false, columnDefinition = "mediumtext")
+ private String description;
+ @OneToMany
+ @PrimaryKeyJoinColumn
+ @JsonManagedReference
+ private List<Tag> tag;
+
+ public String getUsernameWithAnonymous(boolean canSeeAnonymous) {
+ return canSeeAnonymous || !anonymous ? user.getUsername() : "Anonymous";
+ }
+}
diff --git a/src/main/java/com/github/example/pt/entity/TransferHistory.java b/src/main/java/com/github/example/pt/entity/TransferHistory.java
new file mode 100644
index 0000000..8d67ad9
--- /dev/null
+++ b/src/main/java/com/github/example/pt/entity/TransferHistory.java
@@ -0,0 +1,58 @@
+package com.github.example.pt.entity;
+
+import com.fasterxml.jackson.annotation.JsonBackReference;
+import com.github.example.pt.type.AnnounceEventType;
+import com.github.example.pt.type.AnnounceEventTypeConverter;
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.sql.Timestamp;
+
+@Entity
+@Table(name = "transfer_history",
+ uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"user_id", "torrent_id"})
+ }
+)
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class TransferHistory {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", nullable = false, updatable = false)
+ private long id;
+ @PrimaryKeyJoinColumn
+ @ManyToOne
+ @JsonBackReference
+ private User user;
+ @PrimaryKeyJoinColumn
+ @ManyToOne
+ @JsonBackReference
+ private Torrent torrent;
+ @Column(name = "to_go", nullable = false)
+ private long left;
+ @Column(name = "started_at", nullable = false, updatable = false)
+ private Timestamp startedAt;
+ @Column(name = "updated_at", nullable = false)
+ private Timestamp updatedAt;
+ @Column(name = "uploaded", nullable = false)
+ private long uploaded;
+ @Column(name = "downloaded", nullable = false)
+ private long downloaded;
+ @Column(name = "actual_uploaded", nullable = false)
+ private long actualUploaded;
+ @Column(name = "actual_downloaded", nullable = false)
+ private long actualDownloaded;
+ @Column(name = "upload_speed", nullable = false)
+ private long uploadSpeed;
+ @Column(name = "download_speed", nullable = false)
+ private long downloadSpeed;
+ @Column(name = "last_event", nullable = false)
+ @Convert(converter = AnnounceEventTypeConverter.class)
+ private AnnounceEventType lastEvent;
+ @Column(name = "have_complete_history", nullable = false)
+ private boolean haveCompleteHistory;
+}
diff --git a/src/main/java/com/github/example/pt/entity/User.java b/src/main/java/com/github/example/pt/entity/User.java
new file mode 100644
index 0000000..60a0a6c
--- /dev/null
+++ b/src/main/java/com/github/example/pt/entity/User.java
@@ -0,0 +1,92 @@
+package com.github.example.pt.entity;
+
+import com.fasterxml.jackson.annotation.JsonBackReference;
+import com.github.example.pt.type.PrivacyLevel;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.PrimaryKeyJoinColumn;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.hibernate.annotations.DynamicUpdate;
+import org.hibernate.annotations.SelectBeforeUpdate;
+
+import java.math.BigDecimal;
+import java.sql.Timestamp;
+
+@Entity
+@Table(name = "users",
+ uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"username"}),
+ @UniqueConstraint(columnNames = {"email"}),
+ @UniqueConstraint(columnNames = {"passkey"}),
+ }
+)
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@DynamicUpdate
+@SelectBeforeUpdate
+public class User {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", nullable = false, updatable = false)
+ private long id;
+ @Column(name = "email", nullable = false)
+ private String email;
+ @Column(name = "password", nullable = false)
+ private String passwordHash;
+ @Column(name = "username", nullable = false)
+ private String username;
+ @PrimaryKeyJoinColumn(name = "group", referencedColumnName = "id")
+ @ManyToOne
+ @JsonBackReference
+ private UserGroup group;
+ @Column(name = "passkey", nullable = false)
+ private String passkey;
+ @Column(name = "create_at", nullable = false, updatable = false)
+ private Timestamp createdAt;
+ @Column(name = "avatar", nullable = false)
+ private String avatar;
+ @Column(name = "custom_title", nullable = false)
+ private String customTitle;
+ @Column(name = "signature", nullable = false)
+ private String signature;
+ @Column(name = "language", nullable = false)
+ private String language;
+ @Column(name = "download_bandwidth", nullable = false)
+ private String downloadBandwidth;
+ @Column(name = "upload_bandwidth", nullable = false)
+ private String uploadBandwidth;
+ @Column(name = "downloaded", nullable = false)
+ private long downloaded;
+ @Column(name = "uploaded", nullable = false)
+ private long uploaded;
+ @Column(name = "real_downloaded", nullable = false)
+ private long realDownloaded;
+ @Column(name = "real_uploaded", nullable = false)
+ private long realUploaded;
+ @Column(name = "isp", nullable = false)
+ private String isp;
+ @Column(name = "karma", nullable = false)
+ private BigDecimal karma;
+ @Column(name = "invite_slot", nullable = false)
+ private int inviteSlot;
+ @Column(name = "seeding_time", nullable = false)
+ private long seedingTime;
+
+ @Column(name = "personal_access_token", nullable = false)
+ private String personalAccessToken;
+ @Column(name = "privacy_level", nullable = false)
+ private PrivacyLevel privacyLevel;
+
+ public UserGroup getGroup() {
+ return group;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/entity/UserGroup.java b/src/main/java/com/github/example/pt/entity/UserGroup.java
new file mode 100644
index 0000000..f9ec921
--- /dev/null
+++ b/src/main/java/com/github/example/pt/entity/UserGroup.java
@@ -0,0 +1,62 @@
+package com.github.example.pt.entity;
+
+import com.fasterxml.jackson.annotation.JsonBackReference;
+import com.fasterxml.jackson.annotation.JsonManagedReference;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.PrimaryKeyJoinColumn;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Entity
+@Table(name = "user_groups",
+ uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"slug"}),
+ }
+)
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class UserGroup {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id", nullable = false, updatable = false)
+ private long id;
+ @Column(name = "slug", nullable = false, updatable = false)
+ private String slug;
+ @Column(name = "display_name", nullable = false)
+ private String displayName;
+ @OneToMany
+ @PrimaryKeyJoinColumn
+ @JsonManagedReference
+ private List<Permission> permissionEntities;
+
+ @PrimaryKeyJoinColumn
+ @ManyToOne
+ @JsonBackReference
+ private PromotionPolicy promotionPolicy;
+
+// @PrimaryKeyJoinColumn
+// @Cascade(org.hibernate.annotations.CascadeType.SAVE_UPDATE)
+// @OneToMany(fetch = FetchType.EAGER)
+// private List<UserGroupEntity> inherited;
+
+ public boolean hasPermission(String permission) {
+ for (Permission perm : permissionEntities) {
+ if (perm.getSlug().equals(permission)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/exception/APIErrorCode.java b/src/main/java/com/github/example/pt/exception/APIErrorCode.java
new file mode 100644
index 0000000..06d9bef
--- /dev/null
+++ b/src/main/java/com/github/example/pt/exception/APIErrorCode.java
@@ -0,0 +1,43 @@
+package com.github.example.pt.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.HttpStatusCode;
+
+public enum APIErrorCode {
+ MISSING_PARAMETERS(1, HttpStatus.BAD_REQUEST),
+ USER_NOT_FOUND(2, HttpStatus.NOT_FOUND),
+ AUTHENTICATION_FAILED(3, HttpStatus.FORBIDDEN),
+ REQUIRED_AUTHENTICATION(4, HttpStatus.UNAUTHORIZED),
+ INVALID_TORRENT_FILE(5, HttpStatus.NOT_ACCEPTABLE),
+ TORRENT_ALREADY_EXISTS(6, HttpStatus.CONFLICT),
+ TORRENT_NOT_EXISTS(7, HttpStatus.NOT_FOUND),
+ TORRENT_FILE_MISSING(8, HttpStatus.INTERNAL_SERVER_ERROR),
+ INVALID_CATEGORY(9, HttpStatus.NOT_FOUND),
+ EMAIL_ALREADY_IN_USAGE(10, HttpStatus.CONFLICT),
+ USERNAME_ALREADY_IN_USAGE(11, HttpStatus.CONFLICT),
+ TOO_MANY_FAILED_AUTHENTICATION_ATTEMPTS(12, HttpStatus.TOO_MANY_REQUESTS),
+ MAX_UPLOAD_SIZE_EXCEEDED(13, HttpStatus.PAYLOAD_TOO_LARGE),
+ YOU_ALREADY_THANKED_THIS_TORRENT(14, HttpStatus.NOT_MODIFIED);
+
+ private final int code;
+ private final HttpStatusCode statusCode;
+
+ APIErrorCode(int code, HttpStatus statusCode) {
+ this.code = code;
+ this.statusCode = statusCode;
+ }
+
+ APIErrorCode(int code, HttpStatusCode statusCode) {
+ this.code = code;
+ this.statusCode = statusCode;
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+
+ public HttpStatusCode getStatusCode() {
+ return statusCode;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/exception/APIGenericException.java b/src/main/java/com/github/example/pt/exception/APIGenericException.java
new file mode 100644
index 0000000..03e6099
--- /dev/null
+++ b/src/main/java/com/github/example/pt/exception/APIGenericException.java
@@ -0,0 +1,40 @@
+package com.github.example.pt.exception;
+
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.annotation.Immutable;
+import org.springframework.http.HttpStatusCode;
+
+import java.io.Serializable;
+
+@Immutable
+public class APIGenericException extends RuntimeException implements Serializable {
+ private final APIErrorCode error;
+ private final String message;
+
+ public APIGenericException(@NotNull APIErrorCode error, @NotNull String message) {
+ this.error = error;
+ this.message = message;
+ }
+
+ public APIGenericException(@NotNull APIErrorCode error) {
+ this.error = error;
+ this.message = error.name();
+ }
+
+ public int getError() {
+ return error.getCode();
+ }
+
+ public String getErrorText() {
+ return error.name();
+ }
+
+ @NotNull
+ public String getMessage() {
+ return message;
+ }
+
+ public HttpStatusCode getStatusCode() {
+ return error.getStatusCode();
+ }
+}
diff --git a/src/main/java/com/github/example/pt/exception/AnnounceBusyException.java b/src/main/java/com/github/example/pt/exception/AnnounceBusyException.java
new file mode 100644
index 0000000..94f056b
--- /dev/null
+++ b/src/main/java/com/github/example/pt/exception/AnnounceBusyException.java
@@ -0,0 +1,7 @@
+package com.github.example.pt.exception;
+
+public class AnnounceBusyException extends RetryableAnnounceException {
+ public AnnounceBusyException() {
+ super("Server is busy for handling announce, try again later", 30);
+ }
+}
diff --git a/src/main/java/com/github/example/pt/exception/AnnounceException.java b/src/main/java/com/github/example/pt/exception/AnnounceException.java
new file mode 100644
index 0000000..e1c1eeb
--- /dev/null
+++ b/src/main/java/com/github/example/pt/exception/AnnounceException.java
@@ -0,0 +1,7 @@
+package com.github.example.pt.exception;
+
+public class AnnounceException extends Exception {
+ public AnnounceException(String reason) {
+ super(reason);
+ }
+}
diff --git a/src/main/java/com/github/example/pt/exception/BadConfigException.java b/src/main/java/com/github/example/pt/exception/BadConfigException.java
new file mode 100644
index 0000000..7d46381
--- /dev/null
+++ b/src/main/java/com/github/example/pt/exception/BadConfigException.java
@@ -0,0 +1,7 @@
+package com.github.example.pt.exception;
+
+public class BadConfigException extends RuntimeException {
+ public BadConfigException() {
+ super("This tracker have a bad configuration in the database. Please contact the administrator.");
+ }
+}
diff --git a/src/main/java/com/github/example/pt/exception/BrowserReadableAnnounceException.java b/src/main/java/com/github/example/pt/exception/BrowserReadableAnnounceException.java
new file mode 100644
index 0000000..a2aff94
--- /dev/null
+++ b/src/main/java/com/github/example/pt/exception/BrowserReadableAnnounceException.java
@@ -0,0 +1,7 @@
+package com.github.example.pt.exception;
+
+public class BrowserReadableAnnounceException extends Exception {
+ public BrowserReadableAnnounceException(String reason) {
+ super(reason);
+ }
+}
diff --git a/src/main/java/com/github/example/pt/exception/EmptyTorrentFileException.java b/src/main/java/com/github/example/pt/exception/EmptyTorrentFileException.java
new file mode 100644
index 0000000..58d39f5
--- /dev/null
+++ b/src/main/java/com/github/example/pt/exception/EmptyTorrentFileException.java
@@ -0,0 +1,8 @@
+package com.github.example.pt.exception;
+
+public class EmptyTorrentFileException extends TorrentException {
+
+ public EmptyTorrentFileException() {
+ super("Torrent files tree are empty!");
+ }
+}
diff --git a/src/main/java/com/github/example/pt/exception/FixedAnnounceException.java b/src/main/java/com/github/example/pt/exception/FixedAnnounceException.java
new file mode 100644
index 0000000..eb7fa4a
--- /dev/null
+++ b/src/main/java/com/github/example/pt/exception/FixedAnnounceException.java
@@ -0,0 +1,9 @@
+package com.github.example.pt.exception;
+
+import org.jetbrains.annotations.NotNull;
+
+public class FixedAnnounceException extends AnnounceException {
+ public FixedAnnounceException(@NotNull String reason) {
+ super(reason);
+ }
+}
diff --git a/src/main/java/com/github/example/pt/exception/InvalidAnnounceException.java b/src/main/java/com/github/example/pt/exception/InvalidAnnounceException.java
new file mode 100644
index 0000000..85b2796
--- /dev/null
+++ b/src/main/java/com/github/example/pt/exception/InvalidAnnounceException.java
@@ -0,0 +1,9 @@
+package com.github.example.pt.exception;
+
+import org.jetbrains.annotations.NotNull;
+
+public class InvalidAnnounceException extends FixedAnnounceException {
+ public InvalidAnnounceException(@NotNull String reason) {
+ super(reason);
+ }
+}
diff --git a/src/main/java/com/github/example/pt/exception/InvalidTorrentFileException.java b/src/main/java/com/github/example/pt/exception/InvalidTorrentFileException.java
new file mode 100644
index 0000000..8e8a254
--- /dev/null
+++ b/src/main/java/com/github/example/pt/exception/InvalidTorrentFileException.java
@@ -0,0 +1,8 @@
+package com.github.example.pt.exception;
+
+public class InvalidTorrentFileException extends TorrentException {
+
+ public InvalidTorrentFileException(String reason) {
+ super(reason);
+ }
+}
diff --git a/src/main/java/com/github/example/pt/exception/InvalidTorrentPiecesException.java b/src/main/java/com/github/example/pt/exception/InvalidTorrentPiecesException.java
new file mode 100644
index 0000000..356a4e9
--- /dev/null
+++ b/src/main/java/com/github/example/pt/exception/InvalidTorrentPiecesException.java
@@ -0,0 +1,14 @@
+package com.github.example.pt.exception;
+
+public class InvalidTorrentPiecesException extends TorrentException {
+ private final int piecesLength;
+
+ public InvalidTorrentPiecesException(int piecesLength) {
+ super("Invalid torrent pieces: " + piecesLength);
+ this.piecesLength = piecesLength;
+ }
+
+ public int getPiecesLength() {
+ return piecesLength;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/exception/InvalidTorrentVerifyException.java b/src/main/java/com/github/example/pt/exception/InvalidTorrentVerifyException.java
new file mode 100644
index 0000000..3c4f2d2
--- /dev/null
+++ b/src/main/java/com/github/example/pt/exception/InvalidTorrentVerifyException.java
@@ -0,0 +1,33 @@
+package com.github.example.pt.exception;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class InvalidTorrentVerifyException extends TorrentException {
+ private final String field;
+ private final String exceptedType;
+ private final String actualType;
+
+ public InvalidTorrentVerifyException(@NotNull String field, @NotNull Class<?> excepted, @Nullable Object actual) {
+ super("The field " + field + " are unexpected. Excepted type: " + excepted.getName() + ", Actual type: " + (actual == null ? "null" : actual.getClass().getName()));
+ this.field = field;
+ this.exceptedType = excepted.getName();
+ this.actualType = actual == null ? "null" : actual.getClass().getName();
+
+ }
+
+ @NotNull
+ public String getActualType() {
+ return actualType;
+ }
+
+ @NotNull
+ public String getExceptedType() {
+ return exceptedType;
+ }
+
+ @NotNull
+ public String getField() {
+ return field;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/exception/InvalidTorrentVersionException.java b/src/main/java/com/github/example/pt/exception/InvalidTorrentVersionException.java
new file mode 100644
index 0000000..5c51ef6
--- /dev/null
+++ b/src/main/java/com/github/example/pt/exception/InvalidTorrentVersionException.java
@@ -0,0 +1,8 @@
+package com.github.example.pt.exception;
+
+public class InvalidTorrentVersionException extends TorrentException {
+
+ public InvalidTorrentVersionException(String reason) {
+ super(reason);
+ }
+}
diff --git a/src/main/java/com/github/example/pt/exception/RESTException.java b/src/main/java/com/github/example/pt/exception/RESTException.java
new file mode 100644
index 0000000..c2859c6
--- /dev/null
+++ b/src/main/java/com/github/example/pt/exception/RESTException.java
@@ -0,0 +1,4 @@
+package com.github.example.pt.exception;
+
+public class RESTException extends Throwable {
+}
diff --git a/src/main/java/com/github/example/pt/exception/RetryableAnnounceException.java b/src/main/java/com/github/example/pt/exception/RetryableAnnounceException.java
new file mode 100644
index 0000000..c872025
--- /dev/null
+++ b/src/main/java/com/github/example/pt/exception/RetryableAnnounceException.java
@@ -0,0 +1,16 @@
+package com.github.example.pt.exception;
+
+import org.jetbrains.annotations.NotNull;
+
+public class RetryableAnnounceException extends AnnounceException {
+ private final int retryIn;
+
+ public RetryableAnnounceException(@NotNull String reason, int retryIn) {
+ super(reason);
+ this.retryIn = retryIn;
+ }
+
+ public int getRetryIn() {
+ return retryIn;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/exception/TorrentException.java b/src/main/java/com/github/example/pt/exception/TorrentException.java
new file mode 100644
index 0000000..1706a4d
--- /dev/null
+++ b/src/main/java/com/github/example/pt/exception/TorrentException.java
@@ -0,0 +1,7 @@
+package com.github.example.pt.exception;
+
+public class TorrentException extends Exception {
+ public TorrentException(String reason) {
+ super(reason);
+ }
+}
diff --git a/src/main/java/com/github/example/pt/exception/TrackerException.java b/src/main/java/com/github/example/pt/exception/TrackerException.java
new file mode 100644
index 0000000..2625dac
--- /dev/null
+++ b/src/main/java/com/github/example/pt/exception/TrackerException.java
@@ -0,0 +1,18 @@
+package com.github.example.pt.exception;
+
+public class TrackerException extends Exception {
+ public TrackerException() {
+ }
+
+ public TrackerException(String s) {
+ super(s);
+ }
+
+ public TrackerException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public TrackerException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/com/github/example/pt/listener/SaTokenEventListener.java b/src/main/java/com/github/example/pt/listener/SaTokenEventListener.java
new file mode 100644
index 0000000..5f3499f
--- /dev/null
+++ b/src/main/java/com/github/example/pt/listener/SaTokenEventListener.java
@@ -0,0 +1,63 @@
+package com.github.example.pt.listener;
+
+import cn.dev33.satoken.listener.SaTokenListener;
+import cn.dev33.satoken.stp.SaLoginModel;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SaTokenEventListener implements SaTokenListener {
+ @Override
+ public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
+
+ }
+
+ @Override
+ public void doLogout(String loginType, Object loginId, String tokenValue) {
+
+ }
+
+ @Override
+ public void doKickout(String loginType, Object loginId, String tokenValue) {
+
+ }
+
+ @Override
+ public void doReplaced(String loginType, Object loginId, String tokenValue) {
+
+ }
+
+ @Override
+ public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {
+
+ }
+
+ @Override
+ public void doUntieDisable(String loginType, Object loginId, String service) {
+
+ }
+
+ @Override
+ public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {
+
+ }
+
+ @Override
+ public void doCloseSafe(String loginType, String tokenValue, String service) {
+
+ }
+
+ @Override
+ public void doCreateSession(String id) {
+
+ }
+
+ @Override
+ public void doLogoutSession(String id) {
+
+ }
+
+ @Override
+ public void doRenewTimeout(String tokenValue, Object loginId, long timeout) {
+
+ }
+}
diff --git a/src/main/java/com/github/example/pt/objects/ResponsePojo.java b/src/main/java/com/github/example/pt/objects/ResponsePojo.java
new file mode 100644
index 0000000..f766734
--- /dev/null
+++ b/src/main/java/com/github/example/pt/objects/ResponsePojo.java
@@ -0,0 +1,24 @@
+package com.github.example.pt.objects;
+
+import lombok.Getter;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Getter
+public abstract class ResponsePojo implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ private final long code;
+
+ public ResponsePojo(long code) {
+ this.code = code;
+ }
+ public ResponsePojo() {
+ this.code = 0;
+ }
+
+ public long getCode() {
+ return code;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/ptApplication.java b/src/main/java/com/github/example/pt/ptApplication.java
new file mode 100644
index 0000000..d7e1546
--- /dev/null
+++ b/src/main/java/com/github/example/pt/ptApplication.java
@@ -0,0 +1,27 @@
+package com.github.example.pt;
+
+import cn.dev33.satoken.SaManager;
+import com.github.example.pt.util.PasswordHash;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import java.util.UUID;
+
+@SpringBootApplication
+@EnableCaching
+@EnableTransactionManagement
+@Slf4j
+public class ptApplication {
+
+ public static void main(String[] args) {
+ System.out.println("Hash: " + PasswordHash.hash("testtest"));
+ System.out.println(new UUID(1, 0).toString().replace("_", ""));
+ System.out.println(new UUID(2, 0).toString().replace("_", ""));
+ SpringApplication.run(ptApplication.class, args);
+ SaManager.getConfig();
+ }
+
+}
diff --git a/src/main/java/com/github/example/pt/redisentity/RedisLoginAttempt.java b/src/main/java/com/github/example/pt/redisentity/RedisLoginAttempt.java
new file mode 100644
index 0000000..aed7a7b
--- /dev/null
+++ b/src/main/java/com/github/example/pt/redisentity/RedisLoginAttempt.java
@@ -0,0 +1,18 @@
+package com.github.example.pt.redisentity;
+
+import lombok.Data;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.redis.core.RedisHash;
+import org.springframework.data.redis.core.index.Indexed;
+
+@RedisHash(value = "login_attempt", timeToLive = 900)
+@Data
+public class RedisLoginAttempt {
+ @Id
+ @Indexed
+ private String ip;
+ @Indexed
+ private long attempts;
+ @Indexed
+ private long lastAttempt;
+}
diff --git a/src/main/java/com/github/example/pt/redisrepository/RedisLoginAttemptRepository.java b/src/main/java/com/github/example/pt/redisrepository/RedisLoginAttemptRepository.java
new file mode 100644
index 0000000..6c4c1b3
--- /dev/null
+++ b/src/main/java/com/github/example/pt/redisrepository/RedisLoginAttemptRepository.java
@@ -0,0 +1,13 @@
+package com.github.example.pt.redisrepository;
+
+import com.github.example.pt.redisentity.RedisLoginAttempt;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+@Repository
+public interface RedisLoginAttemptRepository extends CrudRepository<RedisLoginAttempt, String> {
+ Optional<RedisLoginAttempt> findByIp(@NotNull String ip);
+ void deleteByIp(@NotNull String ip);
+}
diff --git a/src/main/java/com/github/example/pt/repository/CategoryRepository.java b/src/main/java/com/github/example/pt/repository/CategoryRepository.java
new file mode 100644
index 0000000..a8178c3
--- /dev/null
+++ b/src/main/java/com/github/example/pt/repository/CategoryRepository.java
@@ -0,0 +1,12 @@
+package com.github.example.pt.repository;
+
+import com.github.example.pt.entity.Category;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+@Repository
+public interface CategoryRepository extends CrudRepository<Category, Long> {
+ Optional<Category> findBySlug(@NotNull String slug);
+}
diff --git a/src/main/java/com/github/example/pt/repository/ExamPlanRepository.java b/src/main/java/com/github/example/pt/repository/ExamPlanRepository.java
new file mode 100644
index 0000000..04dc94b
--- /dev/null
+++ b/src/main/java/com/github/example/pt/repository/ExamPlanRepository.java
@@ -0,0 +1,19 @@
+package com.github.example.pt.repository;
+
+import com.github.example.pt.entity.ExamPlan;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+@Repository
+public interface ExamPlanRepository extends CrudRepository<ExamPlan, Long> {
+ Optional<ExamPlan> findBySlug(String code);
+
+ Optional<ExamPlan> findByDisplayName(String displayName);
+
+ @Override
+ @NotNull
+ Optional<ExamPlan> findById(@NotNull Long id);
+
+}
diff --git a/src/main/java/com/github/example/pt/repository/ExamRepository.java b/src/main/java/com/github/example/pt/repository/ExamRepository.java
new file mode 100644
index 0000000..c771345
--- /dev/null
+++ b/src/main/java/com/github/example/pt/repository/ExamRepository.java
@@ -0,0 +1,10 @@
+package com.github.example.pt.repository;
+
+import com.github.example.pt.entity.Exam;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface ExamRepository extends CrudRepository<Exam, Long> {
+
+}
diff --git a/src/main/java/com/github/example/pt/repository/LoginHistoryRepository.java b/src/main/java/com/github/example/pt/repository/LoginHistoryRepository.java
new file mode 100644
index 0000000..38f631b
--- /dev/null
+++ b/src/main/java/com/github/example/pt/repository/LoginHistoryRepository.java
@@ -0,0 +1,20 @@
+package com.github.example.pt.repository;
+
+import com.github.example.pt.entity.LoginHistory;
+import com.github.example.pt.entity.User;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+import java.sql.Timestamp;
+import java.util.List;
+
+@Repository
+public interface LoginHistoryRepository extends CrudRepository<LoginHistory, Long> {
+ List<LoginHistory> findAllByIpAddress(@NotNull String ipAddress);
+ List<LoginHistory> findAllByUserAgent(@NotNull String userAgent);
+
+ List<LoginHistory> findAllByLoginTimeBetween(@NotNull Timestamp start, @NotNull Timestamp end);
+
+ List<LoginHistory> findAllByUserOrderByLoginTimeDesc(@NotNull User user);
+}
diff --git a/src/main/java/com/github/example/pt/repository/PeersRepository.java b/src/main/java/com/github/example/pt/repository/PeersRepository.java
new file mode 100644
index 0000000..8c474df
--- /dev/null
+++ b/src/main/java/com/github/example/pt/repository/PeersRepository.java
@@ -0,0 +1,25 @@
+package com.github.example.pt.repository;
+
+import com.github.example.pt.entity.Peer;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+public interface PeersRepository extends CrudRepository<Peer, Long> {
+ Optional<Peer> findByIpAndPortAndInfoHashIgnoreCase(@NotNull String ip, int port, @NotNull String infoHash);
+
+ Optional<Peer> findByPeerIdAndInfoHashIgnoreCase(@NotNull String peerId, @NotNull String infoHash);
+
+ List<Peer> findPeersByInfoHashIgnoreCaseOrderByUpdateAtDesc(@NotNull String infoHash, @NotNull Pageable singlePage);
+
+ //List<PeerEntity> findPeersByUserId(long userId);
+ List<Peer> findAllByUpdateAtIsLessThan(@NotNull Timestamp timestamp);
+
+ void deletePeerByInfoHashIgnoreCaseAndPeerId(String infoHash, String peerId);
+}
diff --git a/src/main/java/com/github/example/pt/repository/PermissionRepository.java b/src/main/java/com/github/example/pt/repository/PermissionRepository.java
new file mode 100644
index 0000000..4b59bf2
--- /dev/null
+++ b/src/main/java/com/github/example/pt/repository/PermissionRepository.java
@@ -0,0 +1,13 @@
+package com.github.example.pt.repository;
+
+import com.github.example.pt.entity.Permission;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface PermissionRepository extends CrudRepository<Permission, Long> {
+ Optional<Permission> findBySlug(@NotNull String code);
+}
diff --git a/src/main/java/com/github/example/pt/repository/PromotionPolicyRepository.java b/src/main/java/com/github/example/pt/repository/PromotionPolicyRepository.java
new file mode 100644
index 0000000..413c7b4
--- /dev/null
+++ b/src/main/java/com/github/example/pt/repository/PromotionPolicyRepository.java
@@ -0,0 +1,12 @@
+package com.github.example.pt.repository;
+
+import com.github.example.pt.entity.PromotionPolicy;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface PromotionPolicyRepository extends CrudRepository<PromotionPolicy, Long> {
+ Optional<PromotionPolicy> findPromotionPolicyBySlug(String slug);
+}
diff --git a/src/main/java/com/github/example/pt/repository/SettingRepository.java b/src/main/java/com/github/example/pt/repository/SettingRepository.java
new file mode 100644
index 0000000..48ea353
--- /dev/null
+++ b/src/main/java/com/github/example/pt/repository/SettingRepository.java
@@ -0,0 +1,14 @@
+package com.github.example.pt.repository;
+
+import com.github.example.pt.entity.SettingEntity;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface SettingRepository extends CrudRepository<SettingEntity, Long> {
+ Optional<SettingEntity> findByKey(String key);
+
+ void deleteByKey(String key);
+}
diff --git a/src/main/java/com/github/example/pt/repository/TagRepository.java b/src/main/java/com/github/example/pt/repository/TagRepository.java
new file mode 100644
index 0000000..947e9b7
--- /dev/null
+++ b/src/main/java/com/github/example/pt/repository/TagRepository.java
@@ -0,0 +1,12 @@
+package com.github.example.pt.repository;
+
+import com.github.example.pt.entity.Tag;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+@Repository
+public interface TagRepository extends CrudRepository<Tag, Long> {
+ Optional<Tag> findByName(@NotNull String name);
+}
diff --git a/src/main/java/com/github/example/pt/repository/ThanksRepository.java b/src/main/java/com/github/example/pt/repository/ThanksRepository.java
new file mode 100644
index 0000000..bce8c34
--- /dev/null
+++ b/src/main/java/com/github/example/pt/repository/ThanksRepository.java
@@ -0,0 +1,19 @@
+package com.github.example.pt.repository;
+
+import com.github.example.pt.entity.Thanks;
+import com.github.example.pt.entity.Torrent;
+import com.github.example.pt.entity.User;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface ThanksRepository extends CrudRepository<Thanks, Long> {
+ boolean existsByTorrentAndUser(Torrent torrent, User user);
+
+ long countAllByTorrent(Torrent torrent);
+
+ List<Thanks> getThanksByTorrentOrderByIdDesc(Torrent torrent, Pageable pageable);
+}
diff --git a/src/main/java/com/github/example/pt/repository/TorrentRepository.java b/src/main/java/com/github/example/pt/repository/TorrentRepository.java
new file mode 100644
index 0000000..94bac28
--- /dev/null
+++ b/src/main/java/com/github/example/pt/repository/TorrentRepository.java
@@ -0,0 +1,39 @@
+package com.github.example.pt.repository;
+
+import com.github.example.pt.entity.Category;
+import com.github.example.pt.entity.Torrent;
+import com.github.example.pt.entity.User;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+public interface TorrentRepository extends JpaRepository<Torrent, Long>, JpaSpecificationExecutor<Torrent>{
+ Optional<Torrent> findByInfoHashIgnoreCase(@NotNull String infoHash);
+
+ @NotNull
+ List<Torrent> findAllByUserIdOrderByIdDesc(long userId);
+
+ @NotNull
+ List<Torrent> findAllByTitle(@NotNull String title);
+
+ @NotNull
+ List<Torrent> findAllByUnderReviewIs(boolean is);
+
+ @NotNull
+ List<Torrent> findAllByAnonymousIs(boolean is);
+
+ @NotNull
+ List<Torrent> findAllByUserOrderByIdDesc(@NotNull User user);
+
+ @NotNull
+ List<Torrent> findAllByCategoryOrderByIdDesc(@NotNull Category category);
+
+ Page<Torrent> searchByTitleLikeIgnoreCase(@NotNull String keyword, @NotNull Pageable pageable);
+}
diff --git a/src/main/java/com/github/example/pt/repository/TransferHistoryRepository.java b/src/main/java/com/github/example/pt/repository/TransferHistoryRepository.java
new file mode 100644
index 0000000..132c6ec
--- /dev/null
+++ b/src/main/java/com/github/example/pt/repository/TransferHistoryRepository.java
@@ -0,0 +1,22 @@
+package com.github.example.pt.repository;
+
+import com.github.example.pt.entity.Torrent;
+import com.github.example.pt.entity.TransferHistory;
+import com.github.example.pt.entity.User;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.Optional;
+@Repository
+public interface TransferHistoryRepository extends CrudRepository<TransferHistory, Long> {
+ Optional<TransferHistory> findByUserAndTorrent(@NotNull User user, @NotNull Torrent torrent);
+
+ List<TransferHistory> findAllByUserOrderByUpdatedAt(@NotNull User user);
+
+ List<TransferHistory> findAllByTorrentOrderByUpdatedAt(@NotNull Torrent torrent);
+
+ List<TransferHistory> findAllByTorrentAndUpdatedAtAfterOrderByUpdatedAt(@NotNull Torrent torrent, Timestamp after);
+}
diff --git a/src/main/java/com/github/example/pt/repository/UserGroupRepository.java b/src/main/java/com/github/example/pt/repository/UserGroupRepository.java
new file mode 100644
index 0000000..e3e0215
--- /dev/null
+++ b/src/main/java/com/github/example/pt/repository/UserGroupRepository.java
@@ -0,0 +1,15 @@
+package com.github.example.pt.repository;
+
+import com.github.example.pt.entity.UserGroup;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface UserGroupRepository extends CrudRepository<UserGroup, Long> {
+ @Override
+ @NotNull
+ Optional<UserGroup> findById(@NotNull Long aLong);
+}
diff --git a/src/main/java/com/github/example/pt/repository/UserRepository.java b/src/main/java/com/github/example/pt/repository/UserRepository.java
new file mode 100644
index 0000000..30b29eb
--- /dev/null
+++ b/src/main/java/com/github/example/pt/repository/UserRepository.java
@@ -0,0 +1,27 @@
+package com.github.example.pt.repository;
+
+import com.github.example.pt.entity.User;
+import com.github.example.pt.entity.UserGroup;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+public interface UserRepository extends CrudRepository<User, Long> {
+ Optional<User> findByUsername(@NotNull String username);
+
+ Optional<User> findByEmail(@NotNull String email);
+
+ Optional<User> findByPasskeyIgnoreCase(@NotNull String passkey);
+
+ Optional<User> findByPersonalAccessTokenIgnoreCase(@NotNull String personalAccessToken);
+
+ List<User> findByEmailContains(@NotNull String emailPart);
+
+ List<User> findByUsernameContains(@NotNull String usernamePart);
+
+ List<User> findByGroup(@NotNull UserGroup group);
+}
diff --git a/src/main/java/com/github/example/pt/service/AnnouncePerformanceMonitorService.java b/src/main/java/com/github/example/pt/service/AnnouncePerformanceMonitorService.java
new file mode 100644
index 0000000..5f102c5
--- /dev/null
+++ b/src/main/java/com/github/example/pt/service/AnnouncePerformanceMonitorService.java
@@ -0,0 +1,64 @@
+package com.github.example.pt.service;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import org.springframework.stereotype.Component;
+
+import java.time.Instant;
+import java.util.UUID;
+
+@Component
+
+public class AnnouncePerformanceMonitorService {
+ private final Instant startTime = Instant.now();
+ private final Cache<UUID, Long> announceTimes = CacheBuilder
+ .newBuilder()
+ .maximumSize(1000)
+ .build();
+ private final Cache<UUID, Long> announceJobTimes = CacheBuilder
+ .newBuilder()
+ .maximumSize(1000)
+ .build();
+ private long handled = 0;
+
+ public void recordStats(long ns) {
+ announceTimes.put(UUID.randomUUID(), ns);
+ handled++;
+ }
+
+ public void recordJobStats(long ns) {
+ announceJobTimes.put(UUID.randomUUID(), ns);
+ }
+
+ public double avgNs() {
+ return announceTimes.asMap().values().stream().mapToLong(Long::longValue).average().orElse(0);
+ }
+
+ public double avgJobMs() {
+ return avgJobNs() / 1000000;
+ }
+
+ public double avgJobNs() {
+ return announceJobTimes.asMap().values().stream().mapToLong(Long::longValue).average().orElse(0);
+ }
+
+ public double avgMs() {
+ return avgNs() / 1000000;
+ }
+
+ public long getHandled() {
+ return handled;
+ }
+
+ public Cache<UUID, Long> getAnnounceTimes() {
+ return announceTimes;
+ }
+
+ public Cache<UUID, Long> getAnnounceJobTimes() {
+ return announceJobTimes;
+ }
+
+ public Instant getStartTime() {
+ return startTime;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/service/AnnounceService.java b/src/main/java/com/github/example/pt/service/AnnounceService.java
new file mode 100644
index 0000000..8e5f7e6
--- /dev/null
+++ b/src/main/java/com/github/example/pt/service/AnnounceService.java
@@ -0,0 +1,183 @@
+package com.github.example.pt.service;
+
+import com.github.example.pt.entity.Peer;
+import com.github.example.pt.entity.Torrent;
+import com.github.example.pt.entity.TransferHistory;
+import com.github.example.pt.entity.User;
+import com.github.example.pt.exception.AnnounceBusyException;
+import com.github.example.pt.type.AnnounceEventType;
+import com.github.example.pt.util.ExecutorUtil;
+import com.github.example.pt.util.HibernateSessionUtil;
+import jakarta.persistence.EntityManagerFactory;
+import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.NoSuchElementException;
+import java.util.concurrent.BlockingDeque;
+import java.util.concurrent.LinkedBlockingDeque;
+
+@Service
+@Slf4j
+public class AnnounceService {
+ private final BlockingDeque<AnnounceTask> taskQueue = new LinkedBlockingDeque<>(40960);
+ @Autowired
+ private ExecutorUtil executor;
+ @Autowired
+ private UserService userService;
+ @Autowired
+ private PeerService peerService;
+ @Autowired
+ private TorrentService torrentService;
+ @Autowired
+ private EntityManagerFactory entityManagerFactory;
+ @Autowired
+ private AnnouncePerformanceMonitorService monitorService;
+ @Autowired
+ private TransferHistoryService transferHistoryService;
+ @Autowired
+ private HibernateSessionUtil sessionUtil;
+
+ public AnnounceService() {
+ Thread announceHandleThread = new Thread(() -> {
+ while (true) {
+ try {
+ AnnounceTask announceTask = taskQueue.take();
+ boolean participate = sessionUtil.bindToThread();
+ try {
+ long start = System.nanoTime();
+ handleTask(announceTask);
+ monitorService.recordJobStats(System.nanoTime() - start);
+ } catch (Exception e) {
+ log.error("Error handling task: {}", announceTask, e);
+ } finally {
+ sessionUtil.closeFromThread(participate);
+ }
+ } catch (InterruptedException e) {
+ log.error("Announce handling thread interrupted", e);
+ }
+ }
+ });
+ announceHandleThread.setName("Announce Handling");
+ announceHandleThread.setDaemon(true);
+ announceHandleThread.start();
+ }
+
+ public void schedule(@NotNull AnnounceTask announceTask) throws AnnounceBusyException {
+ if (!this.taskQueue.offer(announceTask))
+ throw new AnnounceBusyException();
+ }
+
+ @Transactional
+ void handleTask(AnnounceTask task) throws NoSuchElementException {
+ // Multi-threaded
+ User user = userService.getUser(task.userId());
+ if (user == null) throw new IllegalStateException("User not exists anymore");
+ Torrent torrent = torrentService.getTorrent(task.torrentId());
+ if (torrent == null) throw new IllegalStateException("Torrent not exists anymore");
+ // Register torrent into peers
+ Peer peer = peerService.getPeer(task.ip(), task.port(), task.infoHash());
+ if (peer == null) {
+ peer = createNewPeer(task, user);
+ }
+ long lastUploaded = peer.getUploaded();
+ long lastDownload = peer.getDownloaded();
+ long uploadedOffset = task.uploaded() - lastUploaded;
+ long downloadedOffset = task.downloaded() - lastDownload;
+ Timestamp lastUpdateAt = torrent.getUpdatedAt();
+ if (uploadedOffset < 0) uploadedOffset = task.uploaded();
+ if (downloadedOffset < 0) downloadedOffset = task.downloaded();
+ long announceInterval = Instant.now().toEpochMilli() - lastUpdateAt.toInstant().toEpochMilli();
+ peer.setUploaded(task.uploaded() + uploadedOffset);
+ peer.setDownloaded(task.downloaded() + downloadedOffset);
+ peer.setLeft(task.left());
+ peer.setSeeder(task.left() == 0);
+ peer.setUpdateAt(Timestamp.from(Instant.now()));
+ peer.setSeedingTime(peer.getSeedingTime() + (Instant.now().toEpochMilli() - lastUpdateAt.toInstant().toEpochMilli()));
+ peer.setPartialSeeder(task.event() == AnnounceEventType.PAUSED);
+ // Update user peer speed
+ long bytesPerSecondUploading = uploadedOffset / (announceInterval / 1000);
+ long bytesPerSecondDownloading = downloadedOffset / (announceInterval / 1000);
+ peer.setUploadSpeed(bytesPerSecondUploading);
+ peer.setDownloadSpeed(bytesPerSecondDownloading);
+ peer = peerService.save(peer);
+ // Update real user data
+ user.setRealDownloaded(user.getRealDownloaded() + lastDownload);
+ user.setRealUploaded(user.getRealUploaded() + lastUploaded);
+ // Apply user promotion policy
+ long promotionUploadOffset = (long) user.getGroup().getPromotionPolicy().applyUploadRatio(lastDownload);
+ long promotionDownloadOffset = (long) user.getGroup().getPromotionPolicy().applyDownloadRatio(lastUploaded);
+ // Apply torrent promotion policy
+ promotionUploadOffset = (long) torrent.getPromotionPolicy().applyUploadRatio(promotionUploadOffset);
+ promotionDownloadOffset = (long) torrent.getPromotionPolicy().applyDownloadRatio(promotionDownloadOffset);
+ user.setUploaded(user.getUploaded() + promotionUploadOffset);
+ user.setDownloaded(user.getDownloaded() + promotionDownloadOffset);
+ user.setSeedingTime(user.getSeedingTime() + (Instant.now().toEpochMilli() - lastUpdateAt.toInstant().toEpochMilli()));
+ user = userService.save(user);
+ TransferHistory transferHistory = transferHistoryService.getTransferHistory(user, torrent);
+ if (transferHistory != null) {
+ long torrentLeft = transferHistory.getLeft();
+ if (torrentLeft != 0 && task.left() == 0) {
+ transferHistory.setHaveCompleteHistory(true);
+ }
+ transferHistory.setUpdatedAt(Timestamp.from(Instant.now()));
+ transferHistory.setLeft(task.left());
+ transferHistory.setUploaded(transferHistory.getUploaded() + promotionUploadOffset);
+ transferHistory.setDownloaded(transferHistory.getDownloaded() + promotionDownloadOffset);
+ transferHistory.setActualUploaded(transferHistory.getActualUploaded() + uploadedOffset);
+ transferHistory.setActualDownloaded(transferHistory.getActualDownloaded() + downloadedOffset);
+ transferHistory.setUploadSpeed(bytesPerSecondUploading);
+ transferHistory.setDownloadSpeed(bytesPerSecondDownloading);
+ } else {
+ transferHistory = new TransferHistory(0, user, torrent,
+ task.left(), Timestamp.from(Instant.now()),
+ Timestamp.from(Instant.now()),
+ promotionUploadOffset, promotionDownloadOffset, uploadedOffset, downloadedOffset,
+ bytesPerSecondUploading, bytesPerSecondDownloading, task.event(), false);
+ }
+ transferHistoryService.save(transferHistory);
+ torrentService.save(torrent);
+ if (task.event() == AnnounceEventType.STOPPED) {
+ if (peer.getId() != 0) {
+ peerService.delete(peer);
+ }
+ }
+
+ }
+
+
+ @NotNull
+ private Peer createNewPeer(AnnounceTask task, User user) {
+ return new Peer(
+ 0,
+ task.ip(),
+ task.port(),
+ task.infoHash(),
+ task.peerId(),
+ task.userAgent(),
+ task.uploaded(),
+ task.downloaded(),
+ task.left(),
+ task.left() == 0,
+ task.event() == AnnounceEventType.PAUSED,
+ task.passKey(),
+ Timestamp.from(Instant.now()),
+ 0, 0,
+ 0,
+ user
+ );
+ }
+
+ public record AnnounceTask(
+ @NotNull String ip, int port, @NotNull String infoHash, @NotNull String peerId,
+ long uploaded, long downloaded, long left, @NotNull AnnounceEventType event,
+ int numWant, long userId, boolean compact, boolean noPeerId,
+ boolean supportCrypto, int redundant, String userAgent, String passKey, long torrentId
+ ) {
+
+ }
+}
diff --git a/src/main/java/com/github/example/pt/service/AuthenticationService.java b/src/main/java/com/github/example/pt/service/AuthenticationService.java
new file mode 100644
index 0000000..f0ffd0d
--- /dev/null
+++ b/src/main/java/com/github/example/pt/service/AuthenticationService.java
@@ -0,0 +1,112 @@
+package com.github.example.pt.service;
+
+import com.github.example.pt.config.SecurityConfig;
+import com.github.example.pt.entity.User;
+import com.github.example.pt.exception.APIErrorCode;
+import com.github.example.pt.exception.APIGenericException;
+import com.github.example.pt.redisentity.RedisLoginAttempt;
+import com.github.example.pt.redisrepository.RedisLoginAttemptRepository;
+import com.github.example.pt.type.LoginType;
+import com.github.example.pt.util.IPUtil;
+import com.github.example.pt.util.PasswordHash;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+@Service
+@Slf4j
+public class AuthenticationService {
+ @Autowired
+ private UserService userService;
+ @Autowired
+ private LoginHistoryService loginHistoryService;
+ @Autowired
+ private HttpServletRequest request;
+ @Autowired
+ private RedisLoginAttemptRepository repository;
+ @Autowired
+ private SettingService settingService;
+
+ private SecurityConfig getSecurityConfig() {
+ return settingService.get(SecurityConfig.getConfigKey(), SecurityConfig.class);
+ }
+
+ public boolean authenticate(@NotNull User user, @NotNull String password, @Nullable String ipAddress) {
+ checkAccountLoginAttempts(ipAddress);
+ boolean verify = PasswordHash.verify(password, user.getPasswordHash());
+ if (StringUtils.isEmpty(ipAddress)) {
+ ipAddress = IPUtil.getRequestIp(request);
+ }
+ if (verify) {
+ cleanUserLoginFail(ipAddress);
+ loginHistoryService.log(user, LoginType.ACCOUNT, ipAddress, request.getHeader("User-Agent"));
+ } else {
+ markUserLoginFail(ipAddress);
+ }
+ return verify;
+ }
+
+ @Nullable
+ public User authenticate(@NotNull String passkey, @Nullable String ipAddress) {
+ checkPasskeyLoginAttempts(ipAddress);
+ User user = userService.getUserByPasskey(passkey);
+ if (StringUtils.isEmpty(ipAddress)) {
+ ipAddress = IPUtil.getRequestIp(request);
+ }
+ if (user != null) {
+ cleanUserLoginFail(ipAddress);
+ loginHistoryService.log(user, LoginType.PASSKEY, ipAddress, request.getHeader("User-Agent"));
+ } else {
+ markUserLoginFail(ipAddress);
+ }
+ return user;
+ }
+
+ public void cleanUserLoginFail(@Nullable String ip) {
+ if(ip == null) return;
+ Optional<RedisLoginAttempt> optional = repository.findByIp(ip);
+ optional.ifPresent(redisLoginAttempt -> repository.delete(redisLoginAttempt));
+ }
+
+ public long markUserLoginFail(@Nullable String ip) {
+ if(ip == null) return 0;
+ Optional<RedisLoginAttempt> optional = repository.findByIp(ip);
+ RedisLoginAttempt loginAttempt;
+ if (optional.isPresent()) {
+ loginAttempt = optional.get();
+ loginAttempt.setAttempts(loginAttempt.getAttempts() + 1);
+ } else {
+ loginAttempt = new RedisLoginAttempt();
+ loginAttempt.setIp(ip);
+ }
+ loginAttempt.setLastAttempt(System.currentTimeMillis());
+ loginAttempt = repository.save(loginAttempt);
+ return loginAttempt.getAttempts();
+ }
+
+ public void checkAccountLoginAttempts(@Nullable String ip){
+ if(ip == null) return;
+ if (getUserFail(ip) > getSecurityConfig().getMaxAuthenticationAttempts()) {
+ throw new APIGenericException(APIErrorCode.TOO_MANY_FAILED_AUTHENTICATION_ATTEMPTS, "Too many failed login attempts");
+ }
+ }
+ public void checkPasskeyLoginAttempts(@Nullable String ip){
+ if(ip == null) return;
+ if (getUserFail(ip) > getSecurityConfig().getMaxPasskeyAuthenticationAttempts()) {
+ throw new APIGenericException(APIErrorCode.TOO_MANY_FAILED_AUTHENTICATION_ATTEMPTS, "Too many failed login attempts");
+ }
+ }
+
+ public long getUserFail(@Nullable String ip) {
+ if (ip == null) return 0;
+ Optional<RedisLoginAttempt> optional = repository.findByIp(ip);
+ long attempts = optional.map(RedisLoginAttempt::getAttempts).orElse(0L);
+ return attempts;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/service/BlacklistClientService.java b/src/main/java/com/github/example/pt/service/BlacklistClientService.java
new file mode 100644
index 0000000..72686d3
--- /dev/null
+++ b/src/main/java/com/github/example/pt/service/BlacklistClientService.java
@@ -0,0 +1,52 @@
+package com.github.example.pt.service;
+
+import com.github.example.pt.exception.BrowserReadableAnnounceException;
+import com.github.example.pt.exception.FixedAnnounceException;
+import jakarta.servlet.http.HttpServletRequest;
+import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.stereotype.Repository;
+import org.springframework.stereotype.Service;
+
+import java.util.Arrays;
+
+@Service
+@Repository
+
+public class BlacklistClientService {
+ private static final String[] BROWSER_BOT_SOFTWARE_KEYWORDS = new String[]{
+ "Mozilla",
+ "Browser",
+ "Chrome",
+ "Safari",
+ "AppleWebKit",
+ "Opera",
+ "Links",
+ "Lynx",
+ "Bot",
+ "Crawler",
+ "Spider",
+ "Unknown"
+ };
+
+ public void checkClient(@NotNull HttpServletRequest request) throws FixedAnnounceException, BrowserReadableAnnounceException {
+ String ua = request.getHeader("User-Agent");
+ if (StringUtils.isEmpty(ua)) {
+ throw new FixedAnnounceException("Client didn't send user-agent to tracker server.");
+ }
+ checkBrowser(ua);
+ if (!checkAllowedClient(ua)) {
+ throw new FixedAnnounceException("Disallowed client: " + ua);
+ }
+ }
+
+ private boolean checkAllowedClient(@NotNull String ua) {
+ return true;
+ }
+
+ private void checkBrowser(@NotNull String ua) throws BrowserReadableAnnounceException {
+ if (Arrays.stream(BROWSER_BOT_SOFTWARE_KEYWORDS).map(String::toLowerCase).anyMatch(ua::contains)) {
+ throw new BrowserReadableAnnounceException("You must use a Bittorrent Client to connect this tracker.");
+ }
+ }
+}
diff --git a/src/main/java/com/github/example/pt/service/CategoryService.java b/src/main/java/com/github/example/pt/service/CategoryService.java
new file mode 100644
index 0000000..ab9c097
--- /dev/null
+++ b/src/main/java/com/github/example/pt/service/CategoryService.java
@@ -0,0 +1,39 @@
+package com.github.example.pt.service;
+
+import com.github.example.pt.entity.Category;
+import com.github.example.pt.repository.CategoryRepository;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+
+public class CategoryService {
+ @Autowired
+ private CategoryRepository repository;
+
+ @Nullable
+ public Category getCategory(@NotNull String slug) {
+ return repository.findBySlug(slug).orElse(null);
+ }
+
+ @Nullable
+ public Category getCategory(long id) {
+ return repository.findById(id).orElse(null);
+ }
+
+ public List<Category> getAllCategories() {
+ List<Category> categories = new ArrayList<>();
+ repository.findAll().forEach(categories::add);
+ return categories;
+ }
+
+ @NotNull
+ public Category save(@NotNull Category category) {
+ return repository.save(category);
+ }
+}
diff --git a/src/main/java/com/github/example/pt/service/ExamPlanService.java b/src/main/java/com/github/example/pt/service/ExamPlanService.java
new file mode 100644
index 0000000..4e22c3d
--- /dev/null
+++ b/src/main/java/com/github/example/pt/service/ExamPlanService.java
@@ -0,0 +1,26 @@
+package com.github.example.pt.service;
+
+import com.github.example.pt.entity.ExamPlan;
+import com.github.example.pt.repository.ExamPlanRepository;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+
+public class ExamPlanService {
+ @Autowired
+ private ExamPlanRepository repository;
+
+ @Nullable
+ public ExamPlan getExamPlan(long id) {
+ return repository.findById(id).orElse(null);
+ }
+
+ @Nullable
+ public ExamPlan getExamPlan(@NotNull String code) {
+ return repository.findBySlug(code).orElse(null);
+ }
+
+}
diff --git a/src/main/java/com/github/example/pt/service/ExamService.java b/src/main/java/com/github/example/pt/service/ExamService.java
new file mode 100644
index 0000000..45074d8
--- /dev/null
+++ b/src/main/java/com/github/example/pt/service/ExamService.java
@@ -0,0 +1,25 @@
+package com.github.example.pt.service;
+
+import com.github.example.pt.entity.Exam;
+import com.github.example.pt.repository.ExamRepository;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+
+public class ExamService {
+ @Autowired
+ private ExamRepository repository;
+ @Autowired
+ private UserService userService;
+ @Autowired
+ private ExamPlanService examPlanService;
+
+ @Nullable
+ public Exam getExam(long id) {
+ return repository.findById(id).orElse(null);
+ }
+
+
+}
diff --git a/src/main/java/com/github/example/pt/service/LoginHistoryService.java b/src/main/java/com/github/example/pt/service/LoginHistoryService.java
new file mode 100644
index 0000000..179d972
--- /dev/null
+++ b/src/main/java/com/github/example/pt/service/LoginHistoryService.java
@@ -0,0 +1,26 @@
+package com.github.example.pt.service;
+
+import com.github.example.pt.entity.LoginHistory;
+import com.github.example.pt.entity.User;
+import com.github.example.pt.repository.LoginHistoryRepository;
+import com.github.example.pt.type.LoginType;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.sql.Timestamp;
+import java.time.Instant;
+
+@Service
+
+public class LoginHistoryService {
+ @Autowired
+ private LoginHistoryRepository repository;
+
+ @NotNull
+ public LoginHistory log(@NotNull User user, @NotNull LoginType loginType, @NotNull String ip, @NotNull String userAgent) {
+ LoginHistory history = new LoginHistory(0, user, Timestamp.from(Instant.now()),
+ loginType, ip, userAgent, "Unknown - GeoIP not initialized");
+ return repository.save(history);
+ }
+}
diff --git a/src/main/java/com/github/example/pt/service/PeerService.java b/src/main/java/com/github/example/pt/service/PeerService.java
new file mode 100644
index 0000000..0d7dff1
--- /dev/null
+++ b/src/main/java/com/github/example/pt/service/PeerService.java
@@ -0,0 +1,63 @@
+package com.github.example.pt.service;
+
+import com.github.example.pt.entity.Peer;
+import com.github.example.pt.repository.PeersRepository;
+import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Repository;
+import org.springframework.stereotype.Service;
+
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import java.util.Locale;
+
+@Service
+@Repository
+@Slf4j
+
+public class PeerService {
+ @Autowired
+ private PeersRepository repository;
+
+ @Nullable
+ public Peer getPeer(@NotNull String ip, int port, @NotNull String infoHash) {
+ infoHash = infoHash.toLowerCase(Locale.ROOT);
+ return repository.findByIpAndPortAndInfoHashIgnoreCase(ip, port, infoHash).orElse(null);
+ }
+
+ @NotNull
+ public List<Peer> getPeers(@NotNull String infoHash, int numWant) {
+ infoHash = infoHash.toLowerCase(Locale.ROOT);
+ Pageable top = PageRequest.of(0, numWant);
+ List<Peer> entities = repository.findPeersByInfoHashIgnoreCaseOrderByUpdateAtDesc(infoHash, top);
+ return entities.stream().toList();
+ }
+
+ @NotNull
+ public Peer save(@NotNull Peer peer) {
+ peer.setInfoHash(peer.getInfoHash().toLowerCase(Locale.ROOT));
+ return repository.save(peer);
+ }
+
+ public void delete(@NotNull Peer peer) {
+ //repository.delete(convert(peer));
+ repository.deleteById(peer.getId());
+ }
+
+ public int cleanup() {
+ List<Peer> entities = repository.findAllByUpdateAtIsLessThan(
+ Timestamp.from(Instant.now().minus(90, ChronoUnit.MINUTES))
+ );
+ int count = entities.size();
+ repository.deleteAll(entities);
+ return count;
+ }
+
+
+}
diff --git a/src/main/java/com/github/example/pt/service/PermissionService.java b/src/main/java/com/github/example/pt/service/PermissionService.java
new file mode 100644
index 0000000..fb7928e
--- /dev/null
+++ b/src/main/java/com/github/example/pt/service/PermissionService.java
@@ -0,0 +1,48 @@
+package com.github.example.pt.service;
+
+import com.github.example.pt.entity.Permission;
+import com.github.example.pt.repository.PermissionRepository;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+@Service
+@Repository
+
+public class PermissionService {
+ @Autowired
+ private PermissionRepository repository;
+
+ @NotNull
+ public Permission registerPermission(@NotNull String code, boolean def) {
+ Optional<Permission> permission = repository.findBySlug(code);
+ if (permission.isPresent()) {
+ Permission entity = permission.get();
+ return new Permission(entity.getId(), entity.getSlug(), entity.isDef());
+ }
+ Permission entity = new Permission(0, code, def);
+ entity = repository.save(entity);
+ return entity;
+ }
+
+ @Nullable
+ public Permission getPermission(long id) {
+ Optional<Permission> permission = repository.findById(id);
+ return permission.orElse(null);
+ }
+
+ @Nullable
+ public Permission getPermission(@NotNull String code) {
+ Optional<Permission> permission = repository.findBySlug(code);
+ return permission.orElse(null);
+ }
+
+ @NotNull
+ public Permission save(@NotNull Permission permission) {
+ return repository.save(permission);
+ }
+}
diff --git a/src/main/java/com/github/example/pt/service/PromotionService.java b/src/main/java/com/github/example/pt/service/PromotionService.java
new file mode 100644
index 0000000..3897439
--- /dev/null
+++ b/src/main/java/com/github/example/pt/service/PromotionService.java
@@ -0,0 +1,48 @@
+package com.github.example.pt.service;
+
+import com.github.example.pt.entity.PromotionPolicy;
+import com.github.example.pt.repository.PromotionPolicyRepository;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+@Service
+
+public class PromotionService {
+ @Autowired
+ private PromotionPolicyRepository repository;
+
+ @Nullable
+ public PromotionPolicy getPromotionPolicy(long id) {
+ Optional<PromotionPolicy> entity = repository.findById(id);
+ return entity.orElse(null);
+ }
+
+ @Nullable
+ public PromotionPolicy getPromotionPolicy(@NotNull String name) {
+ Optional<PromotionPolicy> entity = repository.findPromotionPolicyBySlug(name);
+ return entity.orElse(null);
+ }
+
+ @Nullable
+ public PromotionPolicy getDefaultPromotionPolicy() {
+ return repository.findAll().iterator().next();
+ }
+
+ @NotNull
+ public List<PromotionPolicy> getAllPromotionPolicies() {
+ List<PromotionPolicy> policies = new ArrayList<>();
+ repository.findAll().forEach(policies::add);
+ return policies;
+ }
+
+ @NotNull
+ public PromotionPolicy save(@NotNull PromotionPolicy promotionPolicy) {
+ return repository.save(promotionPolicy);
+ }
+}
diff --git a/src/main/java/com/github/example/pt/service/SaTokenPermImpl.java b/src/main/java/com/github/example/pt/service/SaTokenPermImpl.java
new file mode 100644
index 0000000..b2b9f0a
--- /dev/null
+++ b/src/main/java/com/github/example/pt/service/SaTokenPermImpl.java
@@ -0,0 +1,50 @@
+package com.github.example.pt.service;
+
+import cn.dev33.satoken.exception.NotLoginException;
+import cn.dev33.satoken.stp.StpInterface;
+import com.github.example.pt.entity.Permission;
+import com.github.example.pt.entity.User;
+import com.github.example.pt.util.HibernateSessionUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+@Slf4j
+
+public class SaTokenPermImpl implements StpInterface {
+ @Autowired
+ private UserService userService;
+ @Autowired
+ private HibernateSessionUtil sessionUtil;
+
+ @Override
+ public List<String> getPermissionList(Object loginId, String loginType) {
+ boolean participate = sessionUtil.bindToThread();
+ try {
+ User user = userService.getUser(Long.parseLong(String.valueOf(loginId)));
+ if (user == null) {
+ throw new NotLoginException("You hadn't logged in yet!", loginType, "Not logged in");
+ }
+ return user.getGroup().getPermissionEntities().stream().map(Permission::getSlug).toList();
+ }finally {
+ sessionUtil.closeFromThread(participate);
+ }
+ }
+
+ @Override
+ public List<String> getRoleList(Object loginId, String loginType) {
+ boolean participate = sessionUtil.bindToThread();
+ try {
+ User user = userService.getUser(Long.parseLong(String.valueOf(loginId)));
+ if (user == null) {
+ throw new NotLoginException("You hadn't logged in yet!", loginType, "Not logged in");
+ }
+ return List.of(user.getGroup().getSlug());
+ }finally {
+ sessionUtil.closeFromThread(participate);
+ }
+ }
+}
diff --git a/src/main/java/com/github/example/pt/service/SettingService.java b/src/main/java/com/github/example/pt/service/SettingService.java
new file mode 100644
index 0000000..3218be4
--- /dev/null
+++ b/src/main/java/com/github/example/pt/service/SettingService.java
@@ -0,0 +1,78 @@
+package com.github.example.pt.service;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.example.pt.entity.SettingEntity;
+import com.github.example.pt.exception.BadConfigException;
+import com.github.example.pt.repository.SettingRepository;
+import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.lang.reflect.Method;
+import java.util.Optional;
+
+@Service
+@Slf4j
+public class SettingService {
+ @Autowired
+ private SettingRepository repository;
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @NotNull
+ public <T> T get(@NotNull String configKey, @NotNull Class<T> clazz) throws BadConfigException {
+ Optional<SettingEntity> configData = repository.findByKey(configKey);
+ if (configData.isPresent()) {
+ String data = configData.get().getValue();
+ try {
+ return objectMapper.readValue(data, clazz);
+ } catch (JsonProcessingException e) {
+ log.error("Unable to deserialize setting object: {} -> {}", configKey, data, e);
+ throw new RuntimeException(e);
+ }
+ } else {
+ log.error("The configuration key {} doesn't exists in database!", configKey);
+ T entity = resolve(clazz);
+ if (entity == null) throw new BadConfigException();
+ try {
+ set(configKey, entity);
+ log.info("Resolved missing configuration key via #spawnDefault static method.");
+ } catch (JsonProcessingException e) {
+ log.error("Unable to serialize setting object: {} -> {}", configKey, entity, e);
+ throw new RuntimeException(e);
+ }
+ return entity;
+ }
+ }
+
+ @Nullable
+ private <T> T resolve(Class<T> clazz) {
+ try {
+ Method method = clazz.getDeclaredMethod("spawnDefault");
+ //noinspection unchecked
+ return (T) method.invoke(null);
+ } catch (Throwable e) {
+ log.error("Failed to resolve {}", clazz.getName(), e);
+ return null;
+ }
+ }
+
+ public <T> void set(@NotNull String configKey, @Nullable T value) throws JsonProcessingException {
+ if (value == null) {
+ repository.deleteByKey(configKey);
+ return;
+ }
+ SettingEntity entity;
+ Optional<SettingEntity> inDatabase = repository.findByKey(configKey);
+ if (inDatabase.isPresent()) {
+ entity = inDatabase.get();
+ } else {
+ entity = new SettingEntity(0, configKey, objectMapper.writeValueAsString(value));
+ }
+ entity = repository.save(entity);
+ }
+
+}
diff --git a/src/main/java/com/github/example/pt/service/TagService.java b/src/main/java/com/github/example/pt/service/TagService.java
new file mode 100644
index 0000000..9d9179f
--- /dev/null
+++ b/src/main/java/com/github/example/pt/service/TagService.java
@@ -0,0 +1,37 @@
+package com.github.example.pt.service;
+
+import com.github.example.pt.entity.Tag;
+import com.github.example.pt.repository.TagRepository;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Locale;
+
+@Service
+
+public class TagService {
+ @Autowired
+ private TagRepository repository;
+
+ @Nullable
+ public Tag getTag(@NotNull String tagName) {
+ tagName = tagName.toLowerCase(Locale.ROOT);
+ return repository.findByName(tagName).orElse(null);
+ }
+ @Nullable
+ public Tag getTag(long id){
+ return repository.findById(id).orElse(null);
+ }
+
+ @NotNull
+ public Tag save(@NotNull Tag tag){
+ return repository.save(tag);
+ }
+
+ @NotNull
+ public Iterable<Tag> getAllTags(){
+ return repository.findAll();
+ }
+}
diff --git a/src/main/java/com/github/example/pt/service/ThanksService.java b/src/main/java/com/github/example/pt/service/ThanksService.java
new file mode 100644
index 0000000..b443e04
--- /dev/null
+++ b/src/main/java/com/github/example/pt/service/ThanksService.java
@@ -0,0 +1,39 @@
+package com.github.example.pt.service;
+
+import com.github.example.pt.entity.Thanks;
+import com.github.example.pt.entity.Torrent;
+import com.github.example.pt.entity.User;
+import com.github.example.pt.repository.ThanksRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+
+public class ThanksService {
+ @Autowired
+ private ThanksRepository repository;
+
+ public boolean sayThanks(Torrent torrent, User user) {
+ if (repository.existsByTorrentAndUser(torrent, user)) {
+ return false;
+ }
+ repository.save(new Thanks(0, user, torrent));
+ return true;
+ }
+
+ public boolean hadThanksFor(Torrent torrent, User user) {
+ return repository.existsByTorrentAndUser(torrent, user);
+ }
+
+ public List<Thanks> getLast25ThanksByTorrent(Torrent torrent) {
+ return repository.getThanksByTorrentOrderByIdDesc(torrent, Pageable.ofSize(25).withPage(0));
+ }
+
+ public long countThanksForTorrent(Torrent torrent) {
+ return repository.countAllByTorrent(torrent);
+ }
+
+}
diff --git a/src/main/java/com/github/example/pt/service/TorrentService.java b/src/main/java/com/github/example/pt/service/TorrentService.java
new file mode 100644
index 0000000..e3a0885
--- /dev/null
+++ b/src/main/java/com/github/example/pt/service/TorrentService.java
@@ -0,0 +1,145 @@
+package com.github.example.pt.service;
+
+import com.github.example.pt.controller.torrent.dto.request.SearchTorrentRequestDTO;
+import com.github.example.pt.entity.Category;
+import com.github.example.pt.entity.PromotionPolicy;
+import com.github.example.pt.entity.Tag;
+import com.github.example.pt.entity.Torrent;
+import com.github.example.pt.repository.TorrentRepository;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.criteria.Predicate;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+@Service
+
+public class TorrentService {
+ @Autowired
+ private TorrentRepository torrentRepository;
+ @Autowired
+ private CategoryService categoryService;
+ @Autowired
+ private PromotionService promotionService;
+ @Autowired
+ private TagService tagService;
+ @Autowired
+ private EntityManager entityManager;
+
+ @Nullable
+ public Torrent getTorrent(long id) {
+ return torrentRepository.findById(id).orElse(null);
+ }
+
+ @Nullable
+ public Torrent getTorrent(@NotNull String infoHash) {
+ infoHash = infoHash.toLowerCase();
+ Optional<Torrent> entity = torrentRepository.findByInfoHashIgnoreCase(infoHash);
+ return entity.orElse(null);
+ }
+
+ public List<Torrent> getAllTorrents() {
+ return new ArrayList<>(torrentRepository.findAll());
+ }
+
+ @Nullable
+ public List<Torrent> getTorrentWithCategory(@Nullable String categorySlug) {
+ List<Torrent> torrents = new ArrayList<>();
+ if (categorySlug == null) {
+ torrents.addAll(torrentRepository.findAll());
+ } else {
+ Category category = categoryService.getCategory(categorySlug);
+ if (category != null) {
+ torrents.addAll(torrentRepository.findAllByCategoryOrderByIdDesc(category));
+ }
+ }
+ return torrents;
+ }
+
+ @NotNull
+ public Torrent save(@NotNull Torrent torrent) {
+ torrent.setInfoHash(torrent.getInfoHash());
+ return torrentRepository.save(torrent);
+ }
+
+ @NotNull
+ public Page<Torrent> search(@NotNull SearchTorrentRequestDTO searchRequestDTO) {
+ List<String> categoriesRequired = new ArrayList<>();
+ List<String> promotionRequired = new ArrayList<>();
+ List<String> tagRequired = new ArrayList<>();
+ String keyword;
+ if(searchRequestDTO.getKeyword() == null) keyword = "";
+ else keyword = searchRequestDTO.getKeyword();
+ if (searchRequestDTO.getCategory() != null) {
+ categoriesRequired.addAll(searchRequestDTO.getCategory());
+ }
+ if (searchRequestDTO.getPromotion() != null) {
+ promotionRequired.addAll(searchRequestDTO.getPromotion());
+ }
+ if (searchRequestDTO.getTag() != null) {
+ tagRequired.addAll(searchRequestDTO.getTag());
+ }
+
+ return search(keyword,
+ categoriesRequired,
+ promotionRequired,
+ tagRequired,
+ Pageable.ofSize(searchRequestDTO.getEntriesPerPage())
+ .withPage(searchRequestDTO.getPage()));
+ }
+
+
+ @NotNull
+ public Page<Torrent> search(@NotNull String keyword, @NotNull List<String> categoriesRequired, @NotNull List<String> promotionRequired, @NotNull List<String> tagRequired, @NotNull Pageable pageable) {
+ List<Category> categoriesRequiredId = new ArrayList<>();
+ List<PromotionPolicy> promotionRequiredId = new ArrayList<>();
+ List<Tag> tagRequiredId = new ArrayList<>();
+ for (String categorySlug : categoriesRequired) {
+ Category category = categoryService.getCategory(categorySlug);
+ if (category != null) {
+ categoriesRequiredId.add(category);
+ }
+ }
+ for (String promotionSlug : promotionRequired) {
+ PromotionPolicy promotion = promotionService.getPromotionPolicy(promotionSlug);
+ if (promotion != null) {
+ promotionRequiredId.add(promotion);
+ }
+ }
+ for (String tagSlug : tagRequired) {
+ Tag tag = tagService.getTag(tagSlug);
+ if (tag != null) {
+ tagRequiredId.add(tag);
+ }
+ }
+ return torrentRepository.findAll((root, query, criteriaBuilder) -> {
+ List<Predicate> predicates = new ArrayList<>();
+ if (!keyword.isEmpty()) {
+ predicates.add(criteriaBuilder.or(
+ criteriaBuilder.like(root.get("title"), "%" + keyword + "%"),
+ criteriaBuilder.like(root.get("subTitle"), "%" + keyword + "%")
+ ));
+ }
+ if (!categoriesRequiredId.isEmpty()) {
+ predicates.add(root.get("category").in(categoriesRequiredId));
+ }
+ if (!promotionRequired.isEmpty()) {
+ predicates.add(root.get("promotion").in(promotionRequiredId));
+ }
+ if (!tagRequired.isEmpty()) {
+ predicates.add(root.get("tag").in(tagRequiredId));
+ }
+ query.orderBy(criteriaBuilder.desc(root.get("id")));
+ return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
+ }, pageable);
+
+ }
+
+}
diff --git a/src/main/java/com/github/example/pt/service/TransferHistoryService.java b/src/main/java/com/github/example/pt/service/TransferHistoryService.java
new file mode 100644
index 0000000..a16bd6f
--- /dev/null
+++ b/src/main/java/com/github/example/pt/service/TransferHistoryService.java
@@ -0,0 +1,96 @@
+package com.github.example.pt.service;
+
+import com.alicp.jetcache.anno.CacheType;
+import com.alicp.jetcache.anno.Cached;
+import com.github.example.pt.config.TrackerConfig;
+import com.github.example.pt.entity.Torrent;
+import com.github.example.pt.entity.TransferHistory;
+import com.github.example.pt.entity.User;
+import com.github.example.pt.repository.TransferHistoryRepository;
+import com.github.example.pt.type.AnnounceEventType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+
+@Service
+
+public class TransferHistoryService {
+ @Autowired
+ private TransferHistoryRepository repository;
+ @Autowired
+ private PeerService peerService;
+ @Autowired
+ private SettingService settingService;
+
+ @Nullable
+ public TransferHistory getTransferHistory(@NotNull User user, @NotNull Torrent torrent) {
+ return repository.findByUserAndTorrent(user, torrent).orElse(null);
+ }
+
+ @NotNull
+ public List<TransferHistory> getTransferHistory(@NotNull Torrent torrent) {
+ return repository.findAllByTorrentOrderByUpdatedAt(torrent);
+ }
+
+ @NotNull
+ public List<TransferHistory> getTransferHistoryActive(@NotNull Torrent torrent) {
+ TrackerConfig config = settingService.get(TrackerConfig.getConfigKey(), TrackerConfig.class);
+ Timestamp timestamp = Timestamp.from(Instant.now().minus(config.getTorrentIntervalMax() + 15000, ChronoUnit.MILLIS));
+ return repository.findAllByTorrentAndUpdatedAtAfterOrderByUpdatedAt(torrent, timestamp);
+ }
+
+ @NotNull
+ @Cached(expire = 600, cacheType = CacheType.BOTH)
+ public PeerStatus getPeerStatus(@NotNull Torrent torrent) {
+ TrackerConfig config = settingService.get(TrackerConfig.getConfigKey(), TrackerConfig.class);
+ List<TransferHistory> histories = getTransferHistory(torrent);
+ int complete = 0;
+ int incomplete = 0;
+ int downloaders = 0;
+ int downloaded = 0;
+ for (TransferHistory history : histories) {
+ if (isTransferActive(history, config)) {
+ if (history.getLastEvent() == AnnounceEventType.PAUSED) { // 部分做种
+ downloaders++;
+ continue;
+ }
+ if (history.isHaveCompleteHistory()) { // 曾经完成过下载
+ downloaded++;
+ }
+ if (history.getLeft() == 0) { // 下载报告已完成
+ complete++;
+ } else { // 下载还未完成 (不和 haveCompleteHistory 合并,因为可以下载多次)
+ incomplete++;
+ }
+ } else {
+ if (history.getLeft() == 0) { // 下载报告已完成
+ complete++;
+ }
+ if (history.isHaveCompleteHistory()) { // 曾经完成过下载
+ downloaded++;
+ }
+ }
+ }
+ return new PeerStatus(complete, incomplete, downloaded, downloaders);
+ }
+
+ private boolean isTransferActive(TransferHistory history, TrackerConfig config) {
+ Timestamp timestamp = Timestamp.from(Instant.now().minus(config.getTorrentIntervalMax() + 15000, ChronoUnit.MILLIS));
+ return history.getUpdatedAt().after(timestamp);
+ }
+
+ @NotNull
+ public TransferHistory save(@NotNull TransferHistory transferHistory) {
+ return repository.save(transferHistory);
+ }
+
+ public record PeerStatus(int complete, int incomplete, int downloaded, int downloaders) {
+
+ }
+}
diff --git a/src/main/java/com/github/example/pt/service/UserGroupService.java b/src/main/java/com/github/example/pt/service/UserGroupService.java
new file mode 100644
index 0000000..0847892
--- /dev/null
+++ b/src/main/java/com/github/example/pt/service/UserGroupService.java
@@ -0,0 +1,42 @@
+package com.github.example.pt.service;
+
+import com.github.example.pt.entity.UserGroup;
+import com.github.example.pt.repository.UserGroupRepository;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+
+public class UserGroupService {
+ @Autowired
+ private UserGroupRepository repository;
+ @Autowired
+ private PermissionService permissionService;
+ @Autowired
+ private PromotionService promotionService;
+
+ @Nullable
+ public UserGroup getUserGroup(long id) {
+ return repository.findById(id).map(userGroup -> new UserGroup(
+ userGroup.getId(),
+ userGroup.getSlug(),
+ userGroup.getDisplayName(),
+ userGroup.getPermissionEntities().stream()
+ .map(perm -> permissionService.getPermission(perm.getId())).toList(),
+ userGroup.getPromotionPolicy()
+ // , userGroupEntity.getInherited().stream().map(this::convert).toList()
+ )).orElse(null);
+ }
+
+ public UserGroup getDefaultUserGroup(){
+ return repository.findAll().iterator().next();
+ }
+
+ @NotNull
+ public UserGroup save(@NotNull UserGroup userGroup) {
+ return repository.save(userGroup);
+ }
+
+}
diff --git a/src/main/java/com/github/example/pt/service/UserService.java b/src/main/java/com/github/example/pt/service/UserService.java
new file mode 100644
index 0000000..5dd661e
--- /dev/null
+++ b/src/main/java/com/github/example/pt/service/UserService.java
@@ -0,0 +1,53 @@
+package com.github.example.pt.service;
+
+import com.github.example.pt.entity.User;
+import com.github.example.pt.repository.UserRepository;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+@Service
+
+public class UserService {
+ @Autowired
+ private UserRepository repository;
+
+ // getUser
+ @Nullable
+ public User getUser(long id) {
+ Optional<User> userEntity = repository.findById(id);
+ return userEntity.orElse(null);
+ }
+
+ @Nullable
+ public User getUserByUsername(String username) {
+ Optional<User> userEntity = repository.findByUsername(username);
+ return userEntity.orElse(null);
+ }
+
+ @Nullable
+ public User getUserByEmail(String email) {
+ Optional<User> userEntity = repository.findByEmail(email);
+ return userEntity.orElse(null);
+ }
+
+ @Nullable
+ public User getUserByPasskey(String passkey) {
+ Optional<User> userEntity = repository.findByPasskeyIgnoreCase(passkey);
+ return userEntity.orElse(null);
+ }
+ @Nullable
+ public User getUserByPersonalAccessToken(@NotNull String personalAccessToken){
+ Optional<User> userEntity = repository.findByPersonalAccessTokenIgnoreCase(personalAccessToken);
+ return userEntity.orElse(null);
+ }
+
+ @NotNull
+ public User save(User user) {
+ return repository.save(user);
+ }
+
+}
diff --git a/src/main/java/com/github/example/pt/type/AnnounceEventType.java b/src/main/java/com/github/example/pt/type/AnnounceEventType.java
new file mode 100644
index 0000000..d98f710
--- /dev/null
+++ b/src/main/java/com/github/example/pt/type/AnnounceEventType.java
@@ -0,0 +1,36 @@
+package com.github.example.pt.type;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Locale;
+
+public enum AnnounceEventType {
+ STARTED("started"),
+ COMPLETED("completed"),
+ STOPPED("stopped"),
+ PAUSED("paused"),
+
+ UNKNOWN("unknown");
+ private final String key;
+
+ AnnounceEventType(String key) {
+ this.key = key;
+ }
+
+ public static @NotNull AnnounceEventType fromName(@Nullable String name) {
+ if (name == null) return UNKNOWN;
+ name = name.toLowerCase(Locale.ROOT);
+ for (AnnounceEventType type : values()) {
+ if (type.getKey().equals(name)) {
+ return type;
+ }
+ }
+ return UNKNOWN;
+ }
+
+ @NotNull
+ public String getKey() {
+ return key;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/type/AnnounceEventTypeConverter.java b/src/main/java/com/github/example/pt/type/AnnounceEventTypeConverter.java
new file mode 100644
index 0000000..f2b00e1
--- /dev/null
+++ b/src/main/java/com/github/example/pt/type/AnnounceEventTypeConverter.java
@@ -0,0 +1,21 @@
+package com.github.example.pt.type;
+
+import jakarta.persistence.AttributeConverter;
+import jakarta.persistence.Converter;
+
+@Converter(autoApply = false)
+public class AnnounceEventTypeConverter implements AttributeConverter<AnnounceEventType, String> {
+ @Override
+ public String convertToDatabaseColumn(AnnounceEventType attribute) {
+ if (attribute == null) {
+ return null;
+ }
+ return attribute.getKey(); // 比如 "completed"
+ }
+
+ @Override
+ public AnnounceEventType convertToEntityAttribute(String dbData) {
+ return AnnounceEventType.fromName(dbData);
+ }
+}
+
diff --git a/src/main/java/com/github/example/pt/type/GuestAccessBlocker.java b/src/main/java/com/github/example/pt/type/GuestAccessBlocker.java
new file mode 100644
index 0000000..f0f989f
--- /dev/null
+++ b/src/main/java/com/github/example/pt/type/GuestAccessBlocker.java
@@ -0,0 +1,9 @@
+package com.github.example.pt.type;
+
+public enum GuestAccessBlocker {
+ NORMAL,
+ NOT_FOUND_PAGE,
+ FORBIDDEN_PAGE,
+ INTERNAL_SERVER_ERROR_PAGE,
+ REDIRECT
+}
diff --git a/src/main/java/com/github/example/pt/type/GuestAccessRequirement.java b/src/main/java/com/github/example/pt/type/GuestAccessRequirement.java
new file mode 100644
index 0000000..9993111
--- /dev/null
+++ b/src/main/java/com/github/example/pt/type/GuestAccessRequirement.java
@@ -0,0 +1,11 @@
+package com.github.example.pt.type;
+
+public enum GuestAccessRequirement {
+ NORMAL,
+ ALREADY_LOGGED_IN,
+ SPECIFIC_SECRET,
+ REFERER,
+ USER_AGENT,
+ IP_ADDRESS,
+ PASSKEY
+}
diff --git a/src/main/java/com/github/example/pt/type/IPFormatRequirement.java b/src/main/java/com/github/example/pt/type/IPFormatRequirement.java
new file mode 100644
index 0000000..2ab4917
--- /dev/null
+++ b/src/main/java/com/github/example/pt/type/IPFormatRequirement.java
@@ -0,0 +1,7 @@
+package com.github.example.pt.type;
+
+public enum IPFormatRequirement {
+ IPV4,
+ IPV6,
+ BOTH,
+}
diff --git a/src/main/java/com/github/example/pt/type/LoginType.java b/src/main/java/com/github/example/pt/type/LoginType.java
new file mode 100644
index 0000000..d87a8f6
--- /dev/null
+++ b/src/main/java/com/github/example/pt/type/LoginType.java
@@ -0,0 +1,9 @@
+package com.github.example.pt.type;
+
+public enum LoginType {
+ ACCOUNT,
+ PASSKEY,
+ PERSONAL_ACCESSTOKEN,
+ PROGRAM_INTERNAL,
+ TWO_STEP_VERIFICATION
+}
diff --git a/src/main/java/com/github/example/pt/type/PrivacyLevel.java b/src/main/java/com/github/example/pt/type/PrivacyLevel.java
new file mode 100644
index 0000000..67a0cba
--- /dev/null
+++ b/src/main/java/com/github/example/pt/type/PrivacyLevel.java
@@ -0,0 +1,7 @@
+package com.github.example.pt.type;
+
+public enum PrivacyLevel {
+ LOW,
+ MEDIUM,
+ HIGH
+}
diff --git a/src/main/java/com/github/example/pt/util/BencodeUtil.java b/src/main/java/com/github/example/pt/util/BencodeUtil.java
new file mode 100644
index 0000000..e1dc5ce
--- /dev/null
+++ b/src/main/java/com/github/example/pt/util/BencodeUtil.java
@@ -0,0 +1,46 @@
+package com.github.example.pt.util;
+
+import com.dampcake.bencode.Bencode;
+import com.github.example.pt.entity.Peer;
+import com.github.example.pt.exception.RetryableAnnounceException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+
+public class BencodeUtil {
+ private static final Bencode BITTORRENT_STANDARD = new Bencode(StandardCharsets.ISO_8859_1);
+ private static final Bencode UTF8_STANDARD = new Bencode(StandardCharsets.UTF_8);
+
+ public static String convertToString(byte[] bytes) {
+ return new String(bytes, BITTORRENT_STANDARD.getCharset());
+ }
+
+ public static Bencode bittorrent() {
+ return BITTORRENT_STANDARD;
+ }
+
+ public static Bencode utf8() {
+ return UTF8_STANDARD;
+ }
+
+ public static String compactPeers(Collection<Peer> peers, boolean isV6) throws RetryableAnnounceException {
+ ByteBuffer buffer = ByteBuffer.allocate((isV6 ? 18 : 6) * peers.size());
+ for (Peer peer : peers) {
+ String ip = peer.getIp();
+ try {
+ for (byte address : InetAddress.getByName(ip).getAddress()) {
+ buffer.put(address);
+ }
+ int in = peer.getPort();
+ buffer.put((byte) ((in >>> 8) & 0xFF));
+ buffer.put((byte) (in & 0xFF));
+ } catch (UnknownHostException e) {
+ throw new RetryableAnnounceException("incorrect ip format encountered when compact peer ip", 0);
+ }
+ }
+ return convertToString(buffer.array());
+ }
+}
diff --git a/src/main/java/com/github/example/pt/util/BooleanUtil.java b/src/main/java/com/github/example/pt/util/BooleanUtil.java
new file mode 100644
index 0000000..9d3f6d4
--- /dev/null
+++ b/src/main/java/com/github/example/pt/util/BooleanUtil.java
@@ -0,0 +1,14 @@
+package com.github.example.pt.util;
+
+import org.jetbrains.annotations.Nullable;
+
+public class BooleanUtil {
+ public static boolean parseBoolean(@Nullable String input) {
+ if (input == null) return false;
+ if ("1".equals(input)) return true;
+ if ("true".equalsIgnoreCase(input)) return true;
+ if ("yes".equalsIgnoreCase(input)) return true;
+ if ("on".equalsIgnoreCase(input)) return true;
+ return "y".equalsIgnoreCase(input);
+ }
+}
diff --git a/src/main/java/com/github/example/pt/util/ByteUtil.java b/src/main/java/com/github/example/pt/util/ByteUtil.java
new file mode 100644
index 0000000..c62e48c
--- /dev/null
+++ b/src/main/java/com/github/example/pt/util/ByteUtil.java
@@ -0,0 +1,22 @@
+package com.github.example.pt.util;
+
+// https://blog.csdn.net/weixin_38820375/article/details/88556771
+public class ByteUtil {
+ public static byte[] short2byte(short s) {
+ byte[] b = new byte[2];
+ for (int i = 0; i < 2; i++) {
+ int offset = 16 - (i + 1) * 8;
+ b[i] = (byte) ((s >> offset) & 0xff);
+ }
+ return b;
+ }
+
+ public static short byte2short(byte[] b) {
+ short l = 0;
+ for (int i = 0; i < 2; i++) {
+ l <<= 8;
+ l |= (b[i] & 0xff);
+ }
+ return l;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/util/ClassUtil.java b/src/main/java/com/github/example/pt/util/ClassUtil.java
new file mode 100644
index 0000000..a8c69a9
--- /dev/null
+++ b/src/main/java/com/github/example/pt/util/ClassUtil.java
@@ -0,0 +1,20 @@
+package com.github.example.pt.util;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import lombok.SneakyThrows;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.ExecutionException;
+
+@Component
+public class ClassUtil {
+ private final Cache<Class<?>, String> simpleNameCache = CacheBuilder.newBuilder()
+ .maximumSize(4096)
+ .build();
+
+ @SneakyThrows(ExecutionException.class)
+ public String getClassSimpleName(Class<?> clazz) {
+ return simpleNameCache.get(clazz, clazz::getSimpleName);
+ }
+}
diff --git a/src/main/java/com/github/example/pt/util/ExecutorUtil.java b/src/main/java/com/github/example/pt/util/ExecutorUtil.java
new file mode 100644
index 0000000..77cec6a
--- /dev/null
+++ b/src/main/java/com/github/example/pt/util/ExecutorUtil.java
@@ -0,0 +1,15 @@
+package com.github.example.pt.util;
+
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+@Component
+public class ExecutorUtil {
+ private final ExecutorService announceExecutor = Executors.newWorkStealingPool();
+
+ public ExecutorService getAnnounceExecutor() {
+ return announceExecutor;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/util/GsonUtil.java b/src/main/java/com/github/example/pt/util/GsonUtil.java
new file mode 100644
index 0000000..0d08912
--- /dev/null
+++ b/src/main/java/com/github/example/pt/util/GsonUtil.java
@@ -0,0 +1,17 @@
+package com.github.example.pt.util;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+public class GsonUtil {
+ private static Gson GSON = new Gson();
+ private static Gson HUMAN_READABLE = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create();
+
+ public static Gson getGSON() {
+ return GSON;
+ }
+
+ public static Gson getHumanReadable() {
+ return HUMAN_READABLE;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/util/HibernateSessionUtil.java b/src/main/java/com/github/example/pt/util/HibernateSessionUtil.java
new file mode 100644
index 0000000..a47d614
--- /dev/null
+++ b/src/main/java/com/github/example/pt/util/HibernateSessionUtil.java
@@ -0,0 +1,43 @@
+package com.github.example.pt.util;
+
+import jakarta.persistence.EntityManagerFactory;
+import org.hibernate.FlushMode;
+import org.hibernate.Session;
+import org.hibernate.SessionFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.orm.hibernate5.SessionFactoryUtils;
+import org.springframework.orm.hibernate5.SessionHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
+
+@Component
+public class HibernateSessionUtil {
+ @Autowired
+ private EntityManagerFactory entityManagerFactory;
+
+ public SessionFactory getSessionFactory(){
+ return entityManagerFactory.unwrap(SessionFactory.class);
+ }
+
+ public void closeFromThread(boolean participate) {
+ if (!participate) {
+ SessionHolder sessionHolder = (SessionHolder) TransactionSynchronizationManager
+ .unbindResource(getSessionFactory());
+ SessionFactoryUtils.closeSession(sessionHolder.getSession());
+ }
+ }
+
+ public boolean bindToThread() {
+ SessionFactory sessionFactory = getSessionFactory();
+ if (TransactionSynchronizationManager.hasResource(sessionFactory)) {
+ // Do not modify the Session: just set the participate flag.
+ return true;
+ } else {
+ Session session = sessionFactory.openSession();
+ session.setFlushMode(FlushMode.MANUAL.toJpaFlushMode());
+ SessionHolder sessionHolder = new SessionHolder(session);
+ TransactionSynchronizationManager.bindResource(sessionFactory, sessionHolder);
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/util/IPUtil.java b/src/main/java/com/github/example/pt/util/IPUtil.java
new file mode 100644
index 0000000..37456f7
--- /dev/null
+++ b/src/main/java/com/github/example/pt/util/IPUtil.java
@@ -0,0 +1,18 @@
+package com.github.example.pt.util;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
+
+@Slf4j
+public class IPUtil {
+ @NotNull
+ public static String getRequestIp(@NotNull HttpServletRequest request) {
+ String realIp = request.getHeader("X-REAL-IP");
+ if (realIp == null)
+ realIp = request.getHeader("X-FORWARDED-FOR");
+ if (realIp == null)
+ realIp = request.getRemoteAddr();
+ return realIp;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/util/InfoHashUtil.java b/src/main/java/com/github/example/pt/util/InfoHashUtil.java
new file mode 100644
index 0000000..189bb26
--- /dev/null
+++ b/src/main/java/com/github/example/pt/util/InfoHashUtil.java
@@ -0,0 +1,26 @@
+package com.github.example.pt.util;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Locale;
+
+public class InfoHashUtil {
+ public static @NotNull String parseInfoHash(String encoded) throws IllegalArgumentException {
+ try {
+ StringBuilder r = new StringBuilder();
+ for (int i = 0; i < encoded.length(); i++) {
+ char c = encoded.charAt(i);
+ if (c == '%') {
+ r.append(encoded.charAt(i + 1));
+ r.append(encoded.charAt(i + 2));
+ i = i + 2;
+ } else {
+ r.append(String.format("%02x", (int) c));
+ }
+ }
+ return r.toString().toLowerCase(Locale.ROOT);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Failed to decode info_hash: " + encoded);
+ }
+ }
+}
diff --git a/src/main/java/com/github/example/pt/util/IpValidator.java b/src/main/java/com/github/example/pt/util/IpValidator.java
new file mode 100644
index 0000000..cf5cf59
--- /dev/null
+++ b/src/main/java/com/github/example/pt/util/IpValidator.java
@@ -0,0 +1,63 @@
+package com.github.example.pt.util;
+
+import lombok.SneakyThrows;
+import org.apache.commons.validator.routines.InetAddressValidator;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+public class IpValidator {
+ private static final InetAddressValidator ipValidator = InetAddressValidator.getInstance();
+ private final static int PORT_MAX_RANGE = 65535;
+ private final static int PORT_MIN_RANGE = 10000;
+
+ /**
+ * Validate the port
+ *
+ * @param port the port to validate
+ * @return valid or not
+ */
+ public static boolean isPortValid(int port) {
+ // Usually the ports < 10000 may host a service, we need prevent client announce it
+ // may cause DDoS attacks
+ if (port > PORT_MAX_RANGE) {
+ return false;
+ }
+ return port >= PORT_MIN_RANGE;
+ }
+
+ /**
+ * Verify a IP address is valid
+ *
+ * @param address The ip address
+ * @return Valid or not
+ */
+ // Suppress UnknownHostException, ipValidator make sure it must
+ // be an ip address so it impossible to trigger the DNS lookup
+ @SneakyThrows(UnknownHostException.class)
+ public static boolean isPortValid(String address) {
+ if (!ipValidator.isValid(address)) {
+ return false;
+ }
+ if (ipValidator.isValidInet4Address(address)) {
+ return validateAddress(Inet4Address.getByName(address));
+ }
+ if (ipValidator.isValidInet6Address(address)) {
+ return validateAddress(Inet6Address.getByName(address));
+ }
+ // neither ipv4 nor ipv6
+ return false;
+ }
+
+ private static boolean validateAddress(InetAddress address) {
+ if (address.isAnyLocalAddress()) {
+ return false;
+ }
+ if (address.isLoopbackAddress()) {
+ return false;
+ }
+ return !address.isSiteLocalAddress();
+ }
+}
diff --git a/src/main/java/com/github/example/pt/util/MiscUtil.java b/src/main/java/com/github/example/pt/util/MiscUtil.java
new file mode 100644
index 0000000..33c7382
--- /dev/null
+++ b/src/main/java/com/github/example/pt/util/MiscUtil.java
@@ -0,0 +1,16 @@
+package com.github.example.pt.util;
+
+import org.jetbrains.annotations.Nullable;
+
+public class MiscUtil {
+ @SafeVarargs
+ @Nullable
+ public static <T> T anyNotNull(T... objects) {
+ for (T object : objects) {
+ if (object != null) {
+ return object;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/util/PackUtil.java b/src/main/java/com/github/example/pt/util/PackUtil.java
new file mode 100644
index 0000000..d4ffa8f
--- /dev/null
+++ b/src/main/java/com/github/example/pt/util/PackUtil.java
@@ -0,0 +1,59 @@
+package com.github.example.pt.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * PHP's binary pack/unpack functions
+ */
+public class PackUtil {
+
+ public static byte[] pack(String str) {
+ int nibbleshift = 4;
+ int position = 0;
+ int len = str.length() / 2 + str.length() % 2;
+ byte[] output = new byte[len];
+ for (char v : str.toCharArray()) {
+ byte n = (byte) v;
+ if (n >= '0' && n <= '9') {
+ n -= '0';
+ } else if (n >= 'A' && n <= 'F') {
+ n -= ('A' - 10);
+ } else if (n >= 'a' && n <= 'f') {
+ n -= ('a' - 10);
+ } else {
+ continue;
+ }
+ output[position] |= (n << nibbleshift);
+
+ if (nibbleshift == 0) {
+ position++;
+ }
+ nibbleshift = (nibbleshift + 4) & 7;
+ }
+
+ return output;
+ }
+
+ public static String unpack(InputStream is, int len) throws IOException {
+ byte[] bytes = new byte[len];
+ is.read(bytes);
+ return unpack(bytes);
+ }
+
+ public static String unpack(byte[] bytes) {
+ StringBuilder stringBuilder = new StringBuilder();
+ if (bytes == null || bytes.length <= 0) {
+ return null;
+ }
+ for (byte aByte : bytes) {
+ int v = aByte & 0xFF;
+ String hv = Integer.toHexString(v);
+ if (hv.length() < 2) {
+ stringBuilder.append(0);
+ }
+ stringBuilder.append(hv);
+ }
+ return stringBuilder.toString();
+ }
+}
diff --git a/src/main/java/com/github/example/pt/util/PasswordHash.java b/src/main/java/com/github/example/pt/util/PasswordHash.java
new file mode 100644
index 0000000..e41b6b8
--- /dev/null
+++ b/src/main/java/com/github/example/pt/util/PasswordHash.java
@@ -0,0 +1,34 @@
+package com.github.example.pt.util;
+
+import at.favre.lib.crypto.bcrypt.BCrypt;
+import at.favre.lib.crypto.bcrypt.LongPasswordStrategies;
+import io.micrometer.common.util.StringUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * BCrypt Hash Utility
+ */
+@Slf4j
+public class PasswordHash {
+ private static final int COST = 6;
+
+ public static boolean verify(@NotNull String password, @NotNull String hash) {
+ BCrypt.Result result = BCrypt.verifyer(BCrypt.Version.VERSION_2A, LongPasswordStrategies.hashSha512(BCrypt.Version.VERSION_2A))
+ .verify(password.getBytes(StandardCharsets.UTF_8), hash.getBytes(StandardCharsets.UTF_8));
+ if (!result.validFormat) {
+ log.warn("Failed to validate the hash {}, because the format is invalid", hash);
+ }
+ if (StringUtils.isNotEmpty(result.formatErrorMessage)) {
+ log.warn("An error occurred when verifying the password hash {} for {}", result.formatErrorMessage, hash);
+ }
+ return result.verified;
+ }
+
+ public static String hash(@NotNull String password) {
+ return BCrypt.with(BCrypt.Version.VERSION_2A, LongPasswordStrategies.hashSha512(BCrypt.Version.VERSION_2A))
+ .hashToString(COST, password.toCharArray());
+ }
+}
diff --git a/src/main/java/com/github/example/pt/util/RandomUtil.java b/src/main/java/com/github/example/pt/util/RandomUtil.java
new file mode 100644
index 0000000..d994fe2
--- /dev/null
+++ b/src/main/java/com/github/example/pt/util/RandomUtil.java
@@ -0,0 +1,24 @@
+package com.github.example.pt.util;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+public class RandomUtil {
+ private static final Random rand = new Random();
+
+ @NotNull
+ public static <T> List<T> getRandomElements(List<T> list, int want) {
+ List<T> listCopy = new ArrayList<>(list);
+ List<T> result = new ArrayList<>(list);
+ if (list.size() <= want) return listCopy;
+ for (int i = 0; i < want; i++) {
+ T obj = listCopy.get(rand.nextInt(listCopy.size()));
+ result.add(obj);
+ listCopy.remove(obj);
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/com/github/example/pt/util/SafeUUID.java b/src/main/java/com/github/example/pt/util/SafeUUID.java
new file mode 100644
index 0000000..16f5e2e
--- /dev/null
+++ b/src/main/java/com/github/example/pt/util/SafeUUID.java
@@ -0,0 +1,58 @@
+package com.github.example.pt.util;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+/**
+ * Convert String to a UUID without IllegalArgumentException
+ */
+public class SafeUUID {
+ @Nullable
+ public static UUID fromString(String str) {
+ try {
+ return UUID.fromString(str);
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+ @NotNull
+ public static String randomNoDashesUUID(){
+ return UUID.randomUUID().toString().replace("_","");
+ }
+
+ @NotNull
+ public static UUID nilUniqueId() {
+ return new UUID(0, 0);
+ }
+
+ @NotNull
+ public static String stripDashes(@NotNull String uuid) {
+ return uuid.replaceAll("-", "");
+ }
+
+ @Nullable
+ public static String addDashes(@NotNull String uuid) {
+ if (!isUUID(uuid)) {
+ return null;
+ }
+ return uuid.substring(0, 8) + "-" + uuid.substring(8, 12) + "-" + uuid.substring(12, 16) + "-" + uuid.substring(16, 20) + "-" + uuid.substring(20);
+ }
+
+ public static boolean isUUID(@NotNull String str) {
+ try {
+ UUID.fromString(str);
+ return true;
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ System.err.println("Failed to check " + str);
+ return false;
+ }
+ // return str.matches("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}");
+ }
+
+ public static boolean isDashesStrippedUUID(@NotNull String str) {
+ return str.matches("[0-9a-fA-F]{32}");
+ }
+}
diff --git a/src/main/java/com/github/example/pt/util/TorrentParser.java b/src/main/java/com/github/example/pt/util/TorrentParser.java
new file mode 100644
index 0000000..2da30fc
--- /dev/null
+++ b/src/main/java/com/github/example/pt/util/TorrentParser.java
@@ -0,0 +1,216 @@
+package com.github.example.pt.util;
+
+import com.dampcake.bencode.BencodeException;
+import com.dampcake.bencode.Type;
+import com.github.example.pt.entity.User;
+import com.github.example.pt.exception.EmptyTorrentFileException;
+import com.github.example.pt.exception.InvalidTorrentFileException;
+import com.github.example.pt.exception.InvalidTorrentVerifyException;
+import com.github.example.pt.exception.InvalidTorrentVersionException;
+import com.github.example.pt.exception.TorrentException;
+import com.google.common.hash.Hashing;
+import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.StringJoiner;
+
+@Slf4j
+public class TorrentParser {
+ private static final List<String> V2_KEYS = List.of("piece layers", "files tree");
+ private final byte[] data;
+ private final Map<String, Long> fileList = new LinkedHashMap<>();
+ private Map<String, Object> dict;
+ private long totalSize;
+ private final boolean calcFiles;
+
+ public TorrentParser(File file, boolean calcFiles) throws IOException, BencodeException, TorrentException, ClassCastException {
+ this.data = Files.readAllBytes(file.toPath());
+ this.calcFiles = calcFiles;
+ init();
+ }
+
+ public TorrentParser(InputStream stream, boolean calcFiles) throws IOException, BencodeException, TorrentException, ClassCastException {
+ this.data = stream.readAllBytes();
+ this.calcFiles = calcFiles;
+ init();
+ }
+
+ public TorrentParser(URL url, boolean calcFiles) throws IOException, BencodeException, TorrentException, ClassCastException {
+ try (InputStream stream = url.openStream()) {
+ this.data = stream.readAllBytes();
+ }
+ this.calcFiles = calcFiles;
+ init();
+ }
+
+ public TorrentParser(byte[] data, boolean calcFiles) throws BencodeException, TorrentException, ClassCastException {
+ this.data = data;
+ this.calcFiles = calcFiles;
+ init();
+ }
+
+ private void init() throws InvalidTorrentVerifyException, InvalidTorrentVersionException, InvalidTorrentFileException, ClassCastException, EmptyTorrentFileException {
+ this.dict = BencodeUtil.bittorrent().decode(this.data, Type.DICTIONARY);
+ validate();
+ if (calcFiles) {
+ verifyAndCalcFiles();
+ }
+ }
+
+ private void validate() throws InvalidTorrentFileException, InvalidTorrentVersionException, InvalidTorrentVerifyException, ClassCastException {
+ if (!this.dict.containsKey("info"))
+ throw new InvalidTorrentFileException("Missing info key");
+ if (isV2Torrent())
+ throw new InvalidTorrentVersionException("version 2");
+ @SuppressWarnings("unchecked")
+ Map<String, Object> info = (Map<String, Object>) this.dict.get("info");
+ if (!info.containsKey("piece length") || !(info.get("piece length") instanceof Number))
+ throw new InvalidTorrentVerifyException("piece length", Number.class, info.get("piece length"));
+ if (!info.containsKey("name") || !(info.get("name") instanceof String))
+ throw new InvalidTorrentVerifyException("name", String.class, info.get("piece length"));
+ if (!info.containsKey("pieces") || !(info.get("pieces") instanceof String))
+ throw new InvalidTorrentVerifyException("pieces", String.class, info.get("pieces"));
+
+ }
+
+ private void verifyAndCalcFiles() throws InvalidTorrentVerifyException, EmptyTorrentFileException {
+ this.fileList.clear();
+ @SuppressWarnings("unchecked")
+ Map<String, Object> info = (Map<String, Object>) this.dict.get("info");
+ if (info.containsKey("length")) {
+ // Single File Torrent
+ String fileName = utf8((String) info.get("name"));
+ long size = Long.parseLong(String.valueOf(info.get("length")));
+ this.fileList.put(fileName, size);
+ this.totalSize = size;
+ return;
+ }
+ // Multiple files
+ if (!info.containsKey("files") || !(info.get("files") instanceof List))
+ throw new InvalidTorrentVerifyException("files", List.class, info.get("files"));
+
+ @SuppressWarnings("unchecked")
+ List<Map<String, Object>> files = (List<Map<String, Object>>) info.get("files");
+ if (files.isEmpty())
+ throw new EmptyTorrentFileException();
+ for (Map<String, Object> file : files) {
+ if (file.get("length") == null || !(file.get("length") instanceof Number))
+ throw new InvalidTorrentVerifyException("length", Number.class, file.get("length"));
+ long size = (Long) file.get("length");
+ List<String> path;
+ if (file.get("path") != null) {
+ //noinspection unchecked
+ path = (List<String>) file.get("path");
+ } else if (file.get("path.utf8") != null) {
+ //noinspection unchecked
+ path = (List<String>) file.get("path.utf8");
+ } else {
+ throw new InvalidTorrentVerifyException("path/path.utf8", List.class, file.get("path"));
+ }
+ List<String> convertedPath = path.stream().map(this::utf8).toList();
+ StringJoiner pathBuilder = new StringJoiner(File.separator);
+ for (String s : convertedPath) {
+ pathBuilder.add(s);
+ }
+ String finalPath = pathBuilder.toString();
+ // BitComet stuff
+ if (finalPath.contains("_____padding_file_")) {
+ //log.debug("Skipped {} because it's a BitComet padding file.", finalPath);
+ continue;
+ }
+ this.fileList.put(finalPath, size);
+ }
+ if (info.containsKey("length")) {
+ // Single File Torrent
+ totalSize = Long.parseLong(String.valueOf(info.get("length")));
+ }else {
+ totalSize = this.fileList.values().stream().mapToLong(v -> v).sum();
+ }
+ }
+
+ public long getTorrentFilesSize() {
+ if (!calcFiles) {
+ throw new IllegalStateException("Files not calculated yet!");
+ }
+ return totalSize;
+ }
+
+ private boolean isV2Torrent() throws ClassCastException {
+ @SuppressWarnings("unchecked")
+ Map<String, Object> info = (Map<String, Object>) this.dict.get("info");
+ for (String v2Key : V2_KEYS) {
+ if (info.containsKey(v2Key))
+ return true;
+ }
+ if (info.containsKey("meta version"))
+ return info.get("meta version").equals(2);
+ return false;
+ }
+
+ public Map<String, Long> getFileList() {
+ if (!calcFiles) {
+ throw new IllegalStateException("Files not calculated yet!");
+ }
+ return fileList;
+ }
+
+ public Map<String, Object> getDict() {
+ return dict;
+ }
+
+ @NotNull
+ public String utf8(@NotNull String latin) {
+ byte[] bytes = latin.getBytes(StandardCharsets.ISO_8859_1);
+ return new String(bytes, StandardCharsets.UTF_8);
+ }
+
+ @NotNull
+ public String getInfoHash() {
+ //noinspection deprecation
+ return Hashing.sha1().hashBytes(BencodeUtil.bittorrent().encode((Map<?, ?>) this.dict.get("info"))).toString().toLowerCase(Locale.ROOT);
+ }
+
+ public byte @NotNull [] rewriteForTracker(@Nullable String siteName, @Nullable String publisher, @Nullable String publisherUrl) {
+ @SuppressWarnings("unchecked")
+ Map<String, Object> info = (Map<String, Object>) dict.get("info");
+ info.put("private", 1);
+ dict.put("info", info);
+ if (siteName != null)
+ dict.put("publish-website", siteName);
+ if (publisher != null)
+ dict.put("publisher", publisher);
+ if (publisherUrl != null)
+ dict.put("publisher-url", publisherUrl);
+ dict.remove("nodes");
+ return BencodeUtil.bittorrent().encode(dict);
+ }
+
+ public byte @NotNull [] rewriteForUser(@NotNull List<String> trackers, @NotNull String passkey, @NotNull User user) {
+ for (int i = 0; i < trackers.size(); i++) {
+ if (i == 0) {
+ dict.put("announce", trackers.get(0) + "?passkey=" + passkey);
+ }
+ dict.put("announce-list", trackers.get(i) + "?passkey=" + passkey);
+ }
+ log.debug("Updated announce: {}", dict.get("announce"));
+ log.debug("Updated announce-list: {}", dict.get("announce-list"));
+ dict.put("torrent-downloader", user.getUsername());
+ dict.put("torrent-downloader-id", user.getId());
+ return BencodeUtil.bittorrent().encode(dict);
+ }
+
+ public byte @NotNull [] save() {
+ return BencodeUtil.bittorrent().encode(this.dict);
+ }
+}
diff --git a/src/main/java/com/github/example/pt/util/URLEncodeUtil.java b/src/main/java/com/github/example/pt/util/URLEncodeUtil.java
new file mode 100644
index 0000000..6404381
--- /dev/null
+++ b/src/main/java/com/github/example/pt/util/URLEncodeUtil.java
@@ -0,0 +1,37 @@
+package com.github.example.pt.util;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.BitSet;
+
+public class URLEncodeUtil {
+ private static final int RADIX = 16;
+ private static final BitSet URLENCODER = new BitSet(256);
+
+ // From Apache HttpClient URLEncodedUtils
+ public static String urlEncode(
+ final String content,
+ final boolean blankAsPlus) {
+ if (content == null) {
+ return null;
+ }
+ final StringBuilder buf = new StringBuilder();
+ final ByteBuffer bb = StandardCharsets.UTF_8.encode(content);
+ while (bb.hasRemaining()) {
+ final int b = bb.get() & 0xff;
+ if (URLENCODER.get(b)) {
+ buf.append((char) b);
+ } else if (blankAsPlus && b == ' ') {
+ buf.append('+');
+ } else {
+ buf.append("%");
+ final char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, RADIX));
+ final char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, RADIX));
+ buf.append(hex1);
+ buf.append(hex2);
+ }
+ }
+ return buf.toString();
+ }
+
+}
diff --git a/src/main/java/com/github/example/pt/util/edazdarevic/commons/net/CIDRUtil.java b/src/main/java/com/github/example/pt/util/edazdarevic/commons/net/CIDRUtil.java
new file mode 100644
index 0000000..68cc7af
--- /dev/null
+++ b/src/main/java/com/github/example/pt/util/edazdarevic/commons/net/CIDRUtil.java
@@ -0,0 +1,142 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2013 Edin Dazdarevic (edin.dazdarevic@gmail.com)
+
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * */
+
+package com.github.example.pt.util.edazdarevic.commons.net;
+
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class that enables to get an IP range from CIDR specification. It supports
+ * both IPv4 and IPv6.
+ */
+@SuppressWarnings("all")
+public class CIDRUtil {
+ private final String cidr;
+ private final int prefixLength;
+ private InetAddress inetAddress;
+ private InetAddress startAddress;
+ private InetAddress endAddress;
+
+
+ public CIDRUtil(String cidr) throws UnknownHostException {
+
+ this.cidr = cidr;
+
+ /* split CIDR to address and prefix part */
+ if (this.cidr.contains("/")) {
+ int index = this.cidr.indexOf("/");
+ String addressPart = this.cidr.substring(0, index);
+ String networkPart = this.cidr.substring(index + 1);
+
+ inetAddress = InetAddress.getByName(addressPart);
+ prefixLength = Integer.parseInt(networkPart);
+
+ calculate();
+ } else {
+ throw new IllegalArgumentException("not an valid CIDR format!");
+ }
+ }
+
+
+ private void calculate() throws UnknownHostException {
+
+ ByteBuffer maskBuffer;
+ int targetSize;
+ if (inetAddress.getAddress().length == 4) {
+ maskBuffer =
+ ByteBuffer
+ .allocate(4)
+ .putInt(-1);
+ targetSize = 4;
+ } else {
+ maskBuffer = ByteBuffer.allocate(16)
+ .putLong(-1L)
+ .putLong(-1L);
+ targetSize = 16;
+ }
+
+ BigInteger mask = (new BigInteger(1, maskBuffer.array())).not().shiftRight(prefixLength);
+
+ ByteBuffer buffer = ByteBuffer.wrap(inetAddress.getAddress());
+ BigInteger ipVal = new BigInteger(1, buffer.array());
+
+ BigInteger startIp = ipVal.and(mask);
+ BigInteger endIp = startIp.add(mask.not());
+
+ byte[] startIpArr = toBytes(startIp.toByteArray(), targetSize);
+ byte[] endIpArr = toBytes(endIp.toByteArray(), targetSize);
+
+ this.startAddress = InetAddress.getByAddress(startIpArr);
+ this.endAddress = InetAddress.getByAddress(endIpArr);
+
+ }
+
+ private byte[] toBytes(byte[] array, int targetSize) {
+ int counter = 0;
+ List<Byte> newArr = new ArrayList<Byte>();
+ while (counter < targetSize && (array.length - 1 - counter >= 0)) {
+ newArr.add(0, array[array.length - 1 - counter]);
+ counter++;
+ }
+
+ int size = newArr.size();
+ for (int i = 0; i < (targetSize - size); i++) {
+
+ newArr.add(0, (byte) 0);
+ }
+
+ byte[] ret = new byte[newArr.size()];
+ for (int i = 0; i < newArr.size(); i++) {
+ ret[i] = newArr.get(i);
+ }
+ return ret;
+ }
+
+ public String getNetworkAddress() {
+
+ return this.startAddress.getHostAddress();
+ }
+
+ public String getBroadcastAddress() {
+ return this.endAddress.getHostAddress();
+ }
+
+ public boolean isInRange(String ipAddress) throws UnknownHostException {
+ InetAddress address = InetAddress.getByName(ipAddress);
+ BigInteger start = new BigInteger(1, this.startAddress.getAddress());
+ BigInteger end = new BigInteger(1, this.endAddress.getAddress());
+ BigInteger target = new BigInteger(1, address.getAddress());
+
+ int st = start.compareTo(target);
+ int te = target.compareTo(end);
+
+ return (st == -1 || st == 0) && (te == -1 || te == 0);
+ }
+}
diff --git a/src/main/resources/ppt.sql b/src/main/resources/ppt.sql
new file mode 100644
index 0000000..1181c77
--- /dev/null
+++ b/src/main/resources/ppt.sql
@@ -0,0 +1,729 @@
+/*
+ Navicat Premium Dump SQL
+
+ Source Server : first
+ Source Server Type : MySQL
+ Source Server Version : 80036 (8.0.36)
+ Source Host : localhost:3306
+ Source Schema : ppt
+
+ Target Server Type : MySQL
+ Target Server Version : 80036 (8.0.36)
+ File Encoding : 65001
+
+ Date: 04/06/2025 14:33:40
+*/
+
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ----------------------------
+-- Table structure for categories
+-- ----------------------------
+DROP TABLE IF EXISTS `categories`;
+CREATE TABLE `categories` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_categories_slug`(`slug` ASC) USING BTREE,
+ UNIQUE INDEX `UKoul14ho7bctbefv8jywp5v3i2`(`slug` ASC) USING BTREE,
+ INDEX `idx_categories_slug`(`slug` ASC) USING BTREE,
+ INDEX `IDXoul14ho7bctbefv8jywp5v3i2`(`slug` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of categories
+-- ----------------------------
+INSERT INTO `categories` VALUES (1, 'os', '操作系统', 'os-icon.png');
+
+-- ----------------------------
+-- Table structure for exam_plans
+-- ----------------------------
+DROP TABLE IF EXISTS `exam_plans`;
+CREATE TABLE `exam_plans` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `display_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `downloaded` bigint NOT NULL,
+ `duration` bigint NOT NULL,
+ `karma` double NOT NULL,
+ `seeding_time` bigint NOT NULL,
+ `seeds` bigint NOT NULL,
+ `share_ratio` double NOT NULL,
+ `slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `uploaded` bigint NOT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `UK74jaepieaj2umswpqn16yy3x5`(`slug` ASC) USING BTREE,
+ INDEX `IDX74jaepieaj2umswpqn16yy3x5`(`slug` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of exam_plans
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for exams
+-- ----------------------------
+DROP TABLE IF EXISTS `exams`;
+CREATE TABLE `exams` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `end_at` datetime(6) NOT NULL,
+ `exam_plan_id` bigint NULL DEFAULT NULL,
+ `user_id` bigint NULL DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `UK2wii0igd3vdfecy00op0un4qt`(`user_id` ASC) USING BTREE,
+ INDEX `IDX2wii0igd3vdfecy00op0un4qt`(`user_id` ASC) USING BTREE,
+ INDEX `FKl0cips1gany3cuppyfs7ntpai`(`exam_plan_id` ASC) USING BTREE,
+ CONSTRAINT `FKi63cpl1xkgy32iq68ru4ypjn4` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
+ CONSTRAINT `FKl0cips1gany3cuppyfs7ntpai` FOREIGN KEY (`exam_plan_id`) REFERENCES `exam_plans` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of exams
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for login_history
+-- ----------------------------
+DROP TABLE IF EXISTS `login_history`;
+CREATE TABLE `login_history` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `user_id` bigint NOT NULL,
+ `time` timestamp NOT NULL,
+ `type` enum('ACCOUNT','PASSKEY','PERSONAL_ACCESSTOKEN','PROGRAM_INTERNAL','TWO_STEP_VERIFICATION') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `ip_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `user_agent` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ INDEX `idx_login_time`(`time` ASC) USING BTREE,
+ INDEX `fk_loginhistory_user`(`user_id` ASC) USING BTREE,
+ INDEX `IDX3lft44makrxommxm63k7xj77d`(`time` ASC) USING BTREE,
+ CONSTRAINT `fk_loginhistory_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 33 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of login_history
+-- ----------------------------
+INSERT INTO `login_history` VALUES (1, 1, '2025-06-03 12:25:33', 'ACCOUNT', '0:0:0:0:0:0:0:1', 'PostmanRuntime/7.43.4', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (2, 1, '2025-06-03 12:51:42', 'ACCOUNT', '0:0:0:0:0:0:0:1', 'PostmanRuntime/7.43.4', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (3, 1, '2025-06-03 12:52:55', 'ACCOUNT', '0:0:0:0:0:0:0:1', 'PostmanRuntime/7.43.4', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (4, 1, '2025-06-03 14:20:55', 'ACCOUNT', '0:0:0:0:0:0:0:1', 'PostmanRuntime/7.43.4', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (5, 1, '2025-06-03 14:31:13', 'ACCOUNT', '0:0:0:0:0:0:0:1', 'PostmanRuntime/7.43.4', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (6, 1, '2025-06-04 03:40:10', 'ACCOUNT', '0:0:0:0:0:0:0:1', 'PostmanRuntime/7.44.0', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (7, 1, '2025-06-04 03:43:09', 'PASSKEY', '192.168.10.10', 'qBittorrent/5.1.0', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (8, 1, '2025-06-04 03:44:27', 'PASSKEY', '192.168.10.10', 'qBittorrent/5.1.0', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (9, 1, '2025-06-04 03:51:35', 'PASSKEY', '192.168.10.10', 'qBittorrent/5.1.0', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (10, 1, '2025-06-04 03:52:22', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (11, 1, '2025-06-04 03:52:53', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (12, 1, '2025-06-04 03:53:12', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (13, 1, '2025-06-04 03:54:46', 'PASSKEY', '192.168.10.10', 'qBittorrent/5.1.0', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (14, 1, '2025-06-04 03:58:14', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (15, 1, '2025-06-04 04:22:30', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (16, 1, '2025-06-04 04:43:29', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (17, 1, '2025-06-04 04:54:09', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (18, 1, '2025-06-04 04:59:13', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (19, 1, '2025-06-04 05:17:07', 'PASSKEY', '192.168.10.10', 'qBittorrent/5.1.0', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (20, 1, '2025-06-04 05:18:28', 'PASSKEY', '192.168.10.10', 'qBittorrent/5.1.0', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (21, 1, '2025-06-04 05:21:25', 'PASSKEY', '192.168.10.10', 'qBittorrent/5.1.0', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (22, 1, '2025-06-04 05:21:58', 'PASSKEY', '192.168.10.10', 'qBittorrent/5.1.0', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (23, 1, '2025-06-04 05:22:24', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (24, 1, '2025-06-04 05:22:30', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (25, 1, '2025-06-04 05:27:44', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (26, 1, '2025-06-04 05:27:50', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (27, 1, '2025-06-04 05:31:45', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (28, 1, '2025-06-04 05:33:00', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (29, 1, '2025-06-04 06:11:31', 'PASSKEY', '192.168.254.1', 'qBittorrent/5.1.0', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (30, 1, '2025-06-04 06:12:13', 'PASSKEY', '192.168.254.1', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (31, 1, '2025-06-04 06:12:27', 'PASSKEY', '192.168.254.1', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (32, 1, '2025-06-04 06:13:21', 'PASSKEY', '192.168.254.1', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+
+-- ----------------------------
+-- Table structure for peers
+-- ----------------------------
+DROP TABLE IF EXISTS `peers`;
+CREATE TABLE `peers` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `port` int NOT NULL,
+ `info_hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `peer_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `user_agent` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `uploaded` bigint NOT NULL,
+ `downloaded` bigint NOT NULL,
+ `to_go` bigint NOT NULL,
+ `seeder` tinyint(1) NOT NULL,
+ `partial_seeder` tinyint(1) NOT NULL,
+ `passkey` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `update_at` timestamp NOT NULL,
+ `seeding_time` bigint NOT NULL,
+ `upload_speed` bigint NOT NULL,
+ `download_speed` bigint NOT NULL,
+ `user_id` bigint NULL DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_peers_ip_port_infohash`(`ip` ASC, `port` ASC, `info_hash` ASC) USING BTREE,
+ UNIQUE INDEX `UKoa8l3xqdvxr898mosks3hq3cb`(`ip` ASC, `port` ASC, `info_hash` ASC) USING BTREE,
+ INDEX `idx_peers_update_at`(`update_at` ASC) USING BTREE,
+ INDEX `fk_peers_user`(`user_id` ASC) USING BTREE,
+ INDEX `IDXmmvk33liy7j5u9e4qhxw2d7h5`(`update_at` ASC) USING BTREE,
+ CONSTRAINT `fk_peers_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+
+-- ----------------------------
+-- Table structure for permissions
+-- ----------------------------
+DROP TABLE IF EXISTS `permissions`;
+CREATE TABLE `permissions` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `def` tinyint(1) NOT NULL,
+ `user_group_id` bigint NULL DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_permissions_slug`(`slug` ASC) USING BTREE,
+ UNIQUE INDEX `UKdv6mwikptsu70hcrjq07sqsfy`(`slug` ASC) USING BTREE,
+ INDEX `fk_permissions_user_group`(`user_group_id` ASC) USING BTREE,
+ CONSTRAINT `fk_permissions_user_group` FOREIGN KEY (`user_group_id`) REFERENCES `user_groups` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of permissions
+-- ----------------------------
+INSERT INTO `permissions` VALUES (1, 'torrent:upload', 1, 1);
+INSERT INTO `permissions` VALUES (2, 'torrent:download', 1, 1);
+INSERT INTO `permissions` VALUES (3, 'torrent:view', 1, 1);
+INSERT INTO `permissions` VALUES (4, 'torrent:search', 1, 1);
+INSERT INTO `permissions` VALUES (5, 'comment:create', 1, 1);
+INSERT INTO `permissions` VALUES (6, 'user:manage', 1, 1);
+INSERT INTO `permissions` VALUES (7, 'torrent:approve', 1, 1);
+INSERT INTO `permissions` VALUES (8, 'torrent:thanks', 1, 1);
+INSERT INTO `permissions` VALUES (9, 'promotion:list', 1, 1);
+
+-- ----------------------------
+-- Table structure for promotion_policies
+-- ----------------------------
+DROP TABLE IF EXISTS `promotion_policies`;
+CREATE TABLE `promotion_policies` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `display_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `upload_ratio` double NULL DEFAULT NULL,
+ `download_ratio` double NULL DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_promotion_policies_slug`(`slug` ASC) USING BTREE,
+ UNIQUE INDEX `UKcjqpe1g15outfc0u6ajvpwxoe`(`slug` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of promotion_policies
+-- ----------------------------
+INSERT INTO `promotion_policies` VALUES (1, 'default', '默认策略', 1, 1);
+INSERT INTO `promotion_policies` VALUES (2, 'vip', '活跃用户策略', 1, 1.5);
+
+-- ----------------------------
+-- Table structure for qrtz_blob_triggers
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_blob_triggers`;
+CREATE TABLE `qrtz_blob_triggers` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `BLOB_DATA` blob NULL,
+ PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE,
+ INDEX `SCHED_NAME`(`SCHED_NAME` ASC, `TRIGGER_NAME` ASC, `TRIGGER_GROUP` ASC) USING BTREE,
+ CONSTRAINT `qrtz_blob_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_blob_triggers
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for qrtz_calendars
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_calendars`;
+CREATE TABLE `qrtz_calendars` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `CALENDAR_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `CALENDAR` blob NOT NULL,
+ PRIMARY KEY (`SCHED_NAME`, `CALENDAR_NAME`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_calendars
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for qrtz_cron_triggers
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_cron_triggers`;
+CREATE TABLE `qrtz_cron_triggers` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `CRON_EXPRESSION` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TIME_ZONE_ID` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE,
+ CONSTRAINT `qrtz_cron_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_cron_triggers
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for qrtz_fired_triggers
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_fired_triggers`;
+CREATE TABLE `qrtz_fired_triggers` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `ENTRY_ID` varchar(95) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `INSTANCE_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `FIRED_TIME` bigint NOT NULL,
+ `SCHED_TIME` bigint NOT NULL,
+ `PRIORITY` int NOT NULL,
+ `STATE` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `JOB_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `JOB_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `IS_NONCONCURRENT` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `REQUESTS_RECOVERY` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ PRIMARY KEY (`SCHED_NAME`, `ENTRY_ID`) USING BTREE,
+ INDEX `IDX_QRTZ_FT_TRIG_INST_NAME`(`SCHED_NAME` ASC, `INSTANCE_NAME` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_FT_INST_JOB_REQ_RCVRY`(`SCHED_NAME` ASC, `INSTANCE_NAME` ASC, `REQUESTS_RECOVERY` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_FT_J_G`(`SCHED_NAME` ASC, `JOB_NAME` ASC, `JOB_GROUP` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_FT_JG`(`SCHED_NAME` ASC, `JOB_GROUP` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_FT_T_G`(`SCHED_NAME` ASC, `TRIGGER_NAME` ASC, `TRIGGER_GROUP` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_FT_TG`(`SCHED_NAME` ASC, `TRIGGER_GROUP` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_fired_triggers
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for qrtz_job_details
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_job_details`;
+CREATE TABLE `qrtz_job_details` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `JOB_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `JOB_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `DESCRIPTION` varchar(250) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `JOB_CLASS_NAME` varchar(250) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `IS_DURABLE` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `IS_NONCONCURRENT` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `IS_UPDATE_DATA` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `REQUESTS_RECOVERY` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `JOB_DATA` blob NULL,
+ PRIMARY KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) USING BTREE,
+ INDEX `IDX_QRTZ_J_REQ_RECOVERY`(`SCHED_NAME` ASC, `REQUESTS_RECOVERY` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_J_GRP`(`SCHED_NAME` ASC, `JOB_GROUP` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_job_details
+-- ----------------------------
+INSERT INTO `qrtz_job_details` VALUES ('sapling_scheduler', 'peers_cleanup', 'DEFAULT', 'Peers Cleanup', 'com.github.example.pt.crontask.PeersCleanup', '1', '0', '0', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787000737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F40000000000010770800000010000000007800);
+
+-- ----------------------------
+-- Table structure for qrtz_locks
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_locks`;
+CREATE TABLE `qrtz_locks` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `LOCK_NAME` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ PRIMARY KEY (`SCHED_NAME`, `LOCK_NAME`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_locks
+-- ----------------------------
+INSERT INTO `qrtz_locks` VALUES ('sapling_scheduler', 'STATE_ACCESS');
+INSERT INTO `qrtz_locks` VALUES ('sapling_scheduler', 'TRIGGER_ACCESS');
+
+-- ----------------------------
+-- Table structure for qrtz_paused_trigger_grps
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_paused_trigger_grps`;
+CREATE TABLE `qrtz_paused_trigger_grps` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ PRIMARY KEY (`SCHED_NAME`, `TRIGGER_GROUP`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_paused_trigger_grps
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for qrtz_scheduler_state
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_scheduler_state`;
+CREATE TABLE `qrtz_scheduler_state` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `INSTANCE_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `LAST_CHECKIN_TIME` bigint NOT NULL,
+ `CHECKIN_INTERVAL` bigint NOT NULL,
+ PRIMARY KEY (`SCHED_NAME`, `INSTANCE_NAME`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_scheduler_state
+-- ----------------------------
+INSERT INTO `qrtz_scheduler_state` VALUES ('sapling_scheduler', 'LAPTOP-V24K5A551749018489588', 1749018510182, 10000);
+
+-- ----------------------------
+-- Table structure for qrtz_simple_triggers
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_simple_triggers`;
+CREATE TABLE `qrtz_simple_triggers` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `REPEAT_COUNT` bigint NOT NULL,
+ `REPEAT_INTERVAL` bigint NOT NULL,
+ `TIMES_TRIGGERED` bigint NOT NULL,
+ PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE,
+ CONSTRAINT `qrtz_simple_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_simple_triggers
+-- ----------------------------
+INSERT INTO `qrtz_simple_triggers` VALUES ('sapling_scheduler', '6da64b5bd2ee-37ffa82a-a598-485c-9718-40ee90c26bc4', 'DEFAULT', -1, 1800000, 1);
+
+-- ----------------------------
+-- Table structure for qrtz_simprop_triggers
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_simprop_triggers`;
+CREATE TABLE `qrtz_simprop_triggers` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `STR_PROP_1` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `STR_PROP_2` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `STR_PROP_3` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `INT_PROP_1` int NULL DEFAULT NULL,
+ `INT_PROP_2` int NULL DEFAULT NULL,
+ `LONG_PROP_1` bigint NULL DEFAULT NULL,
+ `LONG_PROP_2` bigint NULL DEFAULT NULL,
+ `DEC_PROP_1` decimal(13, 4) NULL DEFAULT NULL,
+ `DEC_PROP_2` decimal(13, 4) NULL DEFAULT NULL,
+ `BOOL_PROP_1` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `BOOL_PROP_2` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE,
+ CONSTRAINT `qrtz_simprop_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_simprop_triggers
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for qrtz_triggers
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_triggers`;
+CREATE TABLE `qrtz_triggers` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `JOB_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `JOB_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `DESCRIPTION` varchar(250) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `NEXT_FIRE_TIME` bigint NULL DEFAULT NULL,
+ `PREV_FIRE_TIME` bigint NULL DEFAULT NULL,
+ `PRIORITY` int NULL DEFAULT NULL,
+ `TRIGGER_STATE` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_TYPE` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `START_TIME` bigint NOT NULL,
+ `END_TIME` bigint NULL DEFAULT NULL,
+ `CALENDAR_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `MISFIRE_INSTR` smallint NULL DEFAULT NULL,
+ `JOB_DATA` blob NULL,
+ PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE,
+ INDEX `IDX_QRTZ_T_J`(`SCHED_NAME` ASC, `JOB_NAME` ASC, `JOB_GROUP` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_JG`(`SCHED_NAME` ASC, `JOB_GROUP` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_C`(`SCHED_NAME` ASC, `CALENDAR_NAME` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_G`(`SCHED_NAME` ASC, `TRIGGER_GROUP` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_STATE`(`SCHED_NAME` ASC, `TRIGGER_STATE` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_N_STATE`(`SCHED_NAME` ASC, `TRIGGER_NAME` ASC, `TRIGGER_GROUP` ASC, `TRIGGER_STATE` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_N_G_STATE`(`SCHED_NAME` ASC, `TRIGGER_GROUP` ASC, `TRIGGER_STATE` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_NEXT_FIRE_TIME`(`SCHED_NAME` ASC, `NEXT_FIRE_TIME` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_NFT_ST`(`SCHED_NAME` ASC, `TRIGGER_STATE` ASC, `NEXT_FIRE_TIME` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_NFT_MISFIRE`(`SCHED_NAME` ASC, `MISFIRE_INSTR` ASC, `NEXT_FIRE_TIME` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_NFT_ST_MISFIRE`(`SCHED_NAME` ASC, `MISFIRE_INSTR` ASC, `NEXT_FIRE_TIME` ASC, `TRIGGER_STATE` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_NFT_ST_MISFIRE_GRP`(`SCHED_NAME` ASC, `MISFIRE_INSTR` ASC, `NEXT_FIRE_TIME` ASC, `TRIGGER_GROUP` ASC, `TRIGGER_STATE` ASC) USING BTREE,
+ CONSTRAINT `qrtz_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) REFERENCES `qrtz_job_details` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_triggers
+-- ----------------------------
+INSERT INTO `qrtz_triggers` VALUES ('sapling_scheduler', '6da64b5bd2ee-37ffa82a-a598-485c-9718-40ee90c26bc4', 'DEFAULT', 'peers_cleanup', 'DEFAULT', NULL, 1749020288505, 1749018488505, 5, 'WAITING', 'SIMPLE', 1749018488505, 0, NULL, 0, '');
+
+-- ----------------------------
+-- Table structure for seedbox
+-- ----------------------------
+DROP TABLE IF EXISTS `seedbox`;
+CREATE TABLE `seedbox` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `download_multiplier_id` bigint NULL DEFAULT NULL,
+ `upload_multiplier_id` bigint NULL DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `UKecbehjtg3bfr4rmyixt1gcvfq`(`address` ASC) USING BTREE,
+ INDEX `FKq63tqhykgl96l4rkwy9widy5b`(`download_multiplier_id` ASC) USING BTREE,
+ INDEX `FK9xl51na3dn8k7ou8cyv7s1wrf`(`upload_multiplier_id` ASC) USING BTREE,
+ CONSTRAINT `FK9xl51na3dn8k7ou8cyv7s1wrf` FOREIGN KEY (`upload_multiplier_id`) REFERENCES `promotion_policies` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
+ CONSTRAINT `FKq63tqhykgl96l4rkwy9widy5b` FOREIGN KEY (`download_multiplier_id`) REFERENCES `promotion_policies` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of seedbox
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for settings
+-- ----------------------------
+DROP TABLE IF EXISTS `settings`;
+CREATE TABLE `settings` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `setting_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `setting_value` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_settings_setting_key`(`setting_key` ASC) USING BTREE,
+ UNIQUE INDEX `UKswd05dvj4ukvw5q135bpbbfae`(`setting_key` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for tags
+-- ----------------------------
+DROP TABLE IF EXISTS `tags`;
+CREATE TABLE `tags` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_tags_name`(`name` ASC) USING BTREE,
+ UNIQUE INDEX `UKt48xdq560gs3gap9g7jg36kgc`(`name` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of tags
+-- ----------------------------
+INSERT INTO `tags` VALUES (9, '[\"linux\"]');
+INSERT INTO `tags` VALUES (3, 'debian');
+INSERT INTO `tags` VALUES (1, 'linux');
+INSERT INTO `tags` VALUES (5, 'macos');
+INSERT INTO `tags` VALUES (2, 'ubuntu');
+INSERT INTO `tags` VALUES (4, 'windows');
+
+-- ----------------------------
+-- Table structure for thanks
+-- ----------------------------
+DROP TABLE IF EXISTS `thanks`;
+CREATE TABLE `thanks` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `torrent_id` bigint NULL DEFAULT NULL,
+ `user_id` bigint NULL DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `UKrt9lg0h53brgpf9iat5hcmf6g`(`user_id` ASC, `torrent_id` ASC) USING BTREE,
+ INDEX `FKp3kgh25tko48vq7x6u2w3dvpp`(`torrent_id` ASC) USING BTREE,
+ CONSTRAINT `FK2t90h21s5hyx6hynewsdlk46j` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
+ CONSTRAINT `FKp3kgh25tko48vq7x6u2w3dvpp` FOREIGN KEY (`torrent_id`) REFERENCES `torrents` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of thanks
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for torrents
+-- ----------------------------
+DROP TABLE IF EXISTS `torrents`;
+CREATE TABLE `torrents` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `info_hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `user_id` bigint NULL DEFAULT NULL,
+ `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `sub_title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `size` bigint NOT NULL,
+ `created_at` timestamp NOT NULL,
+ `updated_at` timestamp NOT NULL,
+ `under_review` tinyint(1) NOT NULL,
+ `anonymous` tinyint(1) NOT NULL,
+ `category_id` bigint NULL DEFAULT NULL,
+ `promotion_policy_id` bigint NULL DEFAULT NULL,
+ `description` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_torrents_info_hash`(`info_hash` ASC) USING BTREE,
+ UNIQUE INDEX `UKhag2ej1vo8snvirb1lv4b8r4x`(`info_hash` ASC) USING BTREE,
+ INDEX `idx_torrents_title`(`title` ASC) USING BTREE,
+ INDEX `idx_torrents_sub_title`(`sub_title` ASC) USING BTREE,
+ INDEX `idx_torrents_promotion_policy_id`(`promotion_policy_id` ASC) USING BTREE,
+ INDEX `fk_torrents_user`(`user_id` ASC) USING BTREE,
+ INDEX `fk_torrents_category`(`category_id` ASC) USING BTREE,
+ INDEX `IDXdplkaapqslelnscuunfpm9eb6`(`title` ASC) USING BTREE,
+ INDEX `IDX6r5kh6i4awpdlytjmm06pk22k`(`sub_title` ASC) USING BTREE,
+ INDEX `IDXd2j3h8td7682cctkv5o77b33y`(`promotion_policy_id` ASC) USING BTREE,
+ CONSTRAINT `fk_torrents_category` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT,
+ CONSTRAINT `fk_torrents_promotion_policy` FOREIGN KEY (`promotion_policy_id`) REFERENCES `promotion_policies` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT,
+ CONSTRAINT `fk_torrents_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of torrents
+-- ----------------------------
+INSERT INTO `torrents` VALUES (7, '499e9c69e90b976c5c84542cf9b88fad1e12ef1c', 1, 'example torrent title', 'subtitle here', 66194, '2025-06-04 03:41:27', '2025-06-04 03:41:27', 0, 0, 1, 1, '演示种子');
+INSERT INTO `torrents` VALUES (8, 'f45775564c88b5a2782a301d162e1d4811b0b6d5', 1, 'example torrent title', 'subtitle here', 231105, '2025-06-04 05:20:37', '2025-06-04 05:20:37', 0, 0, 1, 1, '演示种子');
+INSERT INTO `torrents` VALUES (9, '8e10c9daa1b5fb18dc5b0e73988808ba958391fa', 1, 'example torrent title', 'subtitle here', 426155, '2025-06-04 06:03:49', '2025-06-04 06:03:49', 0, 0, 1, 1, '演示种子');
+INSERT INTO `torrents` VALUES (10, '1e3275f54ec074af2d99828311d4a8488eb7a487', 1, 'example torrent title', 'subtitle here', 13861694, '2025-06-04 06:10:23', '2025-06-04 06:10:23', 0, 0, 1, 1, '演示种子');
+
+-- ----------------------------
+-- Table structure for torrents_tag
+-- ----------------------------
+DROP TABLE IF EXISTS `torrents_tag`;
+CREATE TABLE `torrents_tag` (
+ `torrent_id` bigint NOT NULL,
+ `tag_id` bigint NOT NULL,
+ PRIMARY KEY (`torrent_id`, `tag_id`) USING BTREE,
+ INDEX `fk_torrents_tag_tag`(`tag_id` ASC) USING BTREE,
+ CONSTRAINT `fk_torrents_tag_tag` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
+ CONSTRAINT `fk_torrents_tag_torrent` FOREIGN KEY (`torrent_id`) REFERENCES `torrents` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of torrents_tag
+-- ----------------------------
+INSERT INTO `torrents_tag` VALUES (7, 9);
+INSERT INTO `torrents_tag` VALUES (8, 9);
+INSERT INTO `torrents_tag` VALUES (9, 9);
+INSERT INTO `torrents_tag` VALUES (10, 9);
+
+-- ----------------------------
+-- Table structure for transfer_history
+-- ----------------------------
+DROP TABLE IF EXISTS `transfer_history`;
+CREATE TABLE `transfer_history` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `user_id` bigint NOT NULL,
+ `torrent_id` bigint NOT NULL,
+ `to_go` bigint NOT NULL,
+ `started_at` timestamp NOT NULL,
+ `updated_at` timestamp NOT NULL,
+ `uploaded` bigint NOT NULL,
+ `downloaded` bigint NOT NULL,
+ `actual_uploaded` bigint NOT NULL,
+ `actual_downloaded` bigint NOT NULL,
+ `upload_speed` bigint NOT NULL,
+ `download_speed` bigint NOT NULL,
+ `last_event` enum('started','completed','stopped','paused','unknown') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `have_complete_history` tinyint(1) NOT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_user_torrent`(`user_id` ASC, `torrent_id` ASC) USING BTREE,
+ UNIQUE INDEX `UKrm5p4xv3rb2vm6psql5je94jh`(`user_id` ASC, `torrent_id` ASC) USING BTREE,
+ INDEX `fk_transfer_torrent`(`torrent_id` ASC) USING BTREE,
+ CONSTRAINT `fk_transfer_torrent` FOREIGN KEY (`torrent_id`) REFERENCES `torrents` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
+ CONSTRAINT `fk_transfer_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of transfer_history
+-- ----------------------------
+INSERT INTO `transfer_history` VALUES (1, 1, 7, 66194, '2025-06-04 04:43:30', '2025-06-04 05:21:25', 0, 0, 0, 0, 0, 0, 'completed', 1);
+INSERT INTO `transfer_history` VALUES (2, 1, 10, 0, '2025-06-04 06:11:31', '2025-06-04 06:13:21', 27723388, 0, 0, 27723388, 0, 77874, 'started', 1);
+
+-- ----------------------------
+-- Table structure for user_groups
+-- ----------------------------
+DROP TABLE IF EXISTS `user_groups`;
+CREATE TABLE `user_groups` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `display_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `promotion_policy_id` bigint NULL DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_user_groups_slug`(`slug` ASC) USING BTREE,
+ UNIQUE INDEX `UKkkje1jbmrkam1k7jbd39homw0`(`slug` ASC) USING BTREE,
+ INDEX `fk_user_groups_promotion_policy`(`promotion_policy_id` ASC) USING BTREE,
+ CONSTRAINT `fk_user_groups_promotion_policy` FOREIGN KEY (`promotion_policy_id`) REFERENCES `promotion_policies` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of user_groups
+-- ----------------------------
+INSERT INTO `user_groups` VALUES (1, 'default', '默认用户组', 1);
+INSERT INTO `user_groups` VALUES (2, 'vip', '活跃用户组', 2);
+
+-- ----------------------------
+-- Table structure for user_groups_permission_entities
+-- ----------------------------
+DROP TABLE IF EXISTS `user_groups_permission_entities`;
+CREATE TABLE `user_groups_permission_entities` (
+ `user_group_id` bigint NOT NULL,
+ `permission_entities_id` bigint NOT NULL,
+ PRIMARY KEY (`user_group_id`, `permission_entities_id`) USING BTREE,
+ UNIQUE INDEX `UK_95j9uaq96bvd9ykn0sngywks6`(`permission_entities_id` ASC) USING BTREE,
+ CONSTRAINT `fk_ugpe_permission` FOREIGN KEY (`permission_entities_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
+ CONSTRAINT `fk_ugpe_user_group` FOREIGN KEY (`user_group_id`) REFERENCES `user_groups` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of user_groups_permission_entities
+-- ----------------------------
+INSERT INTO `user_groups_permission_entities` VALUES (1, 1);
+INSERT INTO `user_groups_permission_entities` VALUES (1, 2);
+INSERT INTO `user_groups_permission_entities` VALUES (1, 3);
+INSERT INTO `user_groups_permission_entities` VALUES (1, 4);
+INSERT INTO `user_groups_permission_entities` VALUES (1, 5);
+INSERT INTO `user_groups_permission_entities` VALUES (1, 6);
+INSERT INTO `user_groups_permission_entities` VALUES (1, 7);
+INSERT INTO `user_groups_permission_entities` VALUES (1, 8);
+INSERT INTO `user_groups_permission_entities` VALUES (1, 9);
+
+-- ----------------------------
+-- Table structure for users
+-- ----------------------------
+DROP TABLE IF EXISTS `users`;
+CREATE TABLE `users` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `group_id` bigint NULL DEFAULT NULL,
+ `passkey` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `create_at` timestamp NOT NULL,
+ `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `custom_title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `signature` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `language` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `download_bandwidth` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `upload_bandwidth` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `downloaded` bigint NOT NULL,
+ `uploaded` bigint NOT NULL,
+ `real_downloaded` bigint NOT NULL,
+ `real_uploaded` bigint NOT NULL,
+ `isp` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `karma` decimal(19, 2) NOT NULL,
+ `invite_slot` int NOT NULL,
+ `seeding_time` bigint NOT NULL,
+ `personal_access_token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `privacy_level` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_users_username`(`username` ASC) USING BTREE,
+ UNIQUE INDEX `uk_users_email`(`email` ASC) USING BTREE,
+ UNIQUE INDEX `uk_users_passkey`(`passkey` ASC) USING BTREE,
+ UNIQUE INDEX `UKr43af9ap4edm43mmtq01oddj6`(`username` ASC) USING BTREE,
+ UNIQUE INDEX `UK6dotkott2kjsp8vw4d0m25fb7`(`email` ASC) USING BTREE,
+ UNIQUE INDEX `UK402sx6cqgk66uwt7eyf54tmij`(`passkey` ASC) USING BTREE,
+ INDEX `fk_users_group`(`group_id` ASC) USING BTREE,
+ CONSTRAINT `fk_users_group` FOREIGN KEY (`group_id`) REFERENCES `user_groups` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of users
+-- ----------------------------
+INSERT INTO `users` VALUES (1, 'testuser@example.com', '$2a$06$JqvHpC94mnN23uwmZX0XHeuyFj3M22Ikw8VBZRopfrlWbPfA4KREu', 'testuser', 1, '47050e8f-f3fd-4a9c-a828-0e0de456691f', '2025-06-03 11:34:08', 'https://www.baidu.com/facivon.ico', '测试用户', '这个用户很懒,还没有个性签名', 'zh-CN', '100mbps', '100mbps', 0, 27723388, 27723388, 0, '未知', 0.00, 0, 30790068, '948c9146-ff5f-4ad6-8ba5-d32e1209cafd', '0');
+
+SET FOREIGN_KEY_CHECKS = 1;
diff --git a/src/main/resources/redisson.yaml b/src/main/resources/redisson.yaml
new file mode 100644
index 0000000..6d8f6d7
--- /dev/null
+++ b/src/main/resources/redisson.yaml
@@ -0,0 +1,2 @@
+singleServerConfig:
+ address: "redis://127.0.0.1:6379"
diff --git a/src/test/java/com/github/example/pt/ptApplicationTests.java b/src/test/java/com/github/example/pt/ptApplicationTests.java
new file mode 100644
index 0000000..3122f61
--- /dev/null
+++ b/src/test/java/com/github/example/pt/ptApplicationTests.java
@@ -0,0 +1,12 @@
+package com.github.example.pt;
+
+import org.junit.jupiter.api.Test;
+
+class ptApplicationTests {
+
+ @Test
+ void contextLoads() {
+ // Basic check to ensure test framework runs
+ System.out.println("Basic test passed.");
+ }
+}
diff --git a/target/classes/com/github/example/pt/autoconfig/JacksonConfig.class b/target/classes/com/github/example/pt/autoconfig/JacksonConfig.class
new file mode 100644
index 0000000..0e823f8
--- /dev/null
+++ b/target/classes/com/github/example/pt/autoconfig/JacksonConfig.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/autoconfig/JetcacheConfig.class b/target/classes/com/github/example/pt/autoconfig/JetcacheConfig.class
new file mode 100644
index 0000000..0a3a219
--- /dev/null
+++ b/target/classes/com/github/example/pt/autoconfig/JetcacheConfig.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/autoconfig/QuartzConfig$AutowiringSpringBeanJobFactory.class b/target/classes/com/github/example/pt/autoconfig/QuartzConfig$AutowiringSpringBeanJobFactory.class
new file mode 100644
index 0000000..29f91eb
--- /dev/null
+++ b/target/classes/com/github/example/pt/autoconfig/QuartzConfig$AutowiringSpringBeanJobFactory.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/autoconfig/QuartzConfig.class b/target/classes/com/github/example/pt/autoconfig/QuartzConfig.class
new file mode 100644
index 0000000..e25343b
--- /dev/null
+++ b/target/classes/com/github/example/pt/autoconfig/QuartzConfig.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/autoconfig/RedisConfig.class b/target/classes/com/github/example/pt/autoconfig/RedisConfig.class
new file mode 100644
index 0000000..1bf4a9e
--- /dev/null
+++ b/target/classes/com/github/example/pt/autoconfig/RedisConfig.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/autoconfig/SaTokenConfig.class b/target/classes/com/github/example/pt/autoconfig/SaTokenConfig.class
new file mode 100644
index 0000000..9344339
--- /dev/null
+++ b/target/classes/com/github/example/pt/autoconfig/SaTokenConfig.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/autoconfig/SafeHTMLConfig.class b/target/classes/com/github/example/pt/autoconfig/SafeHTMLConfig.class
new file mode 100644
index 0000000..27795fe
--- /dev/null
+++ b/target/classes/com/github/example/pt/autoconfig/SafeHTMLConfig.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/autoconfig/WorkDirectoryConfig.class b/target/classes/com/github/example/pt/autoconfig/WorkDirectoryConfig.class
new file mode 100644
index 0000000..2a3117e
--- /dev/null
+++ b/target/classes/com/github/example/pt/autoconfig/WorkDirectoryConfig.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/config/ApiPrinter.class b/target/classes/com/github/example/pt/config/ApiPrinter.class
new file mode 100644
index 0000000..9007cf5
--- /dev/null
+++ b/target/classes/com/github/example/pt/config/ApiPrinter.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/config/MailConfig.class b/target/classes/com/github/example/pt/config/MailConfig.class
new file mode 100644
index 0000000..3091d5c
--- /dev/null
+++ b/target/classes/com/github/example/pt/config/MailConfig.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/config/SecurityConfig.class b/target/classes/com/github/example/pt/config/SecurityConfig.class
new file mode 100644
index 0000000..554fba6
--- /dev/null
+++ b/target/classes/com/github/example/pt/config/SecurityConfig.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/config/SiteBasicConfig.class b/target/classes/com/github/example/pt/config/SiteBasicConfig.class
new file mode 100644
index 0000000..bdd9c21
--- /dev/null
+++ b/target/classes/com/github/example/pt/config/SiteBasicConfig.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/config/TrackerConfig.class b/target/classes/com/github/example/pt/config/TrackerConfig.class
new file mode 100644
index 0000000..b059e69
--- /dev/null
+++ b/target/classes/com/github/example/pt/config/TrackerConfig.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/DebugController$DebugPeer.class b/target/classes/com/github/example/pt/controller/DebugController$DebugPeer.class
new file mode 100644
index 0000000..355d0c6
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/DebugController$DebugPeer.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/DebugController$DebugTorrent.class b/target/classes/com/github/example/pt/controller/DebugController$DebugTorrent.class
new file mode 100644
index 0000000..22fc1b8
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/DebugController$DebugTorrent.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/DebugController.class b/target/classes/com/github/example/pt/controller/DebugController.class
new file mode 100644
index 0000000..36f44f1
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/DebugController.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/advice/GlobalControllerAdvice.class b/target/classes/com/github/example/pt/controller/advice/GlobalControllerAdvice.class
new file mode 100644
index 0000000..14c7374
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/advice/GlobalControllerAdvice.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/announce/AnnounceController$PeerResult.class b/target/classes/com/github/example/pt/controller/announce/AnnounceController$PeerResult.class
new file mode 100644
index 0000000..32f477b
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/announce/AnnounceController$PeerResult.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/announce/AnnounceController.class b/target/classes/com/github/example/pt/controller/announce/AnnounceController.class
new file mode 100644
index 0000000..4c5e952
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/announce/AnnounceController.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/auth/AuthController.class b/target/classes/com/github/example/pt/controller/auth/AuthController.class
new file mode 100644
index 0000000..4e64bf6
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/auth/AuthController.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/auth/dto/request/LoginRequestDTO.class b/target/classes/com/github/example/pt/controller/auth/dto/request/LoginRequestDTO.class
new file mode 100644
index 0000000..33bd779
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/auth/dto/request/LoginRequestDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/auth/dto/request/RegisterRequestDTO.class b/target/classes/com/github/example/pt/controller/auth/dto/request/RegisterRequestDTO.class
new file mode 100644
index 0000000..05fc197
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/auth/dto/request/RegisterRequestDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/category/CategoryController.class b/target/classes/com/github/example/pt/controller/category/CategoryController.class
new file mode 100644
index 0000000..b84a22a
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/category/CategoryController.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/dto/response/CategoryResponseDTO.class b/target/classes/com/github/example/pt/controller/dto/response/CategoryResponseDTO.class
new file mode 100644
index 0000000..0f17497
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/dto/response/CategoryResponseDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/dto/response/LoginStatusResponseDTO.class b/target/classes/com/github/example/pt/controller/dto/response/LoginStatusResponseDTO.class
new file mode 100644
index 0000000..87095b5
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/dto/response/LoginStatusResponseDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/dto/response/PeerInfoResponseDTO.class b/target/classes/com/github/example/pt/controller/dto/response/PeerInfoResponseDTO.class
new file mode 100644
index 0000000..82db9c4
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/dto/response/PeerInfoResponseDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/dto/response/PromotionResponseDTO.class b/target/classes/com/github/example/pt/controller/dto/response/PromotionResponseDTO.class
new file mode 100644
index 0000000..d709bed
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/dto/response/PromotionResponseDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/dto/response/ScrapeContainerDTO.class b/target/classes/com/github/example/pt/controller/dto/response/ScrapeContainerDTO.class
new file mode 100644
index 0000000..164d202
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/dto/response/ScrapeContainerDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/dto/response/TorrentBasicResponseDTO.class b/target/classes/com/github/example/pt/controller/dto/response/TorrentBasicResponseDTO.class
new file mode 100644
index 0000000..7f31360
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/dto/response/TorrentBasicResponseDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/dto/response/TorrentInfoResponseDTO.class b/target/classes/com/github/example/pt/controller/dto/response/TorrentInfoResponseDTO.class
new file mode 100644
index 0000000..29afc9e
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/dto/response/TorrentInfoResponseDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/dto/response/TransferHistoryDTO.class b/target/classes/com/github/example/pt/controller/dto/response/TransferHistoryDTO.class
new file mode 100644
index 0000000..fc8a3ab
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/dto/response/TransferHistoryDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/dto/response/UserBasicResponseDTO.class b/target/classes/com/github/example/pt/controller/dto/response/UserBasicResponseDTO.class
new file mode 100644
index 0000000..0c433f2
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/dto/response/UserBasicResponseDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/dto/response/UserGroupResponseDTO.class b/target/classes/com/github/example/pt/controller/dto/response/UserGroupResponseDTO.class
new file mode 100644
index 0000000..24b37b8
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/dto/response/UserGroupResponseDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/dto/response/UserResponseDTO.class b/target/classes/com/github/example/pt/controller/dto/response/UserResponseDTO.class
new file mode 100644
index 0000000..72232d0
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/dto/response/UserResponseDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/dto/response/UserSessionResponseDTO.class b/target/classes/com/github/example/pt/controller/dto/response/UserSessionResponseDTO.class
new file mode 100644
index 0000000..62d76fb
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/dto/response/UserSessionResponseDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/dto/response/UserTinyResponseDTO.class b/target/classes/com/github/example/pt/controller/dto/response/UserTinyResponseDTO.class
new file mode 100644
index 0000000..a928092
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/dto/response/UserTinyResponseDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/feed/FeedController.class b/target/classes/com/github/example/pt/controller/feed/FeedController.class
new file mode 100644
index 0000000..8abc4af
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/feed/FeedController.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/promotion/PromotionController.class b/target/classes/com/github/example/pt/controller/promotion/PromotionController.class
new file mode 100644
index 0000000..0c7c567
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/promotion/PromotionController.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/torrent/TorrentController.class b/target/classes/com/github/example/pt/controller/torrent/TorrentController.class
new file mode 100644
index 0000000..77f8437
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/torrent/TorrentController.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/torrent/dto/request/SearchTorrentRequestDTO.class b/target/classes/com/github/example/pt/controller/torrent/dto/request/SearchTorrentRequestDTO.class
new file mode 100644
index 0000000..c859017
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/torrent/dto/request/SearchTorrentRequestDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/torrent/dto/request/ThanksResponseDTO.class b/target/classes/com/github/example/pt/controller/torrent/dto/request/ThanksResponseDTO.class
new file mode 100644
index 0000000..cb052d5
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/torrent/dto/request/ThanksResponseDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/torrent/dto/request/TorrentScrapeRequestDTO.class b/target/classes/com/github/example/pt/controller/torrent/dto/request/TorrentScrapeRequestDTO.class
new file mode 100644
index 0000000..1becb2d
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/torrent/dto/request/TorrentScrapeRequestDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/torrent/dto/response/TorrentScrapeResponseDTO.class b/target/classes/com/github/example/pt/controller/torrent/dto/response/TorrentScrapeResponseDTO.class
new file mode 100644
index 0000000..72ca36f
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/torrent/dto/response/TorrentScrapeResponseDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/torrent/dto/response/TorrentSearchResultResponseDTO.class b/target/classes/com/github/example/pt/controller/torrent/dto/response/TorrentSearchResultResponseDTO.class
new file mode 100644
index 0000000..c9e38fc
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/torrent/dto/response/TorrentSearchResultResponseDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/torrent/dto/response/TorrentUploadSuccessResponseDTO.class b/target/classes/com/github/example/pt/controller/torrent/dto/response/TorrentUploadSuccessResponseDTO.class
new file mode 100644
index 0000000..2f33bef
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/torrent/dto/response/TorrentUploadSuccessResponseDTO.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/controller/torrent/form/TorrentUploadForm.class b/target/classes/com/github/example/pt/controller/torrent/form/TorrentUploadForm.class
new file mode 100644
index 0000000..c545ae5
--- /dev/null
+++ b/target/classes/com/github/example/pt/controller/torrent/form/TorrentUploadForm.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/crontask/PeersCleanup.class b/target/classes/com/github/example/pt/crontask/PeersCleanup.class
new file mode 100644
index 0000000..c80d93a
--- /dev/null
+++ b/target/classes/com/github/example/pt/crontask/PeersCleanup.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/entity/Category.class b/target/classes/com/github/example/pt/entity/Category.class
new file mode 100644
index 0000000..3c255d5
--- /dev/null
+++ b/target/classes/com/github/example/pt/entity/Category.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/entity/Exam.class b/target/classes/com/github/example/pt/entity/Exam.class
new file mode 100644
index 0000000..8473aba
--- /dev/null
+++ b/target/classes/com/github/example/pt/entity/Exam.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/entity/ExamPlan.class b/target/classes/com/github/example/pt/entity/ExamPlan.class
new file mode 100644
index 0000000..1d2d99d
--- /dev/null
+++ b/target/classes/com/github/example/pt/entity/ExamPlan.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/entity/LoginHistory.class b/target/classes/com/github/example/pt/entity/LoginHistory.class
new file mode 100644
index 0000000..0f6212a
--- /dev/null
+++ b/target/classes/com/github/example/pt/entity/LoginHistory.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/entity/Peer.class b/target/classes/com/github/example/pt/entity/Peer.class
new file mode 100644
index 0000000..3629440
--- /dev/null
+++ b/target/classes/com/github/example/pt/entity/Peer.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/entity/Permission.class b/target/classes/com/github/example/pt/entity/Permission.class
new file mode 100644
index 0000000..c9b4831
--- /dev/null
+++ b/target/classes/com/github/example/pt/entity/Permission.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/entity/PromotionPolicy.class b/target/classes/com/github/example/pt/entity/PromotionPolicy.class
new file mode 100644
index 0000000..d36ae48
--- /dev/null
+++ b/target/classes/com/github/example/pt/entity/PromotionPolicy.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/entity/SeedBox.class b/target/classes/com/github/example/pt/entity/SeedBox.class
new file mode 100644
index 0000000..2f3ffe9
--- /dev/null
+++ b/target/classes/com/github/example/pt/entity/SeedBox.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/entity/SettingEntity.class b/target/classes/com/github/example/pt/entity/SettingEntity.class
new file mode 100644
index 0000000..715ca3b
--- /dev/null
+++ b/target/classes/com/github/example/pt/entity/SettingEntity.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/entity/Tag.class b/target/classes/com/github/example/pt/entity/Tag.class
new file mode 100644
index 0000000..ffaea13
--- /dev/null
+++ b/target/classes/com/github/example/pt/entity/Tag.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/entity/Thanks.class b/target/classes/com/github/example/pt/entity/Thanks.class
new file mode 100644
index 0000000..96c6e4b
--- /dev/null
+++ b/target/classes/com/github/example/pt/entity/Thanks.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/entity/Torrent.class b/target/classes/com/github/example/pt/entity/Torrent.class
new file mode 100644
index 0000000..3941ca1
--- /dev/null
+++ b/target/classes/com/github/example/pt/entity/Torrent.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/entity/TransferHistory.class b/target/classes/com/github/example/pt/entity/TransferHistory.class
new file mode 100644
index 0000000..82ef10c
--- /dev/null
+++ b/target/classes/com/github/example/pt/entity/TransferHistory.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/entity/User.class b/target/classes/com/github/example/pt/entity/User.class
new file mode 100644
index 0000000..4a30cc3
--- /dev/null
+++ b/target/classes/com/github/example/pt/entity/User.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/entity/UserGroup.class b/target/classes/com/github/example/pt/entity/UserGroup.class
new file mode 100644
index 0000000..1b814cb
--- /dev/null
+++ b/target/classes/com/github/example/pt/entity/UserGroup.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/exception/APIErrorCode.class b/target/classes/com/github/example/pt/exception/APIErrorCode.class
new file mode 100644
index 0000000..c2286ec
--- /dev/null
+++ b/target/classes/com/github/example/pt/exception/APIErrorCode.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/exception/APIGenericException.class b/target/classes/com/github/example/pt/exception/APIGenericException.class
new file mode 100644
index 0000000..79847d4
--- /dev/null
+++ b/target/classes/com/github/example/pt/exception/APIGenericException.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/exception/AnnounceBusyException.class b/target/classes/com/github/example/pt/exception/AnnounceBusyException.class
new file mode 100644
index 0000000..350e092
--- /dev/null
+++ b/target/classes/com/github/example/pt/exception/AnnounceBusyException.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/exception/AnnounceException.class b/target/classes/com/github/example/pt/exception/AnnounceException.class
new file mode 100644
index 0000000..63d1a3a
--- /dev/null
+++ b/target/classes/com/github/example/pt/exception/AnnounceException.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/exception/BadConfigException.class b/target/classes/com/github/example/pt/exception/BadConfigException.class
new file mode 100644
index 0000000..1e525fe
--- /dev/null
+++ b/target/classes/com/github/example/pt/exception/BadConfigException.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/exception/BrowserReadableAnnounceException.class b/target/classes/com/github/example/pt/exception/BrowserReadableAnnounceException.class
new file mode 100644
index 0000000..4318034
--- /dev/null
+++ b/target/classes/com/github/example/pt/exception/BrowserReadableAnnounceException.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/exception/EmptyTorrentFileException.class b/target/classes/com/github/example/pt/exception/EmptyTorrentFileException.class
new file mode 100644
index 0000000..875d5bc
--- /dev/null
+++ b/target/classes/com/github/example/pt/exception/EmptyTorrentFileException.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/exception/FixedAnnounceException.class b/target/classes/com/github/example/pt/exception/FixedAnnounceException.class
new file mode 100644
index 0000000..1b8ab5f
--- /dev/null
+++ b/target/classes/com/github/example/pt/exception/FixedAnnounceException.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/exception/InvalidAnnounceException.class b/target/classes/com/github/example/pt/exception/InvalidAnnounceException.class
new file mode 100644
index 0000000..0d147b2
--- /dev/null
+++ b/target/classes/com/github/example/pt/exception/InvalidAnnounceException.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/exception/InvalidTorrentFileException.class b/target/classes/com/github/example/pt/exception/InvalidTorrentFileException.class
new file mode 100644
index 0000000..199c438
--- /dev/null
+++ b/target/classes/com/github/example/pt/exception/InvalidTorrentFileException.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/exception/InvalidTorrentPiecesException.class b/target/classes/com/github/example/pt/exception/InvalidTorrentPiecesException.class
new file mode 100644
index 0000000..38a0cc9
--- /dev/null
+++ b/target/classes/com/github/example/pt/exception/InvalidTorrentPiecesException.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/exception/InvalidTorrentVerifyException.class b/target/classes/com/github/example/pt/exception/InvalidTorrentVerifyException.class
new file mode 100644
index 0000000..e8f45d2
--- /dev/null
+++ b/target/classes/com/github/example/pt/exception/InvalidTorrentVerifyException.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/exception/InvalidTorrentVersionException.class b/target/classes/com/github/example/pt/exception/InvalidTorrentVersionException.class
new file mode 100644
index 0000000..2b550aa
--- /dev/null
+++ b/target/classes/com/github/example/pt/exception/InvalidTorrentVersionException.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/exception/RESTException.class b/target/classes/com/github/example/pt/exception/RESTException.class
new file mode 100644
index 0000000..4976033
--- /dev/null
+++ b/target/classes/com/github/example/pt/exception/RESTException.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/exception/RetryableAnnounceException.class b/target/classes/com/github/example/pt/exception/RetryableAnnounceException.class
new file mode 100644
index 0000000..c64c060
--- /dev/null
+++ b/target/classes/com/github/example/pt/exception/RetryableAnnounceException.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/exception/TorrentException.class b/target/classes/com/github/example/pt/exception/TorrentException.class
new file mode 100644
index 0000000..2cb3d02
--- /dev/null
+++ b/target/classes/com/github/example/pt/exception/TorrentException.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/exception/TrackerException.class b/target/classes/com/github/example/pt/exception/TrackerException.class
new file mode 100644
index 0000000..6acf75c
--- /dev/null
+++ b/target/classes/com/github/example/pt/exception/TrackerException.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/listener/SaTokenEventListener.class b/target/classes/com/github/example/pt/listener/SaTokenEventListener.class
new file mode 100644
index 0000000..2ece9af
--- /dev/null
+++ b/target/classes/com/github/example/pt/listener/SaTokenEventListener.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/objects/ResponsePojo.class b/target/classes/com/github/example/pt/objects/ResponsePojo.class
new file mode 100644
index 0000000..540cad8
--- /dev/null
+++ b/target/classes/com/github/example/pt/objects/ResponsePojo.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/ptApplication.class b/target/classes/com/github/example/pt/ptApplication.class
new file mode 100644
index 0000000..2611a87
--- /dev/null
+++ b/target/classes/com/github/example/pt/ptApplication.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/redisentity/RedisLoginAttempt.class b/target/classes/com/github/example/pt/redisentity/RedisLoginAttempt.class
new file mode 100644
index 0000000..678a3fb
--- /dev/null
+++ b/target/classes/com/github/example/pt/redisentity/RedisLoginAttempt.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/redisrepository/RedisLoginAttemptRepository.class b/target/classes/com/github/example/pt/redisrepository/RedisLoginAttemptRepository.class
new file mode 100644
index 0000000..071a726
--- /dev/null
+++ b/target/classes/com/github/example/pt/redisrepository/RedisLoginAttemptRepository.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/repository/CategoryRepository.class b/target/classes/com/github/example/pt/repository/CategoryRepository.class
new file mode 100644
index 0000000..233a156
--- /dev/null
+++ b/target/classes/com/github/example/pt/repository/CategoryRepository.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/repository/ExamPlanRepository.class b/target/classes/com/github/example/pt/repository/ExamPlanRepository.class
new file mode 100644
index 0000000..a034008
--- /dev/null
+++ b/target/classes/com/github/example/pt/repository/ExamPlanRepository.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/repository/ExamRepository.class b/target/classes/com/github/example/pt/repository/ExamRepository.class
new file mode 100644
index 0000000..978c9ff
--- /dev/null
+++ b/target/classes/com/github/example/pt/repository/ExamRepository.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/repository/LoginHistoryRepository.class b/target/classes/com/github/example/pt/repository/LoginHistoryRepository.class
new file mode 100644
index 0000000..1b28ea0
--- /dev/null
+++ b/target/classes/com/github/example/pt/repository/LoginHistoryRepository.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/repository/PeersRepository.class b/target/classes/com/github/example/pt/repository/PeersRepository.class
new file mode 100644
index 0000000..44aebde
--- /dev/null
+++ b/target/classes/com/github/example/pt/repository/PeersRepository.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/repository/PermissionRepository.class b/target/classes/com/github/example/pt/repository/PermissionRepository.class
new file mode 100644
index 0000000..ede42a3
--- /dev/null
+++ b/target/classes/com/github/example/pt/repository/PermissionRepository.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/repository/PromotionPolicyRepository.class b/target/classes/com/github/example/pt/repository/PromotionPolicyRepository.class
new file mode 100644
index 0000000..1bb58f3
--- /dev/null
+++ b/target/classes/com/github/example/pt/repository/PromotionPolicyRepository.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/repository/SettingRepository.class b/target/classes/com/github/example/pt/repository/SettingRepository.class
new file mode 100644
index 0000000..adf2e82
--- /dev/null
+++ b/target/classes/com/github/example/pt/repository/SettingRepository.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/repository/TagRepository.class b/target/classes/com/github/example/pt/repository/TagRepository.class
new file mode 100644
index 0000000..abf86cc
--- /dev/null
+++ b/target/classes/com/github/example/pt/repository/TagRepository.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/repository/ThanksRepository.class b/target/classes/com/github/example/pt/repository/ThanksRepository.class
new file mode 100644
index 0000000..7a8b45a
--- /dev/null
+++ b/target/classes/com/github/example/pt/repository/ThanksRepository.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/repository/TorrentRepository.class b/target/classes/com/github/example/pt/repository/TorrentRepository.class
new file mode 100644
index 0000000..d222a8e
--- /dev/null
+++ b/target/classes/com/github/example/pt/repository/TorrentRepository.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/repository/TransferHistoryRepository.class b/target/classes/com/github/example/pt/repository/TransferHistoryRepository.class
new file mode 100644
index 0000000..152df00
--- /dev/null
+++ b/target/classes/com/github/example/pt/repository/TransferHistoryRepository.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/repository/UserGroupRepository.class b/target/classes/com/github/example/pt/repository/UserGroupRepository.class
new file mode 100644
index 0000000..78a05c5
--- /dev/null
+++ b/target/classes/com/github/example/pt/repository/UserGroupRepository.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/repository/UserRepository.class b/target/classes/com/github/example/pt/repository/UserRepository.class
new file mode 100644
index 0000000..4d8d29b
--- /dev/null
+++ b/target/classes/com/github/example/pt/repository/UserRepository.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/AnnouncePerformanceMonitorService.class b/target/classes/com/github/example/pt/service/AnnouncePerformanceMonitorService.class
new file mode 100644
index 0000000..106c031
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/AnnouncePerformanceMonitorService.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/AnnounceService$AnnounceTask.class b/target/classes/com/github/example/pt/service/AnnounceService$AnnounceTask.class
new file mode 100644
index 0000000..207d093
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/AnnounceService$AnnounceTask.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/AnnounceService.class b/target/classes/com/github/example/pt/service/AnnounceService.class
new file mode 100644
index 0000000..3c611bc
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/AnnounceService.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/AuthenticationService.class b/target/classes/com/github/example/pt/service/AuthenticationService.class
new file mode 100644
index 0000000..deaef28
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/AuthenticationService.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/BlacklistClientService.class b/target/classes/com/github/example/pt/service/BlacklistClientService.class
new file mode 100644
index 0000000..8514c94
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/BlacklistClientService.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/CategoryService.class b/target/classes/com/github/example/pt/service/CategoryService.class
new file mode 100644
index 0000000..78f5266
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/CategoryService.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/ExamPlanService.class b/target/classes/com/github/example/pt/service/ExamPlanService.class
new file mode 100644
index 0000000..f7dc083
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/ExamPlanService.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/ExamService.class b/target/classes/com/github/example/pt/service/ExamService.class
new file mode 100644
index 0000000..beab2f6
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/ExamService.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/LoginHistoryService.class b/target/classes/com/github/example/pt/service/LoginHistoryService.class
new file mode 100644
index 0000000..d2cee3d
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/LoginHistoryService.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/PeerService.class b/target/classes/com/github/example/pt/service/PeerService.class
new file mode 100644
index 0000000..40e8fa7
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/PeerService.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/PermissionService.class b/target/classes/com/github/example/pt/service/PermissionService.class
new file mode 100644
index 0000000..a7d6823
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/PermissionService.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/PromotionService.class b/target/classes/com/github/example/pt/service/PromotionService.class
new file mode 100644
index 0000000..512f454
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/PromotionService.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/SaTokenPermImpl.class b/target/classes/com/github/example/pt/service/SaTokenPermImpl.class
new file mode 100644
index 0000000..6b3dbd3
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/SaTokenPermImpl.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/SettingService.class b/target/classes/com/github/example/pt/service/SettingService.class
new file mode 100644
index 0000000..62595e4
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/SettingService.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/TagService.class b/target/classes/com/github/example/pt/service/TagService.class
new file mode 100644
index 0000000..251ab50
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/TagService.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/ThanksService.class b/target/classes/com/github/example/pt/service/ThanksService.class
new file mode 100644
index 0000000..0562244
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/ThanksService.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/TorrentService.class b/target/classes/com/github/example/pt/service/TorrentService.class
new file mode 100644
index 0000000..2a51300
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/TorrentService.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/TransferHistoryService$PeerStatus.class b/target/classes/com/github/example/pt/service/TransferHistoryService$PeerStatus.class
new file mode 100644
index 0000000..4c1e718
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/TransferHistoryService$PeerStatus.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/TransferHistoryService.class b/target/classes/com/github/example/pt/service/TransferHistoryService.class
new file mode 100644
index 0000000..08df053
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/TransferHistoryService.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/UserGroupService.class b/target/classes/com/github/example/pt/service/UserGroupService.class
new file mode 100644
index 0000000..60d996c
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/UserGroupService.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/service/UserService.class b/target/classes/com/github/example/pt/service/UserService.class
new file mode 100644
index 0000000..a43dfaf
--- /dev/null
+++ b/target/classes/com/github/example/pt/service/UserService.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/type/AnnounceEventType.class b/target/classes/com/github/example/pt/type/AnnounceEventType.class
new file mode 100644
index 0000000..62dd62e
--- /dev/null
+++ b/target/classes/com/github/example/pt/type/AnnounceEventType.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/type/AnnounceEventTypeConverter.class b/target/classes/com/github/example/pt/type/AnnounceEventTypeConverter.class
new file mode 100644
index 0000000..76000e3
--- /dev/null
+++ b/target/classes/com/github/example/pt/type/AnnounceEventTypeConverter.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/type/GuestAccessBlocker.class b/target/classes/com/github/example/pt/type/GuestAccessBlocker.class
new file mode 100644
index 0000000..59e03a5
--- /dev/null
+++ b/target/classes/com/github/example/pt/type/GuestAccessBlocker.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/type/GuestAccessRequirement.class b/target/classes/com/github/example/pt/type/GuestAccessRequirement.class
new file mode 100644
index 0000000..1eb953c
--- /dev/null
+++ b/target/classes/com/github/example/pt/type/GuestAccessRequirement.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/type/IPFormatRequirement.class b/target/classes/com/github/example/pt/type/IPFormatRequirement.class
new file mode 100644
index 0000000..a57ca21
--- /dev/null
+++ b/target/classes/com/github/example/pt/type/IPFormatRequirement.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/type/LoginType.class b/target/classes/com/github/example/pt/type/LoginType.class
new file mode 100644
index 0000000..a0aa3b7
--- /dev/null
+++ b/target/classes/com/github/example/pt/type/LoginType.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/type/PrivacyLevel.class b/target/classes/com/github/example/pt/type/PrivacyLevel.class
new file mode 100644
index 0000000..7fb6f07
--- /dev/null
+++ b/target/classes/com/github/example/pt/type/PrivacyLevel.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/util/BencodeUtil.class b/target/classes/com/github/example/pt/util/BencodeUtil.class
new file mode 100644
index 0000000..78f9a29
--- /dev/null
+++ b/target/classes/com/github/example/pt/util/BencodeUtil.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/util/BooleanUtil.class b/target/classes/com/github/example/pt/util/BooleanUtil.class
new file mode 100644
index 0000000..a1089ae
--- /dev/null
+++ b/target/classes/com/github/example/pt/util/BooleanUtil.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/util/ByteUtil.class b/target/classes/com/github/example/pt/util/ByteUtil.class
new file mode 100644
index 0000000..928054c
--- /dev/null
+++ b/target/classes/com/github/example/pt/util/ByteUtil.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/util/ClassUtil.class b/target/classes/com/github/example/pt/util/ClassUtil.class
new file mode 100644
index 0000000..3adb993
--- /dev/null
+++ b/target/classes/com/github/example/pt/util/ClassUtil.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/util/ExecutorUtil.class b/target/classes/com/github/example/pt/util/ExecutorUtil.class
new file mode 100644
index 0000000..d71601f
--- /dev/null
+++ b/target/classes/com/github/example/pt/util/ExecutorUtil.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/util/GsonUtil.class b/target/classes/com/github/example/pt/util/GsonUtil.class
new file mode 100644
index 0000000..2be7e7f
--- /dev/null
+++ b/target/classes/com/github/example/pt/util/GsonUtil.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/util/HibernateSessionUtil.class b/target/classes/com/github/example/pt/util/HibernateSessionUtil.class
new file mode 100644
index 0000000..43d5115
--- /dev/null
+++ b/target/classes/com/github/example/pt/util/HibernateSessionUtil.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/util/IPUtil.class b/target/classes/com/github/example/pt/util/IPUtil.class
new file mode 100644
index 0000000..dab2236
--- /dev/null
+++ b/target/classes/com/github/example/pt/util/IPUtil.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/util/InfoHashUtil.class b/target/classes/com/github/example/pt/util/InfoHashUtil.class
new file mode 100644
index 0000000..b18f975
--- /dev/null
+++ b/target/classes/com/github/example/pt/util/InfoHashUtil.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/util/IpValidator.class b/target/classes/com/github/example/pt/util/IpValidator.class
new file mode 100644
index 0000000..297623d
--- /dev/null
+++ b/target/classes/com/github/example/pt/util/IpValidator.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/util/MiscUtil.class b/target/classes/com/github/example/pt/util/MiscUtil.class
new file mode 100644
index 0000000..84a4c68
--- /dev/null
+++ b/target/classes/com/github/example/pt/util/MiscUtil.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/util/PackUtil.class b/target/classes/com/github/example/pt/util/PackUtil.class
new file mode 100644
index 0000000..afc1880
--- /dev/null
+++ b/target/classes/com/github/example/pt/util/PackUtil.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/util/PasswordHash.class b/target/classes/com/github/example/pt/util/PasswordHash.class
new file mode 100644
index 0000000..4a8d67a
--- /dev/null
+++ b/target/classes/com/github/example/pt/util/PasswordHash.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/util/RandomUtil.class b/target/classes/com/github/example/pt/util/RandomUtil.class
new file mode 100644
index 0000000..969ad27
--- /dev/null
+++ b/target/classes/com/github/example/pt/util/RandomUtil.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/util/SafeUUID.class b/target/classes/com/github/example/pt/util/SafeUUID.class
new file mode 100644
index 0000000..a070c31
--- /dev/null
+++ b/target/classes/com/github/example/pt/util/SafeUUID.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/util/TorrentParser.class b/target/classes/com/github/example/pt/util/TorrentParser.class
new file mode 100644
index 0000000..4cb7ca9
--- /dev/null
+++ b/target/classes/com/github/example/pt/util/TorrentParser.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/util/URLEncodeUtil.class b/target/classes/com/github/example/pt/util/URLEncodeUtil.class
new file mode 100644
index 0000000..f258ccd
--- /dev/null
+++ b/target/classes/com/github/example/pt/util/URLEncodeUtil.class
Binary files differ
diff --git a/target/classes/com/github/example/pt/util/edazdarevic/commons/net/CIDRUtil.class b/target/classes/com/github/example/pt/util/edazdarevic/commons/net/CIDRUtil.class
new file mode 100644
index 0000000..147e25d
--- /dev/null
+++ b/target/classes/com/github/example/pt/util/edazdarevic/commons/net/CIDRUtil.class
Binary files differ
diff --git a/target/classes/ppt.sql b/target/classes/ppt.sql
new file mode 100644
index 0000000..1181c77
--- /dev/null
+++ b/target/classes/ppt.sql
@@ -0,0 +1,729 @@
+/*
+ Navicat Premium Dump SQL
+
+ Source Server : first
+ Source Server Type : MySQL
+ Source Server Version : 80036 (8.0.36)
+ Source Host : localhost:3306
+ Source Schema : ppt
+
+ Target Server Type : MySQL
+ Target Server Version : 80036 (8.0.36)
+ File Encoding : 65001
+
+ Date: 04/06/2025 14:33:40
+*/
+
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ----------------------------
+-- Table structure for categories
+-- ----------------------------
+DROP TABLE IF EXISTS `categories`;
+CREATE TABLE `categories` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_categories_slug`(`slug` ASC) USING BTREE,
+ UNIQUE INDEX `UKoul14ho7bctbefv8jywp5v3i2`(`slug` ASC) USING BTREE,
+ INDEX `idx_categories_slug`(`slug` ASC) USING BTREE,
+ INDEX `IDXoul14ho7bctbefv8jywp5v3i2`(`slug` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of categories
+-- ----------------------------
+INSERT INTO `categories` VALUES (1, 'os', '操作系统', 'os-icon.png');
+
+-- ----------------------------
+-- Table structure for exam_plans
+-- ----------------------------
+DROP TABLE IF EXISTS `exam_plans`;
+CREATE TABLE `exam_plans` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `display_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `downloaded` bigint NOT NULL,
+ `duration` bigint NOT NULL,
+ `karma` double NOT NULL,
+ `seeding_time` bigint NOT NULL,
+ `seeds` bigint NOT NULL,
+ `share_ratio` double NOT NULL,
+ `slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `uploaded` bigint NOT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `UK74jaepieaj2umswpqn16yy3x5`(`slug` ASC) USING BTREE,
+ INDEX `IDX74jaepieaj2umswpqn16yy3x5`(`slug` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of exam_plans
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for exams
+-- ----------------------------
+DROP TABLE IF EXISTS `exams`;
+CREATE TABLE `exams` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `end_at` datetime(6) NOT NULL,
+ `exam_plan_id` bigint NULL DEFAULT NULL,
+ `user_id` bigint NULL DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `UK2wii0igd3vdfecy00op0un4qt`(`user_id` ASC) USING BTREE,
+ INDEX `IDX2wii0igd3vdfecy00op0un4qt`(`user_id` ASC) USING BTREE,
+ INDEX `FKl0cips1gany3cuppyfs7ntpai`(`exam_plan_id` ASC) USING BTREE,
+ CONSTRAINT `FKi63cpl1xkgy32iq68ru4ypjn4` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
+ CONSTRAINT `FKl0cips1gany3cuppyfs7ntpai` FOREIGN KEY (`exam_plan_id`) REFERENCES `exam_plans` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of exams
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for login_history
+-- ----------------------------
+DROP TABLE IF EXISTS `login_history`;
+CREATE TABLE `login_history` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `user_id` bigint NOT NULL,
+ `time` timestamp NOT NULL,
+ `type` enum('ACCOUNT','PASSKEY','PERSONAL_ACCESSTOKEN','PROGRAM_INTERNAL','TWO_STEP_VERIFICATION') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `ip_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `user_agent` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ INDEX `idx_login_time`(`time` ASC) USING BTREE,
+ INDEX `fk_loginhistory_user`(`user_id` ASC) USING BTREE,
+ INDEX `IDX3lft44makrxommxm63k7xj77d`(`time` ASC) USING BTREE,
+ CONSTRAINT `fk_loginhistory_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 33 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of login_history
+-- ----------------------------
+INSERT INTO `login_history` VALUES (1, 1, '2025-06-03 12:25:33', 'ACCOUNT', '0:0:0:0:0:0:0:1', 'PostmanRuntime/7.43.4', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (2, 1, '2025-06-03 12:51:42', 'ACCOUNT', '0:0:0:0:0:0:0:1', 'PostmanRuntime/7.43.4', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (3, 1, '2025-06-03 12:52:55', 'ACCOUNT', '0:0:0:0:0:0:0:1', 'PostmanRuntime/7.43.4', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (4, 1, '2025-06-03 14:20:55', 'ACCOUNT', '0:0:0:0:0:0:0:1', 'PostmanRuntime/7.43.4', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (5, 1, '2025-06-03 14:31:13', 'ACCOUNT', '0:0:0:0:0:0:0:1', 'PostmanRuntime/7.43.4', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (6, 1, '2025-06-04 03:40:10', 'ACCOUNT', '0:0:0:0:0:0:0:1', 'PostmanRuntime/7.44.0', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (7, 1, '2025-06-04 03:43:09', 'PASSKEY', '192.168.10.10', 'qBittorrent/5.1.0', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (8, 1, '2025-06-04 03:44:27', 'PASSKEY', '192.168.10.10', 'qBittorrent/5.1.0', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (9, 1, '2025-06-04 03:51:35', 'PASSKEY', '192.168.10.10', 'qBittorrent/5.1.0', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (10, 1, '2025-06-04 03:52:22', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (11, 1, '2025-06-04 03:52:53', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (12, 1, '2025-06-04 03:53:12', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (13, 1, '2025-06-04 03:54:46', 'PASSKEY', '192.168.10.10', 'qBittorrent/5.1.0', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (14, 1, '2025-06-04 03:58:14', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (15, 1, '2025-06-04 04:22:30', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (16, 1, '2025-06-04 04:43:29', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (17, 1, '2025-06-04 04:54:09', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (18, 1, '2025-06-04 04:59:13', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (19, 1, '2025-06-04 05:17:07', 'PASSKEY', '192.168.10.10', 'qBittorrent/5.1.0', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (20, 1, '2025-06-04 05:18:28', 'PASSKEY', '192.168.10.10', 'qBittorrent/5.1.0', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (21, 1, '2025-06-04 05:21:25', 'PASSKEY', '192.168.10.10', 'qBittorrent/5.1.0', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (22, 1, '2025-06-04 05:21:58', 'PASSKEY', '192.168.10.10', 'qBittorrent/5.1.0', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (23, 1, '2025-06-04 05:22:24', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (24, 1, '2025-06-04 05:22:30', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (25, 1, '2025-06-04 05:27:44', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (26, 1, '2025-06-04 05:27:50', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (27, 1, '2025-06-04 05:31:45', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (28, 1, '2025-06-04 05:33:00', 'PASSKEY', '192.168.10.10', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (29, 1, '2025-06-04 06:11:31', 'PASSKEY', '192.168.254.1', 'qBittorrent/5.1.0', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (30, 1, '2025-06-04 06:12:13', 'PASSKEY', '192.168.254.1', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (31, 1, '2025-06-04 06:12:27', 'PASSKEY', '192.168.254.1', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+INSERT INTO `login_history` VALUES (32, 1, '2025-06-04 06:13:21', 'PASSKEY', '192.168.254.1', 'Transmission/4.0.6', 'Unknown - GeoIP not initialized');
+
+-- ----------------------------
+-- Table structure for peers
+-- ----------------------------
+DROP TABLE IF EXISTS `peers`;
+CREATE TABLE `peers` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `port` int NOT NULL,
+ `info_hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `peer_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `user_agent` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `uploaded` bigint NOT NULL,
+ `downloaded` bigint NOT NULL,
+ `to_go` bigint NOT NULL,
+ `seeder` tinyint(1) NOT NULL,
+ `partial_seeder` tinyint(1) NOT NULL,
+ `passkey` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `update_at` timestamp NOT NULL,
+ `seeding_time` bigint NOT NULL,
+ `upload_speed` bigint NOT NULL,
+ `download_speed` bigint NOT NULL,
+ `user_id` bigint NULL DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_peers_ip_port_infohash`(`ip` ASC, `port` ASC, `info_hash` ASC) USING BTREE,
+ UNIQUE INDEX `UKoa8l3xqdvxr898mosks3hq3cb`(`ip` ASC, `port` ASC, `info_hash` ASC) USING BTREE,
+ INDEX `idx_peers_update_at`(`update_at` ASC) USING BTREE,
+ INDEX `fk_peers_user`(`user_id` ASC) USING BTREE,
+ INDEX `IDXmmvk33liy7j5u9e4qhxw2d7h5`(`update_at` ASC) USING BTREE,
+ CONSTRAINT `fk_peers_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+
+-- ----------------------------
+-- Table structure for permissions
+-- ----------------------------
+DROP TABLE IF EXISTS `permissions`;
+CREATE TABLE `permissions` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `def` tinyint(1) NOT NULL,
+ `user_group_id` bigint NULL DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_permissions_slug`(`slug` ASC) USING BTREE,
+ UNIQUE INDEX `UKdv6mwikptsu70hcrjq07sqsfy`(`slug` ASC) USING BTREE,
+ INDEX `fk_permissions_user_group`(`user_group_id` ASC) USING BTREE,
+ CONSTRAINT `fk_permissions_user_group` FOREIGN KEY (`user_group_id`) REFERENCES `user_groups` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of permissions
+-- ----------------------------
+INSERT INTO `permissions` VALUES (1, 'torrent:upload', 1, 1);
+INSERT INTO `permissions` VALUES (2, 'torrent:download', 1, 1);
+INSERT INTO `permissions` VALUES (3, 'torrent:view', 1, 1);
+INSERT INTO `permissions` VALUES (4, 'torrent:search', 1, 1);
+INSERT INTO `permissions` VALUES (5, 'comment:create', 1, 1);
+INSERT INTO `permissions` VALUES (6, 'user:manage', 1, 1);
+INSERT INTO `permissions` VALUES (7, 'torrent:approve', 1, 1);
+INSERT INTO `permissions` VALUES (8, 'torrent:thanks', 1, 1);
+INSERT INTO `permissions` VALUES (9, 'promotion:list', 1, 1);
+
+-- ----------------------------
+-- Table structure for promotion_policies
+-- ----------------------------
+DROP TABLE IF EXISTS `promotion_policies`;
+CREATE TABLE `promotion_policies` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `display_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `upload_ratio` double NULL DEFAULT NULL,
+ `download_ratio` double NULL DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_promotion_policies_slug`(`slug` ASC) USING BTREE,
+ UNIQUE INDEX `UKcjqpe1g15outfc0u6ajvpwxoe`(`slug` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of promotion_policies
+-- ----------------------------
+INSERT INTO `promotion_policies` VALUES (1, 'default', '默认策略', 1, 1);
+INSERT INTO `promotion_policies` VALUES (2, 'vip', '活跃用户策略', 1, 1.5);
+
+-- ----------------------------
+-- Table structure for qrtz_blob_triggers
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_blob_triggers`;
+CREATE TABLE `qrtz_blob_triggers` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `BLOB_DATA` blob NULL,
+ PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE,
+ INDEX `SCHED_NAME`(`SCHED_NAME` ASC, `TRIGGER_NAME` ASC, `TRIGGER_GROUP` ASC) USING BTREE,
+ CONSTRAINT `qrtz_blob_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_blob_triggers
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for qrtz_calendars
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_calendars`;
+CREATE TABLE `qrtz_calendars` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `CALENDAR_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `CALENDAR` blob NOT NULL,
+ PRIMARY KEY (`SCHED_NAME`, `CALENDAR_NAME`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_calendars
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for qrtz_cron_triggers
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_cron_triggers`;
+CREATE TABLE `qrtz_cron_triggers` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `CRON_EXPRESSION` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TIME_ZONE_ID` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE,
+ CONSTRAINT `qrtz_cron_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_cron_triggers
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for qrtz_fired_triggers
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_fired_triggers`;
+CREATE TABLE `qrtz_fired_triggers` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `ENTRY_ID` varchar(95) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `INSTANCE_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `FIRED_TIME` bigint NOT NULL,
+ `SCHED_TIME` bigint NOT NULL,
+ `PRIORITY` int NOT NULL,
+ `STATE` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `JOB_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `JOB_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `IS_NONCONCURRENT` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `REQUESTS_RECOVERY` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ PRIMARY KEY (`SCHED_NAME`, `ENTRY_ID`) USING BTREE,
+ INDEX `IDX_QRTZ_FT_TRIG_INST_NAME`(`SCHED_NAME` ASC, `INSTANCE_NAME` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_FT_INST_JOB_REQ_RCVRY`(`SCHED_NAME` ASC, `INSTANCE_NAME` ASC, `REQUESTS_RECOVERY` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_FT_J_G`(`SCHED_NAME` ASC, `JOB_NAME` ASC, `JOB_GROUP` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_FT_JG`(`SCHED_NAME` ASC, `JOB_GROUP` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_FT_T_G`(`SCHED_NAME` ASC, `TRIGGER_NAME` ASC, `TRIGGER_GROUP` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_FT_TG`(`SCHED_NAME` ASC, `TRIGGER_GROUP` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_fired_triggers
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for qrtz_job_details
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_job_details`;
+CREATE TABLE `qrtz_job_details` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `JOB_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `JOB_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `DESCRIPTION` varchar(250) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `JOB_CLASS_NAME` varchar(250) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `IS_DURABLE` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `IS_NONCONCURRENT` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `IS_UPDATE_DATA` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `REQUESTS_RECOVERY` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `JOB_DATA` blob NULL,
+ PRIMARY KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) USING BTREE,
+ INDEX `IDX_QRTZ_J_REQ_RECOVERY`(`SCHED_NAME` ASC, `REQUESTS_RECOVERY` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_J_GRP`(`SCHED_NAME` ASC, `JOB_GROUP` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_job_details
+-- ----------------------------
+INSERT INTO `qrtz_job_details` VALUES ('sapling_scheduler', 'peers_cleanup', 'DEFAULT', 'Peers Cleanup', 'com.github.example.pt.crontask.PeersCleanup', '1', '0', '0', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787000737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F40000000000010770800000010000000007800);
+
+-- ----------------------------
+-- Table structure for qrtz_locks
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_locks`;
+CREATE TABLE `qrtz_locks` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `LOCK_NAME` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ PRIMARY KEY (`SCHED_NAME`, `LOCK_NAME`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_locks
+-- ----------------------------
+INSERT INTO `qrtz_locks` VALUES ('sapling_scheduler', 'STATE_ACCESS');
+INSERT INTO `qrtz_locks` VALUES ('sapling_scheduler', 'TRIGGER_ACCESS');
+
+-- ----------------------------
+-- Table structure for qrtz_paused_trigger_grps
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_paused_trigger_grps`;
+CREATE TABLE `qrtz_paused_trigger_grps` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ PRIMARY KEY (`SCHED_NAME`, `TRIGGER_GROUP`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_paused_trigger_grps
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for qrtz_scheduler_state
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_scheduler_state`;
+CREATE TABLE `qrtz_scheduler_state` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `INSTANCE_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `LAST_CHECKIN_TIME` bigint NOT NULL,
+ `CHECKIN_INTERVAL` bigint NOT NULL,
+ PRIMARY KEY (`SCHED_NAME`, `INSTANCE_NAME`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_scheduler_state
+-- ----------------------------
+INSERT INTO `qrtz_scheduler_state` VALUES ('sapling_scheduler', 'LAPTOP-V24K5A551749018489588', 1749018510182, 10000);
+
+-- ----------------------------
+-- Table structure for qrtz_simple_triggers
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_simple_triggers`;
+CREATE TABLE `qrtz_simple_triggers` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `REPEAT_COUNT` bigint NOT NULL,
+ `REPEAT_INTERVAL` bigint NOT NULL,
+ `TIMES_TRIGGERED` bigint NOT NULL,
+ PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE,
+ CONSTRAINT `qrtz_simple_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_simple_triggers
+-- ----------------------------
+INSERT INTO `qrtz_simple_triggers` VALUES ('sapling_scheduler', '6da64b5bd2ee-37ffa82a-a598-485c-9718-40ee90c26bc4', 'DEFAULT', -1, 1800000, 1);
+
+-- ----------------------------
+-- Table structure for qrtz_simprop_triggers
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_simprop_triggers`;
+CREATE TABLE `qrtz_simprop_triggers` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `STR_PROP_1` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `STR_PROP_2` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `STR_PROP_3` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `INT_PROP_1` int NULL DEFAULT NULL,
+ `INT_PROP_2` int NULL DEFAULT NULL,
+ `LONG_PROP_1` bigint NULL DEFAULT NULL,
+ `LONG_PROP_2` bigint NULL DEFAULT NULL,
+ `DEC_PROP_1` decimal(13, 4) NULL DEFAULT NULL,
+ `DEC_PROP_2` decimal(13, 4) NULL DEFAULT NULL,
+ `BOOL_PROP_1` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `BOOL_PROP_2` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE,
+ CONSTRAINT `qrtz_simprop_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_simprop_triggers
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for qrtz_triggers
+-- ----------------------------
+DROP TABLE IF EXISTS `qrtz_triggers`;
+CREATE TABLE `qrtz_triggers` (
+ `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `JOB_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `JOB_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `DESCRIPTION` varchar(250) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `NEXT_FIRE_TIME` bigint NULL DEFAULT NULL,
+ `PREV_FIRE_TIME` bigint NULL DEFAULT NULL,
+ `PRIORITY` int NULL DEFAULT NULL,
+ `TRIGGER_STATE` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `TRIGGER_TYPE` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `START_TIME` bigint NOT NULL,
+ `END_TIME` bigint NULL DEFAULT NULL,
+ `CALENDAR_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
+ `MISFIRE_INSTR` smallint NULL DEFAULT NULL,
+ `JOB_DATA` blob NULL,
+ PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE,
+ INDEX `IDX_QRTZ_T_J`(`SCHED_NAME` ASC, `JOB_NAME` ASC, `JOB_GROUP` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_JG`(`SCHED_NAME` ASC, `JOB_GROUP` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_C`(`SCHED_NAME` ASC, `CALENDAR_NAME` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_G`(`SCHED_NAME` ASC, `TRIGGER_GROUP` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_STATE`(`SCHED_NAME` ASC, `TRIGGER_STATE` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_N_STATE`(`SCHED_NAME` ASC, `TRIGGER_NAME` ASC, `TRIGGER_GROUP` ASC, `TRIGGER_STATE` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_N_G_STATE`(`SCHED_NAME` ASC, `TRIGGER_GROUP` ASC, `TRIGGER_STATE` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_NEXT_FIRE_TIME`(`SCHED_NAME` ASC, `NEXT_FIRE_TIME` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_NFT_ST`(`SCHED_NAME` ASC, `TRIGGER_STATE` ASC, `NEXT_FIRE_TIME` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_NFT_MISFIRE`(`SCHED_NAME` ASC, `MISFIRE_INSTR` ASC, `NEXT_FIRE_TIME` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_NFT_ST_MISFIRE`(`SCHED_NAME` ASC, `MISFIRE_INSTR` ASC, `NEXT_FIRE_TIME` ASC, `TRIGGER_STATE` ASC) USING BTREE,
+ INDEX `IDX_QRTZ_T_NFT_ST_MISFIRE_GRP`(`SCHED_NAME` ASC, `MISFIRE_INSTR` ASC, `NEXT_FIRE_TIME` ASC, `TRIGGER_GROUP` ASC, `TRIGGER_STATE` ASC) USING BTREE,
+ CONSTRAINT `qrtz_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) REFERENCES `qrtz_job_details` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of qrtz_triggers
+-- ----------------------------
+INSERT INTO `qrtz_triggers` VALUES ('sapling_scheduler', '6da64b5bd2ee-37ffa82a-a598-485c-9718-40ee90c26bc4', 'DEFAULT', 'peers_cleanup', 'DEFAULT', NULL, 1749020288505, 1749018488505, 5, 'WAITING', 'SIMPLE', 1749018488505, 0, NULL, 0, '');
+
+-- ----------------------------
+-- Table structure for seedbox
+-- ----------------------------
+DROP TABLE IF EXISTS `seedbox`;
+CREATE TABLE `seedbox` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `download_multiplier_id` bigint NULL DEFAULT NULL,
+ `upload_multiplier_id` bigint NULL DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `UKecbehjtg3bfr4rmyixt1gcvfq`(`address` ASC) USING BTREE,
+ INDEX `FKq63tqhykgl96l4rkwy9widy5b`(`download_multiplier_id` ASC) USING BTREE,
+ INDEX `FK9xl51na3dn8k7ou8cyv7s1wrf`(`upload_multiplier_id` ASC) USING BTREE,
+ CONSTRAINT `FK9xl51na3dn8k7ou8cyv7s1wrf` FOREIGN KEY (`upload_multiplier_id`) REFERENCES `promotion_policies` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
+ CONSTRAINT `FKq63tqhykgl96l4rkwy9widy5b` FOREIGN KEY (`download_multiplier_id`) REFERENCES `promotion_policies` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of seedbox
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for settings
+-- ----------------------------
+DROP TABLE IF EXISTS `settings`;
+CREATE TABLE `settings` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `setting_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `setting_value` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_settings_setting_key`(`setting_key` ASC) USING BTREE,
+ UNIQUE INDEX `UKswd05dvj4ukvw5q135bpbbfae`(`setting_key` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Table structure for tags
+-- ----------------------------
+DROP TABLE IF EXISTS `tags`;
+CREATE TABLE `tags` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_tags_name`(`name` ASC) USING BTREE,
+ UNIQUE INDEX `UKt48xdq560gs3gap9g7jg36kgc`(`name` ASC) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of tags
+-- ----------------------------
+INSERT INTO `tags` VALUES (9, '[\"linux\"]');
+INSERT INTO `tags` VALUES (3, 'debian');
+INSERT INTO `tags` VALUES (1, 'linux');
+INSERT INTO `tags` VALUES (5, 'macos');
+INSERT INTO `tags` VALUES (2, 'ubuntu');
+INSERT INTO `tags` VALUES (4, 'windows');
+
+-- ----------------------------
+-- Table structure for thanks
+-- ----------------------------
+DROP TABLE IF EXISTS `thanks`;
+CREATE TABLE `thanks` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `torrent_id` bigint NULL DEFAULT NULL,
+ `user_id` bigint NULL DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `UKrt9lg0h53brgpf9iat5hcmf6g`(`user_id` ASC, `torrent_id` ASC) USING BTREE,
+ INDEX `FKp3kgh25tko48vq7x6u2w3dvpp`(`torrent_id` ASC) USING BTREE,
+ CONSTRAINT `FK2t90h21s5hyx6hynewsdlk46j` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
+ CONSTRAINT `FKp3kgh25tko48vq7x6u2w3dvpp` FOREIGN KEY (`torrent_id`) REFERENCES `torrents` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of thanks
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for torrents
+-- ----------------------------
+DROP TABLE IF EXISTS `torrents`;
+CREATE TABLE `torrents` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `info_hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `user_id` bigint NULL DEFAULT NULL,
+ `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `sub_title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `size` bigint NOT NULL,
+ `created_at` timestamp NOT NULL,
+ `updated_at` timestamp NOT NULL,
+ `under_review` tinyint(1) NOT NULL,
+ `anonymous` tinyint(1) NOT NULL,
+ `category_id` bigint NULL DEFAULT NULL,
+ `promotion_policy_id` bigint NULL DEFAULT NULL,
+ `description` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_torrents_info_hash`(`info_hash` ASC) USING BTREE,
+ UNIQUE INDEX `UKhag2ej1vo8snvirb1lv4b8r4x`(`info_hash` ASC) USING BTREE,
+ INDEX `idx_torrents_title`(`title` ASC) USING BTREE,
+ INDEX `idx_torrents_sub_title`(`sub_title` ASC) USING BTREE,
+ INDEX `idx_torrents_promotion_policy_id`(`promotion_policy_id` ASC) USING BTREE,
+ INDEX `fk_torrents_user`(`user_id` ASC) USING BTREE,
+ INDEX `fk_torrents_category`(`category_id` ASC) USING BTREE,
+ INDEX `IDXdplkaapqslelnscuunfpm9eb6`(`title` ASC) USING BTREE,
+ INDEX `IDX6r5kh6i4awpdlytjmm06pk22k`(`sub_title` ASC) USING BTREE,
+ INDEX `IDXd2j3h8td7682cctkv5o77b33y`(`promotion_policy_id` ASC) USING BTREE,
+ CONSTRAINT `fk_torrents_category` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT,
+ CONSTRAINT `fk_torrents_promotion_policy` FOREIGN KEY (`promotion_policy_id`) REFERENCES `promotion_policies` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT,
+ CONSTRAINT `fk_torrents_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of torrents
+-- ----------------------------
+INSERT INTO `torrents` VALUES (7, '499e9c69e90b976c5c84542cf9b88fad1e12ef1c', 1, 'example torrent title', 'subtitle here', 66194, '2025-06-04 03:41:27', '2025-06-04 03:41:27', 0, 0, 1, 1, '演示种子');
+INSERT INTO `torrents` VALUES (8, 'f45775564c88b5a2782a301d162e1d4811b0b6d5', 1, 'example torrent title', 'subtitle here', 231105, '2025-06-04 05:20:37', '2025-06-04 05:20:37', 0, 0, 1, 1, '演示种子');
+INSERT INTO `torrents` VALUES (9, '8e10c9daa1b5fb18dc5b0e73988808ba958391fa', 1, 'example torrent title', 'subtitle here', 426155, '2025-06-04 06:03:49', '2025-06-04 06:03:49', 0, 0, 1, 1, '演示种子');
+INSERT INTO `torrents` VALUES (10, '1e3275f54ec074af2d99828311d4a8488eb7a487', 1, 'example torrent title', 'subtitle here', 13861694, '2025-06-04 06:10:23', '2025-06-04 06:10:23', 0, 0, 1, 1, '演示种子');
+
+-- ----------------------------
+-- Table structure for torrents_tag
+-- ----------------------------
+DROP TABLE IF EXISTS `torrents_tag`;
+CREATE TABLE `torrents_tag` (
+ `torrent_id` bigint NOT NULL,
+ `tag_id` bigint NOT NULL,
+ PRIMARY KEY (`torrent_id`, `tag_id`) USING BTREE,
+ INDEX `fk_torrents_tag_tag`(`tag_id` ASC) USING BTREE,
+ CONSTRAINT `fk_torrents_tag_tag` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
+ CONSTRAINT `fk_torrents_tag_torrent` FOREIGN KEY (`torrent_id`) REFERENCES `torrents` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of torrents_tag
+-- ----------------------------
+INSERT INTO `torrents_tag` VALUES (7, 9);
+INSERT INTO `torrents_tag` VALUES (8, 9);
+INSERT INTO `torrents_tag` VALUES (9, 9);
+INSERT INTO `torrents_tag` VALUES (10, 9);
+
+-- ----------------------------
+-- Table structure for transfer_history
+-- ----------------------------
+DROP TABLE IF EXISTS `transfer_history`;
+CREATE TABLE `transfer_history` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `user_id` bigint NOT NULL,
+ `torrent_id` bigint NOT NULL,
+ `to_go` bigint NOT NULL,
+ `started_at` timestamp NOT NULL,
+ `updated_at` timestamp NOT NULL,
+ `uploaded` bigint NOT NULL,
+ `downloaded` bigint NOT NULL,
+ `actual_uploaded` bigint NOT NULL,
+ `actual_downloaded` bigint NOT NULL,
+ `upload_speed` bigint NOT NULL,
+ `download_speed` bigint NOT NULL,
+ `last_event` enum('started','completed','stopped','paused','unknown') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `have_complete_history` tinyint(1) NOT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_user_torrent`(`user_id` ASC, `torrent_id` ASC) USING BTREE,
+ UNIQUE INDEX `UKrm5p4xv3rb2vm6psql5je94jh`(`user_id` ASC, `torrent_id` ASC) USING BTREE,
+ INDEX `fk_transfer_torrent`(`torrent_id` ASC) USING BTREE,
+ CONSTRAINT `fk_transfer_torrent` FOREIGN KEY (`torrent_id`) REFERENCES `torrents` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
+ CONSTRAINT `fk_transfer_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of transfer_history
+-- ----------------------------
+INSERT INTO `transfer_history` VALUES (1, 1, 7, 66194, '2025-06-04 04:43:30', '2025-06-04 05:21:25', 0, 0, 0, 0, 0, 0, 'completed', 1);
+INSERT INTO `transfer_history` VALUES (2, 1, 10, 0, '2025-06-04 06:11:31', '2025-06-04 06:13:21', 27723388, 0, 0, 27723388, 0, 77874, 'started', 1);
+
+-- ----------------------------
+-- Table structure for user_groups
+-- ----------------------------
+DROP TABLE IF EXISTS `user_groups`;
+CREATE TABLE `user_groups` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `display_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `promotion_policy_id` bigint NULL DEFAULT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_user_groups_slug`(`slug` ASC) USING BTREE,
+ UNIQUE INDEX `UKkkje1jbmrkam1k7jbd39homw0`(`slug` ASC) USING BTREE,
+ INDEX `fk_user_groups_promotion_policy`(`promotion_policy_id` ASC) USING BTREE,
+ CONSTRAINT `fk_user_groups_promotion_policy` FOREIGN KEY (`promotion_policy_id`) REFERENCES `promotion_policies` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of user_groups
+-- ----------------------------
+INSERT INTO `user_groups` VALUES (1, 'default', '默认用户组', 1);
+INSERT INTO `user_groups` VALUES (2, 'vip', '活跃用户组', 2);
+
+-- ----------------------------
+-- Table structure for user_groups_permission_entities
+-- ----------------------------
+DROP TABLE IF EXISTS `user_groups_permission_entities`;
+CREATE TABLE `user_groups_permission_entities` (
+ `user_group_id` bigint NOT NULL,
+ `permission_entities_id` bigint NOT NULL,
+ PRIMARY KEY (`user_group_id`, `permission_entities_id`) USING BTREE,
+ UNIQUE INDEX `UK_95j9uaq96bvd9ykn0sngywks6`(`permission_entities_id` ASC) USING BTREE,
+ CONSTRAINT `fk_ugpe_permission` FOREIGN KEY (`permission_entities_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
+ CONSTRAINT `fk_ugpe_user_group` FOREIGN KEY (`user_group_id`) REFERENCES `user_groups` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of user_groups_permission_entities
+-- ----------------------------
+INSERT INTO `user_groups_permission_entities` VALUES (1, 1);
+INSERT INTO `user_groups_permission_entities` VALUES (1, 2);
+INSERT INTO `user_groups_permission_entities` VALUES (1, 3);
+INSERT INTO `user_groups_permission_entities` VALUES (1, 4);
+INSERT INTO `user_groups_permission_entities` VALUES (1, 5);
+INSERT INTO `user_groups_permission_entities` VALUES (1, 6);
+INSERT INTO `user_groups_permission_entities` VALUES (1, 7);
+INSERT INTO `user_groups_permission_entities` VALUES (1, 8);
+INSERT INTO `user_groups_permission_entities` VALUES (1, 9);
+
+-- ----------------------------
+-- Table structure for users
+-- ----------------------------
+DROP TABLE IF EXISTS `users`;
+CREATE TABLE `users` (
+ `id` bigint NOT NULL AUTO_INCREMENT,
+ `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `group_id` bigint NULL DEFAULT NULL,
+ `passkey` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `create_at` timestamp NOT NULL,
+ `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `custom_title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `signature` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `language` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `download_bandwidth` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `upload_bandwidth` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `downloaded` bigint NOT NULL,
+ `uploaded` bigint NOT NULL,
+ `real_downloaded` bigint NOT NULL,
+ `real_uploaded` bigint NOT NULL,
+ `isp` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `karma` decimal(19, 2) NOT NULL,
+ `invite_slot` int NOT NULL,
+ `seeding_time` bigint NOT NULL,
+ `personal_access_token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ `privacy_level` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_users_username`(`username` ASC) USING BTREE,
+ UNIQUE INDEX `uk_users_email`(`email` ASC) USING BTREE,
+ UNIQUE INDEX `uk_users_passkey`(`passkey` ASC) USING BTREE,
+ UNIQUE INDEX `UKr43af9ap4edm43mmtq01oddj6`(`username` ASC) USING BTREE,
+ UNIQUE INDEX `UK6dotkott2kjsp8vw4d0m25fb7`(`email` ASC) USING BTREE,
+ UNIQUE INDEX `UK402sx6cqgk66uwt7eyf54tmij`(`passkey` ASC) USING BTREE,
+ INDEX `fk_users_group`(`group_id` ASC) USING BTREE,
+ CONSTRAINT `fk_users_group` FOREIGN KEY (`group_id`) REFERENCES `user_groups` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
+) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of users
+-- ----------------------------
+INSERT INTO `users` VALUES (1, 'testuser@example.com', '$2a$06$JqvHpC94mnN23uwmZX0XHeuyFj3M22Ikw8VBZRopfrlWbPfA4KREu', 'testuser', 1, '47050e8f-f3fd-4a9c-a828-0e0de456691f', '2025-06-03 11:34:08', 'https://www.baidu.com/facivon.ico', '测试用户', '这个用户很懒,还没有个性签名', 'zh-CN', '100mbps', '100mbps', 0, 27723388, 27723388, 0, '未知', 0.00, 0, 30790068, '948c9146-ff5f-4ad6-8ba5-d32e1209cafd', '0');
+
+SET FOREIGN_KEY_CHECKS = 1;
diff --git a/target/classes/redisson.yaml b/target/classes/redisson.yaml
new file mode 100644
index 0000000..6d8f6d7
--- /dev/null
+++ b/target/classes/redisson.yaml
@@ -0,0 +1,2 @@
+singleServerConfig:
+ address: "redis://127.0.0.1:6379"
diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
new file mode 100644
index 0000000..fe42053
--- /dev/null
+++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
@@ -0,0 +1,138 @@
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\util\SafeUUID.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\dto\response\CategoryResponseDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\entity\SeedBox.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\entity\Exam.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\repository\PeersRepository.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\util\TorrentParser.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\exception\APIGenericException.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\service\TagService.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\util\IpValidator.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\repository\TorrentRepository.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\autoconfig\SafeHTMLConfig.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\auth\AuthController.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\type\GuestAccessBlocker.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\util\edazdarevic\commons\net\CIDRUtil.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\dto\response\UserBasicResponseDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\torrent\dto\response\TorrentUploadSuccessResponseDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\type\GuestAccessRequirement.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\util\BooleanUtil.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\service\AuthenticationService.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\util\BencodeUtil.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\entity\UserGroup.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\repository\ExamPlanRepository.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\dto\response\ScrapeContainerDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\dto\response\PeerInfoResponseDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\entity\ExamPlan.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\repository\PromotionPolicyRepository.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\service\TorrentService.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\dto\response\UserSessionResponseDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\entity\TransferHistory.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\config\SiteBasicConfig.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\objects\ResponsePojo.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\util\HibernateSessionUtil.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\repository\PermissionRepository.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\autoconfig\WorkDirectoryConfig.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\repository\ThanksRepository.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\repository\TransferHistoryRepository.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\util\PasswordHash.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\entity\User.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\exception\InvalidTorrentFileException.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\util\ByteUtil.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\entity\Permission.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\repository\UserRepository.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\dto\response\PromotionResponseDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\repository\ExamRepository.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\announce\AnnounceController.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\util\PackUtil.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\dto\response\TransferHistoryDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\autoconfig\JetcacheConfig.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\util\RandomUtil.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\config\SecurityConfig.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\advice\GlobalControllerAdvice.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\exception\RetryableAnnounceException.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\service\CategoryService.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\category\CategoryController.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\promotion\PromotionController.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\torrent\TorrentController.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\dto\response\UserTinyResponseDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\exception\EmptyTorrentFileException.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\repository\SettingRepository.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\config\ApiPrinter.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\dto\response\TorrentInfoResponseDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\torrent\dto\response\TorrentSearchResultResponseDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\autoconfig\RedisConfig.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\listener\SaTokenEventListener.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\service\ExamService.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\entity\Tag.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\service\BlacklistClientService.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\exception\InvalidTorrentVerifyException.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\repository\UserGroupRepository.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\util\GsonUtil.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\exception\TorrentException.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\service\ExamPlanService.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\config\TrackerConfig.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\exception\BadConfigException.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\service\PermissionService.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\service\LoginHistoryService.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\service\SettingService.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\entity\LoginHistory.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\exception\InvalidAnnounceException.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\auth\dto\request\RegisterRequestDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\torrent\dto\request\TorrentScrapeRequestDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\service\UserGroupService.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\DebugController.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\util\InfoHashUtil.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\config\MailConfig.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\torrent\dto\response\TorrentScrapeResponseDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\service\PeerService.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\exception\InvalidTorrentVersionException.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\type\IPFormatRequirement.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\dto\response\TorrentBasicResponseDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\entity\Torrent.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\util\ExecutorUtil.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\torrent\form\TorrentUploadForm.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\autoconfig\SaTokenConfig.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\torrent\dto\request\SearchTorrentRequestDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\entity\Category.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\repository\TagRepository.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\torrent\dto\request\ThanksResponseDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\redisentity\RedisLoginAttempt.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\service\TransferHistoryService.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\service\PromotionService.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\entity\Peer.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\entity\Thanks.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\entity\SettingEntity.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\util\URLEncodeUtil.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\autoconfig\JacksonConfig.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\autoconfig\QuartzConfig.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\type\PrivacyLevel.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\service\AnnouncePerformanceMonitorService.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\service\SaTokenPermImpl.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\type\AnnounceEventTypeConverter.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\type\LoginType.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\entity\PromotionPolicy.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\type\AnnounceEventType.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\util\IPUtil.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\exception\FixedAnnounceException.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\service\AnnounceService.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\exception\InvalidTorrentPiecesException.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\exception\TrackerException.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\exception\APIErrorCode.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\exception\BrowserReadableAnnounceException.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\repository\CategoryRepository.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\service\UserService.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\dto\response\UserResponseDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\exception\AnnounceBusyException.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\service\ThanksService.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\dto\response\UserGroupResponseDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\crontask\PeersCleanup.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\dto\response\LoginStatusResponseDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\feed\FeedController.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\exception\AnnounceException.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\util\ClassUtil.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\controller\auth\dto\request\LoginRequestDTO.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\util\MiscUtil.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\exception\RESTException.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\ptApplication.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\redisrepository\RedisLoginAttemptRepository.java
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\main\java\com\github\example\pt\repository\LoginHistoryRepository.java
diff --git a/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst
new file mode 100644
index 0000000..21af624
--- /dev/null
+++ b/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst
@@ -0,0 +1 @@
+com\github\example\pt\ptApplicationTests.class
diff --git a/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst
new file mode 100644
index 0000000..95276f3
--- /dev/null
+++ b/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst
@@ -0,0 +1 @@
+E:\desktop\EX\PT_Sixth_Backend_MVN\src\test\java\com\github\example\pt\ptApplicationTests.java
diff --git a/target/surefire-reports/TEST-com.github.example.pt.ptApplicationTests.xml b/target/surefire-reports/TEST-com.github.example.pt.ptApplicationTests.xml
new file mode 100644
index 0000000..5bfe282
--- /dev/null
+++ b/target/surefire-reports/TEST-com.github.example.pt.ptApplicationTests.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<testsuite xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://maven.apache.org/surefire/maven-surefire-plugin/xsd/surefire-test-report.xsd" name="com.github.example.pt.ptApplicationTests" time="12.046" tests="1" errors="0" skipped="0" failures="0">
+ <properties>
+ <property name="java.specification.version" value="17"/>
+ <property name="sun.cpu.isalist" value="amd64"/>
+ <property name="sun.jnu.encoding" value="GBK"/>
+ <property name="java.class.path" value="E:\desktop\EX\PT_Sixth_Backend_MVN\target\test-classes;E:\desktop\EX\PT_Sixth_Backend_MVN\target\classes;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-cache\3.0.2\spring-boot-starter-cache-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter\3.0.2\spring-boot-starter-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-logging\3.0.2\spring-boot-starter-logging-3.0.2.jar;C:\Users\张家豪\.m2\repository\ch\qos\logback\logback-classic\1.4.5\logback-classic-1.4.5.jar;C:\Users\张家豪\.m2\repository\ch\qos\logback\logback-core\1.4.5\logback-core-1.4.5.jar;C:\Users\张家豪\.m2\repository\org\apache\logging\log4j\log4j-to-slf4j\2.19.0\log4j-to-slf4j-2.19.0.jar;C:\Users\张家豪\.m2\repository\org\apache\logging\log4j\log4j-api\2.19.0\log4j-api-2.19.0.jar;C:\Users\张家豪\.m2\repository\org\slf4j\jul-to-slf4j\2.0.6\jul-to-slf4j-2.0.6.jar;C:\Users\张家豪\.m2\repository\org\yaml\snakeyaml\1.33\snakeyaml-1.33.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-context-support\6.0.4\spring-context-support-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-beans\6.0.4\spring-beans-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-context\6.0.4\spring-context-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-data-redis\3.0.2\spring-boot-starter-data-redis-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\data\spring-data-redis\3.0.1\spring-data-redis-3.0.1.jar;C:\Users\张家豪\.m2\repository\org\springframework\data\spring-data-keyvalue\3.0.1\spring-data-keyvalue-3.0.1.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-oxm\6.0.4\spring-oxm-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-aop\6.0.4\spring-aop-6.0.4.jar;C:\Users\张家豪\.m2\repository\io\lettuce\lettuce-core\6.2.2.RELEASE\lettuce-core-6.2.2.RELEASE.jar;C:\Users\张家豪\.m2\repository\io\netty\netty-common\4.1.87.Final\netty-common-4.1.87.Final.jar;C:\Users\张家豪\.m2\repository\io\netty\netty-handler\4.1.87.Final\netty-handler-4.1.87.Final.jar;C:\Users\张家豪\.m2\repository\io\netty\netty-transport-native-unix-common\4.1.87.Final\netty-transport-native-unix-common-4.1.87.Final.jar;C:\Users\张家豪\.m2\repository\io\netty\netty-transport\4.1.87.Final\netty-transport-4.1.87.Final.jar;C:\Users\张家豪\.m2\repository\io\projectreactor\reactor-core\3.5.2\reactor-core-3.5.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-data-jpa\3.0.2\spring-boot-starter-data-jpa-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-aop\3.0.2\spring-boot-starter-aop-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\aspectj\aspectjweaver\1.9.19\aspectjweaver-1.9.19.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-jdbc\3.0.2\spring-boot-starter-jdbc-3.0.2.jar;C:\Users\张家豪\.m2\repository\com\zaxxer\HikariCP\5.0.1\HikariCP-5.0.1.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-jdbc\6.0.4\spring-jdbc-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\hibernate\orm\hibernate-core\6.1.6.Final\hibernate-core-6.1.6.Final.jar;C:\Users\张家豪\.m2\repository\jakarta\persistence\jakarta.persistence-api\3.1.0\jakarta.persistence-api-3.1.0.jar;C:\Users\张家豪\.m2\repository\jakarta\transaction\jakarta.transaction-api\2.0.1\jakarta.transaction-api-2.0.1.jar;C:\Users\张家豪\.m2\repository\org\jboss\logging\jboss-logging\3.5.0.Final\jboss-logging-3.5.0.Final.jar;C:\Users\张家豪\.m2\repository\org\hibernate\common\hibernate-commons-annotations\6.0.2.Final\hibernate-commons-annotations-6.0.2.Final.jar;C:\Users\张家豪\.m2\repository\org\jboss\jandex\2.4.2.Final\jandex-2.4.2.Final.jar;C:\Users\张家豪\.m2\repository\com\fasterxml\classmate\1.5.1\classmate-1.5.1.jar;C:\Users\张家豪\.m2\repository\net\bytebuddy\byte-buddy\1.12.22\byte-buddy-1.12.22.jar;C:\Users\张家豪\.m2\repository\org\glassfish\jaxb\jaxb-runtime\4.0.1\jaxb-runtime-4.0.1.jar;C:\Users\张家豪\.m2\repository\org\glassfish\jaxb\jaxb-core\4.0.1\jaxb-core-4.0.1.jar;C:\Users\张家豪\.m2\repository\org\glassfish\jaxb\txw2\4.0.1\txw2-4.0.1.jar;C:\Users\张家豪\.m2\repository\com\sun\istack\istack-commons-runtime\4.1.1\istack-commons-runtime-4.1.1.jar;C:\Users\张家豪\.m2\repository\jakarta\inject\jakarta.inject-api\2.0.0\jakarta.inject-api-2.0.0.jar;C:\Users\张家豪\.m2\repository\org\antlr\antlr4-runtime\4.10.1\antlr4-runtime-4.10.1.jar;C:\Users\张家豪\.m2\repository\org\springframework\data\spring-data-jpa\3.0.1\spring-data-jpa-3.0.1.jar;C:\Users\张家豪\.m2\repository\org\springframework\data\spring-data-commons\3.0.1\spring-data-commons-3.0.1.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-orm\6.0.4\spring-orm-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-aspects\6.0.4\spring-aspects-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-mail\3.0.2\spring-boot-starter-mail-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\eclipse\angus\jakarta.mail\1.0.0\jakarta.mail-1.0.0.jar;C:\Users\张家豪\.m2\repository\jakarta\activation\jakarta.activation-api\2.1.1\jakarta.activation-api-2.1.1.jar;C:\Users\张家豪\.m2\repository\org\eclipse\angus\angus-activation\1.0.0\angus-activation-1.0.0.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-quartz\3.0.2\spring-boot-starter-quartz-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-tx\6.0.4\spring-tx-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\quartz-scheduler\quartz\2.3.2\quartz-2.3.2.jar;C:\Users\张家豪\.m2\repository\com\mchange\mchange-commons-java\0.2.15\mchange-commons-java-0.2.15.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-validation\3.0.2\spring-boot-starter-validation-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\apache\tomcat\embed\tomcat-embed-el\10.1.5\tomcat-embed-el-10.1.5.jar;C:\Users\张家豪\.m2\repository\org\hibernate\validator\hibernate-validator\8.0.0.Final\hibernate-validator-8.0.0.Final.jar;C:\Users\张家豪\.m2\repository\jakarta\validation\jakarta.validation-api\3.0.2\jakarta.validation-api-3.0.2.jar;C:\Users\张家豪\.m2\repository\cn\dev33\sa-token-spring-boot3-starter\1.34.0\sa-token-spring-boot3-starter-1.34.0.jar;C:\Users\张家豪\.m2\repository\cn\dev33\sa-token-jakarta-servlet\1.34.0\sa-token-jakarta-servlet-1.34.0.jar;C:\Users\张家豪\.m2\repository\jakarta\servlet\jakarta.servlet-api\6.0.0\jakarta.servlet-api-6.0.0.jar;C:\Users\张家豪\.m2\repository\cn\dev33\sa-token-spring-boot-autoconfig\1.34.0\sa-token-spring-boot-autoconfig-1.34.0.jar;C:\Users\张家豪\.m2\repository\cn\dev33\sa-token-dao-redis-jackson\1.34.0\sa-token-dao-redis-jackson-1.34.0.jar;C:\Users\张家豪\.m2\repository\cn\dev33\sa-token-core\1.34.0\sa-token-core-1.34.0.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-web\3.0.2\spring-boot-starter-web-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-web\6.0.4\spring-web-6.0.4.jar;C:\Users\张家豪\.m2\repository\io\micrometer\micrometer-observation\1.10.3\micrometer-observation-1.10.3.jar;C:\Users\张家豪\.m2\repository\io\micrometer\micrometer-commons\1.10.3\micrometer-commons-1.10.3.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-webmvc\6.0.4\spring-webmvc-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-expression\6.0.4\spring-expression-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-json\3.0.2\spring-boot-starter-json-3.0.2.jar;C:\Users\张家豪\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.14.1\jackson-databind-2.14.1.jar;C:\Users\张家豪\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.14.1\jackson-datatype-jdk8-2.14.1.jar;C:\Users\张家豪\.m2\repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.14.1\jackson-module-parameter-names-2.14.1.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-tomcat\3.0.2\spring-boot-starter-tomcat-3.0.2.jar;C:\Users\张家豪\.m2\repository\jakarta\annotation\jakarta.annotation-api\2.1.1\jakarta.annotation-api-2.1.1.jar;C:\Users\张家豪\.m2\repository\org\apache\tomcat\embed\tomcat-embed-core\10.1.5\tomcat-embed-core-10.1.5.jar;C:\Users\张家豪\.m2\repository\org\apache\tomcat\embed\tomcat-embed-websocket\10.1.5\tomcat-embed-websocket-10.1.5.jar;C:\Users\张家豪\.m2\repository\commons-io\commons-io\2.11.0\commons-io-2.11.0.jar;C:\Users\张家豪\.m2\repository\commons-codec\commons-codec\1.15\commons-codec-1.15.jar;C:\Users\张家豪\.m2\repository\commons-validator\commons-validator\1.7\commons-validator-1.7.jar;C:\Users\张家豪\.m2\repository\commons-beanutils\commons-beanutils\1.9.4\commons-beanutils-1.9.4.jar;C:\Users\张家豪\.m2\repository\commons-digester\commons-digester\2.1\commons-digester-2.1.jar;C:\Users\张家豪\.m2\repository\commons-logging\commons-logging\1.2\commons-logging-1.2.jar;C:\Users\张家豪\.m2\repository\commons-collections\commons-collections\3.2.2\commons-collections-3.2.2.jar;C:\Users\张家豪\.m2\repository\org\apache\commons\commons-lang3\3.12.0\commons-lang3-3.12.0.jar;C:\Users\张家豪\.m2\repository\org\apache\commons\commons-text\1.10.0\commons-text-1.10.0.jar;C:\Users\张家豪\.m2\repository\org\apache\commons\commons-compress\1.22\commons-compress-1.22.jar;C:\Users\张家豪\.m2\repository\org\jetbrains\annotations\23.1.0\annotations-23.1.0.jar;C:\Users\张家豪\.m2\repository\com\google\guava\guava\31.1-jre\guava-31.1-jre.jar;C:\Users\张家豪\.m2\repository\com\google\guava\failureaccess\1.0.1\failureaccess-1.0.1.jar;C:\Users\张家豪\.m2\repository\com\google\guava\listenablefuture\9999.0-empty-to-avoid-conflict-with-guava\listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar;C:\Users\张家豪\.m2\repository\com\google\code\findbugs\jsr305\3.0.2\jsr305-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\checkerframework\checker-qual\3.12.0\checker-qual-3.12.0.jar;C:\Users\张家豪\.m2\repository\com\google\errorprone\error_prone_annotations\2.11.0\error_prone_annotations-2.11.0.jar;C:\Users\张家豪\.m2\repository\com\google\j2objc\j2objc-annotations\1.3\j2objc-annotations-1.3.jar;C:\Users\张家豪\.m2\repository\at\favre\lib\bcrypt\0.9.0\bcrypt-0.9.0.jar;C:\Users\张家豪\.m2\repository\at\favre\lib\bytes\1.3.0\bytes-1.3.0.jar;C:\Users\张家豪\.m2\repository\com\konghq\unirest-java\3.14.1\unirest-java-3.14.1.jar;C:\Users\张家豪\.m2\repository\org\apache\httpcomponents\httpclient\4.5.14\httpclient-4.5.14.jar;C:\Users\张家豪\.m2\repository\org\apache\httpcomponents\httpcore\4.4.16\httpcore-4.4.16.jar;C:\Users\张家豪\.m2\repository\org\apache\httpcomponents\httpmime\4.5.14\httpmime-4.5.14.jar;C:\Users\张家豪\.m2\repository\org\apache\httpcomponents\httpcore-nio\4.4.16\httpcore-nio-4.4.16.jar;C:\Users\张家豪\.m2\repository\org\apache\httpcomponents\httpasyncclient\4.1.5\httpasyncclient-4.1.5.jar;C:\Users\张家豪\.m2\repository\com\google\code\gson\gson\2.9.1\gson-2.9.1.jar;C:\Users\张家豪\.m2\repository\me\tongfei\progressbar\0.9.5\progressbar-0.9.5.jar;C:\Users\张家豪\.m2\repository\org\jline\jline\3.21.0\jline-3.21.0.jar;C:\Users\张家豪\.m2\repository\com\dampcake\bencode\1.4\bencode-1.4.jar;C:\Users\张家豪\.m2\repository\com\mysql\mysql-connector-j\8.0.32\mysql-connector-j-8.0.32.jar;C:\Users\张家豪\.m2\repository\com\h2database\h2\2.1.214\h2-2.1.214.jar;C:\Users\张家豪\.m2\repository\org\projectlombok\lombok\1.18.24\lombok-1.18.24.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-devtools\3.0.2\spring-boot-devtools-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot\3.0.2\spring-boot-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\3.0.2\spring-boot-autoconfigure-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-test\3.0.2\spring-boot-starter-test-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-test\3.0.2\spring-boot-test-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-test-autoconfigure\3.0.2\spring-boot-test-autoconfigure-3.0.2.jar;C:\Users\张家豪\.m2\repository\com\jayway\jsonpath\json-path\2.7.0\json-path-2.7.0.jar;C:\Users\张家豪\.m2\repository\net\minidev\json-smart\2.4.8\json-smart-2.4.8.jar;C:\Users\张家豪\.m2\repository\net\minidev\accessors-smart\2.4.8\accessors-smart-2.4.8.jar;C:\Users\张家豪\.m2\repository\org\ow2\asm\asm\9.1\asm-9.1.jar;C:\Users\张家豪\.m2\repository\jakarta\xml\bind\jakarta.xml.bind-api\4.0.0\jakarta.xml.bind-api-4.0.0.jar;C:\Users\张家豪\.m2\repository\org\assertj\assertj-core\3.23.1\assertj-core-3.23.1.jar;C:\Users\张家豪\.m2\repository\org\hamcrest\hamcrest\2.2\hamcrest-2.2.jar;C:\Users\张家豪\.m2\repository\org\junit\jupiter\junit-jupiter\5.9.2\junit-jupiter-5.9.2.jar;C:\Users\张家豪\.m2\repository\org\junit\jupiter\junit-jupiter-api\5.9.2\junit-jupiter-api-5.9.2.jar;C:\Users\张家豪\.m2\repository\org\opentest4j\opentest4j\1.2.0\opentest4j-1.2.0.jar;C:\Users\张家豪\.m2\repository\org\junit\platform\junit-platform-commons\1.9.2\junit-platform-commons-1.9.2.jar;C:\Users\张家豪\.m2\repository\org\apiguardian\apiguardian-api\1.1.2\apiguardian-api-1.1.2.jar;C:\Users\张家豪\.m2\repository\org\junit\jupiter\junit-jupiter-params\5.9.2\junit-jupiter-params-5.9.2.jar;C:\Users\张家豪\.m2\repository\org\junit\jupiter\junit-jupiter-engine\5.9.2\junit-jupiter-engine-5.9.2.jar;C:\Users\张家豪\.m2\repository\org\junit\platform\junit-platform-engine\1.9.2\junit-platform-engine-1.9.2.jar;C:\Users\张家豪\.m2\repository\org\mockito\mockito-core\4.8.1\mockito-core-4.8.1.jar;C:\Users\张家豪\.m2\repository\net\bytebuddy\byte-buddy-agent\1.12.22\byte-buddy-agent-1.12.22.jar;C:\Users\张家豪\.m2\repository\org\objenesis\objenesis\3.2\objenesis-3.2.jar;C:\Users\张家豪\.m2\repository\org\mockito\mockito-junit-jupiter\4.8.1\mockito-junit-jupiter-4.8.1.jar;C:\Users\张家豪\.m2\repository\org\skyscreamer\jsonassert\1.5.1\jsonassert-1.5.1.jar;C:\Users\张家豪\.m2\repository\com\vaadin\external\google\android-json\0.0.20131108.vaadin1\android-json-0.0.20131108.vaadin1.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-core\6.0.4\spring-core-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-jcl\6.0.4\spring-jcl-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-test\6.0.4\spring-test-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\xmlunit\xmlunit-core\2.9.1\xmlunit-core-2.9.1.jar;C:\Users\张家豪\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.14.1\jackson-datatype-jsr310-2.14.1.jar;C:\Users\张家豪\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.14.1\jackson-annotations-2.14.1.jar;C:\Users\张家豪\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.14.1\jackson-core-2.14.1.jar;C:\Users\张家豪\.m2\repository\org\redisson\redisson-hibernate-6\3.19.3\redisson-hibernate-6-3.19.3.jar;C:\Users\张家豪\.m2\repository\org\redisson\redisson\3.19.3\redisson-3.19.3.jar;C:\Users\张家豪\.m2\repository\io\netty\netty-codec\4.1.87.Final\netty-codec-4.1.87.Final.jar;C:\Users\张家豪\.m2\repository\io\netty\netty-buffer\4.1.87.Final\netty-buffer-4.1.87.Final.jar;C:\Users\张家豪\.m2\repository\io\netty\netty-resolver\4.1.87.Final\netty-resolver-4.1.87.Final.jar;C:\Users\张家豪\.m2\repository\io\netty\netty-resolver-dns\4.1.87.Final\netty-resolver-dns-4.1.87.Final.jar;C:\Users\张家豪\.m2\repository\io\netty\netty-codec-dns\4.1.87.Final\netty-codec-dns-4.1.87.Final.jar;C:\Users\张家豪\.m2\repository\javax\cache\cache-api\1.1.1\cache-api-1.1.1.jar;C:\Users\张家豪\.m2\repository\org\reactivestreams\reactive-streams\1.0.4\reactive-streams-1.0.4.jar;C:\Users\张家豪\.m2\repository\io\reactivex\rxjava3\rxjava\3.1.6\rxjava-3.1.6.jar;C:\Users\张家豪\.m2\repository\org\jboss\marshalling\jboss-marshalling\2.0.11.Final\jboss-marshalling-2.0.11.Final.jar;C:\Users\张家豪\.m2\repository\org\jboss\marshalling\jboss-marshalling-river\2.0.11.Final\jboss-marshalling-river-2.0.11.Final.jar;C:\Users\张家豪\.m2\repository\com\esotericsoftware\kryo\5.4.0\kryo-5.4.0.jar;C:\Users\张家豪\.m2\repository\com\esotericsoftware\reflectasm\1.11.9\reflectasm-1.11.9.jar;C:\Users\张家豪\.m2\repository\com\esotericsoftware\minlog\1.3.1\minlog-1.3.1.jar;C:\Users\张家豪\.m2\repository\com\fasterxml\jackson\dataformat\jackson-dataformat-yaml\2.14.1\jackson-dataformat-yaml-2.14.1.jar;C:\Users\张家豪\.m2\repository\org\jodd\jodd-bean\5.1.6\jodd-bean-5.1.6.jar;C:\Users\张家豪\.m2\repository\org\jodd\jodd-core\5.1.6\jodd-core-5.1.6.jar;C:\Users\张家豪\.m2\repository\com\googlecode\owasp-java-html-sanitizer\owasp-java-html-sanitizer\20220608.1\owasp-java-html-sanitizer-20220608.1.jar;C:\Users\张家豪\.m2\repository\org\apache\commons\commons-pool2\2.11.1\commons-pool2-2.11.1.jar;C:\Users\张家豪\.m2\repository\com\rometools\rome\1.18.0\rome-1.18.0.jar;C:\Users\张家豪\.m2\repository\com\rometools\rome-utils\1.18.0\rome-utils-1.18.0.jar;C:\Users\张家豪\.m2\repository\org\jdom\jdom2\2.0.6.1\jdom2-2.0.6.1.jar;C:\Users\张家豪\.m2\repository\org\slf4j\slf4j-api\2.0.6\slf4j-api-2.0.6.jar;C:\Users\张家豪\.m2\repository\com\alicp\jetcache\jetcache-starter-redis\2.7.3\jetcache-starter-redis-2.7.3.jar;C:\Users\张家豪\.m2\repository\com\alicp\jetcache\jetcache-autoconfigure\2.7.3\jetcache-autoconfigure-2.7.3.jar;C:\Users\张家豪\.m2\repository\com\alicp\jetcache\jetcache-anno\2.7.3\jetcache-anno-2.7.3.jar;C:\Users\张家豪\.m2\repository\com\alicp\jetcache\jetcache-core\2.7.3\jetcache-core-2.7.3.jar;C:\Users\张家豪\.m2\repository\com\alicp\jetcache\jetcache-anno-api\2.7.3\jetcache-anno-api-2.7.3.jar;C:\Users\张家豪\.m2\repository\com\alibaba\fastjson2\fastjson2\2.0.21\fastjson2-2.0.21.jar;C:\Users\张家豪\.m2\repository\com\github\ben-manes\caffeine\caffeine\3.1.2\caffeine-3.1.2.jar;C:\Users\张家豪\.m2\repository\javax\annotation\javax.annotation-api\1.3.1\javax.annotation-api-1.3.1.jar;C:\Users\张家豪\.m2\repository\com\alicp\jetcache\jetcache-redis\2.7.3\jetcache-redis-2.7.3.jar;C:\Users\张家豪\.m2\repository\redis\clients\jedis\4.3.1\jedis-4.3.1.jar;C:\Users\张家豪\.m2\repository\org\json\json\20220320\json-20220320.jar;C:\Users\张家豪\.m2\repository\org\greenrobot\eventbus-java\3.3.1\eventbus-java-3.3.1.jar;"/>
+ <property name="java.vm.vendor" value="Microsoft"/>
+ <property name="sun.arch.data.model" value="64"/>
+ <property name="user.variant" value=""/>
+ <property name="java.vendor.url" value="https://www.microsoft.com"/>
+ <property name="user.timezone" value="Asia/Shanghai"/>
+ <property name="os.name" value="Windows 11"/>
+ <property name="java.vm.specification.version" value="17"/>
+ <property name="sun.java.launcher" value="SUN_STANDARD"/>
+ <property name="user.country" value="CN"/>
+ <property name="sun.boot.library.path" value="C:\Users\张家豪\.jdks\ms-17.0.15\bin"/>
+ <property name="sun.java.command" value="C:\WINDOWS\TEMP\surefire4451744251208519186\surefirebooter15867557814126146353.jar C:\Windows\Temp\surefire4451744251208519186 2025-06-04T22-30-38_359-jvmRun1 surefire1302598587474915774tmp surefire_011250240831511699183tmp"/>
+ <property name="jdk.debug" value="release"/>
+ <property name="surefire.test.class.path" value="E:\desktop\EX\PT_Sixth_Backend_MVN\target\test-classes;E:\desktop\EX\PT_Sixth_Backend_MVN\target\classes;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-cache\3.0.2\spring-boot-starter-cache-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter\3.0.2\spring-boot-starter-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-logging\3.0.2\spring-boot-starter-logging-3.0.2.jar;C:\Users\张家豪\.m2\repository\ch\qos\logback\logback-classic\1.4.5\logback-classic-1.4.5.jar;C:\Users\张家豪\.m2\repository\ch\qos\logback\logback-core\1.4.5\logback-core-1.4.5.jar;C:\Users\张家豪\.m2\repository\org\apache\logging\log4j\log4j-to-slf4j\2.19.0\log4j-to-slf4j-2.19.0.jar;C:\Users\张家豪\.m2\repository\org\apache\logging\log4j\log4j-api\2.19.0\log4j-api-2.19.0.jar;C:\Users\张家豪\.m2\repository\org\slf4j\jul-to-slf4j\2.0.6\jul-to-slf4j-2.0.6.jar;C:\Users\张家豪\.m2\repository\org\yaml\snakeyaml\1.33\snakeyaml-1.33.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-context-support\6.0.4\spring-context-support-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-beans\6.0.4\spring-beans-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-context\6.0.4\spring-context-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-data-redis\3.0.2\spring-boot-starter-data-redis-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\data\spring-data-redis\3.0.1\spring-data-redis-3.0.1.jar;C:\Users\张家豪\.m2\repository\org\springframework\data\spring-data-keyvalue\3.0.1\spring-data-keyvalue-3.0.1.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-oxm\6.0.4\spring-oxm-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-aop\6.0.4\spring-aop-6.0.4.jar;C:\Users\张家豪\.m2\repository\io\lettuce\lettuce-core\6.2.2.RELEASE\lettuce-core-6.2.2.RELEASE.jar;C:\Users\张家豪\.m2\repository\io\netty\netty-common\4.1.87.Final\netty-common-4.1.87.Final.jar;C:\Users\张家豪\.m2\repository\io\netty\netty-handler\4.1.87.Final\netty-handler-4.1.87.Final.jar;C:\Users\张家豪\.m2\repository\io\netty\netty-transport-native-unix-common\4.1.87.Final\netty-transport-native-unix-common-4.1.87.Final.jar;C:\Users\张家豪\.m2\repository\io\netty\netty-transport\4.1.87.Final\netty-transport-4.1.87.Final.jar;C:\Users\张家豪\.m2\repository\io\projectreactor\reactor-core\3.5.2\reactor-core-3.5.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-data-jpa\3.0.2\spring-boot-starter-data-jpa-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-aop\3.0.2\spring-boot-starter-aop-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\aspectj\aspectjweaver\1.9.19\aspectjweaver-1.9.19.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-jdbc\3.0.2\spring-boot-starter-jdbc-3.0.2.jar;C:\Users\张家豪\.m2\repository\com\zaxxer\HikariCP\5.0.1\HikariCP-5.0.1.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-jdbc\6.0.4\spring-jdbc-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\hibernate\orm\hibernate-core\6.1.6.Final\hibernate-core-6.1.6.Final.jar;C:\Users\张家豪\.m2\repository\jakarta\persistence\jakarta.persistence-api\3.1.0\jakarta.persistence-api-3.1.0.jar;C:\Users\张家豪\.m2\repository\jakarta\transaction\jakarta.transaction-api\2.0.1\jakarta.transaction-api-2.0.1.jar;C:\Users\张家豪\.m2\repository\org\jboss\logging\jboss-logging\3.5.0.Final\jboss-logging-3.5.0.Final.jar;C:\Users\张家豪\.m2\repository\org\hibernate\common\hibernate-commons-annotations\6.0.2.Final\hibernate-commons-annotations-6.0.2.Final.jar;C:\Users\张家豪\.m2\repository\org\jboss\jandex\2.4.2.Final\jandex-2.4.2.Final.jar;C:\Users\张家豪\.m2\repository\com\fasterxml\classmate\1.5.1\classmate-1.5.1.jar;C:\Users\张家豪\.m2\repository\net\bytebuddy\byte-buddy\1.12.22\byte-buddy-1.12.22.jar;C:\Users\张家豪\.m2\repository\org\glassfish\jaxb\jaxb-runtime\4.0.1\jaxb-runtime-4.0.1.jar;C:\Users\张家豪\.m2\repository\org\glassfish\jaxb\jaxb-core\4.0.1\jaxb-core-4.0.1.jar;C:\Users\张家豪\.m2\repository\org\glassfish\jaxb\txw2\4.0.1\txw2-4.0.1.jar;C:\Users\张家豪\.m2\repository\com\sun\istack\istack-commons-runtime\4.1.1\istack-commons-runtime-4.1.1.jar;C:\Users\张家豪\.m2\repository\jakarta\inject\jakarta.inject-api\2.0.0\jakarta.inject-api-2.0.0.jar;C:\Users\张家豪\.m2\repository\org\antlr\antlr4-runtime\4.10.1\antlr4-runtime-4.10.1.jar;C:\Users\张家豪\.m2\repository\org\springframework\data\spring-data-jpa\3.0.1\spring-data-jpa-3.0.1.jar;C:\Users\张家豪\.m2\repository\org\springframework\data\spring-data-commons\3.0.1\spring-data-commons-3.0.1.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-orm\6.0.4\spring-orm-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-aspects\6.0.4\spring-aspects-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-mail\3.0.2\spring-boot-starter-mail-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\eclipse\angus\jakarta.mail\1.0.0\jakarta.mail-1.0.0.jar;C:\Users\张家豪\.m2\repository\jakarta\activation\jakarta.activation-api\2.1.1\jakarta.activation-api-2.1.1.jar;C:\Users\张家豪\.m2\repository\org\eclipse\angus\angus-activation\1.0.0\angus-activation-1.0.0.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-quartz\3.0.2\spring-boot-starter-quartz-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-tx\6.0.4\spring-tx-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\quartz-scheduler\quartz\2.3.2\quartz-2.3.2.jar;C:\Users\张家豪\.m2\repository\com\mchange\mchange-commons-java\0.2.15\mchange-commons-java-0.2.15.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-validation\3.0.2\spring-boot-starter-validation-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\apache\tomcat\embed\tomcat-embed-el\10.1.5\tomcat-embed-el-10.1.5.jar;C:\Users\张家豪\.m2\repository\org\hibernate\validator\hibernate-validator\8.0.0.Final\hibernate-validator-8.0.0.Final.jar;C:\Users\张家豪\.m2\repository\jakarta\validation\jakarta.validation-api\3.0.2\jakarta.validation-api-3.0.2.jar;C:\Users\张家豪\.m2\repository\cn\dev33\sa-token-spring-boot3-starter\1.34.0\sa-token-spring-boot3-starter-1.34.0.jar;C:\Users\张家豪\.m2\repository\cn\dev33\sa-token-jakarta-servlet\1.34.0\sa-token-jakarta-servlet-1.34.0.jar;C:\Users\张家豪\.m2\repository\jakarta\servlet\jakarta.servlet-api\6.0.0\jakarta.servlet-api-6.0.0.jar;C:\Users\张家豪\.m2\repository\cn\dev33\sa-token-spring-boot-autoconfig\1.34.0\sa-token-spring-boot-autoconfig-1.34.0.jar;C:\Users\张家豪\.m2\repository\cn\dev33\sa-token-dao-redis-jackson\1.34.0\sa-token-dao-redis-jackson-1.34.0.jar;C:\Users\张家豪\.m2\repository\cn\dev33\sa-token-core\1.34.0\sa-token-core-1.34.0.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-web\3.0.2\spring-boot-starter-web-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-web\6.0.4\spring-web-6.0.4.jar;C:\Users\张家豪\.m2\repository\io\micrometer\micrometer-observation\1.10.3\micrometer-observation-1.10.3.jar;C:\Users\张家豪\.m2\repository\io\micrometer\micrometer-commons\1.10.3\micrometer-commons-1.10.3.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-webmvc\6.0.4\spring-webmvc-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-expression\6.0.4\spring-expression-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-json\3.0.2\spring-boot-starter-json-3.0.2.jar;C:\Users\张家豪\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.14.1\jackson-databind-2.14.1.jar;C:\Users\张家豪\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.14.1\jackson-datatype-jdk8-2.14.1.jar;C:\Users\张家豪\.m2\repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.14.1\jackson-module-parameter-names-2.14.1.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-tomcat\3.0.2\spring-boot-starter-tomcat-3.0.2.jar;C:\Users\张家豪\.m2\repository\jakarta\annotation\jakarta.annotation-api\2.1.1\jakarta.annotation-api-2.1.1.jar;C:\Users\张家豪\.m2\repository\org\apache\tomcat\embed\tomcat-embed-core\10.1.5\tomcat-embed-core-10.1.5.jar;C:\Users\张家豪\.m2\repository\org\apache\tomcat\embed\tomcat-embed-websocket\10.1.5\tomcat-embed-websocket-10.1.5.jar;C:\Users\张家豪\.m2\repository\commons-io\commons-io\2.11.0\commons-io-2.11.0.jar;C:\Users\张家豪\.m2\repository\commons-codec\commons-codec\1.15\commons-codec-1.15.jar;C:\Users\张家豪\.m2\repository\commons-validator\commons-validator\1.7\commons-validator-1.7.jar;C:\Users\张家豪\.m2\repository\commons-beanutils\commons-beanutils\1.9.4\commons-beanutils-1.9.4.jar;C:\Users\张家豪\.m2\repository\commons-digester\commons-digester\2.1\commons-digester-2.1.jar;C:\Users\张家豪\.m2\repository\commons-logging\commons-logging\1.2\commons-logging-1.2.jar;C:\Users\张家豪\.m2\repository\commons-collections\commons-collections\3.2.2\commons-collections-3.2.2.jar;C:\Users\张家豪\.m2\repository\org\apache\commons\commons-lang3\3.12.0\commons-lang3-3.12.0.jar;C:\Users\张家豪\.m2\repository\org\apache\commons\commons-text\1.10.0\commons-text-1.10.0.jar;C:\Users\张家豪\.m2\repository\org\apache\commons\commons-compress\1.22\commons-compress-1.22.jar;C:\Users\张家豪\.m2\repository\org\jetbrains\annotations\23.1.0\annotations-23.1.0.jar;C:\Users\张家豪\.m2\repository\com\google\guava\guava\31.1-jre\guava-31.1-jre.jar;C:\Users\张家豪\.m2\repository\com\google\guava\failureaccess\1.0.1\failureaccess-1.0.1.jar;C:\Users\张家豪\.m2\repository\com\google\guava\listenablefuture\9999.0-empty-to-avoid-conflict-with-guava\listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar;C:\Users\张家豪\.m2\repository\com\google\code\findbugs\jsr305\3.0.2\jsr305-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\checkerframework\checker-qual\3.12.0\checker-qual-3.12.0.jar;C:\Users\张家豪\.m2\repository\com\google\errorprone\error_prone_annotations\2.11.0\error_prone_annotations-2.11.0.jar;C:\Users\张家豪\.m2\repository\com\google\j2objc\j2objc-annotations\1.3\j2objc-annotations-1.3.jar;C:\Users\张家豪\.m2\repository\at\favre\lib\bcrypt\0.9.0\bcrypt-0.9.0.jar;C:\Users\张家豪\.m2\repository\at\favre\lib\bytes\1.3.0\bytes-1.3.0.jar;C:\Users\张家豪\.m2\repository\com\konghq\unirest-java\3.14.1\unirest-java-3.14.1.jar;C:\Users\张家豪\.m2\repository\org\apache\httpcomponents\httpclient\4.5.14\httpclient-4.5.14.jar;C:\Users\张家豪\.m2\repository\org\apache\httpcomponents\httpcore\4.4.16\httpcore-4.4.16.jar;C:\Users\张家豪\.m2\repository\org\apache\httpcomponents\httpmime\4.5.14\httpmime-4.5.14.jar;C:\Users\张家豪\.m2\repository\org\apache\httpcomponents\httpcore-nio\4.4.16\httpcore-nio-4.4.16.jar;C:\Users\张家豪\.m2\repository\org\apache\httpcomponents\httpasyncclient\4.1.5\httpasyncclient-4.1.5.jar;C:\Users\张家豪\.m2\repository\com\google\code\gson\gson\2.9.1\gson-2.9.1.jar;C:\Users\张家豪\.m2\repository\me\tongfei\progressbar\0.9.5\progressbar-0.9.5.jar;C:\Users\张家豪\.m2\repository\org\jline\jline\3.21.0\jline-3.21.0.jar;C:\Users\张家豪\.m2\repository\com\dampcake\bencode\1.4\bencode-1.4.jar;C:\Users\张家豪\.m2\repository\com\mysql\mysql-connector-j\8.0.32\mysql-connector-j-8.0.32.jar;C:\Users\张家豪\.m2\repository\com\h2database\h2\2.1.214\h2-2.1.214.jar;C:\Users\张家豪\.m2\repository\org\projectlombok\lombok\1.18.24\lombok-1.18.24.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-devtools\3.0.2\spring-boot-devtools-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot\3.0.2\spring-boot-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\3.0.2\spring-boot-autoconfigure-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-starter-test\3.0.2\spring-boot-starter-test-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-test\3.0.2\spring-boot-test-3.0.2.jar;C:\Users\张家豪\.m2\repository\org\springframework\boot\spring-boot-test-autoconfigure\3.0.2\spring-boot-test-autoconfigure-3.0.2.jar;C:\Users\张家豪\.m2\repository\com\jayway\jsonpath\json-path\2.7.0\json-path-2.7.0.jar;C:\Users\张家豪\.m2\repository\net\minidev\json-smart\2.4.8\json-smart-2.4.8.jar;C:\Users\张家豪\.m2\repository\net\minidev\accessors-smart\2.4.8\accessors-smart-2.4.8.jar;C:\Users\张家豪\.m2\repository\org\ow2\asm\asm\9.1\asm-9.1.jar;C:\Users\张家豪\.m2\repository\jakarta\xml\bind\jakarta.xml.bind-api\4.0.0\jakarta.xml.bind-api-4.0.0.jar;C:\Users\张家豪\.m2\repository\org\assertj\assertj-core\3.23.1\assertj-core-3.23.1.jar;C:\Users\张家豪\.m2\repository\org\hamcrest\hamcrest\2.2\hamcrest-2.2.jar;C:\Users\张家豪\.m2\repository\org\junit\jupiter\junit-jupiter\5.9.2\junit-jupiter-5.9.2.jar;C:\Users\张家豪\.m2\repository\org\junit\jupiter\junit-jupiter-api\5.9.2\junit-jupiter-api-5.9.2.jar;C:\Users\张家豪\.m2\repository\org\opentest4j\opentest4j\1.2.0\opentest4j-1.2.0.jar;C:\Users\张家豪\.m2\repository\org\junit\platform\junit-platform-commons\1.9.2\junit-platform-commons-1.9.2.jar;C:\Users\张家豪\.m2\repository\org\apiguardian\apiguardian-api\1.1.2\apiguardian-api-1.1.2.jar;C:\Users\张家豪\.m2\repository\org\junit\jupiter\junit-jupiter-params\5.9.2\junit-jupiter-params-5.9.2.jar;C:\Users\张家豪\.m2\repository\org\junit\jupiter\junit-jupiter-engine\5.9.2\junit-jupiter-engine-5.9.2.jar;C:\Users\张家豪\.m2\repository\org\junit\platform\junit-platform-engine\1.9.2\junit-platform-engine-1.9.2.jar;C:\Users\张家豪\.m2\repository\org\mockito\mockito-core\4.8.1\mockito-core-4.8.1.jar;C:\Users\张家豪\.m2\repository\net\bytebuddy\byte-buddy-agent\1.12.22\byte-buddy-agent-1.12.22.jar;C:\Users\张家豪\.m2\repository\org\objenesis\objenesis\3.2\objenesis-3.2.jar;C:\Users\张家豪\.m2\repository\org\mockito\mockito-junit-jupiter\4.8.1\mockito-junit-jupiter-4.8.1.jar;C:\Users\张家豪\.m2\repository\org\skyscreamer\jsonassert\1.5.1\jsonassert-1.5.1.jar;C:\Users\张家豪\.m2\repository\com\vaadin\external\google\android-json\0.0.20131108.vaadin1\android-json-0.0.20131108.vaadin1.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-core\6.0.4\spring-core-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-jcl\6.0.4\spring-jcl-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\springframework\spring-test\6.0.4\spring-test-6.0.4.jar;C:\Users\张家豪\.m2\repository\org\xmlunit\xmlunit-core\2.9.1\xmlunit-core-2.9.1.jar;C:\Users\张家豪\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.14.1\jackson-datatype-jsr310-2.14.1.jar;C:\Users\张家豪\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.14.1\jackson-annotations-2.14.1.jar;C:\Users\张家豪\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.14.1\jackson-core-2.14.1.jar;C:\Users\张家豪\.m2\repository\org\redisson\redisson-hibernate-6\3.19.3\redisson-hibernate-6-3.19.3.jar;C:\Users\张家豪\.m2\repository\org\redisson\redisson\3.19.3\redisson-3.19.3.jar;C:\Users\张家豪\.m2\repository\io\netty\netty-codec\4.1.87.Final\netty-codec-4.1.87.Final.jar;C:\Users\张家豪\.m2\repository\io\netty\netty-buffer\4.1.87.Final\netty-buffer-4.1.87.Final.jar;C:\Users\张家豪\.m2\repository\io\netty\netty-resolver\4.1.87.Final\netty-resolver-4.1.87.Final.jar;C:\Users\张家豪\.m2\repository\io\netty\netty-resolver-dns\4.1.87.Final\netty-resolver-dns-4.1.87.Final.jar;C:\Users\张家豪\.m2\repository\io\netty\netty-codec-dns\4.1.87.Final\netty-codec-dns-4.1.87.Final.jar;C:\Users\张家豪\.m2\repository\javax\cache\cache-api\1.1.1\cache-api-1.1.1.jar;C:\Users\张家豪\.m2\repository\org\reactivestreams\reactive-streams\1.0.4\reactive-streams-1.0.4.jar;C:\Users\张家豪\.m2\repository\io\reactivex\rxjava3\rxjava\3.1.6\rxjava-3.1.6.jar;C:\Users\张家豪\.m2\repository\org\jboss\marshalling\jboss-marshalling\2.0.11.Final\jboss-marshalling-2.0.11.Final.jar;C:\Users\张家豪\.m2\repository\org\jboss\marshalling\jboss-marshalling-river\2.0.11.Final\jboss-marshalling-river-2.0.11.Final.jar;C:\Users\张家豪\.m2\repository\com\esotericsoftware\kryo\5.4.0\kryo-5.4.0.jar;C:\Users\张家豪\.m2\repository\com\esotericsoftware\reflectasm\1.11.9\reflectasm-1.11.9.jar;C:\Users\张家豪\.m2\repository\com\esotericsoftware\minlog\1.3.1\minlog-1.3.1.jar;C:\Users\张家豪\.m2\repository\com\fasterxml\jackson\dataformat\jackson-dataformat-yaml\2.14.1\jackson-dataformat-yaml-2.14.1.jar;C:\Users\张家豪\.m2\repository\org\jodd\jodd-bean\5.1.6\jodd-bean-5.1.6.jar;C:\Users\张家豪\.m2\repository\org\jodd\jodd-core\5.1.6\jodd-core-5.1.6.jar;C:\Users\张家豪\.m2\repository\com\googlecode\owasp-java-html-sanitizer\owasp-java-html-sanitizer\20220608.1\owasp-java-html-sanitizer-20220608.1.jar;C:\Users\张家豪\.m2\repository\org\apache\commons\commons-pool2\2.11.1\commons-pool2-2.11.1.jar;C:\Users\张家豪\.m2\repository\com\rometools\rome\1.18.0\rome-1.18.0.jar;C:\Users\张家豪\.m2\repository\com\rometools\rome-utils\1.18.0\rome-utils-1.18.0.jar;C:\Users\张家豪\.m2\repository\org\jdom\jdom2\2.0.6.1\jdom2-2.0.6.1.jar;C:\Users\张家豪\.m2\repository\org\slf4j\slf4j-api\2.0.6\slf4j-api-2.0.6.jar;C:\Users\张家豪\.m2\repository\com\alicp\jetcache\jetcache-starter-redis\2.7.3\jetcache-starter-redis-2.7.3.jar;C:\Users\张家豪\.m2\repository\com\alicp\jetcache\jetcache-autoconfigure\2.7.3\jetcache-autoconfigure-2.7.3.jar;C:\Users\张家豪\.m2\repository\com\alicp\jetcache\jetcache-anno\2.7.3\jetcache-anno-2.7.3.jar;C:\Users\张家豪\.m2\repository\com\alicp\jetcache\jetcache-core\2.7.3\jetcache-core-2.7.3.jar;C:\Users\张家豪\.m2\repository\com\alicp\jetcache\jetcache-anno-api\2.7.3\jetcache-anno-api-2.7.3.jar;C:\Users\张家豪\.m2\repository\com\alibaba\fastjson2\fastjson2\2.0.21\fastjson2-2.0.21.jar;C:\Users\张家豪\.m2\repository\com\github\ben-manes\caffeine\caffeine\3.1.2\caffeine-3.1.2.jar;C:\Users\张家豪\.m2\repository\javax\annotation\javax.annotation-api\1.3.1\javax.annotation-api-1.3.1.jar;C:\Users\张家豪\.m2\repository\com\alicp\jetcache\jetcache-redis\2.7.3\jetcache-redis-2.7.3.jar;C:\Users\张家豪\.m2\repository\redis\clients\jedis\4.3.1\jedis-4.3.1.jar;C:\Users\张家豪\.m2\repository\org\json\json\20220320\json-20220320.jar;C:\Users\张家豪\.m2\repository\org\greenrobot\eventbus-java\3.3.1\eventbus-java-3.3.1.jar;"/>
+ <property name="sun.cpu.endian" value="little"/>
+ <property name="user.home" value="C:\Users\张家豪"/>
+ <property name="user.language" value="zh"/>
+ <property name="java.specification.vendor" value="Oracle Corporation"/>
+ <property name="java.version.date" value="2025-04-15"/>
+ <property name="java.home" value="C:\Users\张家豪\.jdks\ms-17.0.15"/>
+ <property name="file.separator" value="\"/>
+ <property name="basedir" value="E:\desktop\EX\PT_Sixth_Backend_MVN"/>
+ <property name="java.vm.compressedOopsMode" value="Zero based"/>
+ <property name="line.separator" value=" "/>
+ <property name="java.vm.specification.vendor" value="Oracle Corporation"/>
+ <property name="java.specification.name" value="Java Platform API Specification"/>
+ <property name="surefire.real.class.path" value="C:\WINDOWS\TEMP\surefire4451744251208519186\surefirebooter15867557814126146353.jar"/>
+ <property name="user.script" value=""/>
+ <property name="sun.management.compiler" value="HotSpot 64-Bit Tiered Compilers"/>
+ <property name="java.runtime.version" value="17.0.15+6-LTS"/>
+ <property name="user.name" value="张家豪"/>
+ <property name="path.separator" value=";"/>
+ <property name="os.version" value="10.0"/>
+ <property name="java.runtime.name" value="OpenJDK Runtime Environment"/>
+ <property name="file.encoding" value="GBK"/>
+ <property name="java.vm.name" value="OpenJDK 64-Bit Server VM"/>
+ <property name="java.vendor.version" value="Microsoft-11369865"/>
+ <property name="localRepository" value="C:\Users\张家豪\.m2\repository"/>
+ <property name="java.vendor.url.bug" value="https://github.com/microsoft/openjdk/issues"/>
+ <property name="java.io.tmpdir" value="C:\WINDOWS\TEMP\"/>
+ <property name="idea.version" value="2024.2.4"/>
+ <property name="java.version" value="17.0.15"/>
+ <property name="user.dir" value="E:\desktop\EX\PT_Sixth_Backend_MVN"/>
+ <property name="os.arch" value="amd64"/>
+ <property name="java.vm.specification.name" value="Java Virtual Machine Specification"/>
+ <property name="sun.os.patch.level" value=""/>
+ <property name="native.encoding" value="GBK"/>
+ <property name="java.library.path" value="C:\Users\张家豪\.jdks\ms-17.0.15\bin;C:\WINDOWS\Sun\Java\bin;C:\WINDOWS\system32;C:\WINDOWS;E:\ProgramData\anaconda3\condabin;E:\Program Files\Python312\Scripts\;E:\Program Files\Python312\;C:\MinGW\bin;C:\Program Files\Common Files\Oracle\Java\javapath;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\Program Files\NVIDIA Corporation\NVIDIA NvDLISR;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;E:\MinGW\bin;C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit\;C:\Program Files\Java\jdk-19\bin;C:\Program Files\Java\jdk-19\jre\bin;E:\ComplierComplier\GnuWin32\bin;E:\ComplierComplier\MinGW\bin;E:\Program Files\nodejs\;E:\Program Files\nodejs\node_global\node_modules;E:\Program Files\Git\cmd;E:\Program Files\Redis\;E:\ProgramData\anaconda3;E:\ProgramData\anaconda3\Scripts;E:\ProgramData\anaconda3\Library\bin;E:\ProgramData\anaconda3\Library\mingw-w64\bin;E:\ProgramData\anaconda3\Library\usr\bin;E:\Program Files\apache-maven-3.9.9\bin;E:\docker\resources\bin;E:\Program Files\Git LFS;C:\MinGW\bin;C:\Users\张家豪\AppData\Local\Microsoft\WindowsApps;C:\texlive\2023\bin\windows;E:\Program Files\IDEA\IntelliJ IDEA Community Edition 2024.1\bin;;E:\Tomcat\apache-tomcat-10.1.20\bin\;E:\Program Files\Python工具\PyCharm Community Edition 2024.1.4\bin;;E:\Program Files\nodejs\node_global;C:\Users\张家豪\AppData\Local\Programs\Microsoft VS Code\bin;;."/>
+ <property name="java.vm.info" value="mixed mode, sharing"/>
+ <property name="java.vendor" value="Microsoft"/>
+ <property name="java.vm.version" value="17.0.15+6-LTS"/>
+ <property name="java.specification.maintenance.version" value="1"/>
+ <property name="sun.io.unicode.encoding" value="UnicodeLittle"/>
+ <property name="java.class.version" value="61.0"/>
+ </properties>
+ <testcase name="contextLoads" classname="com.github.example.pt.ptApplicationTests" time="0.365"/>
+</testsuite>
\ No newline at end of file
diff --git a/target/surefire-reports/com.github.example.pt.ptApplicationTests.txt b/target/surefire-reports/com.github.example.pt.ptApplicationTests.txt
new file mode 100644
index 0000000..ffad9d6
--- /dev/null
+++ b/target/surefire-reports/com.github.example.pt.ptApplicationTests.txt
@@ -0,0 +1,4 @@
+-------------------------------------------------------------------------------
+Test set: com.github.example.pt.ptApplicationTests
+-------------------------------------------------------------------------------
+Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 12.046 s - in com.github.example.pt.ptApplicationTests
diff --git a/target/test-classes/com/github/example/pt/ptApplicationTests.class b/target/test-classes/com/github/example/pt/ptApplicationTests.class
new file mode 100644
index 0000000..f4948e0
--- /dev/null
+++ b/target/test-classes/com/github/example/pt/ptApplicationTests.class
Binary files differ