mirror of
https://github.com/TheWhiteDog9487/WebAPI.git
synced 2026-08-11 23:01:30 +08:00
使用Kotlin重写
This commit is contained in:
@@ -36,6 +36,9 @@ out/
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Kotlin ###
|
||||
.kotlin
|
||||
|
||||
API密钥.txt
|
||||
SQLiteDataBase.db
|
||||
*.http
|
||||
+66
-22
@@ -1,14 +1,19 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
java
|
||||
id("org.springframework.boot") version "4.0.0"
|
||||
kotlin("jvm") version "2.3.21"
|
||||
kotlin("kapt") version "2.3.21"
|
||||
kotlin("plugin.spring") version "2.3.21"
|
||||
kotlin("plugin.jpa") version "2.3.21"
|
||||
kotlin("plugin.lombok") version "2.3.21"
|
||||
id("org.springframework.boot") version "4.1.0"
|
||||
id("io.spring.dependency-management") version "1.1.7"
|
||||
id("org.hibernate.orm") version "7.1.8.Final"
|
||||
id("org.graalvm.buildtools.native") version "0.11.3"
|
||||
id("org.springdoc.openapi-gradle-plugin") version "1.9.0"
|
||||
id("org.hibernate.orm") version "7.4.1.Final"
|
||||
id("org.graalvm.buildtools.native") version "1.1.1"
|
||||
}
|
||||
|
||||
group = "xyz.thewhitedog9487"
|
||||
version = "0.7.5"
|
||||
version = "0.8.0"
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
@@ -16,12 +21,6 @@ java {
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
compileOnly {
|
||||
extendsFrom(configurations.annotationProcessor.get())
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
@@ -29,35 +28,71 @@ repositories {
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-redis")
|
||||
implementation("org.springframework.boot:spring-boot-starter-jdbc")
|
||||
// implementation("org.springframework.boot:spring-boot-starter-data-redis")
|
||||
// implementation("org.springframework.boot:spring-boot-starter-jdbc")
|
||||
// implementation("org.springframework.boot:spring-boot-starter-restclient")
|
||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||
// implementation("org.springframework.boot:spring-boot-starter-security-oauth2-authorization-server")
|
||||
// implementation("org.springframework.boot:spring-boot-starter-security-oauth2-client")
|
||||
// implementation("org.springframework.boot:spring-boot-starter-security-oauth2-resource-server")
|
||||
implementation("org.springframework.boot:spring-boot-starter-webmvc")
|
||||
// implementation("org.springframework.boot:spring-boot-starter-websocket")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
implementation("tools.jackson.module:jackson-module-kotlin")
|
||||
compileOnly("org.projectlombok:lombok")
|
||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||
developmentOnly("org.springframework.boot:spring-boot-docker-compose")
|
||||
// developmentOnly("org.springframework.boot:spring-boot-docker-compose")
|
||||
// runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
|
||||
// runtimeOnly("org.postgresql:postgresql")
|
||||
runtimeOnly("org.xerial:sqlite-jdbc")
|
||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
||||
annotationProcessor("org.projectlombok:lombok")
|
||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
||||
kapt("org.springframework.boot:spring-boot-configuration-processor")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-actuator-test")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-data-jpa-test")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-data-redis-test")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-jdbc-test")
|
||||
// testImplementation("org.springframework.boot:spring-boot-starter-data-redis-test")
|
||||
// testImplementation("org.springframework.boot:spring-boot-starter-jdbc-test")
|
||||
// testImplementation("org.springframework.boot:spring-boot-starter-restclient-test")
|
||||
// testImplementation("org.springframework.boot:spring-boot-starter-security-oauth2-authorization-server-test")
|
||||
// testImplementation("org.springframework.boot:spring-boot-starter-security-oauth2-client-test")
|
||||
// testImplementation("org.springframework.boot:spring-boot-starter-security-oauth2-resource-server-test")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-security-test")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test")
|
||||
// testImplementation("org.springframework.boot:spring-boot-starter-websocket-test")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
|
||||
testCompileOnly("org.projectlombok:lombok")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
testAnnotationProcessor("org.projectlombok:lombok")
|
||||
testImplementation(kotlin("test"))
|
||||
|
||||
implementation("io.github.oshai:kotlin-logging-jvm:7.0.3")
|
||||
implementation("org.hibernate.orm:hibernate-community-dialects")
|
||||
implementation("com.discord4j:discord4j-core:3.3.0")
|
||||
implementation("tools.jackson.core:jackson-core:3.0.3")
|
||||
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.0")
|
||||
implementation("org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.0")
|
||||
implementation("dev.kord:kord-core:0.18.1")
|
||||
implementation("tools.jackson.core:jackson-core")
|
||||
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.3")
|
||||
implementation("org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.3")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
|
||||
implementation("io.micrometer:context-propagation")
|
||||
implementation("org.apache.commons:commons-lang3")
|
||||
}
|
||||
|
||||
hibernate {
|
||||
enhancement {
|
||||
}
|
||||
}
|
||||
|
||||
allOpen {
|
||||
annotation("jakarta.persistence.Entity")
|
||||
annotation("jakarta.persistence.MappedSuperclass")
|
||||
annotation("jakarta.persistence.Embeddable")
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
|
||||
tasks.bootJar {
|
||||
archiveClassifier.set("boot")
|
||||
}
|
||||
@@ -66,3 +101,12 @@ tasks.bootJar {
|
||||
// enabled = false
|
||||
//}
|
||||
// https://docs.spring.io/spring-boot/gradle-plugin/packaging.html#packaging-executable.and-plain-archives
|
||||
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-Xannotation-default-target=param-property")
|
||||
}
|
||||
}
|
||||
kapt {
|
||||
keepJavacAnnotationProcessors = true
|
||||
}
|
||||
@@ -1,4 +1,21 @@
|
||||
services:
|
||||
# mariadb:
|
||||
# image: 'mariadb:latest'
|
||||
# environment:
|
||||
# - 'MARIADB_DATABASE=mydatabase'
|
||||
# - 'MARIADB_PASSWORD=secret'
|
||||
# - 'MARIADB_ROOT_PASSWORD=verysecret'
|
||||
# - 'MARIADB_USER=myuser'
|
||||
# ports:
|
||||
# - '3306'
|
||||
# postgres:
|
||||
# image: 'postgres:latest'
|
||||
# environment:
|
||||
# - 'POSTGRES_DB=mydatabase'
|
||||
# - 'POSTGRES_PASSWORD=secret'
|
||||
# - 'POSTGRES_USER=myuser'
|
||||
# ports:
|
||||
# - '5432'
|
||||
redis:
|
||||
image: 'redis:latest'
|
||||
ports:
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+3
-1
@@ -1,7 +1,9 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-all.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -57,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -114,7 +114,6 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -172,7 +171,6 @@ fi
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
@@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
|
||||
Vendored
+10
-22
@@ -23,8 +23,8 @@
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||
setlocal EnableExtensions
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@@ -51,7 +51,7 @@ echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
@@ -65,30 +65,18 @@ echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@rem which allows us to clear the local environment before executing the java command
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
:exitWithErrorLevel
|
||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Configuration;
|
||||
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
import org.springframework.aot.hint.TypeReference;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.ImportRuntimeHints;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
|
||||
import org.springframework.core.type.classreading.MetadataReader;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ImportRuntimeHints(RegisterGraalVMBuildHints.class)
|
||||
class GraalVMBuildHints {}
|
||||
|
||||
/**
|
||||
* GPT写的,测试了一下能用,先作为实验性内容加入
|
||||
* @author ChatGPT GPT5
|
||||
*/
|
||||
class RegisterGraalVMBuildHints implements RuntimeHintsRegistrar{
|
||||
|
||||
@Override
|
||||
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
|
||||
String basePackage = "discord4j";
|
||||
String pattern = "classpath*:" + ClassUtils.convertClassNameToResourcePath(basePackage) + "/**/*.class";
|
||||
|
||||
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(classLoader);
|
||||
CachingMetadataReaderFactory mrf = new CachingMetadataReaderFactory(classLoader);
|
||||
|
||||
int ok = 0, skip = 0;
|
||||
try {
|
||||
Resource[] resources = resolver.getResources(pattern);
|
||||
for (Resource resource : resources) {
|
||||
if (!resource.isReadable()) {
|
||||
continue;
|
||||
}
|
||||
MetadataReader mr = mrf.getMetadataReader(resource);
|
||||
String className = mr.getClassMetadata().getClassName();
|
||||
try {
|
||||
// 只处理目标包(防御性判断,避免扫到别的)
|
||||
if (!className.startsWith(basePackage)) {
|
||||
continue;
|
||||
}
|
||||
Class<?> clazz = ClassUtils.forName(className, classLoader);
|
||||
// 暴力但稳:把构造器/方法/字段都暴露给反射(含内部类、Builder、$Json)
|
||||
hints.reflection().registerType(clazz,b -> b.withMembers(
|
||||
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
|
||||
MemberCategory.INVOKE_DECLARED_METHODS,
|
||||
MemberCategory.ACCESS_DECLARED_FIELDS));
|
||||
ok++;
|
||||
} catch (Throwable e) {
|
||||
// 有些类可能因缺失依赖/被 JDK 模块限制而加载失败,跳过即可
|
||||
skip++;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Scan & register discord4j classes failed", e);
|
||||
}
|
||||
|
||||
hints.reflection().registerType(
|
||||
TypeReference.of("com.github.benmanes.caffeine.cache.SSLMS"),
|
||||
builder -> builder.withMembers(
|
||||
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
|
||||
MemberCategory.INVOKE_DECLARED_METHODS,
|
||||
MemberCategory.ACCESS_DECLARED_FIELDS
|
||||
)
|
||||
);
|
||||
|
||||
hints.reflection().registerType(
|
||||
TypeReference.of("com.github.benmanes.caffeine.cache.SSMS"),
|
||||
builder -> builder.withMembers(
|
||||
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
|
||||
MemberCategory.INVOKE_DECLARED_METHODS,
|
||||
MemberCategory.ACCESS_DECLARED_FIELDS
|
||||
)
|
||||
);
|
||||
|
||||
hints.reflection().registerType(
|
||||
TypeReference.of("com.github.benmanes.caffeine.cache.PSLMS"),
|
||||
builder -> builder.withMembers(
|
||||
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
|
||||
MemberCategory.INVOKE_DECLARED_METHODS,
|
||||
MemberCategory.ACCESS_DECLARED_FIELDS
|
||||
)
|
||||
);
|
||||
|
||||
hints.reflection().registerType(
|
||||
TypeReference.of("com.github.benmanes.caffeine.cache.PSMS"),
|
||||
builder -> builder.withMembers(
|
||||
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
|
||||
MemberCategory.INVOKE_DECLARED_METHODS,
|
||||
MemberCategory.ACCESS_DECLARED_FIELDS
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
// 用 System.out 打印(AOT 阶段 SLF4J 常常是 NOP)
|
||||
System.out.println("[Discord4jJsonHints] registered classes: " + ok + ", skipped: " + skip);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Configuration;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
class OpenAPIConfiguration {
|
||||
@Bean
|
||||
OpenAPI CustomOpenAPIConfiguration() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("TheWhiteDog9487的通用API们")
|
||||
.summary("")
|
||||
.description("")
|
||||
.termsOfService("")
|
||||
.contact(new Contact()
|
||||
.name("TheWhiteDog9487")
|
||||
.url("https://www.github.com/TheWhiteDog9487/WebAPI"))
|
||||
.version("0.7.5")
|
||||
.license(new License()
|
||||
.name("WTFPL")
|
||||
.url("https://spdx.org/licenses/WTFPL")) )
|
||||
.servers(List.of(
|
||||
new Server()
|
||||
.url("https://api.thewhitedog9487.xyz")
|
||||
.description("主服务器"),
|
||||
new Server()
|
||||
.url("https://dev.thewhitedog9487.xyz")
|
||||
.description("本地开发服务器(经过Cloudflare)") ) );} }
|
||||
-107
@@ -1,107 +0,0 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Configuration.Security;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import xyz.thewhitedog9487.WebAPI.Controller.ResponseData;
|
||||
import xyz.thewhitedog9487.WebAPI.Data.Entity.AccessLog;
|
||||
import xyz.thewhitedog9487.WebAPI.Data.Repository.AccessLogRepository;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class ApiKeyAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
List<String> ApiKeyList;
|
||||
AccessLogRepository AccessLogRepository;
|
||||
|
||||
@Override
|
||||
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||
String ApiKey = request.getHeader("X-API-Key");
|
||||
if (ApiKey == null) {
|
||||
var ResponseBody = new ResponseData(
|
||||
HttpStatus.UNAUTHORIZED.value(),
|
||||
"API密钥验证失败,未传递X-API-Key请求头");
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write(ResponseBody.ToJson());
|
||||
var Log = new AccessLog(
|
||||
null,
|
||||
request.getRequestId(),
|
||||
request.getHeader("CF-Connecting-IP".toLowerCase() ),
|
||||
request.getHeader("X-Forwarded-For".toLowerCase() ),
|
||||
request.getRemoteAddr(),
|
||||
request.getHeader("CF-IPCountry".toLowerCase() ),
|
||||
( request.getHeader("CF-IPCountry".toLowerCase() ) == null ) ? null : Locale.of(Locale.PRC.getLanguage(), request.getHeader("CF-IPCountry".toLowerCase() ), Locale.SIMPLIFIED_CHINESE.getVariant()).getISO3Country(),
|
||||
request.getHeader("User-Agent"),
|
||||
request.getMethod(),
|
||||
request.getScheme(),
|
||||
request.getProtocol(),
|
||||
request.getRequestURL().toString(),
|
||||
request.getQueryString(),
|
||||
Collections.list( request.getHeaderNames() )
|
||||
.stream()
|
||||
.map( name -> name + ": " + Collections.list(request.getHeaders(name)) )
|
||||
.reduce( ( a, b ) -> a + "\n" + b )
|
||||
.orElse(""),
|
||||
Instant.now(),
|
||||
HttpStatus.UNAUTHORIZED.value(),
|
||||
ResponseBody.ToJson());
|
||||
AccessLogRepository.save(Log);
|
||||
return; }
|
||||
if (ApiKeyList.contains(ApiKey) == true) {
|
||||
var Auth = new UsernamePasswordAuthenticationToken(
|
||||
"",
|
||||
ApiKey,
|
||||
List.of(new SimpleGrantedAuthority("API")));
|
||||
SecurityContextHolder.getContext().setAuthentication(Auth);
|
||||
filterChain.doFilter(request, response);
|
||||
} else {
|
||||
var ResponseBody = new ResponseData(
|
||||
HttpStatus.UNAUTHORIZED.value(),
|
||||
"API密钥验证失败",
|
||||
Map.of("提供的密钥:", ApiKey));
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write(ResponseBody.ToJson());
|
||||
var Log = new AccessLog(
|
||||
null,
|
||||
request.getRequestId(),
|
||||
request.getHeader("CF-Connecting-IP".toLowerCase() ),
|
||||
request.getHeader("X-Forwarded-For".toLowerCase() ),
|
||||
request.getRemoteAddr(),
|
||||
request.getHeader("CF-IPCountry".toLowerCase() ),
|
||||
( request.getHeader("CF-IPCountry".toLowerCase() ) == null ) ? null : Locale.of(Locale.PRC.getLanguage(), request.getHeader("CF-IPCountry".toLowerCase() ), Locale.SIMPLIFIED_CHINESE.getVariant()).getISO3Country(),
|
||||
request.getHeader("User-Agent"),
|
||||
request.getMethod(),
|
||||
request.getScheme(),
|
||||
request.getProtocol(),
|
||||
request.getRequestURL().toString(),
|
||||
request.getQueryString(),
|
||||
Collections.list( request.getHeaderNames() )
|
||||
.stream()
|
||||
.map( name -> name + ": " + Collections.list(request.getHeaders(name)) )
|
||||
.reduce( ( a, b ) -> a + "\n" + b )
|
||||
.orElse(""),
|
||||
Instant.now(),
|
||||
HttpStatus.UNAUTHORIZED.value(),
|
||||
ResponseBody.ToJson() );
|
||||
AccessLogRepository.save(Log);
|
||||
return; } } }
|
||||
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Configuration;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import xyz.thewhitedog9487.WebAPI.Configuration.Security.ApiKeyAuthenticationFilter;
|
||||
import xyz.thewhitedog9487.WebAPI.Data.Repository.AccessLogRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
class SpringSecurityConfiguration {
|
||||
|
||||
@Autowired List<String> ApiKeyList;
|
||||
@Autowired AccessLogRepository AccessLogRepository;
|
||||
|
||||
/**
|
||||
* @see ApiKeyAuthenticationFilter#doFilterInternal(HttpServletRequest, HttpServletResponse, FilterChain)
|
||||
*/
|
||||
@Order(1)
|
||||
@Bean
|
||||
SecurityFilterChain RequireAPIKey(HttpSecurity Security) throws Exception {
|
||||
Security
|
||||
.securityMatcher("/message/**", "/accesslog/**")
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.authorizeHttpRequests(AuthorizationManagerRequestMatcherRegistry -> {
|
||||
AuthorizationManagerRequestMatcherRegistry
|
||||
.anyRequest()
|
||||
.authenticated(); })
|
||||
.addFilterBefore(new ApiKeyAuthenticationFilter(ApiKeyList, AccessLogRepository), UsernamePasswordAuthenticationFilter.class);
|
||||
return Security.build(); }
|
||||
|
||||
@Order(2)
|
||||
@Bean
|
||||
SecurityFilterChain PermitAll(HttpSecurity Security) throws Exception {
|
||||
Security
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.authorizeHttpRequests(AuthorizationManagerRequestMatcherRegistry -> {
|
||||
AuthorizationManagerRequestMatcherRegistry
|
||||
.anyRequest()
|
||||
.permitAll(); });
|
||||
return Security.build(); }
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.ExampleObject;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import xyz.thewhitedog9487.WebAPI.Data.Repository.AccessLogRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "日志相关")
|
||||
@RestController
|
||||
@RequestMapping("/accesslog")
|
||||
class AccessLog {
|
||||
@Autowired AccessLogRepository AccessLogRepository;
|
||||
|
||||
@Operation(summary = "获取访问日志数据库内所有的记录",
|
||||
description = "这是一个私有API,需要在请求头中提供X-API-Key以进行身份验证")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200",
|
||||
description = "成功获取到数据",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
array = @ArraySchema(
|
||||
minItems = 0,
|
||||
schema = @Schema(
|
||||
implementation = xyz.thewhitedog9487.WebAPI.Data.Entity.AccessLog.class ) ),
|
||||
examples = @ExampleObject(value = """
|
||||
[
|
||||
{
|
||||
"ID": 18,
|
||||
"RequestID": "3",
|
||||
"CF_Connecting_IP": "223.87.14.146",
|
||||
"X_Forwarded_For": "223.87.14.146, 141.101.99.156",
|
||||
"RemoteAddress": "192.168.128.11",
|
||||
"CF_IPCountry": "CN",
|
||||
"ISO3166": "CHN",
|
||||
"UserAgent": "Mozilla/5.0 (Linux; Android 10; SM-A202F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile Safari/537.36",
|
||||
"HttpMethod": "GET",
|
||||
"Protocol": "http",
|
||||
"ProtocolVersion": "HTTP/1.0",
|
||||
"URL": "http://dev.thewhitedog9487.xyz/",
|
||||
"QueryString": null,
|
||||
"Header": "host: [dev.thewhitedog9487.xyz]\\nx-real-ip: [141.101.99.156]\\nx-forwarded-for: [223.87.14.146, 141.101.99.156]\\nx-forwarded-proto: [https]\\nconnection: [close]\\ncf-ray: [986883191fd5ed0b-LHR]\\nuser-agent: [Mozilla/5.0 (Linux; Android 10; SM-A202F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile Safari/537.36]\\naccept-encoding: [gzip, br]\\nreferer: [https://www.baidu.com]\\ncdn-loop: [cloudflare; loops=1]\\ncf-connecting-ip: [223.87.14.146]\\ncf-ipcountry: [CN]\\ncf-visitor: [{\\"scheme\\":\\"https\\"}]",
|
||||
"Timestamp": "2025-09-29T03:49:42.766Z",
|
||||
"ResponseStatusCode": 302,
|
||||
"ResponseBody": ""
|
||||
},
|
||||
{
|
||||
"ID": 261,
|
||||
"RequestID": "31",
|
||||
"CF_Connecting_IP": "2409:8962:77a:49c:a579:f5af:58a3:e2c",
|
||||
"X_Forwarded_For": "2409:8962:77a:49c:a579:f5af:58a3:e2c, 172.71.150.157",
|
||||
"RemoteAddress": "192.168.128.11",
|
||||
"CF_IPCountry": "CN",
|
||||
"ISO3166": "CHN",
|
||||
"UserAgent": "IntelliJ HTTP Client/IntelliJ IDEA 2025.2.3",
|
||||
"HttpMethod": "GET",
|
||||
"Protocol": "http",
|
||||
"ProtocolVersion": "HTTP/1.0",
|
||||
"URL": "http://dev.thewhitedog9487.xyz/accesslog",
|
||||
"QueryString": null,
|
||||
"Header": "host: dev.thewhitedog9487.xyz\\nx-real-ip: 172.71.150.157\\nx-forwarded-for: 2409:8962:77a:49c:a579:f5af:58a3:e2c, 172.71.150.157\\nx-forwarded-proto: https\\nconnection: close\\nx-api-key: asdasdaFSEA452453sdcv\\nuser-agent: IntelliJ HTTP Client/IntelliJ IDEA 2025.2.3\\naccept: */*\\ncf-ray: 98cc7d6aaea1c37a-SEA\\naccept-encoding: gzip, br\\ncdn-loop: cloudflare; loops=1\\ncf-connecting-ip: 2409:8962:77a:49c:a579:f5af:58a3:e2c\\ncf-ipcountry: CN\\ncf-visitor: {\\"scheme\\":\\"https\\"}\\ncookie: JSESSIONID=A49527B913170D86A5544A252BD31040",
|
||||
"Timestamp": "2025-10-11T07:02:05.718Z",
|
||||
"ResponseStatusCode": 200,
|
||||
"ResponseBody": null
|
||||
},
|
||||
{
|
||||
"ID": 263,
|
||||
"RequestID": "35",
|
||||
"CF_Connecting_IP": "2409:8962:77a:49c:a579:f5af:58a3:e2c",
|
||||
"X_Forwarded_For": "2409:8962:77a:49c:a579:f5af:58a3:e2c, 108.162.246.209",
|
||||
"RemoteAddress": "192.168.128.11",
|
||||
"CF_IPCountry": "CN",
|
||||
"ISO3166": "CHN",
|
||||
"UserAgent": "IntelliJ HTTP Client/IntelliJ IDEA 2025.2.3",
|
||||
"HttpMethod": "GET",
|
||||
"Protocol": "http",
|
||||
"ProtocolVersion": "HTTP/1.0",
|
||||
"URL": "http://dev.thewhitedog9487.xyz/ip/ip",
|
||||
"QueryString": null,
|
||||
"Header": "host: dev.thewhitedog9487.xyz\\nx-real-ip: 108.162.246.209\\nx-forwarded-for: 2409:8962:77a:49c:a579:f5af:58a3:e2c, 108.162.246.209\\nx-forwarded-proto: https\\nconnection: close\\naccept-encoding: gzip, br\\nuser-agent: IntelliJ HTTP Client/IntelliJ IDEA 2025.2.3\\naccept: */*\\ncf-ray: 98cc9ec12c48a1a5-SEA\\ncdn-loop: cloudflare; loops=1\\ncf-connecting-ip: 2409:8962:77a:49c:a579:f5af:58a3:e2c\\ncf-ipcountry: CN\\ncf-visitor: {\\"scheme\\":\\"https\\"}\\ncookie: JSESSIONID=A49527B913170D86A5544A252BD31040",
|
||||
"Timestamp": "2025-10-11T07:24:51.504Z",
|
||||
"ResponseStatusCode": 200,
|
||||
"ResponseBody": "2409:8962:77a:49c:a579:f5af:58a3:e2c"
|
||||
}
|
||||
]
|
||||
""") ) ),
|
||||
@ApiResponse(responseCode = "401",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = @Schema(implementation = ResponseData.class),
|
||||
examples = {
|
||||
@ExampleObject(value = "{\n" +
|
||||
" \"code\": 401,\n" +
|
||||
" \"message\": \"请求缺少必要的头部信息\",\n" +
|
||||
" \"data\": {\n" +
|
||||
" \"缺失的头部\": \"X-API-Key\"\n" +
|
||||
" }\n" +
|
||||
"}", name = "缺少头部信息"),
|
||||
@ExampleObject(value = "{\n" +
|
||||
" \"code\": 401,\n" +
|
||||
" \"message\": \"API密钥验证失败\",\n" +
|
||||
" \"data\": {\n" +
|
||||
" \"提供的密钥\": \"123456\"\n" +
|
||||
" }\n" +
|
||||
"}", name = "密钥不正确") } ),
|
||||
description = "API密钥验证失败") } )
|
||||
@GetMapping
|
||||
List<xyz.thewhitedog9487.WebAPI.Data.Entity.AccessLog> GetFullLog(
|
||||
@Parameter(description = "用于身份验证的API密钥", in = ParameterIn.HEADER, required = true, example = "ds1858dscc8745sfwe")
|
||||
@RequestHeader("X-API-Key") String ApiKey){
|
||||
return AccessLogRepository.findAll(); }
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Controller.Filter;
|
||||
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
||||
import xyz.thewhitedog9487.WebAPI.Data.Entity.AccessLog;
|
||||
import xyz.thewhitedog9487.WebAPI.Data.Repository.AccessLogRepository;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class LogClientInfo implements Filter {
|
||||
public static List<String> IgnorePaths = List.of(
|
||||
"/favicon.ico",
|
||||
"/swagger-ui",
|
||||
"/v3/api-docs",
|
||||
"/accesslog");
|
||||
|
||||
@Autowired AccessLogRepository AccessLogRepository;
|
||||
@Autowired Lock SQLiteWriteLock;
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
HttpServletRequest HttpServletRequest = (HttpServletRequest) request;
|
||||
HttpServletResponse HttpServletResponse = (HttpServletResponse) response;
|
||||
var Response = new ContentCachingResponseWrapper(HttpServletResponse);
|
||||
var Log = new AccessLog(
|
||||
null,
|
||||
HttpServletRequest.getRequestId(),
|
||||
HttpServletRequest.getHeader("CF-Connecting-IP"),
|
||||
HttpServletRequest.getHeader("X-Forwarded-For"),
|
||||
HttpServletRequest.getRemoteAddr(),
|
||||
HttpServletRequest.getHeader("CF-IPCountry"),
|
||||
( HttpServletRequest.getHeader("CF-IPCountry") == null ) ? null : Locale.of(Locale.PRC.getLanguage(), HttpServletRequest.getHeader("CF-IPCountry"), Locale.SIMPLIFIED_CHINESE.getVariant()).getISO3Country(),
|
||||
HttpServletRequest.getHeader("User-Agent" ),
|
||||
HttpServletRequest.getMethod(),
|
||||
HttpServletRequest.getScheme(),
|
||||
HttpServletRequest.getProtocol(),
|
||||
HttpServletRequest.getRequestURL().toString(),
|
||||
HttpServletRequest.getQueryString(),
|
||||
Collections.list( HttpServletRequest.getHeaderNames() )
|
||||
.stream()
|
||||
.map( name -> name + ": " + Collections.list(HttpServletRequest.getHeaders(name)).getFirst() )
|
||||
.reduce( ( a, b ) -> a + "\n" + b )
|
||||
.orElse(""),
|
||||
Instant.now(),
|
||||
null,
|
||||
null );
|
||||
try {
|
||||
SQLiteWriteLock.lock();
|
||||
Log = AccessLogRepository.save(Log); }
|
||||
catch (Exception e) {
|
||||
log.error(e.getLocalizedMessage()); }
|
||||
finally {
|
||||
SQLiteWriteLock.unlock(); }
|
||||
chain.doFilter(request, Response);
|
||||
var ResponseBody = IgnorePaths.stream()
|
||||
.anyMatch( path -> HttpServletRequest.getRequestURI().startsWith(path) )
|
||||
? null : new String( Response.getContentAsByteArray(), Response.getCharacterEncoding() );
|
||||
Log.setResponseStatusCode(HttpServletResponse.getStatus());
|
||||
Log.setResponseBody(ResponseBody);
|
||||
try {
|
||||
SQLiteWriteLock.lock();
|
||||
AccessLogRepository.save(Log); }
|
||||
catch (Exception e) {
|
||||
log.error(e.getLocalizedMessage()); }
|
||||
finally {
|
||||
SQLiteWriteLock.unlock(); }
|
||||
Response.copyBodyToResponse(); } }
|
||||
@@ -1,23 +0,0 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Controller.Filter;
|
||||
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@Component
|
||||
class SetDefaultContentType extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||
if (response.getContentType() == null) {
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE); }
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Controller.Filter;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
// ↑ 拉高优先级,确保在日志记录之前设置MDC,不然和没设置就没有区别了
|
||||
class SetMDCRequestID extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||
try {
|
||||
MDC.put("RequestID", request.getRequestId());
|
||||
filterChain.doFilter(request, response);
|
||||
} finally {
|
||||
// ↓ 防止内存泄漏
|
||||
MDC.remove("RequestID"); } } }
|
||||
@@ -1,87 +0,0 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.ExampleObject;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name="请求客户端IP地址相关")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/ip")
|
||||
class IP {
|
||||
@Operation(summary = "获取请求客户端的IP地址", description = """
|
||||
优先级:
|
||||
1. CF-Connecting-IP
|
||||
2. X-Forwarded-For
|
||||
3. 直接使用请求的远程地址
|
||||
""")
|
||||
@ApiResponse(responseCode = "200",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN_VALUE,
|
||||
schema = @Schema(implementation = String.class),
|
||||
examples = @ExampleObject(value = "78.141.226.247")),
|
||||
description = "成功获取到IP地址,内容为纯文本格式的IP地址")
|
||||
@GetMapping({"/ip", "", "/"})
|
||||
ResponseEntity<String> GetIP(@RequestHeader("CF-COnnecting-IP") String Header_CFConnectingIP,
|
||||
@RequestHeader("X-Forwarded-For") String Header_XForwardedFor,
|
||||
HttpServletRequest Request) {
|
||||
if (Header_CFConnectingIP != null) {
|
||||
return new ResponseEntity<>(Header_CFConnectingIP,
|
||||
new HttpHeaders(MultiValueMap.fromSingleValue(Map.of("Content-Type", "text/plain;charset=UTF-8"))),
|
||||
HttpStatus.OK); }
|
||||
else if (Header_XForwardedFor != null) {
|
||||
Header_XForwardedFor = Header_XForwardedFor.split(",")[0].trim();
|
||||
return new ResponseEntity<>(Header_XForwardedFor,
|
||||
new HttpHeaders(MultiValueMap.fromSingleValue(Map.of("Content-Type", "text/plain;charset=UTF-8"))),
|
||||
HttpStatus.OK); }
|
||||
else {
|
||||
return new ResponseEntity<>(Request.getRemoteAddr(),
|
||||
new HttpHeaders(MultiValueMap.fromSingleValue(Map.of("Content-Type", "text/plain;charset=UTF-8"))),
|
||||
HttpStatus.OK); } }
|
||||
|
||||
@Operation(summary = "获取请求客户端的ISO 3166-1 alpha-2国家代码", description = """
|
||||
依赖Cloudflare的CF-IPCountry头部
|
||||
<br>
|
||||
如果请求没有经过Cloudflare,则会返回"未找到CF-IPCountry头部"
|
||||
""")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN_VALUE,
|
||||
schema = @Schema(implementation = String.class),
|
||||
examples = @ExampleObject(value = "HK") ),
|
||||
description = "成功获取到ISO 3166-1 alpha-2国家代码,内容为纯文本格式的国家代码"),
|
||||
@ApiResponse(responseCode = "404",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN_VALUE,
|
||||
schema = @Schema(implementation = String.class),
|
||||
examples = { @ExampleObject(value = "未找到CF-IPCountry头部") } ),
|
||||
description = "未找到CF-IPCountry头部,内容为纯文本格式的错误信息") } )
|
||||
@GetMapping("iso3166")
|
||||
ResponseEntity<String> GetISO3166(@RequestHeader("CF-IPCountry") String Header_CFIPCountry) {
|
||||
if (Header_CFIPCountry != null) {
|
||||
return new ResponseEntity<>(Header_CFIPCountry,
|
||||
new HttpHeaders(MultiValueMap.fromSingleValue(Map.of("Content-Type", "text/plain;charset=UTF-8"))),
|
||||
HttpStatus.OK); }
|
||||
else {
|
||||
return new ResponseEntity<>("未找到CF-IPCountry头部",
|
||||
new HttpHeaders(MultiValueMap.fromSingleValue(Map.of("Content-Type", "text/plain;charset=UTF-8"))),
|
||||
HttpStatus.NOT_FOUND); } }
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Controller;
|
||||
|
||||
import discord4j.common.util.Snowflake;
|
||||
import discord4j.core.GatewayDiscordClient;
|
||||
import discord4j.rest.http.client.ClientException;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.ExampleObject;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.web.bind.MissingRequestHeaderException;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name="远程消息处理相关")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/message")
|
||||
class Message {
|
||||
@Autowired GatewayDiscordClient DiscordBotClient;
|
||||
|
||||
@Schema(description = "包含了发送消息的数据包")
|
||||
record PostMessageData(
|
||||
@Schema(description = "Discord频道ID", example = "1398192763845214239") Long ChannelID,
|
||||
@Schema(description = "要发送的消息内容", example = "成功完成备份,最新文件时间为2025 08 21") String Content) {}
|
||||
|
||||
@Operation(summary = "通过Discord Bot向指定频道发送消息", description = """
|
||||
这是一个私有API,需要在请求头中提供X-API-Key以进行身份验证
|
||||
<br>
|
||||
需要在请求体中提供频道ID和消息内容
|
||||
""")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "201",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = @Schema(implementation = ResponseData.class),
|
||||
examples = @ExampleObject(value = "{\n" +
|
||||
" \"code\": 201,\n" +
|
||||
" \"message\": \"消息发送成功\",\n" +
|
||||
" \"data\": {\n" +
|
||||
" \"新ID\": \"1407848258813825135\",\n" +
|
||||
" \"内容\": \"写点什么好呢\",\n" +
|
||||
" \"频道ID\": \"1398192763845214239\"\n" +
|
||||
" }\n" +
|
||||
"}", name = "成功") ),
|
||||
description = "消息发送成功,响应体中包含新消息的ID、频道ID和内容"),
|
||||
@ApiResponse(responseCode = "400",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = @Schema(implementation = ResponseData.class),
|
||||
examples = @ExampleObject(value = "{\n" +
|
||||
" \"code\": 400,\n" +
|
||||
" \"message\": \"请求体无法解析\",\n" +
|
||||
" \"data\": {\n" +
|
||||
" \"错误信息\": \"JSON parse error: Unexpected character ('}' (code 125)): was expecting double-quote to start field name\"\n" +
|
||||
" }\n" +
|
||||
"}", name = "请求体无法解析") ),
|
||||
description = "客户端发送的请求存在问题,请检查响应的data字段以获取更多信息"),
|
||||
@ApiResponse(responseCode = "401",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = @Schema(implementation = ResponseData.class),
|
||||
examples = {
|
||||
@ExampleObject(value = "{\n" +
|
||||
" \"code\": 401,\n" +
|
||||
" \"message\": \"请求缺少必要的头部信息\",\n" +
|
||||
" \"data\": {\n" +
|
||||
" \"缺失的头部\": \"X-API-Key\"\n" +
|
||||
" }\n" +
|
||||
"}", name = "缺少头部信息"),
|
||||
@ExampleObject(value = "{\n" +
|
||||
" \"code\": 401,\n" +
|
||||
" \"message\": \"API密钥验证失败\",\n" +
|
||||
" \"data\": {\n" +
|
||||
" \"提供的密钥\": \"123456\"\n" +
|
||||
" }\n" +
|
||||
"}", name = "密钥不正确") } ),
|
||||
description = "API密钥验证失败"),
|
||||
@ApiResponse(responseCode = "500",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = @Schema(implementation = ResponseData.class),
|
||||
examples = { @ExampleObject(value = "{\n" +
|
||||
" \"code\": 500,\n" +
|
||||
" \"message\": \"消息发送失败\",\n" +
|
||||
" \"data\": {\n" +
|
||||
" \"内容\": \"芝士异常\",\n" +
|
||||
" \"错误信息\": \"POST /channels/1398192763845214/messages returned 404 Not Found with response {code=10003, message=Unknown Channel}\",\n" +
|
||||
" \"频道ID\": \"1398192763845214\"\n" +
|
||||
" }\n" +
|
||||
"}", name = "消息发送失败"),
|
||||
@ExampleObject(value = "{\n" +
|
||||
" \"code\": 500,\n" +
|
||||
" \"message\": \"处理请求时发生未知错误\",\n" +
|
||||
" \"data\": {\n" +
|
||||
" \"错误信息\": \"\"\n" +
|
||||
" }\n" +
|
||||
"}", name = "未知错误") } ),
|
||||
description = """
|
||||
消息发送失败,可能是由于Discord服务器问题或其他内部错误,请查看响应Body以确定原因
|
||||
<br>
|
||||
另外,所有未被针对性处理的异常都会触发此响应
|
||||
""") } )
|
||||
@PostMapping("/discord")
|
||||
ResponseEntity<ResponseData> DiscordPush(
|
||||
@Parameter(description = "用于身份验证的API密钥", in = ParameterIn.HEADER, required = true, example = "ds1858dscc8745sfwe")
|
||||
@RequestHeader("X-API-Key") String ApiKey,
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
description = "包含频道ID和消息内容的JSON对象",
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = @Schema(implementation = PostMessageData.class) ) )
|
||||
@RequestBody PostMessageData RequestBody){
|
||||
|
||||
var ChannelID = Snowflake.of(RequestBody.ChannelID);
|
||||
try {
|
||||
var MessageData = DiscordBotClient
|
||||
.rest()
|
||||
.getChannelById(ChannelID)
|
||||
.createMessage(RequestBody.Content)
|
||||
.block();
|
||||
return new ResponseEntity<>(new ResponseData(
|
||||
HttpStatus.CREATED.value(),
|
||||
"消息发送成功",
|
||||
Map.of(
|
||||
"新ID", MessageData.id().asString(),
|
||||
"频道ID", ChannelID.asString(),
|
||||
"内容", RequestBody.Content)), HttpStatus.CREATED);
|
||||
} catch (ClientException e) {
|
||||
return new ResponseEntity<>(new ResponseData(
|
||||
HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
||||
"消息发送失败",
|
||||
Map.of(
|
||||
"错误信息", e.getMessage(),
|
||||
"频道ID", ChannelID.asString(),
|
||||
"内容", RequestBody.Content)), HttpStatus.INTERNAL_SERVER_ERROR);}}
|
||||
|
||||
@ExceptionHandler(MissingRequestHeaderException.class)
|
||||
ResponseEntity<ResponseData> HandleMissingHeader(MissingRequestHeaderException Exception, HttpServletRequest Request) {
|
||||
return new ResponseEntity<>(new ResponseData(
|
||||
HttpStatus.BAD_REQUEST.value(),
|
||||
"请求缺少必要的头部信息",
|
||||
Map.of("缺失的头部", Exception.getHeaderName())), HttpStatus.BAD_REQUEST); }
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
ResponseEntity<ResponseData> HandleMessageNotReadable(HttpMessageNotReadableException Exception, HttpServletRequest Request) {
|
||||
return new ResponseEntity<>(new ResponseData(
|
||||
HttpStatus.BAD_REQUEST.value(),
|
||||
"请求体无法解析",
|
||||
Map.of("错误信息", Exception.getMessage())), HttpStatus.BAD_REQUEST); }
|
||||
@ExceptionHandler(Exception.class)
|
||||
ResponseEntity<ResponseData> HandleOtherException(Exception Exception, HttpServletRequest Request) {
|
||||
return new ResponseEntity<>(new ResponseData(
|
||||
HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
||||
"处理请求时发生未知错误",
|
||||
Map.of("错误信息", Exception.getMessage() == null ? "" : Exception.getMessage()) ), HttpStatus.INTERNAL_SERVER_ERROR); }
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Controller;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.servlet.view.RedirectView;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
class RedirectRootToSwaggerWebUI {
|
||||
static String RedirectTo = "/swagger-ui/index.html";
|
||||
|
||||
@GetMapping("/")
|
||||
RedirectView Redirect(){
|
||||
log.info("正在将访问 / 的请求重定向到 {}", RedirectTo);
|
||||
return new RedirectView(RedirectTo); } }
|
||||
@@ -1,35 +0,0 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Controller;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "所有API通用的标准响应格式")
|
||||
public record ResponseData(
|
||||
@Schema(description = "内部用响应代码" ,example = "201") int code,
|
||||
@Schema(description = "人类可读说明信息", example = "消息发送成功") Object message,
|
||||
@Schema(description = "具体的响应数据", example = "{\n" +
|
||||
" \"新ID\": \"1407848258813825135\",\n" +
|
||||
" \"内容\": \"写点什么好呢\",\n" +
|
||||
" \"频道ID\": \"1398192763845214239\"\n" +
|
||||
" }") Object data) {
|
||||
static ObjectMapper JsonMapper = new ObjectMapper();
|
||||
public ResponseData(int code) {
|
||||
this(code, null, null); }
|
||||
public ResponseData(int code, Object message) {
|
||||
this(code, List.of(message), null); }
|
||||
|
||||
public String ToJson(){
|
||||
try {
|
||||
return JsonMapper.writeValueAsString(this);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e); } }
|
||||
|
||||
public static String AnyToJson(Object obj){
|
||||
try {
|
||||
return JsonMapper.writeValueAsString(obj);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e); } }
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Data.Entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@JsonAutoDetect(
|
||||
fieldVisibility = JsonAutoDetect.Visibility.ANY,
|
||||
getterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
setterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
creatorVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
@Getter
|
||||
@Setter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Entity
|
||||
@Table
|
||||
@Schema(description = "访问日志条目")
|
||||
public class AccessLog{
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(nullable = false)
|
||||
@JsonProperty("ID")
|
||||
@Schema(description = "主键", example = "1")
|
||||
Long ID;
|
||||
|
||||
@JsonProperty("RequestID")
|
||||
@Schema(description = "Servlet请求ID", example = "0")
|
||||
String RequestID;
|
||||
|
||||
@JsonProperty("CF_Connecting_IP")
|
||||
@Column(name = "\"CF-Connecting-IP\"")
|
||||
@Schema(description = "Cloudflare提供的请求IP", example = "2409:9802:74a3:6eff:3cae:5cba:25ac:788a")
|
||||
String CF_Connecting_IP;
|
||||
|
||||
@JsonProperty("X_Forwarded_For")
|
||||
@Column(name = "\"X-Forwarded-For\"")
|
||||
@Schema(description = "X-Forwarded-For请求头提供的请求IP", example = "2409:9802:74a3:6eff:3cae:5cba:25ac:788a, 108.162.245.89")
|
||||
String X_Forwarded_For;
|
||||
|
||||
@JsonProperty("RemoteAddress")
|
||||
@Schema(description = "Servlet报告的请求IP", example = "192.168.128.11")
|
||||
String RemoteAddress;
|
||||
|
||||
@JsonProperty("CF_IPCountry")
|
||||
@Column(name = "\"CF-IPCountry\"")
|
||||
@Schema(description = "Cloudflare提供的请求国家代码", example = "CN")
|
||||
String CF_IPCountry;
|
||||
|
||||
@JsonProperty("ISO3166")
|
||||
@Schema(description = "Java内部根据CF-IPCountry得到的对应ISO3166代码", example = "CHN")
|
||||
String ISO3166;
|
||||
|
||||
@JsonProperty("UserAgent")
|
||||
@Schema(description = "标准HTTP User-Agent请求头", example = "IntelliJ HTTP Client/IntelliJ IDEA 2025.2.2")
|
||||
String UserAgent;
|
||||
|
||||
@JsonProperty("HttpMethod")
|
||||
@Schema(description = "HTTP请求方法", example = "GET")
|
||||
String HttpMethod;
|
||||
|
||||
@JsonProperty("Protocol")
|
||||
@Schema(description = "请求协议", example = "http")
|
||||
String Protocol;
|
||||
|
||||
@JsonProperty("ProtocolVersion")
|
||||
@Schema(description = "协议版本", example = "HTTP/1.1")
|
||||
String ProtocolVersion;
|
||||
|
||||
@JsonProperty("URL")
|
||||
@Schema(description = "请求的完整URL", example = "http://dev.thewhitedog9487.xyz/ip/ip")
|
||||
String URL;
|
||||
|
||||
@JsonProperty("QueryString")
|
||||
@Schema(description = "查询字符串", example = "")
|
||||
String QueryString;
|
||||
|
||||
@JsonProperty("Header")
|
||||
@Column(columnDefinition = "text")
|
||||
@Schema(description = "所有HTTP请求头的JSON化字符串", example = "host: dev.thewhitedog9487.xyz\n" +
|
||||
"x-real-ip: 108.42.101.215\n" +
|
||||
"x-forwarded-for: 2409:3e5a:642:1f64:a9d4:c96c:72d1:88ef, 108.42.101.215\n" +
|
||||
"x-forwarded-proto: https\n" +
|
||||
"connection: close\n" +
|
||||
"x-api-key: 985651Fardsh6452453sdcv\n" +
|
||||
"user-agent: IntelliJ HTTP Client/IntelliJ IDEA 2025.2.3\n" +
|
||||
"accept: */*\n" +
|
||||
"cf-ray: 98b487dc1dead465-SEA\n" +
|
||||
"accept-encoding: gzip, br\n" +
|
||||
"cdn-loop: cloudflare; loops=1\n" +
|
||||
"cf-connecting-ip: 2409:3e5a:642:1f64:a9d4:c96c:72d1:88ef\n" +
|
||||
"cf-ipcountry: CN\n" +
|
||||
"cf-visitor: {\"scheme\":\"https\"}\n" +
|
||||
"cookie: JSESSIONID=59DB3634C2FB313AFFEE54D4F933F101")
|
||||
String Header;
|
||||
|
||||
@JsonProperty("Timestamp")
|
||||
@Schema(description = "请求时间的Unix时间戳", example = "1758437539600")
|
||||
Instant Timestamp;
|
||||
|
||||
@JsonProperty("ResponseStatusCode")
|
||||
@Schema(description = "响应的HTTP状态码", example = "200")
|
||||
Integer ResponseStatusCode;
|
||||
|
||||
@JsonProperty("ResponseBody")
|
||||
@Column(columnDefinition = "text")
|
||||
@Schema(description = "响应内容", example = "")
|
||||
String ResponseBody;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Data.Repository;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import xyz.thewhitedog9487.WebAPI.Data.Entity.AccessLog;
|
||||
|
||||
public interface AccessLogRepository extends JpaRepository<AccessLog, Long> {
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package xyz.thewhitedog9487.WebAPI;
|
||||
|
||||
import discord4j.core.DiscordClientBuilder;
|
||||
import discord4j.core.GatewayDiscordClient;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
class GlobalSharedBean {
|
||||
@Value("${Discord_Bot_Token:}") String DiscordBotToken;
|
||||
|
||||
@Bean
|
||||
GatewayDiscordClient GetDiscordClient(){
|
||||
if (DiscordBotToken.isEmpty()) {
|
||||
log.error("未设置环境变量Discord_Bot_Token,请检查配置。");
|
||||
System.exit(-1); }
|
||||
return DiscordClientBuilder.create(DiscordBotToken)
|
||||
.build()
|
||||
.login()
|
||||
.block();}
|
||||
|
||||
@Bean
|
||||
List<String> ApiKeyList() {
|
||||
Path FileName = Path.of("API密钥.txt");
|
||||
try {
|
||||
return Files.readAllLines(FileName, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
try {
|
||||
Files.createFile(FileName);
|
||||
var DefaultPassword = RandomStringUtils.secure().nextAlphanumeric(30);
|
||||
Files.writeString(FileName, DefaultPassword, StandardCharsets.UTF_8);
|
||||
log.info("已生成API密钥文件,默认密钥为:{}", DefaultPassword);
|
||||
return List.of(DefaultPassword);
|
||||
} catch (IOException e1) {
|
||||
log.error("API密钥文件不存在且无法创建API密钥文件。", e1);
|
||||
System.exit(-1); }
|
||||
return null; } }
|
||||
|
||||
@Bean
|
||||
Lock SQLiteWriteLock(){
|
||||
return new ReentrantLock(); }
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Startup;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.system.ApplicationHome;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
@Slf4j
|
||||
public class SystemdInstallerManager{
|
||||
static String TemplateContent = """
|
||||
[Unit]
|
||||
Description=WebAPI
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=%s
|
||||
WorkingDirectory=%s
|
||||
ExecStart=%s
|
||||
Restart=always
|
||||
Type=simple
|
||||
Environment="%s"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
""";
|
||||
|
||||
static ApplicationHome AppHome = new ApplicationHome( SystemdInstallerManager.class );
|
||||
static String CurrentOS = System.getProperty("os.name");
|
||||
static String CurrentUser = System.getProperty("user.name");
|
||||
static Path CurrentExecutableFilePath = GetExecutblePath();
|
||||
static Path WorkingDirectory = AppHome
|
||||
.getDir()
|
||||
.toPath();
|
||||
static Path ServiceFileDirectory = Path.of("/etc/systemd/system/");
|
||||
static Path ServiceFileName = Path.of("WebAPI.service");
|
||||
static String SoftLinkFileName = "Current";
|
||||
static Path SymbolicLinkPath = WorkingDirectory.resolve(SoftLinkFileName);
|
||||
static String DiscordBotToken = System.getenv("Discord_Bot_Token");
|
||||
/**
|
||||
* 检测当前运行环境是否为GraalVM生成的Native Image
|
||||
*/
|
||||
static boolean IsImageCode(){
|
||||
try {
|
||||
return (boolean) Class.forName("org.graalvm.nativeimage.ImageInfo")
|
||||
.getMethod("inImageCode")
|
||||
.invoke(null);
|
||||
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException e) {
|
||||
return false; } }
|
||||
|
||||
/**
|
||||
* 获取当前可执行文件的路径。
|
||||
* <br>
|
||||
* 如果是通过GraalVM生成的Native Image,则返回可执行文件的路径
|
||||
* <br>
|
||||
* 如果是直接用JRE运行的JAR包,则返回JAR文件的路径
|
||||
*/
|
||||
static Path GetExecutblePath(){
|
||||
try {
|
||||
var IsImageCode = IsImageCode();
|
||||
log.debug("IsImageCode: {}", IsImageCode);
|
||||
if( IsImageCode == true ){
|
||||
Object ExecutablePath = Class.forName("org.graalvm.nativeimage.ProcessProperties")
|
||||
.getMethod("getExecutableName")
|
||||
.invoke(null);
|
||||
log.debug("ExecutablePath: {}", ExecutablePath);
|
||||
return Path.of((String) ExecutablePath); }
|
||||
else{
|
||||
// ↓ 没有错误,但是获取Jar包位置的代码在catch里面,扔一个异常出去以进入catch分支
|
||||
// ↓ 这个主要是针对使用GraalVM的JVM而不是Native Image的情况
|
||||
throw new RuntimeException("Not Image Code"); }
|
||||
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException |
|
||||
RuntimeException e) {
|
||||
try {
|
||||
return AppHome
|
||||
.getSource()
|
||||
.toPath()
|
||||
.toRealPath();
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException(ex); } } }
|
||||
|
||||
@SneakyThrows
|
||||
public static void ProcessArguments(String[] CommandLineArguments) {
|
||||
log.debug("CommandLineArguments: {}", Arrays.toString(CommandLineArguments));
|
||||
for (String Argument : CommandLineArguments) {
|
||||
if ( List.of("install", "--install").contains(Argument) == true ) {
|
||||
/*
|
||||
在Native Image / Jar文件旁边生成指向自身的,名为Current的软连接
|
||||
然后在软连接旁生成systemd服务文件,执行目标指向软连接
|
||||
*/
|
||||
try {
|
||||
DiscordBotToken = Arrays.stream(CommandLineArguments)
|
||||
.filter(s -> s.startsWith("--Discord_Bot_Token=") == true)
|
||||
.toList()
|
||||
.getFirst()
|
||||
.substring("--"
|
||||
.length()); }
|
||||
catch (NoSuchElementException _){ }
|
||||
if ( DiscordBotToken == null ) {
|
||||
log.error("必须通过\"--Discord_Bot_Token=\"参数或Discord_Bot_Token环境变量提供Discord机器人的令牌以使本程序正常工作。");
|
||||
throw new IllegalArgumentException("缺少必须的--Discord_Bot_Token参数"); }
|
||||
log.info("开始安装systemd服务");
|
||||
log.debug("SystemProperties.os.name: {}", CurrentOS);
|
||||
if ( CurrentOS.startsWith("Linux") ) {
|
||||
log.debug("SystemProperties.user.name: {}", CurrentUser);
|
||||
if (CurrentUser.equals("root") == false) {
|
||||
log.error("只有root才有权在系统层级安装systemd服务,但是JVM报告的当前用户是{}", CurrentUser);
|
||||
throw new IllegalStateException("请使用root用户运行此命令以安装为systemd服务"); }
|
||||
log.debug("CurrentExecutableFilePath: {}", CurrentExecutableFilePath);
|
||||
log.debug("WorkingDirectory: {}", WorkingDirectory);
|
||||
log.debug("SymbolicLinkPath: {}", SymbolicLinkPath);
|
||||
new ProcessBuilder("systemctl", "stop", ServiceFileName.toString()).start().waitFor();
|
||||
log.info("已停止systemd服务: {}", ServiceFileName);
|
||||
Files.deleteIfExists(SymbolicLinkPath);
|
||||
Files.createSymbolicLink(SymbolicLinkPath, CurrentExecutableFilePath);
|
||||
log.info("成功创建指向当前程序的软链接: {} -> {}", SymbolicLinkPath, CurrentExecutableFilePath);
|
||||
Path CurrentJavaPath = null;
|
||||
var IsImageCode = IsImageCode();
|
||||
if ( IsImageCode == false ) {
|
||||
CurrentJavaPath = Path.of("/proc/self/exe").toRealPath();
|
||||
log.debug("CurrentJavaPath: {}", CurrentJavaPath); }
|
||||
var ExecCommand = (IsImageCode == true) ?
|
||||
SymbolicLinkPath.toString() :
|
||||
CurrentJavaPath + " -jar " + SymbolicLinkPath.toString();
|
||||
log.debug("ExecCommand: {}", ExecCommand);
|
||||
var ServiceFileContent = String.format(
|
||||
TemplateContent,
|
||||
CurrentUser,
|
||||
WorkingDirectory,
|
||||
ExecCommand,
|
||||
"Discord_Bot_Token=" + DiscordBotToken);
|
||||
log.debug("ServiceFileContent: \n{}", ServiceFileContent);
|
||||
Files.deleteIfExists( WorkingDirectory.resolve(ServiceFileName) );
|
||||
Files.writeString(WorkingDirectory.resolve(ServiceFileName), ServiceFileContent);
|
||||
log.info("成功创建systemd服务文件: {}", WorkingDirectory.resolve(ServiceFileName));
|
||||
Files.deleteIfExists( ServiceFileDirectory.resolve(ServiceFileName) );
|
||||
Files.createSymbolicLink(ServiceFileDirectory.resolve(ServiceFileName), WorkingDirectory.resolve(ServiceFileName));
|
||||
log.info("成功创建指向服务文件的软链接: {} -> {}", ServiceFileDirectory.resolve(ServiceFileName), WorkingDirectory.resolve(ServiceFileName));
|
||||
new ProcessBuilder("systemctl", "daemon-reload").start().waitFor();
|
||||
log.info("已完成systemd守护进程重载");
|
||||
new ProcessBuilder("systemctl", "enable", ServiceFileName.toString()).start().waitFor();
|
||||
log.info("已启用systemd服务: {}", ServiceFileName);
|
||||
new ProcessBuilder("systemctl", "start", ServiceFileName.toString()).start().waitFor();
|
||||
log.info("已启动systemd服务: {}", ServiceFileName); }
|
||||
else if ( CurrentOS.startsWith("Windows") ) {
|
||||
log.error("只有Linux才能使用systemd,但是JVM报告的当前系统是{}", CurrentOS);
|
||||
throw new IllegalStateException("Windows系统不支持systemd服务管理,暂不支持在Windows上自动管理服务。"); }
|
||||
System.exit(0); }
|
||||
else if ( List.of("uninstall", "--uninstall").contains(Argument) == true ) {
|
||||
log.info("开始卸载systemd服务");
|
||||
log.debug("SystemProperties.os.name: {}", CurrentOS);
|
||||
if ( CurrentOS.startsWith("Windows") ) {
|
||||
log.error("只有Linux才能使用systemd,但是JVM报告的当前系统是{}", CurrentOS);
|
||||
throw new IllegalStateException("Windows系统不支持systemd服务管理,暂不支持在Windows上自动管理服务。"); }
|
||||
else if ( CurrentOS.startsWith("Linux") ) {
|
||||
if (CurrentUser.equals("root") == false) {
|
||||
log.error("只有root才有权在系统层级卸载systemd服务,但是JVM报告的当前用户是{}", CurrentUser);
|
||||
throw new IllegalStateException("请使用root用户运行此命令以卸载systemd服务"); }
|
||||
new ProcessBuilder("systemctl", "stop", ServiceFileName.toString()).start().waitFor();
|
||||
log.info("已停止systemd服务: {}", ServiceFileName);
|
||||
new ProcessBuilder("systemctl", "disable", ServiceFileName.toString()).start().waitFor();
|
||||
log.info("已禁用systemd服务: {}", ServiceFileName);
|
||||
Files.deleteIfExists(ServiceFileDirectory.resolve(ServiceFileName));
|
||||
log.info("成功删除systemd服务文件: {}", ServiceFileDirectory.resolve(ServiceFileName));
|
||||
Files.deleteIfExists(WorkingDirectory.resolve(ServiceFileName));
|
||||
log.info("成功删除程序旁的服务文件: {}", WorkingDirectory.resolve(ServiceFileName));
|
||||
Files.deleteIfExists(SymbolicLinkPath);
|
||||
log.info("成功删除指向程序自身的软链接: {}", SymbolicLinkPath);
|
||||
new ProcessBuilder("systemctl", "daemon-reload").start().waitFor();
|
||||
log.info("已完成systemd守护进程重载"); }
|
||||
System.exit(0); } } }
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package xyz.thewhitedog9487.WebAPI;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import xyz.thewhitedog9487.WebAPI.Startup.SystemdInstallerManager;
|
||||
|
||||
@SpringBootApplication
|
||||
public class WebApiApplication {
|
||||
public static void main(String[] args) {
|
||||
//TODO: 从配置文件或环境变量读取并设置HTTP端口号
|
||||
var SpringApp = new SpringApplication(WebApiApplication.class);
|
||||
SystemdInstallerManager.ProcessArguments(args);
|
||||
SpringApp.run(args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package xyz.thewhitedog9487.WebAPI.BackgroundTask
|
||||
|
||||
import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.apache.commons.lang3.RandomStringUtils
|
||||
import org.springframework.stereotype.Component
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardWatchEventKinds
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
import kotlin.io.path.absolute
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
@Component
|
||||
class ApiKeyManager {
|
||||
private val Logger: KLogger = KotlinLogging.logger{}
|
||||
internal val ApiKeyFile = Path.of("API密钥.txt").absolute()
|
||||
|
||||
internal val ApiKeyListBackendField: CopyOnWriteArraySet<String> = CopyOnWriteArraySet()
|
||||
val ApiKeyList: Set<String> get() = ApiKeyListBackendField
|
||||
|
||||
init {
|
||||
try {
|
||||
if (Files.exists(ApiKeyFile)) {
|
||||
ApiKeyListBackendField.clear()
|
||||
ApiKeyListBackendField.addAll(Files.readAllLines(ApiKeyFile, StandardCharsets.UTF_8))
|
||||
Logger.info{"API密钥列表已完成加载,当前数量:${ApiKeyListBackendField.size}"}
|
||||
} else {
|
||||
try {
|
||||
Files.createFile(ApiKeyFile)
|
||||
val DefaultPassword = RandomStringUtils.secure().nextAlphanumeric(32)
|
||||
Files.writeString(ApiKeyFile, DefaultPassword, StandardCharsets.UTF_8)
|
||||
Logger.info{"已生成API密钥文件,默认密钥为:$DefaultPassword"}
|
||||
ApiKeyListBackendField.clear()
|
||||
ApiKeyListBackendField.add(DefaultPassword)
|
||||
} catch (Exception: IOException) {
|
||||
Logger.error{"API密钥文件不存在且无法创建API密钥文件。"}
|
||||
Logger.error { Exception }
|
||||
exitProcess(-1) } }
|
||||
} catch (Exception: IOException) {
|
||||
Logger.error{"读取API密钥文件失败 $Exception"} }
|
||||
|
||||
Thread.startVirtualThread {
|
||||
val ParentDirectory = ApiKeyFile.parent
|
||||
val WatchService = FileSystems.getDefault().newWatchService()
|
||||
ParentDirectory.register(WatchService, StandardWatchEventKinds.ENTRY_MODIFY)
|
||||
Logger.info { "开始在目录 $ParentDirectory 监视API密钥文件变动:${ApiKeyFile.fileName}" }
|
||||
while (true) {
|
||||
val WatchKey = WatchService.take()
|
||||
for (Event in WatchKey.pollEvents()) {
|
||||
val ChangedFile = Event.context() as Path
|
||||
if (ChangedFile.toString() == ApiKeyFile.fileName.toString()) {
|
||||
Logger.info { "检测到API密钥文件 ${Event.kind()} 事件,正在重载..." }
|
||||
try {
|
||||
ApiKeyListBackendField.clear()
|
||||
ApiKeyListBackendField.addAll(Files.readAllLines(ApiKeyFile, StandardCharsets.UTF_8))
|
||||
Logger.info { "API密钥列表已重新加载,当前数量:${ApiKeyListBackendField.size}" }
|
||||
} catch (Exception: IOException) {
|
||||
Logger.error { "读取API密钥文件失败 $Exception" } } } }
|
||||
WatchKey.reset() } } } }
|
||||
@@ -0,0 +1,31 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Configuration
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI
|
||||
import io.swagger.v3.oas.models.info.Contact
|
||||
import io.swagger.v3.oas.models.info.Info
|
||||
import io.swagger.v3.oas.models.info.License
|
||||
import io.swagger.v3.oas.models.servers.Server
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
@Configuration
|
||||
class OpenApiConfiguration {
|
||||
@Bean
|
||||
fun CustomOpenAPIConfiguration(): OpenAPI {
|
||||
return OpenAPI()
|
||||
.info(Info()
|
||||
.title("TheWhiteDog9487的通用API们")
|
||||
.summary("")
|
||||
.description("")
|
||||
.termsOfService("")
|
||||
.contact(Contact()
|
||||
.name("TheWhiteDog9487")
|
||||
.url("https://www.github.com/TheWhiteDog9487/WebAPI"))
|
||||
.version("0.8.0")
|
||||
.license(License()
|
||||
.name("WTFPL")
|
||||
.url("https://spdx.org/licenses/WTFPL")) )
|
||||
.servers(listOf<Server>(
|
||||
Server()
|
||||
.url("https://api.thewhitedog9487.xyz")
|
||||
.description("主服务器") ) ) } }
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Configuration.Security
|
||||
|
||||
import jakarta.servlet.FilterChain
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.beans.factory.getBean
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import org.springframework.web.filter.OncePerRequestFilter
|
||||
import xyz.thewhitedog9487.WebAPI.BackgroundTask.ApiKeyManager
|
||||
import xyz.thewhitedog9487.WebAPI.Controller.ResponseData
|
||||
import xyz.thewhitedog9487.WebAPI.Data.Entity.AccessLog
|
||||
import xyz.thewhitedog9487.WebAPI.Miscellaneous.SpringContextHolder.SpringContext
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
class ApiKeyAuthenticationFilter(): OncePerRequestFilter() {
|
||||
val ApiKeyManagerInstance = SpringContext.getBean<ApiKeyManager>()
|
||||
|
||||
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
|
||||
val ApiKey = request.getHeader("X-API-Key") ?: run {
|
||||
val Log = AccessLog(request)
|
||||
Log.ResponseStatusCode = HttpStatus.UNAUTHORIZED.value()
|
||||
Log.SaveIntoDatabase()
|
||||
val ResponseBody = ResponseData(
|
||||
HttpStatus.UNAUTHORIZED.value(),
|
||||
"API密钥验证失败,未传递X-API-Key请求头")
|
||||
response.contentType = MediaType.APPLICATION_JSON_VALUE
|
||||
response.characterEncoding = StandardCharsets.UTF_8.name()
|
||||
response.status = HttpStatus.UNAUTHORIZED.value()
|
||||
response.writer.write(ResponseBody.json)
|
||||
return }
|
||||
if (ApiKey in ApiKeyManagerInstance.ApiKeyList) {
|
||||
val Auth = UsernamePasswordAuthenticationToken(
|
||||
"",
|
||||
ApiKey,
|
||||
listOf(SimpleGrantedAuthority("API")))
|
||||
SecurityContextHolder.getContext().authentication = Auth
|
||||
filterChain.doFilter(request, response) }
|
||||
else {
|
||||
val Log = AccessLog(request)
|
||||
Log.ResponseStatusCode = HttpStatus.FORBIDDEN.value()
|
||||
Log.SaveIntoDatabase()
|
||||
val ResponseBody = ResponseData(
|
||||
HttpStatus.FORBIDDEN.value(),
|
||||
"API密钥验证失败,API密钥无效")
|
||||
response.contentType = MediaType.APPLICATION_JSON_VALUE
|
||||
response.characterEncoding = StandardCharsets.UTF_8.name()
|
||||
response.status = HttpStatus.FORBIDDEN.value()
|
||||
response.writer.write(ResponseBody.json)
|
||||
return } } }
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Configuration.Security
|
||||
|
||||
import jakarta.servlet.DispatcherType
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
||||
|
||||
@Configuration
|
||||
class SpringSecurityConfiguration{
|
||||
@Order(1)
|
||||
@Bean
|
||||
fun Permit(Security: HttpSecurity): SecurityFilterChain {
|
||||
return Security
|
||||
.securityMatcher("/actuator/health")
|
||||
.csrf( { CsrfConfigurer -> CsrfConfigurer.disable() } )
|
||||
.authorizeHttpRequests( { AuthorizationManagerRequestMatcherRegistry ->
|
||||
AuthorizationManagerRequestMatcherRegistry
|
||||
.anyRequest()
|
||||
.permitAll() } )
|
||||
.build() }
|
||||
|
||||
/**
|
||||
* @see ApiKeyAuthenticationFilter#doFilterInternal(HttpServletRequest, HttpServletResponse, FilterChain)
|
||||
*/
|
||||
@Order(2)
|
||||
@Bean
|
||||
fun RequireApiKey(Security: HttpSecurity): SecurityFilterChain {
|
||||
return Security
|
||||
.securityMatcher("/message/**", "/accesslog/**",
|
||||
"/actuator/**")
|
||||
.csrf( { CsrfConfigurer -> CsrfConfigurer.disable() } )
|
||||
.authorizeHttpRequests( { AuthorizationManagerRequestMatcherRegistry ->
|
||||
AuthorizationManagerRequestMatcherRegistry
|
||||
.dispatcherTypeMatchers(DispatcherType.ASYNC)
|
||||
.permitAll()
|
||||
.dispatcherTypeMatchers(DispatcherType.ERROR)
|
||||
.permitAll()
|
||||
.anyRequest()
|
||||
.authenticated() } )
|
||||
.addFilterBefore( ApiKeyAuthenticationFilter(), UsernamePasswordAuthenticationFilter::class.java)
|
||||
.build() }
|
||||
|
||||
@Order(3)
|
||||
@Bean
|
||||
fun PermitAll(Security: HttpSecurity): SecurityFilterChain {
|
||||
return Security
|
||||
.csrf( { CsrfConfigurer -> CsrfConfigurer.disable() } )
|
||||
.authorizeHttpRequests( { AuthorizationManagerRequestMatcherRegistry ->
|
||||
AuthorizationManagerRequestMatcherRegistry
|
||||
.anyRequest()
|
||||
.permitAll() } )
|
||||
.build() } }
|
||||
@@ -0,0 +1,11 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Configuration
|
||||
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||
|
||||
@Configuration
|
||||
class SpringMvcConfiguration: WebMvcConfigurer{
|
||||
override fun configureContentNegotiation(ContentNegotiationConfigurer: ContentNegotiationConfigurer) {
|
||||
ContentNegotiationConfigurer.defaultContentType(MediaType.APPLICATION_JSON) } }
|
||||
@@ -0,0 +1,190 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Controller
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.Parameter
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema
|
||||
import io.swagger.v3.oas.annotations.media.Content
|
||||
import io.swagger.v3.oas.annotations.media.ExampleObject
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import jakarta.validation.constraints.Min
|
||||
import org.springframework.data.domain.Sort
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import xyz.thewhitedog9487.WebAPI.Data.Entity.AccessLog
|
||||
import xyz.thewhitedog9487.WebAPI.Data.Repository.AccessLogRepository
|
||||
import xyz.thewhitedog9487.WebAPI.Data.Specification.AccessLogSpecification
|
||||
import kotlin.reflect.full.declaredMemberProperties
|
||||
|
||||
@Tag(name = "日志相关")
|
||||
@RestController
|
||||
@RequestMapping("/accesslog")
|
||||
class AccessLog(val AccessLogRepositoryInstance: AccessLogRepository) {
|
||||
|
||||
@Operation(
|
||||
summary = "获取访问日志数据库内所有的记录",
|
||||
description = "这是一个私有API,需要在请求头中提供X-API-Key以进行身份验证")
|
||||
@ApiResponses(
|
||||
value = [
|
||||
ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "请求参数错误",
|
||||
content = [
|
||||
Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = Schema(implementation = ResponseData::class),
|
||||
examples = [
|
||||
ExampleObject(
|
||||
name = "Limit小于0",
|
||||
value = """
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Limit参数不能小于0",
|
||||
"data": null
|
||||
}
|
||||
""" ),
|
||||
ExampleObject(
|
||||
name = "字段不存在",
|
||||
value = """
|
||||
{
|
||||
"code": 400,
|
||||
"message": "试图访问一个不存在的数据库字段",
|
||||
"data": "AccessLog类中不存在名为 NotExistField 的字段"
|
||||
}
|
||||
""" ) ] ) ] ),
|
||||
ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "成功获取到数据",
|
||||
content = [
|
||||
Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
array = ArraySchema(
|
||||
minItems = 0,
|
||||
schema = Schema(implementation = AccessLog::class) ),
|
||||
examples = [
|
||||
ExampleObject(
|
||||
value = """
|
||||
[
|
||||
{
|
||||
"ID": 18,
|
||||
"RequestID": "3",
|
||||
"CF_Connecting_IP": "223.87.14.146",
|
||||
"X_Forwarded_For": "223.87.14.146, 141.101.99.156",
|
||||
"RemoteAddress": "192.168.128.11",
|
||||
"CF_IPCountry": "CN",
|
||||
"ISO3166": "CHN",
|
||||
"UserAgent": "Mozilla/5.0 (Linux; Android 10; SM-A202F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile Safari/537.36",
|
||||
"HttpMethod": "GET",
|
||||
"Protocol": "http",
|
||||
"ProtocolVersion": "HTTP/1.0",
|
||||
"URL": "http://dev.thewhitedog9487.xyz/",
|
||||
"QueryString": null,
|
||||
"Header": "host: dev.thewhitedog9487.xyz\nx-real-ip: 141.101.99.156\nx-forwarded-for: 223.87.14.146, 141.101.99.156\nx-forwarded-proto: https\nconnection: close\ncf-ray: 986883191fd5ed0b-LHR\nuser-agent: Mozilla/5.0 (Linux; Android 10; SM-A202F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile Safari/537.36\naccept-encoding: gzip, br\nreferer: https://www.baidu.com\ncdn-loop: cloudflare; loops=1\ncf-connecting-ip: 223.87.14.146\ncf-ipcountry: CN\ncf-visitor: {\"scheme\":\"https\"}",
|
||||
"Timestamp": "2025-09-29T03:49:42.766Z",
|
||||
"ResponseStatusCode": 302
|
||||
},
|
||||
{
|
||||
"ID": 261,
|
||||
"RequestID": "31",
|
||||
"CF_Connecting_IP": "2409:8962:77a:49c:a579:f5af:58a3:e2c",
|
||||
"X_Forwarded_For": "2409:8962:77a:49c:a579:f5af:58a3:e2c, 172.71.150.157",
|
||||
"RemoteAddress": "192.168.128.11",
|
||||
"CF_IPCountry": "CN",
|
||||
"ISO3166": "CHN",
|
||||
"UserAgent": "IntelliJ HTTP Client/IntelliJ IDEA 2025.2.3",
|
||||
"HttpMethod": "GET",
|
||||
"Protocol": "http",
|
||||
"ProtocolVersion": "HTTP/1.0",
|
||||
"URL": "http://dev.thewhitedog9487.xyz/accesslog",
|
||||
"QueryString": null,
|
||||
"Header": "host: dev.thewhitedog9487.xyz\nx-real-ip: 172.71.150.157\nx-forwarded-for: 2409:8962:77a:49c:a579:f5af:58a3:e2c, 172.71.150.157\nx-forwarded-proto: https\nconnection: close\nx-api-key: asdasdaFSEA452453sdcv\nuser-agent: IntelliJ HTTP Client/IntelliJ IDEA 2025.2.3\naccept: */*\ncf-ray: 98cc7d6aaea1c37a-SEA\naccept-encoding: gzip, br\ncdn-loop: cloudflare; loops=1\ncf-connecting-ip: 2409:8962:77a:49c:a579:f5af:58a3:e2c\ncf-ipcountry: CN\ncf-visitor: {\"scheme\":\"https\"}\ncookie: JSESSIONID=A49527B913170D86A5544A252BD31040",
|
||||
"Timestamp": "2025-10-11T07:02:05.718Z",
|
||||
"ResponseStatusCode": 200
|
||||
},
|
||||
{
|
||||
"ID": 263,
|
||||
"RequestID": "35",
|
||||
"CF_Connecting_IP": "2409:8962:77a:49c:a579:f5af:58a3:e2c",
|
||||
"X_Forwarded_For": "2409:8962:77a:49c:a579:f5af:58a3:e2c, 108.162.246.209",
|
||||
"RemoteAddress": "192.168.128.11",
|
||||
"CF_IPCountry": "CN",
|
||||
"ISO3166": "CHN",
|
||||
"UserAgent": "IntelliJ HTTP Client/IntelliJ IDEA 2025.2.3",
|
||||
"HttpMethod": "GET",
|
||||
"Protocol": "http",
|
||||
"ProtocolVersion": "HTTP/1.0",
|
||||
"URL": "http://dev.thewhitedog9487.xyz/ip/ip",
|
||||
"QueryString": null,
|
||||
"Header": "host: dev.thewhitedog9487.xyz\nx-real-ip: 108.162.246.209\nx-forwarded-for: 2409:8962:77a:49c:a579:f5af:58a3:e2c, 108.162.246.209\nx-forwarded-proto: https\nconnection: close\naccept-encoding: gzip, br\nuser-agent: IntelliJ HTTP Client/IntelliJ IDEA 2025.2.3\naccept: */*\ncf-ray: 98cc9ec12c48a1a5-SEA\ncdn-loop: cloudflare; loops=1\ncf-connecting-ip: 2409:8962:77a:49c:a579:f5af:58a3:e2c\ncf-ipcountry: CN\ncf-visitor: {\"scheme\":\"https\"}\ncookie: JSESSIONID=A49527B913170D86A5544A252BD31040",
|
||||
"Timestamp": "2025-10-11T07:24:51.504Z",
|
||||
"ResponseStatusCode": 200
|
||||
}
|
||||
]
|
||||
""" ) ] ) ] ),
|
||||
ApiResponse(
|
||||
responseCode = "401",
|
||||
description = "未提供API密钥",
|
||||
content = [
|
||||
Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = Schema(implementation = ResponseData::class),
|
||||
examples = [
|
||||
ExampleObject(
|
||||
value = """
|
||||
{
|
||||
"code": 401,
|
||||
"message": "API密钥验证失败,未传递X-API-Key请求头",
|
||||
"data": null
|
||||
}
|
||||
""" ) ] ) ] ),
|
||||
ApiResponse(
|
||||
responseCode = "403",
|
||||
description = "API密钥无效",
|
||||
content = [
|
||||
Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = Schema(implementation = ResponseData::class),
|
||||
examples = [
|
||||
ExampleObject(
|
||||
value = """
|
||||
{
|
||||
"code": 403,
|
||||
"message": "API密钥验证失败,API密钥无效",
|
||||
"data": null
|
||||
}
|
||||
""" ) ] ) ] ) ] )
|
||||
@GetMapping
|
||||
fun GetLog(@Parameter(description = "用于身份验证的API密钥", `in` = ParameterIn.HEADER, required = true, example = "ds1858dscc8745sfwe" )
|
||||
@RequestHeader("X-API-Key") ApiKey: String,
|
||||
|
||||
@Min(0)
|
||||
@Parameter(description = "返回记录的数量限制,0表示不限制", example = "10")
|
||||
@RequestParam(defaultValue = "0") Limit: Int,
|
||||
|
||||
@Parameter(description = "排序方向:ASC 升序 从小到大 / DESC 降序 从大到小", example = "DESC")
|
||||
@RequestParam(defaultValue = "ASC") Direction: Sort.Direction,
|
||||
|
||||
@Parameter(description = "排序字段名", example = "ID")
|
||||
@RequestParam(defaultValue = "Timestamp") OrderByFieldName: String): List<AccessLog> {
|
||||
Limit < 0 && throw IllegalArgumentException("Limit参数不能小于0")
|
||||
AccessLog::class.declaredMemberProperties.all{
|
||||
it.name != OrderByFieldName } && throw NoSuchFieldException("${AccessLog::class.simpleName}类中不存在名为 $OrderByFieldName 的字段")
|
||||
|
||||
val Results = AccessLogRepositoryInstance.findAll(AccessLogSpecification.OrderByField(OrderByFieldName, Direction))
|
||||
// ↑ ↓ 不能用 AccessLogRepositoryInstance.findAll(Sort.by()) ,字段名处理有问题,传入排序字段Abc会在不知道什么地方变成abc导致查询失败
|
||||
return if (Limit > 0) Results.take(Limit) else Results }
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException::class)
|
||||
fun TriggerWhenLimitLessThanZero(e: IllegalArgumentException): ResponseEntity<ResponseData> {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ResponseData(HttpStatus.BAD_REQUEST.value(), message = e.message ) ) }
|
||||
|
||||
@ExceptionHandler(NoSuchFieldException::class)
|
||||
fun TriggerWhenAccessFieldThatNotExist(e: NoSuchFieldException): ResponseEntity<ResponseData> {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ResponseData(HttpStatus.BAD_REQUEST.value(), "试图访问一个不存在的数据库字段", e.message ) ) } }
|
||||
@@ -0,0 +1,41 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Controller.Filter
|
||||
|
||||
import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import jakarta.servlet.*
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper
|
||||
import xyz.thewhitedog9487.WebAPI.Data.Entity.AccessLog
|
||||
|
||||
@Component
|
||||
class LogClientInfo(): Filter {
|
||||
val Logger: KLogger = KotlinLogging.logger{}
|
||||
|
||||
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
|
||||
val HttpServletRequest = request as HttpServletRequest
|
||||
val HttpServletResponse = response as HttpServletResponse
|
||||
val CachedResponse = ContentCachingResponseWrapper(HttpServletResponse)
|
||||
val Log = AccessLog(HttpServletRequest )
|
||||
try {
|
||||
chain.doFilter(request, CachedResponse)
|
||||
if (request.isAsyncStarted){
|
||||
request.asyncContext.addListener(object : AsyncListener {
|
||||
override fun onComplete(event: AsyncEvent?) {
|
||||
Log.ResponseStatusCode = CachedResponse.status
|
||||
Log.SaveIntoDatabase(CachedResponse) }
|
||||
override fun onTimeout(event: AsyncEvent?) {
|
||||
Logger.error{ event?.throwable?.localizedMessage }
|
||||
Log.ResponseStatusCode = CachedResponse.status
|
||||
Log.SaveIntoDatabase(CachedResponse) }
|
||||
override fun onError(event: AsyncEvent?) = onTimeout(event)
|
||||
override fun onStartAsync(event: AsyncEvent?) { } } )
|
||||
return }
|
||||
Log.ResponseStatusCode = CachedResponse.status
|
||||
} catch (e: Exception) {
|
||||
Logger.error{ e.localizedMessage }
|
||||
Log.ResponseStatusCode = CachedResponse.status
|
||||
Log.SaveIntoDatabase(CachedResponse)
|
||||
throw e }
|
||||
Log.SaveIntoDatabase(CachedResponse) } }
|
||||
@@ -0,0 +1,22 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Controller.Filter
|
||||
|
||||
import jakarta.servlet.FilterChain
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.slf4j.MDC
|
||||
import org.springframework.core.Ordered
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.filter.OncePerRequestFilter
|
||||
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
// ↑ 拉高优先级,确保在日志记录之前设置MDC,不然和没设置就没有区别了
|
||||
class SetMdcRequestId: OncePerRequestFilter() {
|
||||
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
|
||||
MDC.put("RequestID", request.requestId)
|
||||
try {
|
||||
filterChain.doFilter(request, response)
|
||||
} finally {
|
||||
// 防止内存泄漏
|
||||
MDC.remove("RequestID") } } }
|
||||
@@ -0,0 +1,77 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Controller
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.media.Content
|
||||
import io.swagger.v3.oas.annotations.media.ExampleObject
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestHeader
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@Tag(name="请求客户端IP地址相关")
|
||||
@RestController
|
||||
@RequestMapping("/ip")
|
||||
class IP {
|
||||
@Operation(summary = "获取请求客户端的IP地址", description =
|
||||
"""
|
||||
优先级:
|
||||
1. CF-Connecting-IP
|
||||
2. X-Forwarded-For
|
||||
3. 直接使用请求的远程地址""")
|
||||
@ApiResponse(responseCode = "200",
|
||||
content = [Content(
|
||||
mediaType = MediaType.TEXT_PLAIN_VALUE,
|
||||
schema = Schema(implementation = String::class),
|
||||
examples = [ExampleObject(value = "78.141.226.247")])],
|
||||
description = "成功获取到IP地址,内容为纯文本格式的IP地址")
|
||||
@GetMapping("")
|
||||
fun GetIP(@RequestHeader("CF-Connecting-IP", required = false) Header_CFConnectingIP: String?,
|
||||
@RequestHeader("X-Forwarded-For", required = false) Header_XForwardedFor: String?,
|
||||
Request: HttpServletRequest): ResponseEntity<String> {
|
||||
Header_CFConnectingIP?.let {
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.TEXT_PLAIN)
|
||||
.body(Header_CFConnectingIP) }
|
||||
Header_XForwardedFor?.let {
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.TEXT_PLAIN)
|
||||
.body(Header_XForwardedFor) }
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.TEXT_PLAIN)
|
||||
.body(Request.remoteAddr) }
|
||||
|
||||
@Operation(summary = "获取请求客户端的ISO 3166-1 alpha-2国家代码", description =
|
||||
"""
|
||||
依赖Cloudflare的CF-IPCountry头部
|
||||
如果请求没有经过Cloudflare,则会返回"未找到CF-IPCountry头部"
|
||||
""")
|
||||
@ApiResponses(value = [
|
||||
ApiResponse(responseCode = "200",
|
||||
content = [Content(
|
||||
mediaType = MediaType.TEXT_PLAIN_VALUE,
|
||||
schema = Schema(implementation = String::class),
|
||||
examples = [ExampleObject(value = "HK")])],
|
||||
description = "成功获取到ISO 3166-1 alpha-2国家代码,内容为纯文本格式的国家代码"),
|
||||
ApiResponse(responseCode = "404",
|
||||
content = [Content(
|
||||
mediaType = MediaType.TEXT_PLAIN_VALUE,
|
||||
schema = Schema(implementation = String::class),
|
||||
examples = [ExampleObject(value = "未找到CF-IPCountry头部")] ) ],
|
||||
description = "未找到CF-IPCountry头部,内容为纯文本格式的错误信息") ] )
|
||||
@GetMapping("iso3166")
|
||||
fun GetIso3166(@RequestHeader("CF-IPCountry", required = false) Header_CFIPCountry: String?): ResponseEntity<String> {
|
||||
Header_CFIPCountry?.let {
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.TEXT_PLAIN)
|
||||
.body(Header_CFIPCountry) }
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.contentType(MediaType.TEXT_PLAIN)
|
||||
.body("未找到CF-IPCountry头部") } }
|
||||
@@ -0,0 +1,194 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Controller
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import dev.kord.common.entity.Snowflake
|
||||
import dev.kord.core.Kord
|
||||
import dev.kord.core.entity.channel.TextChannel
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.Parameter
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn
|
||||
import io.swagger.v3.oas.annotations.media.Content
|
||||
import io.swagger.v3.oas.annotations.media.ExampleObject
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException
|
||||
import org.springframework.web.bind.MissingRequestHeaderException
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import xyz.thewhitedog9487.WebAPI.Exception.NoSuchDiscordChannelException
|
||||
import xyz.thewhitedog9487.WebAPI.Miscellaneous.VirtualThreadCoroutineDispatcher
|
||||
import xyz.thewhitedog9487.WebAPI.Miscellaneous.link
|
||||
import java.net.URI
|
||||
|
||||
@Tag(name="远程消息处理相关")
|
||||
@RestController
|
||||
@RequestMapping("/message")
|
||||
class Message(val DiscordBotClient: Kord) {
|
||||
@JsonAutoDetect(
|
||||
fieldVisibility = JsonAutoDetect.Visibility.ANY,
|
||||
getterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
setterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
creatorVisibility = JsonAutoDetect.Visibility.NONE)
|
||||
@Schema(description = "包含了发送消息的数据包")
|
||||
data class PostMessageData(
|
||||
@Schema(description = "Discord频道ID", example = "1398192763845214239")
|
||||
@JsonProperty("ChannelId")
|
||||
val ChannelId: Long,
|
||||
@Schema(description = "要发送的消息内容", example = "成功完成备份,最新文件时间为2025 08 21")
|
||||
@JsonProperty("Content")
|
||||
val Content: String)
|
||||
|
||||
@Operation(summary = "通过Discord Bot向指定频道发送消息", description =
|
||||
"""这是一个私有API
|
||||
需要在请求头中提供X-API-Key以进行身份验证
|
||||
需要在请求体中提供频道ID和消息内容""")
|
||||
@ApiResponses(value = [
|
||||
ApiResponse(responseCode = "201",
|
||||
content = [Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = Schema(implementation = ResponseData::class),
|
||||
examples = [ExampleObject(value =
|
||||
"""
|
||||
{
|
||||
"code": 201,
|
||||
"message": "消息发送成功",
|
||||
"data": {
|
||||
"新ID": "1407848258813825135",
|
||||
"频道ID": "1398192763845214239",
|
||||
"内容": "写点什么好呢"
|
||||
}
|
||||
}
|
||||
""", name = "成功") ] ) ],
|
||||
description = "消息发送成功,响应体中包含新消息的ID、频道ID和内容"),
|
||||
ApiResponse(responseCode = "400",
|
||||
content = [Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = Schema(implementation = ResponseData::class),
|
||||
examples = [ExampleObject(value =
|
||||
"""
|
||||
{
|
||||
"code": 400,
|
||||
"message": "请求体无法解析",
|
||||
"data": {
|
||||
"错误信息": "JSON parse error: Unexpected character ('}' (code 125)): was expecting double-quote to start field name"
|
||||
}
|
||||
}
|
||||
""", name = "请求体无法解析"),
|
||||
ExampleObject(value =
|
||||
"""
|
||||
{
|
||||
"code": 400,
|
||||
"message": "消息发送失败",
|
||||
"data": {
|
||||
"错误信息": "ID为1398192763845214239的频道不存在",
|
||||
"频道ID": "1398192763845214239"
|
||||
}
|
||||
}
|
||||
""", name = "频道不存在")] ) ],
|
||||
description = "客户端发送的请求存在问题,请检查响应的data字段以获取更多信息"),
|
||||
ApiResponse(responseCode = "401",
|
||||
content = [Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = Schema(implementation = ResponseData::class),
|
||||
examples = [ExampleObject(value =
|
||||
"""
|
||||
{
|
||||
"code": 401,
|
||||
"message": "API密钥验证失败,未传递X-API-Key请求头",
|
||||
"data": null
|
||||
}
|
||||
""") ] ) ],
|
||||
description = "未提供API密钥"),
|
||||
ApiResponse(responseCode = "403",
|
||||
content = [Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = Schema(implementation = ResponseData::class),
|
||||
examples = [ExampleObject(value =
|
||||
"""
|
||||
{
|
||||
"code": 403,
|
||||
"message": "API密钥验证失败,API密钥无效",
|
||||
"data": null
|
||||
}
|
||||
""") ] ) ],
|
||||
description = "API密钥无效"),
|
||||
ApiResponse(responseCode = "500",
|
||||
content = [Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = Schema(implementation = ResponseData::class),
|
||||
examples = [ExampleObject(value =
|
||||
"""{
|
||||
"code": 500,
|
||||
"message": "处理请求时发生未知错误",
|
||||
"data": {
|
||||
"错误信息": "POST /channels/1398192763845214/messages returned 404 Not Found with response {code=10003, message=Unknown Channel}"
|
||||
}
|
||||
}""", name = "未知错误") ] ) ],
|
||||
description =
|
||||
"""
|
||||
处理请求时发生未知错误,可能是由于Discord服务器问题或其他内部错误,请查看响应Body以确定原因
|
||||
另外,所有未被针对性处理的异常都会触发此响应
|
||||
""") ] )
|
||||
@PostMapping("/discord")
|
||||
suspend fun DiscordPush(
|
||||
@Parameter(description = "用于身份验证的API密钥", `in` = ParameterIn.HEADER, required = true, example = "ds1858dscc8745sfwe")
|
||||
@RequestHeader("X-API-Key") ApiKey: String,
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
description = "包含频道ID和消息内容的JSON对象",
|
||||
required = true,
|
||||
content = [Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_VALUE,
|
||||
schema = Schema(implementation = PostMessageData::class) ) ] )
|
||||
@RequestBody RequestBody: PostMessageData): ResponseEntity<ResponseData> {
|
||||
return withContext(VirtualThreadCoroutineDispatcher) {
|
||||
val ChannelID = Snowflake(RequestBody.ChannelId)
|
||||
val MessageData = DiscordBotClient
|
||||
.getChannelOf<TextChannel>(ChannelID)
|
||||
?.createMessage(RequestBody.Content)
|
||||
?: throw NoSuchDiscordChannelException(ChannelID)
|
||||
return@withContext ResponseEntity.created(URI.create(MessageData.link))
|
||||
.body(ResponseData(
|
||||
HttpStatus.CREATED.value(),
|
||||
"消息发送成功",
|
||||
mapOf(
|
||||
"新ID" to MessageData.id.toString(),
|
||||
"频道ID" to ChannelID.value,
|
||||
"内容" to RequestBody.Content ) ) ) } }
|
||||
@ExceptionHandler(NoSuchDiscordChannelException::class)
|
||||
fun TriggerWhenSpecificChannelDoesNotExist(Exception: NoSuchDiscordChannelException, Request: HttpServletRequest): ResponseEntity<ResponseData> {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ResponseData(
|
||||
HttpStatus.BAD_REQUEST.value(),
|
||||
"消息发送失败",
|
||||
mapOf(
|
||||
"错误信息" to Exception.message,
|
||||
"频道ID" to Exception.TargetChannelId.value ) ) ) }
|
||||
@ExceptionHandler(MissingRequestHeaderException::class)
|
||||
fun HandleMissingHeader(Exception: MissingRequestHeaderException, Request: HttpServletRequest): ResponseEntity<ResponseData> {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ResponseData(
|
||||
HttpStatus.BAD_REQUEST.value(),
|
||||
"请求缺少必要的头部信息",
|
||||
mapOf("缺失的头部" to Exception.headerName ) ) ) }
|
||||
@ExceptionHandler(HttpMessageNotReadableException::class)
|
||||
fun HandleMessageNotReadable(Exception: HttpMessageNotReadableException, Request: HttpServletRequest): ResponseEntity<ResponseData> {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(ResponseData(
|
||||
HttpStatus.BAD_REQUEST.value(),
|
||||
"请求体无法解析",
|
||||
mapOf("错误信息" to Exception.message ) ) ) }
|
||||
@ExceptionHandler(Exception::class)
|
||||
fun HandleOtherException(Exception: Exception, Request: HttpServletRequest): ResponseEntity<ResponseData> {
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(ResponseData(
|
||||
HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
||||
"处理请求时发生未知错误",
|
||||
mapOf("错误信息" to Exception.message ) ) ) } }
|
||||
@@ -0,0 +1,17 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Controller
|
||||
|
||||
import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.servlet.view.RedirectView
|
||||
|
||||
@Controller
|
||||
class RedirectRootToSwaggerWebUI {
|
||||
val Logger: KLogger = KotlinLogging.logger{}
|
||||
val RedirectTo = "/swagger-ui/index.html"
|
||||
|
||||
@GetMapping("/")
|
||||
fun Redirect(): RedirectView {
|
||||
Logger.info{"正在将访问 / 的请求重定向到 $RedirectTo"}
|
||||
return RedirectView(RedirectTo) } }
|
||||
@@ -0,0 +1,25 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Controller
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import tools.jackson.databind.ObjectMapper
|
||||
|
||||
@Schema(description = "所有API通用的标准响应格式")
|
||||
data class ResponseData(
|
||||
@Schema(description = "内部用响应代码", example = "201") val code: Int,
|
||||
@Schema(description = "人类可读说明信息", example = "消息发送成功") val message: Any? = null,
|
||||
@Schema(description = "具体的响应数据", example = """
|
||||
{
|
||||
"新ID": "1407848258813825135",
|
||||
"内容": "写点什么好呢",
|
||||
"频道ID": "1398192763845214239"
|
||||
}
|
||||
""") val data: Any? = null){
|
||||
constructor(code: Int, message: Any): this(code, message, null)
|
||||
|
||||
@JsonIgnore
|
||||
@Schema(hidden = true)
|
||||
val json: String = JsonMapper.writeValueAsString(this)
|
||||
|
||||
companion object{
|
||||
val JsonMapper = ObjectMapper() } }
|
||||
@@ -0,0 +1,140 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Data.Entity
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import jakarta.persistence.*
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.springframework.beans.factory.getBean
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper
|
||||
import xyz.thewhitedog9487.WebAPI.Data.Repository.AccessLogRepository
|
||||
import xyz.thewhitedog9487.WebAPI.Miscellaneous.SpringContextHolder.SpringContext
|
||||
import java.time.Instant
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
@JsonAutoDetect(
|
||||
fieldVisibility = JsonAutoDetect.Visibility.ANY,
|
||||
getterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
setterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||
creatorVisibility = JsonAutoDetect.Visibility.NONE )
|
||||
@Schema(description = "访问日志条目")
|
||||
@Entity
|
||||
@Table
|
||||
class AccessLog(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(nullable = false)
|
||||
@JsonProperty("ID")
|
||||
@Schema(description = "主键", example = "1")
|
||||
var ID: Long? = null,
|
||||
|
||||
@JsonProperty("RequestID")
|
||||
@Schema(description = "Servlet请求ID", example = "0")
|
||||
var RequestID: String = "",
|
||||
|
||||
@JsonProperty("CF_Connecting_IP")
|
||||
@Column(name = "\"CF-Connecting-IP\"")
|
||||
@Schema(description = "Cloudflare提供的请求IP", example = "2409:9802:74a3:6eff:3cae:5cba:25ac:788a")
|
||||
var CF_Connecting_IP: String? = null,
|
||||
|
||||
@JsonProperty("X_Forwarded_For")
|
||||
@Column(name = "\"X-Forwarded-For\"")
|
||||
@Schema(description = "X-Forwarded-For请求头提供的请求IP", example = "2409:9802:74a3:6eff:3cae:5cba:25ac:788a, 108.162.245.89")
|
||||
var X_Forwarded_For: String? = null,
|
||||
|
||||
@JsonProperty("RemoteAddress")
|
||||
@Schema(description = "Servlet报告的请求IP", example = "192.168.128.11")
|
||||
var RemoteAddress: String = "",
|
||||
|
||||
@JsonProperty("CF_IPCountry")
|
||||
@Column(name = "\"CF-IPCountry\"")
|
||||
@Schema(description = "Cloudflare提供的请求国家代码", example = "CN")
|
||||
var CF_IPCountry: String? = null,
|
||||
|
||||
@JsonProperty("ISO3166")
|
||||
@Schema(description = "Java内部根据CF-IPCountry得到的对应ISO3166代码", example = "CHN")
|
||||
var ISO3166: String? = null,
|
||||
|
||||
@JsonProperty("UserAgent")
|
||||
@Schema(description = "标准HTTP User-Agent请求头", example = "IntelliJ HTTP Client/IntelliJ IDEA 2025.2.2")
|
||||
var UserAgent: String? = null,
|
||||
|
||||
@JsonProperty("HttpMethod")
|
||||
@Schema(description = "HTTP请求方法", example = "GET")
|
||||
var HttpMethod: String = "",
|
||||
|
||||
@JsonProperty("Protocol")
|
||||
@Schema(description = "请求协议", example = "http")
|
||||
var Protocol: String = "",
|
||||
|
||||
@JsonProperty("ProtocolVersion")
|
||||
@Schema(description = "协议版本", example = "HTTP/1.1")
|
||||
var ProtocolVersion: String = "",
|
||||
|
||||
@JsonProperty("URL")
|
||||
@Schema(description = "请求的完整URL", example = "https://dev.thewhitedog9487.xyz/ip/ip")
|
||||
var URL: String = "",
|
||||
|
||||
@JsonProperty("QueryString")
|
||||
@Schema(description = "查询字符串", example = "")
|
||||
var QueryString: String? = null,
|
||||
|
||||
@JsonProperty("Header")
|
||||
@Column(columnDefinition = "text")
|
||||
@Schema(
|
||||
description = "所有HTTP请求头的文本字符串", example = (
|
||||
"""
|
||||
host: dev.thewhitedog9487.xyz
|
||||
x-real-ip: 162.158.42.207
|
||||
x-forwarded-for: 2409:8962:f29:7dc:3996:6b11:d996:3cc0, 162.158.42.207
|
||||
x-forwarded-proto: https
|
||||
connection: close
|
||||
x-api-key: asdasdaFSEA452453sdcv
|
||||
user-agent: IntelliJ HTTP Client/IntelliJ IDEA 2025.2.2
|
||||
accept: */*
|
||||
cf-ray: 986868be1ca5def5-SEA
|
||||
accept-encoding: gzip, br
|
||||
cdn-loop: cloudflare; loops=1
|
||||
cf-connecting-ip: 2409:8962:f29:7dc:3996:6b11:d996:3cc0
|
||||
cf-ipcountry: CN
|
||||
cf-visitor: {"scheme":"https"}
|
||||
cookie: JSESSIONID=A49527B913170D86A5544A252BD31040
|
||||
"""))
|
||||
var Header: String = "",
|
||||
|
||||
@JsonProperty("Timestamp")
|
||||
@Schema(description = "请求时间(ISO-8601格式)", example = "2025-09-29T03:49:42.766Z")
|
||||
var Timestamp: Instant = Instant.now(),
|
||||
|
||||
@JsonProperty("ResponseStatusCode")
|
||||
@Schema(description = "响应的HTTP状态码", example = "200")
|
||||
var ResponseStatusCode: Int = 0 ){
|
||||
constructor(httpRequest: HttpServletRequest): this(
|
||||
null,
|
||||
httpRequest.requestId,
|
||||
httpRequest.getHeader("CF-Connecting-IP"),
|
||||
httpRequest.getHeader("X-Forwarded-For"),
|
||||
httpRequest.remoteAddr,
|
||||
httpRequest.getHeader("CF-IPCountry"),
|
||||
if (httpRequest.getHeader("CF-IPCountry") == null) null else Locale.of(Locale.PRC.language, httpRequest.getHeader("CF-IPCountry"), Locale.SIMPLIFIED_CHINESE.variant).isO3Country,
|
||||
httpRequest.getHeader("User-Agent"),
|
||||
httpRequest.method,
|
||||
httpRequest.scheme,
|
||||
httpRequest.protocol,
|
||||
httpRequest.requestURL.toString(),
|
||||
httpRequest.queryString,
|
||||
httpRequest.headerNames.toList().joinToString("\n") { name ->
|
||||
return@joinToString "$name: ${httpRequest.getHeaders(name).toList().joinToString(", ")}" },
|
||||
Instant.now(),
|
||||
0 )
|
||||
fun SaveIntoDatabase(CachedResponse: ContentCachingResponseWrapper? = null){
|
||||
val SQLiteWriteLock = SpringContext.getBean("getSQLiteWriteLock", ReentrantLock::class.java)
|
||||
val AccessLogRepository = SpringContext.getBean<AccessLogRepository>()
|
||||
try {
|
||||
SQLiteWriteLock.lock()
|
||||
AccessLogRepository.save(this) }
|
||||
finally {
|
||||
SQLiteWriteLock.unlock()
|
||||
CachedResponse?.copyBodyToResponse() } } }
|
||||
@@ -0,0 +1,7 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Data.Repository
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor
|
||||
import xyz.thewhitedog9487.WebAPI.Data.Entity.AccessLog
|
||||
|
||||
interface AccessLogRepository: JpaRepository<AccessLog, Long>, JpaSpecificationExecutor<AccessLog>
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Data.Specification
|
||||
|
||||
import org.springframework.data.domain.Sort
|
||||
import org.springframework.data.jpa.domain.Specification
|
||||
import xyz.thewhitedog9487.WebAPI.Data.Entity.AccessLog
|
||||
|
||||
object AccessLogSpecification {
|
||||
fun OrderByField(TargetFieldName: String, Order: Sort.Direction) = Specification<AccessLog>{
|
||||
Root, CriteriaQuery, CriteriaBuilder ->
|
||||
CriteriaQuery.orderBy(
|
||||
when(Order){
|
||||
Sort.Direction.ASC -> CriteriaBuilder.asc(Root.get<Any>(TargetFieldName))
|
||||
Sort.Direction.DESC -> CriteriaBuilder.desc(Root.get<Any>(TargetFieldName)) })
|
||||
return@Specification CriteriaBuilder.conjunction() } }
|
||||
@@ -0,0 +1,7 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Exception
|
||||
|
||||
import dev.kord.common.entity.Snowflake
|
||||
|
||||
class NoSuchDiscordChannelException(val TargetChannelId: Snowflake,
|
||||
message: String? = null,
|
||||
cause: Throwable? = null): Exception(message ?: "ID为${TargetChannelId}的频道不存在", cause)
|
||||
@@ -0,0 +1,46 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Miscellaneous
|
||||
|
||||
import dev.kord.core.Kord
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.coroutines.*
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.SmartLifecycle
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.stereotype.Component
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.locks.Lock
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
val VirtualThreadCoroutineDispatcher: ExecutorCoroutineDispatcher =
|
||||
Executors.newVirtualThreadPerTaskExecutor()
|
||||
.asCoroutineDispatcher()
|
||||
|
||||
@Component
|
||||
class GlobalSharedBean {
|
||||
val Logger = KotlinLogging.logger{}
|
||||
|
||||
@Bean
|
||||
fun DiscordClient(@Value("\${Discord_Bot_Token:}") DiscordBotToken: String): Kord {
|
||||
if (DiscordBotToken.isEmpty()) {
|
||||
Logger.error { "未通过参数--Discord_Bot_Token或环境变量Discord_Bot_Token提供Discord登录令牌,请检查配置。" }
|
||||
throw IllegalArgumentException("未提供Discord登录令牌") }
|
||||
return runBlocking(VirtualThreadCoroutineDispatcher) { Kord(DiscordBotToken) } }
|
||||
|
||||
@Bean
|
||||
fun KordLifecycleManager(KordInstance: Kord) = object: SmartLifecycle {
|
||||
var Job: Job? = null
|
||||
override fun start() {
|
||||
Job = CoroutineScope(VirtualThreadCoroutineDispatcher).launch {
|
||||
KordInstance.login { } } }
|
||||
|
||||
override fun stop() {
|
||||
runBlocking(VirtualThreadCoroutineDispatcher) {
|
||||
try {
|
||||
KordInstance.logout()
|
||||
} catch (_: IllegalStateException) { }
|
||||
Job?.cancelAndJoin() } }
|
||||
|
||||
override fun isRunning() = Job?.isActive ?: false }
|
||||
|
||||
@get:Bean
|
||||
val SQLiteWriteLock: Lock = ReentrantLock() }
|
||||
@@ -0,0 +1,14 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Miscellaneous
|
||||
|
||||
import dev.kord.core.entity.Message
|
||||
import dev.kord.core.entity.channel.GuildChannel
|
||||
import dev.kord.core.supplier.getChannelOfOrNull
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
val Message.link get(): String {
|
||||
return runBlocking(VirtualThreadCoroutineDispatcher) {
|
||||
data.guildId.value?.let { guildId ->
|
||||
return@runBlocking "https://discord.com/channels/$guildId/$channelId/$id" }
|
||||
val channel = kord.defaultSupplier.getChannelOfOrNull<GuildChannel>(channelId)
|
||||
val guild = channel?.guildId?.toString() ?: "@me"
|
||||
return@runBlocking "https://discord.com/channels/$guild/$channelId/$id" } }
|
||||
@@ -0,0 +1,12 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Miscellaneous
|
||||
|
||||
import org.springframework.context.ApplicationContext
|
||||
import org.springframework.context.ApplicationContextAware
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
object SpringContextHolder: ApplicationContextAware{
|
||||
lateinit var SpringContext: ApplicationContext
|
||||
|
||||
override fun setApplicationContext(applicationContext: ApplicationContext) {
|
||||
SpringContext = applicationContext } }
|
||||
@@ -0,0 +1,152 @@
|
||||
package xyz.thewhitedog9487.WebAPI.Startup
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.boot.system.ApplicationHome
|
||||
import xyz.thewhitedog9487.WebAPI.WebApiApplication
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
val Logger = KotlinLogging.logger{}
|
||||
val AppHome: ApplicationHome = ApplicationHome(WebApiApplication::class.java)
|
||||
val CurrentOS: String = System.getProperty("os.name")
|
||||
val CurrentUser: String = System.getProperty("user.name")
|
||||
/**
|
||||
* 当前可执行文件的路径。
|
||||
*
|
||||
* 如果是通过GraalVM生成的Native Image,则是可执行文件的路径
|
||||
*
|
||||
* 如果是直接用JRE运行的JAR包,则是JAR文件的路径
|
||||
*/
|
||||
val CurrentExecutableFilePath: Path get() {
|
||||
Logger.debug { "IsImageCode: $IsImageCode" }
|
||||
if (IsImageCode == true) {
|
||||
val ExecutablePath = Class.forName("org.graalvm.nativeimage.ProcessProperties")
|
||||
.getMethod("getExecutableName")
|
||||
.invoke(null) as String
|
||||
Logger.debug { "${"ExecutablePath: {}"} $ExecutablePath" }
|
||||
return Path(ExecutablePath)
|
||||
} else {
|
||||
// ↓ 使用GraalVM的JVM而不是Native Image
|
||||
return AppHome
|
||||
.source!!
|
||||
.toPath()
|
||||
.toRealPath() } }
|
||||
val WorkingDirectory: Path = AppHome
|
||||
.dir
|
||||
.toPath()
|
||||
val ServiceFileDirectory: Path = Path("/etc/systemd/system/")
|
||||
val ServiceFileName: Path = Path("WebAPI.service")
|
||||
const val SoftLinkFileName: String = "Current"
|
||||
val SymbolicLinkPath: Path = WorkingDirectory.resolve(SoftLinkFileName)
|
||||
var DiscordBotToken: String = System.getenv("Discord_Bot_Token")
|
||||
|
||||
/**
|
||||
* 检测当前运行环境是否为GraalVM生成的Native Image
|
||||
*/
|
||||
val IsImageCode: Boolean get() {
|
||||
return try {
|
||||
Class.forName("org.graalvm.nativeimage.ImageInfo")
|
||||
.getMethod("inImageCode")
|
||||
.invoke(null) as Boolean
|
||||
} catch (_: Exception) {
|
||||
false } }
|
||||
|
||||
fun ProcessArguments(CommandLineArguments: Array<String>) {
|
||||
Logger.debug { "${"CommandLineArguments: {}"} ${CommandLineArguments.contentToString()}" }
|
||||
for (Argument in CommandLineArguments) {
|
||||
if (Argument in listOf("install", "--install")) {
|
||||
/*
|
||||
在Native Image / Jar文件旁边生成指向自身的,名为Current的软连接
|
||||
然后在软连接旁生成systemd服务文件,执行目标指向软连接
|
||||
*/
|
||||
DiscordBotToken = if (CommandLineArguments.contains("--Discord_Bot_Token") &&
|
||||
CommandLineArguments.getOrNull(CommandLineArguments.indexOf("--Discord_Bot_Token") + 1) != null ){
|
||||
CommandLineArguments[CommandLineArguments.indexOf("--Discord_Bot_Token") + 1] }
|
||||
else System.getProperty("Discord_Bot_Token", "")
|
||||
if (DiscordBotToken.isEmpty()) {
|
||||
Logger.error { "必须通过--Discord_Bot_Token参数或Discord_Bot_Token环境变量提供Discord机器人的令牌以使本程序正常工作。" }
|
||||
throw IllegalArgumentException("缺少必须的--Discord_Bot_Token参数") }
|
||||
Logger.info { "开始安装systemd服务" }
|
||||
Logger.debug { "SystemProperties.os.name: $CurrentOS" }
|
||||
if (CurrentOS.startsWith("Linux")) {
|
||||
Logger.debug { "SystemProperties.user.name: $CurrentUser" }
|
||||
if (CurrentUser != "root") {
|
||||
Logger.error { "只有root才有权在系统层级安装systemd服务,但是JVM报告的当前用户是$CurrentUser" }
|
||||
throw IllegalStateException("请使用root用户运行此命令以安装为systemd服务") }
|
||||
Logger.debug { "CurrentExecutableFilePath: $CurrentExecutableFilePath" }
|
||||
Logger.debug { "WorkingDirectory: $WorkingDirectory" }
|
||||
Logger.debug { "SymbolicLinkPath: $SymbolicLinkPath" }
|
||||
ProcessBuilder("systemctl", "stop", ServiceFileName.toString()).start().waitFor()
|
||||
Logger.info { "已停止systemd服务: $ServiceFileName" }
|
||||
Files.deleteIfExists(SymbolicLinkPath)
|
||||
Files.createSymbolicLink(SymbolicLinkPath, CurrentExecutableFilePath)
|
||||
Logger.info { "成功创建指向当前程序的软链接: $SymbolicLinkPath -> $CurrentExecutableFilePath" }
|
||||
val CurrentJavaPath by lazy {
|
||||
if (IsImageCode == false) {
|
||||
Logger.debug { "CurrentJavaPath: ${Path("/proc/self/exe").toRealPath()}" }
|
||||
Path("/proc/self/exe").toRealPath() }
|
||||
else throw Exception() }
|
||||
val ExecCommand = if (IsImageCode) SymbolicLinkPath.toString() else "$CurrentJavaPath -jar $SymbolicLinkPath"
|
||||
Logger.debug { "ExecCommand: $ExecCommand" }
|
||||
val ServiceFileContent = """
|
||||
[Unit]
|
||||
Description=WebAPI
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=$CurrentUser
|
||||
WorkingDirectory=$WorkingDirectory
|
||||
ExecStart=$ExecCommand
|
||||
Restart=always
|
||||
Type=simple
|
||||
Environment="Discord_Bot_Token=$DiscordBotToken"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
""".trimIndent()
|
||||
Logger.debug { "ServiceFileContent: \n$ServiceFileContent" }
|
||||
Files.deleteIfExists(WorkingDirectory.resolve(ServiceFileName))
|
||||
Files.writeString(WorkingDirectory.resolve(ServiceFileName), ServiceFileContent)
|
||||
Logger.info { "成功创建systemd服务文件: ${WorkingDirectory.resolve(ServiceFileName)}" }
|
||||
Files.deleteIfExists(ServiceFileDirectory.resolve(ServiceFileName))
|
||||
Files.createSymbolicLink(
|
||||
ServiceFileDirectory.resolve(ServiceFileName),
|
||||
WorkingDirectory.resolve(ServiceFileName) )
|
||||
Logger.info {
|
||||
"成功创建指向服务文件的软链接: ${ServiceFileDirectory.resolve(ServiceFileName)} -> ${WorkingDirectory.resolve(ServiceFileName)}" }
|
||||
ProcessBuilder("systemctl", "daemon-reload").start().waitFor()
|
||||
Logger.info { "已完成systemd守护进程重载" }
|
||||
ProcessBuilder("systemctl", "enable", ServiceFileName.toString()).start().waitFor()
|
||||
Logger.info { "已启用systemd服务: $ServiceFileName" }
|
||||
ProcessBuilder("systemctl", "start", ServiceFileName.toString()).start().waitFor()
|
||||
Logger.info { "已启动systemd服务: $ServiceFileName" }
|
||||
} else if (CurrentOS.startsWith("Windows")) {
|
||||
Logger.error{"只有Linux才能使用systemd,但是JVM报告的当前系统是$CurrentOS"}
|
||||
throw IllegalStateException("Windows系统不支持systemd服务管理,暂不支持在Windows上自动管理服务。") }
|
||||
exitProcess(0)
|
||||
} else if (Argument in listOf("uninstall", "--uninstall")) {
|
||||
Logger.info { "开始卸载systemd服务" }
|
||||
Logger.debug { "SystemProperties.os.name: $CurrentOS" }
|
||||
if (CurrentOS.startsWith("Windows")) {
|
||||
Logger.error { "只有Linux才能使用systemd,但是JVM报告的当前系统是$CurrentOS" }
|
||||
throw IllegalStateException("Windows系统不支持systemd服务管理,暂不支持在Windows上自动管理服务。")
|
||||
} else if (CurrentOS.startsWith("Linux")) {
|
||||
if (CurrentUser != "root") {
|
||||
Logger.error { "只有root才有权在系统层级卸载systemd服务,但是JVM报告的当前用户是$CurrentUser" }
|
||||
throw IllegalStateException("请使用root用户运行此命令以卸载systemd服务") }
|
||||
ProcessBuilder("systemctl", "stop", ServiceFileName.toString()).start().waitFor()
|
||||
Logger.info { "已停止systemd服务: $ServiceFileName" }
|
||||
ProcessBuilder("systemctl", "disable", ServiceFileName.toString()).start().waitFor()
|
||||
Logger.info { "已禁用systemd服务: $ServiceFileName" }
|
||||
Files.deleteIfExists(ServiceFileDirectory.resolve(ServiceFileName))
|
||||
Logger.info { "成功删除systemd服务文件: ${ServiceFileDirectory.resolve(ServiceFileName)}" }
|
||||
Files.deleteIfExists(WorkingDirectory.resolve(ServiceFileName))
|
||||
Logger.info { "成功删除程序旁的服务文件: ${WorkingDirectory.resolve(ServiceFileName)}" }
|
||||
Files.deleteIfExists(SymbolicLinkPath)
|
||||
Logger.info { "成功删除指向程序自身的软链接: $SymbolicLinkPath" }
|
||||
ProcessBuilder("systemctl", "daemon-reload").start().waitFor()
|
||||
Logger.info { "已完成systemd守护进程重载" } }
|
||||
exitProcess(0) } } }
|
||||
@@ -0,0 +1,17 @@
|
||||
package xyz.thewhitedog9487.WebAPI
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import reactor.core.publisher.Hooks
|
||||
import xyz.thewhitedog9487.WebAPI.Startup.ProcessArguments
|
||||
|
||||
@SpringBootApplication
|
||||
class WebApiApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
Hooks.enableAutomaticContextPropagation()
|
||||
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL)
|
||||
ProcessArguments(args)
|
||||
runApplication<WebApiApplication>(*args)
|
||||
}
|
||||
@@ -19,9 +19,14 @@ spring:
|
||||
username:
|
||||
password:
|
||||
url: jdbc:sqlite:SQLiteDataBase.db
|
||||
servlet:
|
||||
encoding:
|
||||
charset: UTF-8
|
||||
force: true
|
||||
|
||||
server:
|
||||
port: 36987
|
||||
|
||||
logging:
|
||||
pattern:
|
||||
level: "%5p %X{RequestID}"
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package xyz.thewhitedog9487.WebAPI;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class WebApiApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package xyz.thewhitedog9487.WebAPI
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
|
||||
@SpringBootTest
|
||||
class WebApiApplicationTests {
|
||||
|
||||
@Test
|
||||
fun contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user