diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 9cc0ba66..f1d7ec25 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.head_ref }} # checkout the correct branch name fetch-depth: 0 # fetch the whole repo history @@ -39,10 +39,10 @@ jobs: major-identifier: "breaking:" minor-identifier: "feature:" - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: - distribution: zulu - java-version: 17 + distribution: temurin + java-version: 21 # - name: Startup Mongodb # uses: supercharge/mongodb-github-action@1.8.0 @@ -52,11 +52,11 @@ jobs: - name: Remove aliyun maven mirror run: | - sed -i '/maven {/,+2d' build.gradle + sed -i '/maven(url/d' build.gradle.kts - name: Set versions run: | - sed -i "s/^version.*$/version '${{ steps.version.outputs.version }}'/g" build.gradle + sed -i "s/^version.*$/version = \"${{ steps.version.outputs.version }}\"/g" build.gradle.kts sed -i 's/"packageVersion.*,/"packageVersion": "${{ steps.version.outputs.version }}",/g' client-config/cpp.json sed -i 's/"packageVersion.*,/"packageVersion": "${{ steps.version.outputs.version }}",/g' client-config/csharp-netcore.json sed -i 's/"packageVersion.*,/"packageVersion": "${{ steps.version.outputs.version }}",/g' client-config/rust.json @@ -71,31 +71,31 @@ jobs: run: ./gradlew generateSwaggerCode - name: upload openapi - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: openapi-${{ steps.version.outputs.version }} path: ./build/docs/swagger.json - name: upload cpp client - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: cpp-client path: ./build/clients/cpp-client/* - name: upload csharp client - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: csharp-client path: ./build/clients/csharp-client/* - name: upload rust client - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: rust-client path: ./build/clients/rust-client/* - name: upload ts client - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ts-client path: ./build/clients/ts-fetch-client/* \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 94cd5841..688ab1e0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.head_ref }} # checkout the correct branch name fetch-depth: 0 # fetch the whole repo history @@ -42,30 +42,30 @@ jobs: export TRUNCATED_GITHUB_SHA=$(echo ${{ github.sha }} | cut -c1-7); echo "VERSION_TAG=${GITHUB_REF/refs\/heads\//}-${TRUNCATED_GITHUB_SHA}" >> $GITHUB_ENV - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: - distribution: zulu - java-version: 17 + distribution: temurin + java-version: 21 - name: Remove aliyun maven mirror run: | - sed -i '/maven {/,+2d' build.gradle + sed -i '/maven(url/d' build.gradle.kts - name: Set Java version run: | - sed -i "s/^version.*$/version '${{ steps.version.outputs.version }}'/g" build.gradle + sed -i "s/^version.*$/version = \"${{ steps.version.outputs.version }}\"/g" build.gradle.kts - name: Build jar run: ./gradlew bootJar - name: Upload jar - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: jar path: ./build/libs/* - name: Log in to the Container registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -73,17 +73,18 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + type=raw,value=dev,enable=${{ github.ref == format('refs/heads/{0}', 'dev') }} type=raw,value={{branch}}-{{sha}} - name: Build and push Docker image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . push: true diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 16678a4a..037b9056 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -23,35 +23,33 @@ jobs: runs-on: ubuntu-latest steps: #checkout代码 - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 #安装graalvm - name: 安装graalvm uses: graalvm/setup-graalvm@v1 with: version: 'latest' - java-version: '17' - components: 'native-image' + java-version: '21' #查看版本信息 - name: 查看版本信息 run: | echo "GRAALVM_HOME: $GRAALVM_HOME" echo "JAVA_HOME: $JAVA_HOME" java --version - gu --version native-image --version #校验Gradle wrapper - name: 校验Gradle wrapper - uses: gradle/wrapper-validation-action@v1 + uses: gradle/wrapper-validation-action@v2 #使用Gradle编译项目 - name: 使用Gradle编译项目 - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v3 with: arguments: bootBuildImage # 登录docker仓库 - name: 登录docker仓库 ${{ env.REGISTRY }} if: github.event_name != 'pull_request' - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ secrets.DOCKER_USERNAME }} diff --git a/Dockerfile b/Dockerfile index 0e3fb809..b6abd98f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -FROM amazoncorretto:17-alpine as runner +FROM amazoncorretto:21-alpine as runner WORKDIR /app -COPY ./build/libs/MaaBackendCenter*.jar /MaaBackendCenter.jar +COPY ./build/libs/MaaBackendCenter*.jar /app/app.jar EXPOSE 7000-9000 -ENTRYPOINT ["java", "-jar", "/MaaBackendCenter.jar"] +ENTRYPOINT ["java", "-jar", "app.jar", "${JAVA_OPTS}"] diff --git a/README.md b/README.md index 389347e5..36ee338d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # MaaBackendCenter -使用 Java 重写的 MAA 作业服务器后端 +使用 Kotlin(Java) 重写的 MAA 作业服务器后端 ## 开发技术栈 -- Java 17 +- kotlin 1.9 (Java 21) - SpringBoot 3 - spring-security - springdoc-openapi @@ -12,31 +12,30 @@ - Redis ## 本地开发指南 - -1. 下载安装 jdk 17 或者以上版本的 jdk,可以考虑从 [zuluJDK](https://www.azul.com/downloads/?version=java-17-lts&package=jdk) 或者 [libreicaJDK](https://bell-sw.com/pages/downloads/#/java-17-lts) 下载安装 -2. 你需要一个有redis和mongoDB的环境,如果你是windows用户,可以从 https://github.com/tporadowski/redis 中下载版本较旧的 redis 使用 -3. 使用你喜欢的 IDE 导入此项目,修改 /src/main/resources/application-template.yml 中的数据库配置以符合你自己配置的环境 -4. 运行 MainApplication 类里的 main 方法 -5. 首次运行建议修改 ArkLevelSyncTask 类的scheduled注解的参数,这样可以将明日方舟中的关卡数据同步到你本地的 mongodb 中,为了防止反复调用造成调试的麻烦,建议首次运行同步成功后再将代码修改回去 +1. 使用你喜欢的 IDE 导入此项目,修改 /src/main/resources/application-template.yml 中的数据库配置以符合你自己配置的环境 +2. 下载安装 JDK 21 或者以上版本的 jdk,可以考虑从 [zuluJDK](https://www.azul.com/downloads/?version=java-17-lts&package=jdk) 或者 [libreicaJDK](https://bell-sw.com/pages/downloads/#/java-17-lts) 下载安装。 Jetbrains Idea 可以使用自带的 JDK 管理器进行下载。 +3. 你需要一个有redis和mongoDB的环境,如果你是windows用户,可以从 https://github.com/tporadowski/redis 中下载版本较旧的 redis 使用。 您也可以直接使用 `./dev-docker/docker-compose.yml` 来启动 docker 服务。 +4. 运行 `./gradlew bootRun`, windows 环境为 `./gradlew.bat bootRun` +5. 首次运行建议修改 ArkLevelSyncTask 类的 scheduled 注解的参数,这样可以将明日方舟中的关卡数据同步到你本地的 mongodb 中,为了防止反复调用造成调试的麻烦,建议首次运行同步成功后再将代码修改回去 ## 项目结构 -- config 存放spring配置 -- controller 交互层 - - request 入参类型 - - response 响应类型 -- repository 数据仓库层,用于和数据库交互 - - entity 与数据库字段对应的类型 -- service 业务处理层,复杂或者公用逻辑放在这里(注:您无需为每个类型都提供对应接口,只有当接口在可见未来有多个实现的时候才考虑建立接口) - - model 应用内传输用类型放这里 -- utils 工具类 +- config # 存放 spring 配置 +- common # 共享的逻辑 +- controller # 交互层 + - request # 入参类型 + - response # 响应类型 +- repository # 数据仓库层,用于和数据库交互 + - entity # 与数据库字段对应的类型 +- service # 业务处理层,复杂或者公用逻辑放在这里(注:您无需为每个类型都提供对应接口,只有当接口在可见未来有多个实现的时候才考虑建立接口) + - model # 应用内传输用类型放这里 ## 编译与部署 -1. 安装 jdk17,可以考虑从 [zuluJDK](https://www.azul.com/downloads/?version=java-17-lts&package=jdk) 或者 [libreicaJDK](https://bell-sw.com/pages/downloads/#/java-17-lts) 下载 +1. 安装 JDK 21,可以考虑从 [zuluJDK](https://www.azul.com/downloads/?version=java-17-lts&package=jdk) 或者 [libreicaJDK](https://bell-sw.com/pages/downloads/#/java-17-lts) 下载 2. clone 此项目 `git clone https://github.com/MaaAssistantArknights/MaaBackendCenter.git` 3. 进入此项目目录 `cd MaaBackendCenter` -4. 编译项目 `./gradlew bootJar -x processAot`,windows环境下请使用 `gradlew.bat bootJar -x processAot` +4. 编译项目 `./gradlew bootJar -x processAot`,windows 环境下请使用 `gradlew.bat bootJar -x processAot` 5. 获得编译后的 jar 文件 `cp ./build/libs/MaaBackendCenter-1.0-SNAPSHOT.jar .` 6. 复制一份配置文件 `cp ./build/resources/main/application-template.yml ./application-prod.yml` 7. 修改配置文件 `application-prod.yml` @@ -45,7 +44,7 @@ ## native 编译(暂时废弃,如果希望协助维护,请查看native分支) 1. 安装 [GraalVM](https://github.com/graalvm/graalvm-ce-builds/releases) - Java17,并配置好环境变量,部分功能需要正确配置 `JAVA_HOME` 变量为 GraalVM 安装目录才能正常使用 + Java21,并配置好环境变量,部分功能需要正确配置 `JAVA_HOME` 变量为 GraalVM 安装目录才能正常使用 2. 如果您处于 Windows 环境下,需要安装 `Visual Studio` 并且安装 C++ 组件,Linux 环境下则需要安装 `gcc` 工具链,Mac 下需要安装 `xcode` 工具链,详情查看 [native-image#prerequisites](https://www.graalvm.org/22.3/reference-manual/native-image/#prerequisites) diff --git a/build.gradle b/build.gradle deleted file mode 100644 index c39cce5d..00000000 --- a/build.gradle +++ /dev/null @@ -1,129 +0,0 @@ -plugins { - id 'java' - id 'org.springframework.boot' version '3.1.5' - id 'io.spring.dependency-management' version '1.1.3' - id 'io.freefair.lombok' version '8.4' - id 'org.springdoc.openapi-gradle-plugin' version '1.8.0' - id 'org.hidetake.swagger.generator' version '2.19.2' - id 'org.graalvm.buildtools.native' version '0.9.28' -} - -group 'plus.maa' -version '1.0-SNAPSHOT' - -repositories { - maven { - url 'https://maven.aliyun.com/repository/public/' - } - maven { - url 'https://maven.aliyun.com/repository/spring/' - } - mavenCentral() -} - -ext { - // 统一管理版本号 - hutoolVersion = '5.8.22' -} - -dependencies { - - annotationProcessor 'com.github.therapi:therapi-runtime-javadoc-scribe:0.13.0' - annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final' - annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' - - implementation 'org.springframework.boot:spring-boot-starter-test' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-webflux' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-data-redis' - implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-cache' - //springdoc相关依赖没有被自动管理,必须保留版本号 - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' - implementation 'com.github.therapi:therapi-runtime-javadoc:0.13.0' - - implementation 'com.squareup.okhttp3:okhttp:4.10.0' - implementation 'com.sun.mail:javax.mail:1.6.2' - // 双引号才能使用变量 - implementation "cn.hutool:hutool-extra:$hutoolVersion" - implementation "cn.hutool:hutool-jwt:$hutoolVersion" - implementation "cn.hutool:hutool-dfa:$hutoolVersion" - - implementation 'org.mapstruct:mapstruct:1.5.5.Final' - implementation 'org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r' - implementation 'org.eclipse.jgit:org.eclipse.jgit.ssh.apache.agent:6.6.0.202305301015-r' - implementation 'org.freemarker:freemarker:2.3.32' - implementation 'com.github.ben-manes.caffeine:caffeine:3.1.6' - implementation 'com.github.erosb:everit-json-schema:1.14.2' - implementation 'com.google.guava:guava:32.1.1-jre' - implementation 'org.aspectj:aspectjweaver:1.9.19' - implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' - - swaggerCodegen 'org.openapitools:openapi-generator-cli:6.5.0' - -} - -test { - useJUnitPlatform() -} - -def swagger_output_dir = "$buildDir/docs" -def swagger_output_name = 'swagger.json' - - -openApi { - apiDocsUrl.set("http://localhost:8848/v3/api-docs") - outputDir.set(file(swagger_output_dir)) - outputFileName.set(swagger_output_name) - waitTimeInSeconds.set(30) -} - -swaggerSources { - def client_dir = "$buildDir/clients" - TsFetch { - inputFile = file("$swagger_output_dir/$swagger_output_name") - code { - language = 'typescript-fetch' - configFile = file('client-config/ts-fetch.json') -// templateDir = file('client-config/typescript-fetch') - rawOptions = ["-e", "mustache"] - outputDir = file("$client_dir/ts-fetch-client") - } - } - CSharp { - inputFile = file("$swagger_output_dir/$swagger_output_name") - code { - language = 'csharp-netcore' - configFile = file('client-config/csharp-netcore.json') - outputDir = file("$client_dir/csharp-client") -// rawOptions = [ -// "--type-mappings", "binary=System.IO.Stream" -// ] - } - } - Cpp { - inputFile = file("$swagger_output_dir/$swagger_output_name") - code { - language = 'cpp-restsdk' - configFile = file('client-config/cpp.json') - outputDir = file("$client_dir/cpp-client") - } - } - Rust { - inputFile = file("$swagger_output_dir/$swagger_output_name") - code { - language = 'rust' - configFile = file('client-config/rust.json') - outputDir = file("$client_dir/rust-client") - } - } -} - -rootProject.afterEvaluate(){ - def forkedSpringBootRun = project.tasks.named("forkedSpringBootRun") - forkedSpringBootRun.configure { - doNotTrackState("See https://github.com/springdoc/springdoc-openapi-gradle-plugin/issues/102") - } -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..7c424e37 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,167 @@ +import org.hidetake.gradle.swagger.generator.GenerateSwaggerCode +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + java + id("org.springframework.boot") version "3.2.2" + id("io.spring.dependency-management") version "1.1.4" + id("org.springdoc.openapi-gradle-plugin") version "1.8.0" +// id("org.graalvm.buildtools.native") version "0.9.28" + id("org.hidetake.swagger.generator") version "2.19.2" + id("com.gorylenko.gradle-git-properties") version "2.4.1" + + kotlin("jvm") version "1.9.22" + kotlin("plugin.spring") version "1.9.22" + kotlin("kapt") version "1.9.22" +} + +group = "plus.maa" +version = "2.0" + +java { + sourceCompatibility = JavaVersion.VERSION_21 +} + +repositories { + maven(url = "https://maven.aliyun.com/repository/public/") + maven(url = "https://maven.aliyun.com/repository/spring/") + mavenCentral() +} + + +dependencies { + val hutoolVersion = "5.8.26" + val mapstructVersion = "1.5.5.Final" + + kapt("org.springframework.boot:spring-boot-configuration-processor") + + testImplementation("io.mockk:mockk:1.13.9") + testImplementation("org.springframework.boot:spring-boot-starter-test") + + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-webflux") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-data-redis") + implementation("org.springframework.boot:spring-boot-starter-data-mongodb") + implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.springframework.boot:spring-boot-starter-cache") + // springdoc 相关依赖没有被自动管理,必须保留版本号, + // springdoc-openapi-starter-webmvc-ui 升级到 2.3.0 以及以上版本会导致 therapi 不兼容 + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0") + implementation("com.github.therapi:therapi-runtime-javadoc:0.15.0") + kapt("com.github.therapi:therapi-runtime-javadoc-scribe:0.15.0") + + // kotlin + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") + + // kotlin-logging + implementation("io.github.oshai:kotlin-logging-jvm:6.0.3") + + implementation("com.squareup.okhttp3:okhttp:4.12.0") + // hutool 的邮箱工具类依赖 + implementation("com.sun.mail:javax.mail:1.6.2") + implementation("cn.hutool:hutool-extra:$hutoolVersion") + implementation("cn.hutool:hutool-jwt:$hutoolVersion") + implementation("cn.hutool:hutool-dfa:$hutoolVersion") + + // mapstruct + implementation("org.mapstruct:mapstruct:${mapstructVersion}") + kapt("org.mapstruct:mapstruct-processor:${mapstructVersion}") + + implementation("org.eclipse.jgit:org.eclipse.jgit:6.8.0.202311291450-r") + implementation("org.eclipse.jgit:org.eclipse.jgit.ssh.apache.agent:6.8.0.202311291450-r") + implementation("org.freemarker:freemarker:2.3.32") + implementation("com.github.ben-manes.caffeine:caffeine:3.1.8") + implementation("com.github.erosb:everit-json-schema:1.14.4") { + exclude("commons-logging", "commons-logging") + } + implementation("com.google.guava:guava:32.1.3-jre") + implementation("org.aspectj:aspectjweaver:1.9.21") + + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + + swaggerCodegen("org.openapitools:openapi-generator-cli:7.2.0") + +} + +kapt { + keepJavacAnnotationProcessors = true +} + +tasks.withType { + kotlinOptions { + freeCompilerArgs += "-Xjsr305=strict" + jvmTarget = "21" + } +} + +tasks.withType { + useJUnitPlatform() +} + + +val swaggerOutputDir = layout.buildDirectory.dir("docs") +val swaggerOutputName = "swagger.json" + + +openApi { + apiDocsUrl = "http://localhost:8848/v3/api-docs" + outputDir = swaggerOutputDir + outputFileName = swaggerOutputName + waitTimeInSeconds = 30 +} + +swaggerSources { + val clientDir = layout.buildDirectory.dir("clients").get() + val swaggerOutputFile = swaggerOutputDir.get().file(swaggerOutputName) + create("TsFetch") { + setInputFile(file(swaggerOutputFile)) + code(closureOf { + language = "typescript-fetch" + configFile = file("client-config/ts-fetch.json") +// templateDir = file('client-config/typescript-fetch') + rawOptions = listOf("-e", "mustache") + outputDir = file(clientDir.dir("ts-fetch-client")) + }) + } + create("CSharp") { + setInputFile(file(swaggerOutputFile)) + code(closureOf { + language = "csharp" + configFile = file("client-config/csharp-netcore.json") + outputDir = file(clientDir.dir("csharp-client")) +// rawOptions = listOf("--type-mappings", "binary=System.IO.Stream") + }) + } + create("Cpp") { + setInputFile(file(swaggerOutputFile)) + code(closureOf { + language = "cpp-restsdk" + configFile = file("client-config/cpp.json") + outputDir = file(clientDir.dir("cpp-client")) + }) + } + create("Rust") { + setInputFile(file(swaggerOutputFile)) + code(closureOf { + language = "rust" + configFile = file("client-config/rust.json") + outputDir = file(clientDir.dir("rust-client")) + }) + } +} + +tasks { + forkedSpringBootRun { + doNotTrackState("See https://github.com/springdoc/springdoc-openapi-gradle-plugin/issues/102") + } +} + + +gitProperties { + failOnNoGitDirectory = false + keys = listOf("git.branch", "git.commit.id", "git.commit.id.abbrev", "git.commit.time") +} diff --git a/dev-docker/test-docker/docker-compose.yml b/dev-docker/test-docker/docker-compose.yml index f4da9ca3..442c915d 100644 --- a/dev-docker/test-docker/docker-compose.yml +++ b/dev-docker/test-docker/docker-compose.yml @@ -15,7 +15,7 @@ services: networks: - maa maa_backend: - image: dragove/maa-backend-center:latest + image: ghcr.io/maaassistantarknights/maabackendcenter:dev container_name: maa-backend environment: - SPRING_DATA_MONGODB_URI=mongodb://mongo/MaaBackend diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832..033e24c4 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 17a8ddce..a80b22ce 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c7873..fcb6fca1 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,10 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +130,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,6 +197,10 @@ if "$cygwin" || "$msys" ; then done fi + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in @@ -205,6 +213,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 107acd32..93e3f59f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +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! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +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 diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 143ed513..00000000 --- a/settings.gradle +++ /dev/null @@ -1,2 +0,0 @@ -rootProject.name = 'MaaBackendCenter' - diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..6e011c5f --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "MaaBackendCenter" diff --git a/src/main/java/plus/maa/backend/MainApplication.java b/src/main/java/plus/maa/backend/MainApplication.java deleted file mode 100644 index 20683a8d..00000000 --- a/src/main/java/plus/maa/backend/MainApplication.java +++ /dev/null @@ -1,26 +0,0 @@ -package plus.maa.backend; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.context.properties.ConfigurationPropertiesScan; -import org.springframework.boot.web.servlet.ServletComponentScan; -import org.springframework.cache.annotation.EnableCaching; -import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; - -/** - * @author AnselYuki - */ -@EnableAsync -@EnableCaching -@EnableScheduling -@SpringBootApplication -@ConfigurationPropertiesScan -@EnableMethodSecurity -@ServletComponentScan -public class MainApplication { - public static void main(String[] args) { - SpringApplication.run(MainApplication.class, args); - } -} diff --git a/src/main/java/plus/maa/backend/common/annotation/AccessLimit.java b/src/main/java/plus/maa/backend/common/annotation/AccessLimit.java deleted file mode 100644 index ba8f7421..00000000 --- a/src/main/java/plus/maa/backend/common/annotation/AccessLimit.java +++ /dev/null @@ -1,22 +0,0 @@ -package plus.maa.backend.common.annotation; - -import java.lang.annotation.*; - -/** - * @author Baip1995 - */ -@Inherited -@Documented -@Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface AccessLimit { - /** - * 指定second 时间内,API最多的请求次数 - */ - int times() default 3; - - /** - * 指定时间second,redis数据过期时间 - */ - int second() default 10; -} diff --git a/src/main/java/plus/maa/backend/common/annotation/CurrentUser.java b/src/main/java/plus/maa/backend/common/annotation/CurrentUser.java deleted file mode 100644 index 7adaa215..00000000 --- a/src/main/java/plus/maa/backend/common/annotation/CurrentUser.java +++ /dev/null @@ -1,17 +0,0 @@ -package plus.maa.backend.common.annotation; - -import org.springframework.security.core.annotation.AuthenticationPrincipal; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * @author john180 - */ -@Target({ElementType.PARAMETER}) -@Retention(RetentionPolicy.RUNTIME) -@AuthenticationPrincipal -public @interface CurrentUser { -} diff --git a/src/main/java/plus/maa/backend/common/annotation/JsonSchema.java b/src/main/java/plus/maa/backend/common/annotation/JsonSchema.java deleted file mode 100644 index 891d2147..00000000 --- a/src/main/java/plus/maa/backend/common/annotation/JsonSchema.java +++ /dev/null @@ -1,16 +0,0 @@ -package plus.maa.backend.common.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * @author LoMu - * Date 2023-01-22 17:49 - */ - -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface JsonSchema { -} diff --git a/src/main/java/plus/maa/backend/common/annotation/SensitiveWordDetection.java b/src/main/java/plus/maa/backend/common/annotation/SensitiveWordDetection.java deleted file mode 100644 index 7999de92..00000000 --- a/src/main/java/plus/maa/backend/common/annotation/SensitiveWordDetection.java +++ /dev/null @@ -1,23 +0,0 @@ -package plus.maa.backend.common.annotation; - -import java.lang.annotation.*; - -/** - * 敏感词检测注解
- * 用于方法上,标注该方法需要进行敏感词检测
- * 通过 SpEL 表达式获取方法参数 - * - * @author lixuhuilll - * Date: 2023-08-25 18:50 - */ - -@Documented -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface SensitiveWordDetection { - - /** - * SpEL 表达式 - */ - String[] value() default {}; -} diff --git a/src/main/java/plus/maa/backend/common/aop/JsonSchemaAop.java b/src/main/java/plus/maa/backend/common/aop/JsonSchemaAop.java deleted file mode 100644 index 60cbfcbb..00000000 --- a/src/main/java/plus/maa/backend/common/aop/JsonSchemaAop.java +++ /dev/null @@ -1,89 +0,0 @@ -package plus.maa.backend.common.aop; - - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.aspectj.lang.JoinPoint; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Before; -import org.aspectj.lang.annotation.Pointcut; -import org.everit.json.schema.Schema; -import org.everit.json.schema.ValidationException; -import org.everit.json.schema.loader.SchemaLoader; -import org.json.JSONObject; -import org.json.JSONTokener; -import org.springframework.core.io.ClassPathResource; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Component; -import plus.maa.backend.common.annotation.JsonSchema; -import plus.maa.backend.controller.request.comments.CommentsRatingDTO; -import plus.maa.backend.controller.request.copilot.CopilotCUDRequest; -import plus.maa.backend.controller.request.copilot.CopilotRatingReq; -import plus.maa.backend.controller.response.MaaResultException; - -import java.io.IOException; -import java.io.InputStream; - - -/** - * @author LoMu - * Date 2023-01-22 17:53 - */ - -@Component -@Aspect -@Slf4j -@RequiredArgsConstructor -public class JsonSchemaAop { - private final ObjectMapper mapper; - private static final String COPILOT_SCHEMA_JSON = "static/templates/maa-copilot-schema.json"; - private static final String RATING_SCHEMA_JSON = "static/templates/maa-rating-schema.json"; - - @Pointcut("@annotation(plus.maa.backend.common.annotation.JsonSchema)") - public void pt() { - } - - /** - * 数据校验 - * - * @param joinPoint 形参 - * @param jsonSchema 注解 - */ - @Before("pt() && @annotation(jsonSchema)") - public void before(JoinPoint joinPoint, JsonSchema jsonSchema) { - String schema_json = null; - String content = null; - //判断是验证的是Copilot还是Rating - for (Object arg : joinPoint.getArgs()) { - if (arg instanceof CopilotCUDRequest) { - content = ((CopilotCUDRequest) arg).getContent(); - schema_json = COPILOT_SCHEMA_JSON; - } - if (arg instanceof CopilotRatingReq || arg instanceof CommentsRatingDTO) { - try { - schema_json = RATING_SCHEMA_JSON; - content = mapper.writeValueAsString(arg); - } catch (JsonProcessingException e) { - log.error("json解析失败", e); - } - } - } - if (content == null) return; - - - //获取json schema json路径并验证 - try (InputStream inputStream = new ClassPathResource(schema_json).getInputStream()) { - JSONObject json = new JSONObject(content); - JSONObject jsonObject = new JSONObject(new JSONTokener(inputStream)); - Schema schema = SchemaLoader.load(jsonObject); - schema.validate(json); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (ValidationException e) { - log.warn("schema Location: {}", e.getViolatedSchema().getSchemaLocation()); - throw new MaaResultException(HttpStatus.BAD_REQUEST.value(), "数据不符合规范,请前往前端作业编辑器进行操作"); - } - } -} \ No newline at end of file diff --git a/src/main/java/plus/maa/backend/common/aop/SensitiveWordAop.java b/src/main/java/plus/maa/backend/common/aop/SensitiveWordAop.java deleted file mode 100644 index ece2a309..00000000 --- a/src/main/java/plus/maa/backend/common/aop/SensitiveWordAop.java +++ /dev/null @@ -1,91 +0,0 @@ -package plus.maa.backend.common.aop; - -import cn.hutool.dfa.WordTree; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.aspectj.lang.JoinPoint; -import org.aspectj.lang.Signature; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Before; -import org.aspectj.lang.reflect.MethodSignature; -import org.jetbrains.annotations.Nullable; -import org.springframework.core.DefaultParameterNameDiscoverer; -import org.springframework.expression.EvaluationContext; -import org.springframework.expression.Expression; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Component; -import plus.maa.backend.common.annotation.SensitiveWordDetection; -import plus.maa.backend.controller.response.MaaResultException; - -import java.util.List; - -/** - * 敏感词处理程序
- * - * @author lixuhuilll - * Date: 2023-08-25 18:50 - */ - -@Slf4j -@Aspect -@Component -@RequiredArgsConstructor -public class SensitiveWordAop { - - // 敏感词库 - private final WordTree wordTree; - - private final ObjectMapper objectMapper; - - // SpEL 表达式解析器 - private final SpelExpressionParser parser = new SpelExpressionParser(); - - // 用于获取方法参数名 - private final DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer(); - - @Nullable - public Object getObjectBySpEL(String spELString, JoinPoint joinPoint) { - // 获取被注解方法 - Signature signature = joinPoint.getSignature(); - if (!(signature instanceof MethodSignature methodSignature)) { - return null; - } - // 获取方法参数名数组 - String[] paramNames = nameDiscoverer.getParameterNames(methodSignature.getMethod()); - // 解析 Spring 表达式对象 - Expression expression = parser.parseExpression(spELString); - // Spring 表达式上下文对象 - EvaluationContext context = new StandardEvaluationContext(); - // 通过 joinPoint 获取被注解方法的参数 - Object[] args = joinPoint.getArgs(); - // 给上下文赋值 - for (int i = 0; i < args.length; i++) { - if (paramNames != null) { - context.setVariable(paramNames[i], args[i]); - } - } - context.setVariable("objectMapper", objectMapper); - // 表达式从上下文中计算出实际参数值 - return expression.getValue(context); - } - - @Before("@annotation(annotation)") // 处理 SensitiveWordDetection 注解 - public void before(JoinPoint joinPoint, SensitiveWordDetection annotation) { - // 获取 SpEL 表达式 - String[] expressions = annotation.value(); - for (String expression : expressions) { - // 解析 SpEL 表达式 - Object value = getObjectBySpEL(expression, joinPoint); - // 校验 - if (value instanceof String text) { - List matchAll = wordTree.matchAll(text); - if (matchAll != null && !matchAll.isEmpty()) { - throw new MaaResultException(HttpStatus.BAD_REQUEST.value(), "包含敏感词:" + matchAll); - } - } - } - } -} diff --git a/src/main/java/plus/maa/backend/common/bo/EmailBusinessObject.java b/src/main/java/plus/maa/backend/common/bo/EmailBusinessObject.java deleted file mode 100644 index 53094885..00000000 --- a/src/main/java/plus/maa/backend/common/bo/EmailBusinessObject.java +++ /dev/null @@ -1,196 +0,0 @@ -package plus.maa.backend.common.bo; - - -import cn.hutool.extra.mail.MailAccount; -import cn.hutool.extra.mail.MailUtil; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.experimental.Accessors; -import lombok.extern.slf4j.Slf4j; -import plus.maa.backend.common.utils.FreeMarkerUtils; - -import java.io.File; -import java.util.*; - - -/** - * @author LoMu - * Date 2022-12-23 23:57 - */ -@Slf4j -@Accessors(chain = true) -@Setter -@NoArgsConstructor -public class EmailBusinessObject { - - // 默认邮件模板 - private static final String DEFAULT_MAIL_TEMPLATE = "mail.ftlh"; - - - private static final String DEFAULT_MAIL_INCLUDE_HTML_TEMPLATE = "mail-includeHtml.ftlh"; - - private static final String DEFAULT_TITLE_PREFIX = "Maa Backend Center"; - - //发件人信息 - private MailAccount mailAccount; - - private List emailList = new ArrayList<>(); - - // 自定义标题 - private String title = DEFAULT_TITLE_PREFIX; - - // 邮件内容 - private String message; - - // html标签是否被识别使用 - private Boolean isHtml = true; - - - /** - * 静态创建工厂 - * - * @return EmailBusinessObject - */ - public static EmailBusinessObject builder() { - return new EmailBusinessObject(); - } - - - public EmailBusinessObject setEmail(String email) { - emailList.add(email); - return this; - } - - /** - * 设置邮件标题 默认为 Maa Backend Center - * - * @param title 标题 - */ - public EmailBusinessObject setTitle(String title) { - this.title = title; - return this; - } - - /** - * 发送自定义信息 - * - * @param content 邮件动态内容 - * @param templateName ftlh名称,例如 mail.ftlh - */ - public void sendCustomStaticTemplates(String content, String templateName) { - sendCustomStaticTemplatesFiles(content, templateName, (File[]) null); - } - - /** - * 通过默认模板发送自定义Message内容 - */ - public void sendCustomMessage() { - sendCustomStaticTemplates(message, DEFAULT_MAIL_TEMPLATE); - } - - /** - * 通过默认模板发送自定义Message内容和附件 - * - * @param files 附件 - */ - public void sendCustomMessageFiles(File... files) { - sendCustomStaticTemplatesFiles(message, DEFAULT_MAIL_TEMPLATE, files); - } - - - /** - * 发送自定义带文件的邮件 - * - * @param content 邮件动态内容 - * @param templateName ftl路径 - * @param files 附件 - */ - public void sendCustomStaticTemplatesFiles(String content, String templateName, File... files) { - try { - log.info("send email to: {}, templateName: {}, content: {}", emailList, templateName, content); - send(this.mailAccount, emailList, title, parseMessages(content, templateName), isHtml, files); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - } - - - /** - * 发送验证码 - */ - public void sendVerificationCodeMessage(String code) { - - try { - send(this.mailAccount, this.emailList - , this.title + " 验证码" - , defaultMailIncludeHtmlTemplates( - "mail-vCode.ftlh", code - ) - , this.isHtml - ); - } catch (Exception ex) { - throw new RuntimeException("邮件发送失败", ex); - } - } - - public void sendCommentNotification(Map map) { - try { - send(this.mailAccount, - this.emailList, - this.title, - defaultMailIncludeHtmlTemplates("mail-comment-notification.ftlh", map), - this.isHtml - ); - } catch (Exception ex) { - throw new RuntimeException("邮件发送失败", ex); - } - } - - private String defaultMailIncludeHtmlTemplates(String content, String obj) { - return parseMessages(content, obj, DEFAULT_MAIL_INCLUDE_HTML_TEMPLATE); - } - - private String defaultMailIncludeHtmlTemplates(String content, Map map) { - return parseMessages(content, DEFAULT_MAIL_INCLUDE_HTML_TEMPLATE, map); - } - - - /** - * @param content 自定义内容 - * @param templateName ftlh路径 - * @return String - */ - private String parseMessages(String content, String templateName) { - return FreeMarkerUtils.parseData(Collections.singletonMap("content", content), templateName); - } - - /** - * ftlh多个参数下 - * - * @param content 邮件内嵌ftlh路径 - * @return String - */ - private String parseMessages(String content, String templateName, Map map) { - map.put("content", content); - return FreeMarkerUtils.parseData(map, templateName); - } - - private String parseMessages(String content, String obj, String templateName) { - return FreeMarkerUtils.parseData(Map.of("content", content, "obj", obj), templateName); - } - - - /** - * 发送邮件给多人 - * - * @param mailAccount 邮件帐户信息 - * @param tos 收件人列表 - * @param subject 标题 - * @param content 正文 - * @param isHtml 是否为HTML格式 - * @param files 附件列表 - */ - private void send(MailAccount mailAccount, Collection tos, String subject, String content, boolean isHtml, File... files) { - MailUtil.send(mailAccount, tos, null, null, subject, content, null, isHtml, files); - } -} diff --git a/src/main/java/plus/maa/backend/common/utils/FreeMarkerUtils.java b/src/main/java/plus/maa/backend/common/utils/FreeMarkerUtils.java deleted file mode 100644 index eaf7fa56..00000000 --- a/src/main/java/plus/maa/backend/common/utils/FreeMarkerUtils.java +++ /dev/null @@ -1,38 +0,0 @@ -package plus.maa.backend.common.utils; - -import freemarker.template.Configuration; -import freemarker.template.Template; -import freemarker.template.TemplateException; -import plus.maa.backend.controller.response.MaaResultException; - -import java.io.IOException; -import java.io.StringWriter; -import java.nio.charset.StandardCharsets; -import java.util.Locale; - -/** - * @author dragove - * created on 2023/1/17 - */ -public class FreeMarkerUtils { - - private static final Configuration cfg = new Configuration(Configuration.VERSION_2_3_32); - static { - cfg.setClassForTemplateLoading(FreeMarkerUtils.class, "/static/templates/ftlh"); - cfg.setEncoding(Locale.CHINA, StandardCharsets.UTF_8.name()); - } - - public static String parseData(Object dataModel, String templateName) { - try { - Template template = cfg.getTemplate(templateName); - StringWriter sw = new StringWriter(); - template.process(dataModel, sw); - return sw.toString(); - } catch (IOException e) { - throw new MaaResultException("获取freemarker模板失败"); - } catch (TemplateException e) { - throw new MaaResultException("freemarker模板处理失败"); - } - } - -} diff --git a/src/main/java/plus/maa/backend/common/utils/IpUtil.java b/src/main/java/plus/maa/backend/common/utils/IpUtil.java deleted file mode 100644 index fd27df99..00000000 --- a/src/main/java/plus/maa/backend/common/utils/IpUtil.java +++ /dev/null @@ -1,48 +0,0 @@ -package plus.maa.backend.common.utils; - -import java.net.InetAddress; -import java.net.UnknownHostException; - -import jakarta.servlet.http.HttpServletRequest; - -/** - * @Author leaves - * @Date 2023/1/20 14:33 - */ -public class IpUtil { - /** - * 获取登录用户IP地址 - * - * @param request - * @return - */ - public static String getIpAddr(HttpServletRequest request) { - String ip = request.getHeader("x-forwarded-for"); - if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("Proxy-Client-IP"); - } - if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("WL-Proxy-Client-IP"); - } - if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { - ip = request.getRemoteAddr(); - if (ip.equals("127.0.0.1")) { - //根据网卡取本机配置的IP - InetAddress inet = null; - try { - inet = InetAddress.getLocalHost(); - ip = inet.getHostAddress(); - } catch (UnknownHostException ignored) { - } - } - } - // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 - if(ip != null && ip.length() > 15){ - if(ip.indexOf(",") > 0){ - ip = ip.substring(0, ip.indexOf(",")); - } - } - return ip; - } - -} diff --git a/src/main/java/plus/maa/backend/common/utils/OkHttpUtils.java b/src/main/java/plus/maa/backend/common/utils/OkHttpUtils.java deleted file mode 100644 index 823c2339..00000000 --- a/src/main/java/plus/maa/backend/common/utils/OkHttpUtils.java +++ /dev/null @@ -1,28 +0,0 @@ -package plus.maa.backend.common.utils; - -import okhttp3.ConnectionPool; -import okhttp3.OkHttpClient; -import org.springframework.context.annotation.Bean; -import org.springframework.stereotype.Component; - -import java.util.concurrent.TimeUnit; - -/** - * @author john180 - */ -@Component -public class OkHttpUtils { - /** - * 缺省 OkHttpClient - * - * @return OkHttpClient - */ - @Bean - public OkHttpClient defaultOkHttpClient() { - return new OkHttpClient().newBuilder() - .connectTimeout(5, TimeUnit.SECONDS) - .readTimeout(5, TimeUnit.SECONDS) - .connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES)) - .build(); - } -} diff --git a/src/main/java/plus/maa/backend/common/utils/SpringUtil.java b/src/main/java/plus/maa/backend/common/utils/SpringUtil.java deleted file mode 100644 index a3d3d003..00000000 --- a/src/main/java/plus/maa/backend/common/utils/SpringUtil.java +++ /dev/null @@ -1,37 +0,0 @@ -package plus.maa.backend.common.utils; - -import org.springframework.beans.BeansException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Component; - -/** - * @Author leaves - * @Date 2023/1/20 19:14 - */ -@Component -@Lazy(false) -public class SpringUtil implements ApplicationContextAware { - private static ApplicationContext applicationContext = null; - public static ApplicationContext getApplicationContext(){return applicationContext;} - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - // TODO Auto-generated method stub - if(SpringUtil.applicationContext == null){ - SpringUtil.applicationContext = applicationContext; - } - } - - public static Object getBean(String name) { - return getApplicationContext().getBean(name); - } - - public static T getBean(Class clazz) { - return getApplicationContext().getBean(clazz); - } - - public static T getBean(String name, Class clazz) { - return getApplicationContext().getBean(name, clazz); - } -} diff --git a/src/main/java/plus/maa/backend/common/utils/WebUtils.java b/src/main/java/plus/maa/backend/common/utils/WebUtils.java deleted file mode 100644 index d66bc5a4..00000000 --- a/src/main/java/plus/maa/backend/common/utils/WebUtils.java +++ /dev/null @@ -1,21 +0,0 @@ -package plus.maa.backend.common.utils; - -import jakarta.servlet.http.HttpServletResponse; - -import java.io.IOException; - -/** - * @author AnselYuki - */ -public class WebUtils { - public static void renderString(HttpServletResponse response, String json, int code) { - try { - response.setStatus(code); - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); - response.getWriter().println(json); - } catch (IOException e) { - e.printStackTrace(); - } - } -} diff --git a/src/main/java/plus/maa/backend/common/utils/converter/ArkLevelConverter.java b/src/main/java/plus/maa/backend/common/utils/converter/ArkLevelConverter.java deleted file mode 100644 index f96ec923..00000000 --- a/src/main/java/plus/maa/backend/common/utils/converter/ArkLevelConverter.java +++ /dev/null @@ -1,21 +0,0 @@ -package plus.maa.backend.common.utils.converter; - -import org.mapstruct.Mapper; - -import plus.maa.backend.controller.response.copilot.ArkLevelInfo; -import plus.maa.backend.repository.entity.ArkLevel; - -import java.util.List; - -/** - * @author dragove - * created on 2022/12/26 - */ -@Mapper(componentModel = "spring") -public interface ArkLevelConverter { - - ArkLevelInfo convert(ArkLevel arkLevel); - - List convert(List arkLevel); - -} diff --git a/src/main/java/plus/maa/backend/common/utils/converter/CommentConverter.java b/src/main/java/plus/maa/backend/common/utils/converter/CommentConverter.java deleted file mode 100644 index ba412411..00000000 --- a/src/main/java/plus/maa/backend/common/utils/converter/CommentConverter.java +++ /dev/null @@ -1,30 +0,0 @@ -package plus.maa.backend.common.utils.converter; - -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; -import plus.maa.backend.controller.response.comments.CommentsInfo; -import plus.maa.backend.controller.response.comments.SubCommentsInfo; -import plus.maa.backend.repository.entity.CommentsArea; -import plus.maa.backend.repository.entity.MaaUser; - -/** - * @author LoMu - * Date 2023-02-21 18:16 - */ - -@Mapper(componentModel = "spring") -public interface CommentConverter { - - @Mapping(target = "like", source = "likeCount") - @Mapping(target = "uploader", source = "maaUser.userName") - @Mapping(target = "commentId", source = "id") - @Mapping(target = "subCommentsInfos", ignore = true) - CommentsInfo toCommentsInfo(CommentsArea commentsArea, String id, int likeCount, MaaUser maaUser); - - - @Mapping(target = "like", source = "likeCount") - @Mapping(target = "uploader", source = "maaUser.userName") - @Mapping(target = "commentId", source = "id") - @Mapping(target = "deleted", source = "delete") - SubCommentsInfo toSubCommentsInfo(CommentsArea commentsArea, String id, int likeCount, MaaUser maaUser, boolean delete); -} diff --git a/src/main/java/plus/maa/backend/common/utils/converter/MaaUserConverter.java b/src/main/java/plus/maa/backend/common/utils/converter/MaaUserConverter.java deleted file mode 100644 index 9c91d139..00000000 --- a/src/main/java/plus/maa/backend/common/utils/converter/MaaUserConverter.java +++ /dev/null @@ -1,25 +0,0 @@ -package plus.maa.backend.common.utils.converter; - -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; -import plus.maa.backend.controller.response.user.MaaUserInfo; -import plus.maa.backend.repository.entity.MaaUser; - -import java.util.Objects; - -/** - * @author dragove - * created on 2022/12/26 - */ -@Mapper(componentModel = "spring", - imports = { - Objects.class - }) -public interface MaaUserConverter { - - @Mapping(source = "userId", target = "id") - @Mapping(target = "activated", expression = "java(Objects.equals(user.getStatus(), 1))") - @Mapping(target = "uploadCount", ignore = true) - MaaUserInfo convert(MaaUser user); - -} diff --git a/src/main/java/plus/maa/backend/config/CorsConfig.java b/src/main/java/plus/maa/backend/config/CorsConfig.java deleted file mode 100644 index 60df9368..00000000 --- a/src/main/java/plus/maa/backend/config/CorsConfig.java +++ /dev/null @@ -1,38 +0,0 @@ -package plus.maa.backend.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import plus.maa.backend.handler.AccessLimitInterceptHandlerImpl; - -/** - * @author AnselYuki - */ -@Configuration -public class CorsConfig implements WebMvcConfigurer { - - - @Override - public void addCorsMappings(CorsRegistry registry) { - // 设置允许跨域的路径 - registry - .addMapping("/**") - // 设置允许跨域请求的域名 - .allowedOriginPatterns("*") - // 是否允许cookie - .allowCredentials(true) - // 设置允许的请求方式 - .allowedMethods("GET", "POST", "DELETE", "PUT") - // 设置允许的header属性 - .allowedHeaders("*") - // 跨域允许时间 - .maxAge(3600); - } - - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new AccessLimitInterceptHandlerImpl()); - } -} diff --git a/src/main/java/plus/maa/backend/config/HttpInterfaceConfig.java b/src/main/java/plus/maa/backend/config/HttpInterfaceConfig.java deleted file mode 100644 index 2515e236..00000000 --- a/src/main/java/plus/maa/backend/config/HttpInterfaceConfig.java +++ /dev/null @@ -1,35 +0,0 @@ -package plus.maa.backend.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.reactive.function.client.ExchangeStrategies; -import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.reactive.function.client.support.WebClientAdapter; -import org.springframework.web.service.invoker.HttpServiceProxyFactory; -import plus.maa.backend.repository.GithubRepository; - -@Configuration -public class HttpInterfaceConfig { - - @Bean - GithubRepository githubRepository() { - WebClient client = WebClient.builder() - .baseUrl("https://api.github.com") - .exchangeStrategies(ExchangeStrategies - .builder() - .codecs(codecs -> codecs - .defaultCodecs() - // 最大 20MB - .maxInMemorySize(20 * 1024 * 1024)) - .build()) - .defaultHeaders(headers -> { - headers.add("Accept", "application/vnd.github+json"); - headers.add("X-GitHub-Api-Version", "2022-11-28"); - }) - .build(); - return HttpServiceProxyFactory.builder(WebClientAdapter.forClient(client)) - .build() - .createClient(GithubRepository.class); - } - -} diff --git a/src/main/java/plus/maa/backend/config/NativeReflectionConfig.java b/src/main/java/plus/maa/backend/config/NativeReflectionConfig.java deleted file mode 100644 index 3e1de075..00000000 --- a/src/main/java/plus/maa/backend/config/NativeReflectionConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -package plus.maa.backend.config; - -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; -import org.springframework.context.annotation.Configuration; -import plus.maa.backend.controller.request.copilot.CopilotDTO; -import plus.maa.backend.repository.entity.gamedata.*; -import plus.maa.backend.service.model.RatingCache; -import plus.maa.backend.service.session.UserSession; - -/** - * 添加所有需要用到反射的类到此处,用于 native image - * 等个大佬修缮 - * - * @author dragove - * created on 2023/08/18 - */ -@Configuration -@RegisterReflectionForBinding({ - ArkActivity.class, ArkCharacter.class, ArkStage.class, - ArkTilePos.class, ArkTilePos.Tile.class, ArkTower.class, - ArkZone.class, CopilotDTO.class, RatingCache.class, - UserSession.class, - PropertyNamingStrategies.SnakeCaseStrategy.class, - PropertyNamingStrategies.LowerCamelCaseStrategy.class -}) -public class NativeReflectionConfig { - -} diff --git a/src/main/java/plus/maa/backend/config/SensitiveWordConfig.java b/src/main/java/plus/maa/backend/config/SensitiveWordConfig.java deleted file mode 100644 index d603a72e..00000000 --- a/src/main/java/plus/maa/backend/config/SensitiveWordConfig.java +++ /dev/null @@ -1,63 +0,0 @@ -package plus.maa.backend.config; - -import cn.hutool.dfa.WordTree; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.Resource; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; - -/** - * 敏感词配置类
- * - * @author lixuhuilll - * Date: 2023-08-25 18:50 - */ - -@Slf4j -@Configuration -public class SensitiveWordConfig { - - // 标准的 Spring 路径匹配语法,默认为 classpath:sensitive-word.txt - @Value("${maa-copilot.sensitive-word.path:classpath:sensitive-word.txt}") - String sensitiveWordPath; - - /** - * 敏感词库初始化
- * 使用 Hutool 的 DFA 算法库,如果后续需要可转其他开源库或者使用付费的敏感词库
- * - * @return 敏感词库 - */ - - @Bean - public WordTree sensitiveWordInit(ApplicationContext applicationContext) throws IOException { - // Spring 上下文获取敏感词文件 - Resource sensitiveWordResource = applicationContext.getResource(sensitiveWordPath); - WordTree wordTree = new WordTree(); - - // 获取载入用时 - long start = System.currentTimeMillis(); - - // 以行为单位载入敏感词 - try (BufferedReader bufferedReader = new BufferedReader( - new InputStreamReader(sensitiveWordResource.getInputStream())) - ) { - String line; - while ((line = bufferedReader.readLine()) != null) { - wordTree.addWord(line); - } - } catch (Exception e) { - log.error("敏感词库初始化失败:{}", e.getMessage()); - throw e; - } - - log.info("敏感词库初始化完成,耗时 {} ms", System.currentTimeMillis() - start); - - return wordTree; - } -} diff --git a/src/main/java/plus/maa/backend/config/SpringDocConfig.java b/src/main/java/plus/maa/backend/config/SpringDocConfig.java deleted file mode 100644 index 4c59d80a..00000000 --- a/src/main/java/plus/maa/backend/config/SpringDocConfig.java +++ /dev/null @@ -1,99 +0,0 @@ -package plus.maa.backend.config; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.swagger.v3.core.jackson.ModelResolver; -import io.swagger.v3.oas.models.Components; -import io.swagger.v3.oas.models.ExternalDocumentation; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.oas.models.info.License; -import io.swagger.v3.oas.models.security.SecurityRequirement; -import io.swagger.v3.oas.models.security.SecurityScheme; -import org.springdoc.core.customizers.OperationCustomizer; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.MethodParameter; -import plus.maa.backend.common.annotation.CurrentUser; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -/** - * @author AnselYuki - */ -@Configuration -public class SpringDocConfig { - - @Value("${maa-copilot.info.version}") - private String version; - - @Value("${maa-copilot.info.title}") - private String title; - - @Value("${maa-copilot.info.description}") - private String description; - - @Value("${maa-copilot.jwt.header}") - private String securitySchemeHeader; - - public static final String SECURITY_SCHEME_NAME = "Bearer"; - - @Bean - public OpenAPI emergencyLogistics() { - return new OpenAPI() - .info(docInfos()) - .externalDocs(new ExternalDocumentation() - .description("GitHub repo") - .url("https://github.com/MaaAssistantArknights/MaaBackendCenter")) - .components(new Components() - .addSecuritySchemes(SECURITY_SCHEME_NAME, - new SecurityScheme() - .type(SecurityScheme.Type.HTTP) - .scheme("bearer") - .in(SecurityScheme.In.HEADER) - .name(securitySchemeHeader) - .description("JWT Authorization header using the Bearer scheme. Raw head example: \"%s: Bearer {token}\"".formatted(securitySchemeHeader)) - )); - } - - /** - * 为使用了 {@link CurrentUser} 注解的接口在 OpenAPI 上添加 security scheme - */ - @Bean - public OperationCustomizer currentUserOperationCustomizer() { - return (operation, handlerMethod) -> { - for (MethodParameter parameter : handlerMethod.getMethodParameters()) { - if (parameter.hasParameterAnnotation(CurrentUser.class)) { - var security = Optional.ofNullable(operation.getSecurity()); - // 已有 security scheme - if (security.stream().flatMap(List::stream).anyMatch(s -> s.containsKey(SECURITY_SCHEME_NAME))) { - break; - } - - // 添加 security scheme - operation.setSecurity(security.orElseGet(ArrayList::new)); - operation.getSecurity().add(new SecurityRequirement().addList(SECURITY_SCHEME_NAME)); - break; - } - } - return operation; - }; - } - - private Info docInfos() { - return new Info() - .title(title) - .description(description) - .version(version) - .license(new License() - .name("GNU Affero General Public License v3.0") - .url("https://www.gnu.org/licenses/agpl-3.0.html")); - } - - @Bean - public ModelResolver modelResolver(ObjectMapper objectMapper) { - return new ModelResolver(objectMapper); - } -} diff --git a/src/main/java/plus/maa/backend/config/external/ArkLevelGit.java b/src/main/java/plus/maa/backend/config/external/ArkLevelGit.java deleted file mode 100644 index d8a860e1..00000000 --- a/src/main/java/plus/maa/backend/config/external/ArkLevelGit.java +++ /dev/null @@ -1,9 +0,0 @@ -package plus.maa.backend.config.external; - - -@lombok.Data -public class ArkLevelGit { - private String repository; - private String localRepository; - private String jsonPath; -} diff --git a/src/main/java/plus/maa/backend/config/external/Cache.java b/src/main/java/plus/maa/backend/config/external/Cache.java deleted file mode 100644 index 4f2e7ede..00000000 --- a/src/main/java/plus/maa/backend/config/external/Cache.java +++ /dev/null @@ -1,6 +0,0 @@ -package plus.maa.backend.config.external; - -@lombok.Data -public class Cache { - private long defaultExpire; -} diff --git a/src/main/java/plus/maa/backend/config/external/CopilotBackup.java b/src/main/java/plus/maa/backend/config/external/CopilotBackup.java deleted file mode 100644 index 845627c4..00000000 --- a/src/main/java/plus/maa/backend/config/external/CopilotBackup.java +++ /dev/null @@ -1,16 +0,0 @@ -package plus.maa.backend.config.external; - -@lombok.Data -public class CopilotBackup { - // 是禁用备份功能 - private boolean disabled; - // 本地备份地址 - private String dir; - // 远程备份地址 - private String uri; - - // git 用户名 - private String username; - // git 邮箱 - private String email; -} diff --git a/src/main/java/plus/maa/backend/config/external/Github.java b/src/main/java/plus/maa/backend/config/external/Github.java deleted file mode 100644 index 6f6d9d07..00000000 --- a/src/main/java/plus/maa/backend/config/external/Github.java +++ /dev/null @@ -1,9 +0,0 @@ -package plus.maa.backend.config.external; - -@lombok.Data -public class Github { - /** - * GitHub api token - */ - private String token; -} \ No newline at end of file diff --git a/src/main/java/plus/maa/backend/config/external/Info.java b/src/main/java/plus/maa/backend/config/external/Info.java deleted file mode 100644 index eca8e46e..00000000 --- a/src/main/java/plus/maa/backend/config/external/Info.java +++ /dev/null @@ -1,10 +0,0 @@ -package plus.maa.backend.config.external; - -@lombok.Data -public class Info { - private String title; - private String description; - private String version; - private String domain; - private String frontendDomain; -} \ No newline at end of file diff --git a/src/main/java/plus/maa/backend/config/external/Jwt.java b/src/main/java/plus/maa/backend/config/external/Jwt.java deleted file mode 100644 index f3718e39..00000000 --- a/src/main/java/plus/maa/backend/config/external/Jwt.java +++ /dev/null @@ -1,22 +0,0 @@ -package plus.maa.backend.config.external; - -@lombok.Data -public class Jwt { - /** - * Header name - */ - private String header; - /** - * 默认的JwtToken过期时间,以秒为单位 - */ - private long expire = 21600; - - /* - * 默认的 Refresh Token 过期时间,以秒为单位 - */ - private long refreshExpire = 30 * 24 * 60 * 60; - /** - * JwtToken的加密密钥 - */ - private String secret; -} \ No newline at end of file diff --git a/src/main/java/plus/maa/backend/config/external/MaaCopilotProperties.java b/src/main/java/plus/maa/backend/config/external/MaaCopilotProperties.java deleted file mode 100644 index c7975d63..00000000 --- a/src/main/java/plus/maa/backend/config/external/MaaCopilotProperties.java +++ /dev/null @@ -1,29 +0,0 @@ -package plus.maa.backend.config.external; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -@ConfigurationProperties("maa-copilot") -@lombok.Data -public class MaaCopilotProperties { - @NestedConfigurationProperty - private Jwt jwt; - @NestedConfigurationProperty - private Github github; - @NestedConfigurationProperty - private Info info; - @NestedConfigurationProperty - private Vcode vcode; - @NestedConfigurationProperty - private Cache cache; - @NestedConfigurationProperty - private ArkLevelGit arkLevelGit; - @NestedConfigurationProperty - private TaskCron taskCron; - @NestedConfigurationProperty - private CopilotBackup backup; - @NestedConfigurationProperty - private Mail mail; - @NestedConfigurationProperty - private SensitiveWord sensitiveWord; -} diff --git a/src/main/java/plus/maa/backend/config/external/Mail.java b/src/main/java/plus/maa/backend/config/external/Mail.java deleted file mode 100644 index 8cafbbd3..00000000 --- a/src/main/java/plus/maa/backend/config/external/Mail.java +++ /dev/null @@ -1,19 +0,0 @@ -package plus.maa.backend.config.external; - -import lombok.Data; - -/** - * @author LoMu - * Date 2023-03-04 14:49 - */ -@Data -public class Mail { - private String host; - private Integer port; - private String from; - private String user; - private String pass; - private Boolean starttls; - private Boolean ssl; - private Boolean notification; -} diff --git a/src/main/java/plus/maa/backend/config/external/SensitiveWord.java b/src/main/java/plus/maa/backend/config/external/SensitiveWord.java deleted file mode 100644 index 535ca52a..00000000 --- a/src/main/java/plus/maa/backend/config/external/SensitiveWord.java +++ /dev/null @@ -1,6 +0,0 @@ -package plus.maa.backend.config.external; - -@lombok.Data -public class SensitiveWord { - private String path; -} diff --git a/src/main/java/plus/maa/backend/config/external/TaskCron.java b/src/main/java/plus/maa/backend/config/external/TaskCron.java deleted file mode 100644 index 09f8f210..00000000 --- a/src/main/java/plus/maa/backend/config/external/TaskCron.java +++ /dev/null @@ -1,8 +0,0 @@ -package plus.maa.backend.config.external; - -@lombok.Data -public class TaskCron { - - private String arkLevel; - -} diff --git a/src/main/java/plus/maa/backend/config/external/Vcode.java b/src/main/java/plus/maa/backend/config/external/Vcode.java deleted file mode 100644 index 34690eee..00000000 --- a/src/main/java/plus/maa/backend/config/external/Vcode.java +++ /dev/null @@ -1,9 +0,0 @@ -package plus.maa.backend.config.external; - -@lombok.Data -public class Vcode { - /** - * 默认的验证码失效时间,以秒为单位 - */ - private long expire; -} \ No newline at end of file diff --git a/src/main/java/plus/maa/backend/config/security/AccessDeniedHandlerImpl.java b/src/main/java/plus/maa/backend/config/security/AccessDeniedHandlerImpl.java deleted file mode 100644 index 17782695..00000000 --- a/src/main/java/plus/maa/backend/config/security/AccessDeniedHandlerImpl.java +++ /dev/null @@ -1,26 +0,0 @@ -package plus.maa.backend.config.security; - -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.http.HttpStatus; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.web.access.AccessDeniedHandler; -import org.springframework.stereotype.Component; -import plus.maa.backend.common.utils.WebUtils; -import plus.maa.backend.controller.response.MaaResult; - -import java.io.IOException; - -/** - * @author AnselYuki - */ -@Component -public class AccessDeniedHandlerImpl implements AccessDeniedHandler { - @Override - public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { - var result = MaaResult.fail(HttpStatus.FORBIDDEN.value(), "权限不足"); - String json = new ObjectMapper().writeValueAsString(result); - WebUtils.renderString(response, json, HttpStatus.FORBIDDEN.value()); - } -} diff --git a/src/main/java/plus/maa/backend/config/security/AuthenticationEntryPointImpl.java b/src/main/java/plus/maa/backend/config/security/AuthenticationEntryPointImpl.java deleted file mode 100644 index a973f4f6..00000000 --- a/src/main/java/plus/maa/backend/config/security/AuthenticationEntryPointImpl.java +++ /dev/null @@ -1,31 +0,0 @@ -package plus.maa.backend.config.security; - -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.stereotype.Component; -import plus.maa.backend.common.utils.WebUtils; -import plus.maa.backend.controller.response.MaaResult; - -import java.io.IOException; - -/** - * @author AnselYuki - */ -@Component -@RequiredArgsConstructor -public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { - - private final ObjectMapper objectMapper; - - @Override - public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { - MaaResult result = MaaResult.fail(HttpStatus.UNAUTHORIZED.value(), authException.getMessage()); - String json = objectMapper.writeValueAsString(result); - WebUtils.renderString(response, json, HttpStatus.UNAUTHORIZED.value()); - } -} diff --git a/src/main/java/plus/maa/backend/config/security/AuthenticationHelper.java b/src/main/java/plus/maa/backend/config/security/AuthenticationHelper.java deleted file mode 100644 index 2bdb7ebf..00000000 --- a/src/main/java/plus/maa/backend/config/security/AuthenticationHelper.java +++ /dev/null @@ -1,76 +0,0 @@ -package plus.maa.backend.config.security; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.springframework.http.HttpStatus; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; -import org.springframework.web.server.ResponseStatusException; -import plus.maa.backend.common.utils.IpUtil; -import plus.maa.backend.service.jwt.JwtAuthToken; -import plus.maa.backend.service.model.LoginUser; - -import java.util.Objects; - -/** - * Auth 助手,统一 auth 的设置和获取 - */ -@Component -public class AuthenticationHelper { - /** - * 设置当前 auth, 是 SecurityContextHolder.getContext().setAuthentication(authentication) 的集中调用 - * - * @param authentication 当前的 auth - */ - public void setAuthentication(Authentication authentication) { - SecurityContextHolder.getContext().setAuthentication(authentication); - } - - /** - * 要求用户 id ,否则抛出异常 - * - * @return 已经验证的用户 id - * @throws ResponseStatusException 用户未通过验证 - */ - public @NotNull String requireUserId() throws ResponseStatusException { - var id = getUserId(); - if (id == null) - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); - return id; - } - - /** - * 获取用户 id - * - * @return 用户 id,如未验证则返回 null - */ - public @Nullable String getUserId() { - var auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth == null) return null; - if (auth instanceof UsernamePasswordAuthenticationToken) { - var principal = auth.getPrincipal(); - if (principal instanceof LoginUser) return ((LoginUser) principal).getUserId(); - } else if (auth instanceof JwtAuthToken) { - return ((JwtAuthToken) auth).getSubject(); - } - return null; - } - - /** - * 获取已验证用户 id 或者未验证用户 ip 地址。在 HTTP request 之外调用该方法获取 ip 会抛出 NPE - * - * @return 用户 id 或者 ip 地址 - */ - public @NotNull String getUserIdOrIpAddress() { - var id = getUserId(); - if (id != null) return id; - - var attributes = Objects.requireNonNull(RequestContextHolder.getRequestAttributes()); - var request = ((ServletRequestAttributes) attributes).getRequest(); - return IpUtil.getIpAddr(request); - } -} diff --git a/src/main/java/plus/maa/backend/config/security/JwtAuthenticationTokenFilter.java b/src/main/java/plus/maa/backend/config/security/JwtAuthenticationTokenFilter.java deleted file mode 100644 index 0d30ceab..00000000 --- a/src/main/java/plus/maa/backend/config/security/JwtAuthenticationTokenFilter.java +++ /dev/null @@ -1,51 +0,0 @@ -package plus.maa.backend.config.security; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.jetbrains.annotations.NotNull; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; -import plus.maa.backend.config.external.MaaCopilotProperties; -import plus.maa.backend.service.jwt.JwtService; - -import java.io.IOException; - -/** - * @author AnselYuki - */ -@Component -public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { - public JwtAuthenticationTokenFilter(AuthenticationHelper helper, MaaCopilotProperties properties, JwtService jwtService) { - this.helper = helper; - this.properties = properties; - this.jwtService = jwtService; - } - - private final AuthenticationHelper helper; - private final MaaCopilotProperties properties; - private final JwtService jwtService; - - @Override - protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws IOException, ServletException { - try { - var token = extractToken(request); - var authToken = jwtService.verifyAndParseAuthToken(token); - helper.setAuthentication(authToken); - } catch (Exception ex) { - logger.trace(ex.getMessage()); - } finally { - filterChain.doFilter(request, response); - } - } - - @NotNull - private String extractToken(HttpServletRequest request) throws Exception { - if (SecurityContextHolder.getContext().getAuthentication() != null) throw new Exception("no need to auth"); - var head = request.getHeader(properties.getJwt().getHeader()); - if (head == null || !head.startsWith("Bearer ")) throw new Exception("token not found"); - return head.substring(7); - } -} diff --git a/src/main/java/plus/maa/backend/config/security/SecurityConfig.java b/src/main/java/plus/maa/backend/config/security/SecurityConfig.java deleted file mode 100644 index 0922176b..00000000 --- a/src/main/java/plus/maa/backend/config/security/SecurityConfig.java +++ /dev/null @@ -1,111 +0,0 @@ -package plus.maa.backend.config.security; - -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; - -import static org.springframework.security.config.Customizer.withDefaults; - -/** - * @author AnselYuki - */ -@Configuration -@RequiredArgsConstructor -public class SecurityConfig { - /** - * 添加放行接口在此处 - */ - private static final String[] URL_WHITELIST = { - "/user/login", - "/user/register", - "/user/sendRegistrationToken" - }; - - private static final String[] URL_PERMIT_ALL = { - "/", - "/error", - "/version", - "/user/activateAccount", - "/user/password/reset_request", - "/user/password/reset", - "/user/refresh", - "/swagger-ui.html", - "/v3/api-docs/**", - "/swagger-ui/**", - "/arknights/level", - "/copilot/query", - "/copilot/get/**", - "/copilot/rating", - "/comments/query", - "/file/upload", - "/comments/status", - "/copilot/status" - }; - - //添加需要权限1才能访问的接口 - private static final String[] URL_AUTHENTICATION_1 = { - "/copilot/delete", - "/copilot/update", - "/copilot/upload", - "/comments/add", - "/comments/delete" - }; - - private static final String[] URL_AUTHENTICATION_2 = { - "/file/download/**", - "/file/download/", - "/file/disable", - "/file/enable", - "/file/upload_ability" - }; - private final AuthenticationConfiguration authenticationConfiguration; - private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; - private final AuthenticationEntryPointImpl authenticationEntryPoint; - private final AccessDeniedHandlerImpl accessDeniedHandler; - - @Bean - public BCryptPasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public AuthenticationManager authenticationManager() throws Exception { - return authenticationConfiguration.getAuthenticationManager(); - } - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - //关闭CSRF,设置无状态连接 - http.csrf(AbstractHttpConfigurer::disable) - //不通过Session获取SecurityContext - .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); - - //允许匿名访问的接口,如果是测试想要方便点就把这段全注释掉 - http.authorizeHttpRequests(authorize -> - authorize.requestMatchers(URL_WHITELIST).anonymous() - .requestMatchers(URL_PERMIT_ALL).permitAll() - //权限 0 未激活 1 激活 等等.. (拥有权限1必然拥有权限0 拥有权限2必然拥有权限1、0) - //指定接口需要指定权限才能访问 如果不开启RBAC注释掉这一段即可 - .requestMatchers(URL_AUTHENTICATION_1).hasAuthority("1") - //此处用于管理员操作接口 - .requestMatchers(URL_AUTHENTICATION_2).hasAuthority("2") - .anyRequest().authenticated()); - //添加过滤器 - http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); - - //配置异常处理器,处理认证失败的JSON响应 - http.exceptionHandling(exceptionHandling -> exceptionHandling.authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler)); - - //开启跨域请求 - http.cors(withDefaults()); - return http.build(); - } -} diff --git a/src/main/java/plus/maa/backend/controller/ArkLevelController.java b/src/main/java/plus/maa/backend/controller/ArkLevelController.java deleted file mode 100644 index 3d5d45c9..00000000 --- a/src/main/java/plus/maa/backend/controller/ArkLevelController.java +++ /dev/null @@ -1,32 +0,0 @@ -package plus.maa.backend.controller; - -import java.util.List; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -import plus.maa.backend.controller.response.copilot.ArkLevelInfo; -import plus.maa.backend.controller.response.MaaResult; -import plus.maa.backend.service.ArkLevelService; - -/** - * @author john180 - */ -@RestController -@RequiredArgsConstructor -@Tag(name = "ArkLevelController", description = "关卡数据管理接口") -public class ArkLevelController { - private final ArkLevelService arkLevelService; - - @Operation(summary = "获取关卡数据") - @ApiResponse(description = "关卡数据") - @GetMapping("/arknights/level") - public MaaResult> getLevels() { - return MaaResult.success(arkLevelService.getArkLevelInfos()); - } - -} diff --git a/src/main/java/plus/maa/backend/controller/CommentsAreaController.java b/src/main/java/plus/maa/backend/controller/CommentsAreaController.java deleted file mode 100644 index ba4770a3..00000000 --- a/src/main/java/plus/maa/backend/controller/CommentsAreaController.java +++ /dev/null @@ -1,96 +0,0 @@ -package plus.maa.backend.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; -import plus.maa.backend.common.annotation.JsonSchema; -import plus.maa.backend.common.annotation.SensitiveWordDetection; -import plus.maa.backend.config.SpringDocConfig; -import plus.maa.backend.config.security.AuthenticationHelper; -import plus.maa.backend.controller.request.comments.*; -import plus.maa.backend.controller.response.MaaResult; -import plus.maa.backend.controller.response.comments.CommentsAreaInfo; -import plus.maa.backend.service.CommentsAreaService; - -/** - * @author LoMu - * Date 2023-02-17 14:56 - */ - -@RestController -@RequiredArgsConstructor -@Tag(name = "CommentArea", description = "评论区管理接口") -@RequestMapping("/comments") -public class CommentsAreaController { - private final CommentsAreaService commentsAreaService; - private final AuthenticationHelper authHelper; - - @SensitiveWordDetection("#comments.message") - @PostMapping("/add") - @Operation(summary = "发送评论") - @ApiResponse(description = "发送评论结果") - @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) - public MaaResult sendComments( - @Parameter(description = "评论") @Valid @RequestBody CommentsAddDTO comments - ) { - commentsAreaService.addComments(authHelper.requireUserId(), comments); - return MaaResult.success("评论成功"); - } - - @GetMapping("/query") - @Operation(summary = "分页查询评论") - @ApiResponse(description = "评论区信息") - public MaaResult queriesCommentsArea( - @Parameter(description = "评论查询对象") @Valid CommentsQueriesDTO parsed - ) { - return MaaResult.success(commentsAreaService.queriesCommentsArea(parsed)); - } - - @PostMapping("/delete") - @Operation(summary = "删除评论") - @ApiResponse(description = "评论删除结果") - @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) - public MaaResult deleteComments( - @Parameter(description = "评论删除对象") @Valid @RequestBody CommentsDeleteDTO comments - ) { - commentsAreaService.deleteComments(authHelper.requireUserId(), comments.getCommentId()); - return MaaResult.success("评论已删除"); - } - - @JsonSchema - @Operation(summary = "为评论点赞") - @ApiResponse(description = "点赞结果") - @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) - @PostMapping("/rating") - public MaaResult ratesComments( - @Parameter(description = "评论点赞对象") @Valid @RequestBody CommentsRatingDTO commentsRatingDTO - ) { - commentsAreaService.rates(authHelper.requireUserId(), commentsRatingDTO); - return MaaResult.success("成功"); - } - - @Operation(summary = "为评论置顶/取消置顶") - @ApiResponse(description = "置顶/取消置顶结果") - @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) - @PostMapping("/topping") - public MaaResult toppingComments( - @Parameter(description = "评论置顶对象") @Valid @RequestBody CommentsToppingDTO commentsToppingDTO - ) { - commentsAreaService.topping(authHelper.requireUserId(), commentsToppingDTO); - return MaaResult.success("成功"); - } - - @Operation(summary = "设置通知接收状态") - @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) - @GetMapping("/status") - public MaaResult modifyStatus(@RequestParam @NotBlank String id, @RequestParam boolean status) { - commentsAreaService.notificationStatus(authHelper.getUserId(), id, status); - return MaaResult.success("success"); - } -} diff --git a/src/main/java/plus/maa/backend/controller/CopilotController.java b/src/main/java/plus/maa/backend/controller/CopilotController.java deleted file mode 100644 index 04da198e..00000000 --- a/src/main/java/plus/maa/backend/controller/CopilotController.java +++ /dev/null @@ -1,119 +0,0 @@ -package plus.maa.backend.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpHeaders; -import org.springframework.web.bind.annotation.*; -import plus.maa.backend.common.annotation.JsonSchema; -import plus.maa.backend.common.annotation.SensitiveWordDetection; -import plus.maa.backend.config.SpringDocConfig; -import plus.maa.backend.config.security.AuthenticationHelper; -import plus.maa.backend.controller.request.copilot.CopilotCUDRequest; -import plus.maa.backend.controller.request.copilot.CopilotQueriesRequest; -import plus.maa.backend.controller.request.copilot.CopilotRatingReq; -import plus.maa.backend.controller.response.MaaResult; -import plus.maa.backend.controller.response.copilot.CopilotInfo; -import plus.maa.backend.controller.response.copilot.CopilotPageInfo; -import plus.maa.backend.service.CopilotService; - -/** - * @author LoMu - * Date 2022-12-25 17:08 - */ - -@RequiredArgsConstructor -@RestController -@RequestMapping("/copilot") -@Tag(name = "CopilotController", description = "作业本体管理接口") -public class CopilotController { - private final CopilotService copilotService; - private final AuthenticationHelper helper; - private final HttpServletResponse response; - - @Operation(summary = "上传作业") - @ApiResponse(description = "上传作业结果") - @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) - @JsonSchema - @SensitiveWordDetection("#request.content != null ? #objectMapper.readTree(#request.content).get('doc')?.toString() : null") - @PostMapping("/upload") - public MaaResult uploadCopilot( - @Parameter(description = "作业操作请求") @RequestBody CopilotCUDRequest request - ) { - return MaaResult.success(copilotService.upload(helper.requireUserId(), request.getContent())); - } - - @Operation(summary = "删除作业") - @ApiResponse(description = "删除作业结果") - @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) - @PostMapping("/delete") - public MaaResult deleteCopilot( - @Parameter(description = "作业操作请求") @RequestBody CopilotCUDRequest request - ) { - copilotService.delete(helper.requireUserId(), request); - return MaaResult.success(); - } - - @Operation(summary = "获取作业") - @ApiResponse(description = "作业信息") - @GetMapping("/get/{id}") - public MaaResult getCopilotById( - @Parameter(description = "作业id") @PathVariable("id") Long id - ) { - var userIdOrIpAddress = helper.getUserIdOrIpAddress(); - return copilotService.getCopilotById(userIdOrIpAddress, id).map(MaaResult::success) - .orElse(MaaResult.fail(404, "数据不存在")); - } - - - @Operation(summary = "分页查询作业,提供登录凭据时查询用户自己的作业") - @ApiResponse(description = "作业信息") - @GetMapping("/query") - public MaaResult queriesCopilot( - @Parameter(description = "作业查询请求") @Valid CopilotQueriesRequest parsed - ) { - // 两秒防抖,缓解前端重复请求问题 - response.setHeader(HttpHeaders.CACHE_CONTROL, "private, max-age=2, must-revalidate"); - return MaaResult.success(copilotService.queriesCopilot(helper.getUserId(), parsed)); - } - - @Operation(summary = "更新作业") - @ApiResponse(description = "更新结果") - @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) - @JsonSchema - @SensitiveWordDetection("#copilotCUDRequest.content != null ? #objectMapper.readTree(#copilotCUDRequest.content).get('doc')?.toString() : null") - @PostMapping("/update") - public MaaResult updateCopilot( - @Parameter(description = "作业操作请求") @RequestBody CopilotCUDRequest copilotCUDRequest - ) { - copilotService.update(helper.requireUserId(), copilotCUDRequest); - return MaaResult.success(); - } - - @Operation(summary = "为作业评分") - @ApiResponse(description = "评分结果") - @JsonSchema - @PostMapping("/rating") - public MaaResult ratesCopilotOperation( - @Parameter(description = "作业评分请求") @RequestBody CopilotRatingReq copilotRatingReq - ) { - copilotService.rates(helper.getUserIdOrIpAddress(), copilotRatingReq); - return MaaResult.success("评分成功"); - } - - @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) - @Operation(summary = "修改通知状态") - @ApiResponse(description = "success") - @GetMapping("/status") - public MaaResult modifyStatus(@RequestParam @NotBlank Long id, @RequestParam boolean status) { - copilotService.notificationStatus(helper.getUserId(), id, status); - return MaaResult.success("success"); - } - -} diff --git a/src/main/java/plus/maa/backend/controller/SystemController.java b/src/main/java/plus/maa/backend/controller/SystemController.java deleted file mode 100644 index 65827d92..00000000 --- a/src/main/java/plus/maa/backend/controller/SystemController.java +++ /dev/null @@ -1,44 +0,0 @@ -package plus.maa.backend.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import plus.maa.backend.config.external.MaaCopilotProperties; -import plus.maa.backend.controller.response.MaaResult; -import plus.maa.backend.controller.response.MaaSystemInfo; - - -/** - * @author AnselYuki - */ -@Tag(name = "System", description = "系统管理接口") -@RequestMapping("") -@RestController -@RequiredArgsConstructor -public class SystemController { - private final MaaCopilotProperties properties; - - @GetMapping("/") - @Operation(summary = "Tests if the server is ready.") - @ApiResponse(description = "系统启动信息") - public MaaResult test() { - return MaaResult.success("Maa Copilot Server is Running", null); - } - - @GetMapping("version") - @Operation(summary = "Gets the current version of the server.") - @ApiResponse(description = "系统版本信息") - public MaaResult getSystemVersion() { - var systemInfo = new MaaSystemInfo(); - var info = properties.getInfo(); - systemInfo.setTitle(info.getTitle()); - systemInfo.setDescription(info.getDescription()); - systemInfo.setVersion(info.getVersion()); - return MaaResult.success(systemInfo); - } - -} diff --git a/src/main/java/plus/maa/backend/controller/UserController.java b/src/main/java/plus/maa/backend/controller/UserController.java deleted file mode 100644 index 0c91e9f8..00000000 --- a/src/main/java/plus/maa/backend/controller/UserController.java +++ /dev/null @@ -1,163 +0,0 @@ -package plus.maa.backend.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import lombok.Data; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import plus.maa.backend.config.SpringDocConfig; -import plus.maa.backend.config.external.MaaCopilotProperties; -import plus.maa.backend.config.security.AuthenticationHelper; -import plus.maa.backend.controller.request.user.*; -import plus.maa.backend.controller.response.MaaResult; -import plus.maa.backend.controller.response.user.MaaLoginRsp; -import plus.maa.backend.controller.response.user.MaaUserInfo; -import plus.maa.backend.service.EmailService; -import plus.maa.backend.service.UserService; - -/** - * 用户相关接口 - * 前端api约定文件 - * - * @author AnselYuki - */ -@Data -@Tag(name = "CopilotUser", description = "用户管理") -@RequestMapping("/user") -@Validated -@RestController -@RequiredArgsConstructor -public class UserController { - private final UserService userService; - private final EmailService emailService; - private final MaaCopilotProperties properties; - private final AuthenticationHelper helper; - @Value("${maa-copilot.jwt.header}") - private String header; - - /** - * 更新当前用户的密码(根据原密码) - * - * @return http响应 - */ - @Operation(summary = "修改当前用户密码", description = "根据原密码") - @ApiResponse(description = "修改密码结果") - @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) - @PostMapping("/update/password") - public MaaResult updatePassword( - @Parameter(description = "修改密码请求") @RequestBody @Valid PasswordUpdateDTO updateDTO - ) { - userService.modifyPassword(helper.requireUserId(), updateDTO.getNewPassword()); - return MaaResult.success(); - } - - /** - * 更新用户详细信息 - * - * @param updateDTO 用户信息参数 - * @return http响应 - */ - @Operation(summary = "更新用户详细信息") - @ApiResponse(description = "更新结果") - @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) - @PostMapping("/update/info") - public MaaResult updateInfo( - @Parameter(description = "更新用户详细信息请求") @Valid @RequestBody UserInfoUpdateDTO updateDTO - ) { - userService.updateUserInfo(helper.requireUserId(), updateDTO); - return MaaResult.success(); - } - - /** - * 邮箱重设密码 - * - * @param passwordResetDTO 通过邮箱修改密码请求 - * @return 成功响应 - */ - @PostMapping("/password/reset") - @Operation(summary = "重置密码") - @ApiResponse(description = "重置密码结果") - public MaaResult passwordReset(@Parameter(description = "重置密码请求") @RequestBody @Valid PasswordResetDTO passwordResetDTO) { - // 校验用户邮箱是否存在 - userService.checkUserExistByEmail(passwordResetDTO.getEmail()); - userService.modifyPasswordByActiveCode(passwordResetDTO); - return MaaResult.success(); - } - - /** - * 验证码重置密码功能: - * 发送验证码用于重置 - * - * @return 成功响应 - */ - @PostMapping("/password/reset_request") - @Operation(summary = "发送用于重置密码的验证码") - @ApiResponse(description = "验证码发送结果") - public MaaResult passwordResetRequest(@Parameter(description = "发送重置密码的验证码请求") @RequestBody @Valid PasswordResetVCodeDTO passwordResetVCodeDTO) { - // 校验用户邮箱是否存在 - userService.checkUserExistByEmail(passwordResetVCodeDTO.getEmail()); - emailService.sendVCode(passwordResetVCodeDTO.getEmail()); - return MaaResult.success(); - } - - /** - * 刷新token - * - * @param request http请求,用于获取请求头 - * @return 成功响应 - */ - @PostMapping("/refresh") - @Operation(summary = "刷新token") - @ApiResponse(description = "刷新token结果") - public MaaResult refresh(@Parameter(description = "刷新token请求") @RequestBody RefreshReq request) { - var res = userService.refreshToken(request.getRefreshToken()); - return MaaResult.success(res); - } - - /** - * 用户注册 - * - * @param user 传入用户参数 - * @return 注册成功用户信息摘要 - */ - @PostMapping("/register") - @Operation(summary = "用户注册") - @ApiResponse(description = "注册结果") - public MaaResult register(@Parameter(description = "用户注册请求") @Valid @RequestBody RegisterDTO user) { - return MaaResult.success(userService.register(user)); - } - - /** - * 获得注册时的验证码 - */ - @PostMapping("/sendRegistrationToken") - @Operation(summary = "注册时发送验证码") - @ApiResponse(description = "发送验证码结果", responseCode = "204") - public MaaResult sendRegistrationToken(@Parameter(description = "发送注册验证码请求") @RequestBody @Valid SendRegistrationTokenDTO regDTO) { - userService.sendRegistrationToken(regDTO); - return new MaaResult<>(204, null, null); - } - - /** - * 用户登录 - * - * @param user 登录参数 - * @return 成功响应,荷载JwtToken - */ - @PostMapping("/login") - @Operation(summary = "用户登录") - @ApiResponse(description = "登录结果") - public MaaResult login(@Parameter(description = "登录请求") @RequestBody @Valid LoginDTO user) { - return MaaResult.success("登陆成功", userService.login(user)); - } -} diff --git a/src/main/java/plus/maa/backend/controller/file/FileController.java b/src/main/java/plus/maa/backend/controller/file/FileController.java deleted file mode 100644 index c8b0b8c0..00000000 --- a/src/main/java/plus/maa/backend/controller/file/FileController.java +++ /dev/null @@ -1,118 +0,0 @@ -package plus.maa.backend.controller.file; - - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; -import plus.maa.backend.common.annotation.AccessLimit; -import plus.maa.backend.config.SpringDocConfig; -import plus.maa.backend.config.security.AuthenticationHelper; -import plus.maa.backend.controller.response.MaaResult; -import plus.maa.backend.service.FileService; - -/** - * @author LoMu - * Date 2023-03-31 16:41 - */ - -@RestController -@RequestMapping("file") -@RequiredArgsConstructor -public class FileController { - private final FileService fileService; - private final AuthenticationHelper helper; - - /** - * 支持匿名 - * - * @param file file - * @return 上传成功, 数据已被接收 - */ - @AccessLimit - @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public MaaResult uploadFile( - @RequestPart MultipartFile file, - @RequestPart String type, - @RequestPart String version, - @RequestPart(required = false) String classification, - @RequestPart(required = false) String label - ) { - fileService.uploadFile(file, type, version, classification, label, helper.getUserIdOrIpAddress()); - return MaaResult.success("上传成功,数据已被接收"); - } - - @Operation(summary = "下载文件") - @ApiResponse( - responseCode = "200", - content = @Content(mediaType = "application/zip", schema = @Schema(type = "string", format = "binary")) - ) - @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) - @AccessLimit - @GetMapping("/download") - public void downloadSpecifiedDateFile( - @Parameter(description = "日期 yyyy-MM-dd") String date, - @Parameter(description = "在日期之前或之后[before,after]") String beLocated, - @Parameter(description = "对查询到的数据进行删除") boolean delete, - HttpServletResponse response - ) { - fileService.downloadDateFile(date, beLocated, delete, response); - } - - @Operation(summary = "下载文件") - @ApiResponse( - responseCode = "200", - content = @Content(mediaType = "application/zip", schema = @Schema(type = "string", format = "binary")) - ) - @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) - @PostMapping("/download") - public void downloadFile(@RequestBody @Valid - ImageDownloadDTO imageDownloadDTO, - HttpServletResponse response) { - fileService.downloadFile(imageDownloadDTO, response); - } - - @Operation(summary = "设置上传文件功能状态") - @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) - @PostMapping("/upload_ability") - public MaaResult setUploadAbility(@RequestBody UploadAbility request) { - fileService.setUploadEnabled(request.enabled); - return MaaResult.success(); - } - - @Operation(summary = "获取上传文件功能状态") - @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) - @GetMapping("/upload_ability") - public MaaResult getUploadAbility() { - return MaaResult.success(new UploadAbility(fileService.isUploadEnabled())); - } - - @Operation(summary = "关闭uploadfile接口") - @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) - @PostMapping("/disable") - public MaaResult disable(@RequestBody boolean status) { - if (!status) { - return MaaResult.fail(403, "Forbidden"); - } - return MaaResult.success(fileService.disable()); - } - - @Operation(summary = "开启uploadfile接口") - @SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_NAME) - @PostMapping("/enable") - public MaaResult enable(@RequestBody boolean status) { - if (!status) { - return MaaResult.fail(403, "Forbidden"); - } - return MaaResult.success(fileService.enable()); - } - -} diff --git a/src/main/java/plus/maa/backend/controller/file/ImageDownloadDTO.java b/src/main/java/plus/maa/backend/controller/file/ImageDownloadDTO.java deleted file mode 100644 index 930204b9..00000000 --- a/src/main/java/plus/maa/backend/controller/file/ImageDownloadDTO.java +++ /dev/null @@ -1,24 +0,0 @@ -package plus.maa.backend.controller.file; - -import jakarta.validation.constraints.NotNull; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.hibernate.validator.constraints.Length; - -import java.util.List; - - -/** - * @author LoMu - * Date 2023-04-16 17:41 - */ - -@Data -public class ImageDownloadDTO { - @NotNull - private String type; - private String classification; - private List version; - private String label; - private boolean delete; -} diff --git a/src/main/java/plus/maa/backend/controller/file/UploadAbility.java b/src/main/java/plus/maa/backend/controller/file/UploadAbility.java deleted file mode 100644 index 4dd47b73..00000000 --- a/src/main/java/plus/maa/backend/controller/file/UploadAbility.java +++ /dev/null @@ -1,15 +0,0 @@ -package plus.maa.backend.controller.file; - -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Data; - -@AllArgsConstructor -@Data -public class UploadAbility { - /** - * 是否开启上传功能 - */ - @NotNull - Boolean enabled; -} diff --git a/src/main/java/plus/maa/backend/controller/request/comments/CommentsAddDTO.java b/src/main/java/plus/maa/backend/controller/request/comments/CommentsAddDTO.java deleted file mode 100644 index 6b2dde89..00000000 --- a/src/main/java/plus/maa/backend/controller/request/comments/CommentsAddDTO.java +++ /dev/null @@ -1,29 +0,0 @@ -package plus.maa.backend.controller.request.comments; - -import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.hibernate.validator.constraints.Length; - -/** - * @author LoMu - * Date 2023-02-17 14:58 - */ - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class CommentsAddDTO { - @Length(min = 1, max = 150, message = "评论内容不可超过150字,请删减") - private String message; - - @NotBlank(message = "作业id不可为空") - private String copilotId; - - //子评论(回复评论) - private String fromCommentId; - - private boolean notification = true; - -} diff --git a/src/main/java/plus/maa/backend/controller/request/comments/CommentsDeleteDTO.java b/src/main/java/plus/maa/backend/controller/request/comments/CommentsDeleteDTO.java deleted file mode 100644 index 5eae397c..00000000 --- a/src/main/java/plus/maa/backend/controller/request/comments/CommentsDeleteDTO.java +++ /dev/null @@ -1,19 +0,0 @@ -package plus.maa.backend.controller.request.comments; - -import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * @author LoMu - * Date 2023-02-19 10:50 - */ - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class CommentsDeleteDTO { - @NotBlank(message = "评论id不可为空") - private String commentId; -} diff --git a/src/main/java/plus/maa/backend/controller/request/comments/CommentsQueriesDTO.java b/src/main/java/plus/maa/backend/controller/request/comments/CommentsQueriesDTO.java deleted file mode 100644 index 9fe4afdb..00000000 --- a/src/main/java/plus/maa/backend/controller/request/comments/CommentsQueriesDTO.java +++ /dev/null @@ -1,27 +0,0 @@ -package plus.maa.backend.controller.request.comments; - -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * @author LoMu - * Date 2023-02-20 17:13 - */ - - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class CommentsQueriesDTO { - @NotNull(message = "作业id不可为空") - private Long copilotId; - private int page = 0; - @Max(value = 50, message = "单页大小不得超过50") - private int limit = 10; - private boolean desc = true; - private String orderBy; - private String justSeeId; -} diff --git a/src/main/java/plus/maa/backend/controller/request/comments/CommentsRatingDTO.java b/src/main/java/plus/maa/backend/controller/request/comments/CommentsRatingDTO.java deleted file mode 100644 index 23b8c995..00000000 --- a/src/main/java/plus/maa/backend/controller/request/comments/CommentsRatingDTO.java +++ /dev/null @@ -1,20 +0,0 @@ -package plus.maa.backend.controller.request.comments; - -import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * @author LoMu - * Date 2023-02-19 13:39 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class CommentsRatingDTO { - @NotBlank(message = "评分id不可为空") - private String commentId; - @NotBlank(message = "评分不能为空") - private String rating; -} diff --git a/src/main/java/plus/maa/backend/controller/request/comments/CommentsToppingDTO.java b/src/main/java/plus/maa/backend/controller/request/comments/CommentsToppingDTO.java deleted file mode 100644 index 5f2d05b9..00000000 --- a/src/main/java/plus/maa/backend/controller/request/comments/CommentsToppingDTO.java +++ /dev/null @@ -1,20 +0,0 @@ -package plus.maa.backend.controller.request.comments; - -import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * @author Lixuhuilll - * Date 2023-08-17 11:20 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class CommentsToppingDTO { - @NotBlank(message = "评论id不可为空") - private String commentId; - // 是否将指定评论置顶 - private boolean topping = true; -} diff --git a/src/main/java/plus/maa/backend/controller/request/copilot/CopilotCUDRequest.java b/src/main/java/plus/maa/backend/controller/request/copilot/CopilotCUDRequest.java deleted file mode 100644 index 6cfbccad..00000000 --- a/src/main/java/plus/maa/backend/controller/request/copilot/CopilotCUDRequest.java +++ /dev/null @@ -1,13 +0,0 @@ -package plus.maa.backend.controller.request.copilot; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class CopilotCUDRequest { - private String content; - private Long id; -} diff --git a/src/main/java/plus/maa/backend/controller/request/copilot/CopilotDTO.java b/src/main/java/plus/maa/backend/controller/request/copilot/CopilotDTO.java deleted file mode 100644 index 5fcbb54a..00000000 --- a/src/main/java/plus/maa/backend/controller/request/copilot/CopilotDTO.java +++ /dev/null @@ -1,44 +0,0 @@ -package plus.maa.backend.controller.request.copilot; - - -import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import plus.maa.backend.repository.entity.Copilot; - -import java.util.List; - -/** - * @author LoMu - * Date 2023-01-10 19:50 - */ - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class CopilotDTO { - - //关卡名 - @NotBlank(message = "关卡名不能为空") - private String stageName; - - //难度 - private int difficulty; - - //版本号(文档中说明:最低要求 maa 版本号,必选。保留字段) - @NotBlank(message = "最低要求 maa 版本不可为空") - private String minimumRequired; - - //指定干员 - private List opers; - //群组 - private List groups; - // 战斗中的操作 - private List actions; - - //描述 - private Copilot.Doc doc; - - private boolean notification; -} diff --git a/src/main/java/plus/maa/backend/controller/request/copilot/CopilotQueriesRequest.java b/src/main/java/plus/maa/backend/controller/request/copilot/CopilotQueriesRequest.java deleted file mode 100644 index 81adcc70..00000000 --- a/src/main/java/plus/maa/backend/controller/request/copilot/CopilotQueriesRequest.java +++ /dev/null @@ -1,49 +0,0 @@ -package plus.maa.backend.controller.request.copilot; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import jakarta.validation.constraints.Max; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * @author LoMu - * Date 2022-12-26 2:48 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class CopilotQueriesRequest { - private int page = 0; - @Max(value = 50, message = "单页大小不得超过50") - private int limit = 10; - private String levelKeyword; - private String operator; - private String content; - private String document; - private String uploaderId; - private boolean desc = true; - private String orderBy; - private String language; - - /* - * 这里为了正确接收前端的下划线风格,手动写了三个 setter 用于起别名 - * 因为 Get 请求传入的参数不是 JSON,所以没办法使用 Jackson 的注解直接实现别名 - * 添加 @JsonAlias 和 @JsonIgnore 注解只是为了保障 Swagger 的文档正确显示 - * (吐槽一下,同样是Get请求,怎么CommentsQueries是驼峰命名,到了CopilotQueries就成了下划线命名) - */ - @JsonIgnore - public void setLevel_keyword(String levelKeyword) { - this.levelKeyword = levelKeyword; - } - - @JsonIgnore - public void setUploader_id(String uploaderId) { - this.uploaderId = uploaderId; - } - - @JsonIgnore - public void setOrder_by(String orderBy) { - this.orderBy = orderBy; - } -} diff --git a/src/main/java/plus/maa/backend/controller/request/copilot/CopilotRatingReq.java b/src/main/java/plus/maa/backend/controller/request/copilot/CopilotRatingReq.java deleted file mode 100644 index d9acc43e..00000000 --- a/src/main/java/plus/maa/backend/controller/request/copilot/CopilotRatingReq.java +++ /dev/null @@ -1,16 +0,0 @@ -package plus.maa.backend.controller.request.copilot; - -import jakarta.validation.constraints.NotBlank; -import lombok.Data; - -/** - * @author LoMu - * Date 2023-01-20 16:25 - */ -@Data -public class CopilotRatingReq { - @NotBlank(message = "评分作业id不能为空") - private Long id; - @NotBlank(message = "评分不能为空") - private String rating; -} diff --git a/src/main/java/plus/maa/backend/controller/request/user/EmailActivateReq.java b/src/main/java/plus/maa/backend/controller/request/user/EmailActivateReq.java deleted file mode 100644 index cc138ebf..00000000 --- a/src/main/java/plus/maa/backend/controller/request/user/EmailActivateReq.java +++ /dev/null @@ -1,14 +0,0 @@ -package plus.maa.backend.controller.request.user; - -import jakarta.validation.constraints.NotBlank; -import lombok.Data; - -/** - * @author dragove - * created on 2023/1/19 - */ -@Data -public class EmailActivateReq { - @NotBlank(message = "激活标识符不能为空") - private String nonce; -} diff --git a/src/main/java/plus/maa/backend/controller/request/user/LoginDTO.java b/src/main/java/plus/maa/backend/controller/request/user/LoginDTO.java deleted file mode 100644 index 2c2f8270..00000000 --- a/src/main/java/plus/maa/backend/controller/request/user/LoginDTO.java +++ /dev/null @@ -1,23 +0,0 @@ -package plus.maa.backend.controller.request.user; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -/** - * @author AnselYuki - */ -@Data -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -public class LoginDTO { - @NotBlank(message = "邮箱格式错误") - @Email(message = "邮箱格式错误") - private String email; - @NotBlank(message = "请输入用户密码") - private String password; -} diff --git a/src/main/java/plus/maa/backend/controller/request/user/PasswordResetDTO.java b/src/main/java/plus/maa/backend/controller/request/user/PasswordResetDTO.java deleted file mode 100644 index e0fdf9ad..00000000 --- a/src/main/java/plus/maa/backend/controller/request/user/PasswordResetDTO.java +++ /dev/null @@ -1,34 +0,0 @@ -package plus.maa.backend.controller.request.user; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -/** - * 通过邮件修改密码请求 - */ -@Data -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -public class PasswordResetDTO { - /** - * 邮箱 - */ - @NotBlank(message = "邮箱格式错误") - @Email(message = "邮箱格式错误") - private String email; - /** - * 验证码 - */ - @NotBlank(message = "请输入验证码") - private String activeCode; - /** - * 修改后的密码 - */ - @NotBlank(message = "请输入用户密码") - private String password; -} diff --git a/src/main/java/plus/maa/backend/controller/request/user/PasswordResetVCodeDTO.java b/src/main/java/plus/maa/backend/controller/request/user/PasswordResetVCodeDTO.java deleted file mode 100644 index 4b843cd0..00000000 --- a/src/main/java/plus/maa/backend/controller/request/user/PasswordResetVCodeDTO.java +++ /dev/null @@ -1,24 +0,0 @@ -package plus.maa.backend.controller.request.user; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -/** - * 通过邮件修改密码发送验证码请求 - */ -@Data -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -public class PasswordResetVCodeDTO { - /** - * 邮箱 - */ - @NotBlank(message = "邮箱格式错误") - @Email(message = "邮箱格式错误") - private String email; -} diff --git a/src/main/java/plus/maa/backend/controller/request/user/PasswordUpdateDTO.java b/src/main/java/plus/maa/backend/controller/request/user/PasswordUpdateDTO.java deleted file mode 100644 index 8775b4c6..00000000 --- a/src/main/java/plus/maa/backend/controller/request/user/PasswordUpdateDTO.java +++ /dev/null @@ -1,23 +0,0 @@ -package plus.maa.backend.controller.request.user; - -import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.hibernate.validator.constraints.Length; - -/** - * @author AnselYuki - */ -@Data -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -public class PasswordUpdateDTO { - @NotBlank(message = "请输入原密码") - private String originalPassword; - @NotBlank(message = "密码长度必须在8-32位之间") - @Length(min = 8, max = 32, message = "密码长度必须在8-32位之间") - private String newPassword; -} diff --git a/src/main/java/plus/maa/backend/controller/request/user/RefreshReq.java b/src/main/java/plus/maa/backend/controller/request/user/RefreshReq.java deleted file mode 100644 index 2f381c43..00000000 --- a/src/main/java/plus/maa/backend/controller/request/user/RefreshReq.java +++ /dev/null @@ -1,8 +0,0 @@ -package plus.maa.backend.controller.request.user; - -import lombok.Data; - -@Data -public class RefreshReq { - private String refreshToken; -} diff --git a/src/main/java/plus/maa/backend/controller/request/user/RegisterDTO.java b/src/main/java/plus/maa/backend/controller/request/user/RegisterDTO.java deleted file mode 100644 index 4272cb9a..00000000 --- a/src/main/java/plus/maa/backend/controller/request/user/RegisterDTO.java +++ /dev/null @@ -1,30 +0,0 @@ -package plus.maa.backend.controller.request.user; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.hibernate.validator.constraints.Length; - -/** - * @author AnselYuki - */ -@Data -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -public class RegisterDTO { - @NotBlank(message = "邮箱格式错误") - @Email(message = "邮箱格式错误") - private String email; - @NotBlank(message = "用户名长度应在4-24位之间") - @Length(min = 4, max = 24, message = "用户名长度应在4-24位之间") - private String userName; - @NotBlank(message = "密码长度必须在8-32位之间") - @Length(min = 8, max = 32, message = "密码长度必须在8-32位之间") - private String password; - @NotBlank(message = "请输入验证码") - private String registrationToken; -} diff --git a/src/main/java/plus/maa/backend/controller/request/user/SendRegistrationTokenDTO.java b/src/main/java/plus/maa/backend/controller/request/user/SendRegistrationTokenDTO.java deleted file mode 100644 index 582271a4..00000000 --- a/src/main/java/plus/maa/backend/controller/request/user/SendRegistrationTokenDTO.java +++ /dev/null @@ -1,12 +0,0 @@ -package plus.maa.backend.controller.request.user; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import lombok.Data; - -@Data -public class SendRegistrationTokenDTO { - @NotBlank(message = "邮箱格式错误") - @Email(message = "邮箱格式错误") - private String email; -} diff --git a/src/main/java/plus/maa/backend/controller/request/user/UserInfoUpdateDTO.java b/src/main/java/plus/maa/backend/controller/request/user/UserInfoUpdateDTO.java deleted file mode 100644 index 2f03476a..00000000 --- a/src/main/java/plus/maa/backend/controller/request/user/UserInfoUpdateDTO.java +++ /dev/null @@ -1,21 +0,0 @@ -package plus.maa.backend.controller.request.user; - -import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.hibernate.validator.constraints.Length; - -/** - * @author AnselYuki - */ -@Data -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -public class UserInfoUpdateDTO { - @NotBlank(message = "用户名长度应在4-24位之间") - @Length(min = 4, max = 24, message = "用户名长度应在4-24位之间") - private String userName; -} diff --git a/src/main/java/plus/maa/backend/controller/response/MaaResult.java b/src/main/java/plus/maa/backend/controller/response/MaaResult.java deleted file mode 100644 index de2cb5da..00000000 --- a/src/main/java/plus/maa/backend/controller/response/MaaResult.java +++ /dev/null @@ -1,32 +0,0 @@ -package plus.maa.backend.controller.response; - -import com.fasterxml.jackson.annotation.JsonInclude; - -import java.io.Serializable; - -/** - * @author AnselYuki - */ -@JsonInclude(JsonInclude.Include.NON_NULL) -public record MaaResult(int statusCode, String message, T data) implements Serializable { - public static MaaResult success(T data) { - return success(null, data); - } - - public static MaaResult success() { - return success(null, null); - } - - public static MaaResult success(String msg, T data) { - return new MaaResult<>(200, msg, data); - } - - public static MaaResult fail(int code, String msg) { - return fail(code, msg, null); - } - - public static MaaResult fail(int code, String msg, T data) { - return new MaaResult<>(code, msg, data); - } - -} \ No newline at end of file diff --git a/src/main/java/plus/maa/backend/controller/response/MaaResultException.java b/src/main/java/plus/maa/backend/controller/response/MaaResultException.java deleted file mode 100644 index 5d98c3f9..00000000 --- a/src/main/java/plus/maa/backend/controller/response/MaaResultException.java +++ /dev/null @@ -1,27 +0,0 @@ -package plus.maa.backend.controller.response; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.springframework.http.HttpStatus; -import plus.maa.backend.common.MaaStatusCode; - -/** - * @author john180 - */ -@Data -@AllArgsConstructor -@EqualsAndHashCode(callSuper = true) -public class MaaResultException extends RuntimeException { - private final int code; - private final String msg; - - public MaaResultException(String msg) { - this(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg); - } - - public MaaResultException(MaaStatusCode statusCode) { - this.code = statusCode.code; - this.msg = statusCode.message; - } -} diff --git a/src/main/java/plus/maa/backend/controller/response/MaaSystemInfo.java b/src/main/java/plus/maa/backend/controller/response/MaaSystemInfo.java deleted file mode 100644 index 8f152138..00000000 --- a/src/main/java/plus/maa/backend/controller/response/MaaSystemInfo.java +++ /dev/null @@ -1,13 +0,0 @@ -package plus.maa.backend.controller.response; - -import lombok.Data; - -/** - * @author AnselYuki - */ -@Data -public class MaaSystemInfo { - private String title; - private String description; - private String version; -} diff --git a/src/main/java/plus/maa/backend/controller/response/comments/CommentsAreaInfo.java b/src/main/java/plus/maa/backend/controller/response/comments/CommentsAreaInfo.java deleted file mode 100644 index 3f526927..00000000 --- a/src/main/java/plus/maa/backend/controller/response/comments/CommentsAreaInfo.java +++ /dev/null @@ -1,25 +0,0 @@ -package plus.maa.backend.controller.response.comments; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -import java.util.List; - - -/** - * @author LoMu - * Date 2023-02-19 11:47 - */ - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Accessors(chain = true) -public class CommentsAreaInfo { - private Boolean hasNext; - private Integer page; - private Long total; - private List data; -} diff --git a/src/main/java/plus/maa/backend/controller/response/comments/CommentsInfo.java b/src/main/java/plus/maa/backend/controller/response/comments/CommentsInfo.java deleted file mode 100644 index f0a8e9be..00000000 --- a/src/main/java/plus/maa/backend/controller/response/comments/CommentsInfo.java +++ /dev/null @@ -1,32 +0,0 @@ -package plus.maa.backend.controller.response.comments; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -/** - * @author LoMu - * Date 2023-02-20 17:04 - */ - -@Data -@NoArgsConstructor -@Accessors(chain = true) -@AllArgsConstructor -public class CommentsInfo { - private String commentId; - private String uploader; - private String uploaderId; - - //评论内容 - private String message; - private LocalDateTime uploadTime; - private int like; - private boolean topping; - private List subCommentsInfos = new ArrayList<>(); -} diff --git a/src/main/java/plus/maa/backend/controller/response/comments/SubCommentsInfo.java b/src/main/java/plus/maa/backend/controller/response/comments/SubCommentsInfo.java deleted file mode 100644 index 5265ce76..00000000 --- a/src/main/java/plus/maa/backend/controller/response/comments/SubCommentsInfo.java +++ /dev/null @@ -1,31 +0,0 @@ -package plus.maa.backend.controller.response.comments; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -import java.time.LocalDateTime; - -/** - * @author LoMu - * Date 2023-02-20 17:05 - */ - -@Data -@NoArgsConstructor -@AllArgsConstructor -@Accessors(chain = true) -public class SubCommentsInfo { - private String commentId; - private String uploader; - private String uploaderId; - - //评论内容 - private String message; - private LocalDateTime uploadTime; - private int like; - private String fromCommentId; - private String mainCommentId; - private boolean deleted; -} diff --git a/src/main/java/plus/maa/backend/controller/response/copilot/ArkLevelInfo.java b/src/main/java/plus/maa/backend/controller/response/copilot/ArkLevelInfo.java deleted file mode 100644 index 7df5976d..00000000 --- a/src/main/java/plus/maa/backend/controller/response/copilot/ArkLevelInfo.java +++ /dev/null @@ -1,28 +0,0 @@ -package plus.maa.backend.controller.response.copilot; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; - -import java.io.Serializable; - -/** - * @author john180 - */ -@Data -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class ArkLevelInfo implements Serializable { - private String levelId; - private String stageId; - private String catOne; - private String catTwo; - private String catThree; - private String name; - private int width; - private int height; -} diff --git a/src/main/java/plus/maa/backend/controller/response/copilot/CopilotInfo.java b/src/main/java/plus/maa/backend/controller/response/copilot/CopilotInfo.java deleted file mode 100644 index 8bb00bc7..00000000 --- a/src/main/java/plus/maa/backend/controller/response/copilot/CopilotInfo.java +++ /dev/null @@ -1,30 +0,0 @@ -package plus.maa.backend.controller.response.copilot; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; -import java.time.LocalDateTime; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class CopilotInfo implements Serializable { - private Long id; - - private LocalDateTime uploadTime; - private String uploader; - //用于前端显示的格式化后的干员信息 [干员名]::[技能] - private int views; - private int hotScore; - private boolean available; - private int ratingLevel; - private boolean isNotEnoughRating; - private double ratingRatio; - private int ratingType; - private long commentsCount; - private String content; - private long like; - private long dislike; -} diff --git a/src/main/java/plus/maa/backend/controller/response/copilot/CopilotPageInfo.java b/src/main/java/plus/maa/backend/controller/response/copilot/CopilotPageInfo.java deleted file mode 100644 index 4d457b80..00000000 --- a/src/main/java/plus/maa/backend/controller/response/copilot/CopilotPageInfo.java +++ /dev/null @@ -1,23 +0,0 @@ -package plus.maa.backend.controller.response.copilot; - -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.annotation.JsonNaming; -import lombok.Data; -import lombok.experimental.Accessors; - -import java.io.Serializable; -import java.util.List; - -/** - * @author LoMu - * Date 2022-12-27 12:39 - */ -@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) -@Accessors(chain = true) -@Data -public class CopilotPageInfo implements Serializable { - private Boolean hasNext; - private Integer page; - private Long total; - private List data; -} diff --git a/src/main/java/plus/maa/backend/controller/response/user/MaaLoginRsp.java b/src/main/java/plus/maa/backend/controller/response/user/MaaLoginRsp.java deleted file mode 100644 index 6c651064..00000000 --- a/src/main/java/plus/maa/backend/controller/response/user/MaaLoginRsp.java +++ /dev/null @@ -1,18 +0,0 @@ -package plus.maa.backend.controller.response.user; - -import lombok.AllArgsConstructor; -import lombok.Data; - -import java.time.LocalDateTime; - -@Data -@AllArgsConstructor -public class MaaLoginRsp { - private String token; - private LocalDateTime validBefore; - private LocalDateTime validAfter; - private String refreshToken; - private LocalDateTime refreshTokenValidBefore; - private LocalDateTime refreshTokenValidAfter; - private MaaUserInfo userInfo; -} diff --git a/src/main/java/plus/maa/backend/controller/response/user/MaaUserInfo.java b/src/main/java/plus/maa/backend/controller/response/user/MaaUserInfo.java deleted file mode 100644 index 5741768f..00000000 --- a/src/main/java/plus/maa/backend/controller/response/user/MaaUserInfo.java +++ /dev/null @@ -1,30 +0,0 @@ -package plus.maa.backend.controller.response.user; - -import org.springframework.beans.BeanUtils; - -import com.fasterxml.jackson.annotation.JsonInclude; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import plus.maa.backend.repository.entity.MaaUser; - -/** - * @author AnselYuki - */ -@Data -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -public class MaaUserInfo { - private String id; - private String userName; - private boolean activated; - private long uploadCount; - - public MaaUserInfo(MaaUser save) { - BeanUtils.copyProperties(save, this); - } -} diff --git a/src/main/java/plus/maa/backend/filter/MaaEtagHeaderFilter.java b/src/main/java/plus/maa/backend/filter/MaaEtagHeaderFilter.java deleted file mode 100644 index 731b1173..00000000 --- a/src/main/java/plus/maa/backend/filter/MaaEtagHeaderFilter.java +++ /dev/null @@ -1,43 +0,0 @@ -package plus.maa.backend.filter; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.annotation.WebFilter; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.jetbrains.annotations.NotNull; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.web.filter.ShallowEtagHeaderFilter; - -import java.io.IOException; - - -/** - * 提供基于 Etag 机制的 HTTP 缓存,有助于降低网络传输的压力 - * - * @author lixuhuilll - */ - -// 配置需要使用 Etag 机制的 URL,注意和 Spring 的 UrlPattern 语法不太一样 -@WebFilter(urlPatterns = { - "/arknights/level", - "/copilot/query" -}) -@RequiredArgsConstructor -public class MaaEtagHeaderFilter extends ShallowEtagHeaderFilter { - - @Override - protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException { - if (!HttpMethod.GET.matches(request.getMethod())) { - // ETag 只处理安全的请求 - filterChain.doFilter(request, response); - return; - } - // 允许使用 Etag (实际是避免默认添加的 no-store) - response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache, max-age=0, must-revalidate"); - // 其他接口默认处理即可,注意默认操作相当于牺牲 CPU 来节约网络带宽,不适用于结果变更过快的接口 - super.doFilterInternal(request, response, filterChain); - } -} diff --git a/src/main/java/plus/maa/backend/handler/AccessLimitInterceptHandlerImpl.java b/src/main/java/plus/maa/backend/handler/AccessLimitInterceptHandlerImpl.java deleted file mode 100644 index 456bcc39..00000000 --- a/src/main/java/plus/maa/backend/handler/AccessLimitInterceptHandlerImpl.java +++ /dev/null @@ -1,116 +0,0 @@ -package plus.maa.backend.handler; - -import java.lang.reflect.Method; -import java.util.concurrent.TimeUnit; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Component; -import org.springframework.web.method.HandlerMethod; -import org.springframework.web.servlet.HandlerInterceptor; -import org.springframework.web.servlet.ModelAndView; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import io.micrometer.common.util.StringUtils; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import plus.maa.backend.common.annotation.AccessLimit; -import plus.maa.backend.common.utils.IpUtil; -import plus.maa.backend.common.utils.SpringUtil; -import plus.maa.backend.common.utils.WebUtils; -import plus.maa.backend.controller.response.MaaResult; - -/** - * @author Baip1995 - */ -@Component -public class AccessLimitInterceptHandlerImpl implements HandlerInterceptor { - - private static final Logger logger = LoggerFactory.getLogger(AccessLimitInterceptHandlerImpl.class); - - // @Resource - // private RedisCache redisCache; - /** - * 接口调用前检查对方ip是否频繁调用接口 - * - * @param request - * @param response - * @param handler - * @return - * @throws Exception - */ - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { - try { - // handler是否为 HandleMethod 实例 - if (handler instanceof HandlerMethod) { - // 强转 - HandlerMethod handlerMethod = (HandlerMethod) handler; - // 获取方法 - Method method = handlerMethod.getMethod(); - // 判断方式是否有AccessLimit注解,有的才需要做限流 - if (!method.isAnnotationPresent(AccessLimit.class)) { - return true; - } - StringRedisTemplate stringRedisTemplate = SpringUtil.getApplicationContext() - .getBean(StringRedisTemplate.class); - - // 获取注解上的内容 - AccessLimit accessLimit = method.getAnnotation(AccessLimit.class); - if (accessLimit == null) { - return true; - } - // 获取方法注解上的请求次数 - int times = accessLimit.times(); - // 获取方法注解上的请求时间 - Integer second = accessLimit.second(); - - // 拼接redis key = IP + Api限流 - String key = IpUtil.getIpAddr(request) + request.getRequestURI(); - - // 获取redis的value - Integer maxTimes = null; - - String value = stringRedisTemplate.opsForValue().get(key); - if (StringUtils.isNotEmpty(value)) { - maxTimes = Integer.valueOf(value); - } - if (maxTimes == null) { - // 如果redis中没有该ip对应的时间则表示第一次调用,保存key到redis - stringRedisTemplate.opsForValue().set(key, "1", second, TimeUnit.SECONDS); - } else if (maxTimes < times) { - // 如果redis中的时间比注解上的时间小则表示可以允许访问,这是修改redis的value时间 - stringRedisTemplate.opsForValue().set(key, maxTimes + 1 + "", second, TimeUnit.SECONDS); - } else { - // 请求过于频繁 - logger.info(key + " 请求过于频繁"); - MaaResult result = MaaResult.fail(HttpStatus.TOO_MANY_REQUESTS.value(), "请求过于频繁"); - String json = new ObjectMapper().writeValueAsString(result); - WebUtils.renderString(response, json, HttpStatus.TOO_MANY_REQUESTS.value()); - return false; - } - } - } catch (Exception e) { - logger.error("API请求限流拦截异常,异常原因:", e); - // throw new Exception(""); - } - return true; - } - - @Override - public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, - ModelAndView modelAndView) throws Exception { - - } - - @Override - public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) - throws Exception { - - } - -} diff --git a/src/main/java/plus/maa/backend/handler/GlobalExceptionHandler.java b/src/main/java/plus/maa/backend/handler/GlobalExceptionHandler.java deleted file mode 100644 index 6f72e382..00000000 --- a/src/main/java/plus/maa/backend/handler/GlobalExceptionHandler.java +++ /dev/null @@ -1,166 +0,0 @@ -package plus.maa.backend.handler; - -import org.springframework.http.HttpStatus; -import org.springframework.security.core.AuthenticationException; -import org.springframework.validation.FieldError; -import org.springframework.web.HttpRequestMethodNotSupportedException; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.MissingServletRequestParameterException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import org.springframework.web.multipart.MultipartException; -import org.springframework.web.server.ResponseStatusException; -import org.springframework.web.servlet.NoHandlerFoundException; - -import jakarta.servlet.http.HttpServletRequest; -import lombok.extern.slf4j.Slf4j; -import plus.maa.backend.controller.response.MaaResult; -import plus.maa.backend.controller.response.MaaResultException; - -/** - * @author john180 - */ -@Slf4j -@RestControllerAdvice -public class GlobalExceptionHandler { - - /** - * @return plus.maa.backend.controller.response.MaaResult - * @author FAll - * @description 请求参数缺失 - * @date 2022/12/23 12:00 - */ - @ExceptionHandler(MissingServletRequestParameterException.class) - public MaaResult missingServletRequestParameterException(MissingServletRequestParameterException e, - HttpServletRequest request) { - logWarn(request); - log.warn("请求参数缺失", e); - return MaaResult.fail(400, String.format("请求参数缺失:%s", e.getParameterName())); - } - - /** - * @return plus.maa.backend.controller.response.MaaResult - * @author FAll - * @description 参数类型不匹配 - * @date 2022/12/23 12:01 - */ - @ExceptionHandler(MethodArgumentTypeMismatchException.class) - public MaaResult methodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, - HttpServletRequest request) { - logWarn(request); - log.warn("参数类型不匹配", e); - return MaaResult.fail(400, String.format("参数类型不匹配:%s", e.getMessage())); - } - - /** - * @return plus.maa.backend.controller.response.MaaResult - * @author FAll - * @description 参数校验错误 - * @date 2022/12/23 12:02 - */ - @ExceptionHandler(MethodArgumentNotValidException.class) - public MaaResult methodArgumentNotValidException(MethodArgumentNotValidException e) { - FieldError fieldError = e.getBindingResult().getFieldError(); - if (fieldError != null) { - return MaaResult.fail(400, String.format("参数校验错误:%s", fieldError.getDefaultMessage())); - } - return MaaResult.fail(400, String.format("参数校验错误:%s", e.getMessage())); - } - - /** - * @return plus.maa.backend.controller.response.MaaResult - * @author FAll - * @description 请求地址不存在 - * @date 2022/12/23 12:03 - */ - @ExceptionHandler(NoHandlerFoundException.class) - public MaaResult noHandlerFoundExceptionHandler(NoHandlerFoundException e) { - log.warn("请求地址不存在", e); - return MaaResult.fail(404, String.format("请求地址 %s 不存在", e.getRequestURL())); - } - - /** - * @return plus.maa.backend.controller.response.MaaResult - * @author FAll - * @description - * @date 2022/12/23 12:04 - */ - @ExceptionHandler(HttpRequestMethodNotSupportedException.class) - public MaaResult httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException e, - HttpServletRequest request) { - logWarn(request); - log.warn("请求方式错误", e); - return MaaResult.fail(405, String.format("请求方法不正确:%s", e.getMessage())); - } - - /** - * 处理由 {@link org.springframework.util.Assert} 工具产生的异常 - */ - @ExceptionHandler({IllegalArgumentException.class, IllegalStateException.class}) - public MaaResult illegalArgumentOrStateExceptionHandler(RuntimeException e) { - return MaaResult.fail(HttpStatus.BAD_REQUEST.value(), e.getMessage()); - } - - /** - * @return plus.maa.backend.controller.response.MaaResult - * @author cbc - * @description - * @date 2022/12/26 12:00 - */ - @ExceptionHandler(MaaResultException.class) - public MaaResult maaResultExceptionHandler(MaaResultException e) { - return MaaResult.fail(e.getCode(), e.getMsg()); - } - - /** - * @author john180 - * @description 用户鉴权相关,异常兜底处理 - */ - @ExceptionHandler(AuthenticationException.class) - public MaaResult authExceptionHandler(AuthenticationException e) { - return MaaResult.fail(401, e.getMessage()); - } - - @ExceptionHandler(MultipartException.class) - public MaaResult fileSizeThresholdHandler(MultipartException e) { - return MaaResult.fail(413, e.getMessage()); - } - - @ExceptionHandler(ResponseStatusException.class) - public MaaResult handleResponseStatusException(ResponseStatusException ex) { - return MaaResult.fail(ex.getStatusCode().value(), ex.getMessage()); - } - - /** - * @return plus.maa.backend.controller.response.MaaResult - * @author john180 - * @description 服务器内部错误,异常兜底处理 - * @date 2022/12/23 12:06 - */ - @ResponseBody - @ExceptionHandler(value = Exception.class) - public MaaResult defaultExceptionHandler(Exception e, - HttpServletRequest request) { - logError(request); - log.error("Exception: ", e); - return MaaResult.fail(500, "服务器内部错误", null); - } - - private void logWarn(HttpServletRequest request) { - log.warn("Request URL: {}", request.getRequestURL()); - log.warn("Request Method: {}", request.getMethod()); - log.warn("Request IP: {}", request.getRemoteAddr()); - log.warn("Request Headers: {}", request.getHeaderNames()); - log.warn("Request Parameters: {}", request.getParameterMap()); - } - - private void logError(HttpServletRequest request) { - log.error("Request URL: {}", request.getRequestURL()); - log.error("Request Method: {}", request.getMethod()); - log.error("Request IP: {}", request.getRemoteAddr()); - log.error("Request Headers: {}", request.getHeaderNames()); - log.error("Request Parameters: {}", request.getParameterMap()); - } -} diff --git a/src/main/java/plus/maa/backend/repository/ArkLevelRepository.java b/src/main/java/plus/maa/backend/repository/ArkLevelRepository.java deleted file mode 100644 index f2e24c22..00000000 --- a/src/main/java/plus/maa/backend/repository/ArkLevelRepository.java +++ /dev/null @@ -1,51 +0,0 @@ -package plus.maa.backend.repository; - -import java.util.Collection; -import java.util.List; -import java.util.stream.Stream; - -import org.springframework.data.mongodb.repository.MongoRepository; -import org.springframework.data.mongodb.repository.Query; - -import plus.maa.backend.repository.entity.ArkLevel; -import plus.maa.backend.repository.entity.ArkLevelSha; - -/** - * @author john180 - */ -public interface ArkLevelRepository extends MongoRepository { - List findAllShaBy(); - - @Query(""" - { - "$or": [ - {"levelId": ?0}, - {"stageId": ?0}, - {"catThree": ?0} - ] - } - """) - Stream findByLevelIdFuzzy(String levelId); - - /** - * 用于前端查询 关卡名、关卡类型、关卡编号 - */ - @Query(""" - { - "$or": [ - {"stageId": {'$regex': ?0 ,'$options':'i'}}, - {"catThree": {'$regex': ?0 ,'$options':'i'}}, - {"catTwo": {'$regex': ?0 ,'$options':'i'}}, - {"catOne": {'$regex': ?0 ,'$options':'i'}}, - {"name": {'$regex': ?0,'$options':'i' }} - ] - } - """) - Stream queryLevelByKeyword(String keyword); - - /** - * 根据stageId列表查询 - */ - List findByStageIdIn(Collection stageIds); - -} diff --git a/src/main/java/plus/maa/backend/repository/CommentsAreaRepository.java b/src/main/java/plus/maa/backend/repository/CommentsAreaRepository.java deleted file mode 100644 index 60b5ded9..00000000 --- a/src/main/java/plus/maa/backend/repository/CommentsAreaRepository.java +++ /dev/null @@ -1,44 +0,0 @@ -package plus.maa.backend.repository; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.mongodb.repository.MongoRepository; -import org.springframework.stereotype.Repository; -import plus.maa.backend.repository.entity.CommentsArea; - -import java.util.Collection; -import java.util.List; -import java.util.stream.Stream; - -/** - * @author LoMu - * Date 2023-02-17 15:06 - */ - -@Repository -public interface CommentsAreaRepository extends MongoRepository { - - - List findByMainCommentId(String commentsId); - - Page findByCopilotIdAndDeleteAndMainCommentIdExists( - Long copilotId, - boolean delete, - boolean exists, - Pageable pageable - ); - - Page findByCopilotIdAndUploaderIdAndDeleteAndMainCommentIdExists(Long copilotId, - String uploaderId, - boolean delete, - boolean exists, - Pageable pageable); - - Stream findByCopilotIdInAndDelete(Collection copilotIds, boolean delete); - - List findByMainCommentIdIn(List ids); - - Long countByCopilotIdAndDelete(Long copilotId, boolean delete); - - -} diff --git a/src/main/java/plus/maa/backend/repository/CopilotRepository.java b/src/main/java/plus/maa/backend/repository/CopilotRepository.java deleted file mode 100644 index 15b729be..00000000 --- a/src/main/java/plus/maa/backend/repository/CopilotRepository.java +++ /dev/null @@ -1,31 +0,0 @@ -package plus.maa.backend.repository; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.mongodb.repository.MongoRepository; -import plus.maa.backend.repository.entity.Copilot; - -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -/** - * @author LoMu - * Date 2022-12-27 10:28 - */ - -public interface CopilotRepository extends MongoRepository { - - Page findAllByDeleteIsFalse(Pageable pageable); - - Optional findFirstByOrderByCopilotIdDesc(); - - Optional findByCopilotIdAndDeleteIsFalse(Long copilotId); - - List findByCopilotIdInAndDeleteIsFalse(Collection copilotIds); - - Optional findByCopilotId(Long copilotId); - - boolean existsCopilotsByCopilotId(Long copilotId); - -} diff --git a/src/main/java/plus/maa/backend/repository/GithubRepository.java b/src/main/java/plus/maa/backend/repository/GithubRepository.java deleted file mode 100644 index 69872aa4..00000000 --- a/src/main/java/plus/maa/backend/repository/GithubRepository.java +++ /dev/null @@ -1,26 +0,0 @@ -package plus.maa.backend.repository; - -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.service.annotation.GetExchange; -import plus.maa.backend.repository.entity.github.GithubCommit; -import plus.maa.backend.repository.entity.github.GithubTrees; - -import java.util.List; - -/** - * @author dragove - * created on 2022/12/23 - */ -public interface GithubRepository { - - /** - * api doc: git trees api - */ - @GetExchange(value = "/repos/MaaAssistantArknights/MaaAssistantArknights/git/trees/{sha}") - GithubTrees getTrees(@RequestHeader("Authorization") String token, @PathVariable("sha") String sha); - - @GetExchange(value = "/repos/MaaAssistantArknights/MaaAssistantArknights/commits") - List getCommits(@RequestHeader("Authorization") String token); - -} diff --git a/src/main/java/plus/maa/backend/repository/RatingRepository.java b/src/main/java/plus/maa/backend/repository/RatingRepository.java deleted file mode 100644 index 646adcb6..00000000 --- a/src/main/java/plus/maa/backend/repository/RatingRepository.java +++ /dev/null @@ -1,23 +0,0 @@ -package plus.maa.backend.repository; - -import org.springframework.data.mongodb.repository.MongoRepository; -import org.springframework.stereotype.Repository; -import plus.maa.backend.repository.entity.Rating; -import plus.maa.backend.service.model.RatingType; - -import java.util.Optional; - -/** - * @author lixuhuilll - * Date 2023-08-20 12:06 - */ - -@Repository -public interface RatingRepository extends MongoRepository { - Optional findByTypeAndKeyAndUserId(Rating.KeyType type, String key, String userId); - - long countByTypeAndKeyAndRating(Rating.KeyType type, String key, RatingType rating); - - long countByTypeAndKey(Rating.KeyType type, String key); -} - diff --git a/src/main/java/plus/maa/backend/repository/RedisCache.java b/src/main/java/plus/maa/backend/repository/RedisCache.java deleted file mode 100644 index 3b21b6e2..00000000 --- a/src/main/java/plus/maa/backend/repository/RedisCache.java +++ /dev/null @@ -1,349 +0,0 @@ -package plus.maa.backend.repository; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.json.JsonMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.Nullable; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.ClassPathResource; -import org.springframework.data.redis.core.Cursor; -import org.springframework.data.redis.core.ScanOptions; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.data.redis.core.script.RedisScript; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; -import java.util.function.Supplier; - -/** - * Redis工具类 - * - * @author AnselYuki - */ -@Slf4j -@Setter -@Component -@RequiredArgsConstructor -public class RedisCache { - @Value("${maa-copilot.cache.default-expire}") - private int expire; - - private final StringRedisTemplate redisTemplate; - - // 添加 JSR310 模块,以便顺利序列化 LocalDateTime 等类型 - private final ObjectMapper writeMapper = JsonMapper.builder() - .addModule(new JavaTimeModule()) - .build(); - private final ObjectMapper readMapper = JsonMapper.builder() - .addModule(new JavaTimeModule()) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .build(); - - /* - 使用 lua 脚本插入数据,维持 ZSet 的相对大小(size <= 实际大小 <= size + 50)以及过期时间 - 实际大小这么设计是为了避免频繁的 ZREMRANGEBYRANK 操作 - */ - private final RedisScript incZSetRedisScript = RedisScript.of(new ClassPathResource("redis-lua/incZSet.lua")); - // 比较与输入的键值对是否相同,相同则删除 - private final RedisScript removeKVIfEqualsScript = RedisScript.of(new ClassPathResource("redis-lua/removeKVIfEquals.lua"), Boolean.class); - - public void setData(final String key, T value) { - setCache(key, value, 0, TimeUnit.SECONDS); - } - - public void setCache(final String key, T value) { - setCache(key, value, expire, TimeUnit.SECONDS); - } - - public void setCache(final String key, T value, long timeout) { - setCache(key, value, timeout, TimeUnit.SECONDS); - } - - public void setCache(final String key, T value, long timeout, TimeUnit timeUnit) { - String json = getJson(value); - if (json == null) return; - if (timeout <= 0) { - redisTemplate.opsForValue().set(key, json); - } else { - redisTemplate.opsForValue().set(key, json, timeout, timeUnit); - } - } - - /** - * 当缓存不存在时,则 set - * - * @param key 缓存的 key - * @param value 被缓存的值 - * @return 是否 set - */ - - public boolean setCacheIfAbsent(final String key, T value) { - return setCacheIfAbsent(key, value, expire); - } - - /** - * 当缓存不存在时,则 set - * - * @param key 缓存的 key - * @param value 被缓存的值 - * @param timeout 过期时间 - * @return 是否 set - */ - - public boolean setCacheIfAbsent(final String key, T value, long timeout) { - return setCacheIfAbsent(key, value, timeout, TimeUnit.SECONDS); - } - - /** - * 当缓存不存在时,则 set - * - * @param key 缓存的 key - * @param value 被缓存的值 - * @param timeout 过期时间 - * @param timeUnit 过期时间的单位 - * @return 是否 set - */ - public boolean setCacheIfAbsent(final String key, T value, long timeout, TimeUnit timeUnit) { - String json = getJson(value); - if (json == null) return false; - boolean result; - if (timeout <= 0) { - result = Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, json)); - } else { - result = Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, json, timeout, timeUnit)); - } - return result; - } - - public void addSet(final String key, Collection set, long timeout) { - addSet(key, set, timeout, TimeUnit.SECONDS); - } - - public void addSet(final String key, Collection set, long timeout, TimeUnit timeUnit) { - if (key == null || set == null || set.isEmpty()) { // Redis 会拒绝空集合 - return; - } - String[] jsonList = new String[set.size()]; - int i = 0; - for (T t : set) { - String json = getJson(t); - if (json == null) return; - jsonList[i++] = json; - } - - if (timeout <= 0) { - redisTemplate.opsForSet().add(key, jsonList); - } else { - redisTemplate.opsForSet().add(key, jsonList); - redisTemplate.expire(key, timeout, timeUnit); - } - } - - - /** - * ZSet 中元素的 score += incScore,如果元素不存在则插入
- * 会维持 ZSet 的相对大小(size <= 实际大小 <= size + 50)以及过期时间
- * 当大小超出 size + 50 时,会优先删除 score 最小的元素,直到大小等于 size - * - * @param key ZSet 的 key - * @param member ZSet 的 member - * @param incScore 增加的 score - * @param size ZSet 的相对大小 - * @param timeout ZSet 的过期时间 - */ - - public void incZSet(final String key, String member, double incScore, long size, long timeout) { - redisTemplate.execute(incZSetRedisScript, List.of(key), member, Double.toString(incScore), Long.toString(size), Long.toString(timeout)); - } - - // 获取的元素是按照 score 从小到大排列的 - @Nullable - public Set getZSet(final String key, long start, long end) { - return redisTemplate.opsForZSet().range(key, start, end); - } - - // 获取的元素是按照 score 从大到小排列的 - @Nullable - public Set getZSetReverse(final String key, long start, long end) { - return redisTemplate.opsForZSet().reverseRange(key, start, end); - } - - public boolean valueMemberInSet(final String key, T value) { - try { - String json = getJson(value); - if (json == null) return false; - return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(key, json)); - } catch (Exception e) { - log.error(e.getMessage(), e); - } - return false; - } - - @Nullable - public T getCache(final String key, Class valueType) { - return getCache(key, valueType, null, expire, TimeUnit.SECONDS); - } - - @Nullable - public T getCache(final String key, Class valueType, Supplier onMiss) { - return getCache(key, valueType, onMiss, expire, TimeUnit.SECONDS); - } - - @Nullable - public T getCache(final String key, Class valueType, Supplier onMiss, long timeout) { - return getCache(key, valueType, onMiss, timeout, TimeUnit.SECONDS); - } - - @Nullable - public T getCache(final String key, Class valueType, Supplier onMiss, long timeout, TimeUnit timeUnit) { - T result; - try { - String json = redisTemplate.opsForValue().get(key); - if (StringUtils.isEmpty(json)) { - if (onMiss != null) { - //上锁 - synchronized (RedisCache.class) { - //再次查询缓存,目的是判断是否前面的线程已经set过了 - json = redisTemplate.opsForValue().get(key); - //第二次校验缓存是否存在 - if (StringUtils.isEmpty(json)) { - result = onMiss.get(); - //数据库中不存在 - if (result == null) { - return null; - } - setCache(key, result, timeout, timeUnit); - return result; - } - } - } else { - return null; - } - } - result = readMapper.readValue(json, valueType); - } catch (Exception e) { - log.error(e.getMessage(), e); - return null; - } - return result; - } - - public void updateCache(final String key, Class valueType, T defaultValue, Function onUpdate) { - updateCache(key, valueType, defaultValue, onUpdate, expire, TimeUnit.SECONDS); - } - - public void updateCache(final String key, Class valueType, T defaultValue, Function onUpdate, long timeout) { - updateCache(key, valueType, defaultValue, onUpdate, timeout, TimeUnit.SECONDS); - } - - public void updateCache(final String key, Class valueType, T defaultValue, Function onUpdate, long timeout, TimeUnit timeUnit) { - T result; - try { - synchronized (RedisCache.class) { - String json = redisTemplate.opsForValue().get(key); - if (StringUtils.isEmpty(json)) { - result = defaultValue; - } else { - result = readMapper.readValue(json, valueType); - } - result = onUpdate.apply(result); - setCache(key, result, timeout, timeUnit); - } - } catch (Exception e) { - log.error(e.getMessage(), e); - } - } - - @Nullable - public String getCacheLevelCommit() { - return getCache("level:commit", String.class); - } - - public void setCacheLevelCommit(String commit) { - setData("level:commit", commit); - } - - public void removeCache(String key) { - redisTemplate.delete(key); - } - - /** - * 相同则删除键值对 - * - * @param key 待比较和删除的键 - * @param value 待比较的值 - * @return 是否删除 - */ - public boolean removeKVIfEquals(String key, T value) { - String json = getJson(value); - if (json == null) return false; - return Boolean.TRUE.equals( - redisTemplate.execute(removeKVIfEqualsScript, List.of(key), json) - ); - } - - /** - * 模糊删除缓存。 - * - * @param pattern 待删除的 Key 表达式,例如 "home:*" 表示删除 Key 以 "home:" 开头的所有缓存 - * @author Lixuhuilll - */ - public void removeCacheByPattern(String pattern) { - // 批量删除的阈值 - final int batchSize = 10000; - // 构造 ScanOptions - ScanOptions scanOptions = ScanOptions.scanOptions() - .count(batchSize) - .match(pattern) - .build(); - - // 保存要删除的键 - List keysToDelete = new ArrayList<>(); - - // try-with-resources 自动关闭 SCAN - try (Cursor cursor = redisTemplate.scan(scanOptions)) { - while (cursor.hasNext()) { - String key = cursor.next(); - // 将要删除的键添加到列表中 - keysToDelete.add(key); - - // 如果达到批量删除的阈值,则执行批量删除 - if (keysToDelete.size() >= batchSize) { - redisTemplate.delete(keysToDelete); - keysToDelete.clear(); - } - } - } - - // 删除剩余的键(不足 batchSize 的最后一批) - if (!keysToDelete.isEmpty()) { - redisTemplate.delete(keysToDelete); - } - } - - - @Nullable - private String getJson(T value) { - String json; - try { - json = writeMapper.writeValueAsString(value); - } catch (JsonProcessingException e) { - if (log.isDebugEnabled()) { - log.debug(e.getMessage(), e); - } - return null; - } - return json; - } -} diff --git a/src/main/java/plus/maa/backend/repository/UserRepository.java b/src/main/java/plus/maa/backend/repository/UserRepository.java deleted file mode 100644 index 35112d39..00000000 --- a/src/main/java/plus/maa/backend/repository/UserRepository.java +++ /dev/null @@ -1,29 +0,0 @@ -package plus.maa.backend.repository; - -import org.springframework.data.mongodb.repository.MongoRepository; -import plus.maa.backend.repository.entity.MaaUser; - -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - -/** - * @author AnselYuki - */ -public interface UserRepository extends MongoRepository { - /** - * 根据邮箱(用户唯一登录凭据)查询 - * - * @param email 邮箱字段 - * @return 查询用户 - */ - MaaUser findByEmail(String email); - - default Map findByUsersId(List userId) { - return findAllById(userId) - .stream().collect(Collectors.toMap(MaaUser::getUserId, Function.identity())); - } - - -} diff --git a/src/main/java/plus/maa/backend/repository/entity/ArkLevel.java b/src/main/java/plus/maa/backend/repository/entity/ArkLevel.java deleted file mode 100644 index da54514c..00000000 --- a/src/main/java/plus/maa/backend/repository/entity/ArkLevel.java +++ /dev/null @@ -1,41 +0,0 @@ -package plus.maa.backend.repository.entity; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.index.Indexed; -import org.springframework.data.mongodb.core.mapping.Document; - -/** - * 地图数据 - * - * @author john180 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Document("maa_level") -public class ArkLevel { - public static final ArkLevel EMPTY = new ArkLevel(); - - @Id - private String id; - private String levelId; - @Indexed - private String stageId; - //文件版本, 用于判断是否需要更新 - private String sha; - //地图类型, 例: 主线、活动、危机合约 - private String catOne; - //所属章节, 例: 怒号光明、寻昼行动 - private String catTwo; - //地图ID, 例: 7-18、FC-1 - private String catThree; - //地图名, 例: 冬逝、爱国者之死 - private String name; - private int width; - private int height; -} diff --git a/src/main/java/plus/maa/backend/repository/entity/ArkLevelSha.java b/src/main/java/plus/maa/backend/repository/entity/ArkLevelSha.java deleted file mode 100644 index 9657a7ff..00000000 --- a/src/main/java/plus/maa/backend/repository/entity/ArkLevelSha.java +++ /dev/null @@ -1,8 +0,0 @@ -package plus.maa.backend.repository.entity; - -/** - * @author john180 - */ -public interface ArkLevelSha { - String getSha(); -} diff --git a/src/main/java/plus/maa/backend/repository/entity/CommentsArea.java b/src/main/java/plus/maa/backend/repository/entity/CommentsArea.java deleted file mode 100644 index f16b3c0d..00000000 --- a/src/main/java/plus/maa/backend/repository/entity/CommentsArea.java +++ /dev/null @@ -1,55 +0,0 @@ -package plus.maa.backend.repository.entity; - -import lombok.Data; -import lombok.experimental.Accessors; -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.index.Indexed; -import org.springframework.data.mongodb.core.mapping.Document; - -import java.io.Serializable; -import java.time.LocalDateTime; - -/** - * @author LoMu - * Date 2023-02-17 14:50 - */ -@Data -@Accessors(chain = true) -@Document("maa_comments_area") -public class CommentsArea implements Serializable { - - @Id - private String id; - - @Indexed - private Long copilotId; - - //答复某个评论 - private String fromCommentId; - - - private String uploaderId; - - //评论内容 - private String message; - - private long likeCount; - - private LocalDateTime uploadTime = LocalDateTime.now(); - - // 是否将该评论置顶 - private boolean topping; - - private boolean delete; - - private LocalDateTime deleteTime; - - //其主评论id(如果自身为主评论则为null) - private String mainCommentId; - - //邮件通知 - private Boolean notification; - -} - - diff --git a/src/main/java/plus/maa/backend/repository/entity/Copilot.java b/src/main/java/plus/maa/backend/repository/entity/Copilot.java deleted file mode 100644 index ce2705ca..00000000 --- a/src/main/java/plus/maa/backend/repository/entity/Copilot.java +++ /dev/null @@ -1,194 +0,0 @@ -package plus.maa.backend.repository.entity; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.annotation.JsonNaming; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.index.Indexed; -import org.springframework.data.mongodb.core.mapping.Document; - -import java.io.Serializable; -import java.time.LocalDateTime; -import java.util.List; - -/** - * @author LoMu - * Date 2022-12-25 17:56 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) -@Accessors(chain = true) -@Document("maa_copilot") -public class Copilot implements Serializable { - @Id - // 作业id - private String id; - // 自增数字ID - @Indexed(unique = true) - private Long copilotId; - // 关卡名 - @Indexed - private String stageName; - - // 上传者id - private String uploaderId; - - // 查看次数 - private Long views = 0L; - - //评级 - private int ratingLevel; - - //评级比率 十分之一代表半星 - private double ratingRatio; - - private long likeCount; - - private long dislikeCount; - - // 热度 - private double hotScore; - - // 难度 - private int difficulty; - - // 版本号(文档中说明:最低要求 maa 版本号,必选。保留字段) - - private String minimumRequired; - - // 指定干员 - private List opers; - // 群组 - private List groups; - // 战斗中的操作 - private List actions; - - // 描述 - private Doc doc; - - private LocalDateTime firstUploadTime; - private LocalDateTime uploadTime; - - // 原始数据 - private String content; - - @JsonIgnore - private boolean delete; - @JsonIgnore - private LocalDateTime deleteTime; - @JsonIgnore - private Boolean notification; - - @Data - @NoArgsConstructor - @AllArgsConstructor - @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) - public static class OperationGroup implements Serializable { - // 干员名 - private String name; - // 技能序号。可选,默认 1 - private int skill = 1; - // 技能用法。可选,默认 0 - private int skillUsage; - - } - - @Data - @NoArgsConstructor - @AllArgsConstructor - @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) - public static class Operators implements Serializable { - // 干员名 - private String name; - // 技能序号。可选,默认 1 - private int skill = 1; - // 技能用法。可选,默认 0 - private int skillUsage; - private Requirements requirements = new Requirements(); - - @Data - @NoArgsConstructor - @AllArgsConstructor - @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) - public static class Requirements implements Serializable { - // 精英化等级。可选,默认为 0, 不要求精英化等级 - private int elite; - // 干员等级。可选,默认为 0 - private int level; - - // 技能等级。可选,默认为 0 - private int skillLevel; - // 模组编号。可选,默认为 0 - private int module; - // 潜能要求。可选,默认为 0 - private int potentiality; - - } - } - - @Data - @NoArgsConstructor - @AllArgsConstructor - @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) - public static class Groups implements Serializable { - // 群组名 - private String name; - - private List opers; - - private List operators; - } - - @Data - @NoArgsConstructor - @AllArgsConstructor - @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) - public static class Action implements Serializable { - // 操作类型,可选,默认 "Deploy" - private String type = "Deploy"; - private int kills; - private int costs; - private int costChanges; - // 默认 -1 - private int cooling = -1; - - private String name; - - // 部署干员的位置。 - private Integer[] location; - // 部署干员的干员朝向 中英文皆可 - private String direction = "None"; - // 修改技能用法。当 type 为 "技能用法" 时必选 - private int skillUsage; - // 前置延时 - private int preDelay; - // 后置延时 - private int postDelay; - // maa:保留字段,暂未实现 - private long timeout; - - // 描述 - private String doc = ""; - private String docColor = "Gray"; - - } - - @Data - @NoArgsConstructor - @AllArgsConstructor - @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) - public static class Doc implements Serializable { - - private String title; - private String titleColor = "Gray"; - private String details = ""; - private String detailsColor = "Gray"; - - } -} diff --git a/src/main/java/plus/maa/backend/repository/entity/MaaUser.java b/src/main/java/plus/maa/backend/repository/entity/MaaUser.java deleted file mode 100644 index 06561db3..00000000 --- a/src/main/java/plus/maa/backend/repository/entity/MaaUser.java +++ /dev/null @@ -1,44 +0,0 @@ -package plus.maa.backend.repository.entity; - -import com.fasterxml.jackson.annotation.JsonInclude; -import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.index.Indexed; -import org.springframework.data.mongodb.core.mapping.Document; -import plus.maa.backend.controller.request.user.UserInfoUpdateDTO; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; - -/** - * @author AnselYuki - */ -@Data -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@JsonInclude(JsonInclude.Include.NON_NULL) -@Document("maa_user") -public class MaaUser implements Serializable { - @Id - private String userId; - private String userName; - @Indexed(unique = true) - @NotBlank(message = "邮箱为唯一身份标识,不能为空") - private String email; - private String password; - private Integer status = 0; - private List refreshJwtIds = new ArrayList<>(); - - public void updateAttribute(UserInfoUpdateDTO updateDTO) { - String userName = updateDTO.getUserName(); - if (!userName.isBlank()) { - this.userName = userName; - } - } -} diff --git a/src/main/java/plus/maa/backend/repository/entity/Rating.java b/src/main/java/plus/maa/backend/repository/entity/Rating.java deleted file mode 100644 index 911fc5ca..00000000 --- a/src/main/java/plus/maa/backend/repository/entity/Rating.java +++ /dev/null @@ -1,49 +0,0 @@ -package plus.maa.backend.repository.entity; - -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.index.CompoundIndex; -import org.springframework.data.mongodb.core.index.CompoundIndexes; -import org.springframework.data.mongodb.core.mapping.Document; -import plus.maa.backend.service.model.RatingType; - -import java.time.LocalDateTime; - -/** - * @author lixuhuilll - * Date 2023-08-20 11:20 - */ - -@Data -@Accessors(chain = true) -@NoArgsConstructor -@AllArgsConstructor -@Document(collection = "maa_rating") -// 复合索引 -@CompoundIndexes({ - // 一个用户对一个对象只能有一种评级 - @CompoundIndex(name = "idx_rating", def = "{'type': 1, 'key': 1, 'userId': 1}", unique = true) -}) -public class Rating { - @Id - private String id; - - // 下面三个字段组成复合索引,一个用户对一个对象只能有一种评级 - @NotNull - private KeyType type; // 评级的类型,如作业(copilot)、评论(comment) - @NotNull - private String key; // 被评级对象的唯一标识,如作业id、评论id - @NotNull - private String userId; // 评级的用户id - - private RatingType rating; // 评级,如 "Like"、"Dislike"、"None" - private LocalDateTime rateTime; // 评级时间 - - public enum KeyType { - COPILOT, COMMENT - } -} diff --git a/src/main/java/plus/maa/backend/repository/entity/gamedata/ArkActivity.java b/src/main/java/plus/maa/backend/repository/entity/gamedata/ArkActivity.java deleted file mode 100644 index 215b80bf..00000000 --- a/src/main/java/plus/maa/backend/repository/entity/gamedata/ArkActivity.java +++ /dev/null @@ -1,13 +0,0 @@ -package plus.maa.backend.repository.entity.gamedata; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class ArkActivity { - private String id; - private String name; -} diff --git a/src/main/java/plus/maa/backend/repository/entity/gamedata/ArkCharacter.java b/src/main/java/plus/maa/backend/repository/entity/gamedata/ArkCharacter.java deleted file mode 100644 index 91da06ee..00000000 --- a/src/main/java/plus/maa/backend/repository/entity/gamedata/ArkCharacter.java +++ /dev/null @@ -1,15 +0,0 @@ -package plus.maa.backend.repository.entity.gamedata; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class ArkCharacter { - private String id; - private String name; - private String profession; - private int rarity; -} diff --git a/src/main/java/plus/maa/backend/repository/entity/gamedata/ArkStage.java b/src/main/java/plus/maa/backend/repository/entity/gamedata/ArkStage.java deleted file mode 100644 index a2cc37e4..00000000 --- a/src/main/java/plus/maa/backend/repository/entity/gamedata/ArkStage.java +++ /dev/null @@ -1,28 +0,0 @@ -package plus.maa.backend.repository.entity.gamedata; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class ArkStage { - /** - * 关卡ID, 需转换为全小写后使用
- * 例: Activities/ACT5D0/level_act5d0_ex08 - */ - private String levelId; - /** - * 例: act14d7_zone2 - */ - private String zoneId; - /** - * 例: act5d0_ex08 - */ - private String stageId; - /** - * 例: CB-EX8 - */ - private String code; -} diff --git a/src/main/java/plus/maa/backend/repository/entity/gamedata/ArkTilePos.java b/src/main/java/plus/maa/backend/repository/entity/gamedata/ArkTilePos.java deleted file mode 100644 index 76628c0f..00000000 --- a/src/main/java/plus/maa/backend/repository/entity/gamedata/ArkTilePos.java +++ /dev/null @@ -1,37 +0,0 @@ -package plus.maa.backend.repository.entity.gamedata; - -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.annotation.JsonNaming; -import lombok.Data; - -import java.util.List; - -/** - * 地图格子数据 - * - * @author dragove - * created on 2022/12/23 - */ -@Data -@JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy.class) -public class ArkTilePos { - - private String code; - private Integer height; - private Integer width; - private String levelId; - private String name; - private String stageId; - private List> tiles; - private List> view; - - @Data - @JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy.class) - public static class Tile { - private String tileKey; - private Integer heightType; - private Integer buildableType; - private Boolean isStart; - private Boolean isEnd; - } -} diff --git a/src/main/java/plus/maa/backend/repository/entity/gamedata/ArkTower.java b/src/main/java/plus/maa/backend/repository/entity/gamedata/ArkTower.java deleted file mode 100644 index 0f07dd95..00000000 --- a/src/main/java/plus/maa/backend/repository/entity/gamedata/ArkTower.java +++ /dev/null @@ -1,14 +0,0 @@ -package plus.maa.backend.repository.entity.gamedata; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class ArkTower { - private String id; - private String name; - private String subName; -} diff --git a/src/main/java/plus/maa/backend/repository/entity/gamedata/ArkZone.java b/src/main/java/plus/maa/backend/repository/entity/gamedata/ArkZone.java deleted file mode 100644 index 557b5443..00000000 --- a/src/main/java/plus/maa/backend/repository/entity/gamedata/ArkZone.java +++ /dev/null @@ -1,23 +0,0 @@ -package plus.maa.backend.repository.entity.gamedata; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class ArkZone { - /** - * 例: main_1 - */ - private String zoneId; - /** - * 例: 第一章 - */ - private String zoneNameFirst; - /** - * 例: 黑暗时代·下 - */ - private String zoneNameSecond; -} diff --git a/src/main/java/plus/maa/backend/repository/entity/github/GithubCommit.java b/src/main/java/plus/maa/backend/repository/entity/github/GithubCommit.java deleted file mode 100644 index 16a5218f..00000000 --- a/src/main/java/plus/maa/backend/repository/entity/github/GithubCommit.java +++ /dev/null @@ -1,15 +0,0 @@ -package plus.maa.backend.repository.entity.github; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * @author john180 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class GithubCommit { - private String sha; -} diff --git a/src/main/java/plus/maa/backend/repository/entity/github/GithubContent.java b/src/main/java/plus/maa/backend/repository/entity/github/GithubContent.java deleted file mode 100644 index eb69dda6..00000000 --- a/src/main/java/plus/maa/backend/repository/entity/github/GithubContent.java +++ /dev/null @@ -1,49 +0,0 @@ -package plus.maa.backend.repository.entity.github; - -import lombok.Data; -import org.apache.commons.lang3.StringUtils; - -import java.util.Objects; - -/** - * @author dragove - * created on 2022/12/23 - */ -@Data -public class GithubContent { - - // 文件名 - private String name; - // 路径 - private String path; - private String sha; - // 文件大小(Byte) - private Long size; - // 路径类型 file-文件 dir-目录 - private String type; - // 下载地址 - private String downloadUrl; - // 访问地址 - private String htmlUrl; - // 对应commit地址 - private String gitUrl; - - /** - * 仿照File类,判断是否目录类型 - * - * @return 如果是目录类型,则返回 true,文件类型则返回 false - */ - public boolean isDir() { - return Objects.equals(type, "dir"); - } - - public String getFileExtension() { - return name == null ? - StringUtils.EMPTY : - (name.contains(".") ? - name.substring(name.lastIndexOf(".") + 1) : - StringUtils.EMPTY - ); - } - -} diff --git a/src/main/java/plus/maa/backend/repository/entity/github/GithubTree.java b/src/main/java/plus/maa/backend/repository/entity/github/GithubTree.java deleted file mode 100644 index 1234c0fa..00000000 --- a/src/main/java/plus/maa/backend/repository/entity/github/GithubTree.java +++ /dev/null @@ -1,19 +0,0 @@ -package plus.maa.backend.repository.entity.github; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * @author john180 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class GithubTree { - private String path; - private String mode; - private String type; - private String sha; - private String url; -} diff --git a/src/main/java/plus/maa/backend/repository/entity/github/GithubTrees.java b/src/main/java/plus/maa/backend/repository/entity/github/GithubTrees.java deleted file mode 100644 index af0f8922..00000000 --- a/src/main/java/plus/maa/backend/repository/entity/github/GithubTrees.java +++ /dev/null @@ -1,19 +0,0 @@ -package plus.maa.backend.repository.entity.github; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -/** - * @author john180 - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class GithubTrees { - private String sha; - private String url; - private List tree; -} diff --git a/src/main/java/plus/maa/backend/service/ArkGameDataService.java b/src/main/java/plus/maa/backend/service/ArkGameDataService.java deleted file mode 100644 index 5edd5f99..00000000 --- a/src/main/java/plus/maa/backend/service/ArkGameDataService.java +++ /dev/null @@ -1,214 +0,0 @@ -package plus.maa.backend.service; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.json.JsonMapper; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; -import org.springframework.stereotype.Service; -import org.springframework.util.ObjectUtils; -import plus.maa.backend.repository.entity.gamedata.*; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * @author john180 - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class ArkGameDataService { - private static final String ARK_STAGE = "https://raw.githubusercontent.com/yuanyan3060/ArknightsGameResource/main/gamedata/excel/stage_table.json"; - private static final String ARK_ZONE = "https://raw.githubusercontent.com/yuanyan3060/ArknightsGameResource/main/gamedata/excel/zone_table.json"; - private static final String ARK_ACTIVITY = "https://raw.githubusercontent.com/yuanyan3060/ArknightsGameResource/main/gamedata/excel/activity_table.json"; - private static final String ARK_CHARACTER = "https://raw.githubusercontent.com/yuanyan3060/ArknightsGameResource/main/gamedata/excel/character_table.json"; - private static final String ARK_TOWER = "https://raw.githubusercontent.com/yuanyan3060/ArknightsGameResource/main/gamedata/excel/climb_tower_table.json"; - private final OkHttpClient okHttpClient; - private final ObjectMapper mapper = JsonMapper.builder() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .build(); - private Map stageMap = new ConcurrentHashMap<>(); - private Map levelStageMap = new ConcurrentHashMap<>(); - private Map zoneMap = new ConcurrentHashMap<>(); - private Map zoneActivityMap = new ConcurrentHashMap<>(); - private Map arkCharacterMap = new ConcurrentHashMap<>(); - private Map arkTowerMap = new ConcurrentHashMap<>(); - - public void syncGameData() { - syncStage(); - syncZone(); - syncActivity(); - syncCharacter(); - syncTower(); - } - - public ArkStage findStage(String levelId, String code, String stageId) { - ArkStage stage = levelStageMap.get(levelId.toLowerCase()); - if (stage != null && stage.getCode().equalsIgnoreCase(code)) { - return stage; - } - return stageMap.get(stageId); - } - - public ArkZone findZone(String levelId, String code, String stageId) { - ArkStage stage = findStage(levelId, code, stageId); - if (stage == null) { - log.error("[DATA]stage不存在:{}, Level: {}", stageId, levelId); - return null; - } - ArkZone zone = zoneMap.get(stage.getZoneId()); - if (zone == null) { - log.error("[DATA]zone不存在:{}, Level: {}", stage.getZoneId(), levelId); - } - return zone; - } - - public ArkTower findTower(String zoneId) { - return arkTowerMap.get(zoneId); - } - - public ArkCharacter findCharacter(String characterId) { - String[] ids = characterId.split("_"); - return arkCharacterMap.get(ids[ids.length - 1]); - } - - public ArkActivity findActivityByZoneId(String zoneId) { - return zoneActivityMap.get(zoneId); - } - - private void syncStage() { - Request req = new Request.Builder().url(ARK_STAGE).get().build(); - try (Response rsp = okHttpClient.newCall(req).execute()) { - ResponseBody body = rsp.body(); - if (body == null) { - log.error("[DATA]获取stage数据失败"); - return; - } - JsonNode node = mapper.reader().readTree(body.string()); - JsonNode stagesNode = node.get("stages"); - Map temp = mapper.convertValue(stagesNode, new TypeReference<>() { - }); - stageMap = new ConcurrentHashMap<>(); - levelStageMap = new ConcurrentHashMap<>(); - temp.forEach((k, v) -> { - stageMap.put(k, v); - if (!ObjectUtils.isEmpty(v.getLevelId())) { - levelStageMap.put(v.getLevelId().toLowerCase(), v); - } - }); - - log.info("[DATA]获取stage数据成功, 共{}条", levelStageMap.size()); - } catch (Exception e) { - log.error("[DATA]同步stage数据异常", e); - } - } - - private void syncZone() { - Request req = new Request.Builder().url(ARK_ZONE).get().build(); - try (Response rsp = okHttpClient.newCall(req).execute()) { - ResponseBody body = rsp.body(); - if (body == null) { - log.error("[DATA]获取zone数据失败"); - return; - } - JsonNode node = mapper.reader().readTree(body.string()); - JsonNode zonesNode = node.get("zones"); - Map temp = mapper.convertValue(zonesNode, new TypeReference<>() { - }); - zoneMap = new ConcurrentHashMap<>(temp); - log.info("[DATA]获取zone数据成功, 共{}条", zoneMap.size()); - } catch (Exception e) { - log.error("[DATA]同步zone数据异常", e); - } - } - - private void syncActivity() { - Request req = new Request.Builder().url(ARK_ACTIVITY).get().build(); - try (Response rsp = okHttpClient.newCall(req).execute()) { - ResponseBody body = rsp.body(); - if (body == null) { - log.error("[DATA]获取activity数据失败"); - return; - } - JsonNode node = mapper.reader().readTree(body.string()); - - //zoneId转换活动Id - JsonNode zonesNode = node.get("zoneToActivity"); - Map zoneToActivity = mapper.convertValue(zonesNode, new TypeReference<>() { - }); - //活动信息 - JsonNode baseInfoNode = node.get("basicInfo"); - Map baseInfos = mapper.convertValue(baseInfoNode, new TypeReference<>() { - }); - Map temp = new ConcurrentHashMap<>(); - zoneToActivity.forEach((zoneId, actId) -> { - ArkActivity act = baseInfos.get(actId); - if (act != null) { - temp.put(zoneId, act); - } - }); - zoneActivityMap = temp; - - log.info("[DATA]获取activity数据成功, 共{}条", zoneActivityMap.size()); - } catch (Exception e) { - log.error("[DATA]同步activity数据异常", e); - } - } - - private void syncCharacter() { - Request req = new Request.Builder().url(ARK_CHARACTER).get().build(); - try (Response rsp = okHttpClient.newCall(req).execute()) { - ResponseBody body = rsp.body(); - if (body == null) { - log.error("[DATA]获取character数据失败"); - return; - } - JsonNode node = mapper.reader().readTree(body.string()); - Map characters = mapper.convertValue(node, new TypeReference<>() { - }); - characters.forEach((id, c) -> c.setId(id)); - Map temp = new ConcurrentHashMap<>(); - characters.values().forEach(c -> { - if (ObjectUtils.isEmpty(c.getId())) { - return; - } - String[] ids = c.getId().split("_"); - if (ids.length != 3) { - //不是干员 - return; - } - temp.put(ids[2], c); - }); - arkCharacterMap = temp; - - log.info("[DATA]获取character数据成功, 共{}条", arkCharacterMap.size()); - } catch (Exception e) { - log.error("[DATA]同步character数据异常", e); - } - } - - private void syncTower() { - Request req = new Request.Builder().url(ARK_TOWER).get().build(); - try (Response rsp = okHttpClient.newCall(req).execute()) { - ResponseBody body = rsp.body(); - if (body == null) { - log.error("[DATA]获取tower数据失败"); - return; - } - JsonNode node = mapper.reader().readTree(body.string()); - JsonNode towerNode = node.get("towers"); - arkTowerMap = mapper.convertValue(towerNode, new TypeReference<>() { - }); - log.info("[DATA]获取tower数据成功, 共{}条", arkTowerMap.size()); - } catch (Exception e) { - log.error("[DATA]同步tower数据异常", e); - } - } -} diff --git a/src/main/java/plus/maa/backend/service/ArkLevelParserService.java b/src/main/java/plus/maa/backend/service/ArkLevelParserService.java deleted file mode 100644 index abfd520b..00000000 --- a/src/main/java/plus/maa/backend/service/ArkLevelParserService.java +++ /dev/null @@ -1,59 +0,0 @@ -package plus.maa.backend.service; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.lang.Nullable; -import org.springframework.stereotype.Service; -import plus.maa.backend.repository.entity.ArkLevel; -import plus.maa.backend.repository.entity.gamedata.ArkTilePos; -import plus.maa.backend.service.model.ArkLevelType; -import plus.maa.backend.service.model.parser.ArkLevelParser; - -import java.util.List; - -/** - * @author john180 - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class ArkLevelParserService { - private final List parsers; - - /** - * 具体地图信息生成规则见 - * GameDataParser - * 尚未全部实现
- * TODO 完成剩余字段实现 - */ - @Nullable - public ArkLevel parseLevel(ArkTilePos tilePos, String sha) { - ArkLevel level = ArkLevel.builder() - .levelId(tilePos.getLevelId()) - .stageId(tilePos.getStageId()) - .sha(sha) - .catThree(tilePos.getCode()) - .name(tilePos.getName()) - .width(tilePos.getWidth()) - .height(tilePos.getHeight()) - .build(); - return parseLevel(level, tilePos); - } - - private ArkLevel parseLevel(ArkLevel level, ArkTilePos tilePos) { - ArkLevelType type = ArkLevelType.fromLevelId(level.getLevelId()); - if (ArkLevelType.UNKNOWN == type) { - log.warn("[PARSER]未知关卡类型:{}", level.getLevelId()); - return null; - } - ArkLevelParser parser = parsers.stream() - .filter(p -> p.supportType(type)) - .findFirst() - .orElse(null); - if (parser == null) { - //类型存在但无对应Parser直接跳过 - return ArkLevel.EMPTY; - } - return parser.parseLevel(level, tilePos); - } -} diff --git a/src/main/java/plus/maa/backend/service/ArkLevelService.java b/src/main/java/plus/maa/backend/service/ArkLevelService.java deleted file mode 100644 index d7ad06de..00000000 --- a/src/main/java/plus/maa/backend/service/ArkLevelService.java +++ /dev/null @@ -1,253 +0,0 @@ -package plus.maa.backend.service; - -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.Data; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import okhttp3.*; -import org.jetbrains.annotations.NotNull; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; -import plus.maa.backend.common.utils.converter.ArkLevelConverter; -import plus.maa.backend.controller.response.copilot.ArkLevelInfo; -import plus.maa.backend.repository.ArkLevelRepository; -import plus.maa.backend.repository.GithubRepository; -import plus.maa.backend.repository.RedisCache; -import plus.maa.backend.repository.entity.ArkLevel; -import plus.maa.backend.repository.entity.ArkLevelSha; -import plus.maa.backend.repository.entity.gamedata.ArkTilePos; -import plus.maa.backend.repository.entity.github.GithubCommit; -import plus.maa.backend.repository.entity.github.GithubTree; -import plus.maa.backend.repository.entity.github.GithubTrees; - -import java.io.IOException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -/** - * @author dragove - * created on 2022/12/23 - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class ArkLevelService { - /** - * Github api调用token 从 tokens 获取 - */ - @Value("${maa-copilot.github.token:}") - private String githubToken; - /** - * maa 主仓库,一般不变 - */ - @Value("${maa-copilot.github.repo:MaaAssistantArknights/MaaAssistantArknights/dev}") - private String maaRepoAndBranch; - /** - * 地图数据所在路径 - */ - @Value("${maa-copilot.github.repo.tile.path:resource/Arknights-Tile-Pos}") - private String tilePosPath; - - private final GithubRepository githubRepo; - private final RedisCache redisCache; - private final ArkLevelRepository arkLevelRepo; - private final ArkLevelParserService parserService; - private final ArkGameDataService gameDataService; - private final ObjectMapper mapper = new ObjectMapper(); - private final OkHttpClient okHttpClient; - private final ArkLevelConverter arkLevelConverter; - - private final List bypassFileNames = List.of("overview.json"); - - @Cacheable("arkLevels") - public List getArkLevelInfos() { - return arkLevelRepo.findAll() - .stream() - .map(arkLevelConverter::convert) - .collect(Collectors.toList()); - } - - @Cacheable("arkLevel") - public ArkLevelInfo findByLevelIdFuzzy(String levelId) { - ArkLevel level = arkLevelRepo.findByLevelIdFuzzy(levelId).findAny().orElse(null); - return arkLevelConverter.convert(level); - } - - - public List queryLevelByKeyword(String keyword) { - List levels = arkLevelRepo.queryLevelByKeyword(keyword).collect(Collectors.toList()); - return arkLevelConverter.convert(levels); - } - - /** - * 地图数据更新任务 - */ - @Async - public void runSyncLevelDataTask() { - log.info("[LEVEL]开始同步地图数据"); - //获取地图文件夹最新的commit, 用于判断是否需要更新 - List commits = githubRepo.getCommits(githubToken); - if (CollectionUtils.isEmpty(commits)) { - log.info("[LEVEL]获取地图数据最新commit失败"); - return; - } - //与缓存的commit比较,如果相同则不更新 - GithubCommit commit = commits.get(0); - String lastCommit = redisCache.getCacheLevelCommit(); - if (lastCommit != null && lastCommit.equals(commit.getSha())) { - log.info("[LEVEL]地图数据已是最新"); - return; - } - //获取根目录文件列表 - GithubTrees trees; - List files = Arrays.stream(tilePosPath.split("/")).toList(); - trees = githubRepo.getTrees(githubToken, commit.getSha()); - //根据路径获取文件列表 - for (String file : files) { - if (trees == null || CollectionUtils.isEmpty(trees.getTree())) { - log.info("[LEVEL]地图数据获取失败"); - return; - } - GithubTree tree = trees.getTree().stream() - .filter(t -> t.getPath().equals(file) && t.getType().equals("tree")) - .findFirst() - .orElse(null); - if (tree == null) { - log.info("[LEVEL]地图数据获取失败, 未找到文件夹{}", file); - return; - } - trees = githubRepo.getTrees(githubToken, tree.getSha()); - } - if (trees == null || CollectionUtils.isEmpty(trees.getTree())) { - log.info("[LEVEL]地图数据获取失败, 未找到文件夹{}", tilePosPath); - return; - } - //根据后缀筛选地图文件列表 - List levelTrees = trees.getTree().stream() - .filter(t -> t.getType().equals("blob") && t.getPath().endsWith(".json")) - .collect(Collectors.toList()); - log.info("[LEVEL]已发现{}份地图数据", levelTrees.size()); - - //根据sha筛选无需更新的地图 - List shaList = arkLevelRepo.findAllShaBy().stream().map(ArkLevelSha::getSha).toList(); - levelTrees.removeIf(t -> shaList.contains(t.getSha())); - // 排除overview文件、肉鸽、训练关卡和 Guide? 不知道是啥 - levelTrees.removeIf(t -> t.getPath().equals("overview.json") || - t.getPath().contains("roguelike") || - t.getPath().startsWith("tr_") || - t.getPath().startsWith("guide_")); - levelTrees.removeIf(t -> t.getPath().contains("roguelike")); - log.info("[LEVEL]{}份地图数据需要更新", levelTrees.size()); - if (levelTrees.isEmpty()) { - return; - } - //同步GameData仓库数据 - gameDataService.syncGameData(); - - DownloadTask task = new DownloadTask(levelTrees.size(), (t) -> { - //仅在全部下载任务成功后更新commit缓存 - if (t.isAllSuccess()) { - redisCache.setCacheLevelCommit(commit.getSha()); - } - }); - levelTrees.forEach(tree -> download(task, tree)); - } - - /** - * 下载地图数据 - */ - private void download(DownloadTask task, GithubTree tree) { - String fileName = URLEncoder.encode(tree.getPath(), StandardCharsets.UTF_8); - if (bypassFileNames.contains(fileName)) { - task.success(); - return; - } - String url = String.format("https://raw.githubusercontent.com/%s/%s/%s", maaRepoAndBranch, tilePosPath, fileName); - okHttpClient.newCall(new Request.Builder().url(url).build()).enqueue(new Callback() { - @Override - public void onFailure(@NotNull Call call, @NotNull IOException e) { - log.error("[LEVEL]下载地图数据失败:" + tree.getPath(), e); - } - - @Override - public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { - try (ResponseBody rspBody = response.body()) { - if (!response.isSuccessful() || rspBody == null) { - task.fail(); - log.error("[LEVEL]下载地图数据失败:" + tree.getPath()); - return; - } - ArkTilePos tilePos = mapper.readValue(rspBody.string(), ArkTilePos.class); - ArkLevel level = parserService.parseLevel(tilePos, tree.getSha()); - if (level == null) { - task.fail(); - log.info("[LEVEL]地图数据解析失败:" + tree.getPath()); - return; - } else if (level == ArkLevel.EMPTY) { - task.pass(); - return; - } - arkLevelRepo.save(level); - - task.success(); - log.info("[LEVEL]下载地图数据 {} 成功, 进度{}/{}, 用时:{}s", tilePos.getName(), task.getCurrent(), task.getTotal(), task.getDuration()); - } - } - }); - } - - @Data - @RequiredArgsConstructor - private static class DownloadTask { - private final long startTime = System.currentTimeMillis(); - private final AtomicInteger success = new AtomicInteger(0); - private final AtomicInteger fail = new AtomicInteger(0); - private final AtomicInteger pass = new AtomicInteger(0); - private final int total; - private final Consumer finishCallback; - - public void success() { - success.incrementAndGet(); - checkFinish(); - } - - public void fail() { - fail.incrementAndGet(); - checkFinish(); - } - - public void pass() { - pass.incrementAndGet(); - checkFinish(); - } - - public int getCurrent() { - return success.get() + fail.get() + pass.get(); - } - - public int getDuration() { - return (int) (System.currentTimeMillis() - startTime) / 1000; - } - - public boolean isAllSuccess() { - return success.get() + pass.get() == total; - } - - private void checkFinish() { - if (success.get() + fail.get() + pass.get() != total) { - return; - } - finishCallback.accept(this); - log.info("[LEVEL]地图数据下载完成, 成功:{}, 失败:{}, 跳过:{} 总用时{}s", success.get(), fail.get(), pass.get(), getDuration()); - } - } - -} diff --git a/src/main/java/plus/maa/backend/service/CommentsAreaService.java b/src/main/java/plus/maa/backend/service/CommentsAreaService.java deleted file mode 100644 index d5bc14c2..00000000 --- a/src/main/java/plus/maa/backend/service/CommentsAreaService.java +++ /dev/null @@ -1,384 +0,0 @@ -package plus.maa.backend.service; - - -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.stereotype.Service; -import org.springframework.util.Assert; -import plus.maa.backend.common.utils.converter.CommentConverter; -import plus.maa.backend.config.external.MaaCopilotProperties; -import plus.maa.backend.controller.request.comments.CommentsAddDTO; -import plus.maa.backend.controller.request.comments.CommentsQueriesDTO; -import plus.maa.backend.controller.request.comments.CommentsRatingDTO; -import plus.maa.backend.controller.request.comments.CommentsToppingDTO; -import plus.maa.backend.controller.response.comments.CommentsAreaInfo; -import plus.maa.backend.controller.response.comments.CommentsInfo; -import plus.maa.backend.controller.response.comments.SubCommentsInfo; -import plus.maa.backend.repository.CommentsAreaRepository; -import plus.maa.backend.repository.CopilotRepository; -import plus.maa.backend.repository.RatingRepository; -import plus.maa.backend.repository.UserRepository; -import plus.maa.backend.repository.entity.CommentsArea; -import plus.maa.backend.repository.entity.Copilot; -import plus.maa.backend.repository.entity.MaaUser; -import plus.maa.backend.repository.entity.Rating; -import plus.maa.backend.service.model.CommentNotification; -import plus.maa.backend.service.model.RatingType; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; - -/** - * @author LoMu - * Date 2023-02-17 15:00 - */ - -@Service -@RequiredArgsConstructor -public class CommentsAreaService { - private final CommentsAreaRepository commentsAreaRepository; - - private final RatingRepository ratingRepository; - - private final CopilotRepository copilotRepository; - - private final UserRepository userRepository; - - private final EmailService emailService; - - private final MaaCopilotProperties maaCopilotProperties; - - private final MaaUser UNKNOWN_USER = new MaaUser().setUserName("未知用户:("); - - private final CommentConverter commentConverter; - - - /** - * 评论 - * 每个评论都有一个uuid加持 - * - * @param userId 登录用户 id - * @param commentsAddDTO CommentsRequest - */ - public void addComments(String userId, CommentsAddDTO commentsAddDTO) { - long copilotId = Long.parseLong(commentsAddDTO.getCopilotId()); - String message = commentsAddDTO.getMessage(); - Optional copilotOptional = copilotRepository.findByCopilotId(copilotId); - Assert.isTrue(StringUtils.isNotBlank(message), "评论不可为空"); - Assert.isTrue(copilotOptional.isPresent(), "作业表不存在"); - - - String fromCommentsId = null; - String mainCommentsId = null; - - CommentsArea commentsArea = null; - Boolean isCopilotAuthor = null; - - - //代表这是一条回复评论 - if (StringUtils.isNoneBlank(commentsAddDTO.getFromCommentId())) { - - Optional commentsAreaOptional = commentsAreaRepository.findById(commentsAddDTO.getFromCommentId()); - Assert.isTrue(commentsAreaOptional.isPresent(), "回复的评论不存在"); - commentsArea = commentsAreaOptional.get(); - Assert.isTrue(!commentsArea.isDelete(), "回复的评论不存在"); - - mainCommentsId = StringUtils - .isNoneBlank(commentsArea.getMainCommentId()) ? - commentsArea.getMainCommentId() : commentsArea.getId(); - - fromCommentsId = StringUtils - .isNoneBlank(commentsArea.getId()) ? - commentsArea.getId() : null; - - if (Objects.isNull(commentsArea.getNotification()) || commentsArea.getNotification()) { - isCopilotAuthor = false; - } - - } else { - isCopilotAuthor = true; - } - - //判断是否需要通知 - if (Objects.nonNull(isCopilotAuthor) && maaCopilotProperties.getMail().getNotification()) { - Copilot copilot = copilotOptional.get(); - - //通知作业作者或是评论作者 - String replyUserId = isCopilotAuthor ? copilot.getUploaderId() : commentsArea.getUploaderId(); - - - Map maaUserMap = userRepository.findByUsersId(List.of(userId, replyUserId)); - - //防止通知自己 - if (!Objects.equals(replyUserId, userId)) { - LocalDateTime time = LocalDateTime.now(); - String timeStr = time.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); - CommentNotification commentNotification = new CommentNotification(); - - - String authorName = maaUserMap.getOrDefault(replyUserId, UNKNOWN_USER).getUserName(); - String reName = maaUserMap.getOrDefault(userId, UNKNOWN_USER).getUserName(); - - String title = isCopilotAuthor ? copilot.getDoc().getTitle() : commentsArea.getMessage(); - - commentNotification - .setTitle(title) - .setDate(timeStr) - .setAuthorName(authorName) - .setReName(reName) - .setReMessage(message); - - - MaaUser maaUser = maaUserMap.get(replyUserId); - if (Objects.nonNull(maaUser)) { - emailService.sendCommentNotification(maaUser.getEmail(), commentNotification); - } - } - } - - - //创建评论表 - commentsAreaRepository.insert( - new CommentsArea().setCopilotId(copilotId) - .setUploaderId(userId) - .setFromCommentId(fromCommentsId) - .setMainCommentId(mainCommentsId) - .setMessage(message) - .setNotification(commentsAddDTO.isNotification()) - ); - - } - - - public void deleteComments(String userId, String commentsId) { - CommentsArea commentsArea = findCommentsById(commentsId); - //允许作者删除评论 - copilotRepository.findByCopilotId(commentsArea.getCopilotId()) - .ifPresent(copilot -> - Assert.isTrue( - Objects.equals(userId, copilot.getUploaderId()) - || Objects.equals(userId, commentsArea.getUploaderId()), - "您无法删除不属于您的评论") - ); - LocalDateTime now = LocalDateTime.now(); - commentsArea.setDelete(true); - commentsArea.setDeleteTime(now); - - //删除所有回复 - if (StringUtils.isBlank(commentsArea.getMainCommentId())) { - List commentsAreaList = commentsAreaRepository.findByMainCommentId(commentsArea.getId()); - commentsAreaList.forEach(ca -> - ca.setDeleteTime(now) - .setDelete(true) - ); - commentsAreaRepository.saveAll(commentsAreaList); - } - commentsAreaRepository.save(commentsArea); - } - - - /** - * 为评论进行点赞 - * - * @param userId 登录用户 id - * @param commentsRatingDTO CommentsRatingDTO - */ - public void rates(String userId, CommentsRatingDTO commentsRatingDTO) { - String rating = commentsRatingDTO.getRating(); - - CommentsArea commentsArea = findCommentsById(commentsRatingDTO.getCommentId()); - - long change; - Optional ratingOptional = ratingRepository.findByTypeAndKeyAndUserId(Rating.KeyType.COMMENT, commentsArea.getId(), userId); - // 判断该用户是否存在评分 - if (ratingOptional.isPresent()) { - // 如果评分发生变化则更新 - if (!Objects.equals(ratingOptional.get().getRating(), RatingType.fromRatingType(rating))) { - RatingType oldRatingType = ratingOptional.get().getRating(); - ratingOptional.get().setRating(RatingType.fromRatingType(rating)); - ratingOptional.get().setRateTime(LocalDateTime.now()); - RatingType newRatingType = ratingRepository.save(ratingOptional.get()).getRating(); - // 更新评分后更新评论的点赞数 - change = newRatingType == RatingType.LIKE ? 1 : - (oldRatingType != RatingType.LIKE ? 0 : -1); - } else { - // 如果评分未发生变化则结束 - return; - } - } else { - // 不存在评分则创建 - Rating newRating = new Rating() - .setType(Rating.KeyType.COMMENT) - .setKey(commentsArea.getId()) - .setUserId(userId) - .setRating(RatingType.fromRatingType(rating)) - .setRateTime(LocalDateTime.now()); - - ratingRepository.insert(newRating); - change = newRating.getRating() == RatingType.LIKE ? 1 : 0; - } - - // 点赞数不需要在高并发下特别精准,大概就行,但是也得避免特别离谱的数字 - long likeCount = commentsArea.getLikeCount() + change; - if (likeCount < 0) { - likeCount = 0; - } - commentsArea.setLikeCount(likeCount); - - commentsAreaRepository.save(commentsArea); - } - - /** - * 评论置顶 - * - * @param userId 登录用户 id - * @param commentsToppingDTO CommentsToppingDTO - */ - public void topping(String userId, CommentsToppingDTO commentsToppingDTO) { - CommentsArea commentsArea = findCommentsById(commentsToppingDTO.getCommentId()); - Assert.isTrue(!commentsArea.isDelete(), "评论不存在"); - // 只允许作者置顶评论 - copilotRepository.findByCopilotId(commentsArea.getCopilotId()) - .ifPresent(copilot -> { - Assert.isTrue( - Objects.equals(userId, copilot.getUploaderId()), - "只有作者才能置顶评论"); - commentsArea.setTopping(commentsToppingDTO.isTopping()); - commentsAreaRepository.save(commentsArea); - } - ); - } - - /** - * 查询 - * - * @param request CommentsQueriesDTO - * @return CommentsAreaInfo - */ - public CommentsAreaInfo queriesCommentsArea(CommentsQueriesDTO request) { - Sort.Order toppingOrder = Sort.Order.desc("topping"); - - Sort.Order sortOrder = new Sort.Order( - request.isDesc() ? Sort.Direction.DESC : Sort.Direction.ASC, - Optional.ofNullable(request.getOrderBy()) - .filter(StringUtils::isNotBlank) - .map(ob -> switch (ob) { - case "hot" -> "likeCount"; - case "id" -> "uploadTime"; - default -> request.getOrderBy(); - }).orElse("likeCount")); - - int page = request.getPage() > 0 ? request.getPage() : 1; - int limit = request.getLimit() > 0 ? request.getLimit() : 10; - - - Pageable pageable = PageRequest.of(page - 1, limit, Sort.by(toppingOrder, sortOrder)); - - - //主评论 - - Page mainCommentsList; - - if (StringUtils.isNotBlank(request.getJustSeeId())) { - mainCommentsList = commentsAreaRepository.findByCopilotIdAndUploaderIdAndDeleteAndMainCommentIdExists(request.getCopilotId(), request.getJustSeeId(), false, false, pageable); - } else { - mainCommentsList = commentsAreaRepository.findByCopilotIdAndDeleteAndMainCommentIdExists(request.getCopilotId(), false, false, pageable); - } - - long count = mainCommentsList.getTotalElements(); - - int pageNumber = mainCommentsList.getTotalPages(); - - // 判断是否存在下一页 - boolean hasNext = count - (long) page * limit > 0; - - - //获取子评论 - List subCommentsList = commentsAreaRepository.findByMainCommentIdIn( - mainCommentsList.stream() - .map(CommentsArea::getId) - .toList() - ); - - //将已删除评论内容替换为空 - subCommentsList.forEach(comment -> { - if (comment.isDelete()) { - comment.setMessage(""); - } - }); - - - //所有评论 - List allComments = new ArrayList<>(mainCommentsList.stream().toList()); - allComments.addAll(subCommentsList); - - //获取所有评论用户 - List userId = allComments.stream().map(CommentsArea::getUploaderId).distinct().toList(); - Map maaUserMap = userRepository.findByUsersId(userId); - - - //转换主评论数据并填充用户名 - List commentsInfos = mainCommentsList.stream().map(mainComment -> { - CommentsInfo commentsInfo = - commentConverter - .toCommentsInfo( - mainComment - , mainComment.getId() - , (int) mainComment.getLikeCount() - , maaUserMap.getOrDefault( - mainComment.getUploaderId() - , UNKNOWN_USER - ) - ); - - List subCommentsInfoList = subCommentsList.stream() - .filter(comment -> Objects.equals(commentsInfo.getCommentId(), comment.getMainCommentId())) - //转换子评论数据并填充用户名 - .map(subComment -> - commentConverter - .toSubCommentsInfo( - subComment - , subComment.getId() - , (int) subComment.getLikeCount() - //填充评论用户名 - , maaUserMap.getOrDefault( - subComment.getUploaderId(), - UNKNOWN_USER - ) - , subComment.isDelete() - ) - ).toList(); - - - commentsInfo.setSubCommentsInfos(subCommentsInfoList); - return commentsInfo; - }).toList(); - - return new CommentsAreaInfo().setHasNext(hasNext) - .setPage(pageNumber) - .setTotal(count) - .setData(commentsInfos); - } - - - private CommentsArea findCommentsById(String commentsId) { - Optional commentsArea = commentsAreaRepository.findById(commentsId); - Assert.isTrue(commentsArea.isPresent(), "评论不存在"); - return commentsArea.get(); - } - - - public void notificationStatus(String userId, String id, boolean status) { - Optional commentsAreaOptional = commentsAreaRepository.findById(id); - Assert.isTrue(commentsAreaOptional.isPresent(), "评论不存在"); - CommentsArea commentsArea = commentsAreaOptional.get(); - Assert.isTrue(Objects.equals(userId, commentsArea.getUploaderId()), "您没有权限修改"); - commentsArea.setNotification(status); - commentsAreaRepository.save(commentsArea); - } -} diff --git a/src/main/java/plus/maa/backend/service/CopilotService.java b/src/main/java/plus/maa/backend/service/CopilotService.java deleted file mode 100644 index 12b7f2e0..00000000 --- a/src/main/java/plus/maa/backend/service/CopilotService.java +++ /dev/null @@ -1,545 +0,0 @@ -package plus.maa.backend.service; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.collect.Sets; -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.Nullable; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.mongodb.core.query.Update; -import org.springframework.stereotype.Service; -import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; -import plus.maa.backend.common.utils.converter.CopilotConverter; -import plus.maa.backend.controller.request.copilot.CopilotCUDRequest; -import plus.maa.backend.controller.request.copilot.CopilotDTO; -import plus.maa.backend.controller.request.copilot.CopilotQueriesRequest; -import plus.maa.backend.controller.request.copilot.CopilotRatingReq; -import plus.maa.backend.controller.response.MaaResultException; -import plus.maa.backend.controller.response.copilot.ArkLevelInfo; -import plus.maa.backend.controller.response.copilot.CopilotInfo; -import plus.maa.backend.controller.response.copilot.CopilotPageInfo; -import plus.maa.backend.repository.*; -import plus.maa.backend.repository.entity.CommentsArea; -import plus.maa.backend.repository.entity.Copilot; -import plus.maa.backend.repository.entity.MaaUser; -import plus.maa.backend.repository.entity.Rating; -import plus.maa.backend.service.model.RatingCache; -import plus.maa.backend.service.model.RatingType; - -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.time.LocalDateTime; -import java.time.temporal.ChronoUnit; -import java.util.*; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -/** - * @author LoMu - * Date 2022-12-25 19:57 - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class CopilotService { - private final CopilotRepository copilotRepository; - private final RatingRepository ratingRepository; - private final MongoTemplate mongoTemplate; - private final ObjectMapper mapper; - private final ArkLevelService levelService; - private final RedisCache redisCache; - private final UserRepository userRepository; - private final CommentsAreaRepository commentsAreaRepository; - - private final CopilotConverter copilotConverter; - private final AtomicLong copilotIncrementId = new AtomicLong(20000); - - /* - 首页分页查询缓存配置 - 格式为:需要缓存的 orderBy 类型(也就是榜单类型) -> 缓存时间 - (Map.of()返回的是不可变对象,无需担心线程安全问题) - */ - private static final Map HOME_PAGE_CACHE_CONFIG = Map.of( - "hot", 3600 * 24L, - "views", 3600L, - "id", 300L - ); - - @PostConstruct - public void init() { - // 初始化copilotId, 从数据库中获取最大的copilotId - // 如果数据库中没有数据, 则从20000开始 - copilotRepository.findFirstByOrderByCopilotIdDesc() - .map(Copilot::getCopilotId) - .ifPresent(last -> copilotIncrementId.set(last + 1)); - - log.info("作业自增ID初始化完成: {}", copilotIncrementId.get()); - } - - /** - * 并修正前端的冗余部分 - * - * @param copilotDTO copilotDTO - */ - private CopilotDTO correctCopilot(CopilotDTO copilotDTO) { - - // 去除name的冗余部分 - // todo 优化空处理代码美观程度 - if (copilotDTO.getGroups() != null) { - copilotDTO.getGroups().forEach( - group -> { - if (group.getOpers() != null) { - group.getOpers().forEach(oper -> oper - .setName(oper.getName() == null ? null : oper.getName().replaceAll("[\"“”]", ""))); - } - }); - } - if (copilotDTO.getOpers() != null) { - copilotDTO.getOpers().forEach(operator -> operator - .setName(operator.getName() == null ? null : operator.getName().replaceAll("[\"“”]", ""))); - } - - // actions name 不是必须 - if (copilotDTO.getActions() != null) { - copilotDTO.getActions().forEach(action -> action - .setName(action.getName() == null ? null : action.getName().replaceAll("[\"“”]", ""))); - } - // 使用stageId存储作业关卡信息 - ArkLevelInfo level = levelService.findByLevelIdFuzzy(copilotDTO.getStageName()); - if (level != null) { - copilotDTO.setStageName(level.getStageId()); - } - return copilotDTO; - } - - /** - * 将content解析为CopilotDTO - * - * @param content content - * @return CopilotDTO - */ - private CopilotDTO parseToCopilotDto(String content) { - Assert.notNull(content, "作业内容不可为空"); - try { - return mapper.readValue(content, CopilotDTO.class); - } catch (JsonProcessingException e) { - log.error("解析copilot失败", e); - throw new MaaResultException("解析copilot失败"); - } - } - - - private Pattern caseInsensitive(String s) { - return Pattern.compile(s, Pattern.CASE_INSENSITIVE); - } - - - /** - * 上传新的作业 - * - * @param content 前端编辑json作业内容 - * @return 返回_id - */ - public Long upload(String loginUserId, String content) { - CopilotDTO copilotDTO = correctCopilot(parseToCopilotDto(content)); - // 将其转换为数据库存储对象 - Copilot copilot = copilotConverter.toCopilot( - copilotDTO, loginUserId, - LocalDateTime.now(), copilotIncrementId.getAndIncrement(), - content); - copilotRepository.insert(copilot); - return copilot.getCopilotId(); - } - - /** - * 根据作业id删除作业 - */ - public void delete(String loginUserId, CopilotCUDRequest request) { - copilotRepository.findByCopilotId(request.getId()).ifPresent(copilot -> { - Assert.state(Objects.equals(copilot.getUploaderId(), loginUserId), "您无法修改不属于您的作业"); - copilot.setDelete(true); - copilotRepository.save(copilot); - /* - * 删除作业时,如果被删除的项在 Redis 首页缓存中存在,则清空对应的首页缓存 - * 新增作业就不必,因为新作业显然不会那么快就登上热度榜和浏览量榜 - */ - for (var kv : HOME_PAGE_CACHE_CONFIG.entrySet()) { - String key = String.format("home:%s:copilotIds", kv.getKey()); - String pattern = String.format("home:%s:*", kv.getKey()); - if (redisCache.valueMemberInSet(key, copilot.getCopilotId())) { - redisCache.removeCacheByPattern(pattern); - } - } - }); - } - - /** - * 指定查询 - */ - public Optional getCopilotById(String userIdOrIpAddress, Long id) { - // 根据ID获取作业, 如作业不存在则抛出异常返回 - Optional copilotOptional = copilotRepository.findByCopilotIdAndDeleteIsFalse(id); - return copilotOptional.map(copilot -> { - // 60分钟内限制同一个用户对访问量的增加 - RatingCache cache = redisCache.getCache("views:" + userIdOrIpAddress, RatingCache.class); - if (Objects.isNull(cache) || Objects.isNull(cache.getCopilotIds()) || - !cache.getCopilotIds().contains(id)) { - Query query = Query.query(Criteria.where("copilotId").is(id)); - Update update = new Update(); - // 增加一次views - update.inc("views"); - mongoTemplate.updateFirst(query, update, Copilot.class); - if (Objects.isNull(cache)) { - redisCache.setCache("views:" + userIdOrIpAddress, new RatingCache(Sets.newHashSet(id))); - } else { - redisCache.updateCache("views:" + userIdOrIpAddress, RatingCache.class, cache, - updateCache -> { - updateCache.getCopilotIds().add(id); - return updateCache; - }, 60, TimeUnit.MINUTES); - } - } - Map maaUser = userRepository.findByUsersId(List.of(copilot.getUploaderId())); - - // 新评分系统 - RatingType ratingType = ratingRepository.findByTypeAndKeyAndUserId(Rating.KeyType.COPILOT, - Long.toString(copilot.getCopilotId()), userIdOrIpAddress) - .map(Rating::getRating) - .orElse(null); - // 用户点进作业会显示点赞信息 - return formatCopilot(copilot, ratingType, maaUser.get(copilot.getUploaderId()).getUserName(), - commentsAreaRepository.countByCopilotIdAndDelete(copilot.getCopilotId(), false)); - }); - } - - /** - * 分页查询。传入 userId 不为空时限制为用户所有的数据 - * 会缓存默认状态下热度和访问量排序的结果 - * - * @param userId 获取已登录用户自己的作业数据 - * @param request 模糊查询 - * @return CopilotPageInfo - */ - public CopilotPageInfo queriesCopilot(@Nullable String userId, CopilotQueriesRequest request) { - - AtomicLong cacheTimeout = new AtomicLong(); - AtomicReference cacheKey = new AtomicReference<>(); - AtomicReference setKey = new AtomicReference<>(); - // 只缓存默认状态下热度和访问量排序的结果,并且最多只缓存前三页 - if (request.getPage() <= 3 && request.getDocument() == null && request.getLevelKeyword() == null && - request.getUploaderId() == null && request.getOperator() == null) { - - Optional cacheOptional = Optional.ofNullable(request.getOrderBy()) - .filter(StringUtils::isNotBlank) - .map(HOME_PAGE_CACHE_CONFIG::get) - .map(t -> { - cacheTimeout.set(t); - setKey.set(String.format("home:%s:copilotIds", request.getOrderBy())); - cacheKey.set(String.format("home:%s:%s", request.getOrderBy(), request.hashCode())); - return redisCache.getCache(cacheKey.get(), CopilotPageInfo.class); - }); - - // 如果缓存存在则直接返回 - if (cacheOptional.isPresent()) { - return cacheOptional.get(); - } - } - - Sort.Order sortOrder = new Sort.Order( - request.isDesc() ? Sort.Direction.DESC : Sort.Direction.ASC, - Optional.ofNullable(request.getOrderBy()) - .filter(StringUtils::isNotBlank) - .map(ob -> switch (ob) { - case "hot" -> "hotScore"; - case "id" -> "copilotId"; - default -> request.getOrderBy(); - }).orElse("copilotId")); - // 判断是否有值 无值则为默认 - int page = request.getPage() > 0 ? request.getPage() : 1; - int limit = request.getLimit() > 0 ? request.getLimit() : 10; - - Pageable pageable = PageRequest.of(page - 1, limit, Sort.by(sortOrder)); - - Query queryObj = new Query(); - Criteria criteriaObj = new Criteria(); - - Set andQueries = new HashSet<>(); - Set norQueries = new HashSet<>(); - Set orQueries = new HashSet<>(); - - andQueries.add(Criteria.where("delete").is(false)); - - - //关卡名、关卡类型、关卡编号 - if (StringUtils.isNotBlank(request.getLevelKeyword())) { - List levelInfo = levelService.queryLevelByKeyword(request.getLevelKeyword()); - if (levelInfo.isEmpty()) { - andQueries.add(Criteria.where("stageName").regex(caseInsensitive(request.getLevelKeyword()))); - } else { - andQueries.add(Criteria.where("stageName").in(levelInfo.stream() - .map(ArkLevelInfo::getStageId).collect(Collectors.toSet()))); - } - } - - //标题、描述、神秘代码 - if (StringUtils.isNotBlank(request.getDocument())) { - orQueries.add(Criteria.where("doc.title").regex(caseInsensitive(request.getDocument()))); - orQueries.add(Criteria.where("doc.details").regex(caseInsensitive(request.getDocument()))); - } - - - //包含或排除干员 - String oper = request.getOperator(); - if (StringUtils.isNotBlank(oper)) { - oper = oper.replaceAll("[“\"”]", ""); - String[] operators = oper.split(","); - for (String operator : operators) { - if (operator.startsWith("~")) { - String exclude = operator.substring(1); - // 排除查询指定干员 - norQueries.add(Criteria.where("opers.name").regex(exclude)); - } else { - // 模糊匹配查询指定干员 - andQueries.add(Criteria.where("opers.name").regex(operator)); - } - } - } - - //查看自己 - if (StringUtils.isNotBlank(request.getUploaderId())) { - if ("me".equals(request.getUploaderId())) { - if (!ObjectUtils.isEmpty(userId)) { - andQueries.add(Criteria.where("uploaderId").is(userId)); - } - } else { - andQueries.add(Criteria.where("uploaderId").is(request.getUploaderId())); - } - } - - // 封装查询 - if (!andQueries.isEmpty()) { - criteriaObj.andOperator(andQueries); - } - if (!norQueries.isEmpty()) { - criteriaObj.norOperator(norQueries); - } - if (!orQueries.isEmpty()) { - criteriaObj.orOperator(orQueries); - } - queryObj.addCriteria(criteriaObj); - // 查询总数 - long count = mongoTemplate.count(queryObj, Copilot.class); - - // 分页排序查询 - List copilots = mongoTemplate.find(queryObj.with(pageable), Copilot.class); - - - // 填充前端所需信息 - Set copilotIds = copilots.stream().map(Copilot::getCopilotId).collect(Collectors.toSet()); - Map maaUsers = userRepository.findByUsersId(copilots.stream().map(Copilot::getUploaderId).toList()); - Map commentsCount = commentsAreaRepository.findByCopilotIdInAndDelete(copilotIds, false) - .collect(Collectors.groupingBy(CommentsArea::getCopilotId, Collectors.counting())); - - // 新版评分系统 - // 反正目前首页和搜索不会直接展示当前用户有没有点赞,干脆直接不查,要用户点进作业才显示自己是否点赞 - List infos = copilots.stream().map(copilot -> - formatCopilot(copilot, null, - maaUsers.get(copilot.getUploaderId()).getUserName(), - commentsCount.get(copilot.getCopilotId()))) - .toList(); - - // 计算页面 - int pageNumber = (int) Math.ceil((double) count / limit); - - // 判断是否存在下一页 - boolean hasNext = count - (long) page * limit > 0; - - // 封装数据 - CopilotPageInfo data = new CopilotPageInfo() - .setTotal(count) - .setHasNext(hasNext) - .setData(infos) - .setPage(pageNumber); - - // 决定是否缓存 - if (cacheKey.get() != null) { - // 记录存在的作业id - redisCache.addSet(setKey.get(), copilotIds, cacheTimeout.get()); - // 缓存数据 - redisCache.setCache(cacheKey.get(), data, cacheTimeout.get()); - } - return data; - } - - /** - * 增量更新 - * - * @param copilotCUDRequest 作业_id content - */ - public void update(String loginUserId, CopilotCUDRequest copilotCUDRequest) { - String content = copilotCUDRequest.getContent(); - Long id = copilotCUDRequest.getId(); - copilotRepository.findByCopilotId(id).ifPresent(copilot -> { - CopilotDTO copilotDTO = correctCopilot(parseToCopilotDto(content)); - Assert.state(Objects.equals(copilot.getUploaderId(), loginUserId), "您无法修改不属于您的作业"); - copilot.setUploadTime(LocalDateTime.now()); - copilotConverter.updateCopilotFromDto(copilotDTO, content, copilot); - copilotRepository.save(copilot); - }); - } - - /** - * 评分相关 - * - * @param request 评分 - * @param userIdOrIpAddress 用于已登录用户作出评分 - */ - public void rates(String userIdOrIpAddress, CopilotRatingReq request) { - String rating = request.getRating(); - - Assert.isTrue(copilotRepository.existsCopilotsByCopilotId(request.getId()), "作业id不存在"); - - int likeCountChange = 0; - int dislikeCountChange = 0; - Optional ratingOptional = ratingRepository.findByTypeAndKeyAndUserId(Rating.KeyType.COPILOT, - Long.toString(request.getId()), userIdOrIpAddress); - // 如果评分存在则更新评分 - if (ratingOptional.isPresent()) { - Rating rating1 = ratingOptional.get(); - // 如果评分相同,则不做任何操作 - if (Objects.equals(rating1.getRating(), RatingType.fromRatingType(rating))) { - return; - } - // 如果评分不同则更新评分 - RatingType oldRatingType = rating1.getRating(); - rating1.setRating(RatingType.fromRatingType(rating)); - rating1.setRateTime(LocalDateTime.now()); - ratingRepository.save(rating1); - // 计算评分变化 - likeCountChange = rating1.getRating() == RatingType.LIKE ? 1 : - (oldRatingType != RatingType.LIKE ? 0 : -1); - dislikeCountChange = rating1.getRating() == RatingType.DISLIKE ? 1 : - (oldRatingType != RatingType.DISLIKE ? 0 : -1); - } - - // 不存在评分 则添加新的评分 - if (ratingOptional.isEmpty()) { - Rating newRating = new Rating() - .setType(Rating.KeyType.COPILOT) - .setKey(Long.toString(request.getId())) - .setUserId(userIdOrIpAddress) - .setRating(RatingType.fromRatingType(rating)) - .setRateTime(LocalDateTime.now()); - - ratingRepository.insert(newRating); - // 计算评分变化 - likeCountChange = newRating.getRating() == RatingType.LIKE ? 1 : 0; - dislikeCountChange = newRating.getRating() == RatingType.DISLIKE ? 1 : 0; - } - - // 获取只包含评分的作业 - Query query = Query.query(Criteria - .where("copilotId").is(request.getId()) - .and("delete").is(false) - ); - // 排除 _id,防止误 save 该不完整作业后原有数据丢失 - query.fields().include("likeCount", "dislikeCount").exclude("_id"); - Copilot copilot = mongoTemplate.findOne(query, Copilot.class); - Assert.notNull(copilot, "作业不存在"); - - // 计算评分相关 - long likeCount = copilot.getLikeCount() + likeCountChange; - likeCount = likeCount < 0 ? 0 : likeCount; - long ratingCount = likeCount + copilot.getDislikeCount() + dislikeCountChange; - ratingCount = ratingCount < 0 ? 0 : ratingCount; - - double rawRatingLevel = ratingCount != 0 ? (double) likeCount / ratingCount : 0; - BigDecimal bigDecimal = new BigDecimal(rawRatingLevel); - // 只取一位小数点 - double ratingLevel = bigDecimal.setScale(1, RoundingMode.HALF_UP).doubleValue(); - // 更新数据 - query = Query.query(Criteria - .where("copilotId").is(request.getId()) - .and("delete").is(false) - ); - Update update = new Update(); - update.set("likeCount", likeCount); - update.set("dislikeCount", ratingCount - likeCount); - update.set("ratingLevel", (int) (ratingLevel * 10)); - update.set("ratingRatio", ratingLevel); - mongoTemplate.updateFirst(query, update, Copilot.class); - - // 记录近期评分变化量前 100 的作业 id - redisCache.incZSet("rate:hot:copilotIds", - Long.toString(request.getId()), - 1, 100, 3600 * 3); - } - - public static double getHotScore(Copilot copilot, long lastWeekLike, long lastWeekDislike) { - LocalDateTime now = LocalDateTime.now(); - LocalDateTime uploadTime = copilot.getUploadTime(); - // 基于时间的基础分 - double base = 6d; - // 相比上传时间过了多少周 - long pastedWeeks = ChronoUnit.WEEKS.between(uploadTime, now) + 1; - base = base / Math.log(pastedWeeks + 1); - // 上一周好评率 - long ups = Math.max(lastWeekLike, 1); - long downs = Math.max(lastWeekDislike, 0); - double greatRate = (double) ups / (ups + downs); - if ((ups + downs) >= 5 && downs >= ups) { - // 将信赖就差评过多的作业打入地狱 - base = base * greatRate; - } - // 上一周好评率 * (上一周评分数 / 10) * (浏览数 / 10) / 过去的周数 - double s = greatRate * (copilot.getViews() / 10d) - * Math.max((ups + downs) / 10d, 1) / pastedWeeks; - double order = Math.log(Math.max(s, 1)); - return order + s / 1000d + base; - } - - /** - * 将数据库内容转换为前端所需格式
- * 新版评分系统 - */ - private CopilotInfo formatCopilot(Copilot copilot, @Nullable RatingType ratingType, String userName, - Long commentsCount) { - CopilotInfo info = copilotConverter.toCopilotInfo(copilot, userName, copilot.getCopilotId(), - commentsCount); - - info.setRatingRatio(copilot.getRatingRatio()); - info.setRatingLevel(copilot.getRatingLevel()); - if (ratingType != null) { - info.setRatingType(ratingType.getDisplay()); - } - // 评分数少于一定数量 - info.setNotEnoughRating(copilot.getLikeCount() + copilot.getDislikeCount() <= 5); - - info.setAvailable(true); - - // 兼容客户端, 将作业ID替换为数字ID - copilot.setId(Long.toString(copilot.getCopilotId())); - return info; - } - - public void notificationStatus(String userId, Long copilotId, boolean status) { - Optional copilotOptional = copilotRepository.findByCopilotId(copilotId); - Assert.isTrue(copilotOptional.isPresent(), "copilot不存在"); - Copilot copilot = copilotOptional.get(); - Assert.isTrue(Objects.equals(userId, copilot.getUploaderId()), "您没有权限修改"); - copilot.setNotification(status); - copilotRepository.save(copilot); - } -} \ No newline at end of file diff --git a/src/main/java/plus/maa/backend/service/EmailService.java b/src/main/java/plus/maa/backend/service/EmailService.java deleted file mode 100644 index b1a6ba5d..00000000 --- a/src/main/java/plus/maa/backend/service/EmailService.java +++ /dev/null @@ -1,143 +0,0 @@ -package plus.maa.backend.service; - -import cn.hutool.extra.mail.MailAccount; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.Resource; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; -import org.apache.logging.log4j.util.Strings; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Lazy; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; -import plus.maa.backend.common.bo.EmailBusinessObject; -import plus.maa.backend.config.external.MaaCopilotProperties; -import plus.maa.backend.config.external.Mail; -import plus.maa.backend.controller.response.MaaResultException; -import plus.maa.backend.repository.RedisCache; -import plus.maa.backend.service.model.CommentNotification; - -import java.util.HashMap; -import java.util.Map; - -/** - * @author LoMu - * Date 2022-12-24 11:05 - */ -@Service -@Slf4j -@RequiredArgsConstructor -public class EmailService { - @Value("${maa-copilot.vcode.expire:600}") - private int expire; - - @Value("${maa-copilot.info.domain}") - private String domain; - - private final MaaCopilotProperties maaCopilotProperties; - - @Value("${debug.email.no-send:false}") - private boolean flagNoSend; - - private final RedisCache redisCache; - - private final MailAccount MAIL_ACCOUNT = new MailAccount(); - - // 注入自身代理类的延迟加载代理类 - @Lazy - @Resource - private EmailService emailService; - - /** - * 初始化邮件账户信息 - */ - @PostConstruct - private void initMailAccount() { - Mail mail = maaCopilotProperties.getMail(); - MAIL_ACCOUNT - .setHost(mail.getHost()) - .setPort(mail.getPort()) - .setFrom(mail.getFrom()) - .setUser(mail.getUser()) - .setPass(mail.getPass()) - .setSslEnable(mail.getSsl()) - .setStarttlsEnable(mail.getStarttls()); - - log.info("邮件账户信息初始化完成: {}", MAIL_ACCOUNT); - } - - /** - * 发送验证码 - * 以email作为 redis key - * vcode(验证码)作为 redis value - * - * @param email 邮箱 - */ - - public void sendVCode(String email) { - // 一个过期周期最多重发十条,记录已发送的邮箱以及间隔时间 - final int timeout = expire / 10; - if (!redisCache.setCacheIfAbsent("HasBeenSentVCode:" + email , timeout, timeout)) { - // 设置失败,说明 key 已存在 - throw new MaaResultException(403, String.format("发送验证码的请求至少需要间隔 %d 秒", timeout)); - } - // 调用注入的代理类执行异步任务 - emailService.asyncSendVCode(email); - } - - @Async - protected void asyncSendVCode(String email) { - // 6位随机数验证码 - String vcode = RandomStringUtils.random(6, true, true).toUpperCase(); - if (flagNoSend) { - log.debug("vcode is " + vcode); - log.warn("Email not sent, no-send enabled"); - } else { - EmailBusinessObject.builder() - .setMailAccount(MAIL_ACCOUNT) - .setEmail(email) - .sendVerificationCodeMessage(vcode); - } - // 存redis - redisCache.setCache("vCodeEmail:" + email, vcode, expire); - } - - /** - * 检验验证码并抛出异常 - * @param email 邮箱 - * @param vcode 验证码 - * @throws MaaResultException 验证码错误 - */ - public void verifyVCode(String email, String vcode) { - if (!redisCache.removeKVIfEquals("vCodeEmail:" + email, vcode.toUpperCase())) { - throw new MaaResultException(401, "验证码错误"); - } - } - - @Async - public void sendCommentNotification(String email, CommentNotification commentNotification) { - int limit = 25; - - String title = commentNotification.getTitle(); - if (Strings.isNotBlank(title)) { - if (title.length() > limit) { - title = title.substring(0, limit) + "...."; - } - } - - Map map = new HashMap<>(); - map.put("authorName", commentNotification.getAuthorName()); - map.put("forntEndLink", maaCopilotProperties.getInfo().getFrontendDomain()); - map.put("reName", commentNotification.getReName()); - map.put("date", commentNotification.getDate()); - map.put("title", title); - map.put("reMessage", commentNotification.getReMessage()); - EmailBusinessObject.builder() - .setTitle("收到新回复 来自用户@" + commentNotification.getReName() + " Re: " + map.get("title")) - .setMailAccount(MAIL_ACCOUNT) - .setEmail(email) - .sendCommentNotification(map); - - } -} diff --git a/src/main/java/plus/maa/backend/service/FileService.java b/src/main/java/plus/maa/backend/service/FileService.java deleted file mode 100644 index 481fc4b6..00000000 --- a/src/main/java/plus/maa/backend/service/FileService.java +++ /dev/null @@ -1,225 +0,0 @@ -package plus.maa.backend.service; - - -import com.mongodb.client.gridfs.GridFSFindIterable; -import com.mongodb.client.gridfs.model.GridFSFile; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.bson.Document; -import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.mongodb.gridfs.GridFsCriteria; -import org.springframework.data.mongodb.gridfs.GridFsOperations; -import org.springframework.stereotype.Service; -import org.springframework.util.Assert; -import org.springframework.web.multipart.MultipartException; -import org.springframework.web.multipart.MultipartFile; -import plus.maa.backend.controller.file.ImageDownloadDTO; -import plus.maa.backend.controller.response.MaaResultException; -import plus.maa.backend.repository.RedisCache; - -import java.io.IOException; -import java.io.InputStream; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.*; -import java.util.concurrent.TimeUnit; -import java.util.regex.Pattern; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -/** - * @author LoMu - * Date 2023-04-16 23:21 - */ - -@RequiredArgsConstructor -@Service -public class FileService { - private final GridFsOperations gridFsOperations; - private final RedisCache redisCache; - - public void uploadFile(MultipartFile file, - String type, - String version, - String classification, - String label, - String ip) { - - //redis持久化 - if (redisCache.getCache("NotEnable:UploadFile", String.class) != null) { - throw new MaaResultException(403, "closed uploadfile"); - } - - //文件小于1024Bytes不接收 - if (file.getSize() < 1024) { - throw new MultipartException("Minimum upload size exceeded"); - } - Assert.notNull(file.getOriginalFilename(), "文件名不可为空"); - - String antecedentVersion = null; - if (version.contains("-")) { - String[] split = version.split("-"); - version = split[0]; - antecedentVersion = split[1]; - } - - Document document = new Document(); - document.put("version", version); - document.put("antecedentVersion", antecedentVersion); - document.put("label", label); - document.put("classification", classification); - document.put("type", type); - document.put("ip", ip); - - int index = file.getOriginalFilename().lastIndexOf("."); - String fileType = ""; - if (index != -1) { - fileType = file.getOriginalFilename().substring(index); - } - - String fileName = "Maa-" + UUID.randomUUID().toString().replaceAll("-", "") + fileType; - - try { - gridFsOperations.store(file.getInputStream(), fileName, document); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - - public void downloadDateFile(String date, String beLocated, boolean delete, HttpServletResponse response) { - SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); - Date d; - Query query; - - if (StringUtils.isBlank(date)) { - d = new Date(System.currentTimeMillis()); - } else { - try { - d = formatter.parse(date); - } catch (ParseException e) { - throw new RuntimeException(e); - } - } - - if (StringUtils.isBlank(beLocated) || Objects.equals("after", beLocated.toLowerCase())) { - query = new Query(Criteria.where("metadata").gte(d)); - } else { - query = new Query(Criteria.where("uploadDate").lte(d)); - } - GridFSFindIterable files = gridFsOperations.find(query); - - response.addHeader("Content-Disposition", "attachment;filename=" + System.currentTimeMillis() + ".zip"); - - gzip(response, files); - - if (delete) { - gridFsOperations.delete(query); - } - } - - - public void downloadFile(ImageDownloadDTO imageDownloadDTO, HttpServletResponse response) { - Query query = new Query(); - Set criteriaSet = new HashSet<>(); - - - //图片类型 - criteriaSet.add(GridFsCriteria.whereMetaData("type").regex(Pattern.compile(imageDownloadDTO.getType(), Pattern.CASE_INSENSITIVE))); - - //指定下载某个类型的图片 - if (StringUtils.isNotBlank(imageDownloadDTO.getClassification())) { - criteriaSet.add(GridFsCriteria.whereMetaData("classification").regex(Pattern.compile(imageDownloadDTO.getClassification(), Pattern.CASE_INSENSITIVE))); - } - - //指定版本或指定范围版本 - if (!Objects.isNull(imageDownloadDTO.getVersion())) { - List version = imageDownloadDTO.getVersion(); - - if (version.size() == 1) { - String antecedentVersion = null; - if (version.get(0).contains("-")) { - String[] split = version.get(0).split("-"); - antecedentVersion = split[1]; - } - criteriaSet.add(GridFsCriteria.whereMetaData("version").is(version.get(0)).and("antecedentVersion").is(antecedentVersion)); - - } else if (version.size() == 2) { - criteriaSet.add(GridFsCriteria.whereMetaData("version").gte(version.get(0)).lte(version.get(1))); - } - } - - if (StringUtils.isNotBlank(imageDownloadDTO.getLabel())) { - criteriaSet.add(GridFsCriteria.whereMetaData("label").regex(Pattern.compile(imageDownloadDTO.getLabel(), Pattern.CASE_INSENSITIVE))); - } - - Criteria criteria = new Criteria().andOperator(criteriaSet); - query.addCriteria(criteria); - - - GridFSFindIterable gridFSFiles = gridFsOperations.find(query); - - response.addHeader("Content-Disposition", "attachment;filename=" + "Maa-" + imageDownloadDTO.getType() + ".zip"); - - gzip(response, gridFSFiles); - - if (imageDownloadDTO.isDelete()) { - gridFsOperations.delete(query); - } - - } - - public String disable() { - setUploadEnabled(false); - return "已关闭"; - } - - public String enable() { - setUploadEnabled(true); - return "已启用"; - } - - public boolean isUploadEnabled() { - return redisCache.getCache("NotEnable:UploadFile", String.class) == null; - } - - /** - * 设置上传功能状态 - * @param enabled 是否开启 - */ - public void setUploadEnabled(boolean enabled) { - // Fixme: redis recovery solution should be added, or change to another storage - if (enabled) { - redisCache.removeCache("NotEnable:UploadFile"); - } else { - redisCache.setCache("NotEnable:UploadFile", "1", 0, TimeUnit.DAYS); - } - } - - - private void gzip(HttpServletResponse response, GridFSFindIterable files) { - try (ZipOutputStream zipOutputStream = new ZipOutputStream(response.getOutputStream())) { - - for (GridFSFile file : files) { - - ZipEntry zipEntry = new ZipEntry(file.getFilename()); - try (InputStream inputStream = gridFsOperations.getResource(file).getInputStream()) { - //添加压缩文件 - zipOutputStream.putNextEntry(zipEntry); - - byte[] bytes = new byte[1024]; - int len; - while ((len = inputStream.read(bytes)) != -1) { - zipOutputStream.write(bytes, 0, len); - zipOutputStream.flush(); - } - } - } - - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/src/main/java/plus/maa/backend/service/UserDetailServiceImpl.java b/src/main/java/plus/maa/backend/service/UserDetailServiceImpl.java deleted file mode 100644 index b3f116ca..00000000 --- a/src/main/java/plus/maa/backend/service/UserDetailServiceImpl.java +++ /dev/null @@ -1,51 +0,0 @@ -package plus.maa.backend.service; - -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; -import plus.maa.backend.repository.UserRepository; -import plus.maa.backend.repository.entity.MaaUser; -import plus.maa.backend.service.model.LoginUser; - -import java.util.ArrayList; -import java.util.Collection; - -/** - * @author AnselYuki - */ -@Service -@RequiredArgsConstructor -public class UserDetailServiceImpl implements UserDetailsService { - private final UserRepository userRepository; - - /** - * 查询用户信息 - * - * @param email 用户使用邮箱登录 - * @return 用户详细信息 - * @throws UsernameNotFoundException 用户名未找到 - */ - @Override - public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - MaaUser user = userRepository.findByEmail(email); - if (user == null) { - throw new UsernameNotFoundException("用户不存在"); - } - - var permissions = collectAuthoritiesFor(user); - //数据封装成UserDetails返回 - return new LoginUser(user, permissions); - } - - public Collection collectAuthoritiesFor(MaaUser user) { - var authorities = new ArrayList(); - for (int i = 0; i <= user.getStatus(); i++) { - authorities.add(new SimpleGrantedAuthority(Integer.toString(i))); - } - return authorities; - } -} diff --git a/src/main/java/plus/maa/backend/service/UserService.java b/src/main/java/plus/maa/backend/service/UserService.java deleted file mode 100644 index 764c55e4..00000000 --- a/src/main/java/plus/maa/backend/service/UserService.java +++ /dev/null @@ -1,216 +0,0 @@ -package plus.maa.backend.service; - -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; -import org.springframework.beans.BeanUtils; -import org.springframework.dao.DuplicateKeyException; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import plus.maa.backend.common.MaaStatusCode; -import plus.maa.backend.common.utils.converter.MaaUserConverter; -import plus.maa.backend.controller.request.user.*; -import plus.maa.backend.controller.response.MaaResultException; -import plus.maa.backend.controller.response.user.MaaLoginRsp; -import plus.maa.backend.controller.response.user.MaaUserInfo; -import plus.maa.backend.repository.RedisCache; -import plus.maa.backend.repository.UserRepository; -import plus.maa.backend.repository.entity.MaaUser; -import plus.maa.backend.service.jwt.JwtExpiredException; -import plus.maa.backend.service.jwt.JwtInvalidException; -import plus.maa.backend.service.jwt.JwtService; - -import java.util.ArrayList; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.UUID; - -/** - * @author AnselYuki - */ -@Setter -@Slf4j -@Service -@RequiredArgsConstructor -public class UserService { - - // 未来转为配置项 - private static final int LOGIN_LIMIT = 1; - - private final UserRepository userRepository; - private final EmailService emailService; - private final PasswordEncoder passwordEncoder; - private final UserDetailServiceImpl userDetailService; - private final JwtService jwtService; - private final MaaUserConverter maaUserConverter; - - /** - * 登录方法 - * - * @param loginDTO 登录参数 - * @return 携带了token的封装类 - */ - public MaaLoginRsp login(LoginDTO loginDTO) { - var user = userRepository.findByEmail(loginDTO.getEmail()); - if (user == null || !passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) { - throw new MaaResultException(401, "用户不存在或者密码错误"); - } - // 未激活的用户 - if (Objects.equals(user.getStatus(), 0)) { - throw new MaaResultException(MaaStatusCode.MAA_USER_NOT_ENABLED); - } - - var jwtId = UUID.randomUUID().toString(); - var jwtIds = user.getRefreshJwtIds(); - jwtIds.add(jwtId); - while (jwtIds.size() > LOGIN_LIMIT) jwtIds.remove(0); - userRepository.save(user); - - var authorities = userDetailService.collectAuthoritiesFor(user); - var authToken = jwtService.issueAuthToken(user.getUserId(), null, authorities); - var refreshToken = jwtService.issueRefreshToken(user.getUserId(), jwtId); - - return new MaaLoginRsp( - authToken.getValue(), - authToken.getExpiresAt(), - authToken.getNotBefore(), - refreshToken.getValue(), - refreshToken.getExpiresAt(), - refreshToken.getNotBefore(), - maaUserConverter.convert(user) - ); - } - - /** - * 修改密码 - * - * @param userId 当前用户 - * @param rawPassword 新密码 - */ - public void modifyPassword(String userId, String rawPassword) { - var userResult = userRepository.findById(userId); - if (userResult.isEmpty()) return; - var maaUser = userResult.get(); - // 修改密码的逻辑,应当使用与 authentication provider 一致的编码器 - maaUser.setPassword(passwordEncoder.encode(rawPassword)); - maaUser.setRefreshJwtIds(new ArrayList<>()); - userRepository.save(maaUser); - } - - /** - * 用户注册 - * - * @param registerDTO 传入用户参数 - * @return 返回注册成功的用户摘要(脱敏) - */ - public MaaUserInfo register(RegisterDTO registerDTO) { - String encode = passwordEncoder.encode(registerDTO.getPassword()); - - // 校验验证码 - emailService.verifyVCode(registerDTO.getEmail(), registerDTO.getRegistrationToken()); - - MaaUser user = new MaaUser(); - BeanUtils.copyProperties(registerDTO, user); - user.setPassword(encode); - user.setStatus(1); - MaaUserInfo userInfo; - try { - MaaUser save = userRepository.save(user); - userInfo = new MaaUserInfo(save); - } catch (DuplicateKeyException e) { - throw new MaaResultException(MaaStatusCode.MAA_USER_EXISTS); - } - return userInfo; - } - - /** - * 更新用户信息 - * - * @param userId 用户id - * @param updateDTO 更新参数 - */ - public void updateUserInfo(@NotNull String userId, UserInfoUpdateDTO updateDTO) { - userRepository.findById(userId).ifPresent((maaUser) -> { - maaUser.updateAttribute(updateDTO); - userRepository.save(maaUser); - }); - } - - /** - * 刷新token - * - * @param token token - */ - public MaaLoginRsp refreshToken(String token) { - try { - var old = jwtService.verifyAndParseRefreshToken(token); - - var userId = old.getSubject(); - var user = userRepository.findById(userId).orElseThrow(); - - var refreshJwtIds = user.getRefreshJwtIds(); - int idIndex = refreshJwtIds.indexOf(old.getJwtId()); - if (idIndex < 0) throw new MaaResultException(401, "invalid token"); - - var jwtId = UUID.randomUUID().toString(); - refreshJwtIds.set(idIndex, jwtId); - - userRepository.save(user); - - var refreshToken = jwtService.newRefreshToken(old, jwtId); - - var authorities = userDetailService.collectAuthoritiesFor(user); - var authToken = jwtService.issueAuthToken(userId, null, authorities); - - return new MaaLoginRsp( - authToken.getValue(), - authToken.getExpiresAt(), - authToken.getNotBefore(), - refreshToken.getValue(), - refreshToken.getExpiresAt(), - refreshToken.getNotBefore(), - maaUserConverter.convert(user) - ); - } catch (JwtInvalidException | JwtExpiredException | NoSuchElementException e) { - throw new MaaResultException(401, e.getMessage()); - } - } - - /** - * 通过邮箱激活码更新密码 - * - * @param passwordResetDTO 通过邮箱修改密码请求 - */ - public void modifyPasswordByActiveCode(PasswordResetDTO passwordResetDTO) { - emailService.verifyVCode(passwordResetDTO.getEmail(), passwordResetDTO.getActiveCode()); - var maaUser = userRepository.findByEmail(passwordResetDTO.getEmail()); - modifyPassword(maaUser.getUserId(), passwordResetDTO.getPassword()); - } - - /** - * 根据邮箱校验用户是否存在 - * - * @param email 用户邮箱 - */ - public void checkUserExistByEmail(String email) { - if (null == userRepository.findByEmail(email)) { - throw new MaaResultException(MaaStatusCode.MAA_USER_NOT_FOUND); - } - } - - /** - * 注册时发送验证码 - */ - public void sendRegistrationToken(SendRegistrationTokenDTO regDTO) { - // 判断用户是否存在 - MaaUser maaUser = userRepository.findByEmail(regDTO.getEmail()); - if (maaUser != null) { - // 用户已存在 - throw new MaaResultException(MaaStatusCode.MAA_USER_EXISTS); - } - // 发送验证码 - emailService.sendVCode(regDTO.getEmail()); - } - -} diff --git a/src/main/java/plus/maa/backend/service/jwt/JwtAuthToken.java b/src/main/java/plus/maa/backend/service/jwt/JwtAuthToken.java deleted file mode 100644 index a091f54a..00000000 --- a/src/main/java/plus/maa/backend/service/jwt/JwtAuthToken.java +++ /dev/null @@ -1,103 +0,0 @@ -package plus.maa.backend.service.jwt; - -import cn.hutool.jwt.JWT; -import lombok.Getter; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.util.StringUtils; - -import java.time.LocalDateTime; -import java.util.Collection; - -/** - * 基于 {@link JWT} 的 AuthToken. 本类实现了 {@link Authentication}, 可直接用于 Spring Security - * 的认证流程 - */ -@Getter -public final class JwtAuthToken extends JwtToken implements Authentication { - /** - * AuthToken 类型值 - */ - public static final String TYPE = "auth"; - private static final String CLAIM_AUTHORITIES = "Authorities"; - private boolean authenticated = false; - - /** - * 从 jwt 构建 token - * - * @param jwt jwt - * @param key 签名密钥 - * @throws JwtInvalidException jwt 未通过签名验证或不符合要求 - */ - public JwtAuthToken(String jwt, byte[] key) throws JwtInvalidException { - super(jwt, TYPE, key); - } - - public JwtAuthToken( - String sub, - String jti, - LocalDateTime iat, - LocalDateTime exp, - LocalDateTime nbf, - Collection authorities, - byte[] key - ) { - super(sub, jti, iat, exp, nbf, TYPE, key); - this.setAuthorities(authorities); - } - - - @Override - public Collection getAuthorities() { - var authorityStrings = getJwt().getPayloads().getStr(CLAIM_AUTHORITIES); - return StringUtils.commaDelimitedListToSet(authorityStrings).stream() - .map(SimpleGrantedAuthority::new) - .toList(); - } - - public void setAuthorities(Collection authorities) { - var authorityStrings = authorities.stream().map(GrantedAuthority::getAuthority).toList(); - var encodedAuthorities = StringUtils.collectionToCommaDelimitedString(authorityStrings); - getJwt().setPayload(CLAIM_AUTHORITIES, encodedAuthorities); - } - - /** - * @return credentials,采用 jwt 的 id - * @inheritDoc - */ - @Override - public Object getCredentials() { - return getJwtId(); - } - - @Override - public Object getDetails() { - return null; - } - - /** - * @return principal,采用 jwt 的 subject - * @inheritDoc - */ - @Override - public Object getPrincipal() { - return getSubject(); - } - - @Override - public boolean isAuthenticated() { - return this.authenticated; - } - - @Override - public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { - this.authenticated = isAuthenticated; - } - - @Override - public String getName() { - return getSubject(); - } - -} diff --git a/src/main/java/plus/maa/backend/service/jwt/JwtExpiredException.java b/src/main/java/plus/maa/backend/service/jwt/JwtExpiredException.java deleted file mode 100644 index f723e627..00000000 --- a/src/main/java/plus/maa/backend/service/jwt/JwtExpiredException.java +++ /dev/null @@ -1,7 +0,0 @@ -package plus.maa.backend.service.jwt; - -public class JwtExpiredException extends Exception { - public JwtExpiredException(String message) { - super(message); - } -} diff --git a/src/main/java/plus/maa/backend/service/jwt/JwtInvalidException.java b/src/main/java/plus/maa/backend/service/jwt/JwtInvalidException.java deleted file mode 100644 index 7d124e19..00000000 --- a/src/main/java/plus/maa/backend/service/jwt/JwtInvalidException.java +++ /dev/null @@ -1,4 +0,0 @@ -package plus.maa.backend.service.jwt; - -public class JwtInvalidException extends Exception{ -} diff --git a/src/main/java/plus/maa/backend/service/jwt/JwtRefreshToken.java b/src/main/java/plus/maa/backend/service/jwt/JwtRefreshToken.java deleted file mode 100644 index ccd319c8..00000000 --- a/src/main/java/plus/maa/backend/service/jwt/JwtRefreshToken.java +++ /dev/null @@ -1,33 +0,0 @@ -package plus.maa.backend.service.jwt; - -import java.time.LocalDateTime; - -public class JwtRefreshToken extends JwtToken { - /** - * RefreshToken 类型值 - */ - public static final String TYPE = "refresh"; - - /** - * 从 jwt 构建 token - * - * @param token jwt - * @param key 签名密钥 - * @throws JwtInvalidException jwt 未通过签名验证或不符合要求 - */ - public JwtRefreshToken(String token, byte[] key) throws JwtInvalidException { - super(token, TYPE, key); - } - - public JwtRefreshToken( - String sub, - String jti, - LocalDateTime iat, - LocalDateTime exp, - LocalDateTime nbf, - byte[] key - ) { - super(sub, jti, iat, exp, nbf, TYPE, key); - } - -} diff --git a/src/main/java/plus/maa/backend/service/jwt/JwtService.java b/src/main/java/plus/maa/backend/service/jwt/JwtService.java deleted file mode 100644 index 9c7dd48c..00000000 --- a/src/main/java/plus/maa/backend/service/jwt/JwtService.java +++ /dev/null @@ -1,99 +0,0 @@ -package plus.maa.backend.service.jwt; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.stereotype.Service; -import plus.maa.backend.config.external.Jwt; -import plus.maa.backend.config.external.MaaCopilotProperties; - -import java.time.LocalDateTime; -import java.util.Collection; - -/** - * 基于 Jwt 的 token 服务。 可直接用于 stateless 情境下的签发和认证, 或结合数据库进行状态管理。 - * 建议 AuthToken 使用无状态方案, RefreshToken 使用有状态方案 - */ -@Service -public class JwtService { - private final Jwt jwtProperties; - private final byte[] key; - - public JwtService(MaaCopilotProperties properties) { - jwtProperties = properties.getJwt(); - key = jwtProperties.getSecret().getBytes(); - } - - /** - * 签发 AuthToken. 过期时间由配置的 {@link Jwt#getExpire()} 计算而来 - * - * @param subject 签发对象,一般设置为对象的标识符 - * @param jwtId jwt 的 id, 一般用于 stateful 场景下 - * @param authorities 授予的权限 - * @return JwtAuthToken - */ - public JwtAuthToken issueAuthToken(String subject, @Nullable String jwtId, Collection authorities) { - LocalDateTime now = LocalDateTime.now(); - LocalDateTime expireAt = now.plusSeconds(jwtProperties.getExpire()); - return new JwtAuthToken(subject, jwtId, now, expireAt, now, authorities, key); - } - - /** - * 验证并解析为 AuthToken. 该方法为 stateless 的验证。 - * - * @param authToken jwt 字符串 - * @return JwtAuthToken - * @throws JwtInvalidException jwt不符合要求 - * @throws JwtExpiredException jwt未生效或者已过期 - */ - @NotNull - public JwtAuthToken verifyAndParseAuthToken(String authToken) throws JwtInvalidException, JwtExpiredException { - var token = new JwtAuthToken(authToken, key); - token.validateDate(LocalDateTime.now()); - token.setAuthenticated(true); - return token; - } - - /** - * 签发 RefreshToken. 过期时间由配置的 {@link Jwt#getRefreshExpire()} 计算而来 - * - * @param subject 签发对象,一般设置为对象的标识符 - * @param jwtId jwt 的 id, 一般用于 stateful 场景下 - * @return JwtAuthToken - */ - @NotNull - public JwtRefreshToken issueRefreshToken(String subject, @Nullable String jwtId) { - LocalDateTime now = LocalDateTime.now(); - LocalDateTime expireAt = now.plusSeconds(jwtProperties.getRefreshExpire()); - return new JwtRefreshToken(subject, jwtId, now, expireAt, now, key); - } - - /** - * 产生新的 RefreshToken. 新的 token 除了签发和生效时间、 id 不同外,其余属性均继承自原来的 token. - * 一般情况下, RefreshToken 应结合数据库使用以避免陷入无法撤销的窘境 - * - * @param old 原 token - * @return 新的 RefreshToken - */ - @NotNull - public JwtRefreshToken newRefreshToken(JwtRefreshToken old, @Nullable String jwtId) { - LocalDateTime now = LocalDateTime.now(); - return new JwtRefreshToken(old.getSubject(), jwtId, now, old.getExpiresAt(), now, key); - } - - /** - * 验证并解析为 RefreshToken. 该方法为 stateless 的验证。 - * - * @param refreshToken jwt字符串 - * @return RefreshToken - * @throws JwtInvalidException jwt不符合要求 - * @throws JwtExpiredException jwt未生效或者已过期 - */ - @NotNull - public JwtRefreshToken verifyAndParseRefreshToken(String refreshToken) throws JwtInvalidException, JwtExpiredException { - var token = new JwtRefreshToken(refreshToken, key); - token.validateDate(LocalDateTime.now()); - return token; - } - -} diff --git a/src/main/java/plus/maa/backend/service/jwt/JwtToken.java b/src/main/java/plus/maa/backend/service/jwt/JwtToken.java deleted file mode 100644 index d0c7d4fb..00000000 --- a/src/main/java/plus/maa/backend/service/jwt/JwtToken.java +++ /dev/null @@ -1,110 +0,0 @@ -package plus.maa.backend.service.jwt; - -import cn.hutool.json.JSONObject; -import cn.hutool.jwt.JWT; -import cn.hutool.jwt.JWTUtil; -import cn.hutool.jwt.RegisteredPayload; -import lombok.Getter; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; -import java.util.TimeZone; - -/** - * 对 {@link JWT} 的包装增强,某些 payload 被标记为 MUST - */ -public class JwtToken { - private static final String CLAIM_TYPE = "typ"; - @Getter - private final JWT jwt; - - private final JSONObject payload; - - public JwtToken(String token, String requiredType, byte[] key) throws JwtInvalidException { - if (!JWTUtil.verify(token, key)) throw new JwtInvalidException(); - this.jwt = JWTUtil.parseToken(token); - this.jwt.setKey(key); - this.payload = jwt.getPayloads(); - - // jwtId is nullable - if (null == getSubject() - || null == getIssuedAt() - || null == getExpiresAt() - || null == getNotBefore() - || !requiredType.equals(getType()) - ) throw new JwtInvalidException(); - } - - public JwtToken( - String sub, - String jti, - LocalDateTime iat, - LocalDateTime exp, - LocalDateTime nbf, - String typ, - byte[] key - ) { - jwt = JWT.create(); - jwt.setPayload(RegisteredPayload.SUBJECT, sub); - jwt.setPayload(RegisteredPayload.JWT_ID, jti); - jwt.setPayload(RegisteredPayload.ISSUED_AT, iat.toInstant(OffsetDateTime.now().getOffset()).toEpochMilli()); - jwt.setPayload(RegisteredPayload.EXPIRES_AT, exp.toInstant(OffsetDateTime.now().getOffset()).toEpochMilli()); - jwt.setPayload(RegisteredPayload.NOT_BEFORE, nbf.toInstant(OffsetDateTime.now().getOffset()).toEpochMilli()); - jwt.setPayload(CLAIM_TYPE, typ); - jwt.setKey(key); - payload = jwt.getPayloads(); - } - - - public String getSubject() { - return payload.getStr(RegisteredPayload.SUBJECT); - } - - - public String getJwtId() { - return payload.getStr(RegisteredPayload.JWT_ID); - } - - - public LocalDateTime getIssuedAt() { - return LocalDateTime.ofInstant(Instant.ofEpochMilli(payload.getLong(RegisteredPayload.ISSUED_AT)), - TimeZone.getDefault().toZoneId()); - } - - - public LocalDateTime getExpiresAt() { - return LocalDateTime.ofInstant(Instant.ofEpochMilli(payload.getLong(RegisteredPayload.EXPIRES_AT)), - TimeZone.getDefault().toZoneId()); - } - - - public LocalDateTime getNotBefore() { - return LocalDateTime.ofInstant(Instant.ofEpochMilli(payload.getLong(RegisteredPayload.NOT_BEFORE)), - TimeZone.getDefault().toZoneId()); - } - - - public String getType() { - return payload.getStr(CLAIM_TYPE); - } - - public void setType(String type) { - payload.set(CLAIM_TYPE, type); - } - - /** - * 生成 jwt 字符串 - * - * @return 签名后的 jwt 字符串 - */ - public String getValue() { - return jwt.sign(); - } - - public void validateDate(LocalDateTime moment) throws JwtExpiredException { - if (!moment.isBefore(getExpiresAt())) throw new JwtExpiredException("expired"); - if (moment.isBefore(getNotBefore())) throw new JwtExpiredException("haven't take effect"); - } - -} diff --git a/src/main/java/plus/maa/backend/service/model/ArkLevelType.java b/src/main/java/plus/maa/backend/service/model/ArkLevelType.java deleted file mode 100644 index 2db6d5e8..00000000 --- a/src/main/java/plus/maa/backend/service/model/ArkLevelType.java +++ /dev/null @@ -1,43 +0,0 @@ -package plus.maa.backend.service.model; - -import lombok.Getter; -import org.springframework.util.ObjectUtils; - -@Getter -public enum ArkLevelType { - MAINLINE("主题曲"), - WEEKLY("资源收集"), - ACTIVITIES("活动关卡"), - CAMPAIGN("剿灭作战"), - MEMORY("悖论模拟"), - RUNE("危机合约"), - LEGION("保全派驻"), - ROGUELIKE("集成战略"), //实际不进行解析 - TRAINING("训练关卡"), //实际不进行解析 - UNKNOWN("未知类型"); - private final String display; - - ArkLevelType(String display) { - this.display = display; - } - - public static ArkLevelType fromLevelId(String levelId) { - if (ObjectUtils.isEmpty(levelId)) { - return UNKNOWN; - } - String[] ids = levelId.toLowerCase().split("/"); - String type = (ids[0].equals("obt")) ? ids[1] : ids[0]; - return switch (type) { - case "main", "hard" -> MAINLINE; - case "weekly", "promote" -> WEEKLY; - case "activities" -> ACTIVITIES; - case "campaign" -> CAMPAIGN; - case "memory" -> MEMORY; - case "rune" -> RUNE; - case "legion" -> LEGION; - case "roguelike" -> ROGUELIKE; - case "training" -> TRAINING; - default -> UNKNOWN; - }; - } -} diff --git a/src/main/java/plus/maa/backend/service/model/CommentNotification.java b/src/main/java/plus/maa/backend/service/model/CommentNotification.java deleted file mode 100644 index ed0bfae5..00000000 --- a/src/main/java/plus/maa/backend/service/model/CommentNotification.java +++ /dev/null @@ -1,20 +0,0 @@ -package plus.maa.backend.service.model; - -import lombok.Data; -import lombok.experimental.Accessors; - -/** - * @author LoMu - * Date 2023-05-18 1:18 - */ - -@Data -@Accessors(chain = true) -public class CommentNotification { - private String authorName; - private String reName; - private String date; - private String title; - private String reMessage; - private String forntEndLink; -} diff --git a/src/main/java/plus/maa/backend/service/model/LoginUser.java b/src/main/java/plus/maa/backend/service/model/LoginUser.java deleted file mode 100644 index d8e9f311..00000000 --- a/src/main/java/plus/maa/backend/service/model/LoginUser.java +++ /dev/null @@ -1,80 +0,0 @@ -package plus.maa.backend.service.model; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import plus.maa.backend.repository.entity.MaaUser; - -import java.util.Collection; - -/** - * @author AnselYuki - */ -public class LoginUser implements UserDetails { - - private final MaaUser maaUser; - private final Collection authorities; - - public LoginUser(MaaUser maaUser, Collection authorities) { - this.maaUser = maaUser; - this.authorities = authorities; - } - - @Override - @JsonIgnore - public Collection getAuthorities() { - return authorities; - } - - @Override - @JsonIgnore - public String getPassword() { - return maaUser.getPassword(); - } - - public String getUserId() { - return maaUser.getUserId(); - } - - /** - * Spring Security框架中的username即唯一身份标识(ID) - * 效果同getEmail - * - * @return 用户邮箱 - */ - @Override - @JsonIgnore - public String getUsername() { - return maaUser.getEmail(); - } - - @JsonIgnore - public String getEmail() { - return maaUser.getEmail(); - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return true; - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - /** - * 默认用户为0(禁用),1为启用 - * - * @return 账户启用状态 - */ - @Override - public boolean isEnabled() { - return true; - } -} diff --git a/src/main/java/plus/maa/backend/service/model/RatingCache.java b/src/main/java/plus/maa/backend/service/model/RatingCache.java deleted file mode 100644 index 6e7e7031..00000000 --- a/src/main/java/plus/maa/backend/service/model/RatingCache.java +++ /dev/null @@ -1,19 +0,0 @@ -package plus.maa.backend.service.model; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.Set; - -/** - * @author LoMu - * Date 2023-01-28 11:37 - */ - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class RatingCache { - private Set copilotIds; -} diff --git a/src/main/java/plus/maa/backend/service/model/RatingCount.java b/src/main/java/plus/maa/backend/service/model/RatingCount.java deleted file mode 100644 index 18864111..00000000 --- a/src/main/java/plus/maa/backend/service/model/RatingCount.java +++ /dev/null @@ -1,13 +0,0 @@ -package plus.maa.backend.service.model; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class RatingCount { - private String key; - private long count; -} diff --git a/src/main/java/plus/maa/backend/service/model/RatingType.java b/src/main/java/plus/maa/backend/service/model/RatingType.java deleted file mode 100644 index ca4173cb..00000000 --- a/src/main/java/plus/maa/backend/service/model/RatingType.java +++ /dev/null @@ -1,39 +0,0 @@ -package plus.maa.backend.service.model; - -import lombok.Getter; - - -/** - * @author LoMu - * Date 2023-01-22 19:48 - */ -@Getter -public enum RatingType { - - LIKE(1), - DISLIKE(2), - NONE(0); - - private final int display; - - RatingType(int display) { - this.display = display; - } - - /** - * 将rating转换为 0 = NONE 1 = LIKE 2 = DISLIKE - * - * @param type rating - * @return type - */ - public static RatingType fromRatingType(String type) { - return switch (type) { - case "Like" -> LIKE; - case "Dislike" -> DISLIKE; - default -> NONE; - }; - - } -} - - diff --git a/src/main/java/plus/maa/backend/service/model/parser/ActivityParser.java b/src/main/java/plus/maa/backend/service/model/parser/ActivityParser.java deleted file mode 100644 index 7cc8a804..00000000 --- a/src/main/java/plus/maa/backend/service/model/parser/ActivityParser.java +++ /dev/null @@ -1,48 +0,0 @@ -package plus.maa.backend.service.model.parser; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Component; -import plus.maa.backend.repository.entity.ArkLevel; -import plus.maa.backend.repository.entity.gamedata.ArkActivity; -import plus.maa.backend.repository.entity.gamedata.ArkStage; -import plus.maa.backend.repository.entity.gamedata.ArkTilePos; -import plus.maa.backend.service.ArkGameDataService; -import plus.maa.backend.service.model.ArkLevelType; - -import java.util.Optional; - -/** - * @author john180 - *

- * Activity level will be tagged like this:
- * Activity -> ACT_NAME -> StageCode == activities/ACT_ID/LEVEL_ID
- * eg:
- * 活动关卡 -> 战地秘闻 -> SW-EV-1 == activities/act4d0/level_act4d0_01
- */ -@Slf4j -@Component -@RequiredArgsConstructor -public class ActivityParser implements ArkLevelParser { - private final ArkGameDataService dataService; - - @Override - public boolean supportType(ArkLevelType type) { - return ArkLevelType.ACTIVITIES.equals(type); - } - - @Override - public ArkLevel parseLevel(ArkLevel level, ArkTilePos tilePos) { - level.setCatOne(ArkLevelType.ACTIVITIES.getDisplay()); - - ArkStage stage = dataService.findStage(level.getLevelId(), tilePos.getCode(), tilePos.getStageId()); - level.setCatTwo( - Optional.ofNullable(stage) - .map(ArkStage::getZoneId) - .map(dataService::findActivityByZoneId) - .map(ArkActivity::getName) - .orElse(StringUtils.EMPTY)); - return level; - } -} diff --git a/src/main/java/plus/maa/backend/service/model/parser/ArkLevelParser.java b/src/main/java/plus/maa/backend/service/model/parser/ArkLevelParser.java deleted file mode 100644 index fff07c55..00000000 --- a/src/main/java/plus/maa/backend/service/model/parser/ArkLevelParser.java +++ /dev/null @@ -1,25 +0,0 @@ -package plus.maa.backend.service.model.parser; - -import plus.maa.backend.repository.entity.ArkLevel; -import plus.maa.backend.repository.entity.gamedata.ArkTilePos; -import plus.maa.backend.service.model.ArkLevelType; - -/** - * @author john180 - */ -public interface ArkLevelParser { - - /** - * 是否支持解析该关卡类型 - * - * @param type 关卡类型 - * @return 是否支持 - */ - boolean supportType(ArkLevelType type); - - /** - * 解析关卡 - */ - ArkLevel parseLevel(ArkLevel level, ArkTilePos tilePos); - -} diff --git a/src/main/java/plus/maa/backend/service/model/parser/CampaignParser.java b/src/main/java/plus/maa/backend/service/model/parser/CampaignParser.java deleted file mode 100644 index 4438d869..00000000 --- a/src/main/java/plus/maa/backend/service/model/parser/CampaignParser.java +++ /dev/null @@ -1,30 +0,0 @@ -package plus.maa.backend.service.model.parser; - -import org.springframework.stereotype.Component; -import plus.maa.backend.repository.entity.ArkLevel; -import plus.maa.backend.repository.entity.gamedata.ArkTilePos; -import plus.maa.backend.service.model.ArkLevelType; - -/** - * @author john180 - *

- * Campaign level will be tagged like this:
- * CAMPAIGN -> CAMPAIGN_CODE -> CAMPAIGN_NAME == obt/campaign/LEVEL_ID
- * eg:
- * 剿灭作战 -> 炎国 -> 龙门外环 == obt/campaign/level_camp_02
- */ -@Component -public class CampaignParser implements ArkLevelParser { - @Override - public boolean supportType(ArkLevelType type) { - return ArkLevelType.CAMPAIGN.equals(type); - } - - @Override - public ArkLevel parseLevel(ArkLevel level, ArkTilePos tilePos) { - level.setCatOne(ArkLevelType.CAMPAIGN.getDisplay()); - level.setCatTwo(tilePos.getCode()); - level.setCatThree(level.getName()); - return level; - } -} diff --git a/src/main/java/plus/maa/backend/service/model/parser/LegionParser.java b/src/main/java/plus/maa/backend/service/model/parser/LegionParser.java deleted file mode 100644 index 66ff3b8d..00000000 --- a/src/main/java/plus/maa/backend/service/model/parser/LegionParser.java +++ /dev/null @@ -1,53 +0,0 @@ -package plus.maa.backend.service.model.parser; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.stereotype.Component; -import plus.maa.backend.repository.entity.ArkLevel; -import plus.maa.backend.repository.entity.gamedata.ArkStage; -import plus.maa.backend.repository.entity.gamedata.ArkTilePos; -import plus.maa.backend.repository.entity.gamedata.ArkTower; -import plus.maa.backend.service.ArkGameDataService; -import plus.maa.backend.service.model.ArkLevelType; - -import java.util.Optional; - -/** - * @author john180 - *

- * Legion level will be tagged like this:
- * LEGION -> POSITION -> StageCode == obt/legion/TOWER_ID/LEVEL_ID
- * eg:
- * 保全派驻 -> 阿卡胡拉丛林 -> LT-1 == obt/legion/lt06/level_lt06_01
- */ -@Slf4j -@Component -@RequiredArgsConstructor -public class LegionParser implements ArkLevelParser { - private final ArkGameDataService dataService; - - @Override - public boolean supportType(ArkLevelType type) { - return ArkLevelType.LEGION.equals(type); - } - - @Override - public ArkLevel parseLevel(ArkLevel level, ArkTilePos tilePos) { - level.setCatOne(ArkLevelType.LEGION.getDisplay()); - - ArkStage stage = dataService.findStage(level.getLevelId(), tilePos.getCode(), tilePos.getStageId()); - if (stage == null) { - log.error("[PARSER]保全派驻关卡未找到stage信息:{}", level.getLevelId()); - return null; - } - - String catTwo= Optional.ofNullable(dataService.findTower(stage.getZoneId())) - .map(ArkTower::getName) - .orElse(StringUtils.EMPTY); - - level.setCatTwo(catTwo); - level.setCatThree(tilePos.getCode()); - return level; - } -} diff --git a/src/main/java/plus/maa/backend/service/model/parser/MainlineParser.java b/src/main/java/plus/maa/backend/service/model/parser/MainlineParser.java deleted file mode 100644 index 3653d03b..00000000 --- a/src/main/java/plus/maa/backend/service/model/parser/MainlineParser.java +++ /dev/null @@ -1,75 +0,0 @@ -package plus.maa.backend.service.model.parser; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import org.springframework.util.ObjectUtils; -import plus.maa.backend.repository.entity.ArkLevel; -import plus.maa.backend.repository.entity.gamedata.ArkTilePos; -import plus.maa.backend.repository.entity.gamedata.ArkZone; -import plus.maa.backend.service.ArkGameDataService; -import plus.maa.backend.service.model.ArkLevelType; - -/** - * @author john180 - *

- * Main story level will be tagged like this:
- * MAINLINE -> CHAPTER_NAME -> StageCode == obt/main/LEVEL_ID
- * eg:
- * 主题曲 -> 序章:黑暗时代·上 -> 0-1 == obt/main/level_main_00-01
- * 主题曲 -> 第四章:急性衰竭 -> S4-7 == obt/main/level_sub_04-3-1
- */ -@Component -@RequiredArgsConstructor -public class MainlineParser implements ArkLevelParser { - private final ArkGameDataService dataService; - - @Override - public boolean supportType(ArkLevelType type) { - return ArkLevelType.MAINLINE.equals(type); - } - - @Override - public ArkLevel parseLevel(ArkLevel level, ArkTilePos tilePos) { - level.setCatOne(ArkLevelType.MAINLINE.getDisplay()); - - String chapterLevelId = level.getLevelId().split("/")[2]; // level_main_10-02 - String[] chapterStrSplit = chapterLevelId.split("_"); // level main 10-02 - String diff = parseDifficulty(chapterStrSplit[1]); // easy、main - String stageCodeEncoded = chapterStrSplit[chapterStrSplit.length - 1]; // 10-02 remark: obt/main/level_easy_sub_09-1-1 - String chapterStr = stageCodeEncoded.split("-")[0]; // 10 (str) - int chapter = Integer.parseInt(chapterStr); // 10 (int) - - ArkZone zone = dataService.findZone(level.getLevelId(), tilePos.getCode(), tilePos.getStageId()); - if (zone == null) { - return null; - } - - String catTwo = parseZoneName(zone); - level.setCatTwo(catTwo); - - String catThreeEx = (chapter >= 9) ? String.format("(%s)", diff) : ""; - level.setCatThree(level.getCatThree() + catThreeEx); - - return level; - } - - private String parseDifficulty(String diff) { - return switch (diff.toLowerCase()) { - case "easy" -> "简单"; - case "tough" -> "磨难"; - default -> "标准"; - }; - } - - private String parseZoneName(ArkZone zone) { - StringBuilder builder = new StringBuilder(); - if (!ObjectUtils.isEmpty(zone.getZoneNameFirst())) { - builder.append(zone.getZoneNameFirst()); - } - builder.append(" "); - if (!ObjectUtils.isEmpty(zone.getZoneNameSecond())) { - builder.append(zone.getZoneNameSecond()); - } - return builder.toString().trim(); - } -} diff --git a/src/main/java/plus/maa/backend/service/model/parser/MemoryParser.java b/src/main/java/plus/maa/backend/service/model/parser/MemoryParser.java deleted file mode 100644 index ac82a737..00000000 --- a/src/main/java/plus/maa/backend/service/model/parser/MemoryParser.java +++ /dev/null @@ -1,66 +0,0 @@ -package plus.maa.backend.service.model.parser; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import plus.maa.backend.repository.entity.ArkLevel; -import plus.maa.backend.repository.entity.gamedata.ArkCharacter; -import plus.maa.backend.repository.entity.gamedata.ArkTilePos; -import plus.maa.backend.service.ArkGameDataService; -import plus.maa.backend.service.model.ArkLevelType; - -/** - * @author john180 - *

- * Memory level will be tagged like this:
- * MEMORY -> POSITION -> OPERATOR_NAME == obt/memory/LEVEL_ID
- * eg:
- * 悖论模拟 -> 狙击 -> 克洛丝 == obt/memory/level_memory_kroos_1
- */ -@Slf4j -@Component -@RequiredArgsConstructor -public class MemoryParser implements ArkLevelParser { - private final ArkGameDataService dataService; - - @Override - public boolean supportType(ArkLevelType type) { - return ArkLevelType.MEMORY.equals(type); - } - - @Override - public ArkLevel parseLevel(ArkLevel level, ArkTilePos tilePos) { - level.setCatOne(ArkLevelType.MEMORY.getDisplay()); - - String[] chIdSplit = level.getStageId().split("_"); //mem_aurora_1 - if (chIdSplit.length != 3) { - log.error("[PARSER]悖论模拟关卡stageId异常:{}, level:{}", level.getStageId(), level.getLevelId()); - return null; - } - String chId = chIdSplit[1]; //aurora - ArkCharacter character = dataService.findCharacter(chId); - if (character == null) { - log.error("[PARSER]悖论模拟关卡未找到角色信息:{}, level:{}", level.getStageId(), level.getLevelId()); - return null; - } - - level.setCatTwo(parseProfession(character.getProfession())); - level.setCatThree(character.getName()); - - return level; - } - - private String parseProfession(String professionId) { - return switch (professionId.toLowerCase()) { - case "medic" -> "医疗"; - case "special" -> "特种"; - case "warrior" -> "近卫"; - case "sniper" -> "狙击"; - case "tank" -> "重装"; - case "caster" -> "术师"; - case "pioneer" -> "先锋"; - case "support" -> "辅助"; - default -> "未知"; - }; - } -} diff --git a/src/main/java/plus/maa/backend/service/model/parser/RuneParser.java b/src/main/java/plus/maa/backend/service/model/parser/RuneParser.java deleted file mode 100644 index 1e7099fb..00000000 --- a/src/main/java/plus/maa/backend/service/model/parser/RuneParser.java +++ /dev/null @@ -1,22 +0,0 @@ -package plus.maa.backend.service.model.parser; - -import org.springframework.stereotype.Component; -import plus.maa.backend.repository.entity.ArkLevel; -import plus.maa.backend.repository.entity.gamedata.ArkTilePos; -import plus.maa.backend.service.model.ArkLevelType; - -@Component -public class RuneParser implements ArkLevelParser { - @Override - public boolean supportType(ArkLevelType type) { - return ArkLevelType.RUNE.equals(type); - } - - @Override - public ArkLevel parseLevel(ArkLevel level, ArkTilePos tilePos) { - level.setCatOne(ArkLevelType.RUNE.getDisplay()); - level.setCatTwo(tilePos.getCode()); - level.setCatThree(level.getName()); - return level; - } -} diff --git a/src/main/java/plus/maa/backend/service/model/parser/UnknownParser.java b/src/main/java/plus/maa/backend/service/model/parser/UnknownParser.java deleted file mode 100644 index d2b3861a..00000000 --- a/src/main/java/plus/maa/backend/service/model/parser/UnknownParser.java +++ /dev/null @@ -1,23 +0,0 @@ -package plus.maa.backend.service.model.parser; - -import org.springframework.stereotype.Component; -import plus.maa.backend.repository.entity.ArkLevel; -import plus.maa.backend.repository.entity.gamedata.ArkTilePos; -import plus.maa.backend.service.model.ArkLevelType; - -@Component -public class UnknownParser implements ArkLevelParser { - @Override - public boolean supportType(ArkLevelType type) { - return ArkLevelType.UNKNOWN.equals(type); - } - - @Override - public ArkLevel parseLevel(ArkLevel level, ArkTilePos tilePos) { - String[] ids = level.getLevelId().toLowerCase().split("/"); - String type = (ids[0].equals("obt")) ? ids[1] : ids[0]; - - level.setCatOne(ArkLevelType.UNKNOWN.getDisplay() + type); - return level; - } -} diff --git a/src/main/java/plus/maa/backend/service/model/parser/WeeklyParser.java b/src/main/java/plus/maa/backend/service/model/parser/WeeklyParser.java deleted file mode 100644 index 70c239f1..00000000 --- a/src/main/java/plus/maa/backend/service/model/parser/WeeklyParser.java +++ /dev/null @@ -1,42 +0,0 @@ -package plus.maa.backend.service.model.parser; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; -import plus.maa.backend.repository.entity.ArkLevel; -import plus.maa.backend.repository.entity.gamedata.ArkTilePos; -import plus.maa.backend.repository.entity.gamedata.ArkZone; -import plus.maa.backend.service.ArkGameDataService; -import plus.maa.backend.service.model.ArkLevelType; - -/** - * @author john180 - *

- * Weekly level will be tagged like this:
- * WEEKLY -> WEEKLY_ZONE_NAME -> StageCode == obt/weekly/LEVEL_ID
- * eg:
- * 资源收集 -> 空中威胁 -> CA-5 == obt/weekly/level_weekly_fly_5
- * 资源收集 -> 身先士卒 -> PR-D-2 == obt/promote/level_promote_d_2
- */ -@Component -@RequiredArgsConstructor -public class WeeklyParser implements ArkLevelParser { - private final ArkGameDataService dataService; - - @Override - public boolean supportType(ArkLevelType type) { - return ArkLevelType.WEEKLY.equals(type); - } - - @Override - public ArkLevel parseLevel(ArkLevel level, ArkTilePos tilePos) { - level.setCatOne(ArkLevelType.WEEKLY.getDisplay()); - - ArkZone zone = dataService.findZone(level.getLevelId(), tilePos.getCode(), tilePos.getStageId()); - if (zone == null) { - return null; - } - - level.setCatTwo(zone.getZoneNameSecond()); - return level; - } -} diff --git a/src/main/java/plus/maa/backend/service/session/UserSession.java b/src/main/java/plus/maa/backend/service/session/UserSession.java deleted file mode 100644 index 0dd09d90..00000000 --- a/src/main/java/plus/maa/backend/service/session/UserSession.java +++ /dev/null @@ -1,4 +0,0 @@ -package plus.maa.backend.service.session; - -public class UserSession { -} diff --git a/src/main/java/plus/maa/backend/service/session/UserSessionService.java b/src/main/java/plus/maa/backend/service/session/UserSessionService.java deleted file mode 100644 index f2d0b464..00000000 --- a/src/main/java/plus/maa/backend/service/session/UserSessionService.java +++ /dev/null @@ -1,54 +0,0 @@ -package plus.maa.backend.service.session; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.springframework.stereotype.Service; -import plus.maa.backend.config.external.MaaCopilotProperties; -import plus.maa.backend.repository.RedisCache; - -import java.util.function.Consumer; - -@Service -public class UserSessionService { - - private static final String REDIS_USER_SESSION_PREFIX = "USER_SESSION_"; - - private static String buildUserCacheKey(String userId) { - return REDIS_USER_SESSION_PREFIX + userId; - } - - private final RedisCache cache; - private final long sessionExpiration; - - public UserSessionService(RedisCache cache, MaaCopilotProperties properties) { - this.cache = cache; - sessionExpiration = properties.getJwt().getRefreshExpire(); - } - - @Nullable - public UserSession getSession(String id) { - return cache.getCache(buildUserCacheKey(id), UserSession.class, null, sessionExpiration); - } - - public void setSession(@NotNull String id, @NotNull UserSession session) { - cache.setCache(buildUserCacheKey(id), session, sessionExpiration); - } - - public void setSession(@NotNull String id,@NotNull Consumer consumer){ - var session = new UserSession(); - consumer.accept(session); - cache.setCache(buildUserCacheKey(id), session, sessionExpiration); - } - - public void updateSessionIfPresent(@NotNull String id, @NotNull Consumer consumer) { - cache.updateCache(id, UserSession.class, null, (session) -> { - if (session != null) consumer.accept(session); - return session; - }, sessionExpiration); - } - - public void removeSession(String id) { - cache.removeCache(buildUserCacheKey(id)); - } - -} diff --git a/src/main/java/plus/maa/backend/task/ArkLevelSyncTask.java b/src/main/java/plus/maa/backend/task/ArkLevelSyncTask.java deleted file mode 100644 index a8c460b8..00000000 --- a/src/main/java/plus/maa/backend/task/ArkLevelSyncTask.java +++ /dev/null @@ -1,24 +0,0 @@ -package plus.maa.backend.task; - -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -import lombok.RequiredArgsConstructor; -import plus.maa.backend.service.ArkLevelService; - -@Component -@RequiredArgsConstructor -public class ArkLevelSyncTask { - - private final ArkLevelService arkLevelService; - - /** - * 地图数据同步定时任务,每10分钟执行一次 - * 应用启动时自动同步一次 - */ - @Scheduled(cron = "${maa-copilot.task-cron.ark-level:-}") - public void syncArkLevels() { - arkLevelService.runSyncLevelDataTask(); - } - -} diff --git a/src/main/java/plus/maa/backend/task/CopilotBackupTask.java b/src/main/java/plus/maa/backend/task/CopilotBackupTask.java deleted file mode 100644 index 19da3794..00000000 --- a/src/main/java/plus/maa/backend/task/CopilotBackupTask.java +++ /dev/null @@ -1,184 +0,0 @@ -package plus.maa.backend.task; - -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.Status; -import org.eclipse.jgit.api.TransportConfigCallback; -import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.lib.PersonIdent; -import org.eclipse.jgit.transport.SshTransport; -import org.eclipse.jgit.transport.sshd.SshdSessionFactoryBuilder; -import org.eclipse.jgit.util.FS; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; -import plus.maa.backend.config.external.CopilotBackup; -import plus.maa.backend.config.external.MaaCopilotProperties; -import plus.maa.backend.controller.response.copilot.ArkLevelInfo; -import plus.maa.backend.repository.CopilotRepository; -import plus.maa.backend.repository.entity.Copilot; -import plus.maa.backend.service.ArkLevelService; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.LocalDate; -import java.util.List; -import java.util.Objects; -import java.util.stream.Stream; - -/** - * CopilotBackupTask - */ -@Slf4j -@Component -@RequiredArgsConstructor -public class CopilotBackupTask { - - private final MaaCopilotProperties config; - - private final CopilotRepository copilotRepository; - - private final ArkLevelService levelService; - - private Git git; - private static final File DEFAULT_SSH_DIR = new File(FS.DETECTED.userHome(), "/.ssh"); - - private static final TransportConfigCallback sshCallback = transport -> { - if (transport instanceof SshTransport sshTransport) { - sshTransport.setSshSessionFactory(new SshdSessionFactoryBuilder() - .setPreferredAuthentications("publickey") - .setHomeDirectory(FS.DETECTED.userHome()) - .setSshDirectory(DEFAULT_SSH_DIR) - .build(null)); - } - }; - - /** - * 初始化Git对象,如果目录已经存在且存在文件,则直接当作git仓库,如果不存在则clone仓库 - */ - @PostConstruct - public void initGit() { - CopilotBackup backup = config.getBackup(); - if (backup.isDisabled()) { - return; - } - File repoDir = new File(backup.getDir()); - if (repoDir.mkdirs()) { - log.info("directory not exist, created: {}", backup.getDir()); - } else { - log.info("directory already exists, dir: {}", backup.getDir()); - } - if (!repoDir.isDirectory()) { - return; - } - try (Stream fileList = Files.list(repoDir.toPath())) { - if (fileList.findFirst().isEmpty()) { - // 不存在文件则初始化 - git = Git.cloneRepository() - .setURI(backup.getUri()) - .setDirectory(repoDir) - .setTransportConfigCallback(sshCallback) - .call(); - } else { - git = Git.open(repoDir); - } - } catch (IOException | GitAPIException e) { - log.error("init copilot backup repo failed, repoDir: {}", repoDir, e); - } - } - - /** - * copilot数据同步定时任务,每天执行一次 - */ - @Scheduled(cron = "${maa-copilot.task-cron.copilot-update:-}") - public void backupCopilots() { - if (config.getBackup().isDisabled() || Objects.isNull(git)) { - return; - } - try { - git.pull().call(); - } catch (GitAPIException e) { - log.error("git pull execute failed, msg: {}", e.getMessage(), e); - } - - File baseDirectory = git.getRepository().getWorkTree(); - List copilots = copilotRepository.findAll(); - copilots.forEach(copilot -> { - ArkLevelInfo level = levelService.findByLevelIdFuzzy(copilot.getStageName()); - if (Objects.isNull(level)) { - return; - } - // 暂时使用 copilotId 作为文件名 - File filePath = new File(String.join(File.separator, baseDirectory.getPath(), level.getCatOne(), - level.getCatTwo(), level.getCatThree(), copilot.getCopilotId() + ".json")); - String content = copilot.getContent(); - if (Objects.isNull(content)) { - return; - } - if (copilot.isDelete()) { - // 删除文件 - deleteCopilot(filePath); - } else { - // 创建或者修改文件 - upsertCopilot(filePath, content); - } - }); - - doCommitAndPush(); - } - - private void upsertCopilot(File file, String content) { - if (!file.exists()) { - if (!file.getParentFile().mkdirs()) { - log.warn("folder may exists, mkdir failed"); - } - } - try { - Files.writeString(file.toPath(), content); - } catch (IOException e) { - log.error("write file failed, path: {}, message: {}", file.getPath(), e.getMessage(), e); - } - } - - private void deleteCopilot(File file) { - if (file.exists()) { - if (file.delete()) { - log.info("delete copilot file: {}", file.getPath()); - } else { - log.error("delete copilot failed, file: {}", file.getPath()); - } - } else { - log.info("file does not exists, no need to delete"); - } - } - - private void doCommitAndPush() { - try { - Status status = git.status().call(); - if (status.getAdded().isEmpty() && - status.getChanged().isEmpty() && - status.getRemoved().isEmpty() && - status.getUntracked().isEmpty() && - status.getModified().isEmpty() && - status.getAdded().isEmpty()) { - log.info("copilot backup with no new added or changes"); - return; - } - git.add().addFilepattern(".").call(); - CopilotBackup backup = config.getBackup(); - PersonIdent committer = new PersonIdent(backup.getUsername(), backup.getEmail()); - git.commit().setCommitter(committer) - .setMessage(LocalDate.now().toString()) - .call(); - git.push() - .setTransportConfigCallback(sshCallback) - .call(); - } catch (GitAPIException e) { - log.error("git committing failed, msg: {}", e.getMessage(), e); - } - } - -} diff --git a/src/main/java/plus/maa/backend/task/CopilotScoreRefreshTask.java b/src/main/java/plus/maa/backend/task/CopilotScoreRefreshTask.java deleted file mode 100644 index 56d46fff..00000000 --- a/src/main/java/plus/maa/backend/task/CopilotScoreRefreshTask.java +++ /dev/null @@ -1,129 +0,0 @@ -package plus.maa.backend.task; - -import lombok.AccessLevel; -import lombok.RequiredArgsConstructor; -import lombok.experimental.FieldDefaults; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.aggregation.Aggregation; -import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; -import plus.maa.backend.repository.CopilotRepository; -import plus.maa.backend.repository.RedisCache; -import plus.maa.backend.repository.entity.Copilot; -import plus.maa.backend.repository.entity.Rating; -import plus.maa.backend.service.CopilotService; -import plus.maa.backend.service.model.RatingCount; -import plus.maa.backend.service.model.RatingType; - -import java.time.LocalDateTime; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * 作业热度值刷入任务,每日执行,用于计算基于时间的热度值 - * - * @author dove - * created on 2023.05.03 - */ -@Component -@RequiredArgsConstructor -@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) -public class CopilotScoreRefreshTask { - - RedisCache redisCache; - CopilotRepository copilotRepository; - MongoTemplate mongoTemplate; - - /** - * 热度值刷入任务,每日三点执行 - */ - @Scheduled(cron = "0 0 3 * * ?") - public void refreshHotScores() { - // 分页获取所有未删除的作业 - Pageable pageable = Pageable.ofSize(1000); - Page copilots = copilotRepository.findAllByDeleteIsFalse(pageable); - - // 循环读取直到没有未删除的作业为止 - while (copilots.hasContent()) { - List copilotIdSTRs = copilots.stream() - .map(copilot -> Long.toString(copilot.getCopilotId())) - .collect(Collectors.toList()); - refresh(copilotIdSTRs, copilots); - // 获取下一页 - if (!copilots.hasNext()) { - // 没有下一页了,跳出循环 - break; - } - pageable = copilots.nextPageable(); - copilots = copilotRepository.findAllByDeleteIsFalse(pageable); - } - - // 移除首页热度缓存 - redisCache.removeCacheByPattern("home:hot:*"); - } - - /** - * 刷入评分变更数 Top 100 的热度值,每日八点到二十点每三小时执行一次 - */ - @Scheduled(cron = "0 0 8-20/3 * * ?") - public void refreshTop100HotScores() { - Set copilotIdSTRs = redisCache.getZSetReverse("rate:hot:copilotIds", 0, 99); - if (copilotIdSTRs == null || copilotIdSTRs.isEmpty()) { - return; - } - - List copilots = copilotRepository.findByCopilotIdInAndDeleteIsFalse( - copilotIdSTRs.stream().map(Long::parseLong).collect(Collectors.toList()) - ); - if (copilots == null || copilots.isEmpty()) { - return; - } - - refresh(copilotIdSTRs, copilots); - - // 移除近期评分变化量缓存 - redisCache.removeCacheByPattern("rate:hot:copilotIds"); - // 移除首页热度缓存 - redisCache.removeCacheByPattern("home:hot:*"); - } - - private void refresh(Collection copilotIdSTRs, Iterable copilots) { - // 批量获取最近七天的点赞和点踩数量 - LocalDateTime now = LocalDateTime.now(); - List likeCounts = counts(copilotIdSTRs, RatingType.LIKE, now.minusDays(7)); - List dislikeCounts = counts(copilotIdSTRs, RatingType.DISLIKE, now.minusDays(7)); - Map likeCountMap = likeCounts.stream().collect(Collectors.toMap(RatingCount::getKey, RatingCount::getCount)); - Map dislikeCountMap = dislikeCounts.stream().collect(Collectors.toMap(RatingCount::getKey, RatingCount::getCount)); - // 计算热度值 - for (Copilot copilot : copilots) { - long likeCount = likeCountMap.getOrDefault(Long.toString(copilot.getCopilotId()), 1L); - long dislikeCount = dislikeCountMap.getOrDefault(Long.toString(copilot.getCopilotId()), 0L); - double hotScore = CopilotService.getHotScore(copilot, likeCount, dislikeCount); - copilot.setHotScore(hotScore); - } - // 批量更新热度值 - copilotRepository.saveAll(copilots); - } - - private List counts(Collection keys, RatingType rating, LocalDateTime startTime) { - Aggregation aggregation = Aggregation.newAggregation( - Aggregation.match(Criteria - .where("type").is(Rating.KeyType.COPILOT) - .and("key").in(keys) - .and("rating").is(rating) - .and("rateTime").gte(startTime) - ), - Aggregation.group("key").count().as("count") - .first("key").as("key"), - Aggregation.project("key", "count") - ).withOptions(Aggregation.newAggregationOptions().allowDiskUse(true).build()); // 放弃内存优化,使用磁盘优化,免得内存炸了 - return mongoTemplate.aggregate(aggregation, Rating.class, RatingCount.class).getMappedResults(); - } - -} diff --git a/src/main/kotlin/plus/maa/backend/MainApplication.kt b/src/main/kotlin/plus/maa/backend/MainApplication.kt new file mode 100644 index 00000000..4c5d416b --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/MainApplication.kt @@ -0,0 +1,21 @@ +package plus.maa.backend + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.ConfigurationPropertiesScan +import org.springframework.boot.runApplication +import org.springframework.cache.annotation.EnableCaching +import org.springframework.scheduling.annotation.EnableAsync +import org.springframework.scheduling.annotation.EnableScheduling +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity + +@EnableAsync +@EnableCaching +@EnableScheduling +@SpringBootApplication +@ConfigurationPropertiesScan +@EnableMethodSecurity +class MainApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/src/main/java/plus/maa/backend/common/MaaStatusCode.java b/src/main/kotlin/plus/maa/backend/common/MaaStatusCode.kt similarity index 65% rename from src/main/java/plus/maa/backend/common/MaaStatusCode.java rename to src/main/kotlin/plus/maa/backend/common/MaaStatusCode.kt index 58e1cdbd..4e86edb8 100644 --- a/src/main/java/plus/maa/backend/common/MaaStatusCode.java +++ b/src/main/kotlin/plus/maa/backend/common/MaaStatusCode.kt @@ -1,12 +1,9 @@ -package plus.maa.backend.common; - -import lombok.AllArgsConstructor; +package plus.maa.backend.common /** * @author AnselYuki */ -@AllArgsConstructor -public enum MaaStatusCode { +enum class MaaStatusCode(val code:Int,val message:String) { /** * MAA自定义状态码 */ @@ -15,8 +12,5 @@ public enum MaaStatusCode { MAA_USER_NOT_ENABLED(10003, "用户未启用"), MAA_USER_EXISTS(10004, "用户已存在"), MAA_REGISTRATION_CODE_NOT_FOUND(10011, "注册验证码错误"), - ; - public final int code; - public final String message; } diff --git a/src/main/kotlin/plus/maa/backend/common/annotation/JsonSchema.kt b/src/main/kotlin/plus/maa/backend/common/annotation/JsonSchema.kt new file mode 100644 index 00000000..32826528 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/common/annotation/JsonSchema.kt @@ -0,0 +1,9 @@ +package plus.maa.backend.common.annotation + +/** + * @author LoMu + * Date 2023-01-22 17:49 + */ +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) +@Retention(AnnotationRetention.RUNTIME) +annotation class JsonSchema diff --git a/src/main/kotlin/plus/maa/backend/common/annotation/SensitiveWordDetection.kt b/src/main/kotlin/plus/maa/backend/common/annotation/SensitiveWordDetection.kt new file mode 100644 index 00000000..6f8e22e0 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/common/annotation/SensitiveWordDetection.kt @@ -0,0 +1,19 @@ +package plus.maa.backend.common.annotation + +/** + * 敏感词检测注解

+ * 用于方法上,标注该方法需要进行敏感词检测

+ * 通过 SpEL 表达式获取方法参数 + * + * @author lixuhuilll + * Date: 2023-08-25 18:50 + */ +@MustBeDocumented +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) +@Retention(AnnotationRetention.RUNTIME) +annotation class SensitiveWordDetection( + /** + * SpEL 表达式 + */ + vararg val value: String = [] +) diff --git a/src/main/kotlin/plus/maa/backend/common/aop/JsonSchemaAop.kt b/src/main/kotlin/plus/maa/backend/common/aop/JsonSchemaAop.kt new file mode 100644 index 00000000..8782a51c --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/common/aop/JsonSchemaAop.kt @@ -0,0 +1,87 @@ +package plus.maa.backend.common.aop + +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.ObjectMapper +import io.github.oshai.kotlinlogging.KotlinLogging +import org.aspectj.lang.JoinPoint +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Before +import org.aspectj.lang.annotation.Pointcut +import org.everit.json.schema.ValidationException +import org.everit.json.schema.loader.SchemaLoader +import org.json.JSONObject +import org.json.JSONTokener +import org.springframework.core.io.ClassPathResource +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Component +import plus.maa.backend.common.annotation.JsonSchema +import plus.maa.backend.controller.request.comments.CommentsRatingDTO +import plus.maa.backend.controller.request.copilot.CopilotCUDRequest +import plus.maa.backend.controller.request.copilot.CopilotRatingReq +import plus.maa.backend.controller.response.MaaResultException +import java.io.IOException + +private val log = KotlinLogging.logger { } + +/** + * @author LoMu + * Date 2023-01-22 17:53 + */ +@Component +@Aspect +class JsonSchemaAop( + private val mapper: ObjectMapper +) { + @Pointcut("@annotation(plus.maa.backend.common.annotation.JsonSchema)") + fun pt() { + } + + /** + * 数据校验 + * + * @param joinPoint 形参 + * @param jsonSchema 注解 + */ + @Before("pt() && @annotation(jsonSchema)") + fun before(joinPoint: JoinPoint, jsonSchema: JsonSchema?) { + var schemaJson: String? = null + var content: String? = null + //判断是验证的是Copilot还是Rating + for (arg in joinPoint.args) { + if (arg is CopilotCUDRequest) { + content = arg.content + schemaJson = COPILOT_SCHEMA_JSON + } + if (arg is CopilotRatingReq || arg is CommentsRatingDTO) { + try { + schemaJson = RATING_SCHEMA_JSON + content = mapper.writeValueAsString(arg) + } catch (e: JsonProcessingException) { + log.error(e) { "json解析失败" } + } + } + } + if (content == null) return + + + //获取json schema json路径并验证 + try { + ClassPathResource(schemaJson!!).inputStream.use { inputStream -> + val json = JSONObject(content) + val jsonObject = JSONObject(JSONTokener(inputStream)) + val schema = SchemaLoader.load(jsonObject) + schema.validate(json) + } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (e: ValidationException) { + log.warn { "schema Location: ${e.violatedSchema.schemaLocation}" } + throw MaaResultException(HttpStatus.BAD_REQUEST.value(), "数据不符合规范,请前往前端作业编辑器进行操作") + } + } + + companion object { + private const val COPILOT_SCHEMA_JSON = "static/templates/maa-copilot-schema.json" + private const val RATING_SCHEMA_JSON = "static/templates/maa-rating-schema.json" + } +} \ No newline at end of file diff --git a/src/main/kotlin/plus/maa/backend/common/aop/SensitiveWordAop.kt b/src/main/kotlin/plus/maa/backend/common/aop/SensitiveWordAop.kt new file mode 100644 index 00000000..e95eed9d --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/common/aop/SensitiveWordAop.kt @@ -0,0 +1,76 @@ +package plus.maa.backend.common.aop + +import cn.hutool.dfa.WordTree +import com.fasterxml.jackson.databind.ObjectMapper +import org.aspectj.lang.JoinPoint +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Before +import org.aspectj.lang.reflect.MethodSignature +import org.springframework.core.DefaultParameterNameDiscoverer +import org.springframework.expression.EvaluationContext +import org.springframework.expression.spel.standard.SpelExpressionParser +import org.springframework.expression.spel.support.StandardEvaluationContext +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Component +import plus.maa.backend.common.annotation.SensitiveWordDetection +import plus.maa.backend.controller.response.MaaResultException + +/** + * 敏感词处理程序

+ * + * @author lixuhuilll + * Date: 2023-08-25 18:50 + */ +@Aspect +@Component +class SensitiveWordAop( + // 敏感词库 + private val wordTree: WordTree, + private val objectMapper: ObjectMapper +) { + + // SpEL 表达式解析器 + private val parser = SpelExpressionParser() + + // 用于获取方法参数名 + private val nameDiscoverer = DefaultParameterNameDiscoverer() + + fun getObjectBySpEL(spELString: String, joinPoint: JoinPoint): Any? { + // 获取被注解方法 + val signature = joinPoint.signature as? MethodSignature ?: return null + // 获取方法参数名数组 + val paramNames = nameDiscoverer.getParameterNames(signature.method) + // 解析 Spring 表达式对象 + val expression = parser.parseExpression(spELString) + // Spring 表达式上下文对象 + val context: EvaluationContext = StandardEvaluationContext() + // 通过 joinPoint 获取被注解方法的参数 + val args = joinPoint.args + // 给上下文赋值 + for (i in args.indices) { + if (paramNames != null) { + context.setVariable(paramNames[i], args[i]) + } + } + context.setVariable("objectMapper", objectMapper) + // 表达式从上下文中计算出实际参数值 + return expression.getValue(context) + } + + @Before("@annotation(annotation)") // 处理 SensitiveWordDetection 注解 + fun before(joinPoint: JoinPoint, annotation: SensitiveWordDetection) { + // 获取 SpEL 表达式 + val expressions = annotation.value + for (expression in expressions) { + // 解析 SpEL 表达式 + val value = getObjectBySpEL(expression, joinPoint) + // 校验 + if (value is String) { + val matchAll = wordTree.matchAll(value) + if (matchAll != null && matchAll.isNotEmpty()) { + throw MaaResultException(HttpStatus.BAD_REQUEST.value(), "包含敏感词:$matchAll") + } + } + } + } +} diff --git a/src/main/kotlin/plus/maa/backend/common/model/CopilotSetType.kt b/src/main/kotlin/plus/maa/backend/common/model/CopilotSetType.kt new file mode 100644 index 00000000..d9606283 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/common/model/CopilotSetType.kt @@ -0,0 +1,20 @@ +package plus.maa.backend.common.model + +import org.springframework.util.Assert + +/** + * @author dragove + * create on 2024-01-01 + */ +interface CopilotSetType { + val copilotIds: MutableList + + fun distinctIdsAndCheck(): MutableList { + if (copilotIds.isEmpty() || copilotIds.size == 1) { + return this.copilotIds + } + val copilotIds = copilotIds.stream().distinct().toList() + Assert.state(copilotIds.size <= 1000, "作业集总作业数量不能超过1000条") + return copilotIds + } +} diff --git a/src/main/kotlin/plus/maa/backend/common/utils/ArkLevelUtil.kt b/src/main/kotlin/plus/maa/backend/common/utils/ArkLevelUtil.kt new file mode 100644 index 00000000..6c52bfb5 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/common/utils/ArkLevelUtil.kt @@ -0,0 +1,24 @@ +package plus.maa.backend.common.utils + +import java.util.regex.Pattern + +object ArkLevelUtil { + private val NOT_KEY_INFO: Pattern = Pattern.compile( // level_、各种难度前缀、season_、前导零、以-或者_划分的后缀 + "^level_|^easy_|^hard_|^tough_|^main_|season_|(?
+ * 例如:a1(骑兵与猎人)、act11d0(沃伦姆德的薄暮)、act11mini(未尽篇章)、crisis_v2_1(浊燃作战) + */ + fun getKeyInfoById(id: String?): String { + if (id == null) { + return "" + } + // 去除所有非关键信息 + return NOT_KEY_INFO.matcher(id).replaceAll("") + } +} diff --git a/src/main/kotlin/plus/maa/backend/common/utils/FreeMarkerUtils.kt b/src/main/kotlin/plus/maa/backend/common/utils/FreeMarkerUtils.kt new file mode 100644 index 00000000..8ac47eb0 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/common/utils/FreeMarkerUtils.kt @@ -0,0 +1,35 @@ +package plus.maa.backend.common.utils + +import freemarker.template.Configuration +import freemarker.template.TemplateException +import plus.maa.backend.controller.response.MaaResultException +import java.io.IOException +import java.io.StringWriter +import java.nio.charset.StandardCharsets +import java.util.* + +/** + * @author dragove + * created on 2023/1/17 + */ +object FreeMarkerUtils { + private val cfg = Configuration(Configuration.VERSION_2_3_32) + + init { + cfg.setClassForTemplateLoading(FreeMarkerUtils::class.java, "/static/templates/ftlh") + cfg.setEncoding(Locale.CHINA, StandardCharsets.UTF_8.name()) + } + + fun parseData(templateName: String, dataModel: Any?): String { + try { + val template = cfg.getTemplate(templateName) + val sw = StringWriter() + template.process(dataModel, sw) + return sw.toString() + } catch (e: IOException) { + throw MaaResultException("获取freemarker模板失败") + } catch (e: TemplateException) { + throw MaaResultException("freemarker模板处理失败") + } + } +} diff --git a/src/main/kotlin/plus/maa/backend/common/utils/IdComponent.kt b/src/main/kotlin/plus/maa/backend/common/utils/IdComponent.kt new file mode 100644 index 00000000..55b0e8f4 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/common/utils/IdComponent.kt @@ -0,0 +1,51 @@ +package plus.maa.backend.common.utils + +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.data.domain.Sort +import org.springframework.data.mongodb.core.MongoTemplate +import org.springframework.data.mongodb.core.query.Query +import org.springframework.stereotype.Component +import plus.maa.backend.repository.entity.CollectionMeta +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong + +private val log = KotlinLogging.logger { } + +@Component +class IdComponent( + private val mongoTemplate: MongoTemplate +) { + private val currentIdMap: MutableMap = ConcurrentHashMap() + + /** + * 获取id数据 + * @param meta 集合元数据 + * @return 新的id + */ + fun getId(meta: CollectionMeta): Long { + val cls = meta.entityClass + val collectionName = mongoTemplate.getCollectionName(cls) + val v = currentIdMap[collectionName] + if (v == null) { + synchronized(cls) { + val rv = currentIdMap[collectionName] + if (rv == null) { + val nv = AtomicLong(getMax(cls, meta.idGetter, meta.incIdField)) + log.info { "初始化获取 collection: $collectionName 的最大 id,id: ${nv.get()}" } + currentIdMap[collectionName] = nv + return nv.incrementAndGet() + } + return rv.incrementAndGet() + } + } + return v.incrementAndGet() + } + + private fun getMax(entityClass: Class, idGetter: (T) -> Long, fieldName: String) = + mongoTemplate.findOne( + Query().with(Sort.by(fieldName).descending()).limit(1), + entityClass + ) + ?.let(idGetter) + ?: 20000L +} diff --git a/src/main/kotlin/plus/maa/backend/common/utils/IpUtil.kt b/src/main/kotlin/plus/maa/backend/common/utils/IpUtil.kt new file mode 100644 index 00000000..d9384dc8 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/common/utils/IpUtil.kt @@ -0,0 +1,46 @@ +package plus.maa.backend.common.utils + +import jakarta.servlet.http.HttpServletRequest +import java.net.InetAddress +import java.net.UnknownHostException + +/** + * @Author leaves + * @Date 2023/1/20 14:33 + */ +object IpUtil { + /** + * 获取登录用户IP地址 + * + * @param request + * @return + */ + fun getIpAddr(request: HttpServletRequest): String { + var ip = request.getHeader("x-forwarded-for") + if (ip.isNullOrEmpty() || "unknown".equals(ip, ignoreCase = true)) { + ip = request.getHeader("Proxy-Client-IP") + } + if (ip.isNullOrEmpty()|| "unknown".equals(ip, ignoreCase = true)) { + ip = request.getHeader("WL-Proxy-Client-IP") + } + if (ip.isNullOrEmpty()|| "unknown".equals(ip, ignoreCase = true)) { + ip = request.remoteAddr + if (ip == "127.0.0.1") { + //根据网卡取本机配置的IP + val inet: InetAddress? + try { + inet = InetAddress.getLocalHost() + ip = inet.hostAddress + } catch (ignored: UnknownHostException) { + } + } + } + // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 + if (ip != null && ip.length > 15) { + if (ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")) + } + } + return ip + } +} diff --git a/src/main/kotlin/plus/maa/backend/common/utils/OkHttpUtils.kt b/src/main/kotlin/plus/maa/backend/common/utils/OkHttpUtils.kt new file mode 100644 index 00000000..225e2627 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/common/utils/OkHttpUtils.kt @@ -0,0 +1,27 @@ +package plus.maa.backend.common.utils + +import okhttp3.ConnectionPool +import okhttp3.OkHttpClient +import org.springframework.context.annotation.Bean +import org.springframework.stereotype.Component +import java.util.concurrent.TimeUnit + +/** + * @author john180 + */ +@Component +class OkHttpUtils { + /** + * 缺省 OkHttpClient + * + * @return OkHttpClient + */ + @Bean + fun defaultOkHttpClient(): OkHttpClient { + return OkHttpClient().newBuilder() + .connectTimeout(5, TimeUnit.SECONDS) + .readTimeout(5, TimeUnit.SECONDS) + .connectionPool(ConnectionPool(10, 5, TimeUnit.MINUTES)) + .build() + } +} diff --git a/src/main/kotlin/plus/maa/backend/common/utils/WebUtils.kt b/src/main/kotlin/plus/maa/backend/common/utils/WebUtils.kt new file mode 100644 index 00000000..9b818b9c --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/common/utils/WebUtils.kt @@ -0,0 +1,20 @@ +package plus.maa.backend.common.utils + +import jakarta.servlet.http.HttpServletResponse +import java.io.IOException + +/** + * @author AnselYuki + */ +object WebUtils { + fun renderString(response: HttpServletResponse, json: String?, code: Int) { + try { + response.status = code + response.contentType = "application/json" + response.characterEncoding = "UTF-8" + response.writer.println(json) + } catch (e: IOException) { + e.printStackTrace() + } + } +} diff --git a/src/main/kotlin/plus/maa/backend/common/utils/converter/ArkLevelConverter.kt b/src/main/kotlin/plus/maa/backend/common/utils/converter/ArkLevelConverter.kt new file mode 100644 index 00000000..ace69dd6 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/common/utils/converter/ArkLevelConverter.kt @@ -0,0 +1,16 @@ +package plus.maa.backend.common.utils.converter + +import org.mapstruct.Mapper +import plus.maa.backend.controller.response.copilot.ArkLevelInfo +import plus.maa.backend.repository.entity.ArkLevel + +/** + * @author dragove + * created on 2022/12/26 + */ +@Mapper(componentModel = "spring") +interface ArkLevelConverter { + fun convert(arkLevel: ArkLevel): ArkLevelInfo + + fun convert(arkLevel: List): List +} diff --git a/src/main/kotlin/plus/maa/backend/common/utils/converter/CommentConverter.kt b/src/main/kotlin/plus/maa/backend/common/utils/converter/CommentConverter.kt new file mode 100644 index 00000000..7cd7710c --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/common/utils/converter/CommentConverter.kt @@ -0,0 +1,29 @@ +package plus.maa.backend.common.utils.converter + +import org.mapstruct.Mapper +import org.mapstruct.Mapping +import plus.maa.backend.controller.response.comments.CommentsInfo +import plus.maa.backend.controller.response.comments.SubCommentsInfo +import plus.maa.backend.repository.entity.CommentsArea +import plus.maa.backend.repository.entity.MaaUser + +/** + * @author LoMu + * Date 2023-02-21 18:16 + */ +@Mapper(componentModel = "spring") +interface CommentConverter { + @Mapping(target = "like", source = "commentsArea.likeCount") + @Mapping(target = "dislike", source = "commentsArea.dislikeCount") + @Mapping(target = "uploader", source = "maaUser.userName") + @Mapping(target = "commentId", source = "commentsArea.id") + fun toCommentsInfo(commentsArea: CommentsArea, maaUser: MaaUser, subCommentsInfos: List): CommentsInfo + + + @Mapping(target = "like", source = "commentsArea.likeCount") + @Mapping(target = "dislike", source = "commentsArea.dislikeCount") + @Mapping(target = "uploader", source = "maaUser.userName") + @Mapping(target = "commentId", source = "commentsArea.id") + @Mapping(target = "deleted", source = "commentsArea.delete") + fun toSubCommentsInfo(commentsArea: CommentsArea, maaUser: MaaUser): SubCommentsInfo +} diff --git a/src/main/java/plus/maa/backend/common/utils/converter/CopilotConverter.java b/src/main/kotlin/plus/maa/backend/common/utils/converter/CopilotConverter.kt similarity index 76% rename from src/main/java/plus/maa/backend/common/utils/converter/CopilotConverter.java rename to src/main/kotlin/plus/maa/backend/common/utils/converter/CopilotConverter.kt index c5bcb239..2cdcf4fe 100644 --- a/src/main/java/plus/maa/backend/common/utils/converter/CopilotConverter.java +++ b/src/main/kotlin/plus/maa/backend/common/utils/converter/CopilotConverter.kt @@ -1,21 +1,17 @@ -package plus.maa.backend.common.utils.converter; - -import org.mapstruct.*; -import plus.maa.backend.controller.request.copilot.CopilotDTO; -import plus.maa.backend.controller.response.copilot.CopilotInfo; -import plus.maa.backend.repository.entity.Copilot; - -import java.time.LocalDateTime; +package plus.maa.backend.common.utils.converter +import org.mapstruct.* +import plus.maa.backend.controller.request.copilot.CopilotDTO +import plus.maa.backend.controller.response.copilot.CopilotInfo +import plus.maa.backend.repository.entity.Copilot +import java.time.LocalDateTime /** * @author LoMu * Date 2023-01-10 19:10 */ - @Mapper(componentModel = "spring") -public interface CopilotConverter { - +interface CopilotConverter { /** * 实现增量更新 * 将copilotDto 映射覆盖数据库中的 copilot @@ -38,7 +34,7 @@ public interface CopilotConverter { @Mapping(target = "ratingRatio", ignore = true) @Mapping(target = "ratingLevel", ignore = true) @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) - void updateCopilotFromDto(CopilotDTO copilotDTO, String content, @MappingTarget Copilot copilot); + fun updateCopilotFromDto(copilotDTO: CopilotDTO, content: String, @MappingTarget copilot: Copilot) @Mapping(target = "id", ignore = true) @Mapping(target = "deleteTime", ignore = true) @@ -52,7 +48,13 @@ public interface CopilotConverter { @Mapping(target = "uploadTime", source = "now") @Mapping(target = "firstUploadTime", source = "now") @Mapping(target = "uploaderId", source = "userId") - Copilot toCopilot(CopilotDTO copilotDto, String userId, LocalDateTime now, Long copilotId, String content); + fun toCopilot( + copilotDto: CopilotDTO, + copilotId: Long, + userId: String, + now: LocalDateTime, + content: String? + ): Copilot @Mapping(target = "ratingType", ignore = true) @Mapping(target = "ratingRatio", ignore = true) @@ -64,5 +66,5 @@ public interface CopilotConverter { @Mapping(target = "like", source = "copilot.likeCount") @Mapping(target = "dislike", source = "copilot.dislikeCount") @Mapping(target = "commentsCount", conditionExpression = "java(commentsCount != null)") - CopilotInfo toCopilotInfo(Copilot copilot, String userName, Long copilotId, Long commentsCount); + fun toCopilotInfo(copilot: Copilot, userName: String, copilotId: Long, commentsCount: Long?): CopilotInfo } diff --git a/src/main/kotlin/plus/maa/backend/common/utils/converter/CopilotSetConverter.kt b/src/main/kotlin/plus/maa/backend/common/utils/converter/CopilotSetConverter.kt new file mode 100644 index 00000000..6261ae97 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/common/utils/converter/CopilotSetConverter.kt @@ -0,0 +1,30 @@ +package plus.maa.backend.common.utils.converter + +import org.mapstruct.Mapper +import org.mapstruct.Mapping +import plus.maa.backend.controller.request.copilotset.CopilotSetCreateReq +import plus.maa.backend.controller.response.copilotset.CopilotSetRes +import plus.maa.backend.controller.response.copilotset.CopilotSetListRes +import plus.maa.backend.repository.entity.CopilotSet +import java.time.LocalDateTime + +/** + * @author dragove + * create on 2024-01-01 + */ +@Mapper( + componentModel = "spring", imports = [LocalDateTime::class] +) +interface CopilotSetConverter { + @Mapping(target = "delete", ignore = true) + @Mapping(target = "deleteTime", ignore = true) + @Mapping(target = "copilotIds", expression = "java(createReq.distinctIdsAndCheck())") + @Mapping(target = "createTime", expression = "java(LocalDateTime.now())") + @Mapping(target = "updateTime", expression = "java(LocalDateTime.now())") + fun convert(createReq: CopilotSetCreateReq, id: Long, creatorId: String): CopilotSet + + @Mapping(target = "creator", ignore = true) + fun convert(copilotSet: CopilotSet, creator: String): CopilotSetListRes + + fun convertDetail(copilotSet: CopilotSet, creator: String): CopilotSetRes +} diff --git a/src/main/kotlin/plus/maa/backend/config/CorsConfig.kt b/src/main/kotlin/plus/maa/backend/config/CorsConfig.kt new file mode 100644 index 00000000..5f3a60eb --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/CorsConfig.kt @@ -0,0 +1,22 @@ +package plus.maa.backend.config + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.CorsRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +/** + * @author AnselYuki + */ +@Configuration +class CorsConfig : WebMvcConfigurer { + override fun addCorsMappings(registry: CorsRegistry) { + // 设置允许跨域的路径 + registry + .addMapping("/**") // 设置允许跨域请求的域名 + .allowedOriginPatterns("*") // 是否允许cookie + .allowCredentials(true) // 设置允许的请求方式 + .allowedMethods("GET", "POST", "DELETE", "PUT") // 设置允许的header属性 + .allowedHeaders("*") // 跨域允许时间 + .maxAge(3600) + } +} diff --git a/src/main/kotlin/plus/maa/backend/config/HttpInterfaceConfig.kt b/src/main/kotlin/plus/maa/backend/config/HttpInterfaceConfig.kt new file mode 100644 index 00000000..9f22afdc --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/HttpInterfaceConfig.kt @@ -0,0 +1,48 @@ +package plus.maa.backend.config + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpHeaders +import org.springframework.http.codec.ClientCodecConfigurer +import org.springframework.http.codec.json.Jackson2JsonDecoder +import org.springframework.http.codec.json.Jackson2JsonEncoder +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder +import org.springframework.web.reactive.function.client.ExchangeStrategies +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.support.WebClientAdapter +import org.springframework.web.service.invoker.HttpServiceProxyFactory +import plus.maa.backend.repository.GithubRepository + +@Configuration +class HttpInterfaceConfig { + @Bean + fun githubRepository(): GithubRepository { + val mapper = Jackson2ObjectMapperBuilder.json() + .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) + .build() + + val client = WebClient.builder() + .baseUrl("https://api.github.com") + .exchangeStrategies(ExchangeStrategies + .builder() + .codecs { codecs: ClientCodecConfigurer -> + codecs.defaultCodecs() + .jackson2JsonEncoder(Jackson2JsonEncoder(mapper)) + codecs.defaultCodecs() + .jackson2JsonDecoder(Jackson2JsonDecoder(mapper)) + // 最大 20MB + codecs.defaultCodecs().maxInMemorySize(20 * 1024 * 1024) + } + .build()) + .defaultHeaders { headers: HttpHeaders -> + headers.add("Accept", "application/vnd.github+json") + headers.add("X-GitHub-Api-Version", "2022-11-28") + } + .build() + return HttpServiceProxyFactory.builderFor(WebClientAdapter.create(client)) + .build() + .createClient(GithubRepository::class.java) + } +} diff --git a/src/main/kotlin/plus/maa/backend/config/JacksonConfig.kt b/src/main/kotlin/plus/maa/backend/config/JacksonConfig.kt new file mode 100644 index 00000000..7fa35089 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/JacksonConfig.kt @@ -0,0 +1,34 @@ +package plus.maa.backend.config + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer +import org.springframework.boot.autoconfigure.jackson.JacksonProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder +import java.io.IOException +import java.time.* +import java.time.format.DateTimeFormatter + +@Configuration +class JacksonConfig(private val jacksonProperties: JacksonProperties) { + @Bean + fun jsonCustomizer(): Jackson2ObjectMapperBuilderCustomizer { + return Jackson2ObjectMapperBuilderCustomizer { builder: Jackson2ObjectMapperBuilder -> + val format = jacksonProperties.dateFormat + val timeZone = jacksonProperties.timeZone + val formatter = DateTimeFormatter.ofPattern(format).withZone(timeZone.toZoneId()) + builder.serializers(LocalDateTimeSerializer(formatter)) + } + } + + class LocalDateTimeSerializer(private val formatter: DateTimeFormatter) : + StdSerializer(LocalDateTime::class.java) { + @Throws(IOException::class) + override fun serialize(value: LocalDateTime, gen: JsonGenerator, provider: SerializerProvider) { + gen.writeString(value.atZone(ZoneId.systemDefault()).format(formatter)) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/plus/maa/backend/config/NativeReflectionConfig.kt b/src/main/kotlin/plus/maa/backend/config/NativeReflectionConfig.kt new file mode 100644 index 00000000..1e112e9f --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/NativeReflectionConfig.kt @@ -0,0 +1,33 @@ +package plus.maa.backend.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.PropertyNamingStrategies.LowerCamelCaseStrategy +import org.springframework.aot.hint.annotation.RegisterReflectionForBinding +import org.springframework.context.annotation.Configuration +import plus.maa.backend.controller.request.copilot.CopilotDTO +import plus.maa.backend.repository.entity.gamedata.* +import plus.maa.backend.repository.entity.gamedata.ArkTilePos.Tile +import plus.maa.backend.service.model.RatingCache + +/** + * 添加所有需要用到反射的类到此处,用于 native image + * 等个大佬修缮 + * + * @author dragove + * created on 2023/08/18 + */ +@Configuration +@RegisterReflectionForBinding( + ArkActivity::class, + ArkCharacter::class, + ArkStage::class, + ArkTilePos::class, + Tile::class, + ArkTower::class, + ArkZone::class, + CopilotDTO::class, + RatingCache::class, + PropertyNamingStrategies.SnakeCaseStrategy::class, + LowerCamelCaseStrategy::class +) +class NativeReflectionConfig diff --git a/src/main/kotlin/plus/maa/backend/config/SensitiveWordConfig.kt b/src/main/kotlin/plus/maa/backend/config/SensitiveWordConfig.kt new file mode 100644 index 00000000..36b4d247 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/SensitiveWordConfig.kt @@ -0,0 +1,63 @@ +package plus.maa.backend.config + +import cn.hutool.dfa.WordTree +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStreamReader + +private val log = KotlinLogging.logger { } + +/** + * 敏感词配置类

+ * + * @author lixuhuilll + * Date: 2023-08-25 18:50 + */ +@Configuration +class SensitiveWordConfig( + // 标准的 Spring 路径匹配语法,默认为 classpath:sensitive-word.txt + @Value("\${maa-copilot.sensitive-word.path:classpath:sensitive-word.txt}") + private val sensitiveWordPath: String +) { + + /** + * 敏感词库初始化

+ * 使用 Hutool 的 DFA 算法库,如果后续需要可转其他开源库或者使用付费的敏感词库

+ * + * @return 敏感词库 + */ + @Bean + @Throws(IOException::class) + fun sensitiveWordInit(applicationContext: ApplicationContext): WordTree { + // Spring 上下文获取敏感词文件 + val sensitiveWordResource = applicationContext.getResource(sensitiveWordPath) + val wordTree = WordTree() + + // 获取载入用时 + val start = System.currentTimeMillis() + + // 以行为单位载入敏感词 + try { + BufferedReader( + InputStreamReader(sensitiveWordResource.inputStream) + ).use { bufferedReader -> + var line: String? + while ((bufferedReader.readLine().also { line = it }) != null) { + wordTree.addWord(line) + } + } + } catch (e: Exception) { + log.error { "敏感词库初始化失败:${e.message}" } + throw e + } + + log.info { "敏感词库初始化完成,耗时 ${System.currentTimeMillis() - start} ms" } + + return wordTree + } +} diff --git a/src/main/kotlin/plus/maa/backend/config/ThreadPoolConfig.kt b/src/main/kotlin/plus/maa/backend/config/ThreadPoolConfig.kt new file mode 100644 index 00000000..99a04010 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/ThreadPoolConfig.kt @@ -0,0 +1,35 @@ +package plus.maa.backend.config + +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration +import org.springframework.boot.task.ThreadPoolTaskExecutorBuilder +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Lazy +import org.springframework.context.annotation.Primary +import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor + +@Configuration(proxyBeanMethods = false) +class ThreadPoolConfig { + @Lazy + @Primary + @Bean( + name = [ + TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME, + AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME + ] + ) + fun defaultTaskExecutor(builder: ThreadPoolTaskExecutorBuilder): ThreadPoolTaskExecutor = builder.build() + + @Bean + fun emailTaskExecutor(): ThreadPoolTaskExecutor { + // 在默认线程池配置的基础上修改了核心线程数和线程名称 + val taskExecutor = ThreadPoolTaskExecutor() + // I/O 密集型配置 + taskExecutor.corePoolSize = Runtime.getRuntime().availableProcessors() * 2 + taskExecutor.setThreadNamePrefix("email-task-") + // 动态的核心线程数量 + taskExecutor.setAllowCoreThreadTimeOut(true) + return taskExecutor + } +} diff --git a/src/main/kotlin/plus/maa/backend/config/accesslimit/AccessLimit.kt b/src/main/kotlin/plus/maa/backend/config/accesslimit/AccessLimit.kt new file mode 100644 index 00000000..cb1af520 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/accesslimit/AccessLimit.kt @@ -0,0 +1,29 @@ +package plus.maa.backend.config.accesslimit + +import java.lang.annotation.Inherited + +/** + * @author Baip1995 + */ +@Inherited +@MustBeDocumented +@Target( + AnnotationTarget.FIELD, + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER +) +@Retention( + AnnotationRetention.RUNTIME +) +annotation class AccessLimit( + /** + * 指定 second 时间内,API 最多的请求次数 + */ + val times: Int = 3, + /** + * 指定时间 second,redis 数据过期时间 + */ + val second: Int = 10 +) diff --git a/src/main/kotlin/plus/maa/backend/config/accesslimit/AccessLimitConfig.kt b/src/main/kotlin/plus/maa/backend/config/accesslimit/AccessLimitConfig.kt new file mode 100644 index 00000000..39d67add --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/accesslimit/AccessLimitConfig.kt @@ -0,0 +1,18 @@ +package plus.maa.backend.config.accesslimit + +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.context.annotation.Configuration +import org.springframework.data.redis.core.StringRedisTemplate +import org.springframework.web.servlet.config.annotation.InterceptorRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +@Configuration +class AccessLimitConfig( + private val stringRedisTemplate: StringRedisTemplate, + private val objectMapper: ObjectMapper, +) : WebMvcConfigurer { + + override fun addInterceptors(registry: InterceptorRegistry) { + registry.addInterceptor(AccessLimitInterceptor(stringRedisTemplate, objectMapper)) + } +} \ No newline at end of file diff --git a/src/main/kotlin/plus/maa/backend/config/accesslimit/AccessLimitInterceptor.kt b/src/main/kotlin/plus/maa/backend/config/accesslimit/AccessLimitInterceptor.kt new file mode 100644 index 00000000..10e484a5 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/accesslimit/AccessLimitInterceptor.kt @@ -0,0 +1,53 @@ +package plus.maa.backend.config.accesslimit + +import com.fasterxml.jackson.databind.ObjectMapper +import io.github.oshai.kotlinlogging.KotlinLogging +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.data.redis.core.StringRedisTemplate +import org.springframework.http.HttpStatus +import org.springframework.web.method.HandlerMethod +import org.springframework.web.servlet.HandlerInterceptor +import plus.maa.backend.common.utils.IpUtil +import plus.maa.backend.common.utils.WebUtils +import plus.maa.backend.controller.response.MaaResult.Companion.fail +import java.util.concurrent.TimeUnit + +/** + * @author Baip1995 + */ +class AccessLimitInterceptor( + private val stringRedisTemplate: StringRedisTemplate, + private val objectMapper: ObjectMapper, +) : HandlerInterceptor { + + private val log = KotlinLogging.logger { } + + @Throws(Exception::class) + override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean { + val ann = (handler as? HandlerMethod)?.method?.getAnnotation(AccessLimit::class.java) ?: return true + // 拼接 redis key = IP + Api 限流 + val key = IpUtil.getIpAddr(request) + request.requestURI + + // 获取 redis 的 value + val count = stringRedisTemplate.opsForValue()[key]?.toInt() ?: 0 + if (count < ann.times) { + // 如果 redis 中的时间比注解上的时间小则表示可以允许访问,这时修改 redis 的 value 时间 + stringRedisTemplate.opsForValue().set( + key, + (count + 1).toString(), + ann.second.toLong(), + TimeUnit.SECONDS + ) + } else { + // 请求过于频繁 + log.info { "$key 请求过于频繁" } + val result = fail(HttpStatus.TOO_MANY_REQUESTS.value(), "请求过于频繁") + val json = objectMapper.writeValueAsString(result) + WebUtils.renderString(response, json, HttpStatus.TOO_MANY_REQUESTS.value()) + return false + } + + return true + } +} diff --git a/src/main/kotlin/plus/maa/backend/config/doc/RequireJwt.kt b/src/main/kotlin/plus/maa/backend/config/doc/RequireJwt.kt new file mode 100644 index 00000000..1384f4c1 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/doc/RequireJwt.kt @@ -0,0 +1,21 @@ +package plus.maa.backend.config.doc + +import io.swagger.v3.oas.annotations.security.SecurityRequirement +import java.lang.annotation.Inherited + +/** + * 指示需要 Jwt 认证 + */ +@Target( + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER, + AnnotationTarget.CLASS, + AnnotationTarget.ANNOTATION_CLASS +) +@Retention( + AnnotationRetention.RUNTIME +) +@Inherited +@SecurityRequirement(name = SpringDocConfig.SECURITY_SCHEME_JWT) +annotation class RequireJwt diff --git a/src/main/kotlin/plus/maa/backend/config/doc/SpringDocConfig.kt b/src/main/kotlin/plus/maa/backend/config/doc/SpringDocConfig.kt new file mode 100644 index 00000000..5e21f73a --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/doc/SpringDocConfig.kt @@ -0,0 +1,57 @@ +package plus.maa.backend.config.doc + +import com.fasterxml.jackson.databind.ObjectMapper +import io.swagger.v3.core.jackson.ModelResolver +import io.swagger.v3.oas.models.Components +import io.swagger.v3.oas.models.ExternalDocumentation +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.info.Info +import io.swagger.v3.oas.models.info.License +import io.swagger.v3.oas.models.security.SecurityScheme +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import plus.maa.backend.config.external.MaaCopilotProperties + +/** + * @author AnselYuki + */ +@Configuration +class SpringDocConfig(properties: MaaCopilotProperties) { + private val _info = properties.info + private val jwt = properties.jwt + + @Bean + fun emergencyLogistics(): OpenAPI = OpenAPI().apply { + info(Info().apply { + title(_info.title) + description(_info.description) + version(_info.version) + license(License().apply { + name("GNU Affero General Public License v3.0") + url("https://www.gnu.org/licenses/agpl-3.0.html") + }) + }) + externalDocs(ExternalDocumentation().apply { + description("GitHub repo") + url("https://github.com/MaaAssistantArknights/MaaBackendCenter") + }) + components(Components().apply { + addSecuritySchemes(SECURITY_SCHEME_JWT, SecurityScheme().apply { + type(SecurityScheme.Type.HTTP) + scheme("bearer") + `in`(SecurityScheme.In.HEADER) + name(jwt.header) + val s = "JWT Authorization header using the Bearer scheme. Raw head example: " + + "\"${jwt.header}: Bearer {token}\"" + description(s) + }) + }) + } + + @Bean + fun modelResolver(objectMapper: ObjectMapper) = ModelResolver(objectMapper) + + companion object { + const val SECURITY_SCHEME_JWT: String = "Jwt" + } +} diff --git a/src/main/kotlin/plus/maa/backend/config/external/ArkLevelGit.kt b/src/main/kotlin/plus/maa/backend/config/external/ArkLevelGit.kt new file mode 100644 index 00000000..8ecabc50 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/external/ArkLevelGit.kt @@ -0,0 +1,8 @@ +package plus.maa.backend.config.external + + +data class ArkLevelGit( + var repository: String = "https://github.com/MaaAssistantArknights/MaaAssistantArknights.git", + var localRepository: String = "./MaaAssistantArknights", + var jsonPath: String = "resource/Arknights-Tile-Pos/", +) diff --git a/src/main/kotlin/plus/maa/backend/config/external/Cache.kt b/src/main/kotlin/plus/maa/backend/config/external/Cache.kt new file mode 100644 index 00000000..99060b2b --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/external/Cache.kt @@ -0,0 +1,6 @@ +package plus.maa.backend.config.external + + +data class Cache ( + var defaultExpire: Long = 0 +) diff --git a/src/main/kotlin/plus/maa/backend/config/external/Copilot.kt b/src/main/kotlin/plus/maa/backend/config/external/Copilot.kt new file mode 100644 index 00000000..110702b5 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/external/Copilot.kt @@ -0,0 +1,12 @@ +package plus.maa.backend.config.external + + +data class Copilot( + /** + * 作业评分总数少于指定值时显示评分不足 + * + * + * 默认值:50 + */ + var minValueShowNotEnoughRating: Int = 50 +) diff --git a/src/main/kotlin/plus/maa/backend/config/external/CopilotBackup.kt b/src/main/kotlin/plus/maa/backend/config/external/CopilotBackup.kt new file mode 100644 index 00000000..c8acf580 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/external/CopilotBackup.kt @@ -0,0 +1,29 @@ +package plus.maa.backend.config.external + + +data class CopilotBackup( + /** + * 是否禁用备份功能 + */ + var disabled: Boolean = false, + + /** + * 本地备份地址 + * */ + var dir: String = "/home/dove/copilotBak", + + /** + * 远程备份地址 + */ + var uri: String = "git@github.com:dragove/maa-copilot-store.git", + + /** + * git 用户名 + */ + var username: String = "dragove", + + /** + * git 邮箱 + */ + var email: String = "dragove@qq.com", +) diff --git a/src/main/kotlin/plus/maa/backend/config/external/Github.kt b/src/main/kotlin/plus/maa/backend/config/external/Github.kt new file mode 100644 index 00000000..a5bfe359 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/external/Github.kt @@ -0,0 +1,9 @@ +package plus.maa.backend.config.external + + +data class Github( + /** + * GitHub api token + */ + var token: String = "github_pat_xxx" +) \ No newline at end of file diff --git a/src/main/kotlin/plus/maa/backend/config/external/Info.kt b/src/main/kotlin/plus/maa/backend/config/external/Info.kt new file mode 100644 index 00000000..5aef2129 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/external/Info.kt @@ -0,0 +1,10 @@ +package plus.maa.backend.config.external + + +data class Info( + var title: String = "MAA Copilot Center API", + var description: String = "MAA Copilot Backend Center", + var version: String = "v1.0.0", + var domain: String = "https://prts.maa.plus", + var frontendDomain: String = "https://prts.plus", +) diff --git a/src/main/kotlin/plus/maa/backend/config/external/Jwt.kt b/src/main/kotlin/plus/maa/backend/config/external/Jwt.kt new file mode 100644 index 00000000..dccdaef3 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/external/Jwt.kt @@ -0,0 +1,24 @@ +package plus.maa.backend.config.external + +data class Jwt( + /** + * Header name + */ + var header: String = "Authorization", + /** + * 默认的JwtToken过期时间,以秒为单位 + */ + var expire: Long = 21600, + /* + * 默认的 Refresh Token 过期时间,以秒为单位 + */ + var refreshExpire: Long = (30 * 24 * 60 * 60).toLong(), + /** + * JwtToken的加密密钥 + */ + var secret: String = "", + /** + * Jwt 最大同时登录设备数 + */ + var maxLogin: Int = 1, +) \ No newline at end of file diff --git a/src/main/kotlin/plus/maa/backend/config/external/MaaCopilotProperties.kt b/src/main/kotlin/plus/maa/backend/config/external/MaaCopilotProperties.kt new file mode 100644 index 00000000..797456b8 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/external/MaaCopilotProperties.kt @@ -0,0 +1,42 @@ +package plus.maa.backend.config.external + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.context.properties.NestedConfigurationProperty +import org.springframework.stereotype.Component + +@Component +@ConfigurationProperties("maa-copilot") +data class MaaCopilotProperties( + @NestedConfigurationProperty + var jwt: Jwt = Jwt(), + + @NestedConfigurationProperty + var github: Github = Github(), + + @NestedConfigurationProperty + var info: Info = Info(), + + @NestedConfigurationProperty + var vcode: Vcode = Vcode(), + + @NestedConfigurationProperty + var cache: Cache = Cache(), + + @NestedConfigurationProperty + var arkLevelGit: ArkLevelGit = ArkLevelGit(), + + @NestedConfigurationProperty + var taskCron: TaskCron = TaskCron(), + + @NestedConfigurationProperty + var backup: CopilotBackup = CopilotBackup(), + + @NestedConfigurationProperty + var mail: Mail = Mail(), + + @NestedConfigurationProperty + var sensitiveWord: SensitiveWord = SensitiveWord(), + + @NestedConfigurationProperty + var copilot: Copilot = Copilot() +) \ No newline at end of file diff --git a/src/main/kotlin/plus/maa/backend/config/external/Mail.kt b/src/main/kotlin/plus/maa/backend/config/external/Mail.kt new file mode 100644 index 00000000..14d1c4bf --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/external/Mail.kt @@ -0,0 +1,13 @@ +package plus.maa.backend.config.external + + +data class Mail( + var host: String = "smtp.qq.com", + var port: Int = 465, + var from: String = "2842775752@qq.com", + var user: String = "2842775752", + var pass: String = "123456789", + var starttls: Boolean = true, + var ssl: Boolean = false, + var notification: Boolean = true, +) diff --git a/src/main/kotlin/plus/maa/backend/config/external/SensitiveWord.kt b/src/main/kotlin/plus/maa/backend/config/external/SensitiveWord.kt new file mode 100644 index 00000000..587a2206 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/external/SensitiveWord.kt @@ -0,0 +1,9 @@ +package plus.maa.backend.config.external + + +data class SensitiveWord( + /** + * 敏感词文件路径,默认为 `classpath:sensitive-word.txt` + */ + var path: String = "classpath:sensitive-word.txt" +) diff --git a/src/main/kotlin/plus/maa/backend/config/external/TaskCron.kt b/src/main/kotlin/plus/maa/backend/config/external/TaskCron.kt new file mode 100644 index 00000000..c301b45e --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/external/TaskCron.kt @@ -0,0 +1,6 @@ +package plus.maa.backend.config.external + +data class TaskCron( + var arkLevel: String = "-", + var copilotUpdate: String = "-" +) diff --git a/src/main/kotlin/plus/maa/backend/config/external/Vcode.kt b/src/main/kotlin/plus/maa/backend/config/external/Vcode.kt new file mode 100644 index 00000000..da90495b --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/external/Vcode.kt @@ -0,0 +1,8 @@ +package plus.maa.backend.config.external + +data class Vcode( + /** + * 默认的验证码失效时间,以秒为单位 + */ + var expire: Long = 0 +) \ No newline at end of file diff --git a/src/main/kotlin/plus/maa/backend/config/security/AccessDeniedHandlerImpl.kt b/src/main/kotlin/plus/maa/backend/config/security/AccessDeniedHandlerImpl.kt new file mode 100644 index 00000000..3ce13d83 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/security/AccessDeniedHandlerImpl.kt @@ -0,0 +1,29 @@ +package plus.maa.backend.config.security + +import com.fasterxml.jackson.databind.ObjectMapper +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.http.HttpStatus +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.web.access.AccessDeniedHandler +import org.springframework.stereotype.Component +import plus.maa.backend.common.utils.WebUtils.renderString +import plus.maa.backend.controller.response.MaaResult.Companion.fail +import java.io.IOException + +/** + * @author AnselYuki + */ +@Component +class AccessDeniedHandlerImpl : AccessDeniedHandler { + @Throws(IOException::class) + override fun handle( + request: HttpServletRequest, + response: HttpServletResponse, + accessDeniedException: AccessDeniedException + ) { + val result = fail(HttpStatus.FORBIDDEN.value(), "权限不足") + val json = ObjectMapper().writeValueAsString(result) + renderString(response, json, HttpStatus.FORBIDDEN.value()) + } +} diff --git a/src/main/kotlin/plus/maa/backend/config/security/AuthenticationEntryPointImpl.kt b/src/main/kotlin/plus/maa/backend/config/security/AuthenticationEntryPointImpl.kt new file mode 100644 index 00000000..36b3ca8b --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/security/AuthenticationEntryPointImpl.kt @@ -0,0 +1,32 @@ +package plus.maa.backend.config.security + +import com.fasterxml.jackson.databind.ObjectMapper +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.http.HttpStatus +import org.springframework.security.core.AuthenticationException +import org.springframework.security.web.AuthenticationEntryPoint +import org.springframework.stereotype.Component +import plus.maa.backend.common.utils.WebUtils.renderString +import plus.maa.backend.controller.response.MaaResult.Companion.fail +import java.io.IOException + +/** + * @author AnselYuki + */ +@Component +class AuthenticationEntryPointImpl( + private val objectMapper: ObjectMapper +) : AuthenticationEntryPoint { + + @Throws(IOException::class) + override fun commence( + request: HttpServletRequest, + response: HttpServletResponse, + authException: AuthenticationException + ) { + val result = fail(HttpStatus.UNAUTHORIZED.value(), authException.message) + val json = objectMapper.writeValueAsString(result) + renderString(response, json, HttpStatus.UNAUTHORIZED.value()) + } +} diff --git a/src/main/kotlin/plus/maa/backend/config/security/AuthenticationHelper.kt b/src/main/kotlin/plus/maa/backend/config/security/AuthenticationHelper.kt new file mode 100644 index 00000000..2499b96f --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/security/AuthenticationHelper.kt @@ -0,0 +1,73 @@ +package plus.maa.backend.config.security + +import org.springframework.http.HttpStatus +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.stereotype.Component +import org.springframework.web.context.request.RequestContextHolder +import org.springframework.web.context.request.ServletRequestAttributes +import org.springframework.web.server.ResponseStatusException +import plus.maa.backend.common.utils.IpUtil.getIpAddr +import plus.maa.backend.service.jwt.JwtAuthToken +import plus.maa.backend.service.model.LoginUser +import java.util.* + +/** + * Auth 助手,统一 auth 的设置和获取 + */ +@Component +class AuthenticationHelper { + /** + * 设置当前 auth, 是 SecurityContextHolder.getContext().setAuthentication(authentication) 的集中调用 + * + * @param authentication 当前的 auth + */ + fun setAuthentication(authentication: Authentication?) { + SecurityContextHolder.getContext().authentication = authentication + } + + /** + * 要求用户 id ,否则抛出异常 + * + * @return 已经验证的用户 id + * @throws ResponseStatusException 用户未通过验证 + */ + @Throws(ResponseStatusException::class) + fun requireUserId(): String { + val id = userId ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED) + return id + } + + val userId: String? + /** + * 获取用户 id + * + * @return 用户 id,如未验证则返回 null + */ + get() { + val auth = SecurityContextHolder.getContext().authentication ?: return null + if (auth is UsernamePasswordAuthenticationToken) { + val principal = auth.getPrincipal() + if (principal is LoginUser) return principal.userId + } else if (auth is JwtAuthToken) { + return auth.subject + } + return null + } + + val userIdOrIpAddress: String + /** + * 获取已验证用户 id 或者未验证用户 ip 地址。在 HTTP request 之外调用该方法获取 ip 会抛出 NPE + * + * @return 用户 id 或者 ip 地址 + */ + get() { + val id = userId + if (id != null) return id + + val attributes = Objects.requireNonNull(RequestContextHolder.getRequestAttributes()) + val request = (attributes as ServletRequestAttributes).request + return getIpAddr(request) + } +} diff --git a/src/main/kotlin/plus/maa/backend/config/security/JwtAuthenticationTokenFilter.kt b/src/main/kotlin/plus/maa/backend/config/security/JwtAuthenticationTokenFilter.kt new file mode 100644 index 00000000..b5ef49ef --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/security/JwtAuthenticationTokenFilter.kt @@ -0,0 +1,47 @@ +package plus.maa.backend.config.security + +import jakarta.servlet.FilterChain +import jakarta.servlet.ServletException +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter +import plus.maa.backend.config.external.MaaCopilotProperties +import plus.maa.backend.service.jwt.JwtService +import java.io.IOException + +/** + * @author AnselYuki + */ +@Component +class JwtAuthenticationTokenFilter( + private val helper: AuthenticationHelper, + private val properties: MaaCopilotProperties, + private val jwtService: JwtService +) : OncePerRequestFilter() { + @Throws(IOException::class, ServletException::class) + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + try { + val token = extractToken(request) + val authToken = jwtService.verifyAndParseAuthToken(token) + helper.setAuthentication(authToken) + } catch (ex: Exception) { + logger.trace(ex.message) + } finally { + filterChain.doFilter(request, response) + } + } + + @Throws(Exception::class) + private fun extractToken(request: HttpServletRequest): String { + if (SecurityContextHolder.getContext().authentication != null) throw Exception("no need to auth") + val head = request.getHeader(properties.jwt.header) + if (head == null || !head.startsWith("Bearer ")) throw Exception("token not found") + return head.substring(7) + } +} diff --git a/src/main/kotlin/plus/maa/backend/config/security/SecurityConfig.kt b/src/main/kotlin/plus/maa/backend/config/security/SecurityConfig.kt new file mode 100644 index 00000000..84ec20b8 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/config/security/SecurityConfig.kt @@ -0,0 +1,120 @@ +package plus.maa.backend.config.security + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.config.Customizer +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer +import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer +import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter + +/** + * @author AnselYuki + */ +@Configuration +class SecurityConfig( + private val authenticationConfiguration: AuthenticationConfiguration, + private val jwtAuthenticationTokenFilter: JwtAuthenticationTokenFilter, + private val authenticationEntryPoint: AuthenticationEntryPointImpl, + private val accessDeniedHandler: AccessDeniedHandlerImpl +) { + + @Bean + fun passwordEncoder() = BCryptPasswordEncoder() + + @Bean + @Throws(Exception::class) + fun authenticationManager(): AuthenticationManager = authenticationConfiguration.authenticationManager + + @Bean + @Throws(Exception::class) + fun filterChain(http: HttpSecurity): SecurityFilterChain { + //关闭CSRF,设置无状态连接 + http.csrf { obj: CsrfConfigurer -> obj.disable() } //不通过Session获取SecurityContext + .sessionManagement { sessionManagement: SessionManagementConfigurer -> + sessionManagement.sessionCreationPolicy( + SessionCreationPolicy.STATELESS + ) + } + + //允许匿名访问的接口,如果是测试想要方便点就把这段全注释掉 + http.authorizeHttpRequests { authorize -> + authorize.requestMatchers(*URL_WHITELIST).anonymous() + .requestMatchers(*URL_PERMIT_ALL).permitAll() //权限 0 未激活 1 激活 等等.. (拥有权限1必然拥有权限0 拥有权限2必然拥有权限1、0) + //指定接口需要指定权限才能访问 如果不开启RBAC注释掉这一段即可 + .requestMatchers(*URL_AUTHENTICATION_1).hasAuthority("1") //此处用于管理员操作接口 + .requestMatchers(*URL_AUTHENTICATION_2).hasAuthority("2") + .anyRequest().authenticated() + } + //添加过滤器 + http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter::class.java) + + //配置异常处理器,处理认证失败的JSON响应 + http.exceptionHandling { exceptionHandling: ExceptionHandlingConfigurer -> + exceptionHandling.authenticationEntryPoint( + authenticationEntryPoint + ).accessDeniedHandler(accessDeniedHandler) + } + + //开启跨域请求 + http.cors(Customizer.withDefaults()) + return http.build() + } + + companion object { + /** + * 添加放行接口在此处 + */ + private val URL_WHITELIST = arrayOf( + "/user/login", + "/user/register", + "/user/sendRegistrationToken" + ) + + private val URL_PERMIT_ALL = arrayOf( + "/", + "/error", + "/version", + "/user/activateAccount", + "/user/password/reset_request", + "/user/password/reset", + "/user/refresh", + "/swagger-ui.html", + "/v3/api-docs/**", + "/swagger-ui/**", + "/arknights/level", + "/copilot/query", + "/set/query", + "/set/get", + "/copilot/get/**", + "/copilot/rating", + "/comments/query", + "/file/upload", + "/comments/status", + "/copilot/status" + ) + + //添加需要权限1才能访问的接口 + private val URL_AUTHENTICATION_1 = arrayOf( + "/copilot/delete", + "/copilot/update", + "/copilot/upload", + "/comments/add", + "/comments/delete" + ) + + private val URL_AUTHENTICATION_2 = arrayOf( + "/file/download/**", + "/file/download/", + "/file/disable", + "/file/enable", + "/file/upload_ability" + ) + } +} diff --git a/src/main/kotlin/plus/maa/backend/controller/ArkLevelController.kt b/src/main/kotlin/plus/maa/backend/controller/ArkLevelController.kt new file mode 100644 index 00000000..9854c9d3 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/ArkLevelController.kt @@ -0,0 +1,28 @@ +package plus.maa.backend.controller + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController +import plus.maa.backend.controller.response.MaaResult +import plus.maa.backend.controller.response.MaaResult.Companion.success +import plus.maa.backend.controller.response.copilot.ArkLevelInfo +import plus.maa.backend.service.ArkLevelService + +/** + * @author john180 + */ +@RestController +@Tag(name = "ArkLevelController", description = "关卡数据管理接口") +class ArkLevelController( + private val arkLevelService: ArkLevelService +) { + + @GetMapping("/arknights/level") + @ApiResponse(description = "关卡数据") + @Operation(summary = "获取关卡数据") + fun getLevels(): MaaResult> { + return success(arkLevelService.arkLevelInfos) + } +} diff --git a/src/main/kotlin/plus/maa/backend/controller/CommentsAreaController.kt b/src/main/kotlin/plus/maa/backend/controller/CommentsAreaController.kt new file mode 100644 index 00000000..1e696c27 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/CommentsAreaController.kt @@ -0,0 +1,83 @@ +package plus.maa.backend.controller + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import jakarta.validation.constraints.NotBlank +import org.springdoc.core.annotations.ParameterObject +import org.springframework.web.bind.annotation.* +import plus.maa.backend.common.annotation.JsonSchema +import plus.maa.backend.common.annotation.SensitiveWordDetection +import plus.maa.backend.config.doc.RequireJwt +import plus.maa.backend.config.security.AuthenticationHelper +import plus.maa.backend.controller.request.comments.* +import plus.maa.backend.controller.response.MaaResult +import plus.maa.backend.controller.response.MaaResult.Companion.success +import plus.maa.backend.controller.response.comments.CommentsAreaInfo +import plus.maa.backend.service.CommentsAreaService + +/** + * @author LoMu + * Date 2023-02-17 14:56 + */ +@RestController +@Tag(name = "CommentArea", description = "评论区管理接口") +@RequestMapping("/comments") +class CommentsAreaController( + private val commentsAreaService: CommentsAreaService, + private val authHelper: AuthenticationHelper +) { + @SensitiveWordDetection("#comments.message") + @PostMapping("/add") + @Operation(summary = "发送评论") + @ApiResponse(description = "发送评论结果") + @RequireJwt + fun sendComments(@RequestBody comments: @Valid CommentsAddDTO): MaaResult { + commentsAreaService.addComments(authHelper.requireUserId(), comments) + return success("评论成功") + } + + @GetMapping("/query") + @Operation(summary = "分页查询评论") + @ApiResponse(description = "评论区信息") + fun queriesCommentsArea(@ParameterObject parsed: @Valid CommentsQueriesDTO): MaaResult { + return success(commentsAreaService.queriesCommentsArea(parsed)) + } + + @PostMapping("/delete") + @Operation(summary = "删除评论") + @ApiResponse(description = "评论删除结果") + @RequireJwt + fun deleteComments(@RequestBody comments: @Valid CommentsDeleteDTO): MaaResult { + commentsAreaService.deleteComments(authHelper.requireUserId(), comments.commentId) + return success("评论已删除") + } + + @JsonSchema + @Operation(summary = "为评论点赞") + @ApiResponse(description = "点赞结果") + @RequireJwt + @PostMapping("/rating") + fun ratesComments(@RequestBody commentsRatingDTO: @Valid CommentsRatingDTO): MaaResult { + commentsAreaService.rates(authHelper.requireUserId(), commentsRatingDTO) + return success("成功") + } + + @Operation(summary = "为评论置顶/取消置顶") + @ApiResponse(description = "置顶/取消置顶结果") + @RequireJwt + @PostMapping("/topping") + fun toppingComments(@RequestBody commentsToppingDTO: @Valid CommentsToppingDTO): MaaResult { + commentsAreaService.topping(authHelper.requireUserId(), commentsToppingDTO) + return success("成功") + } + + @Operation(summary = "设置通知接收状态") + @RequireJwt + @GetMapping("/status") + fun modifyStatus(@RequestParam id: @NotBlank String, @RequestParam status: Boolean): MaaResult { + commentsAreaService.notificationStatus(authHelper.requireUserId(), id, status) + return success("success") + } +} diff --git a/src/main/kotlin/plus/maa/backend/controller/CopilotController.kt b/src/main/kotlin/plus/maa/backend/controller/CopilotController.kt new file mode 100644 index 00000000..6e0b88e4 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/CopilotController.kt @@ -0,0 +1,110 @@ +package plus.maa.backend.controller + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.servlet.http.HttpServletResponse +import jakarta.validation.Valid +import jakarta.validation.constraints.NotBlank +import org.springdoc.core.annotations.ParameterObject +import org.springframework.http.HttpHeaders +import org.springframework.web.bind.annotation.* +import plus.maa.backend.common.annotation.JsonSchema +import plus.maa.backend.common.annotation.SensitiveWordDetection +import plus.maa.backend.config.doc.RequireJwt +import plus.maa.backend.config.security.AuthenticationHelper +import plus.maa.backend.controller.request.copilot.CopilotCUDRequest +import plus.maa.backend.controller.request.copilot.CopilotQueriesRequest +import plus.maa.backend.controller.request.copilot.CopilotRatingReq +import plus.maa.backend.controller.response.MaaResult +import plus.maa.backend.controller.response.MaaResult.Companion.fail +import plus.maa.backend.controller.response.MaaResult.Companion.success +import plus.maa.backend.controller.response.copilot.CopilotInfo +import plus.maa.backend.controller.response.copilot.CopilotPageInfo +import plus.maa.backend.service.CopilotService + +/** + * @author LoMu + * Date 2022-12-25 17:08 + */ +@RestController +@RequestMapping("/copilot") +@Tag(name = "CopilotController", description = "作业本体管理接口") +class CopilotController( + private val copilotService: CopilotService, + private val helper: AuthenticationHelper, + private val response: HttpServletResponse +) { + + @Operation(summary = "上传作业") + @ApiResponse(description = "上传作业结果") + @RequireJwt + @JsonSchema + @SensitiveWordDetection("#request.content != null ? #objectMapper.readTree(#request.content).get('doc')?.toString() : null") + @PostMapping("/upload") + fun uploadCopilot(@RequestBody request: CopilotCUDRequest): MaaResult { + return success(copilotService.upload(helper.requireUserId(), request.content)) + } + + @Operation(summary = "删除作业") + @ApiResponse(description = "删除作业结果") + @RequireJwt + @PostMapping("/delete") + fun deleteCopilot(@RequestBody request: CopilotCUDRequest?): MaaResult { + copilotService.delete(helper.requireUserId(), request!!) + return success() + } + + @Operation(summary = "获取作业") + @ApiResponse(description = "作业信息") + @GetMapping("/get/{id}") + fun getCopilotById( + @Parameter(description = "作业id") @PathVariable("id") id: Long + ): MaaResult { + val userIdOrIpAddress = helper.userIdOrIpAddress + return copilotService.getCopilotById(userIdOrIpAddress, id)?.let { success(it) } + ?: fail(404, "作业不存在") + } + + + @Operation(summary = "分页查询作业,提供登录凭据时查询用户自己的作业") + @ApiResponse(description = "作业信息") + @GetMapping("/query") + fun queriesCopilot( + @ParameterObject parsed: @Valid CopilotQueriesRequest + ): MaaResult { + // 三秒防抖,缓解前端重复请求问题 + response.setHeader(HttpHeaders.CACHE_CONTROL, "private, max-age=3, must-revalidate") + return success(copilotService.queriesCopilot(helper.userId, parsed)) + } + + @Operation(summary = "更新作业") + @ApiResponse(description = "更新结果") + @RequireJwt + @JsonSchema + @SensitiveWordDetection("#copilotCUDRequest.content != null ? #objectMapper.readTree(#copilotCUDRequest.content).get('doc')?.toString() : null") + @PostMapping("/update") + fun updateCopilot(@RequestBody copilotCUDRequest: CopilotCUDRequest): MaaResult { + copilotService.update(helper.requireUserId(), copilotCUDRequest) + return success() + } + + @Operation(summary = "为作业评分") + @ApiResponse(description = "评分结果") + @JsonSchema + @PostMapping("/rating") + fun ratesCopilotOperation(@RequestBody copilotRatingReq: CopilotRatingReq): MaaResult { + copilotService.rates(helper.userIdOrIpAddress, copilotRatingReq) + return success("评分成功") + } + + @RequireJwt + @Operation(summary = "修改通知状态") + @ApiResponse(description = "success") + @GetMapping("/status") + fun modifyStatus(@RequestParam id: @NotBlank Long, @RequestParam status: Boolean): MaaResult { + copilotService.notificationStatus(helper.userId, id, status) + return success("success") + } +} diff --git a/src/main/kotlin/plus/maa/backend/controller/CopilotSetController.kt b/src/main/kotlin/plus/maa/backend/controller/CopilotSetController.kt new file mode 100644 index 00000000..30b4bfc2 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/CopilotSetController.kt @@ -0,0 +1,87 @@ +package plus.maa.backend.controller + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.web.bind.annotation.* +import plus.maa.backend.config.doc.RequireJwt +import plus.maa.backend.config.security.AuthenticationHelper +import plus.maa.backend.controller.request.CommonIdReq +import plus.maa.backend.controller.request.copilotset.CopilotSetCreateReq +import plus.maa.backend.controller.request.copilotset.CopilotSetModCopilotsReq +import plus.maa.backend.controller.request.copilotset.CopilotSetQuery +import plus.maa.backend.controller.request.copilotset.CopilotSetUpdateReq +import plus.maa.backend.controller.response.MaaResult +import plus.maa.backend.controller.response.MaaResult.Companion.success +import plus.maa.backend.controller.response.copilotset.CopilotSetPageRes +import plus.maa.backend.controller.response.copilotset.CopilotSetRes +import plus.maa.backend.service.CopilotSetService + +/** + * @author dragove + * create on 2024-01-01 + */ +@Tag(name = "CopilotSet", description = "作业集相关接口") +@RequestMapping("/set") +@RestController +class CopilotSetController( + private val service: CopilotSetService, + private val helper: AuthenticationHelper +) { + + @Operation(summary = "查询作业集列表") + @ApiResponse(description = "作业集id") + @PostMapping("/query") + fun querySets(@RequestBody req: @Valid CopilotSetQuery): MaaResult { + return success(service.query(req)) + } + + @Operation(summary = "查询作业集列表") + @ApiResponse(description = "作业集id") + @GetMapping("/get") + fun getSet(@RequestParam @Parameter(description = "作业id") id: Long): MaaResult { + return success(service.get(id)) + } + + @Operation(summary = "创建作业集") + @ApiResponse(description = "作业集id") + @RequireJwt + @PostMapping("/create") + fun createSet(@RequestBody req: @Valid CopilotSetCreateReq): MaaResult { + return success(service.create(req, helper.userId)) + } + + @Operation(summary = "添加作业集作业列表") + @RequireJwt + @PostMapping("/add") + fun addCopilotIds(@RequestBody req: @Valid CopilotSetModCopilotsReq): MaaResult { + service.addCopilotIds(req, helper.requireUserId()) + return success() + } + + @Operation(summary = "添加作业集作业列表") + @RequireJwt + @PostMapping("/remove") + fun removeCopilotIds(@RequestBody req: @Valid CopilotSetModCopilotsReq): MaaResult { + service.removeCopilotIds(req, helper.requireUserId()) + return success() + } + + @Operation(summary = "更新作业集信息") + @RequireJwt + @PostMapping("/update") + fun updateCopilotSet(@RequestBody req: @Valid CopilotSetUpdateReq): MaaResult { + service.update(req, helper.requireUserId()) + return success() + } + + @Operation(summary = "删除作业集") + @RequireJwt + @PostMapping("/delete") + fun deleteCopilotSet(@RequestBody req: @Valid CommonIdReq): MaaResult { + service.delete(req.id, helper.requireUserId()) + return success() + } +} diff --git a/src/main/kotlin/plus/maa/backend/controller/SystemController.kt b/src/main/kotlin/plus/maa/backend/controller/SystemController.kt new file mode 100644 index 00000000..cbc8a389 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/SystemController.kt @@ -0,0 +1,51 @@ +package plus.maa.backend.controller + +import io.swagger.v3.oas.annotations.tags.Tag +import kotlinx.coroutines.delay +import org.springframework.boot.info.GitProperties +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import plus.maa.backend.config.external.MaaCopilotProperties +import plus.maa.backend.controller.response.MaaResult + +/** + * @author AnselYuki + */ +@Tag(name = "System", description = "系统管理接口") +@RequestMapping("") +@RestController +class SystemController( + private val properties: MaaCopilotProperties, + private val gitProperties: GitProperties +) { + + /** + * Tests if the server is ready. + * @return 系统启动信息 + */ + @GetMapping("/") + suspend fun test(): MaaResult { + delay(1000L) + return MaaResult.success("Maa Copilot Server is Running", null) + } + + + /** + * Gets the current version of the server. + * @return 系统版本信息 + */ + @GetMapping("version") + fun getSystemVersion(): MaaResult { + val info = properties.info + val systemInfo = MaaSystemInfo(info.title, info.description, info.version, gitProperties) + return MaaResult.success(systemInfo) + } + + data class MaaSystemInfo( + val title: String, + val description: String, + val version: String, + val git: GitProperties, + ) +} \ No newline at end of file diff --git a/src/main/kotlin/plus/maa/backend/controller/UserController.kt b/src/main/kotlin/plus/maa/backend/controller/UserController.kt new file mode 100644 index 00000000..a5ba5754 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/UserController.kt @@ -0,0 +1,149 @@ +package plus.maa.backend.controller + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import plus.maa.backend.config.doc.RequireJwt +import plus.maa.backend.config.security.AuthenticationHelper +import plus.maa.backend.controller.request.user.* +import plus.maa.backend.controller.response.MaaResult +import plus.maa.backend.controller.response.MaaResult.Companion.success +import plus.maa.backend.controller.response.user.MaaLoginRsp +import plus.maa.backend.controller.response.user.MaaUserInfo +import plus.maa.backend.service.EmailService +import plus.maa.backend.service.UserService + +/** + * 用户相关接口 + * [前端api约定文件](https://github.com/MaaAssistantArknights/maa-copilot-frontend/blob/dev/src/apis/auth.ts) + * + * @author AnselYuki + */ +@Tag(name = "CopilotUser", description = "用户管理") +@RequestMapping("/user") +@Validated +@RestController +class UserController( + private val userService: UserService, + private val emailService: EmailService, + private val helper: AuthenticationHelper, +) { + + /** + * 更新当前用户的密码(根据原密码) + * + * @return http响应 + */ + @Operation(summary = "修改当前用户密码", description = "根据原密码") + @ApiResponse(description = "修改密码结果") + @RequireJwt + @PostMapping("/update/password") + fun updatePassword(@RequestBody updateDTO: @Valid PasswordUpdateDTO): MaaResult { + userService.modifyPassword(helper.requireUserId(), updateDTO.newPassword, updateDTO.originalPassword) + return success() + } + + /** + * 更新用户详细信息 + * + * @param updateDTO 用户信息参数 + * @return http响应 + */ + @Operation(summary = "更新用户详细信息") + @ApiResponse(description = "更新结果") + @RequireJwt + @PostMapping("/update/info") + fun updateInfo(@RequestBody updateDTO: @Valid UserInfoUpdateDTO): MaaResult { + userService.updateUserInfo(helper.requireUserId(), updateDTO) + return success() + } + + /** + * 邮箱重设密码 + * + * @param passwordResetDTO 通过邮箱修改密码请求 + * @return 成功响应 + */ + @PostMapping("/password/reset") + @Operation(summary = "重置密码") + @ApiResponse(description = "重置密码结果") + fun passwordReset(@RequestBody passwordResetDTO: @Valid PasswordResetDTO): MaaResult { + // 校验用户邮箱是否存在 + userService.checkUserExistByEmail(passwordResetDTO.email) + userService.modifyPasswordByActiveCode(passwordResetDTO) + return success() + } + + /** + * 验证码重置密码功能: + * 发送验证码用于重置 + * + * @return 成功响应 + */ + @PostMapping("/password/reset_request") + @Operation(summary = "发送用于重置密码的验证码") + @ApiResponse(description = "验证码发送结果") + fun passwordResetRequest(@RequestBody passwordResetVCodeDTO: @Valid PasswordResetVCodeDTO): MaaResult { + // 校验用户邮箱是否存在 + userService.checkUserExistByEmail(passwordResetVCodeDTO.email) + emailService.sendVCode(passwordResetVCodeDTO.email) + return success() + } + + /** + * 刷新token + * + * @param request http请求,用于获取请求头 + * @return 成功响应 + */ + @PostMapping("/refresh") + @Operation(summary = "刷新token") + @ApiResponse(description = "刷新token结果") + fun refresh(@RequestBody request: RefreshReq): MaaResult { + val res = userService.refreshToken(request.refreshToken) + return success(res) + } + + /** + * 用户注册 + * + * @param user 传入用户参数 + * @return 注册成功用户信息摘要 + */ + @PostMapping("/register") + @Operation(summary = "用户注册") + @ApiResponse(description = "注册结果") + fun register(@RequestBody user: @Valid RegisterDTO): MaaResult { + return success(userService.register(user)) + } + + /** + * 获得注册时的验证码 + */ + @PostMapping("/sendRegistrationToken") + @Operation(summary = "注册时发送验证码") + @ApiResponse(description = "发送验证码结果", responseCode = "204") + fun sendRegistrationToken(@RequestBody regDTO: @Valid SendRegistrationTokenDTO): MaaResult { + userService.sendRegistrationToken(regDTO) + return success() + } + + /** + * 用户登录 + * + * @param user 登录参数 + * @return 成功响应,荷载JwtToken + */ + @PostMapping("/login") + @Operation(summary = "用户登录") + @ApiResponse(description = "登录结果") + fun login(@RequestBody user: @Valid LoginDTO): MaaResult { + return success("登陆成功", userService.login(user)) + } +} diff --git a/src/main/kotlin/plus/maa/backend/controller/file/FileController.kt b/src/main/kotlin/plus/maa/backend/controller/file/FileController.kt new file mode 100644 index 00000000..85226975 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/file/FileController.kt @@ -0,0 +1,115 @@ +package plus.maa.backend.controller.file + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import jakarta.servlet.http.HttpServletResponse +import jakarta.validation.Valid +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartFile +import plus.maa.backend.config.accesslimit.AccessLimit +import plus.maa.backend.config.doc.RequireJwt +import plus.maa.backend.config.security.AuthenticationHelper +import plus.maa.backend.controller.response.MaaResult +import plus.maa.backend.controller.response.MaaResult.Companion.fail +import plus.maa.backend.controller.response.MaaResult.Companion.success +import plus.maa.backend.service.FileService + + +/** + * @author LoMu + * Date 2023-03-31 16:41 + */ +@RestController +@RequestMapping("file") +class FileController( + private val fileService: FileService, + private val helper: AuthenticationHelper +) { + /** + * 支持匿名 + * + * @param file file + * @return 上传成功, 数据已被接收 + */ + @AccessLimit + @PostMapping(value = ["/upload"], consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) + fun uploadFile( + @RequestPart file: MultipartFile, + @RequestPart type: String?, + @RequestPart version: String, + @RequestPart(required = false) classification: String?, + @RequestPart(required = false) label: String + ): MaaResult { + fileService.uploadFile(file, type, version, classification, label, helper.userIdOrIpAddress) + return success("上传成功,数据已被接收") + } + + @Operation(summary = "下载文件") + @ApiResponse( + responseCode = "200", + content = [Content(mediaType = "application/zip", schema = Schema(type = "string", format = "binary"))] + ) + @RequireJwt + @AccessLimit + @GetMapping("/download") + fun downloadSpecifiedDateFile( + @Parameter(description = "日期 yyyy-MM-dd") date: String?, + @Parameter(description = "在日期之前或之后[before,after]") beLocated: String, + @Parameter(description = "对查询到的数据进行删除") delete: Boolean, + response: HttpServletResponse + ) { + fileService.downloadDateFile(date, beLocated, delete, response) + } + + @Operation(summary = "下载文件") + @ApiResponse( + responseCode = "200", + content = [Content(mediaType = "application/zip", schema = Schema(type = "string", format = "binary"))] + ) + @RequireJwt + @PostMapping("/download") + fun downloadFile( + @RequestBody imageDownloadDTO: @Valid ImageDownloadDTO, + response: HttpServletResponse + ) { + fileService.downloadFile(imageDownloadDTO, response) + } + + @Operation(summary = "设置上传文件功能状态") + @RequireJwt + @PostMapping("/upload_ability") + fun setUploadAbility(@RequestBody request: UploadAbility): MaaResult { + fileService.isUploadEnabled = request.enabled + return success() + } + + @GetMapping("/upload_ability") + @RequireJwt + @Operation(summary = "获取上传文件功能状态") + fun getUploadAbility(): MaaResult { + return success(UploadAbility(fileService.isUploadEnabled)) + } + @Operation(summary = "关闭uploadfile接口") + @RequireJwt + @PostMapping("/disable") + fun disable(@RequestBody status: Boolean): MaaResult { + if (!status) { + return fail(403, "Forbidden") + } + return success(fileService.disable()) + } + + @Operation(summary = "开启uploadfile接口") + @RequireJwt + @PostMapping("/enable") + fun enable(@RequestBody status: Boolean): MaaResult { + if (!status) { + return fail(403, "Forbidden") + } + return success(fileService.enable()) + } +} diff --git a/src/main/kotlin/plus/maa/backend/controller/file/ImageDownloadDTO.kt b/src/main/kotlin/plus/maa/backend/controller/file/ImageDownloadDTO.kt new file mode 100644 index 00000000..9a7dc258 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/file/ImageDownloadDTO.kt @@ -0,0 +1,16 @@ +package plus.maa.backend.controller.file + +import jakarta.validation.constraints.NotNull + +/** + * @author LoMu + * Date 2023-04-16 17:41 + */ +data class ImageDownloadDTO( + @field:NotNull + val type: String, + val classification: String? = null, + val version: List? = null, + val label: String? = null, + val delete: Boolean = false +) diff --git a/src/main/kotlin/plus/maa/backend/controller/file/UploadAbility.kt b/src/main/kotlin/plus/maa/backend/controller/file/UploadAbility.kt new file mode 100644 index 00000000..80281dd9 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/file/UploadAbility.kt @@ -0,0 +1,11 @@ +package plus.maa.backend.controller.file + +import jakarta.validation.constraints.NotNull + +class UploadAbility( + /** + * 是否开启上传功能 + */ + @field:NotNull + var enabled: Boolean +) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/CommonIdReq.kt b/src/main/kotlin/plus/maa/backend/controller/request/CommonIdReq.kt new file mode 100644 index 00000000..084b2359 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/CommonIdReq.kt @@ -0,0 +1,12 @@ +package plus.maa.backend.controller.request + +import jakarta.validation.constraints.NotNull + +/** + * @author dragove + * create on 2024-01-05 + */ +data class CommonIdReq( + @field:NotNull(message = "id必填") + val id: T +) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/comments/CommentsAddDTO.kt b/src/main/kotlin/plus/maa/backend/controller/request/comments/CommentsAddDTO.kt new file mode 100644 index 00000000..2a9012c0 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/comments/CommentsAddDTO.kt @@ -0,0 +1,20 @@ +package plus.maa.backend.controller.request.comments + +import jakarta.validation.constraints.NotBlank +import org.hibernate.validator.constraints.Length + +/** + * @author LoMu + * Date 2023-02-17 14:58 + */ +data class CommentsAddDTO( + // 评论内容 + @field:Length(min = 1, max = 150, message = "评论内容不可超过150字,请删减") + @field:NotBlank(message = "请填写评论内容") + val message: String, + // 评论的作业id + val copilotId: Long, + // 子评论来源评论id(回复评论) + val fromCommentId: String? = null, + val notification: Boolean = true +) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/comments/CommentsDeleteDTO.kt b/src/main/kotlin/plus/maa/backend/controller/request/comments/CommentsDeleteDTO.kt new file mode 100644 index 00000000..2c7d8667 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/comments/CommentsDeleteDTO.kt @@ -0,0 +1,12 @@ +package plus.maa.backend.controller.request.comments + +import jakarta.validation.constraints.NotBlank + +/** + * @author LoMu + * Date 2023-02-19 10:50 + */ +data class CommentsDeleteDTO( + @field:NotBlank(message = "评论id不可为空") + val commentId: String +) \ No newline at end of file diff --git a/src/main/kotlin/plus/maa/backend/controller/request/comments/CommentsQueriesDTO.kt b/src/main/kotlin/plus/maa/backend/controller/request/comments/CommentsQueriesDTO.kt new file mode 100644 index 00000000..85b76510 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/comments/CommentsQueriesDTO.kt @@ -0,0 +1,19 @@ +package plus.maa.backend.controller.request.comments + +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.NotNull + +/** + * @author LoMu + * Date 2023-02-20 17:13 + */ +data class CommentsQueriesDTO( + @field:NotNull(message = "作业id不可为空") + val copilotId: Long, + val page: Int = 0, + @field:Max(value = 50, message = "单页大小不得超过50") + val limit: Int = 10, + val desc: Boolean = true, + val orderBy: String? = null, + val justSeeId: String? = null +) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/comments/CommentsRatingDTO.kt b/src/main/kotlin/plus/maa/backend/controller/request/comments/CommentsRatingDTO.kt new file mode 100644 index 00000000..54dbda36 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/comments/CommentsRatingDTO.kt @@ -0,0 +1,14 @@ +package plus.maa.backend.controller.request.comments + +import jakarta.validation.constraints.NotBlank + +/** + * @author LoMu + * Date 2023-02-19 13:39 + */ +data class CommentsRatingDTO( + @field:NotBlank(message = "评分id不可为空") + val commentId: String, + @field:NotBlank(message = "评分不能为空") + val rating: String +) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/comments/CommentsToppingDTO.kt b/src/main/kotlin/plus/maa/backend/controller/request/comments/CommentsToppingDTO.kt new file mode 100644 index 00000000..ccd33698 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/comments/CommentsToppingDTO.kt @@ -0,0 +1,14 @@ +package plus.maa.backend.controller.request.comments + +import jakarta.validation.constraints.NotBlank + +/** + * @author Lixuhuilll + * Date 2023-08-17 11:20 + */ +data class CommentsToppingDTO ( + @field:NotBlank(message = "评论id不可为空") + val commentId: String, + // 是否将指定评论置顶 + val topping: Boolean = true +) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotCUDRequest.kt b/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotCUDRequest.kt new file mode 100644 index 00000000..a4d73a3f --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotCUDRequest.kt @@ -0,0 +1,6 @@ +package plus.maa.backend.controller.request.copilot + +data class CopilotCUDRequest ( + val content: String? = null, + val id: Long? = null +) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotDTO.kt b/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotDTO.kt new file mode 100644 index 00000000..0cf4d665 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotDTO.kt @@ -0,0 +1,36 @@ +package plus.maa.backend.controller.request.copilot + +import jakarta.validation.constraints.NotBlank +import plus.maa.backend.repository.entity.Copilot + + +/** + * @author LoMu + * Date 2023-01-10 19:50 + */ +data class CopilotDTO ( + //关卡名 + @field:NotBlank(message = "关卡名不能为空") + var stageName: String, + + //难度 + val difficulty: Int = 0, + + //版本号(文档中说明:最低要求 maa 版本号,必选。保留字段) + @field:NotBlank(message = "最低要求 maa 版本不可为空") + val minimumRequired: String, + + //指定干员 + val opers: List? = null, + + //群组 + val groups: List? = null, + + // 战斗中的操作 + val actions: List? = null, + + //描述 + val doc: Copilot.Doc? = null, + + val notification: Boolean = false +) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotQueriesRequest.kt b/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotQueriesRequest.kt new file mode 100644 index 00000000..1062d48e --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotQueriesRequest.kt @@ -0,0 +1,53 @@ +package plus.maa.backend.controller.request.copilot + +import com.fasterxml.jackson.annotation.JsonIgnore +import jakarta.validation.constraints.Max + +/** + * @author LoMu + * Date 2022-12-26 2:48 + */ +data class CopilotQueriesRequest( + val page: Int = 0, + val limit: @Max(value = 50, message = "单页大小不得超过50") Int = 10, + var levelKeyword: String? = null, + val operator: String? = null, + val content: String? = null, + val document: String? = null, + var uploaderId: String? = null, + val desc: Boolean = true, + var orderBy: String? = null, + val language: String? = null, + var copilotIds: List? = null +) { + + /* + * 这里为了正确接收前端的下划线风格,手动写了三个 setter 用于起别名 + * 因为 Get 请求传入的参数不是 JSON,所以没办法使用 Jackson 的注解直接实现别名 + * 添加 @JsonAlias 和 @JsonIgnore 注解只是为了保障 Swagger 的文档正确显示 + * (吐槽一下,同样是Get请求,怎么CommentsQueries是驼峰命名,到了CopilotQueries就成了下划线命名) + */ + @JsonIgnore + @Suppress("unused") + fun setLevel_keyword(levelKeyword: String?) { + this.levelKeyword = levelKeyword + } + + @JsonIgnore + @Suppress("unused") + fun setUploader_id(uploaderId: String?) { + this.uploaderId = uploaderId + } + + @JsonIgnore + @Suppress("unused") + fun setOrder_by(orderBy: String?) { + this.orderBy = orderBy + } + + @JsonIgnore + @Suppress("unused") + fun setCopilot_ids(copilotIds: List?) { + this.copilotIds = copilotIds + } +} diff --git a/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotRatingReq.kt b/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotRatingReq.kt new file mode 100644 index 00000000..a241bcec --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/copilot/CopilotRatingReq.kt @@ -0,0 +1,14 @@ +package plus.maa.backend.controller.request.copilot + +import jakarta.validation.constraints.NotBlank + +/** + * @author LoMu + * Date 2023-01-20 16:25 + */ +data class CopilotRatingReq( + @NotBlank(message = "评分作业id不能为空") + val id: Long, + @NotBlank(message = "评分不能为空") + val rating: String +) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetCreateReq.kt b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetCreateReq.kt new file mode 100644 index 00000000..d5348866 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetCreateReq.kt @@ -0,0 +1,33 @@ +package plus.maa.backend.controller.request.copilotset + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Size +import plus.maa.backend.common.model.CopilotSetType +import plus.maa.backend.service.model.CopilotSetStatus + +/** + * @author dragove + * create on 2024-01-01 + */ +@Schema(title = "作业集创建请求") +data class CopilotSetCreateReq( + @Schema(title = "作业集名称") + @field:NotBlank(message = "作业集名称不能为空") + val name: String, + + @Schema(title = "作业集额外描述") + val description: String = "", + + @Schema(title = "初始关联作业列表") + @field:NotNull(message = "作业id列表字段不能为null") @Size( + max = 1000, + message = "作业集作业列表最大只能为1000" + ) + override val copilotIds: MutableList, + + @Schema(title = "作业集公开状态", enumAsRef = true) + @field:NotNull(message = "作业集公开状态不能为null") + val status: CopilotSetStatus +) : CopilotSetType diff --git a/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetModCopilotsReq.kt b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetModCopilotsReq.kt new file mode 100644 index 00000000..1d3b3199 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetModCopilotsReq.kt @@ -0,0 +1,20 @@ +package plus.maa.backend.controller.request.copilotset + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotEmpty +import jakarta.validation.constraints.NotNull + +/** + * @author dragove + * create on 2024-01-02 + */ +@Schema(title = "作业集新增作业列表请求") +data class CopilotSetModCopilotsReq( + @Schema(title = "作业集id") + @field:NotNull(message = "作业集id必填") + val id: Long, + + @Schema(title = "添加/删除收藏的作业id列表") + @field:NotEmpty(message = "添加/删除作业id列表不可为空") + val copilotIds: MutableList +) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetQuery.kt b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetQuery.kt new file mode 100644 index 00000000..bdd3642d --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetQuery.kt @@ -0,0 +1,25 @@ +package plus.maa.backend.controller.request.copilotset + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Max +import jakarta.validation.constraints.Positive +import jakarta.validation.constraints.PositiveOrZero + +/** + * @author dragove + * create on 2024-01-06 + */ +@Schema(title = "作业集列表查询接口参数") +data class CopilotSetQuery ( + @Schema(title = "页码") + val page: @Positive(message = "页码必须为大于0的数字") Int = 1, + + @Schema(title = "单页数据量") + val limit: @PositiveOrZero(message = "单页数据量必须为大于等于0的数字") @Max( + value = 50, + message = "单页大小不得超过50" + ) Int = 10, + + @Schema(title = "查询关键词") + val keyword: String? = null +) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetUpdateReq.kt b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetUpdateReq.kt new file mode 100644 index 00000000..c38ba2fa --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/copilotset/CopilotSetUpdateReq.kt @@ -0,0 +1,27 @@ +package plus.maa.backend.controller.request.copilotset + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import plus.maa.backend.service.model.CopilotSetStatus + +/** + * @author dragove + * create on 2024-01-02 + */ +@Schema(title = "作业集更新请求") +data class CopilotSetUpdateReq( + @field:NotNull(message = "作业集id不能为空") + val id: Long, + + @Schema(title = "作业集名称") + @field:NotBlank(message = "作业集名称不能为空") + val name: String, + + @Schema(title = "作业集额外描述") + val description: String = "", + + @Schema(title = "作业集公开状态", enumAsRef = true) + @field:NotNull(message = "作业集公开状态不能为null") + val status: CopilotSetStatus +) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/user/LoginDTO.kt b/src/main/kotlin/plus/maa/backend/controller/request/user/LoginDTO.kt new file mode 100644 index 00000000..f81f7fe6 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/user/LoginDTO.kt @@ -0,0 +1,15 @@ +package plus.maa.backend.controller.request.user + +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.NotBlank + +/** + * @author AnselYuki + */ +data class LoginDTO( + @field:NotBlank(message = "邮箱格式错误") + @field:Email(message = "邮箱格式错误") + val email: String, + @field:NotBlank(message = "请输入用户密码") + val password: String +) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/user/PasswordResetDTO.kt b/src/main/kotlin/plus/maa/backend/controller/request/user/PasswordResetDTO.kt new file mode 100644 index 00000000..6b113ba0 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/user/PasswordResetDTO.kt @@ -0,0 +1,28 @@ +package plus.maa.backend.controller.request.user + +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.NotBlank + +/** + * 通过邮件修改密码请求 + */ +data class PasswordResetDTO( + /** + * 邮箱 + */ + @field:NotBlank(message = "邮箱格式错误") + @field:Email(message = "邮箱格式错误") + val email: String, + + /** + * 验证码 + */ + @field:NotBlank(message = "请输入验证码") + val activeCode: String, + + /** + * 修改后的密码 + */ + @field:NotBlank(message = "请输入用户密码") + val password: String +) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/user/PasswordResetVCodeDTO.kt b/src/main/kotlin/plus/maa/backend/controller/request/user/PasswordResetVCodeDTO.kt new file mode 100644 index 00000000..c573f315 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/user/PasswordResetVCodeDTO.kt @@ -0,0 +1,16 @@ +package plus.maa.backend.controller.request.user + +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.NotBlank + +/** + * 通过邮件修改密码发送验证码请求 + */ +data class PasswordResetVCodeDTO( + /** + * 邮箱 + */ + @field:NotBlank(message = "邮箱格式错误") + @field:Email(message = "邮箱格式错误") + val email: String +) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/user/PasswordUpdateDTO.kt b/src/main/kotlin/plus/maa/backend/controller/request/user/PasswordUpdateDTO.kt new file mode 100644 index 00000000..824ba8b0 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/user/PasswordUpdateDTO.kt @@ -0,0 +1,15 @@ +package plus.maa.backend.controller.request.user + +import jakarta.validation.constraints.NotBlank +import org.hibernate.validator.constraints.Length + +/** + * @author AnselYuki + */ +data class PasswordUpdateDTO( + @field:NotBlank(message = "请输入原密码") + val originalPassword: String, + @field:NotBlank(message = "密码长度必须在8-32位之间") + @field:Length(min = 8, max = 32, message = "密码长度必须在8-32位之间") + val newPassword: String +) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/user/RefreshReq.kt b/src/main/kotlin/plus/maa/backend/controller/request/user/RefreshReq.kt new file mode 100644 index 00000000..9a48f7b4 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/user/RefreshReq.kt @@ -0,0 +1,5 @@ +package plus.maa.backend.controller.request.user + +data class RefreshReq( + val refreshToken: String +) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/user/RegisterDTO.kt b/src/main/kotlin/plus/maa/backend/controller/request/user/RegisterDTO.kt new file mode 100644 index 00000000..69e1ceaa --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/user/RegisterDTO.kt @@ -0,0 +1,22 @@ +package plus.maa.backend.controller.request.user + +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.NotBlank +import org.hibernate.validator.constraints.Length + +/** + * @author AnselYuki + */ +data class RegisterDTO( + @field:NotBlank(message = "邮箱格式错误") + @field:Email(message = "邮箱格式错误") + val email: String, + @field:NotBlank(message = "用户名长度应在4-24位之间") + @field:Length(min = 4, max = 24, message = "用户名长度应在4-24位之间") + val userName: String, + @field:NotBlank(message = "密码长度必须在8-32位之间") + @field:Length(min = 8, max = 32, message = "密码长度必须在8-32位之间") + val password: String, + @field:NotBlank(message = "请输入验证码") + val registrationToken: String +) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/user/SendRegistrationTokenDTO.kt b/src/main/kotlin/plus/maa/backend/controller/request/user/SendRegistrationTokenDTO.kt new file mode 100644 index 00000000..2e5d5e85 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/user/SendRegistrationTokenDTO.kt @@ -0,0 +1,10 @@ +package plus.maa.backend.controller.request.user + +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.NotBlank + +data class SendRegistrationTokenDTO( + @field:NotBlank(message = "邮箱格式错误") + @field:Email(message = "邮箱格式错误") + val email: String +) diff --git a/src/main/kotlin/plus/maa/backend/controller/request/user/UserInfoUpdateDTO.kt b/src/main/kotlin/plus/maa/backend/controller/request/user/UserInfoUpdateDTO.kt new file mode 100644 index 00000000..8fcf21a6 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/request/user/UserInfoUpdateDTO.kt @@ -0,0 +1,13 @@ +package plus.maa.backend.controller.request.user + +import jakarta.validation.constraints.NotBlank +import org.hibernate.validator.constraints.Length + +/** + * @author AnselYuki + */ +data class UserInfoUpdateDTO( + @field:NotBlank(message = "用户名长度应在4-24位之间") + @field:Length(min = 4, max = 24, message = "用户名长度应在4-24位之间") + val userName: String +) diff --git a/src/main/kotlin/plus/maa/backend/controller/response/MaaResult.kt b/src/main/kotlin/plus/maa/backend/controller/response/MaaResult.kt new file mode 100644 index 00000000..ea3f8356 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/response/MaaResult.kt @@ -0,0 +1,28 @@ +package plus.maa.backend.controller.response + +import com.fasterxml.jackson.annotation.JsonInclude + +/** + * @author AnselYuki + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +data class MaaResult(val statusCode: Int, val message: String?, val data: T?) { + companion object { + fun success(data: T): MaaResult { + return success(null, data) + } + + fun success(): MaaResult { + return success(null, Unit) + } + + fun success(msg: String?, data: T?): MaaResult { + return MaaResult(200, msg, data) + } + + fun fail(code: Int, msg: String?): MaaResult { + return MaaResult(code, msg, null) + } + + } +} \ No newline at end of file diff --git a/src/main/kotlin/plus/maa/backend/controller/response/MaaResultException.kt b/src/main/kotlin/plus/maa/backend/controller/response/MaaResultException.kt new file mode 100644 index 00000000..2e69ed93 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/response/MaaResultException.kt @@ -0,0 +1,17 @@ +package plus.maa.backend.controller.response + +import org.springframework.http.HttpStatus +import plus.maa.backend.common.MaaStatusCode + +/** + * @author john180 + */ +class MaaResultException( + val code: Int, + val msg: String? +) : RuntimeException() { + + constructor(statusCode: MaaStatusCode) : this(statusCode.code, statusCode.message) + constructor(msg: String) : this(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg) + +} diff --git a/src/main/kotlin/plus/maa/backend/controller/response/comments/CommentsAreaInfo.kt b/src/main/kotlin/plus/maa/backend/controller/response/comments/CommentsAreaInfo.kt new file mode 100644 index 00000000..7532a8e1 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/response/comments/CommentsAreaInfo.kt @@ -0,0 +1,12 @@ +package plus.maa.backend.controller.response.comments + +/** + * @author LoMu + * Date 2023-02-19 11:47 + */ +data class CommentsAreaInfo ( + val hasNext: Boolean, + val page: Int, + val total: Long, + val data: List +) diff --git a/src/main/kotlin/plus/maa/backend/controller/response/comments/CommentsInfo.kt b/src/main/kotlin/plus/maa/backend/controller/response/comments/CommentsInfo.kt new file mode 100644 index 00000000..b7e3fb66 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/response/comments/CommentsInfo.kt @@ -0,0 +1,21 @@ +package plus.maa.backend.controller.response.comments + +import java.time.LocalDateTime + +/** + * @author LoMu + * Date 2023-02-20 17:04 + */ +data class CommentsInfo( + val commentId: String, + val uploader: String, + val uploaderId: String, + + //评论内容, + val message: String, + val uploadTime: LocalDateTime, + val like: Long = 0, + val dislike: Long = 0, + val topping: Boolean = false, + val subCommentsInfos: List, +) diff --git a/src/main/kotlin/plus/maa/backend/controller/response/comments/SubCommentsInfo.kt b/src/main/kotlin/plus/maa/backend/controller/response/comments/SubCommentsInfo.kt new file mode 100644 index 00000000..3ac2838f --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/response/comments/SubCommentsInfo.kt @@ -0,0 +1,21 @@ +package plus.maa.backend.controller.response.comments + +import java.time.LocalDateTime + +/** + * @author LoMu + * Date 2023-02-20 17:05 + */ +data class SubCommentsInfo( + val commentId: String, + val uploader: String, + val uploaderId: String, + //评论内容, + val message: String, + val uploadTime: LocalDateTime, + val like: Long = 0, + val dislike: Long = 0, + val fromCommentId: String, + val mainCommentId: String, + val deleted: Boolean = false +) diff --git a/src/main/kotlin/plus/maa/backend/controller/response/copilot/ArkLevelInfo.kt b/src/main/kotlin/plus/maa/backend/controller/response/copilot/ArkLevelInfo.kt new file mode 100644 index 00000000..bcbb2412 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/response/copilot/ArkLevelInfo.kt @@ -0,0 +1,17 @@ +package plus.maa.backend.controller.response.copilot + +import java.io.Serializable + +/** + * @author john180 + */ +data class ArkLevelInfo( + val levelId: String, + val stageId: String, + val catOne: String, + val catTwo: String, + val catThree: String, + val name: String, + val width: Int = 0, + val height: Int = 0 +) : Serializable \ No newline at end of file diff --git a/src/main/kotlin/plus/maa/backend/controller/response/copilot/CopilotInfo.kt b/src/main/kotlin/plus/maa/backend/controller/response/copilot/CopilotInfo.kt new file mode 100644 index 00000000..7bebf995 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/response/copilot/CopilotInfo.kt @@ -0,0 +1,24 @@ +package plus.maa.backend.controller.response.copilot + +import java.io.Serializable +import java.time.LocalDateTime + +data class CopilotInfo ( + val id: Long, + + val uploadTime: LocalDateTime, + val uploader: String, + + //用于前端显示的格式化后的干员信息 [干员名]::[技能] + val views: Long = 0, + val hotScore: Double = 0.0, + var available: Boolean = false, + var ratingLevel: Int = 0, + var notEnoughRating: Boolean = false, + var ratingRatio: Double = 0.0, + var ratingType: Int = 0, + val commentsCount: Long = 0, + val content: String, + val like: Long = 0, + val dislike: Long = 0 +): Serializable diff --git a/src/main/kotlin/plus/maa/backend/controller/response/copilot/CopilotPageInfo.kt b/src/main/kotlin/plus/maa/backend/controller/response/copilot/CopilotPageInfo.kt new file mode 100644 index 00000000..713212ec --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/response/copilot/CopilotPageInfo.kt @@ -0,0 +1,18 @@ +package plus.maa.backend.controller.response.copilot + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import java.io.Serializable + +/** + * @author LoMu + * Date 2022-12-27 12:39 + */ +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) +data class CopilotPageInfo( + val hasNext: Boolean, + val page: Int, + val total: Long, + val data: List +) : Serializable + diff --git a/src/main/kotlin/plus/maa/backend/controller/response/copilotset/CopilotSetListRes.kt b/src/main/kotlin/plus/maa/backend/controller/response/copilotset/CopilotSetListRes.kt new file mode 100644 index 00000000..6b3782c9 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/response/copilotset/CopilotSetListRes.kt @@ -0,0 +1,32 @@ +package plus.maa.backend.controller.response.copilotset + +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDateTime + +/** + * @author dragove + * create on 2024-01-06 + */ +@Schema(title = "作业集响应(列表)") +data class CopilotSetListRes ( + @Schema(title = "作业集id") + val id: Long, + + @Schema(title = "作业集名称") + val name: String, + + @Schema(title = "额外描述") + val description: String, + + @Schema(title = "上传者id") + val creatorId: String, + + @Schema(title = "上传者昵称") + val creator: String, + + @Schema(title = "创建时间") + val createTime: LocalDateTime, + + @Schema(title = "更新时间") + val updateTime: LocalDateTime +) diff --git a/src/main/kotlin/plus/maa/backend/controller/response/copilotset/CopilotSetPageRes.kt b/src/main/kotlin/plus/maa/backend/controller/response/copilotset/CopilotSetPageRes.kt new file mode 100644 index 00000000..063a0ddc --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/response/copilotset/CopilotSetPageRes.kt @@ -0,0 +1,22 @@ +package plus.maa.backend.controller.response.copilotset + +import io.swagger.v3.oas.annotations.media.Schema + +/** + * @author dragove + * create on 2024-01-06 + */ +@Schema(title = "作业集分页返回数据") +data class CopilotSetPageRes ( + @Schema(title = "是否有下一页") + val hasNext: Boolean = false, + + @Schema(title = "当前页码") + val page: Int = 0, + + @Schema(title = "总数据量") + val total: Long = 0, + + @Schema(title = "作业集列表") + val data: MutableList +) diff --git a/src/main/kotlin/plus/maa/backend/controller/response/copilotset/CopilotSetRes.kt b/src/main/kotlin/plus/maa/backend/controller/response/copilotset/CopilotSetRes.kt new file mode 100644 index 00000000..95cb0713 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/response/copilotset/CopilotSetRes.kt @@ -0,0 +1,39 @@ +package plus.maa.backend.controller.response.copilotset + +import io.swagger.v3.oas.annotations.media.Schema +import plus.maa.backend.service.model.CopilotSetStatus +import java.time.LocalDateTime + +/** + * @author dragove + * create on 2024-01-06 + */ +@Schema(title = "作业集响应") +data class CopilotSetRes( + @Schema(title = "作业集id") + val id: Long, + + @Schema(title = "作业集名称") + val name: String, + + @Schema(title = "额外描述") + val description: String, + + @Schema(title = "作业id列表") + val copilotIds: List, + + @Schema(title = "上传者id") + val creatorId: String, + + @Schema(title = "上传者昵称") + val creator: String, + + @Schema(title = "创建时间") + val createTime: LocalDateTime, + + @Schema(title = "更新时间") + val updateTime: LocalDateTime, + + @Schema(title = "作业状态", enumAsRef = true) + val status: CopilotSetStatus +) diff --git a/src/main/kotlin/plus/maa/backend/controller/response/user/MaaLoginRsp.kt b/src/main/kotlin/plus/maa/backend/controller/response/user/MaaLoginRsp.kt new file mode 100644 index 00000000..9e98b05f --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/response/user/MaaLoginRsp.kt @@ -0,0 +1,13 @@ +package plus.maa.backend.controller.response.user + +import java.time.Instant + +data class MaaLoginRsp( + val token: String, + val validBefore: Instant, + val validAfter: Instant, + val refreshToken: String, + val refreshTokenValidBefore: Instant, + val refreshTokenValidAfter: Instant, + val userInfo: MaaUserInfo +) diff --git a/src/main/kotlin/plus/maa/backend/controller/response/user/MaaUserInfo.kt b/src/main/kotlin/plus/maa/backend/controller/response/user/MaaUserInfo.kt new file mode 100644 index 00000000..decbb2ea --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/controller/response/user/MaaUserInfo.kt @@ -0,0 +1,14 @@ +package plus.maa.backend.controller.response.user + +import plus.maa.backend.repository.entity.MaaUser + +/** + * @author AnselYuki + */ +data class MaaUserInfo( + val id: String, + val userName: String, + val activated: Boolean = false +) { + constructor(user: MaaUser) : this(user.userId!!, user.userName, user.status == 1) +} diff --git a/src/main/kotlin/plus/maa/backend/filter/ContentLengthRepairFilter.kt b/src/main/kotlin/plus/maa/backend/filter/ContentLengthRepairFilter.kt new file mode 100644 index 00000000..dd9d9858 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/filter/ContentLengthRepairFilter.kt @@ -0,0 +1,44 @@ +package plus.maa.backend.filter + +import jakarta.servlet.FilterChain +import jakarta.servlet.ServletException +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.stereotype.Component +import org.springframework.web.filter.ShallowEtagHeaderFilter +import org.springframework.web.util.ContentCachingResponseWrapper +import java.io.IOException +import java.io.InputStream + +/** + * 解决了 GZIP 无法对 JSON 响应正常处理 min-response-size 的问题, + * 借助了 ETag 处理流程中的 Response 包装类包装所有响应, + * 从而正常获取 Content-Length + * + * @author lixuhuilll + */ +@Component +@ConditionalOnProperty(name = ["server.compression.enabled"], havingValue = "true") +class ContentLengthRepairFilter : ShallowEtagHeaderFilter() { + @Throws(ServletException::class, IOException::class) + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + if (response is ContentCachingResponseWrapper) { + // 不对已包装过的响应体做处理 + filterChain.doFilter(request, response) + } else { + super.doFilterInternal(request, response, filterChain) + } + } + + override fun isEligibleForEtag( + request: HttpServletRequest, + response: HttpServletResponse, + responseStatusCode: Int, + inputStream: InputStream + ) = false +} diff --git a/src/main/kotlin/plus/maa/backend/filter/MaaEtagHeaderFilterRegistrationBean.kt b/src/main/kotlin/plus/maa/backend/filter/MaaEtagHeaderFilterRegistrationBean.kt new file mode 100644 index 00000000..fb26e1d0 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/filter/MaaEtagHeaderFilterRegistrationBean.kt @@ -0,0 +1,60 @@ +package plus.maa.backend.filter + +import jakarta.annotation.PostConstruct +import jakarta.servlet.Filter +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.boot.web.servlet.FilterRegistrationBean +import org.springframework.http.HttpHeaders +import org.springframework.stereotype.Component +import org.springframework.web.filter.ShallowEtagHeaderFilter +import java.io.InputStream + +/** + * 提供基于 Etag 机制的 HTTP 缓存,有助于降低网络传输的压力 + * + * @author lixuhuilll + */ +@Component +class MaaEtagHeaderFilterRegistrationBean : FilterRegistrationBean() { + /** + * 配置需要使用 Etag 机制的 URI,采用 Servlet 的 URI 匹配语法 + */ + private val ETAG_URI = setOf( + "/arknights/level", + "/copilot/query" + ) + + @PostConstruct + fun init() { + filter = MaaEtagHeaderFilter() + urlPatterns = ETAG_URI + } + + private class MaaEtagHeaderFilter : ShallowEtagHeaderFilter() { + override fun initFilterBean() { + // Etag 必须使用弱校验才能与自动压缩兼容 + isWriteWeakETag = true + } + + override fun isEligibleForEtag( + request: HttpServletRequest, response: HttpServletResponse, + responseStatusCode: Int, inputStream: InputStream + ): Boolean { + if (super.isEligibleForEtag(request, response, responseStatusCode, inputStream)) { + // 使用 ETag 机制的 URI,若其响应中不存在缓存控制头,则配置默认值 + val cacheControl = response.getHeader(HttpHeaders.CACHE_CONTROL) + if (cacheControl == null) { + response.setHeader(HttpHeaders.CACHE_CONTROL, CACHE_HEAD) + } + return true + } + + return false + } + + companion object { + private const val CACHE_HEAD = "private, no-cache, max-age=0, must-revalidate" + } + } +} diff --git a/src/main/kotlin/plus/maa/backend/handler/GlobalExceptionHandler.kt b/src/main/kotlin/plus/maa/backend/handler/GlobalExceptionHandler.kt new file mode 100644 index 00000000..664958b9 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/handler/GlobalExceptionHandler.kt @@ -0,0 +1,173 @@ +package plus.maa.backend.handler + +import io.github.oshai.kotlinlogging.KotlinLogging +import jakarta.servlet.http.HttpServletRequest +import org.springframework.http.HttpStatus +import org.springframework.security.core.AuthenticationException +import org.springframework.web.HttpRequestMethodNotSupportedException +import org.springframework.web.bind.MethodArgumentNotValidException +import org.springframework.web.bind.MissingServletRequestParameterException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.ResponseBody +import org.springframework.web.bind.annotation.RestControllerAdvice +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException +import org.springframework.web.multipart.MultipartException +import org.springframework.web.server.ResponseStatusException +import org.springframework.web.servlet.NoHandlerFoundException +import plus.maa.backend.controller.response.MaaResult +import plus.maa.backend.controller.response.MaaResult.Companion.fail +import plus.maa.backend.controller.response.MaaResultException + +private val log = KotlinLogging.logger { } + +/** + * @author john180 + */ +@RestControllerAdvice +class GlobalExceptionHandler { + /** + * @return plus.maa.backend.controller.response.MaaResult + * @author FAll + * @description 请求参数缺失 + * @date 2022/12/23 12:00 + */ + @ExceptionHandler(MissingServletRequestParameterException::class) + fun missingServletRequestParameterException( + e: MissingServletRequestParameterException, + request: HttpServletRequest + ): MaaResult { + logWarn(request) + log.warn(e) { "请求参数缺失" } + return fail(400, String.format("请求参数缺失:%s", e.parameterName)) + } + + /** + * @return plus.maa.backend.controller.response.MaaResult + * @author FAll + * @description 参数类型不匹配 + * @date 2022/12/23 12:01 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException::class) + fun methodArgumentTypeMismatchException( + e: MethodArgumentTypeMismatchException, + request: HttpServletRequest + ): MaaResult { + logWarn(request) + log.warn(e) { "参数类型不匹配" } + return fail(400, String.format("参数类型不匹配:%s", e.message)) + } + + /** + * @return plus.maa.backend.controller.response.MaaResult + * @author FAll + * @description 参数校验错误 + * @date 2022/12/23 12:02 + */ + @ExceptionHandler(MethodArgumentNotValidException::class) + fun methodArgumentNotValidException(e: MethodArgumentNotValidException): MaaResult { + val fieldError = e.bindingResult.fieldError + if (fieldError != null) { + return fail(400, String.format("参数校验错误: %s", fieldError.defaultMessage)) + } + return fail(400, String.format("参数校验错误: %s", e.message)) + } + + /** + * @return plus.maa.backend.controller.response.MaaResult + * @author FAll + * @description 请求地址不存在 + * @date 2022/12/23 12:03 + */ + @ExceptionHandler(NoHandlerFoundException::class) + fun noHandlerFoundExceptionHandler(e: NoHandlerFoundException): MaaResult { + log.warn(e) { "请求地址不存在" } + return fail(404, String.format("请求地址 %s 不存在", e.requestURL)) + } + + /** + * @return plus.maa.backend.controller.response.MaaResult + * @author FAll + * @description + * @date 2022/12/23 12:04 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException::class) + fun httpRequestMethodNotSupportedExceptionHandler( + e: HttpRequestMethodNotSupportedException, + request: HttpServletRequest + ): MaaResult { + logWarn(request) + log.warn(e) { "请求方式错误" } + return fail(405, String.format("请求方法不正确:%s", e.message)) + } + + /** + * 处理由 [org.springframework.util.Assert] 工具产生的异常 + */ + @ExceptionHandler(IllegalArgumentException::class, IllegalStateException::class) + fun illegalArgumentOrStateExceptionHandler(e: RuntimeException): MaaResult { + return fail(HttpStatus.BAD_REQUEST.value(), e.message) + } + + /** + * @return plus.maa.backend.controller.response.MaaResult + * @author cbc + * @description + * @date 2022/12/26 12:00 + */ + @ExceptionHandler(MaaResultException::class) + fun maaResultExceptionHandler(e: MaaResultException): MaaResult { + return fail(e.code, e.msg) + } + + /** + * @author john180 + * @description 用户鉴权相关,异常兜底处理 + */ + @ExceptionHandler(AuthenticationException::class) + fun authExceptionHandler(e: AuthenticationException): MaaResult { + return fail(401, e.message) + } + + @ExceptionHandler(MultipartException::class) + fun fileSizeThresholdHandler(e: MultipartException): MaaResult { + return fail(413, e.message) + } + + @ExceptionHandler(ResponseStatusException::class) + fun handleResponseStatusException(ex: ResponseStatusException): MaaResult { + return fail(ex.statusCode.value(), ex.message) + } + + /** + * @return plus.maa.backend.controller.response.MaaResult + * @author john180 + * @description 服务器内部错误,异常兜底处理 + * @date 2022/12/23 12:06 + */ + @ResponseBody + @ExceptionHandler(value = [Exception::class]) + fun defaultExceptionHandler( + e: Exception, + request: HttpServletRequest + ): MaaResult<*> { + logError(request) + log.error(e) { "Exception: " } + return fail(500, "服务器内部错误") + } + + private fun logWarn(request: HttpServletRequest) { + log.warn { "Request URL: ${request.requestURL}" } + log.warn { "Request Method: ${request.method}" } + log.warn { "Request IP: ${request.remoteAddr}" } + log.warn { "Request Headers: ${request.headerNames}" } + log.warn { "Request Parameters: ${request.parameterMap}" } + } + + private fun logError(request: HttpServletRequest) { + log.error { "Request URL: ${request.requestURL}" } + log.error { "Request Method: ${request.method}" } + log.error { "Request IP: ${request.remoteAddr}" } + log.error { "Request Headers: ${request.headerNames}" } + log.error { "Request Parameters: ${request.parameterMap}" } + } +} diff --git a/src/main/kotlin/plus/maa/backend/repository/ArkLevelRepository.kt b/src/main/kotlin/plus/maa/backend/repository/ArkLevelRepository.kt new file mode 100644 index 00000000..7fc8b6ab --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/ArkLevelRepository.kt @@ -0,0 +1,51 @@ +package plus.maa.backend.repository + +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.mongodb.repository.MongoRepository +import org.springframework.data.mongodb.repository.Query +import plus.maa.backend.repository.entity.ArkLevel +import plus.maa.backend.repository.entity.ArkLevelSha + +/** + * @author john180 + */ +interface ArkLevelRepository : MongoRepository { + fun findAllShaBy(): List + + fun findAllByCatOne(catOne: String, pageable: Pageable): Page + + @Query( + """ + { + "${'$'}or": [ + {"levelId": ?0}, + {"stageId": ?0}, + {"catThree": ?0} + ] + } + + """ + ) + fun findByLevelIdFuzzy(levelId: String): List + + /** + * 用于前端查询 关卡名、关卡类型、关卡编号 + */ + @Query( + """ + { + "${'$'}or": [ + {"stageId": {'${'$'}regex': ?0 ,'${'$'}options':'i'}}, + {"catThree": {'${'$'}regex': ?0 ,'${'$'}options':'i'}}, + {"catTwo": {'${'$'}regex': ?0 ,'${'$'}options':'i'}}, + {"catOne": {'${'$'}regex': ?0 ,'${'$'}options':'i'}}, + {"name": {'${'$'}regex': ?0,'${'$'}options':'i' }} + ] + } + + """ + ) + fun queryLevelByKeyword(keyword: String): List + +} diff --git a/src/main/kotlin/plus/maa/backend/repository/CommentsAreaRepository.kt b/src/main/kotlin/plus/maa/backend/repository/CommentsAreaRepository.kt new file mode 100644 index 00000000..9857195a --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/CommentsAreaRepository.kt @@ -0,0 +1,37 @@ +package plus.maa.backend.repository + +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.mongodb.repository.MongoRepository +import org.springframework.stereotype.Repository +import plus.maa.backend.repository.entity.CommentsArea + +/** + * @author LoMu + * Date 2023-02-17 15:06 + */ +@Repository +interface CommentsAreaRepository : MongoRepository { + fun findByMainCommentId(commentsId: String): List + + fun findByCopilotIdAndDeleteAndMainCommentIdExists( + copilotId: Long, + delete: Boolean, + exists: Boolean, + pageable: Pageable + ): Page + + fun findByCopilotIdAndUploaderIdAndDeleteAndMainCommentIdExists( + copilotId: Long, + uploaderId: String, + delete: Boolean, + exists: Boolean, + pageable: Pageable + ): Page + + fun findByCopilotIdInAndDelete(copilotIds: Collection, delete: Boolean): List + + fun findByMainCommentIdIn(ids: List): List + + fun countByCopilotIdAndDelete(copilotId: Long, delete: Boolean): Long +} diff --git a/src/main/kotlin/plus/maa/backend/repository/CopilotRepository.kt b/src/main/kotlin/plus/maa/backend/repository/CopilotRepository.kt new file mode 100644 index 00000000..7d168828 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/CopilotRepository.kt @@ -0,0 +1,24 @@ +package plus.maa.backend.repository + +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.mongodb.repository.MongoRepository +import plus.maa.backend.repository.entity.Copilot + +/** + * @author LoMu + * Date 2022-12-27 10:28 + */ +interface CopilotRepository : MongoRepository { + fun findAllByDeleteIsFalse(pageable: Pageable): Page + + fun findFirstByOrderByCopilotIdDesc(): Copilot? + + fun findByCopilotIdAndDeleteIsFalse(copilotId: Long): Copilot? + + fun findByCopilotIdInAndDeleteIsFalse(copilotIds: Collection): List + + fun findByCopilotId(copilotId: Long): Copilot? + + fun existsCopilotsByCopilotId(copilotId: Long): Boolean +} diff --git a/src/main/kotlin/plus/maa/backend/repository/CopilotSetRepository.kt b/src/main/kotlin/plus/maa/backend/repository/CopilotSetRepository.kt new file mode 100644 index 00000000..6a70242c --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/CopilotSetRepository.kt @@ -0,0 +1,26 @@ +package plus.maa.backend.repository + +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.mongodb.repository.MongoRepository +import org.springframework.data.mongodb.repository.Query +import plus.maa.backend.repository.entity.CopilotSet + +/** + * @author dragove + * create on 2024-01-01 + */ +interface CopilotSetRepository : MongoRepository { + @Query( + """ + { + "${'$'}or": [ + {"name": {'${'$'}regex': ?0 ,'${'$'}options':'i'}}, + {"description": {'${'$'}regex': ?0,'${'$'}options':'i' }} + ] + } + + """ + ) + fun findByKeyword(keyword: String, pageable: Pageable): Page +} diff --git a/src/main/kotlin/plus/maa/backend/repository/GithubRepository.kt b/src/main/kotlin/plus/maa/backend/repository/GithubRepository.kt new file mode 100644 index 00000000..d1924f21 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/GithubRepository.kt @@ -0,0 +1,29 @@ +package plus.maa.backend.repository + +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestHeader +import org.springframework.web.service.annotation.GetExchange +import plus.maa.backend.repository.entity.github.GithubCommit +import plus.maa.backend.repository.entity.github.GithubContent +import plus.maa.backend.repository.entity.github.GithubTrees + +/** + * @author dragove + * created on 2022/12/23 + */ +interface GithubRepository { + /** + * api doc: [git trees api](https://docs.github.com/en/rest/git/trees?apiVersion=2022-11-28#get-a-tree) + */ + @GetExchange(value = "/repos/MaaAssistantArknights/MaaAssistantArknights/git/trees/{sha}") + fun getTrees(@RequestHeader("Authorization") token: String, @PathVariable("sha") sha: String): GithubTrees + + @GetExchange(value = "/repos/MaaAssistantArknights/MaaAssistantArknights/commits") + fun getCommits(@RequestHeader("Authorization") token: String): List + + @GetExchange(value = "/repos/MaaAssistantArknights/MaaAssistantArknights/contents/{path}") + fun getContents( + @RequestHeader("Authorization") token: String, + @PathVariable("path") path: String + ): List +} diff --git a/src/main/kotlin/plus/maa/backend/repository/RatingRepository.kt b/src/main/kotlin/plus/maa/backend/repository/RatingRepository.kt new file mode 100644 index 00000000..a46fa0e8 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/RatingRepository.kt @@ -0,0 +1,16 @@ +package plus.maa.backend.repository + +import org.springframework.data.mongodb.repository.MongoRepository +import org.springframework.stereotype.Repository +import plus.maa.backend.repository.entity.Rating + +/** + * @author lixuhuilll + * Date 2023-08-20 12:06 + */ +@Repository +interface RatingRepository : MongoRepository { + fun findByTypeAndKeyAndUserId(type: Rating.KeyType, key: String, userId: String): Rating? + +} + diff --git a/src/main/kotlin/plus/maa/backend/repository/RedisCache.kt b/src/main/kotlin/plus/maa/backend/repository/RedisCache.kt new file mode 100644 index 00000000..d0baa3b9 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/RedisCache.kt @@ -0,0 +1,378 @@ +package plus.maa.backend.repository + +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import io.github.oshai.kotlinlogging.KotlinLogging +import org.apache.commons.lang3.StringUtils +import org.springframework.beans.factory.annotation.Value +import org.springframework.core.io.ClassPathResource +import org.springframework.dao.InvalidDataAccessApiUsageException +import org.springframework.data.redis.RedisSystemException +import org.springframework.data.redis.core.ScanOptions +import org.springframework.data.redis.core.StringRedisTemplate +import org.springframework.data.redis.core.script.RedisScript +import org.springframework.scheduling.annotation.Async +import org.springframework.stereotype.Component +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean + +private val log = KotlinLogging.logger { } + +/** + * Redis工具类 + * + * @author AnselYuki + */ +@Component +class RedisCache( + @Value("\${maa-copilot.cache.default-expire}") + private val expire: Int, + private val redisTemplate: StringRedisTemplate +) { + + // 添加 JSR310 模块,以便顺利序列化 LocalDateTime 等类型 + private val writeMapper: ObjectMapper = jacksonObjectMapper() + .registerModules(JavaTimeModule()) + private val readMapper: ObjectMapper = jacksonObjectMapper() + .registerModules(JavaTimeModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + + private val supportUnlink = AtomicBoolean(true) + + /* + 使用 lua 脚本插入数据,维持 ZSet 的相对大小(size <= 实际大小 <= size + 50)以及过期时间 + 实际大小这么设计是为了避免频繁的 ZREMRANGEBYRANK 操作 + */ + private val incZSetRedisScript: RedisScript = RedisScript.of(ClassPathResource("redis-lua/incZSet.lua")) + + // 比较与输入的键值对是否相同,相同则删除 + private val removeKVIfEqualsScript: RedisScript = + RedisScript.of(ClassPathResource("redis-lua/removeKVIfEquals.lua"), Boolean::class.java) + + fun setData(key: String, value: T) { + setCache(key, value, 0, TimeUnit.SECONDS) + } + + fun setCache(key: String, value: T) { + setCache(key, value, expire.toLong(), TimeUnit.SECONDS) + } + + fun setCache(key: String, value: T, timeout: Long) { + setCache(key, value, timeout, TimeUnit.SECONDS) + } + + fun setCache(key: String, value: T, timeout: Long, timeUnit: TimeUnit) { + val json = getJson(value) ?: return + if (timeout <= 0) { + redisTemplate.opsForValue()[key] = json + } else { + redisTemplate.opsForValue()[key, json, timeout] = timeUnit + } + } + + /** + * 当缓存不存在时,则 set + * + * @param key 缓存的 key + * @param value 被缓存的值 + * @return 是否 set + */ + fun setCacheIfAbsent(key: String, value: T): Boolean { + return setCacheIfAbsent(key, value, expire.toLong()) + } + + /** + * 当缓存不存在时,则 set + * + * @param key 缓存的 key + * @param value 被缓存的值 + * @param timeout 过期时间 + * @return 是否 set + */ + fun setCacheIfAbsent(key: String, value: T, timeout: Long): Boolean { + return setCacheIfAbsent(key, value, timeout, TimeUnit.SECONDS) + } + + /** + * 当缓存不存在时,则 set + * + * @param key 缓存的 key + * @param value 被缓存的值 + * @param timeout 过期时间 + * @param timeUnit 过期时间的单位 + * @return 是否 set + */ + fun setCacheIfAbsent(key: String, value: T, timeout: Long, timeUnit: TimeUnit): Boolean { + val json = getJson(value) ?: return false + val result = if (timeout <= 0) { + java.lang.Boolean.TRUE == redisTemplate.opsForValue().setIfAbsent(key, json) + } else { + java.lang.Boolean.TRUE == redisTemplate.opsForValue().setIfAbsent(key, json, timeout, timeUnit) + } + return result + } + + fun addSet(key: String, set: Collection, timeout: Long) { + addSet(key, set, timeout, TimeUnit.SECONDS) + } + + fun addSet(key: String, set: Collection, timeout: Long, timeUnit: TimeUnit) { + if (set.isEmpty()) { + return + } + val jsonList = arrayOfNulls(set.size) + for ((i, t) in set.withIndex()) { + val json = getJson(t) ?: return + jsonList[i] = json + } + + if (timeout <= 0) { + redisTemplate.opsForSet().add(key, *jsonList) + } else { + redisTemplate.opsForSet().add(key, *jsonList) + redisTemplate.expire(key, timeout, timeUnit) + } + } + + + /** + * ZSet 中元素的 score += incScore,如果元素不存在则插入

+ * 会维持 ZSet 的相对大小(size <= 实际大小 <= size + 50)以及过期时间

+ * 当大小超出 size + 50 时,会优先删除 score 最小的元素,直到大小等于 size + * + * @param key ZSet 的 key + * @param member ZSet 的 member + * @param incScore 增加的 score + * @param size ZSet 的相对大小 + * @param timeout ZSet 的过期时间 + */ + fun incZSet(key: String, member: String?, incScore: Double, size: Long, timeout: Long) { + redisTemplate.execute( + incZSetRedisScript, + listOf(key), + member, + incScore.toString(), + size.toString(), + timeout.toString() + ) + } + + // 获取的元素是按照 score 从小到大排列的 + fun getZSet(key: String, start: Long, end: Long): Set? { + return redisTemplate.opsForZSet().range(key, start, end) + } + + // 获取的元素是按照 score 从大到小排列的 + fun getZSetReverse(key: String, start: Long, end: Long): Set? { + return redisTemplate.opsForZSet().reverseRange(key, start, end) + } + + fun valueMemberInSet(key: String, value: T): Boolean { + try { + val json = getJson(value) ?: return false + return java.lang.Boolean.TRUE == redisTemplate.opsForSet().isMember(key, json) + } catch (e: Exception) { + log.error(e) { e.message } + } + return false + } + + fun getCache(key: String, valueType: Class): T? { + return getCache(key, valueType, null, expire.toLong(), TimeUnit.SECONDS) + } + + fun getCache(key: String, valueType: Class, onMiss: (()->T)?): T? { + return getCache(key, valueType, onMiss, expire.toLong(), TimeUnit.SECONDS) + } + + fun getCache(key: String, valueType: Class, onMiss: (()->T)?, timeout: Long): T? { + return getCache(key, valueType, onMiss, timeout, TimeUnit.SECONDS) + } + + fun getCache(key: String, valueType: Class, onMiss: (()->T)?, timeout: Long, timeUnit: TimeUnit): T? { + try { + var json = redisTemplate.opsForValue()[key] + if (StringUtils.isEmpty(json)) { + if (onMiss == null){ + return null + } + //上锁 + synchronized(RedisCache::class.java) { + //再次查询缓存,目的是判断是否前面的线程已经set过了 + json = redisTemplate.opsForValue()[key] + //第二次校验缓存是否存在 + if (StringUtils.isEmpty(json)) { + val result = onMiss() + //数据库中不存在 + setCache(key, result, timeout, timeUnit) + return result + } + } + } + return readMapper.readValue(json, valueType) + } catch (e: Exception) { + log.error(e) { e.message } + return null + } + } + + fun updateCache(key: String, valueType: Class, defaultValue: T, onUpdate: (T)->T) { + updateCache(key, valueType, defaultValue, onUpdate, expire.toLong(), TimeUnit.SECONDS) + } + + fun updateCache(key: String, valueType: Class?, defaultValue: T, onUpdate: (T)->T, timeout: Long) { + updateCache(key, valueType, defaultValue, onUpdate, timeout, TimeUnit.SECONDS) + } + + fun updateCache( + key: String, + valueType: Class?, + defaultValue: T, + onUpdate: (T)->T, + timeout: Long, + timeUnit: TimeUnit + ) { + var result: T + try { + synchronized(RedisCache::class.java) { + val json = redisTemplate.opsForValue()[key] + result = if (StringUtils.isEmpty(json)) { + defaultValue + } else { + readMapper.readValue(json, valueType) + } + result = onUpdate(result) + setCache(key, result, timeout, timeUnit) + } + } catch (e: Exception) { + log.error(e) { e.message } + } + } + + var cacheLevelCommit: String? + get() = getCache("level:commit", String::class.java) + set(commit) { + setData("level:commit", commit) + } + + @JvmOverloads + fun removeCache(key: String, notUseUnlink: Boolean = false) { + removeCache(listOf(key), notUseUnlink) + } + + @JvmOverloads + fun removeCache(keys: Collection, notUseUnlink: Boolean = false) { + if (!notUseUnlink && supportUnlink.get()) { + try { + redisTemplate.unlink(keys) + return + } catch (e: InvalidDataAccessApiUsageException) { + // Redisson、Jedis、Lettuce + val cause = e.cause + if (cause == null || !StringUtils.containsAny( + cause.message, "unknown command", "not support" + ) + ) { + throw e + } + if (supportUnlink.compareAndSet(true, false)) { + log.warn { "当前连接的 Redis Service 可能不支持 Unlink 命令,切换为 Del" } + } + } catch (e: RedisSystemException) { + val cause = e.cause + if (cause == null || !StringUtils.containsAny( + cause.message, "unknown command", "not support" + ) + ) { + throw e + } + if (supportUnlink.compareAndSet(true, false)) { + log.warn { "当前连接的 Redis Service 可能不支持 Unlink 命令,切换为 Del" } + } + } + } + + // 兜底的 Del 命令 + redisTemplate.delete(keys) + } + + /** + * 相同则删除键值对 + * + * @param key 待比较和删除的键 + * @param value 待比较的值 + * @return 是否删除 + */ + fun removeKVIfEquals(key: String, value: T): Boolean { + val json = getJson(value) ?: return false + return java.lang.Boolean.TRUE == redisTemplate.execute(removeKVIfEqualsScript, listOf(key), json) + } + + /** + * 模糊删除缓存。不保证立即删除,不保证完全删除。

+ * 异步,因为 Scan 虽然不会阻塞 Redis,但客户端会阻塞 + * + * @param pattern 待删除的 Key 表达式,例如 "home:*" 表示删除 Key 以 "home:" 开头的所有缓存 + * @author Lixuhuilll + */ + @Async + fun removeCacheByPattern(pattern: String) { + syncRemoveCacheByPattern(pattern) + } + + /** + * 模糊删除缓存。不保证立即删除,不保证完全删除。

+ * 同步调用 Scan,不会长时间阻塞 Redis,但会阻塞客户端,阻塞时间视 Redis 中 key 的数量而定。 + * 删除期间,其他线程或客户端可对 Redis 进行 CURD(因为不阻塞 Redis),因此不保证删除的时机,也不保证完全删除干净 + * + * @param pattern 待删除的 Key 表达式,例如 "home:*" 表示删除 Key 以 "home:" 开头的所有缓存 + * @author Lixuhuilll + */ + fun syncRemoveCacheByPattern(pattern: String) { + // 批量删除的阈值 + val batchSize = 2000 + // 构造 ScanOptions + val scanOptions = ScanOptions.scanOptions() + .count(batchSize.toLong()) + .match(pattern) + .build() + + // 保存要删除的键 + val keysToDelete: MutableList = ArrayList(batchSize) + + redisTemplate.scan(scanOptions).use { cursor -> + while (cursor.hasNext()) { + val key = cursor.next() + // 将要删除的键添加到列表中 + keysToDelete.add(key) + + // 如果达到批量删除的阈值,则执行批量删除 + if (keysToDelete.size >= batchSize) { + removeCache(keysToDelete) + keysToDelete.clear() + } + } + } + // 删除剩余的键(不足 batchSize 的最后一批) + if (keysToDelete.isNotEmpty()) { + removeCache(keysToDelete) + } + } + + + private fun getJson(value: T): String? { + val json: String + try { + json = writeMapper.writeValueAsString(value) + } catch (e: JsonProcessingException) { + if (log.isDebugEnabled()) { + log.debug(e) { e.message } + } + return null + } + return json + } +} diff --git a/src/main/kotlin/plus/maa/backend/repository/UserRepository.kt b/src/main/kotlin/plus/maa/backend/repository/UserRepository.kt new file mode 100644 index 00000000..5325dff8 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/UserRepository.kt @@ -0,0 +1,25 @@ +package plus.maa.backend.repository + +import org.springframework.data.mongodb.repository.MongoRepository +import plus.maa.backend.repository.entity.MaaUser + +/** + * @author AnselYuki + */ +interface UserRepository : MongoRepository { + /** + * 根据邮箱(用户唯一登录凭据)查询 + * + * @param email 邮箱字段 + * @return 查询用户 + */ + fun findByEmail(email: String): MaaUser? + + fun findByUserId(userId: String): MaaUser? + +} + +fun UserRepository.findByUsersId(userId: List): Map { + return findAllById(userId).associateBy { it.userId!! } +} + diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/ArkLevel.kt b/src/main/kotlin/plus/maa/backend/repository/entity/ArkLevel.kt new file mode 100644 index 00000000..9e841118 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/ArkLevel.kt @@ -0,0 +1,50 @@ +package plus.maa.backend.repository.entity + +import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.index.Indexed +import org.springframework.data.mongodb.core.mapping.Document +import java.time.LocalDateTime + +/** + * 地图数据 + * + * @author john180 + */ +@Document("maa_level") +data class ArkLevel ( + @Id + val id: String? = null, + val levelId: String? = null, + + @Indexed + val stageId: String? = null, + + //文件版本, 用于判断是否需要更新 + val sha: String = "", + + //地图类型, 例: 主线、活动、危机合约 + var catOne: String? = null, + + //所属章节, 例: 怒号光明、寻昼行动 + var catTwo: String? = null, + + //地图ID, 例: 7-18、FC-1 + var catThree: String? = null, + + //地图名, 例: 冬逝、爱国者之死 + val name: String? = null, + val width: Int = 0, + val height: Int = 0, + + // 只是服务器认为的当前版本地图是否开放 + var isOpen: Boolean? = null, + + // 非实际意义上的活动地图关闭时间,只是服务器认为的关闭时间 + var closeTime: LocalDateTime? = null + +) { + companion object { + // 暂时这么做,有机会用和类型分别处理成功、失败以及解析器未实现的情况 + val EMPTY: ArkLevel = ArkLevel() + } +} diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/ArkLevelSha.kt b/src/main/kotlin/plus/maa/backend/repository/entity/ArkLevelSha.kt new file mode 100644 index 00000000..81df48d8 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/ArkLevelSha.kt @@ -0,0 +1,8 @@ +package plus.maa.backend.repository.entity + +/** + * @author john180 + */ +interface ArkLevelSha { + val sha: String +} diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/CollectionMeta.kt b/src/main/kotlin/plus/maa/backend/repository/entity/CollectionMeta.kt new file mode 100644 index 00000000..cc491cdf --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/CollectionMeta.kt @@ -0,0 +1,13 @@ +package plus.maa.backend.repository.entity + +import java.io.Serializable + +/** + * mongodb 集合元数据 + * + * @param 集合对应实体数据类型 + * @author dragove + * created on 2023-12-27 + */ +data class CollectionMeta(val idGetter: (T) -> Long, val incIdField: String, val entityClass: Class) : + Serializable diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/CommentsArea.kt b/src/main/kotlin/plus/maa/backend/repository/entity/CommentsArea.kt new file mode 100644 index 00000000..303e769c --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/CommentsArea.kt @@ -0,0 +1,49 @@ +package plus.maa.backend.repository.entity + +import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.index.Indexed +import org.springframework.data.mongodb.core.mapping.Document +import java.io.Serializable +import java.time.LocalDateTime + +/** + * @author LoMu + * Date 2023-02-17 14:50 + */ +@Document("maa_comments_area") +class CommentsArea( + @Id + var id: String? = null, + + @Indexed + val copilotId: Long, + + // 答复某个评论 + val fromCommentId: String? = null, + + val uploaderId: String, + + // 评论内容 + var message: String, + + var likeCount: Long = 0, + + var dislikeCount: Long = 0, + + val uploadTime: LocalDateTime = LocalDateTime.now(), + + // 是否将该评论置顶 + var topping: Boolean = false, + + var delete: Boolean = false, + + var deleteTime: LocalDateTime? = null, + + // 其主评论id(如果自身为主评论则为null) + val mainCommentId: String? = null, + + // 邮件通知 + var notification: Boolean = false +) : Serializable + + diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/Copilot.kt b/src/main/kotlin/plus/maa/backend/repository/entity/Copilot.kt new file mode 100644 index 00000000..a86bc637 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/Copilot.kt @@ -0,0 +1,189 @@ +package plus.maa.backend.repository.entity + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Transient +import org.springframework.data.mongodb.core.index.Indexed +import org.springframework.data.mongodb.core.mapping.Document +import java.io.Serializable +import java.time.LocalDateTime + +/** + * @author LoMu + * Date 2022-12-25 17:56 + */ +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) +@Document("maa_copilot") +class Copilot( + @Id + var id: String? = null, + + // 自增数字ID + @Indexed(unique = true) + var copilotId: Long? = null, + + // 关卡名 + @Indexed + var stageName: String? = null, + + // 上传者id + var uploaderId: String? = null, + + // 查看次数 + var views: Long = 0L, + + //评级 + var ratingLevel: Int = 0, + + //评级比率 十分之一代表半星 + var ratingRatio: Double = 0.0, + + var likeCount: Long = 0, + + var dislikeCount: Long = 0, + + // 热度 + var hotScore: Double = 0.0, + + // 难度 + var difficulty: Int = 0, + + // 版本号(文档中说明:最低要求 maa 版本号,必选。保留字段) + var minimumRequired: String? = null, + + // 指定干员 + var opers: List? = null, + + // 群组 + var groups: List? = null, + + // 战斗中的操作 + var actions: List? = null, + + // 描述 + var doc: Doc?, + + var firstUploadTime: LocalDateTime? = null, + var uploadTime: LocalDateTime? = null, + + // 原始数据 + var content: String? = null, + + @JsonIgnore + var delete: Boolean = false, + + @JsonIgnore + var deleteTime: LocalDateTime? = null, + + @JsonIgnore + var notification: Boolean? = null +) : Serializable { + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) + data class OperationGroup ( + // 干员名 + var name: String? = null, + + // 技能序号。可选,默认 1 + var skill: Int = 1, + + // 技能用法。可选,默认 0 + var skillUsage: Int = 0 + + ): Serializable + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) + data class Operators ( + // 干员名 + var name: String? = null, + + // 技能序号。可选,默认 1 + var skill: Int = 1, + + // 技能用法。可选,默认 0 + var skillUsage: Int = 0, + var requirements: Requirements = Requirements() + ): Serializable { + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) + data class Requirements ( + // 精英化等级。可选,默认为 0, 不要求精英化等级 + var elite: Int = 0, + + // 干员等级。可选,默认为 0 + var level: Int = 0, + + // 技能等级。可选,默认为 0 + var skillLevel: Int = 0, + + // 模组编号。可选,默认为 0 + var module: Int = 0, + + // 潜能要求。可选,默认为 0 + var potentiality: Int = 0 + ): Serializable + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) + data class Groups( + // 群组名 + var name: String? = null, + + val opers: List? = null, + var operators: List? = null + ) : Serializable + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) + data class Action( + // 操作类型,可选,默认 "Deploy" + var type: String? = "Deploy", + var kills: Int? = 0, + var costs: Int? = 0, + var costChanges: Int? = 0, + + // 默认 -1 + var cooling: Int? = -1, + + var name: String? = null, + + // 部署干员的位置。 + var location: Array?, + + // 部署干员的干员朝向 中英文皆可 + var direction: String? = "None", + + // 修改技能用法。当 type 为 "技能用法" 时必选 + var skillUsage: Int? = 0, + + // 前置延时 + var preDelay: Int? = 0, + + // 后置延时 + var postDelay: Int? = 0, + + // maa:保留字段,暂未实现 + var timeout: Int? = 0, + + // 描述 + var doc: String? = "", + var docColor: String? = "Gray" + ) : Serializable + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) + data class Doc( + var title: String, + var titleColor: String? = "Gray", + var details: String? = "", + var detailsColor: String? = "Gray" + ) : Serializable + + companion object { + @Transient + val META: CollectionMeta = CollectionMeta( + { obj: Copilot -> obj.copilotId!! }, + "copilotId", Copilot::class.java + ) + } +} diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/CopilotSet.kt b/src/main/kotlin/plus/maa/backend/repository/entity/CopilotSet.kt new file mode 100644 index 00000000..8980d296 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/CopilotSet.kt @@ -0,0 +1,78 @@ +package plus.maa.backend.repository.entity + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Transient +import org.springframework.data.mongodb.core.mapping.Document +import plus.maa.backend.common.model.CopilotSetType +import plus.maa.backend.service.model.CopilotSetStatus +import java.io.Serializable +import java.time.LocalDateTime + +/** + * 作业集数据 + */ +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) +@Document("maa_copilot_set") +data class CopilotSet( + /** + * 作业集id + */ + @field:Id + val id: Long = 0, + + /** + * 作业集名称 + */ + var name: String, + + /** + * 额外描述 + */ + var description: String, + + /** + * 作业id列表 + * 使用 list 保证有序 + * 作业添加时应当保证唯一 + */ + override var copilotIds: MutableList, + + /** + * 上传者id + */ + val creatorId: String, + + /** + * 创建时间 + */ + val createTime: LocalDateTime, + + /** + * 更新时间 + */ + var updateTime: LocalDateTime, + + /** + * 作业状态 + * [plus.maa.backend.service.model.CopilotSetStatus] + */ + var status: CopilotSetStatus, + + @field:JsonIgnore + var delete: Boolean = false, + + @field:JsonIgnore + var deleteTime: LocalDateTime? = null + +) : Serializable, CopilotSetType { + companion object { + @field:Transient + val meta = CollectionMeta( + { obj: CopilotSet -> obj.id }, + "id", CopilotSet::class.java + ) + } +} diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/MaaUser.kt b/src/main/kotlin/plus/maa/backend/repository/entity/MaaUser.kt new file mode 100644 index 00000000..0cfb032b --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/MaaUser.kt @@ -0,0 +1,43 @@ +package plus.maa.backend.repository.entity + +import com.fasterxml.jackson.annotation.JsonInclude +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Transient +import org.springframework.data.mongodb.core.index.Indexed +import org.springframework.data.mongodb.core.mapping.Document +import plus.maa.backend.controller.request.user.UserInfoUpdateDTO +import java.io.Serializable + +/** + * @author AnselYuki + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@Document("maa_user") +data class MaaUser( + @Id + val userId: String? = null, + var userName: String, + + @Indexed(unique = true) + val email: String, + var password: String, + var status: Int = 0, + var refreshJwtIds: MutableList = ArrayList() +) : Serializable { + + fun updateAttribute(updateDTO: UserInfoUpdateDTO) { + val userName = updateDTO.userName + if (userName.isNotBlank()) { + this.userName = userName + } + } + + companion object { + @Transient + val UNKNOWN: MaaUser = MaaUser( + userName = "未知用户:(", + email = "unknown@unkown.unkown", + password = "unknown" + ) + } +} diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/Rating.kt b/src/main/kotlin/plus/maa/backend/repository/entity/Rating.kt new file mode 100644 index 00000000..2e977f84 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/Rating.kt @@ -0,0 +1,31 @@ +package plus.maa.backend.repository.entity + +import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.index.CompoundIndex +import org.springframework.data.mongodb.core.index.CompoundIndexes +import org.springframework.data.mongodb.core.mapping.Document +import plus.maa.backend.service.model.RatingType +import java.time.LocalDateTime + +/** + * @author lixuhuilll + * Date 2023-08-20 11:20 + */ +@Document(collection = "maa_rating") // 复合索引 +@CompoundIndexes(CompoundIndex(name = "idx_rating", def = "{'type': 1, 'key': 1, 'userId': 1}", unique = true)) +data class Rating( + @Id + val id: String? = null, + + // 下面三个字段组成复合索引,一个用户对一个对象只能有一种评级 + val type: KeyType, // 评级的类型,如作业(copilot)、评论(comment) + val key: String, // 被评级对象的唯一标识,如作业id、评论id + val userId: String, // 评级的用户id + + var rating: RatingType, // 评级,如 "Like"、"Dislike"、"None" + var rateTime: LocalDateTime // 评级时间 +) { + enum class KeyType { + COPILOT, COMMENT + } +} diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkActivity.kt b/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkActivity.kt new file mode 100644 index 00000000..1f65c8b0 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkActivity.kt @@ -0,0 +1,7 @@ +package plus.maa.backend.repository.entity.gamedata + + +data class ArkActivity( + val id: String, + val name: String +) diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkCharacter.kt b/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkCharacter.kt new file mode 100644 index 00000000..8457fdd9 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkCharacter.kt @@ -0,0 +1,9 @@ +package plus.maa.backend.repository.entity.gamedata + +data class ArkCharacter ( + val name: String, + val profession: String, + val rarity: Int +) { + var id: String? = null +} diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkCrisisV2Info.kt b/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkCrisisV2Info.kt new file mode 100644 index 00000000..429634d6 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkCrisisV2Info.kt @@ -0,0 +1,10 @@ +package plus.maa.backend.repository.entity.gamedata + +data class ArkCrisisV2Info ( + val seasonId: String, + val name: String, + + // 时间戳,单位:秒 + val startTs: Long, + val endTs: Long +) diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkStage.kt b/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkStage.kt new file mode 100644 index 00000000..4d29f519 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkStage.kt @@ -0,0 +1,24 @@ +package plus.maa.backend.repository.entity.gamedata + +data class ArkStage( + /** + * 关卡ID, 需转换为全小写后使用

+ * 例: Activities/ACT5D0/level_act5d0_ex08 + */ + val levelId: String?, + + /** + * 例: act14d7_zone2 + */ + val zoneId: String, + + /** + * 例: act5d0_ex08 + */ + val stageId: String, + + /** + * 例: CB-EX8 + */ + val code: String +) \ No newline at end of file diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkTilePos.kt b/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkTilePos.kt new file mode 100644 index 00000000..434755ce --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkTilePos.kt @@ -0,0 +1,31 @@ +package plus.maa.backend.repository.entity.gamedata + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.LowerCamelCaseStrategy +import com.fasterxml.jackson.databind.annotation.JsonNaming + +/** + * 地图格子数据 + * + * @author dragove + * created on 2022/12/23 + */ +@JsonNaming(LowerCamelCaseStrategy::class) +data class ArkTilePos( + val code: String? = null, + val height: Int = 0, + val width: Int = 0, + val levelId: String? = null, + val name: String? = null, + val stageId: String? = null, + val tiles: List>? = null, + val view: List>? = null, +) { + @JsonNaming(LowerCamelCaseStrategy::class) + data class Tile( + val tileKey: String? = null, + val heightType: Int? = null, + val buildableType: Int? = null, + val isStart: Boolean? = null, + val isEnd: Boolean? = null, + ) +} diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkTower.kt b/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkTower.kt new file mode 100644 index 00000000..4842e555 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkTower.kt @@ -0,0 +1,7 @@ +package plus.maa.backend.repository.entity.gamedata + +data class ArkTower( + val id: String, + val name: String, + val subName: String +) diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkZone.kt b/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkZone.kt new file mode 100644 index 00000000..a58c0848 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/ArkZone.kt @@ -0,0 +1,18 @@ +package plus.maa.backend.repository.entity.gamedata + +data class ArkZone( + /** + * 例: main_1 + */ + val zoneID: String, + + /** + * 例: 第一章 + */ + val zoneNameFirst: String?, + + /** + * 例: 黑暗时代·下 + */ + val zoneNameSecond: String? +) diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/MaaArkStage.kt b/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/MaaArkStage.kt new file mode 100644 index 00000000..262a0e20 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/gamedata/MaaArkStage.kt @@ -0,0 +1,17 @@ +package plus.maa.backend.repository.entity.gamedata + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +// 忽略对服务器无用的数据 +@JsonIgnoreProperties(ignoreUnknown = true) +data class MaaArkStage( + /** + * 例: CB-EX8 + */ + val code: String, + + /** + * 例: act5d0_ex08 + */ + val stageId: String? +) diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/github/GithubCommit.kt b/src/main/kotlin/plus/maa/backend/repository/entity/github/GithubCommit.kt new file mode 100644 index 00000000..10110ddf --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/github/GithubCommit.kt @@ -0,0 +1,8 @@ +package plus.maa.backend.repository.entity.github + +/** + * @author john180 + */ +data class GithubCommit( + val sha: String +) diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/github/GithubContent.kt b/src/main/kotlin/plus/maa/backend/repository/entity/github/GithubContent.kt new file mode 100644 index 00000000..b0ace716 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/github/GithubContent.kt @@ -0,0 +1,37 @@ +package plus.maa.backend.repository.entity.github + +/** + * @author dragove + * created on 2022/12/23 + */ +data class GithubContent( + // 文件名 + val name: String, + + // 路径 + val path: String, + val sha: String, + + // 文件大小(Byte) + val size: Long, + + // 路径类型 file-文件 dir-目录 + val type: String, + + // 下载地址 + val downloadUrl: String?, + + // 访问地址 + val htmlUrl: String, + + // 对应commit地址 + val gitUrl: String +) { + val isFile: Boolean + /** + * 仿照File类,判断是否问类型 + * + * @return 如果是文件类型,则返回 true,目录类型则返回 false + */ + get() = type == "file" +} diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/github/GithubTree.kt b/src/main/kotlin/plus/maa/backend/repository/entity/github/GithubTree.kt new file mode 100644 index 00000000..97595811 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/github/GithubTree.kt @@ -0,0 +1,12 @@ +package plus.maa.backend.repository.entity.github + +/** + * @author john180 + */ +data class GithubTree ( + val path: String, + val mode: String, + val type: String, + val sha: String, + val url: String? +) diff --git a/src/main/kotlin/plus/maa/backend/repository/entity/github/GithubTrees.kt b/src/main/kotlin/plus/maa/backend/repository/entity/github/GithubTrees.kt new file mode 100644 index 00000000..2b3047df --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/repository/entity/github/GithubTrees.kt @@ -0,0 +1,10 @@ +package plus.maa.backend.repository.entity.github + +/** + * @author john180 + */ +class GithubTrees ( + val sha: String, + val url: String, + val tree: List, +) diff --git a/src/main/kotlin/plus/maa/backend/service/ArkGameDataService.kt b/src/main/kotlin/plus/maa/backend/service/ArkGameDataService.kt new file mode 100644 index 00000000..2130d676 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/ArkGameDataService.kt @@ -0,0 +1,273 @@ +package plus.maa.backend.service + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import io.github.oshai.kotlinlogging.KotlinLogging +import okhttp3.OkHttpClient +import okhttp3.Request +import org.springframework.stereotype.Service +import plus.maa.backend.common.utils.ArkLevelUtil +import plus.maa.backend.repository.entity.gamedata.* +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +private val log = KotlinLogging.logger {} + +/** + * @author john180 + */ +@Service +class ArkGameDataService(private val okHttpClient: OkHttpClient) { + + companion object { + private const val ARK_STAGE = + "https://raw.githubusercontent.com/yuanyan3060/ArknightsGameResource/main/gamedata/excel/stage_table.json" + private const val ARK_ZONE = + "https://raw.githubusercontent.com/yuanyan3060/ArknightsGameResource/main/gamedata/excel/zone_table.json" + private const val ARK_ACTIVITY = + "https://raw.githubusercontent.com/yuanyan3060/ArknightsGameResource/main/gamedata/excel/activity_table.json" + private const val ARK_CHARACTER = + "https://raw.githubusercontent.com/yuanyan3060/ArknightsGameResource/main/gamedata/excel/character_table.json" + private const val ARK_TOWER = + "https://raw.githubusercontent.com/yuanyan3060/ArknightsGameResource/main/gamedata/excel/climb_tower_table.json" + private const val ARK_CRISIS_V2 = + "https://raw.githubusercontent.com/yuanyan3060/ArknightsGameResource/main/gamedata/excel/crisis_v2_table.json" + } + + private final val mapper = jacksonObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + private val stageMap = ConcurrentHashMap() + private val levelStageMap = ConcurrentHashMap() + private val zoneMap = ConcurrentHashMap() + private val zoneActivityMap = ConcurrentHashMap() + private val arkCharacterMap = ConcurrentHashMap() + private val arkTowerMap = ConcurrentHashMap() + private val arkCrisisV2InfoMap = ConcurrentHashMap() + + fun syncGameData(): Boolean { + return syncStage() && + syncZone() && + syncActivity() && + syncCharacter() && + syncTower() && + syncCrisisV2Info() + } + + fun findStage(levelId: String, code: String, stageId: String): ArkStage? { + val stage = levelStageMap[levelId.lowercase(Locale.getDefault())] + if (stage != null && stage.code.equals(code, ignoreCase = true)) { + return stage + } + return stageMap[stageId] + } + + fun findZone(levelId: String, code: String, stageId: String): ArkZone? { + val stage = findStage(levelId, code, stageId) + if (stage == null) { + log.error { "[DATA]stage不存在:$stageId, Level: $levelId" } + return null + } + val zone = zoneMap[stage.zoneId] + if (zone == null) { + log.error { "[DATA]zone不存在:${stage.zoneId}, Level: $levelId" } + } + return zone + } + + fun findTower(zoneId: String) = arkTowerMap[zoneId] + + + fun findCharacter(characterId: String): ArkCharacter? { + val ids = characterId.split("_") + return arkCharacterMap[ids[ids.size - 1]] + } + + fun findActivityByZoneId(zoneId: String) = zoneActivityMap[zoneId] + + /** + * 通过 stageId 或者 seasonId 提取危机合约信息 + * + * @param id stageId 或者 seasonId + * @return 危机合约信息,包含合约名、开始时间、结束时间等 + */ + fun findCrisisV2InfoById(id: String?) = findCrisisV2InfoByKeyInfo(ArkLevelUtil.getKeyInfoById(id)) + + /** + * 通过地图系列的唯一标识提取危机合约信息 + * + * @param keyInfo 地图系列的唯一标识 + * @return 危机合约信息,包含合约名、开始时间、结束时间等 + */ + fun findCrisisV2InfoByKeyInfo(keyInfo: String) = arkCrisisV2InfoMap[keyInfo] + + private fun syncStage(): Boolean { + val req = Request.Builder().url(ARK_STAGE).get().build() + try { + okHttpClient.newCall(req).execute().use { rsp -> + val body = rsp.body + if (body == null) { + log.error { "[DATA]获取stage数据失败" } + return false + } + val node = mapper.reader().readTree(body.string()) + val stagesNode = node.get("stages") + val temp = mapper.convertValue(stagesNode, object : TypeReference>() {}) + stageMap.clear() + levelStageMap.clear() + temp.forEach { (k, v) -> + stageMap[k] = v + v.levelId?.let { + levelStageMap[it.lowercase(Locale.getDefault())] = v + } + } + log.info { "[DATA]获取stage数据成功, 共${levelStageMap.size}条" } + } + } catch (e: Exception) { + log.error(e) { "[DATA]同步stage数据异常" } + return false + } + return true + } + + private fun syncZone(): Boolean { + val req = Request.Builder().url(ARK_ZONE).get().build() + try { + okHttpClient.newCall(req).execute().use { rsp -> + val body = rsp.body + if (body == null) { + log.error { "[DATA]获取zone数据失败" } + return false + } + val node = mapper.reader().readTree(body.string()) + val zonesNode = node.get("zones") + val temp = mapper.convertValue(zonesNode, object : TypeReference>() {}) + zoneMap.clear() + zoneMap.putAll(temp) + log.info { "[DATA]获取zone数据成功, 共${zoneMap.size}条" } + } + } catch (e: Exception) { + log.error(e) { "[DATA]同步zone数据异常" } + return false + } + + return true + } + + private fun syncActivity(): Boolean { + val req = Request.Builder().url(ARK_ACTIVITY).get().build() + try { + okHttpClient.newCall(req).execute().use { rsp -> + val body = rsp.body + if (body == null) { + log.error { "[DATA]获取activity数据失败" } + return false + } + val node = mapper.reader().readTree(body.string()) + val zonesNode = node.get("zoneToActivity") + val zoneToActivity = mapper.convertValue(zonesNode, object : TypeReference>() {}) + val baseInfoNode = node.get("basicInfo") + val baseInfos = mapper.convertValue(baseInfoNode, object : TypeReference>() {}) + val temp = ConcurrentHashMap() + zoneToActivity.forEach { (zoneId, actId) -> + val act = baseInfos[actId] + act?.let { + temp[zoneId] = it + } + } + zoneActivityMap.clear() + zoneActivityMap.putAll(temp) + log.info { "[DATA]获取activity数据成功, 共${zoneActivityMap.size}条" } + } + } catch (e: Exception) { + log.error(e) { "[DATA]同步activity数据异常" } + return false + } + + return true + } + + private fun syncCharacter(): Boolean { + val req = Request.Builder().url(ARK_CHARACTER).get().build() + try { + okHttpClient.newCall(req).execute().use { rsp -> + val body = rsp.body + if (body == null) { + log.error { "[DATA]获取character数据失败" } + return false + } + val node = mapper.reader().readTree(body.string()) + val characters = mapper.convertValue(node, object : TypeReference>() {}) + characters.forEach { (id, c) -> c.id = id } + arkCharacterMap.clear() + characters.values.forEach { c -> + if (c.id.isNullOrBlank()) { + return@forEach + } + val ids = c.id!!.split("_") + if (ids.size != 3) { + // 不是干员 + return@forEach + } + arkCharacterMap[ids[2]] = c + } + log.info { "[DATA]获取character数据成功, 共${arkCharacterMap.size}条" } + } + } catch (e: Exception) { + log.error(e) { "[DATA]同步character数据异常" } + return false + } + + return true + } + + private fun syncTower(): Boolean { + val req = Request.Builder().url(ARK_TOWER).get().build() + try { + okHttpClient.newCall(req).execute().use { rsp -> + val body = rsp.body + if (body == null) { + log.error { "[DATA]获取tower数据失败" } + return false + } + val node = mapper.reader().readTree(body.string()) + val towerNode = node.get("towers") + arkTowerMap.clear() + arkTowerMap.putAll(mapper.convertValue(towerNode, object : TypeReference>() {})) + log.info { "[DATA]获取tower数据成功, 共${arkTowerMap.size}条" } + } + } catch (e: Exception) { + log.error(e) { "[DATA]同步tower数据异常" } + return false + } + + return true + } + + fun syncCrisisV2Info(): Boolean { + val req = Request.Builder().url(ARK_CRISIS_V2).get().build() + try { + okHttpClient.newCall(req).execute().use { rsp -> + val body = rsp.body + if (body == null) { + log.error { "[DATA]获取crisisV2Info数据失败" } + return false + } + val node = mapper.reader().readTree(body.string()) + val crisisV2InfoNode = node.get("seasonInfoDataMap") + val crisisV2InfoMap = + mapper.convertValue(crisisV2InfoNode, object : TypeReference>() {}) + val temp = ConcurrentHashMap() + crisisV2InfoMap.forEach { (k, v) -> temp[ArkLevelUtil.getKeyInfoById(k)] = v } + arkCrisisV2InfoMap.clear() + arkCrisisV2InfoMap.putAll(temp) + log.info { "[DATA]获取crisisV2Info数据成功, 共${arkCrisisV2InfoMap.size}条" } + } + } catch (e: Exception) { + log.error(e) { "[DATA]同步crisisV2Info数据异常" } + return false + } + + return true + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/ArkLevelParserService.kt b/src/main/kotlin/plus/maa/backend/service/ArkLevelParserService.kt new file mode 100644 index 00000000..40eeeb1b --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/ArkLevelParserService.kt @@ -0,0 +1,52 @@ +package plus.maa.backend.service + +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.lang.Nullable +import org.springframework.stereotype.Service +import plus.maa.backend.repository.entity.ArkLevel +import plus.maa.backend.repository.entity.gamedata.ArkTilePos +import plus.maa.backend.service.model.ArkLevelType +import plus.maa.backend.service.model.parser.ArkLevelParser + +private val log = KotlinLogging.logger { } + +/** + * @author john180 + */ +@Service +class ArkLevelParserService(private val parsers: List) { + + /** + * 具体地图信息生成规则见 + * [GameDataParser](https://github.com/MaaAssistantArknights/MaaCopilotServer/blob/main/src/MaaCopilotServer.GameData/GameDataParser.cs) + * 尚未全部实现

+ * TODO 完成剩余字段实现 + */ + @Nullable + fun parseLevel(tilePos: ArkTilePos, sha: String): ArkLevel? { + val level = ArkLevel( + levelId = tilePos.levelId, + stageId = tilePos.stageId, + sha = sha, + catThree = tilePos.code, + name = tilePos.name, + width = tilePos.width, + height = tilePos.height, + ) + return parseLevel(level, tilePos) + } + + private fun parseLevel(level: ArkLevel, tilePos: ArkTilePos): ArkLevel? { + val type = ArkLevelType.fromLevelId(level.levelId) + if (ArkLevelType.UNKNOWN == type) { + log.warn { "[PARSER]未知关卡类型:${level.levelId}" } + return null + } + val parser = parsers.firstOrNull { it.supportType(type) } + if (parser == null) { + //类型存在但无对应Parser直接跳过 + return ArkLevel.EMPTY + } + return parser.parseLevel(level, tilePos) + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/ArkLevelService.kt b/src/main/kotlin/plus/maa/backend/service/ArkLevelService.kt new file mode 100644 index 00000000..fbec4887 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/ArkLevelService.kt @@ -0,0 +1,372 @@ +package plus.maa.backend.service + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import io.github.oshai.kotlinlogging.KotlinLogging +import okhttp3.* +import org.springframework.beans.factory.annotation.Value +import org.springframework.cache.annotation.Cacheable +import org.springframework.data.domain.Pageable +import org.springframework.scheduling.annotation.Async +import org.springframework.stereotype.Service +import plus.maa.backend.common.utils.ArkLevelUtil +import plus.maa.backend.common.utils.converter.ArkLevelConverter +import plus.maa.backend.controller.response.copilot.ArkLevelInfo +import plus.maa.backend.repository.ArkLevelRepository +import plus.maa.backend.repository.GithubRepository +import plus.maa.backend.repository.RedisCache +import plus.maa.backend.repository.entity.ArkLevel +import plus.maa.backend.repository.entity.ArkLevelSha +import plus.maa.backend.repository.entity.gamedata.ArkTilePos +import plus.maa.backend.repository.entity.gamedata.MaaArkStage +import plus.maa.backend.repository.entity.github.GithubContent +import plus.maa.backend.repository.entity.github.GithubTree +import plus.maa.backend.repository.entity.github.GithubTrees +import plus.maa.backend.service.model.ArkLevelType +import java.io.IOException +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.util.* +import java.util.concurrent.atomic.AtomicInteger + +private val log = KotlinLogging.logger { } + +/** + * @author dragove + * created on 2022/12/23 + */ +@Service +class ArkLevelService( + /** + * GitHub api调用token 从 [tokens](https://github.com/settings/tokens) 获取 + */ + @Value("\${maa-copilot.github.token:}") + private val githubToken: String, + /** + * maa 主仓库,一般不变 + */ + @Value("\${maa-copilot.github.repo:MaaAssistantArknights/MaaAssistantArknights/dev}") + private val maaRepoAndBranch: String, + /** + * 地图数据所在路径 + */ + @Value("\${maa-copilot.github.repo.tile.path:resource/Arknights-Tile-Pos}") + private val tilePosPath: String, + + private val githubRepo: GithubRepository, + private val redisCache: RedisCache, + private val arkLevelRepo: ArkLevelRepository, + private val parserService: ArkLevelParserService, + private val gameDataService: ArkGameDataService, + private val mapper: ObjectMapper, + private val okHttpClient: OkHttpClient, + private val arkLevelConverter: ArkLevelConverter +) { + private val bypassFileNames = listOf("overview.json") + + @get:Cacheable("arkLevelInfos") + val arkLevelInfos: List + get() = arkLevelRepo.findAll() + .map { arkLevel -> arkLevelConverter.convert(arkLevel) } + .toList() + + @Cacheable("arkLevel") + fun findByLevelIdFuzzy(levelId: String): ArkLevel? { + return arkLevelRepo.findByLevelIdFuzzy(levelId).firstOrNull() + } + + fun queryLevelInfosByKeyword(keyword: String): List { + val levels = arkLevelRepo.queryLevelByKeyword(keyword).toList() + return arkLevelConverter.convert(levels) + } + + /** + * 地图数据更新任务 + */ + @Async + fun runSyncLevelDataTask() { + log.info { "[LEVEL]开始同步地图数据" } + //获取地图文件夹最新的commit, 用于判断是否需要更新 + val commits = githubRepo.getCommits(githubToken) + if (commits.isEmpty()) { + log.info { "[LEVEL]获取地图数据最新commit失败" } + return + } + //与缓存的commit比较,如果相同则不更新 + val commit = commits[0] + val lastCommit = redisCache.cacheLevelCommit + if (lastCommit != null && lastCommit == commit.sha) { + log.info { "[LEVEL]地图数据已是最新" } + return + } + //获取根目录文件列表 + var trees: GithubTrees? + val files = tilePosPath.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray().toList() + trees = githubRepo.getTrees(githubToken, commit.sha) + //根据路径获取文件列表 + for (file in files) { + if (trees == null || trees.tree.isEmpty()) { + log.info { "[LEVEL]地图数据获取失败" } + return + } + val tree = trees.tree.firstOrNull { t: GithubTree -> t.path == file && t.type == "tree" } + if (tree == null) { + log.info { "[LEVEL]地图数据获取失败, 未找到文件夹$file" } + return + } + trees = githubRepo.getTrees(githubToken, tree.sha) + } + if (trees == null || trees.tree.isEmpty()) { + log.info { "[LEVEL]地图数据获取失败, 未找到文件夹$tilePosPath" } + return + } + //根据后缀筛选地图文件列表 + val levelTrees = trees.tree + .filter { t: GithubTree -> t.type == "blob" && t.path.endsWith(".json") } + .toMutableList() + log.info { "[LEVEL]已发现${levelTrees.size}份地图数据" } + + //根据sha筛选无需更新的地图 + val shaList = arkLevelRepo.findAllShaBy().map { obj: ArkLevelSha -> obj.sha }.toList() + levelTrees.removeIf { t: GithubTree -> shaList.contains(t.sha) } + // 排除overview文件、肉鸽、训练关卡和 Guide? 不知道是啥 + levelTrees.removeIf { t: GithubTree -> + t.path == "overview.json" || + t.path.contains("roguelike") || + t.path.startsWith("tr_") || + t.path.startsWith("guide_") + } + levelTrees.removeIf { t: GithubTree -> t.path.contains("roguelike") } + log.info { "[LEVEL]${levelTrees.size}份地图数据需要更新" } + if (levelTrees.isEmpty()) { + return + } + //同步GameData仓库数据 + if (!gameDataService.syncGameData()) { + log.error { "[LEVEL]GameData仓库数据同步失败" } + return + } + + val task = DownloadTask(total = levelTrees.size, finishCallback = { t: DownloadTask -> + //仅在全部下载任务成功后更新commit缓存 + if (t.isAllSuccess) { + redisCache.cacheLevelCommit = commit.sha + } + }) + levelTrees.forEach { tree -> download(task, tree) } + } + + /** + * 更新活动地图开放状态 + */ + fun updateActivitiesOpenStatus() { + log.info { "[ACTIVITIES-OPEN-STATUS]准备更新活动地图开放状态" } + val stages = githubRepo.getContents(githubToken, "resource").firstOrNull { content: GithubContent -> content.isFile && "stages.json" == content.name } + + if (stages == null) { + log.info { "[ACTIVITIES-OPEN-STATUS]活动地图开放状态数据不存在" } + return + } + + val lastStagesSha = redisCache.getCache("level:stages:sha", String::class.java) + if (lastStagesSha != null && lastStagesSha == stages.sha) { + log.info { "[ACTIVITIES-OPEN-STATUS]活动地图开放状态已是最新" } + return + } + + log.info { "[ACTIVITIES-OPEN-STATUS]开始更新活动地图开放状态" } + // 就一个文件,直接在当前线程下载数据 + try { + if (stages.downloadUrl == null) { + return + } + okHttpClient + .newCall(Request.Builder().url(stages.downloadUrl).build()) + .execute().use { response -> + if (!response.isSuccessful || response.body == null) { + log.error { "[ACTIVITIES-OPEN-STATUS]活动地图开放状态下载失败" } + return + } + val body = response.body!!.byteStream() + val stagesList: List = + mapper.readValue(body, object : TypeReference>() { + }) + + val keyInfos = stagesList + .map { it.stageId } // 提取地图系列的唯一标识 + .map { id: String? -> ArkLevelUtil.getKeyInfoById(id) } + .toSet() + + // 修改活动地图 + val catOne = ArkLevelType.ACTIVITIES.display + // 分页修改 + var pageable = Pageable.ofSize(1000) + var arkLevelPage = arkLevelRepo.findAllByCatOne(catOne, pageable) + + // 获取当前时间 + val nowTime = LocalDateTime.now() + + while (arkLevelPage.hasContent()) { + arkLevelPage.forEach{ arkLevel: ArkLevel -> + // 只考虑地图系列的唯一标识 + if (keyInfos.contains(ArkLevelUtil.getKeyInfoById(arkLevel.stageId))) { + arkLevel.isOpen = true + // 如果一个旧地图重新开放,关闭时间也需要另算 + arkLevel.closeTime = null + } else if (arkLevel.isOpen != null) { + // 数据可能存在部分缺失,因此地图此前必须被匹配过,才会认为其关闭 + arkLevel.isOpen = false + // 不能每天都变更关闭时间 + if (arkLevel.closeTime == null) { + arkLevel.closeTime = nowTime + } + } + } + + arkLevelRepo.saveAll(arkLevelPage) + + if (!arkLevelPage.hasNext()) { + // 没有下一页了,跳出循环 + break + } + pageable = arkLevelPage.nextPageable() + arkLevelPage = arkLevelRepo.findAllByCatOne(catOne, pageable) + } + + redisCache.setData("level:stages:sha", stages.sha) + log.info { "[ACTIVITIES-OPEN-STATUS]活动地图开放状态更新完成" } + } + } catch (e: Exception) { + log.error(e) { "[ACTIVITIES-OPEN-STATUS]活动地图开放状态更新失败" } + } + } + + fun updateCrisisV2OpenStatus() { + log.info { "[CRISIS-V2-OPEN-STATUS]准备更新危机合约开放状态" } + // 同步危机合约信息 + if (!gameDataService.syncCrisisV2Info()) { + log.error { "[CRISIS-V2-OPEN-STATUS]同步危机合约信息失败" } + return + } + + val catOne = ArkLevelType.RUNE.display + // 分页修改 + var pageable = Pageable.ofSize(1000) + var arkCrisisV2Page = arkLevelRepo.findAllByCatOne(catOne, pageable) + + // 获取当前时间 + val nowInstant = Instant.now() + val nowTime = LocalDateTime.ofInstant(nowInstant, ZoneId.systemDefault()) + + while (arkCrisisV2Page.hasContent()) { + arkCrisisV2Page.forEach { arkCrisisV2: ArkLevel -> + // 危机合约信息比较准,因此未匹配一律视为已关闭 + arkCrisisV2.isOpen = false + gameDataService.findCrisisV2InfoById(arkCrisisV2.stageId)?.let { crisisV2Info -> + val instant = Instant.ofEpochSecond(crisisV2Info.endTs) + arkCrisisV2.isOpen = instant.isAfter(nowInstant) + } + if (arkCrisisV2.closeTime == null && java.lang.Boolean.FALSE == arkCrisisV2.isOpen) { + // 危机合约应该不存在赛季重新开放的问题,只要不每天变动关闭时间即可 + arkCrisisV2.closeTime = nowTime + } + } + + arkLevelRepo.saveAll(arkCrisisV2Page) + + if (!arkCrisisV2Page.hasNext()) { + // 没有下一页了,跳出循环 + break + } + pageable = arkCrisisV2Page.nextPageable() + arkCrisisV2Page = arkLevelRepo.findAllByCatOne(catOne, pageable) + } + log.info { "[CRISIS-V2-OPEN-STATUS]危机合约开放状态更新完毕" } + } + + /** + * 下载地图数据 + */ + private fun download(task: DownloadTask, tree: GithubTree) { + val fileName = URLEncoder.encode(tree.path, StandardCharsets.UTF_8) + if (bypassFileNames.contains(fileName)) { + task.success() + return + } + val url = String.format("https://raw.githubusercontent.com/%s/%s/%s", maaRepoAndBranch, tilePosPath, fileName) + okHttpClient.newCall(Request.Builder().url(url).build()).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + log.error(e) { "[LEVEL]下载地图数据失败:" + tree.path } + } + + @Throws(IOException::class) + override fun onResponse(call: Call, response: Response) { + response.body.use { rspBody -> + if (!response.isSuccessful || rspBody == null) { + task.fail() + log.error { "[LEVEL]下载地图数据失败:" + tree.path } + return + } + val tilePos = mapper.readValue(rspBody.string(), ArkTilePos::class.java) + val level = parserService.parseLevel(tilePos, tree.sha) + if (level == null) { + task.fail() + log.info { "[LEVEL]地图数据解析失败:" + tree.path } + return + } else if (level === ArkLevel.EMPTY) { + task.pass() + return + } + arkLevelRepo.save(level) + + task.success() + log.info { "[LEVEL]下载地图数据 ${tilePos.name} 成功, 进度${task.current}/${task.total}, 用时:${task.duration}s" } + } + } + }) + } + + private class DownloadTask( + private val startTime: Long = System.currentTimeMillis(), + private val success: AtomicInteger = AtomicInteger(0), + private val fail: AtomicInteger = AtomicInteger(0), + private val pass: AtomicInteger = AtomicInteger(0), + val total: Int = 0, + private val finishCallback: ((DownloadTask) -> Unit)? = null + ) { + fun success() { + success.incrementAndGet() + checkFinish() + } + + fun fail() { + fail.incrementAndGet() + checkFinish() + } + + fun pass() { + pass.incrementAndGet() + checkFinish() + } + + val current: Int + get() = success.get() + fail.get() + pass.get() + + val duration: Int + get() = (System.currentTimeMillis() - startTime).toInt() / 1000 + + val isAllSuccess: Boolean + get() = success.get() + pass.get() == total + + private fun checkFinish() { + if (success.get() + fail.get() + pass.get() != total) { + return + } + finishCallback!!.invoke(this) + log.info { "[LEVEL]地图数据下载完成, 成功:${success.get()}, 失败:${fail.get()}, 跳过:${pass.get()} 总用时${duration}s" } + } + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/CommentsAreaService.kt b/src/main/kotlin/plus/maa/backend/service/CommentsAreaService.kt new file mode 100644 index 00000000..3bea2ac8 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/CommentsAreaService.kt @@ -0,0 +1,366 @@ +package plus.maa.backend.service + +import org.apache.commons.lang3.StringUtils +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort +import org.springframework.stereotype.Service +import org.springframework.util.Assert +import plus.maa.backend.common.utils.converter.CommentConverter +import plus.maa.backend.config.external.MaaCopilotProperties +import plus.maa.backend.controller.request.comments.CommentsAddDTO +import plus.maa.backend.controller.request.comments.CommentsQueriesDTO +import plus.maa.backend.controller.request.comments.CommentsRatingDTO +import plus.maa.backend.controller.request.comments.CommentsToppingDTO +import plus.maa.backend.controller.response.comments.CommentsAreaInfo +import plus.maa.backend.repository.* +import plus.maa.backend.repository.entity.CommentsArea +import plus.maa.backend.repository.entity.Copilot +import plus.maa.backend.repository.entity.MaaUser +import plus.maa.backend.repository.entity.Rating +import plus.maa.backend.service.model.CommentNotification +import plus.maa.backend.service.model.RatingType +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.* + +/** + * @author LoMu + * Date 2023-02-17 15:00 + */ +@Service +class CommentsAreaService( + private val commentsAreaRepository: CommentsAreaRepository, + private val ratingRepository: RatingRepository, + private val copilotRepository: CopilotRepository, + private val userRepository: UserRepository, + private val emailService: EmailService, + private val maaCopilotProperties: MaaCopilotProperties, + private val commentConverter: CommentConverter, +) { + + + /** + * 评论 + * 每个评论都有一个uuid加持 + * + * @param userId 登录用户 id + * @param commentsAddDTO CommentsRequest + */ + fun addComments(userId: String, commentsAddDTO: CommentsAddDTO) { + val copilotId = commentsAddDTO.copilotId + val message = commentsAddDTO.message + val copilotOptional = copilotRepository.findByCopilotId(copilotId) + Assert.isTrue(StringUtils.isNotBlank(message), "评论不可为空") + Assert.isTrue(copilotOptional != null, "作业表不存在") + + + var fromCommentsId: String? = null + var mainCommentsId: String? = null + + var commentsArea: CommentsArea? = null + var isCopilotAuthor: Boolean? = null + + + //代表这是一条回复评论 + if (!commentsAddDTO.fromCommentId.isNullOrBlank()) { + val commentsAreaOptional = commentsAreaRepository.findById(commentsAddDTO.fromCommentId) + Assert.isTrue(commentsAreaOptional.isPresent, "回复的评论不存在") + commentsArea = commentsAreaOptional.get() + Assert.isTrue(!commentsArea.delete, "回复的评论不存在") + + mainCommentsId = if (StringUtils + .isNoneBlank(commentsArea.mainCommentId) + ) commentsArea.mainCommentId else commentsArea.id + + fromCommentsId = if (StringUtils + .isNoneBlank(commentsArea.id) + ) commentsArea.id else null + + if (Objects.isNull(commentsArea.notification) || commentsArea.notification) { + isCopilotAuthor = false + } + } else { + isCopilotAuthor = true + } + + //判断是否需要通知 + if (Objects.nonNull(isCopilotAuthor) && maaCopilotProperties.mail.notification) { + val copilot = copilotOptional!! + + //通知作业作者或是评论作者 + val replyUserId = if (isCopilotAuthor!!) copilot.uploaderId else commentsArea!!.uploaderId + + + val maaUserMap = userRepository.findByUsersId(listOf(userId, replyUserId!!)) + + //防止通知自己 + if (replyUserId != userId) { + val time = LocalDateTime.now() + val timeStr = time.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + + + val authorName = maaUserMap.getOrDefault(replyUserId, MaaUser.UNKNOWN).userName + val reName = maaUserMap.getOrDefault(userId, MaaUser.UNKNOWN).userName + + val title = if (isCopilotAuthor) copilot.doc?.title else commentsArea!!.message + + val commentNotification = CommentNotification(authorName, reName, timeStr, title, message) + + + val maaUser = maaUserMap[replyUserId] + if (Objects.nonNull(maaUser)) { + emailService.sendCommentNotification(maaUser!!.email, commentNotification) + } + } + } + + + //创建评论表 + commentsAreaRepository.insert( + CommentsArea( + copilotId = copilotId, + uploaderId = userId, + fromCommentId = fromCommentsId, + mainCommentId = mainCommentsId, + message = message, + notification = commentsAddDTO.notification + ) + ) + } + + + fun deleteComments(userId: String, commentsId: String) { + val commentsArea = findCommentsById(commentsId) + //允许作者删除评论 + copilotRepository.findByCopilotId(commentsArea.copilotId)?.let { copilot: Copilot -> + Assert.isTrue( + userId == copilot.uploaderId || userId == commentsArea.uploaderId, + "您无法删除不属于您的评论" + ) + } + val now = LocalDateTime.now() + commentsArea.delete = true + commentsArea.deleteTime = now + + //删除所有回复 + if (StringUtils.isBlank(commentsArea.mainCommentId)) { + val commentsAreaList = commentsAreaRepository.findByMainCommentId(commentsArea.id!!) + commentsAreaList.forEach { ca: CommentsArea -> + ca.deleteTime = now + ca.delete = true + } + commentsAreaRepository.saveAll(commentsAreaList) + } + commentsAreaRepository.save(commentsArea) + } + + + /** + * 为评论进行点赞 + * + * @param userId 登录用户 id + * @param commentsRatingDTO CommentsRatingDTO + */ + fun rates(userId: String, commentsRatingDTO: CommentsRatingDTO) { + val rating = commentsRatingDTO.rating + + val commentsArea = findCommentsById(commentsRatingDTO.commentId) + + val likeCountChange: Long + val dislikeCountChange: Long + + val ratingOptional = + ratingRepository.findByTypeAndKeyAndUserId( + Rating.KeyType.COMMENT, + commentsArea.id!!, userId + ) + // 判断该用户是否存在评分 + if (ratingOptional != null) { + // 如果评分发生变化则更新 + if (ratingOptional.rating != RatingType.fromRatingType(rating)) { + val oldRatingType = ratingOptional.rating + ratingOptional.rating = RatingType.fromRatingType(rating) + ratingOptional.rateTime = LocalDateTime.now() + val newRatingType = ratingRepository.save(ratingOptional).rating + // 更新评分后更新评论的点赞数 + likeCountChange = + (if (newRatingType == RatingType.LIKE) 1 else (if (oldRatingType != RatingType.LIKE) 0 else -1)).toLong() + dislikeCountChange = + (if (newRatingType == RatingType.DISLIKE) 1 else (if (oldRatingType != RatingType.DISLIKE) 0 else -1)).toLong() + } else { + // 如果评分未发生变化则结束 + return + } + } else { + // 不存在评分则创建 + val newRating = Rating( + null, + Rating.KeyType.COMMENT, + commentsArea.id!!, + userId, + RatingType.fromRatingType(rating), + LocalDateTime.now() + ) + + ratingRepository.insert(newRating) + likeCountChange = (if (newRating.rating == RatingType.LIKE) 1 else 0).toLong() + dislikeCountChange = (if (newRating.rating == RatingType.DISLIKE) 1 else 0).toLong() + } + + // 点赞数不需要在高并发下特别精准,大概就行,但是也得避免特别离谱的数字 + var likeCount = commentsArea.likeCount + likeCountChange + if (likeCount < 0) { + likeCount = 0 + } + + var dislikeCount = commentsArea.dislikeCount + dislikeCountChange + if (dislikeCount < 0) { + dislikeCount = 0 + } + + commentsArea.likeCount = likeCount + commentsArea.dislikeCount = dislikeCount + + commentsAreaRepository.save(commentsArea) + } + + /** + * 评论置顶 + * + * @param userId 登录用户 id + * @param commentsToppingDTO CommentsToppingDTO + */ + fun topping(userId: String, commentsToppingDTO: CommentsToppingDTO) { + val commentsArea = findCommentsById(commentsToppingDTO.commentId) + Assert.isTrue(!commentsArea.delete, "评论不存在") + // 只允许作者置顶评论 + copilotRepository.findByCopilotId(commentsArea.copilotId)?.let { copilot: Copilot -> + Assert.isTrue( + userId == copilot.uploaderId, + "只有作者才能置顶评论" + ) + commentsArea.topping = commentsToppingDTO.topping + commentsAreaRepository.save(commentsArea) + } + } + + /** + * 查询 + * + * @param request CommentsQueriesDTO + * @return CommentsAreaInfo + */ + fun queriesCommentsArea(request: CommentsQueriesDTO): CommentsAreaInfo { + val toppingOrder = Sort.Order.desc("topping") + + val sortOrder = Sort.Order( + if (request.desc) Sort.Direction.DESC else Sort.Direction.ASC, + when (request.orderBy) { + "hot" -> "likeCount" + "id" -> "uploadTime" + else -> request.orderBy ?: "likeCount" + } + ) + + val page = if (request.page > 0) request.page else 1 + val limit = if (request.limit > 0) request.limit else 10 + + + val pageable: Pageable = PageRequest.of(page - 1, limit, Sort.by(toppingOrder, sortOrder)) + + + //主评论 + val mainCommentsList = if (!request.justSeeId.isNullOrBlank()) { + commentsAreaRepository.findByCopilotIdAndUploaderIdAndDeleteAndMainCommentIdExists( + request.copilotId, + request.justSeeId, + delete = false, + exists = false, + pageable = pageable + ) + } else { + commentsAreaRepository.findByCopilotIdAndDeleteAndMainCommentIdExists( + request.copilotId, + delete = false, + exists = false, + pageable = pageable + ) + } + + val count = mainCommentsList.totalElements + + val pageNumber = mainCommentsList.totalPages + + // 判断是否存在下一页 + val hasNext = count - page.toLong() * limit > 0 + + + //获取子评论 + val subCommentsList = commentsAreaRepository.findByMainCommentIdIn( + mainCommentsList + .map { obj: CommentsArea -> requireNotNull(obj.id) } + .toList() + ) + + //将已删除评论内容替换为空 + subCommentsList.forEach { comment: CommentsArea -> + if (comment.delete) { + comment.message = "" + } + } + + + //所有评论 + val allComments: MutableList = ArrayList(mainCommentsList.toList()) + allComments.addAll(subCommentsList) + + //获取所有评论用户 + val userIds = allComments.map { obj: CommentsArea -> obj.uploaderId }.distinct().toList() + val maaUserMap = userRepository.findByUsersId(userIds) + + + //转换主评论数据并填充用户名 + val commentsInfos = mainCommentsList.map { mainComment: CommentsArea -> + val subCommentsInfoList = subCommentsList + .filter { comment: CommentsArea -> mainComment.id == comment.mainCommentId } //转换子评论数据并填充用户名 + .map { subComment: CommentsArea -> + commentConverter.toSubCommentsInfo( + subComment, //填充评论用户名 + maaUserMap.getOrDefault( + subComment.uploaderId, + MaaUser.UNKNOWN + ) + ) + }.toList() + val commentsInfo = commentConverter.toCommentsInfo( + mainComment, + maaUserMap.getOrDefault( + mainComment.uploaderId, + MaaUser.UNKNOWN + ), + subCommentsInfoList + ) + commentsInfo + }.toList() + + return CommentsAreaInfo(hasNext, pageNumber, count, commentsInfos) + } + + + private fun findCommentsById(commentsId: String): CommentsArea { + val commentsArea = commentsAreaRepository.findById(commentsId) + Assert.isTrue(commentsArea.isPresent, "评论不存在") + return commentsArea.get() + } + + + fun notificationStatus(userId: String, id: String, status: Boolean) { + val commentsAreaOptional = commentsAreaRepository.findById(id) + Assert.isTrue(commentsAreaOptional.isPresent, "评论不存在") + val commentsArea = commentsAreaOptional.get() + Assert.isTrue(userId == commentsArea.uploaderId, "您没有权限修改") + commentsArea.notification = status + commentsAreaRepository.save(commentsArea) + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/CopilotService.kt b/src/main/kotlin/plus/maa/backend/service/CopilotService.kt new file mode 100644 index 00000000..65d0f263 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/CopilotService.kt @@ -0,0 +1,560 @@ +package plus.maa.backend.service + +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.ObjectMapper +import com.google.common.collect.Sets +import io.github.oshai.kotlinlogging.KotlinLogging +import org.apache.commons.lang3.StringUtils +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort +import org.springframework.data.mongodb.core.MongoTemplate +import org.springframework.data.mongodb.core.query.Criteria +import org.springframework.data.mongodb.core.query.Query +import org.springframework.data.mongodb.core.query.Update +import org.springframework.stereotype.Service +import org.springframework.util.Assert +import org.springframework.util.ObjectUtils +import plus.maa.backend.common.utils.IdComponent +import plus.maa.backend.common.utils.converter.CopilotConverter +import plus.maa.backend.config.external.MaaCopilotProperties +import plus.maa.backend.controller.request.copilot.CopilotCUDRequest +import plus.maa.backend.controller.request.copilot.CopilotDTO +import plus.maa.backend.controller.request.copilot.CopilotQueriesRequest +import plus.maa.backend.controller.request.copilot.CopilotRatingReq +import plus.maa.backend.controller.response.MaaResultException +import plus.maa.backend.controller.response.copilot.ArkLevelInfo +import plus.maa.backend.controller.response.copilot.CopilotInfo +import plus.maa.backend.controller.response.copilot.CopilotPageInfo +import plus.maa.backend.repository.* +import plus.maa.backend.repository.entity.Copilot +import plus.maa.backend.repository.entity.Copilot.OperationGroup +import plus.maa.backend.repository.entity.Rating +import plus.maa.backend.service.model.RatingCache +import plus.maa.backend.service.model.RatingType +import java.math.BigDecimal +import java.math.RoundingMode +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit +import java.util.* +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.atomic.AtomicReference +import java.util.regex.Pattern +import kotlin.collections.component1 +import kotlin.math.ceil +import kotlin.math.ln +import kotlin.math.max + +private val log = KotlinLogging.logger { } + +/** + * @author LoMu + * Date 2022-12-25 19:57 + */ +@Service +class CopilotService( + private val copilotRepository: CopilotRepository, + private val ratingRepository: RatingRepository, + private val mongoTemplate: MongoTemplate, + private val mapper: ObjectMapper, + private val levelService: ArkLevelService, + private val redisCache: RedisCache, + private val idComponent: IdComponent, + private val userRepository: UserRepository, + private val commentsAreaRepository: CommentsAreaRepository, + private val properties: MaaCopilotProperties, + private val copilotConverter: CopilotConverter +) { + + /** + * 并修正前端的冗余部分 + * + * @param copilotDTO copilotDTO + */ + private fun correctCopilot(copilotDTO: CopilotDTO): CopilotDTO { + // 去除name的冗余部分 + if (copilotDTO.groups != null) { + copilotDTO.groups.forEach { group: Copilot.Groups -> + if (group.opers != null) { + group.opers.forEach { oper: OperationGroup -> + oper.name = oper.name?.replace("[\"“”]".toRegex(), "") + } + } + } + } + if (copilotDTO.opers != null) { + copilotDTO.opers.forEach { operator: Copilot.Operators -> + operator.name = operator.name?.replace("[\"“”]".toRegex(), "") + } + } + + // actions name 不是必须 + if (copilotDTO.actions != null) { + copilotDTO.actions.forEach { action: Copilot.Action -> + action.name = if (action.name == null) null else action.name!!.replace("[\"“”]".toRegex(), "") + } + } + // 使用stageId存储作业关卡信息 + val level = levelService.findByLevelIdFuzzy(copilotDTO.stageName) + level?.stageId?.let { + copilotDTO.stageName = it + } + return copilotDTO + } + + /** + * 将content解析为CopilotDTO + * + * @param content content + * @return CopilotDTO + */ + private fun parseToCopilotDto(content: String?): CopilotDTO { + requireNotNull(content) { + "作业内容不可为空" + } + try { + return mapper.readValue(content, CopilotDTO::class.java) + } catch (e: JsonProcessingException) { + log.error(e) { "解析copilot失败" } + throw MaaResultException("解析copilot失败") + } + } + + + private fun caseInsensitive(s: String): Pattern { + return Pattern.compile(s, Pattern.CASE_INSENSITIVE) + } + + + /** + * 上传新的作业 + * + * @param content 前端编辑json作业内容 + * @return 返回_id + */ + fun upload(loginUserId: String, content: String?): Long { + val copilotDTO = correctCopilot(parseToCopilotDto(content)) + // 将其转换为数据库存储对象 + val copilot = copilotConverter.toCopilot( + copilotDTO, + idComponent.getId(Copilot.META), loginUserId, LocalDateTime.now(), content + ) + copilotRepository.insert(copilot) + return copilot.copilotId!! + } + + /** + * 根据作业id删除作业 + */ + fun delete(loginUserId: String, request: CopilotCUDRequest) { + copilotRepository.findByCopilotId(request.id!!)?.let { copilot: Copilot -> + Assert.state(copilot.uploaderId == loginUserId, "您无法修改不属于您的作业") + copilot.delete = true + copilotRepository.save(copilot) + /* + * 删除作业时,如果被删除的项在 Redis 首页缓存中存在,则清空对应的首页缓存 + * 新增作业就不必,因为新作业显然不会那么快就登上热度榜和浏览量榜 + */ + for ((key1) in HOME_PAGE_CACHE_CONFIG) { + val key = String.format("home:%s:copilotIds", key1) + val pattern = String.format("home:%s:*", key1) + if (redisCache.valueMemberInSet(key, copilot.copilotId)) { + redisCache.removeCacheByPattern(pattern) + } + } + } + } + + /** + * 指定查询 + */ + fun getCopilotById(userIdOrIpAddress: String, id: Long): CopilotInfo? { + // 根据ID获取作业, 如作业不存在则抛出异常返回 + val copilotOptional = copilotRepository.findByCopilotIdAndDeleteIsFalse(id) + return copilotOptional?.let { copilot: Copilot -> + // 60分钟内限制同一个用户对访问量的增加 + val cache = redisCache.getCache("views:$userIdOrIpAddress", RatingCache::class.java) + if (Objects.isNull(cache) || Objects.isNull(cache!!.copilotIds) || + !cache.copilotIds.contains(id) + ) { + val query = Query.query(Criteria.where("copilotId").`is`(id)) + val update = Update() + // 增加一次views + update.inc("views") + mongoTemplate.updateFirst(query, update, Copilot::class.java) + if (cache == null) { + redisCache.setCache("views:$userIdOrIpAddress", RatingCache(Sets.newHashSet(id))) + } else { + redisCache.updateCache( + "views:$userIdOrIpAddress", RatingCache::class.java, cache, + { updateCache: RatingCache? -> + updateCache!!.copilotIds.add(id) + updateCache + }, 60, TimeUnit.MINUTES + ) + } + } + val maaUser = userRepository.findByUserId(copilot.uploaderId!!) + + // 新评分系统 + val ratingType = ratingRepository.findByTypeAndKeyAndUserId( + Rating.KeyType.COPILOT, + copilot.copilotId.toString(), userIdOrIpAddress + )?.rating + formatCopilot( + copilot, ratingType, maaUser!!.userName, + commentsAreaRepository.countByCopilotIdAndDelete(copilot.copilotId!!, false) + ) + } + } + + /** + * 分页查询。传入 userId 不为空时限制为用户所有的数据 + * 会缓存默认状态下热度和访问量排序的结果 + * + * @param userId 获取已登录用户自己的作业数据 + * @param request 模糊查询 + * @return CopilotPageInfo + */ + fun queriesCopilot(userId: String?, request: CopilotQueriesRequest): CopilotPageInfo { + val cacheTimeout = AtomicLong() + val cacheKey = AtomicReference() + val setKey = AtomicReference() + // 只缓存默认状态下热度和访问量排序的结果,并且最多只缓存前三页 + if (request.page <= 3 && request.document == null && request.levelKeyword == null && request.uploaderId == null && request.operator == null && + request.copilotIds.isNullOrEmpty() + ) { + request.orderBy?.takeIf { orderBy -> orderBy.isNotBlank() } + ?.let { key -> HOME_PAGE_CACHE_CONFIG[key] } + ?.let { t -> + cacheTimeout.set(t) + setKey.set(String.format("home:%s:copilotIds", request.orderBy)) + cacheKey.set(String.format("home:%s:%s", request.orderBy, request.hashCode())) + redisCache.getCache(cacheKey.get()!!, CopilotPageInfo::class.java) + }?.let { return it } + } + + val sortOrder = Sort.Order( + if (request.desc) Sort.Direction.DESC else Sort.Direction.ASC, + request.orderBy?.takeIf { orderBy -> orderBy.isNotBlank() }?.let { ob -> + when (ob) { + "hot" -> "hotScore" + "id" -> "copilotId" + else -> request.orderBy + } + } ?: "copilotId" + ) + // 判断是否有值 无值则为默认 + val page = if (request.page > 0) request.page else 1 + val limit = if (request.limit > 0) request.limit else 10 + + val pageable: Pageable = PageRequest.of(page - 1, limit, Sort.by(sortOrder)) + + val queryObj = Query() + val criteriaObj = Criteria() + + val andQueries: MutableSet = HashSet() + val norQueries: MutableSet = HashSet() + val orQueries: MutableSet = HashSet() + + andQueries.add(Criteria.where("delete").`is`(false)) + + + //关卡名、关卡类型、关卡编号 + if (!request.levelKeyword.isNullOrBlank()) { + val keyword = request.levelKeyword!! + val levelInfo = levelService.queryLevelInfosByKeyword(keyword) + if (levelInfo.isEmpty()) { + andQueries.add(Criteria.where("stageName").regex(caseInsensitive(keyword))) + } else { + andQueries.add( + Criteria.where("stageName").`in`( + levelInfo.map { obj: ArkLevelInfo -> obj.stageId }.toSet() + ) + ) + } + } + + // 作业id列表 + if (!request.copilotIds.isNullOrEmpty()) { + andQueries.add(Criteria.where("copilotId").`in`(request.copilotIds!!)) + } + + //标题、描述、神秘代码 + if (!request.document.isNullOrBlank()) { + orQueries.add(Criteria.where("doc.title").regex(caseInsensitive(request.document))) + orQueries.add(Criteria.where("doc.details").regex(caseInsensitive(request.document))) + } + + + //包含或排除干员 + var oper = request.operator + if (!oper.isNullOrBlank()) { + oper = oper.replace("[“\"”]".toRegex(), "") + val operators = oper.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + for (operator in operators) { + if (operator.startsWith("~")) { + val exclude = operator.substring(1) + // 排除查询指定干员 + norQueries.add(Criteria.where("opers.name").regex(exclude)) + } else { + // 模糊匹配查询指定干员 + andQueries.add(Criteria.where("opers.name").regex(operator)) + } + } + } + + //查看自己 + if (StringUtils.isNotBlank(request.uploaderId)) { + if ("me" == request.uploaderId) { + if (!ObjectUtils.isEmpty(userId)) { + andQueries.add(Criteria.where("uploaderId").`is`(userId)) + } + } else { + andQueries.add(Criteria.where("uploaderId").`is`(request.uploaderId)) + } + } + + // 封装查询 + if (andQueries.isNotEmpty()) { + criteriaObj.andOperator(andQueries) + } + if (norQueries.isNotEmpty()) { + criteriaObj.norOperator(norQueries) + } + if (orQueries.isNotEmpty()) { + criteriaObj.orOperator(orQueries) + } + queryObj.addCriteria(criteriaObj) + // 查询总数 + val count = mongoTemplate.count(queryObj, Copilot::class.java) + + // 分页排序查询 + val copilots = mongoTemplate.find(queryObj.with(pageable), Copilot::class.java) + + // 填充前端所需信息 + val copilotIds = copilots.map { + it.copilotId!! + }.toSet() + val maaUsers = userRepository.findByUsersId(copilots.map { it.uploaderId!! }.toList()) + val commentsCount = commentsAreaRepository.findByCopilotIdInAndDelete(copilotIds, false) + .groupBy { it.copilotId } + .mapValues { it.value.size.toLong() } + + // 新版评分系统 + // 反正目前首页和搜索不会直接展示当前用户有没有点赞,干脆直接不查,要用户点进作业才显示自己是否点赞 + val infos = copilots.map { copilot -> + formatCopilot( + copilot, null, + maaUsers[copilot.uploaderId]!!.userName, + commentsCount[copilot.copilotId] + ) + }.toList() + + // 计算页面 + val pageNumber = ceil(count.toDouble() / limit).toInt() + + // 判断是否存在下一页 + val hasNext = count - page.toLong() * limit > 0 + + // 封装数据 + val data = CopilotPageInfo(hasNext, pageNumber, count, infos) + + // 决定是否缓存 + if (cacheKey.get() != null) { + // 记录存在的作业id + redisCache.addSet(setKey.get(), copilotIds, cacheTimeout.get()) + // 缓存数据 + redisCache.setCache(cacheKey.get()!!, data, cacheTimeout.get()) + } + return data + } + + /** + * 增量更新 + * + * @param copilotCUDRequest 作业_id content + */ + fun update(loginUserId: String, copilotCUDRequest: CopilotCUDRequest) { + val content = copilotCUDRequest.content + val id = copilotCUDRequest.id + copilotRepository.findByCopilotId(id!!)?.let { copilot: Copilot -> + val copilotDTO = correctCopilot(parseToCopilotDto(content)) + Assert.state(copilot.uploaderId == loginUserId, "您无法修改不属于您的作业") + copilot.uploadTime = LocalDateTime.now() + copilotConverter.updateCopilotFromDto(copilotDTO, content!!, copilot) + copilotRepository.save(copilot) + } + } + + /** + * 评分相关 + * + * @param request 评分 + * @param userIdOrIpAddress 用于已登录用户作出评分 + */ + fun rates(userIdOrIpAddress: String, request: CopilotRatingReq) { + val rating = request.rating + + Assert.isTrue(copilotRepository.existsCopilotsByCopilotId(request.id), "作业id不存在") + + var likeCountChange = 0 + var dislikeCountChange = 0 + val ratingOptional = ratingRepository.findByTypeAndKeyAndUserId( + Rating.KeyType.COPILOT, + request.id.toString(), + userIdOrIpAddress + ) + // 如果评分存在则更新评分 + if (ratingOptional != null) { + // 如果评分相同,则不做任何操作 + if (ratingOptional.rating == RatingType.fromRatingType(rating)) { + return + } + // 如果评分不同则更新评分 + val oldRatingType = ratingOptional.rating + ratingOptional.rating = RatingType.fromRatingType(rating) + ratingOptional.rateTime = LocalDateTime.now() + ratingRepository.save(ratingOptional) + // 计算评分变化 + likeCountChange = + if (ratingOptional.rating == RatingType.LIKE) 1 else (if (oldRatingType != RatingType.LIKE) 0 else -1) + dislikeCountChange = + if (ratingOptional.rating == RatingType.DISLIKE) 1 else (if (oldRatingType != RatingType.DISLIKE) 0 else -1) + } + + // 不存在评分 则添加新的评分 + if (ratingOptional == null) { + val newRating = Rating( + null, + Rating.KeyType.COPILOT, + request.id.toString(), + userIdOrIpAddress, + RatingType.fromRatingType(rating), + LocalDateTime.now() + ) + + ratingRepository.insert(newRating) + // 计算评分变化 + likeCountChange = if (newRating.rating == RatingType.LIKE) 1 else 0 + dislikeCountChange = if (newRating.rating == RatingType.DISLIKE) 1 else 0 + } + + // 获取只包含评分的作业 + var query = Query.query( + Criteria + .where("copilotId").`is`(request.id) + .and("delete").`is`(false) + ) + // 排除 _id,防止误 save 该不完整作业后原有数据丢失 + query.fields().include("likeCount", "dislikeCount").exclude("_id") + val copilot = mongoTemplate.findOne(query, Copilot::class.java) + Assert.notNull(copilot, "作业不存在") + + // 计算评分相关 + var likeCount = copilot!!.likeCount + likeCountChange + likeCount = if (likeCount < 0) 0 else likeCount + var ratingCount = likeCount + copilot.dislikeCount + dislikeCountChange + ratingCount = if (ratingCount < 0) 0 else ratingCount + + val rawRatingLevel = if (ratingCount != 0L) likeCount.toDouble() / ratingCount else 0.0 + val bigDecimal = BigDecimal(rawRatingLevel) + // 只取一位小数点 + val ratingLevel = bigDecimal.setScale(1, RoundingMode.HALF_UP).toDouble() + // 更新数据 + query = Query.query( + Criteria + .where("copilotId").`is`(request.id) + .and("delete").`is`(false) + ) + val update = Update() + update["likeCount"] = likeCount + update["dislikeCount"] = ratingCount - likeCount + update["ratingLevel"] = (ratingLevel * 10).toInt() + update["ratingRatio"] = ratingLevel + mongoTemplate.updateFirst(query, update, Copilot::class.java) + + // 记录近期评分变化量前 100 的作业 id + redisCache.incZSet( + "rate:hot:copilotIds", + request.id.toString(), + 1.0, 100, (3600 * 3).toLong() + ) + } + + /** + * 将数据库内容转换为前端所需格式

+ * 新版评分系统 + */ + private fun formatCopilot( + copilot: Copilot, ratingType: RatingType?, userName: String, + commentsCount: Long? + ): CopilotInfo { + val info = copilotConverter.toCopilotInfo( + copilot, userName, copilot.copilotId!!, + commentsCount + ) + + info.ratingRatio = copilot.ratingRatio + info.ratingLevel = copilot.ratingLevel + if (ratingType != null) { + info.ratingType = ratingType.display + } + // 评分数少于一定数量 + info.notEnoughRating = + copilot.likeCount + copilot.dislikeCount <= properties.copilot.minValueShowNotEnoughRating + + info.available = true + + // 兼容客户端, 将作业ID替换为数字ID + copilot.id = copilot.copilotId.toString() + return info + } + + fun notificationStatus(userId: String?, copilotId: Long, status: Boolean) { + val copilotOptional = copilotRepository.findByCopilotId(copilotId) + Assert.isTrue(copilotOptional != null, "copilot不存在") + val copilot = copilotOptional!! + Assert.isTrue(userId == copilot.uploaderId, "您没有权限修改") + copilot.notification = status + copilotRepository.save(copilot) + } + + companion object { + /* + 首页分页查询缓存配置 + 格式为:需要缓存的 orderBy 类型(也就是榜单类型) -> 缓存时间 + (Map.of()返回的是不可变对象,无需担心线程安全问题) + */ + private val HOME_PAGE_CACHE_CONFIG: Map = java.util.Map.of( + "hot", 3600 * 24L, + "views", 3600L, + "id", 300L + ) + + @JvmStatic + fun getHotScore(copilot: Copilot, lastWeekLike: Long, lastWeekDislike: Long): Double { + val now = LocalDateTime.now() + val uploadTime = copilot.uploadTime + // 基于时间的基础分 + var base = 6.0 + // 相比上传时间过了多少周 + val pastedWeeks = ChronoUnit.WEEKS.between(uploadTime, now) + 1 + base /= ln((pastedWeeks + 1).toDouble()) + // 上一周好评率 + val ups = max(lastWeekLike.toDouble(), 1.0).toLong() + val downs = max(lastWeekDislike.toDouble(), 0.0).toLong() + val greatRate = ups.toDouble() / (ups + downs) + if ((ups + downs) >= 5 && downs >= ups) { + // 将信赖就差评过多的作业打入地狱 + base *= greatRate + } + // 上一周好评率 * (上一周评分数 / 10) * (浏览数 / 10) / 过去的周数 + val s = (greatRate * (copilot.views / 10.0) + * max((ups + downs) / 10.0, 1.0)) / pastedWeeks + val order = ln(max(s, 1.0)) + return order + s / 1000.0 + base + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/plus/maa/backend/service/CopilotSetService.kt b/src/main/kotlin/plus/maa/backend/service/CopilotSetService.kt new file mode 100644 index 00000000..3f502a69 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/CopilotSetService.kt @@ -0,0 +1,138 @@ +package plus.maa.backend.service + +import cn.hutool.core.lang.Assert +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Sort +import org.springframework.stereotype.Service +import plus.maa.backend.common.utils.IdComponent +import plus.maa.backend.common.utils.converter.CopilotSetConverter +import plus.maa.backend.controller.request.copilotset.CopilotSetCreateReq +import plus.maa.backend.controller.request.copilotset.CopilotSetModCopilotsReq +import plus.maa.backend.controller.request.copilotset.CopilotSetQuery +import plus.maa.backend.controller.request.copilotset.CopilotSetUpdateReq +import plus.maa.backend.controller.response.copilotset.CopilotSetPageRes +import plus.maa.backend.controller.response.copilotset.CopilotSetRes +import plus.maa.backend.repository.CopilotSetRepository +import plus.maa.backend.repository.UserRepository +import plus.maa.backend.repository.entity.CopilotSet +import plus.maa.backend.repository.entity.MaaUser +import plus.maa.backend.repository.findByUsersId +import java.time.LocalDateTime + +private val log = KotlinLogging.logger { } + +/** + * @author dragove + * create on 2024-01-01 + */ +@Service +class CopilotSetService( + private val idComponent: IdComponent, + private val converter: CopilotSetConverter, + private val repository: CopilotSetRepository, + private val userRepository: UserRepository, +) { + + private val defaultSort: Sort = Sort.by("id").descending() + + /** + * 创建作业集 + * + * @param req 作业集创建请求 + * @param userId 创建者用户id + * @return 作业集id + */ + fun create(req: CopilotSetCreateReq, userId: String?): Long { + val id = idComponent.getId(CopilotSet.meta) + val newCopilotSet = converter.convert(req, id, userId!!) + repository.insert(newCopilotSet) + return id + } + + /** + * 往作业集中加入作业id列表 + */ + fun addCopilotIds(req: CopilotSetModCopilotsReq, userId: String) { + val copilotSet = repository.findById(req.id) + .orElseThrow { IllegalArgumentException("作业集不存在") } + Assert.state(copilotSet.creatorId == userId, "您不是该作业集的创建者,无权修改该作业集") + copilotSet.copilotIds.addAll(req.copilotIds) + copilotSet.copilotIds = copilotSet.distinctIdsAndCheck() + repository.save(copilotSet) + } + + /** + * 往作业集中删除作业id列表 + */ + fun removeCopilotIds(req: CopilotSetModCopilotsReq, userId: String) { + val copilotSet = repository.findById(req.id) + .orElseThrow { IllegalArgumentException("作业集不存在") } + Assert.state(copilotSet.creatorId == userId, "您不是该作业集的创建者,无权修改该作业集") + val removeIds: Set = HashSet(req.copilotIds) + copilotSet.copilotIds.removeIf { o: Long -> removeIds.contains(o) } + repository.save(copilotSet) + } + + /** + * 更新作业集信息 + */ + fun update(req: CopilotSetUpdateReq, userId: String) { + val copilotSet = repository.findById(req.id) + .orElseThrow { IllegalArgumentException("作业集不存在") } + Assert.state(copilotSet.creatorId == userId, "您不是该作业集的创建者,无权修改该作业集") + copilotSet.name = req.name + copilotSet.description = req.description + copilotSet.status = req.status + repository.save(copilotSet) + } + + /** + * 删除作业集信息(逻辑删除,保留详情接口查询结果) + * + * @param id 作业集id + * @param userId 登陆用户id + */ + fun delete(id: Long, userId: String) { + log.info { "delete copilot set for id: $id, userId: $userId" } + val copilotSet = repository.findById(id) + .orElseThrow { IllegalArgumentException("作业集不存在") } + Assert.state(copilotSet.creatorId == userId, "您不是该作业集的创建者,无权删除该作业集") + copilotSet.delete = true + copilotSet.deleteTime = LocalDateTime.now() + repository.save(copilotSet) + } + + fun query(req: CopilotSetQuery): CopilotSetPageRes { + val pageRequest = PageRequest.of(req.page - 1, req.limit, defaultSort) + + val keyword = req.keyword + val copilotSets = if (keyword.isNullOrBlank()) { + repository.findAll(pageRequest) + } else { + repository.findByKeyword(keyword, pageRequest) + } + + val userIds = copilotSets + .map { obj: CopilotSet -> obj.creatorId } + .distinct() + .toList() + val userById = userRepository.findByUsersId(userIds) + return CopilotSetPageRes( + copilotSets.totalPages > req.page, + copilotSets.number + 1, + copilotSets.totalElements, + copilotSets.map { cs: CopilotSet -> + val user = userById.getOrDefault(cs.creatorId, MaaUser.UNKNOWN) + converter.convert(cs, user.userName) + }.toList() + ) + } + + fun get(id: Long): CopilotSetRes { + return repository.findById(id).map { copilotSet: CopilotSet -> + val userName = (userRepository.findByUserId(copilotSet.creatorId) ?: MaaUser.UNKNOWN).userName + converter.convertDetail(copilotSet, userName) + }.orElseThrow { IllegalArgumentException("作业不存在") } + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/EmailService.kt b/src/main/kotlin/plus/maa/backend/service/EmailService.kt new file mode 100644 index 00000000..fbd52f8b --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/EmailService.kt @@ -0,0 +1,110 @@ +package plus.maa.backend.service + +import cn.hutool.extra.mail.MailAccount +import cn.hutool.extra.mail.MailUtil +import io.github.oshai.kotlinlogging.KotlinLogging +import jakarta.annotation.Resource +import org.apache.commons.lang3.RandomStringUtils +import org.springframework.beans.factory.annotation.Value +import org.springframework.core.task.AsyncTaskExecutor +import org.springframework.stereotype.Service +import plus.maa.backend.common.utils.FreeMarkerUtils +import plus.maa.backend.config.external.MaaCopilotProperties +import plus.maa.backend.controller.response.MaaResultException +import plus.maa.backend.repository.RedisCache +import plus.maa.backend.service.model.CommentNotification +import java.util.* + +/** + * @author LoMu + * Date 2022-12-24 11:05 + */ +@Service +class EmailService( + private val maaCopilotProperties: MaaCopilotProperties, + @Value("\${debug.email.no-send:false}") + private val flagNoSend: Boolean = false, + private val redisCache: RedisCache, + @Resource(name = "emailTaskExecutor") + private val emailTaskExecutor: AsyncTaskExecutor +) { + private val log = KotlinLogging.logger { } + private val mail = maaCopilotProperties.mail + private val mailAccount = MailAccount() + .setHost(mail.host) + .setPort(mail.port) + .setFrom(mail.from) + .setUser(mail.user) + .setPass(mail.pass) + .setSslEnable(mail.ssl) + .setStarttlsEnable(mail.starttls) + + /** + * 发送验证码 + * 以email作为 redis key + * vcode(验证码)作为 redis value + * + * @param email 邮箱 + */ + fun sendVCode(email: String) { + // 一个过期周期最多重发十条,记录已发送的邮箱以及间隔时间 + val timeout = maaCopilotProperties.vcode.expire / 10 + if (!redisCache.setCacheIfAbsent("HasBeenSentVCode:$email", timeout, timeout)) { + // 设置失败,说明 key 已存在 + throw MaaResultException(403, String.format("发送验证码的请求至少需要间隔 %d 秒", timeout)) + } + // 执行异步任务 + asyncSendVCode(email) + } + + private fun asyncSendVCode(email: String) = emailTaskExecutor.execute { + // 6位随机数验证码 + val vCode = RandomStringUtils.random(6, true, true).uppercase(Locale.getDefault()) + if (flagNoSend) { + log.warn { "Email not sent, no-send enabled, vcode is $vCode" } + } else { + val subject = "Maa Backend Center 验证码" + val dataModel = mapOf( + "content" to "mail-vCode.ftlh", + "obj" to vCode, + ) + val content = FreeMarkerUtils.parseData("mail-includeHtml.ftlh", dataModel) + MailUtil.send(mailAccount, listOf(email), subject, content, true) + } + // 存redis + redisCache.setCache("vCodeEmail:$email", vCode, maaCopilotProperties.vcode.expire) + } + + /** + * 检验验证码并抛出异常 + * @param email 邮箱 + * @param vcode 验证码 + * @throws MaaResultException 验证码错误 + */ + fun verifyVCode(email: String, vcode: String) { + if (!redisCache.removeKVIfEquals("vCodeEmail:$email", vcode.uppercase(Locale.getDefault()))) { + throw MaaResultException(401, "验证码错误") + } + } + + fun sendCommentNotification(email: String, commentNotification: CommentNotification) = emailTaskExecutor.execute { + val limit = 25 + val title = (commentNotification.title ?: "").let { + if (it.length > limit) it.substring(0, limit - 4) + "...." else it + } + + val subject = "收到新回复 来自用户@${commentNotification.reName} Re: $title" + val dataModel = mapOf( + "content" to "mail-comment-notification.ftlh", + "authorName" to commentNotification.authorName, + "frontendLink" to maaCopilotProperties.info.frontendDomain, + "reName" to commentNotification.reName, + "date" to commentNotification.date, + "title" to title, + "reMessage" to commentNotification.reMessage, + ) + val content = FreeMarkerUtils.parseData("mail-includeHtml.ftlh", dataModel) + + MailUtil.send(mailAccount, listOf(email), subject, content, true) + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/FileService.kt b/src/main/kotlin/plus/maa/backend/service/FileService.kt new file mode 100644 index 00000000..e473fe13 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/FileService.kt @@ -0,0 +1,228 @@ +package plus.maa.backend.service + +import com.mongodb.client.gridfs.GridFSFindIterable +import jakarta.servlet.http.HttpServletResponse +import org.apache.commons.lang3.StringUtils +import org.bson.Document +import org.springframework.data.mongodb.core.query.Criteria +import org.springframework.data.mongodb.core.query.Query +import org.springframework.data.mongodb.gridfs.GridFsCriteria +import org.springframework.data.mongodb.gridfs.GridFsOperations +import org.springframework.stereotype.Service +import org.springframework.util.Assert +import org.springframework.web.multipart.MultipartException +import org.springframework.web.multipart.MultipartFile +import plus.maa.backend.controller.file.ImageDownloadDTO +import plus.maa.backend.controller.response.MaaResultException +import plus.maa.backend.repository.RedisCache +import java.io.IOException +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.TimeUnit +import java.util.regex.Pattern +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + + +/** + * @author LoMu + * Date 2023-04-16 23:21 + */ +@Service +class FileService( + private val gridFsOperations: GridFsOperations, + private val redisCache: RedisCache +) { + + fun uploadFile( + file: MultipartFile, + type: String?, + version: String, + classification: String?, + label: String?, + ip: String? + ) { + //redis持久化 + + var realVersion = version + if (redisCache.getCache("NotEnable:UploadFile", String::class.java) != null) { + throw MaaResultException(403, "closed uploadfile") + } + + //文件小于1024Bytes不接收 + if (file.size < 1024) { + throw MultipartException("Minimum upload size exceeded") + } + Assert.notNull(file.originalFilename, "文件名不可为空") + + var antecedentVersion: String? = null + if (realVersion.contains("-")) { + val split = realVersion.split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + realVersion = split[0] + antecedentVersion = split[1] + } + + val document = Document() + document["version"] = realVersion + document["antecedentVersion"] = antecedentVersion + document["label"] = label + document["classification"] = classification + document["type"] = type + document["ip"] = ip + + val index = file.originalFilename!!.lastIndexOf(".") + var fileType = "" + if (index != -1) { + fileType = file.originalFilename!!.substring(index) + } + + val fileName = "Maa-" + UUID.randomUUID().toString().replace("-".toRegex(), "") + fileType + + try { + gridFsOperations.store(file.inputStream, fileName, document) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + + fun downloadDateFile(date: String?, beLocated: String, delete: Boolean, response: HttpServletResponse) { + val formatter = SimpleDateFormat("yyyy-MM-dd") + val query: Query + + val d = if (date.isNullOrBlank()) { + Date(System.currentTimeMillis()) + } else { + try { + formatter.parse(date) + } catch (e: ParseException) { + throw RuntimeException(e) + } + } + + query = if (StringUtils.isBlank(beLocated) || "after" == beLocated.lowercase(Locale.getDefault())) { + Query(Criteria.where("metadata").gte(d)) + } else { + Query(Criteria.where("uploadDate").lte(d)) + } + val files = gridFsOperations.find(query) + + response.addHeader("Content-Disposition", "attachment;filename=" + System.currentTimeMillis() + ".zip") + + gzip(response, files) + + if (delete) { + gridFsOperations.delete(query) + } + } + + + fun downloadFile(imageDownloadDTO: ImageDownloadDTO, response: HttpServletResponse) { + val query = Query() + val criteriaSet: MutableSet = HashSet() + + + //图片类型 + criteriaSet.add( + GridFsCriteria.whereMetaData("type").regex(Pattern.compile(imageDownloadDTO.type, Pattern.CASE_INSENSITIVE)) + ) + + //指定下载某个类型的图片 + if (!imageDownloadDTO.classification.isNullOrBlank()) { + criteriaSet.add( + GridFsCriteria.whereMetaData("classification") + .regex(Pattern.compile(imageDownloadDTO.classification, Pattern.CASE_INSENSITIVE)) + ) + } + + //指定版本或指定范围版本 + if (!imageDownloadDTO.version.isNullOrEmpty()) { + val version = imageDownloadDTO.version + + if (version.size == 1) { + var antecedentVersion: String? = null + if (version[0].contains("-")) { + val split = version[0].split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + antecedentVersion = split[1] + } + criteriaSet.add( + GridFsCriteria.whereMetaData("version").`is`(version[0]).and("antecedentVersion") + .`is`(antecedentVersion) + ) + } else if (version.size == 2) { + criteriaSet.add(GridFsCriteria.whereMetaData("version").gte(version[0]).lte(version[1])) + } + } + + if (!imageDownloadDTO.label.isNullOrBlank()) { + criteriaSet.add( + GridFsCriteria.whereMetaData("label") + .regex(Pattern.compile(imageDownloadDTO.label, Pattern.CASE_INSENSITIVE)) + ) + } + + val criteria = Criteria().andOperator(criteriaSet) + query.addCriteria(criteria) + + + val gridFSFiles = gridFsOperations.find(query) + + response.addHeader("Content-Disposition", "attachment;filename=" + "Maa-" + imageDownloadDTO.type + ".zip") + + gzip(response, gridFSFiles) + + if (imageDownloadDTO.delete) { + gridFsOperations.delete(query) + } + } + + fun disable(): String { + isUploadEnabled = false + return "已关闭" + } + + fun enable(): String { + isUploadEnabled = true + return "已启用" + } + + var isUploadEnabled: Boolean + get() = redisCache.getCache("NotEnable:UploadFile", String::class.java) == null + /** + * 设置上传功能状态 + * @param enabled 是否开启 + */ + set(enabled) { + // Fixme: redis recovery solution should be added, or change to another storage + if (enabled) { + redisCache.removeCache("NotEnable:UploadFile") + } else { + redisCache.setCache("NotEnable:UploadFile", "1", 0, TimeUnit.DAYS) + } + } + + + private fun gzip(response: HttpServletResponse, files: GridFSFindIterable) { + try { + ZipOutputStream(response.outputStream).use { zipOutputStream -> + for (file in files) { + val zipEntry = ZipEntry(file.filename) + gridFsOperations.getResource(file).inputStream.use { inputStream -> + //添加压缩文件 + zipOutputStream.putNextEntry(zipEntry) + + val bytes = ByteArray(1024) + var len: Int + while ((inputStream.read(bytes).also { len = it }) != -1) { + zipOutputStream.write(bytes, 0, len) + zipOutputStream.flush() + } + } + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/UserDetailServiceImpl.kt b/src/main/kotlin/plus/maa/backend/service/UserDetailServiceImpl.kt new file mode 100644 index 00000000..3890afd7 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/UserDetailServiceImpl.kt @@ -0,0 +1,42 @@ +package plus.maa.backend.service + +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.stereotype.Service +import plus.maa.backend.repository.UserRepository +import plus.maa.backend.repository.entity.MaaUser +import plus.maa.backend.service.model.LoginUser + +/** + * @author AnselYuki + */ +@Service +class UserDetailServiceImpl(private val userRepository: UserRepository) : UserDetailsService { + + /** + * 查询用户信息 + * + * @param email 用户使用邮箱登录 + * @return 用户详细信息 + * @throws UsernameNotFoundException 用户名未找到 + */ + @Throws(UsernameNotFoundException::class) + override fun loadUserByUsername(email: String): UserDetails { + val user = userRepository.findByEmail(email) ?: throw UsernameNotFoundException("用户不存在") + + val permissions = collectAuthoritiesFor(user) + //数据封装成UserDetails返回 + return LoginUser(user, permissions) + } + + fun collectAuthoritiesFor(user: MaaUser): Collection { + val authorities = ArrayList() + for (i in 0..user.status) { + authorities.add(SimpleGrantedAuthority(i.toString())) + } + return authorities + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/UserService.kt b/src/main/kotlin/plus/maa/backend/service/UserService.kt new file mode 100644 index 00000000..a8fd5cff --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/UserService.kt @@ -0,0 +1,220 @@ +package plus.maa.backend.service + +import org.springframework.beans.BeanUtils +import org.springframework.dao.DuplicateKeyException +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Service +import plus.maa.backend.common.MaaStatusCode +import plus.maa.backend.config.external.MaaCopilotProperties +import plus.maa.backend.controller.request.user.* +import plus.maa.backend.controller.response.MaaResultException +import plus.maa.backend.controller.response.user.MaaLoginRsp +import plus.maa.backend.controller.response.user.MaaUserInfo +import plus.maa.backend.repository.UserRepository +import plus.maa.backend.repository.entity.MaaUser +import plus.maa.backend.service.jwt.JwtExpiredException +import plus.maa.backend.service.jwt.JwtInvalidException +import plus.maa.backend.service.jwt.JwtService +import java.util.* + +/** + * @author AnselYuki + */ +@Service +class UserService( + private val userRepository: UserRepository, + private val emailService: EmailService, + private val passwordEncoder: PasswordEncoder, + private val userDetailService: UserDetailServiceImpl, + private val jwtService: JwtService, + private val properties: MaaCopilotProperties +) { + + /** + * 登录方法 + * + * @param loginDTO 登录参数 + * @return 携带了token的封装类 + */ + fun login(loginDTO: LoginDTO): MaaLoginRsp { + val user = userRepository.findByEmail(loginDTO.email) + if (user == null || !passwordEncoder.matches(loginDTO.password, user.password)) { + throw MaaResultException(401, "用户不存在或者密码错误") + } + // 未激活的用户 + if (user.status == 0) { + throw MaaResultException(MaaStatusCode.MAA_USER_NOT_ENABLED) + } + + val jwtId = UUID.randomUUID().toString() + val jwtIds = user.refreshJwtIds + jwtIds.add(jwtId) + while (jwtIds.size > properties.jwt.maxLogin) jwtIds.removeAt(0) + userRepository.save(user) + + val authorities = userDetailService.collectAuthoritiesFor(user) + val authToken = jwtService.issueAuthToken(user.userId!!, null, authorities) + val refreshToken = jwtService.issueRefreshToken(user.userId, jwtId) + + return MaaLoginRsp( + authToken.value, + authToken.expiresAt, + authToken.notBefore, + refreshToken.value, + refreshToken.expiresAt, + refreshToken.notBefore, + MaaUserInfo(user) + ) + } + + /** + * 修改密码 + * + * @param userId 当前用户 + * @param rawPassword 新密码 + */ + fun modifyPassword( + userId: String, rawPassword: String, + originPassword: String? = null, + verifyOriginPassword: Boolean = true + ) { + val userResult = userRepository.findById(userId) + if (userResult.isEmpty) return + val maaUser = userResult.get() + if (verifyOriginPassword) { + check(!originPassword.isNullOrEmpty()) { + "请输入原密码" + } + check(passwordEncoder.matches(originPassword, maaUser.password)) { + "原密码错误" + } + } + // 修改密码的逻辑,应当使用与 authentication provider 一致的编码器 + maaUser.password = passwordEncoder.encode(rawPassword) + maaUser.refreshJwtIds = ArrayList() + userRepository.save(maaUser) + } + + /** + * 用户注册 + * + * @param registerDTO 传入用户参数 + * @return 返回注册成功的用户摘要(脱敏) + */ + fun register(registerDTO: RegisterDTO): MaaUserInfo { + val encode = passwordEncoder.encode(registerDTO.password) + + // 校验验证码 + emailService.verifyVCode(registerDTO.email, registerDTO.registrationToken) + + val user = MaaUser( + userName = registerDTO.userName, + email = registerDTO.email, + password = registerDTO.password + ) + BeanUtils.copyProperties(registerDTO, user) + user.password = encode + user.status = 1 + val userInfo: MaaUserInfo + try { + val save = userRepository.save(user) + userInfo = MaaUserInfo(save) + } catch (e: DuplicateKeyException) { + throw MaaResultException(MaaStatusCode.MAA_USER_EXISTS) + } + return userInfo + } + + /** + * 更新用户信息 + * + * @param userId 用户id + * @param updateDTO 更新参数 + */ + fun updateUserInfo(userId: String, updateDTO: UserInfoUpdateDTO) { + userRepository.findById(userId).ifPresent { maaUser: MaaUser -> + maaUser.updateAttribute(updateDTO) + userRepository.save(maaUser) + } + } + + /** + * 刷新token + * + * @param token token + */ + fun refreshToken(token: String): MaaLoginRsp { + try { + val old = jwtService.verifyAndParseRefreshToken(token) + + val userId = old.subject + val user = userRepository.findById(userId).orElseThrow() + + val refreshJwtIds = user.refreshJwtIds + val idIndex = refreshJwtIds.indexOf(old.jwtId) + if (idIndex < 0) throw MaaResultException(401, "invalid token") + + val jwtId = UUID.randomUUID().toString() + refreshJwtIds[idIndex] = jwtId + + userRepository.save(user) + + val refreshToken = jwtService.newRefreshToken(old, jwtId) + + val authorities = userDetailService.collectAuthoritiesFor(user) + val authToken = jwtService.issueAuthToken(userId, null, authorities) + + return MaaLoginRsp( + authToken.value, + authToken.expiresAt, + authToken.notBefore, + refreshToken.value, + refreshToken.expiresAt, + refreshToken.notBefore, + MaaUserInfo(user) + ) + } catch (e: JwtInvalidException) { + throw MaaResultException(401, e.message) + } catch (e: JwtExpiredException) { + throw MaaResultException(401, e.message) + } catch (e: NoSuchElementException) { + throw MaaResultException(401, e.message) + } + } + + /** + * 通过邮箱激活码更新密码 + * + * @param passwordResetDTO 通过邮箱修改密码请求 + */ + fun modifyPasswordByActiveCode(passwordResetDTO: PasswordResetDTO) { + emailService.verifyVCode(passwordResetDTO.email, passwordResetDTO.activeCode) + val maaUser = userRepository.findByEmail(passwordResetDTO.email) + modifyPassword(maaUser!!.userId!!, passwordResetDTO.password, verifyOriginPassword = false) + } + + /** + * 根据邮箱校验用户是否存在 + * + * @param email 用户邮箱 + */ + fun checkUserExistByEmail(email: String) { + if (null == userRepository.findByEmail(email)) { + throw MaaResultException(MaaStatusCode.MAA_USER_NOT_FOUND) + } + } + + /** + * 注册时发送验证码 + */ + fun sendRegistrationToken(regDTO: SendRegistrationTokenDTO) { + // 判断用户是否存在 + val maaUser = userRepository.findByEmail(regDTO.email) + if (maaUser != null) { + // 用户已存在 + throw MaaResultException(MaaStatusCode.MAA_USER_EXISTS) + } + // 发送验证码 + emailService.sendVCode(regDTO.email) + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/jwt/JwtAuthToken.kt b/src/main/kotlin/plus/maa/backend/service/jwt/JwtAuthToken.kt new file mode 100644 index 00000000..ad47fbc7 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/jwt/JwtAuthToken.kt @@ -0,0 +1,91 @@ +package plus.maa.backend.service.jwt + +import org.springframework.security.core.Authentication +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.util.StringUtils +import java.time.Instant + +/** + * 基于 JWT 的 AuthToken. 本类实现了 Authentication, 可直接用于 Spring Security + * 的认证流程 + */ +class JwtAuthToken : JwtToken, Authentication { + private var authenticated = false + + /** + * 从 jwt 构建 token + * + * @param jwt jwt + * @param key 签名密钥 + * @throws JwtInvalidException jwt 未通过签名验证或不符合要求 + */ + constructor(jwt: String, key: ByteArray) : super(jwt, TYPE, key) + + constructor( + sub: String, + jti: String?, + iat: Instant, + exp: Instant, + nbf: Instant, + authorities: Collection, + key: ByteArray + ) : super(sub, jti, iat, exp, nbf, TYPE, key) { + this.authorities = authorities + } + + + override fun getAuthorities(): Collection { + val authorityStrings = jwt.payloads.getStr(CLAIM_AUTHORITIES) + return StringUtils.commaDelimitedListToSet(authorityStrings).stream() + .map { role: String? -> SimpleGrantedAuthority(role) } + .toList() + } + + fun setAuthorities(authorities: Collection) { + val authorityStrings = authorities.stream().map { obj: GrantedAuthority -> obj.authority }.toList() + val encodedAuthorities = StringUtils.collectionToCommaDelimitedString(authorityStrings) + jwt.setPayload(CLAIM_AUTHORITIES, encodedAuthorities) + } + + /** + * @return credentials,采用 jwt 的 id + * @inheritDoc + */ + override fun getCredentials(): Any { + return jwtId!! + } + + override fun getDetails(): Any? { + return null + } + + /** + * @return principal,采用 jwt 的 subject + * @inheritDoc + */ + override fun getPrincipal(): Any { + return subject + } + + override fun isAuthenticated(): Boolean { + return this.authenticated + } + + @Throws(IllegalArgumentException::class) + override fun setAuthenticated(isAuthenticated: Boolean) { + this.authenticated = isAuthenticated + } + + override fun getName(): String { + return subject + } + + companion object { + /** + * AuthToken 类型值 + */ + const val TYPE: String = "auth" + private const val CLAIM_AUTHORITIES = "Authorities" + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/jwt/JwtExpiredException.kt b/src/main/kotlin/plus/maa/backend/service/jwt/JwtExpiredException.kt new file mode 100644 index 00000000..7e693683 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/jwt/JwtExpiredException.kt @@ -0,0 +1,3 @@ +package plus.maa.backend.service.jwt + +class JwtExpiredException(message: String) : Exception(message) diff --git a/src/main/kotlin/plus/maa/backend/service/jwt/JwtInvalidException.kt b/src/main/kotlin/plus/maa/backend/service/jwt/JwtInvalidException.kt new file mode 100644 index 00000000..492b70ef --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/jwt/JwtInvalidException.kt @@ -0,0 +1,3 @@ +package plus.maa.backend.service.jwt + +class JwtInvalidException : Exception() diff --git a/src/main/kotlin/plus/maa/backend/service/jwt/JwtRefreshToken.kt b/src/main/kotlin/plus/maa/backend/service/jwt/JwtRefreshToken.kt new file mode 100644 index 00000000..c85c817c --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/jwt/JwtRefreshToken.kt @@ -0,0 +1,30 @@ +package plus.maa.backend.service.jwt + +import java.time.Instant + +class JwtRefreshToken : JwtToken { + /** + * 从 jwt 构建 token + * + * @param token jwt + * @param key 签名密钥 + * @throws JwtInvalidException jwt 未通过签名验证或不符合要求 + */ + constructor(token: String, key: ByteArray) : super(token, TYPE, key) + + constructor( + sub: String, + jti: String?, + iat: Instant, + exp: Instant, + nbf: Instant, + key: ByteArray + ) : super(sub, jti, iat, exp, nbf, TYPE, key) + + companion object { + /** + * RefreshToken 类型值 + */ + const val TYPE: String = "refresh" + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/jwt/JwtService.kt b/src/main/kotlin/plus/maa/backend/service/jwt/JwtService.kt new file mode 100644 index 00000000..328492d0 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/jwt/JwtService.kt @@ -0,0 +1,86 @@ +package plus.maa.backend.service.jwt + +import org.springframework.security.core.GrantedAuthority +import org.springframework.stereotype.Service +import plus.maa.backend.config.external.MaaCopilotProperties +import java.time.Instant + +/** + * 基于 Jwt 的 token 服务。 可直接用于 stateless 情境下的签发和认证, 或结合数据库进行状态管理。 + * 建议 AuthToken 使用无状态方案, RefreshToken 使用有状态方案 + */ +@Service +class JwtService(properties: MaaCopilotProperties) { + private val jwtProperties = properties.jwt + private val key = jwtProperties.secret.toByteArray() + + /** + * 签发 AuthToken. 过期时间由配置的 jwt.expire 计算而来 + * + * @param subject 签发对象,一般设置为对象的标识符 + * @param jwtId jwt 的 id, 一般用于 stateful 场景下 + * @param authorities 授予的权限 + * @return JwtAuthToken + */ + fun issueAuthToken(subject: String, jwtId: String?, authorities: Collection): JwtAuthToken { + val now = Instant.now() + val expireAt = now.plusSeconds(jwtProperties.expire) + return JwtAuthToken(subject, jwtId, now, expireAt, now, authorities, key) + } + + /** + * 验证并解析为 AuthToken. 该方法为 stateless 的验证。 + * + * @param authToken jwt 字符串 + * @return JwtAuthToken + * @throws JwtInvalidException jwt不符合要求 + * @throws JwtExpiredException jwt未生效或者已过期 + */ + @Throws(JwtInvalidException::class, JwtExpiredException::class) + fun verifyAndParseAuthToken(authToken: String): JwtAuthToken { + val token = JwtAuthToken(authToken, key) + token.validateDate(Instant.now()) + token.isAuthenticated = true + return token + } + + /** + * 签发 RefreshToken. 过期时间由配置的 Jwt.getRefreshExpire 计算而来 + * + * @param subject 签发对象,一般设置为对象的标识符 + * @param jwtId jwt 的 id, 一般用于 stateful 场景下 + * @return JwtAuthToken + */ + fun issueRefreshToken(subject: String, jwtId: String?): JwtRefreshToken { + val now = Instant.now() + val expireAt = now.plusSeconds(jwtProperties.refreshExpire) + return JwtRefreshToken(subject, jwtId, now, expireAt, now, key) + } + + /** + * 产生新的 RefreshToken. 新的 token 除了签发和生效时间、 id 不同外,其余属性均继承自原来的 token. + * 一般情况下, RefreshToken 应结合数据库使用以避免陷入无法撤销的窘境 + * + * @param old 原 token + * @return 新的 RefreshToken + */ + fun newRefreshToken(old: JwtRefreshToken, jwtId: String?): JwtRefreshToken { + val now = Instant.now() + return JwtRefreshToken(old.subject, jwtId, now, old.expiresAt, now, key) + } + + /** + * 验证并解析为 RefreshToken. 该方法为 stateless 的验证。 + * + * @param refreshToken jwt字符串 + * @return RefreshToken + * @throws JwtInvalidException jwt不符合要求 + * @throws JwtExpiredException jwt未生效或者已过期 + */ + @Throws(JwtInvalidException::class, JwtExpiredException::class) + fun verifyAndParseRefreshToken(refreshToken: String): JwtRefreshToken { + val token = JwtRefreshToken(refreshToken, key) + token.validateDate(Instant.now()) + return token + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/jwt/JwtToken.kt b/src/main/kotlin/plus/maa/backend/service/jwt/JwtToken.kt new file mode 100644 index 00000000..f87a5910 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/jwt/JwtToken.kt @@ -0,0 +1,83 @@ +package plus.maa.backend.service.jwt + +import cn.hutool.json.JSONObject +import cn.hutool.jwt.JWT +import cn.hutool.jwt.JWTUtil +import cn.hutool.jwt.RegisteredPayload +import java.time.Instant +import java.util.* + +/** + * 对 [JWT] 的包装增强,某些 payload 被标记为 MUST + */ +open class JwtToken { + protected val jwt: JWT + + private val payload: JSONObject + + constructor(token: String, requiredType: String, key: ByteArray) { + if (!JWTUtil.verify(token, key)) throw JwtInvalidException() + this.jwt = JWTUtil.parseToken(token) + jwt.setKey(key) + this.payload = jwt.payloads + + // jwtId is nullable + if (requiredType != type + || payload.getStr(RegisteredPayload.SUBJECT) == null + || payload.getLong(RegisteredPayload.ISSUED_AT) == null + || payload.getLong(RegisteredPayload.EXPIRES_AT) == null + || payload.getLong(RegisteredPayload.NOT_BEFORE) == null + ) throw JwtInvalidException() + } + + constructor( + sub: String?, + jti: String?, + iat: Instant, + exp: Instant, + nbf: Instant, + typ: String?, + key: ByteArray + ) { + jwt = JWT.create() + jwt.setPayload(RegisteredPayload.SUBJECT, sub) + jwt.setPayload(RegisteredPayload.JWT_ID, jti) + jwt.setPayload(RegisteredPayload.ISSUED_AT, iat.toEpochMilli()) + jwt.setPayload(RegisteredPayload.EXPIRES_AT, exp.toEpochMilli()) + jwt.setPayload(RegisteredPayload.NOT_BEFORE, nbf.toEpochMilli()) + jwt.setPayload(CLAIM_TYPE, typ) + jwt.setKey(key) + payload = jwt.payloads + } + + val subject: String get() = payload.getStr(RegisteredPayload.SUBJECT) + + val jwtId: String? get() = payload.getStr(RegisteredPayload.JWT_ID) + + val issuedAt: Instant get() = Instant.ofEpochMilli(payload.getLong(RegisteredPayload.ISSUED_AT)) + + val expiresAt: Instant get() = Instant.ofEpochMilli(payload.getLong(RegisteredPayload.EXPIRES_AT)) + + val notBefore: Instant get() = Instant.ofEpochMilli(payload.getLong(RegisteredPayload.NOT_BEFORE)) + + var type: String? + get() = payload.getStr(CLAIM_TYPE) + set(type) { + payload[CLAIM_TYPE] = type + } + + /** + * 签名后的 jwt 字符串 + */ + val value: String get() = jwt.sign() + + @Throws(JwtExpiredException::class) + fun validateDate(moment: Instant) { + if (!moment.isBefore(expiresAt)) throw JwtExpiredException("expired") + if (moment.isBefore(notBefore)) throw JwtExpiredException("haven't take effect") + } + + companion object { + private const val CLAIM_TYPE = "typ" + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/model/ArkLevelType.kt b/src/main/kotlin/plus/maa/backend/service/model/ArkLevelType.kt new file mode 100644 index 00000000..7e972309 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/model/ArkLevelType.kt @@ -0,0 +1,39 @@ +package plus.maa.backend.service.model + +import java.util.* + +enum class ArkLevelType(val display: String) { + MAINLINE("主题曲"), + WEEKLY("资源收集"), + ACTIVITIES("活动关卡"), + CAMPAIGN("剿灭作战"), + MEMORY("悖论模拟"), + RUNE("危机合约"), + LEGION("保全派驻"), + ROGUELIKE("集成战略"), //实际不进行解析 + TRAINING("训练关卡"), //实际不进行解析 + UNKNOWN("未知类型"); + + companion object { + fun fromLevelId(levelId: String?): ArkLevelType { + if (levelId.isNullOrBlank()) { + return UNKNOWN + } + val ids = levelId.lowercase(Locale.getDefault()).split("/".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + val type = if ((ids[0] == "obt")) ids[1] else ids[0] + return when (type) { + "main", "hard" -> MAINLINE + "weekly", "promote" -> WEEKLY + "activities" -> ACTIVITIES + "campaign" -> CAMPAIGN + "memory" -> MEMORY + "rune", "crisis" -> RUNE + "legion" -> LEGION + "roguelike" -> ROGUELIKE + "training" -> TRAINING + else -> UNKNOWN + } + } + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/model/CommentNotification.kt b/src/main/kotlin/plus/maa/backend/service/model/CommentNotification.kt new file mode 100644 index 00000000..29553803 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/model/CommentNotification.kt @@ -0,0 +1,13 @@ +package plus.maa.backend.service.model + +/** + * @author LoMu + * Date 2023-05-18 1:18 + */ +data class CommentNotification( + val authorName: String, + val reName: String, + val date: String, + val title: String?, + val reMessage: String +) diff --git a/src/main/kotlin/plus/maa/backend/service/model/CopilotSetStatus.kt b/src/main/kotlin/plus/maa/backend/service/model/CopilotSetStatus.kt new file mode 100644 index 00000000..27933b58 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/model/CopilotSetStatus.kt @@ -0,0 +1,17 @@ +package plus.maa.backend.service.model + +/** + * @author dragove + * create on 2024-01-01 + */ +enum class CopilotSetStatus { + /** + * 私有,仅查看自己的作业集的时候展示,其他列表页面不展示,但是通过详情接口可查询(无权限控制) + */ + PRIVATE, + + /** + * 公开,可以被搜索 + */ + PUBLIC, +} diff --git a/src/main/kotlin/plus/maa/backend/service/model/LoginUser.kt b/src/main/kotlin/plus/maa/backend/service/model/LoginUser.kt new file mode 100644 index 00000000..7e782dea --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/model/LoginUser.kt @@ -0,0 +1,62 @@ +package plus.maa.backend.service.model + +import com.fasterxml.jackson.annotation.JsonIgnore +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.userdetails.UserDetails +import plus.maa.backend.repository.entity.MaaUser + +/** + * @author AnselYuki + */ +class LoginUser( + private val maaUser: MaaUser, + private val authorities: Collection) : UserDetails { + @JsonIgnore + override fun getAuthorities(): Collection { + return authorities + } + + @JsonIgnore + override fun getPassword(): String { + return maaUser.password + } + + val userId: String? + get() = maaUser.userId + + /** + * Spring Security框架中的username即唯一身份标识(ID) + * 效果同getEmail + * + * @return 用户邮箱 + */ + @JsonIgnore + override fun getUsername(): String { + return maaUser.email + } + + @get:JsonIgnore + val email: String + get() = maaUser.email + + override fun isAccountNonExpired(): Boolean { + return true + } + + override fun isAccountNonLocked(): Boolean { + return true + } + + override fun isCredentialsNonExpired(): Boolean { + return true + } + + /** + * 默认用户为0(禁用),1为启用 + * + * @return 账户启用状态 + */ + override fun isEnabled(): Boolean { + return true + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/model/RatingCache.kt b/src/main/kotlin/plus/maa/backend/service/model/RatingCache.kt new file mode 100644 index 00000000..6af878e3 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/model/RatingCache.kt @@ -0,0 +1,9 @@ +package plus.maa.backend.service.model + +/** + * @author LoMu + * Date 2023-01-28 11:37 + */ +data class RatingCache( + val copilotIds: MutableSet +) diff --git a/src/main/kotlin/plus/maa/backend/service/model/RatingCount.kt b/src/main/kotlin/plus/maa/backend/service/model/RatingCount.kt new file mode 100644 index 00000000..5362df7c --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/model/RatingCount.kt @@ -0,0 +1,6 @@ +package plus.maa.backend.service.model + +data class RatingCount( + val key: String, + val count: Long = 0 +) diff --git a/src/main/kotlin/plus/maa/backend/service/model/RatingType.kt b/src/main/kotlin/plus/maa/backend/service/model/RatingType.kt new file mode 100644 index 00000000..e63b19be --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/model/RatingType.kt @@ -0,0 +1,29 @@ +package plus.maa.backend.service.model + +/** + * @author LoMu + * Date 2023-01-22 19:48 + */ +enum class RatingType(val display: Int) { + LIKE(1), + DISLIKE(2), + NONE(0); + + companion object { + /** + * 将rating转换为 0 = NONE 1 = LIKE 2 = DISLIKE + * + * @param type rating + * @return type + */ + fun fromRatingType(type: String?): RatingType { + return when (type) { + "Like" -> LIKE + "Dislike" -> DISLIKE + else -> NONE + } + } + } +} + + diff --git a/src/main/kotlin/plus/maa/backend/service/model/parser/ActivityParser.kt b/src/main/kotlin/plus/maa/backend/service/model/parser/ActivityParser.kt new file mode 100644 index 00000000..22eb9db9 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/model/parser/ActivityParser.kt @@ -0,0 +1,37 @@ +package plus.maa.backend.service.model.parser + +import org.springframework.stereotype.Component +import plus.maa.backend.repository.entity.ArkLevel +import plus.maa.backend.repository.entity.gamedata.ArkTilePos +import plus.maa.backend.service.ArkGameDataService +import plus.maa.backend.service.model.ArkLevelType + + +/** + * @author john180 + * + * + * Activity level will be tagged like this:

+ * Activity -> ACT_NAME -> StageCode == activities/ACT_ID/LEVEL_ID

+ * eg:

+ * 活动关卡 -> 战地秘闻 -> SW-EV-1 == activities/act4d0/level_act4d0_01

+ */ +@Component +class ActivityParser( + private val dataService: ArkGameDataService +) : ArkLevelParser { + + override fun supportType(type: ArkLevelType): Boolean { + return ArkLevelType.ACTIVITIES == type + } + + override fun parseLevel(level: ArkLevel, tilePos: ArkTilePos): ArkLevel? { + level.catOne = ArkLevelType.ACTIVITIES.display + + val stage = dataService.findStage(level.levelId!!, tilePos.code!!, tilePos.stageId!!) + level.catTwo = stage?.zoneId + ?.let { dataService.findActivityByZoneId(it) } + ?.name ?: "" + return level + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/model/parser/ArkLevelParser.kt b/src/main/kotlin/plus/maa/backend/service/model/parser/ArkLevelParser.kt new file mode 100644 index 00000000..b95eb069 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/model/parser/ArkLevelParser.kt @@ -0,0 +1,23 @@ +package plus.maa.backend.service.model.parser + +import plus.maa.backend.repository.entity.ArkLevel +import plus.maa.backend.repository.entity.gamedata.ArkTilePos +import plus.maa.backend.service.model.ArkLevelType + +/** + * @author john180 + */ +interface ArkLevelParser { + /** + * 是否支持解析该关卡类型 + * + * @param type 关卡类型 + * @return 是否支持 + */ + fun supportType(type: ArkLevelType): Boolean + + /** + * 解析关卡 + */ + fun parseLevel(level: ArkLevel, tilePos: ArkTilePos): ArkLevel? +} diff --git a/src/main/kotlin/plus/maa/backend/service/model/parser/CampaignParser.kt b/src/main/kotlin/plus/maa/backend/service/model/parser/CampaignParser.kt new file mode 100644 index 00000000..63e69acc --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/model/parser/CampaignParser.kt @@ -0,0 +1,29 @@ +package plus.maa.backend.service.model.parser + +import org.springframework.stereotype.Component +import plus.maa.backend.repository.entity.ArkLevel +import plus.maa.backend.repository.entity.gamedata.ArkTilePos +import plus.maa.backend.service.model.ArkLevelType + +/** + * @author john180 + * + * + * Campaign level will be tagged like this:

+ * CAMPAIGN -> CAMPAIGN_CODE -> CAMPAIGN_NAME == obt/campaign/LEVEL_ID

+ * eg:

+ * 剿灭作战 -> 炎国 -> 龙门外环 == obt/campaign/level_camp_02

+ */ +@Component +class CampaignParser : ArkLevelParser { + override fun supportType(type: ArkLevelType): Boolean { + return ArkLevelType.CAMPAIGN == type + } + + override fun parseLevel(level: ArkLevel, tilePos: ArkTilePos): ArkLevel? { + level.catOne = ArkLevelType.CAMPAIGN.display + level.catTwo = tilePos.code + level.catThree = level.name + return level + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/model/parser/LegionParser.kt b/src/main/kotlin/plus/maa/backend/service/model/parser/LegionParser.kt new file mode 100644 index 00000000..afd8d71a --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/model/parser/LegionParser.kt @@ -0,0 +1,45 @@ +package plus.maa.backend.service.model.parser + +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.stereotype.Component +import plus.maa.backend.repository.entity.ArkLevel +import plus.maa.backend.repository.entity.gamedata.ArkTilePos +import plus.maa.backend.service.ArkGameDataService +import plus.maa.backend.service.model.ArkLevelType + +private val log = KotlinLogging.logger { } + +/** + * @author john180 + * + * + * Legion level will be tagged like this:

+ * LEGION -> POSITION -> StageCode == obt/legion/TOWER_ID/LEVEL_ID

+ * eg:

+ * 保全派驻 -> 阿卡胡拉丛林 -> LT-1 == obt/legion/lt06/level_lt06_01

+ */ +@Component +class LegionParser( + private val dataService: ArkGameDataService +) : ArkLevelParser { + + override fun supportType(type: ArkLevelType): Boolean { + return ArkLevelType.LEGION == type + } + + override fun parseLevel(level: ArkLevel, tilePos: ArkTilePos): ArkLevel? { + level.catOne = ArkLevelType.LEGION.display + + val stage = dataService.findStage(level.levelId!!, tilePos.code!!, tilePos.stageId!!) + if (stage == null) { + log.error { "[PARSER]保全派驻关卡未找到stage信息: ${level.levelId}" } + return null + } + + val catTwo: String = dataService.findTower(stage.zoneId)?.name ?: "" + + level.catTwo = catTwo + level.catThree = tilePos.code + return level + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/model/parser/MainlineParser.kt b/src/main/kotlin/plus/maa/backend/service/model/parser/MainlineParser.kt new file mode 100644 index 00000000..1f1472d1 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/model/parser/MainlineParser.kt @@ -0,0 +1,75 @@ +package plus.maa.backend.service.model.parser + +import org.springframework.stereotype.Component +import org.springframework.util.ObjectUtils +import plus.maa.backend.repository.entity.ArkLevel +import plus.maa.backend.repository.entity.gamedata.ArkTilePos +import plus.maa.backend.repository.entity.gamedata.ArkZone +import plus.maa.backend.service.ArkGameDataService +import plus.maa.backend.service.model.ArkLevelType +import java.util.* + +/** + * @author john180 + * + * + * Main story level will be tagged like this:

+ * MAINLINE -> CHAPTER_NAME -> StageCode == obt/main/LEVEL_ID

+ * eg:

+ * 主题曲 -> 序章:黑暗时代·上 -> 0-1 == obt/main/level_main_00-01

+ * 主题曲 -> 第四章:急性衰竭 -> S4-7 == obt/main/level_sub_04-3-1

+ */ +@Component +class MainlineParser( + private val dataService: ArkGameDataService +) : ArkLevelParser { + + override fun supportType(type: ArkLevelType): Boolean { + return ArkLevelType.MAINLINE == type + } + + override fun parseLevel(level: ArkLevel, tilePos: ArkTilePos): ArkLevel? { + level.catOne = ArkLevelType.MAINLINE.display + + val chapterLevelId = + level.levelId!!.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[2] // level_main_10-02 + val chapterStrSplit = + chapterLevelId.split("_".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() // level main 10-02 + val diff = parseDifficulty(chapterStrSplit[1]) // easy、main + val stageCodeEncoded = + chapterStrSplit[chapterStrSplit.size - 1] // 10-02 remark: obt/main/level_easy_sub_09-1-1 + val chapterStr = + stageCodeEncoded.split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0] // 10 (str) + val chapter = chapterStr.toInt() // 10 (int) + + val zone = dataService.findZone(level.levelId, tilePos.code!!, tilePos.stageId!!) ?: return null + + val catTwo = parseZoneName(zone) + level.catTwo = catTwo + + val catThreeEx = if ((chapter >= 9)) String.format("(%s)", diff) else "" + level.catThree += catThreeEx + + return level + } + + private fun parseDifficulty(diff: String): String { + return when (diff.lowercase(Locale.getDefault())) { + "easy" -> "简单" + "tough" -> "磨难" + else -> "标准" + } + } + + private fun parseZoneName(zone: ArkZone): String { + val builder = StringBuilder() + if (!ObjectUtils.isEmpty(zone.zoneNameFirst)) { + builder.append(zone.zoneNameFirst) + } + builder.append(" ") + if (!ObjectUtils.isEmpty(zone.zoneNameSecond)) { + builder.append(zone.zoneNameSecond) + } + return builder.toString().trim { it <= ' ' } + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/model/parser/MemoryParser.kt b/src/main/kotlin/plus/maa/backend/service/model/parser/MemoryParser.kt new file mode 100644 index 00000000..df191a8c --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/model/parser/MemoryParser.kt @@ -0,0 +1,66 @@ +package plus.maa.backend.service.model.parser + +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.stereotype.Component +import plus.maa.backend.repository.entity.ArkLevel +import plus.maa.backend.repository.entity.gamedata.ArkTilePos +import plus.maa.backend.service.ArkGameDataService +import plus.maa.backend.service.model.ArkLevelType +import java.util.* + +private val log = KotlinLogging.logger { } + +/** + * @author john180 + * + * + * Memory level will be tagged like this:

+ * MEMORY -> POSITION -> OPERATOR_NAME == obt/memory/LEVEL_ID

+ * eg:

+ * 悖论模拟 -> 狙击 -> 克洛丝 == obt/memory/level_memory_kroos_1

+ */ +@Component +class MemoryParser( + val dataService: ArkGameDataService +) : ArkLevelParser { + + override fun supportType(type: ArkLevelType): Boolean { + return ArkLevelType.MEMORY == type + } + + override fun parseLevel(level: ArkLevel, tilePos: ArkTilePos): ArkLevel? { + level.catOne = ArkLevelType.MEMORY.display + + val chIdSplit = + level.stageId!!.split("_".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() //mem_aurora_1 + if (chIdSplit.size != 3) { + log.error { "[PARSER]悖论模拟关卡stageId异常: ${level.stageId}, level: ${level.levelId}" } + return null + } + val chId = chIdSplit[1] //aurora + val character = dataService.findCharacter(chId) + if (character == null) { + log.error { "[PARSER]悖论模拟关卡未找到角色信息: ${level.stageId}, level: ${level.levelId}" } + return null + } + + level.catTwo = parseProfession(character.profession) + level.catThree = character.name + + return level + } + + private fun parseProfession(professionId: String): String { + return when (professionId.lowercase(Locale.getDefault())) { + "medic" -> "医疗" + "special" -> "特种" + "warrior" -> "近卫" + "sniper" -> "狙击" + "tank" -> "重装" + "caster" -> "术师" + "pioneer" -> "先锋" + "support" -> "辅助" + else -> "未知" + } + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/model/parser/RuneParser.kt b/src/main/kotlin/plus/maa/backend/service/model/parser/RuneParser.kt new file mode 100644 index 00000000..ce281d49 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/model/parser/RuneParser.kt @@ -0,0 +1,27 @@ +package plus.maa.backend.service.model.parser + +import org.springframework.stereotype.Component +import plus.maa.backend.repository.entity.ArkLevel +import plus.maa.backend.repository.entity.gamedata.ArkTilePos +import plus.maa.backend.service.ArkGameDataService +import plus.maa.backend.service.model.ArkLevelType + +@Component +class RuneParser( + private val dataService: ArkGameDataService +) : ArkLevelParser { + + override fun supportType(type: ArkLevelType): Boolean { + return ArkLevelType.RUNE == type + } + + override fun parseLevel(level: ArkLevel, tilePos: ArkTilePos): ArkLevel? { + level.catOne = ArkLevelType.RUNE.display + level.catTwo = level.stageId + ?.let { dataService.findCrisisV2InfoById(it) } + ?.name ?: tilePos.code + + level.catThree = level.name + return level + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/model/parser/UnknownParser.kt b/src/main/kotlin/plus/maa/backend/service/model/parser/UnknownParser.kt new file mode 100644 index 00000000..840df945 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/model/parser/UnknownParser.kt @@ -0,0 +1,24 @@ +package plus.maa.backend.service.model.parser + +import org.springframework.stereotype.Component +import plus.maa.backend.repository.entity.ArkLevel +import plus.maa.backend.repository.entity.gamedata.ArkTilePos +import plus.maa.backend.service.model.ArkLevelType +import java.util.* + +@Component +class UnknownParser : ArkLevelParser { + override fun supportType(type: ArkLevelType): Boolean { + return ArkLevelType.UNKNOWN == type + } + + override fun parseLevel(level: ArkLevel, tilePos: ArkTilePos): ArkLevel? { + val ids = level.levelId!!.lowercase(Locale.getDefault()) + .split("/".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + val type = if ((ids[0] == "obt")) ids[1] else ids[0] + + level.catOne = ArkLevelType.UNKNOWN.display + type + return level + } +} diff --git a/src/main/kotlin/plus/maa/backend/service/model/parser/WeeklyParser.kt b/src/main/kotlin/plus/maa/backend/service/model/parser/WeeklyParser.kt new file mode 100644 index 00000000..ab818bf9 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/service/model/parser/WeeklyParser.kt @@ -0,0 +1,37 @@ +package plus.maa.backend.service.model.parser + +import org.springframework.stereotype.Component +import plus.maa.backend.repository.entity.ArkLevel +import plus.maa.backend.repository.entity.gamedata.ArkTilePos +import plus.maa.backend.service.ArkGameDataService +import plus.maa.backend.service.model.ArkLevelType + +/** + * @author john180 + * + * + * Weekly level will be tagged like this:

+ * WEEKLY -> WEEKLY_ZONE_NAME -> StageCode == obt/weekly/LEVEL_ID

+ * eg:

+ * 资源收集 -> 空中威胁 -> CA-5 == obt/weekly/level_weekly_fly_5

+ * 资源收集 -> 身先士卒 -> PR-D-2 == obt/promote/level_promote_d_2

+ */ +@Component +class WeeklyParser( + private val dataService: ArkGameDataService +) : ArkLevelParser { + + override fun supportType(type: ArkLevelType): Boolean { + return ArkLevelType.WEEKLY == type + } + + override fun parseLevel(level: ArkLevel, tilePos: ArkTilePos): ArkLevel? { + level.catOne = ArkLevelType.WEEKLY.display + + val zone = dataService.findZone(level.levelId!!, tilePos.code!!, tilePos.stageId!!) + ?: return null + + level.catTwo = zone.zoneNameSecond + return level + } +} diff --git a/src/main/kotlin/plus/maa/backend/task/ArkLevelSyncTask.kt b/src/main/kotlin/plus/maa/backend/task/ArkLevelSyncTask.kt new file mode 100644 index 00000000..fc0b1284 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/task/ArkLevelSyncTask.kt @@ -0,0 +1,30 @@ +package plus.maa.backend.task + +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component +import plus.maa.backend.service.ArkLevelService + +@Component +class ArkLevelSyncTask( + private val arkLevelService: ArkLevelService +) { + + /** + * 地图数据同步定时任务,每10分钟执行一次 + * 应用启动时自动同步一次 + */ + @Scheduled(cron = "\${maa-copilot.task-cron.ark-level:-}") + fun syncArkLevels() { + arkLevelService.runSyncLevelDataTask() + } + + /** + * 更新开放状态,每天凌晨执行,最好和热度值刷入任务保持相对顺序 + * 4:00、4:15 各执行一次,避免网络波动导致更新失败 + */ + @Scheduled(cron = "0 0-15/15 4 * * ?") + fun updateOpenStatus() { + arkLevelService.updateActivitiesOpenStatus() + arkLevelService.updateCrisisV2OpenStatus() + } +} diff --git a/src/main/kotlin/plus/maa/backend/task/CopilotBackupTask.kt b/src/main/kotlin/plus/maa/backend/task/CopilotBackupTask.kt new file mode 100644 index 00000000..428658d3 --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/task/CopilotBackupTask.kt @@ -0,0 +1,180 @@ +package plus.maa.backend.task + +import io.github.oshai.kotlinlogging.KotlinLogging +import jakarta.annotation.PostConstruct +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.api.TransportConfigCallback +import org.eclipse.jgit.api.errors.GitAPIException +import org.eclipse.jgit.lib.PersonIdent +import org.eclipse.jgit.transport.SshTransport +import org.eclipse.jgit.transport.Transport +import org.eclipse.jgit.transport.sshd.SshdSessionFactoryBuilder +import org.eclipse.jgit.util.FS +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component +import plus.maa.backend.config.external.MaaCopilotProperties +import plus.maa.backend.repository.CopilotRepository +import plus.maa.backend.repository.entity.Copilot +import plus.maa.backend.service.ArkLevelService +import java.io.File +import java.io.IOException +import java.nio.file.Files +import java.time.LocalDate +import java.util.* + +private val log = KotlinLogging.logger { } + +/** + * CopilotBackupTask + */ +@Component +class CopilotBackupTask( + private val config: MaaCopilotProperties, + private val copilotRepository: CopilotRepository, + private val levelService: ArkLevelService, +) { + + private lateinit var git: Git + + /** + * 初始化Git对象,如果目录已经存在且存在文件,则直接当作git仓库,如果不存在则clone仓库 + */ + @PostConstruct + fun initGit() { + val backup = config.backup + if (backup.disabled) { + return + } + val repoDir = File(backup.dir) + if (repoDir.mkdirs()) { + log.info { "directory not exist, created: ${backup.dir}" } + } else { + log.info { "directory already exists, dir: ${backup.dir}" } + } + if (!repoDir.isDirectory) { + return + } + try { + Files.list(repoDir.toPath()).use { fileList -> + git = if (fileList.findFirst().isEmpty) { + // 不存在文件则初始化 + Git.cloneRepository() + .setURI(backup.uri) + .setDirectory(repoDir) + .setTransportConfigCallback(sshCallback) + .call() + } else { + Git.open(repoDir) + } + } + } catch (e: IOException) { + log.error { "init copilot backup repo failed, repoDir: $repoDir, $e"} + } catch (e: GitAPIException) { + log.error { "init copilot backup repo failed, repoDir: $repoDir, $e" } + } + } + + /** + * copilot数据同步定时任务,每天执行一次 + */ + @Scheduled(cron = "\${maa-copilot.task-cron.copilot-update:-}") + fun backupCopilots() { + if (config.backup.disabled || Objects.isNull(git)) { + return + } + try { + git.pull().call() + } catch (e: GitAPIException) { + log.error { "git pull execute failed, msg: ${e.message}, $e" } + } + + val baseDirectory = git.repository.workTree + val copilots = copilotRepository.findAll() + copilots.forEach{ copilot: Copilot -> + val level = levelService.findByLevelIdFuzzy(copilot.stageName!!) ?: return@forEach + // 暂时使用 copilotId 作为文件名 + val filePath = File( + java.lang.String.join( + File.separator, baseDirectory.path, level.catOne, + level.catTwo, level.catThree, copilot.copilotId.toString() + ".json" + ) + ) + val content = copilot.content ?: return@forEach + if (copilot.delete) { + // 删除文件 + deleteCopilot(filePath) + } else { + // 创建或者修改文件 + upsertCopilot(filePath, content) + } + } + + doCommitAndPush() + } + + private fun upsertCopilot(file: File, content: String) { + if (!file.exists()) { + if (!file.parentFile.mkdirs()) { + log.warn { "folder may exists, mkdir failed" } + } + } + try { + Files.writeString(file.toPath(), content) + } catch (e: IOException) { + log.error { "write file failed, path: ${file.path}, message: ${e.message}, $e" } + } + } + + private fun deleteCopilot(file: File) { + if (file.exists()) { + if (file.delete()) { + log.info { "delete copilot file: ${file.path}" } + } else { + log.error { "delete copilot failed, file: ${file.path}" } + } + } else { + log.info { "file does not exists, no need to delete" } + } + } + + private fun doCommitAndPush() { + try { + val status = git.status().call() + if (status.added.isEmpty() && + status.changed.isEmpty() && + status.removed.isEmpty() && + status.untracked.isEmpty() && + status.modified.isEmpty() && + status.added.isEmpty() + ) { + log.info { "copilot backup with no new added or changes" } + return + } + git.add().addFilepattern(".").call() + val backup = config.backup + val committer = PersonIdent(backup.username, backup.email) + git.commit().setCommitter(committer) + .setMessage(LocalDate.now().toString()) + .call() + git.push() + .setTransportConfigCallback(sshCallback) + .call() + } catch (e: GitAPIException) { + log.error { "git committing failed, msg: ${e.message}, $e" } + } + } + + companion object { + private val DEFAULT_SSH_DIR = File(FS.DETECTED.userHome(), "/.ssh") + + private val sshCallback = TransportConfigCallback { transport: Transport -> + if (transport is SshTransport) { + transport.sshSessionFactory = SshdSessionFactoryBuilder() + .setPreferredAuthentications("publickey") + .setHomeDirectory(FS.DETECTED.userHome()) + .setSshDirectory(DEFAULT_SSH_DIR) + .build(null) + } + } + } +} diff --git a/src/main/kotlin/plus/maa/backend/task/CopilotScoreRefreshTask.kt b/src/main/kotlin/plus/maa/backend/task/CopilotScoreRefreshTask.kt new file mode 100644 index 00000000..4c42818a --- /dev/null +++ b/src/main/kotlin/plus/maa/backend/task/CopilotScoreRefreshTask.kt @@ -0,0 +1,130 @@ +package plus.maa.backend.task + +import org.springframework.data.domain.Pageable +import org.springframework.data.mongodb.core.MongoTemplate +import org.springframework.data.mongodb.core.aggregation.Aggregation +import org.springframework.data.mongodb.core.query.Criteria +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component +import plus.maa.backend.repository.CopilotRepository +import plus.maa.backend.repository.RedisCache +import plus.maa.backend.repository.entity.Copilot +import plus.maa.backend.repository.entity.Rating +import plus.maa.backend.service.ArkLevelService +import plus.maa.backend.service.CopilotService.Companion.getHotScore +import plus.maa.backend.service.model.RatingCount +import plus.maa.backend.service.model.RatingType +import java.time.LocalDateTime + +/** + * 作业热度值刷入任务,每日执行,用于计算基于时间的热度值 + * + * @author dove + * created on 2023.05.03 + */ +@Component +class CopilotScoreRefreshTask( + private val arkLevelService: ArkLevelService, + private val redisCache: RedisCache, + private val copilotRepository: CopilotRepository, + private val mongoTemplate: MongoTemplate +) { + /** + * 热度值刷入任务,每日四点三十执行(实际可能会更晚,因为需要等待之前启动的定时任务完成) + */ + @Scheduled(cron = "0 30 4 * * ?") + fun refreshHotScores() { + // 分页获取所有未删除的作业 + var pageable = Pageable.ofSize(1000) + var copilots = copilotRepository.findAllByDeleteIsFalse(pageable) + + // 循环读取直到没有未删除的作业为止 + while (copilots.hasContent()) { + val copilotIdSTRs = copilots.map { copilot -> + copilot.copilotId.toString() + }.toList() + refresh(copilotIdSTRs, copilots) + // 获取下一页 + if (!copilots.hasNext()) { + // 没有下一页了,跳出循环 + break + } + pageable = copilots.nextPageable() + copilots = copilotRepository.findAllByDeleteIsFalse(pageable) + } + + // 移除首页热度缓存 + redisCache.syncRemoveCacheByPattern("home:hot:*") + } + + /** + * 刷入评分变更数 Top 100 的热度值,每日八点到二十点每三小时执行一次 + */ + @Scheduled(cron = "0 0 8-20/3 * * ?") + fun refreshTop100HotScores() { + val copilotIdSTRs = redisCache.getZSetReverse("rate:hot:copilotIds", 0, 99) + if (copilotIdSTRs.isNullOrEmpty()) { + return + } + + val copilots = copilotRepository.findByCopilotIdInAndDeleteIsFalse( + copilotIdSTRs.map { s: String? -> s!!.toLong() } + ) + if (copilots.isEmpty()) { + return + } + + refresh(copilotIdSTRs, copilots) + + // 移除近期评分变化量缓存 + redisCache.removeCache("rate:hot:copilotIds") + // 移除首页热度缓存 + redisCache.syncRemoveCacheByPattern("home:hot:*") + } + + private fun refresh(copilotIdSTRs: Collection, copilots: Iterable) { + // 批量获取最近七天的点赞和点踩数量 + val now = LocalDateTime.now() + val likeCounts = counts(copilotIdSTRs, RatingType.LIKE, now.minusDays(7)) + val dislikeCounts = counts(copilotIdSTRs, RatingType.DISLIKE, now.minusDays(7)) + val likeCountMap = likeCounts.associate { it.key to it.count } + val dislikeCountMap = dislikeCounts.associate { it.key to it.count } + // 计算热度值 + for (copilot in copilots) { + val likeCount = likeCountMap.getOrDefault(copilot.copilotId.toString(), 1L) + val dislikeCount = dislikeCountMap.getOrDefault(copilot.copilotId.toString(), 0L) + var hotScore = getHotScore(copilot, likeCount, dislikeCount) + // 判断关卡是否开放 + val level = arkLevelService.findByLevelIdFuzzy(copilot.stageName!!) + // 关卡已关闭,且作业在关闭前上传 + if (level!!.closeTime != null + && copilot.firstUploadTime != null + && false == level.isOpen + && copilot.firstUploadTime!!.isBefore(level.closeTime) + ) { + // 非开放关卡打入冷宫 + + hotScore /= 3.0 + } + copilot.hotScore = hotScore + } + // 批量更新热度值 + copilotRepository.saveAll(copilots) + } + + private fun counts(keys: Collection, rating: RatingType, startTime: LocalDateTime): List { + val aggregation = Aggregation.newAggregation( + Aggregation.match( + Criteria + .where("type").`is`(Rating.KeyType.COPILOT) + .and("key").`in`(keys) + .and("rating").`is`(rating) + .and("rateTime").gte(startTime) + ), + Aggregation.group("key").count().`as`("count") + .first("key").`as`("key"), + Aggregation.project("key", "count") + ).withOptions(Aggregation.newAggregationOptions().allowDiskUse(true).build()) // 放弃内存优化,使用磁盘优化,免得内存炸了 + return mongoTemplate.aggregate(aggregation, Rating::class.java, RatingCount::class.java).mappedResults + } +} diff --git a/src/main/resources/application-template.yml b/src/main/resources/application-template.yml index f04a8ee3..5d160c0d 100644 --- a/src/main/resources/application-template.yml +++ b/src/main/resources/application-template.yml @@ -30,6 +30,7 @@ maa-copilot: expire: 21600 # JwtToken的加密密钥 secret: $I_Am_The_Bone_Of_My_Sword!Steel_Is_My_Body_And_Fire_Is_My_Blood!$ + max-login: 1 github: # GitHub api token token: github_pat_xxx @@ -60,7 +61,10 @@ maa-copilot: ssl: false #邮件通知 notification: true - + copilot: + min-value-show-not-enough-rating: 50 + sensitive-word: + path: "classpath:sensitive-word.txt" springdoc: diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index fd86378f..9952552e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -21,7 +21,7 @@ spring: check-template-location: false # 缓存配置,使用caffeine缓存框架,缓存时长为5分钟,最大缓存数量500 cache: - cache-names: arkLevel, arkLevels, copilotPage + cache-names: arkLevel, arkLevelInfos, copilotPage type: caffeine caffeine: spec: maximumSize=500,expireAfterWrite=300s @@ -31,6 +31,8 @@ server: tomcat: #限制post表单最大为30KB max-http-form-post-size: 30KB + compression: + enabled: true logging: file: @@ -38,4 +40,4 @@ logging: logback: rollingpolicy: max-history: 14 - clean-history-on-start: true \ No newline at end of file + clean-history-on-start: true diff --git a/src/main/resources/static/templates/ftlh/mail-comment-notification.ftlh b/src/main/resources/static/templates/ftlh/mail-comment-notification.ftlh index 1de4e5e7..67efeb01 100644 --- a/src/main/resources/static/templates/ftlh/mail-comment-notification.ftlh +++ b/src/main/resources/static/templates/ftlh/mail-comment-notification.ftlh @@ -1,6 +1,6 @@ -

Hi,${authorName}

+

Hi, ${authorName}

- 在Maa Copilot + 在Maa Copilot 收到了${reName}新回复${date}

diff --git a/src/test/java/plus/maa/backend/BaseMockTest.java b/src/test/java/plus/maa/backend/BaseMockTest.java deleted file mode 100644 index a9588fd1..00000000 --- a/src/test/java/plus/maa/backend/BaseMockTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package plus.maa.backend; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.TestInstance; -import org.mockito.MockitoAnnotations; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class BaseMockTest { - - AutoCloseable closeable; - - @BeforeAll - void initTest() { - this.closeable = MockitoAnnotations.openMocks(this); - } - - @AfterAll - void endTest() throws Exception { - this.closeable.close(); - } - -} diff --git a/src/test/java/plus/maa/backend/config/security/JwtAuthenticationTokenFilterTest.java b/src/test/java/plus/maa/backend/config/security/JwtAuthenticationTokenFilterTest.java deleted file mode 100644 index 493320c8..00000000 --- a/src/test/java/plus/maa/backend/config/security/JwtAuthenticationTokenFilterTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package plus.maa.backend.config.security; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.security.core.context.SecurityContextHolder; -import plus.maa.backend.config.external.Jwt; -import plus.maa.backend.config.external.MaaCopilotProperties; -import plus.maa.backend.service.jwt.JwtService; - -import java.util.ArrayList; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -@SpringBootTest -class JwtAuthenticationTokenFilterTest { - - @Test - void testValidToken() { - var properties = new MaaCopilotProperties(); - var jwtSettings = new Jwt(); - jwtSettings.setSecret("whatever you want"); - jwtSettings.setExpire(86400); - properties.setJwt(jwtSettings); - - var jwtService = new JwtService(properties); - var userId = "some user id"; - var authToken = jwtService.issueAuthToken(userId, null, new ArrayList<>()); - var jwt = authToken.getValue(); - - var filter = new JwtAuthenticationTokenFilter(new AuthenticationHelper(), properties, jwtService); - var request = mock(HttpServletRequest.class); - when(request.getHeader(properties.getJwt().getHeader())).thenReturn("Bearer " + jwt); - var filterChain = mock(FilterChain.class); - try { - filter.doFilter(request, mock(HttpServletResponse.class), filterChain); - } catch (Exception ignored) { - } - assert SecurityContextHolder.getContext().getAuthentication() != null; - } -} \ No newline at end of file diff --git a/src/test/java/plus/maa/backend/repository/GithubRepositoryTest.java b/src/test/java/plus/maa/backend/repository/GithubRepositoryTest.java deleted file mode 100644 index cc4a3989..00000000 --- a/src/test/java/plus/maa/backend/repository/GithubRepositoryTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package plus.maa.backend.repository; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import plus.maa.backend.config.external.MaaCopilotProperties; - -import static org.junit.jupiter.api.Assertions.*; - -@SpringBootTest -class GithubRepositoryTest { - - @Autowired - private GithubRepository repository; - @Autowired - private MaaCopilotProperties properties; - - @Test - public void testGetTrees() { - var maaTree = repository.getTrees(properties.getGithub().getToken(), "d989739981db071e80df1c66e473c729b50e8073"); - assertNotNull(maaTree); - } - - @Test - public void testGetCommits() { - var commits = repository.getCommits(properties.getGithub().getToken()); - assertNotNull(commits); - assertFalse(commits.isEmpty()); - } - -} \ No newline at end of file diff --git a/src/test/java/plus/maa/backend/service/CopilotServiceTest.java b/src/test/java/plus/maa/backend/service/CopilotServiceTest.java deleted file mode 100644 index e64e5517..00000000 --- a/src/test/java/plus/maa/backend/service/CopilotServiceTest.java +++ /dev/null @@ -1,73 +0,0 @@ -package plus.maa.backend.service; - -import org.junit.jupiter.api.Test; -import plus.maa.backend.repository.entity.Copilot; - -import java.time.LocalDateTime; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class CopilotServiceTest { - - @Test - void testHotScores() { - LocalDateTime now = LocalDateTime.now(); - LocalDateTime beforeWeek = now.minusDays(8L); - Copilot[] copilots = new Copilot[5]; - long[] lastWeekLikeCounts = new long[5]; - long[] lastWeekDislikeCounts = new long[5]; - // 一月前的作业,评分高,但是只有一条近期好评,浏览量高 - Copilot oldGreat = new Copilot(); - oldGreat.setUploadTime(beforeWeek.minusDays(14)); - oldGreat.setViews(20000L); - copilots[0] = oldGreat; - lastWeekLikeCounts[0] = 1L; - lastWeekDislikeCounts[0] = 0L; - - // 近期作业,含有差评,但是均为近期评分 - Copilot newGreat = new Copilot(); - newGreat.setUploadTime(now); - newGreat.setViews(1000L); - copilots[1] = newGreat; - lastWeekLikeCounts[1] = 6L; - lastWeekDislikeCounts[1] = 1L; - - - // 近期作业,差评较多,均为近期评分 - Copilot newBad = new Copilot(); - newBad.setUploadTime(now); - newBad.setViews(500L); - copilots[2] = newBad; - lastWeekLikeCounts[2] = 2L; - lastWeekDislikeCounts[2] = 4L; - - - // 一月前的作业,评分高,但是只有一条近期好评,浏览量尚可 - Copilot oldNormal = new Copilot(); - oldNormal.setUploadTime(beforeWeek.minusDays(21L)); - oldNormal.setViews(4000L); - copilots[3] = oldNormal; - lastWeekLikeCounts[3] = 1L; - lastWeekDislikeCounts[3] = 0L; - - - // 新增作业,暂无评分 - Copilot newEmpty = new Copilot(); - newEmpty.setUploadTime(now); - newEmpty.setViews(100L); - copilots[4] = newEmpty; - lastWeekLikeCounts[4] = 0L; - lastWeekDislikeCounts[4] = 0L; - - for (int i = 0; i < 5; i++) { - copilots[i].setHotScore(CopilotService.getHotScore(copilots[i], lastWeekLikeCounts[i], lastWeekDislikeCounts[i])); - } - - // 近期好评 > 远古好评 > 近期新增 > 近期差评 > 远古一般 - assertTrue(newGreat.getHotScore() > oldGreat.getHotScore()); - assertTrue(newEmpty.getHotScore() > oldGreat.getHotScore()); - assertTrue(oldGreat.getHotScore() > newBad.getHotScore()); - assertTrue(oldNormal.getHotScore() > newBad.getHotScore()); - } - -} diff --git a/src/test/java/plus/maa/backend/service/jwt/JwtServiceTest.java b/src/test/java/plus/maa/backend/service/jwt/JwtServiceTest.java deleted file mode 100644 index 490c67bc..00000000 --- a/src/test/java/plus/maa/backend/service/jwt/JwtServiceTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package plus.maa.backend.service.jwt; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import plus.maa.backend.config.external.Jwt; -import plus.maa.backend.config.external.MaaCopilotProperties; - -import java.util.ArrayList; - -class JwtServiceTest { - - JwtService createService() { - var properties = new MaaCopilotProperties(); - var jwtSettings = new Jwt(); - jwtSettings.setSecret("whatever you want"); - properties.setJwt(jwtSettings); - - return new JwtService(properties); - } - - @Test - void authTokenCodec() throws JwtExpiredException, JwtInvalidException { - var service = createService(); - var subject = "some user id"; - var jwtId = "some jwt Id"; - - var token = service.issueAuthToken(subject, jwtId, new ArrayList<>()); - var parsedToken = service.verifyAndParseAuthToken(token.getValue()); - - assert subject.equals(parsedToken.getSubject()); - assert jwtId.equals(parsedToken.getJwtId()); - assert parsedToken.isAuthenticated(); - } - - @Test - void refreshTokenCodec() throws JwtExpiredException, JwtInvalidException { - var service = createService(); - - var subject = "some user id"; - var origin = service.issueRefreshToken(subject, null); - - var parsedToken = service.verifyAndParseRefreshToken(origin.getValue()); - assert subject.equals(parsedToken.getSubject()); - - var newToken = service.newRefreshToken(parsedToken, null); - assert !newToken.getIssuedAt().isBefore(parsedToken.getIssuedAt()); - assert !newToken.getNotBefore().isBefore(parsedToken.getNotBefore()); - assert newToken.getExpiresAt().equals(parsedToken.getExpiresAt()); - } - - @Test - void wrongTypeParseShouldFail() { - var service = createService(); - var authToken = service.issueAuthToken("some user id", null, new ArrayList<>()); - Assertions.assertThrows(JwtInvalidException.class, () -> service.verifyAndParseRefreshToken(authToken.getValue())); - var refreshToken = service.issueRefreshToken("some user id", null); - Assertions.assertThrows(JwtInvalidException.class, () -> service.verifyAndParseAuthToken(refreshToken.getValue())); - } - -} \ No newline at end of file diff --git a/src/test/java/plus/maa/backend/task/CopilotScoreRefreshTaskTest.java b/src/test/java/plus/maa/backend/task/CopilotScoreRefreshTaskTest.java deleted file mode 100644 index 3280dafa..00000000 --- a/src/test/java/plus/maa/backend/task/CopilotScoreRefreshTaskTest.java +++ /dev/null @@ -1,108 +0,0 @@ -package plus.maa.backend.task; - -import org.bson.Document; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.aggregation.AggregationResults; -import plus.maa.backend.BaseMockTest; -import plus.maa.backend.repository.CopilotRepository; -import plus.maa.backend.repository.RedisCache; -import plus.maa.backend.repository.entity.Copilot; -import plus.maa.backend.repository.entity.Rating; -import plus.maa.backend.service.model.RatingCount; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.when; - -public class CopilotScoreRefreshTaskTest extends BaseMockTest { - - @InjectMocks - CopilotScoreRefreshTask refreshTask; - - @Mock - CopilotRepository copilotRepository; - @Mock - MongoTemplate mongoTemplate; - @Mock - RedisCache redisCache; - - @Test - void testRefreshScores() { - LocalDateTime now = LocalDateTime.now(); - Copilot copilot1 = new Copilot(); - copilot1.setCopilotId(1L); - copilot1.setViews(100L); - copilot1.setUploadTime(now); - Copilot copilot2 = new Copilot(); - copilot2.setCopilotId(2L); - copilot2.setViews(200L); - copilot2.setUploadTime(now); - Copilot copilot3 = new Copilot(); - copilot3.setCopilotId(3L); - copilot3.setViews(200L); - copilot3.setUploadTime(now); - - // 配置copilotRepository - when(copilotRepository.findAllByDeleteIsFalse(any(Pageable.class))) - .thenReturn(new PageImpl<>(List.of(copilot1, copilot2, copilot3))); - - // 配置mongoTemplate - when(mongoTemplate.aggregate(any(), eq(Rating.class), eq(RatingCount.class))) - .thenReturn(new AggregationResults<>(List.of( - new RatingCount("1", 1L), - new RatingCount("2", 0L), - new RatingCount("3", 0L)), new Document())); - - refreshTask.refreshHotScores(); - - assertTrue(copilot1.getHotScore() > 0); - assertTrue(copilot2.getHotScore() > 0); - } - - @Test - void testRefreshTop100HotScores() { - LocalDateTime now = LocalDateTime.now(); - Copilot copilot1 = new Copilot(); - copilot1.setCopilotId(1L); - copilot1.setViews(100L); - copilot1.setUploadTime(now); - Copilot copilot2 = new Copilot(); - copilot2.setCopilotId(2L); - copilot2.setViews(200L); - copilot2.setUploadTime(now); - Copilot copilot3 = new Copilot(); - copilot3.setCopilotId(3L); - copilot3.setViews(200L); - copilot3.setUploadTime(now); - - // 配置 RedisCache - when(redisCache.getZSetReverse("rate:hot:copilotIds", 0, 99)) - .thenReturn(Set.of("1", "2", "3")); - - // 配置copilotRepository - when(copilotRepository.findByCopilotIdInAndDeleteIsFalse(anyCollection())) - .thenReturn(List.of(copilot1, copilot2, copilot3)); - - // 配置mongoTemplate - when(mongoTemplate.aggregate(any(), eq(Rating.class), eq(RatingCount.class))) - .thenReturn(new AggregationResults<>(List.of( - new RatingCount("1", 1L), - new RatingCount("2", 0L), - new RatingCount("3", 0L)), new Document())); - - refreshTask.refreshTop100HotScores(); - - assertTrue(copilot1.getHotScore() > 0); - assertTrue(copilot2.getHotScore() > 0); - } - -} diff --git a/src/test/kotlin/plus/maa/backend/common/utils/ArkLevelUtilTest.kt b/src/test/kotlin/plus/maa/backend/common/utils/ArkLevelUtilTest.kt new file mode 100644 index 00000000..298ca850 --- /dev/null +++ b/src/test/kotlin/plus/maa/backend/common/utils/ArkLevelUtilTest.kt @@ -0,0 +1,40 @@ +package plus.maa.backend.common.utils + +import org.junit.jupiter.api.Test + +class ArkLevelUtilTest { + @Test + fun testGetKeyInfoById() { + val ids = mapOf( + "level_rune_09-01" to "level_rune_09-02", + "level_crisis_v2_01-07" to "crisis_v2_season_1_1", + "a001_01_perm" to "a001_ex05", + "act11d0_ex08#f#" to "act11d0_s02", + "act11mini_03#f#" to "act11mini_04", + "act17side_01" to "act17side_s01_a", + "act17side_01_rep" to "act17side_02_perm" + ) + + val idsWithInfo = mapOf( + "level_rune_09-01" to "rune_9", + "level_crisis_v2_01-07" to "crisis_v2_1", + "a001_01_perm" to "a1", + "act11d0_ex08#f#" to "act11d0", + "act11mini_03#f#" to "act11mini", + "act17side_01" to "act17side", + "act17side_01_rep" to "act17side" + ) + + for ((key, value) in ids) { + val infoOfKey = ArkLevelUtil.getKeyInfoById(key) + check(infoOfKey == ArkLevelUtil.getKeyInfoById(value)) { + "$key 与 $value 的地图标识不相同" + } + + val infoOfMap = idsWithInfo[key] + check(infoOfKey == infoOfMap) { + "$key 的地图标识不为 $infoOfMap" + } + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/plus/maa/backend/config/security/JwtAuthenticationTokenFilterTest.kt b/src/test/kotlin/plus/maa/backend/config/security/JwtAuthenticationTokenFilterTest.kt new file mode 100644 index 00000000..0afce177 --- /dev/null +++ b/src/test/kotlin/plus/maa/backend/config/security/JwtAuthenticationTokenFilterTest.kt @@ -0,0 +1,38 @@ +package plus.maa.backend.config.security + +import io.mockk.every +import io.mockk.spyk +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.junit.jupiter.api.Test +import org.springframework.security.core.context.SecurityContextHolder +import plus.maa.backend.config.external.Jwt +import plus.maa.backend.config.external.MaaCopilotProperties +import plus.maa.backend.service.jwt.JwtService + +class JwtAuthenticationTokenFilterTest { + @Test + fun testValidToken() { + val properties = MaaCopilotProperties() + val jwtSettings = Jwt() + jwtSettings.secret = "whatever you want" + jwtSettings.expire = 86400 + properties.jwt = jwtSettings + + val jwtService = JwtService(properties) + val userId = "some user id" + val authToken = jwtService.issueAuthToken(userId, null, ArrayList()) + val jwt = authToken.value + + val filter = JwtAuthenticationTokenFilter(AuthenticationHelper(), properties, jwtService) + val request = spyk() + every { request.getHeader(properties.jwt.header) } returns "Bearer $jwt" + val filterChain = spyk() + try { + filter.doFilter(request, spyk(), filterChain) + } catch (ignored: Exception) { + } + requireNotNull(SecurityContextHolder.getContext().authentication) + } +} \ No newline at end of file diff --git a/src/test/kotlin/plus/maa/backend/repository/GithubRepositoryTest.kt b/src/test/kotlin/plus/maa/backend/repository/GithubRepositoryTest.kt new file mode 100644 index 00000000..6bbc3e17 --- /dev/null +++ b/src/test/kotlin/plus/maa/backend/repository/GithubRepositoryTest.kt @@ -0,0 +1,29 @@ +package plus.maa.backend.repository + +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import plus.maa.backend.config.external.MaaCopilotProperties + +@SpringBootTest +class GithubRepositoryTest( + @Autowired val repository: GithubRepository, + @Autowired val properties: MaaCopilotProperties +) { + @Test + fun testGetTrees() { + repository.getTrees(properties.github.token, "d989739981db071e80df1c66e473c729b50e8073") + } + + @Test + fun testGetCommits() { + val commits = repository.getCommits(properties.github.token) + check(commits.isNotEmpty()) + } + + @Test + fun testGetContents() { + val contents = repository.getContents(properties.github.token, "") + check(contents.isNotEmpty()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/plus/maa/backend/service/CopilotServiceTest.kt b/src/test/kotlin/plus/maa/backend/service/CopilotServiceTest.kt new file mode 100644 index 00000000..368aceed --- /dev/null +++ b/src/test/kotlin/plus/maa/backend/service/CopilotServiceTest.kt @@ -0,0 +1,70 @@ +package plus.maa.backend.service + +import org.junit.jupiter.api.Test +import plus.maa.backend.repository.entity.Copilot +import plus.maa.backend.service.CopilotService.Companion.getHotScore +import java.time.LocalDateTime + +class CopilotServiceTest { + @Test + fun testHotScores() { + val now = LocalDateTime.now() + val beforeWeek = now.minusDays(8L) + val copilots = arrayOfNulls(5) + val lastWeekLikeCounts = LongArray(5) + val lastWeekDislikeCounts = LongArray(5) + // 一月前的作业,评分高,但是只有一条近期好评,浏览量高 + val oldGreat = Copilot(doc = Copilot.Doc(title = "test")) + oldGreat.uploadTime = beforeWeek.minusDays(14) + oldGreat.views = 20000L + copilots[0] = oldGreat + lastWeekLikeCounts[0] = 1L + lastWeekDislikeCounts[0] = 0L + + // 近期作业,含有差评,但是均为近期评分 + val newGreat = Copilot(doc = Copilot.Doc(title = "test")) + newGreat.uploadTime = now + newGreat.views = 1000L + copilots[1] = newGreat + lastWeekLikeCounts[1] = 6L + lastWeekDislikeCounts[1] = 1L + + + // 近期作业,差评较多,均为近期评分 + val newBad = Copilot(doc = Copilot.Doc(title = "test")) + newBad.uploadTime = now + newBad.views = 500L + copilots[2] = newBad + lastWeekLikeCounts[2] = 2L + lastWeekDislikeCounts[2] = 4L + + + // 一月前的作业,评分高,但是只有一条近期好评,浏览量尚可 + val oldNormal = Copilot(doc = Copilot.Doc(title = "test")) + oldNormal.uploadTime = beforeWeek.minusDays(21L) + oldNormal.views = 4000L + copilots[3] = oldNormal + lastWeekLikeCounts[3] = 1L + lastWeekDislikeCounts[3] = 0L + + + // 新增作业,暂无评分 + val newEmpty = Copilot(doc = Copilot.Doc(title = "test")) + newEmpty.uploadTime = now + newEmpty.views = 100L + copilots[4] = newEmpty + lastWeekLikeCounts[4] = 0L + lastWeekDislikeCounts[4] = 0L + + for (i in 0..4) { + copilots[i]!!.hotScore = + getHotScore(copilots[i]!!, lastWeekLikeCounts[i], lastWeekDislikeCounts[i]) + } + + // 近期好评 > 远古好评 > 近期新增 > 近期差评 > 远古一般 + check(newGreat.hotScore > oldGreat.hotScore) + check(newEmpty.hotScore > oldGreat.hotScore) + check(oldGreat.hotScore > newBad.hotScore) + check(oldNormal.hotScore > newBad.hotScore) + } +} diff --git a/src/test/kotlin/plus/maa/backend/service/jwt/JwtServiceTest.kt b/src/test/kotlin/plus/maa/backend/service/jwt/JwtServiceTest.kt new file mode 100644 index 00000000..6f65ed05 --- /dev/null +++ b/src/test/kotlin/plus/maa/backend/service/jwt/JwtServiceTest.kt @@ -0,0 +1,61 @@ +package plus.maa.backend.service.jwt + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import plus.maa.backend.config.external.Jwt +import plus.maa.backend.config.external.MaaCopilotProperties + +class JwtServiceTest { + private fun createService(): JwtService { + val properties = MaaCopilotProperties() + val jwtSettings = Jwt() + jwtSettings.secret = "whatever you want" + properties.jwt = jwtSettings + + return JwtService(properties) + } + + @Test + @Throws(JwtExpiredException::class, JwtInvalidException::class) + fun authTokenCodec() { + val service = createService() + val subject = "some user id" + val jwtId = "some jwt Id" + + val token = service.issueAuthToken(subject, jwtId, ArrayList()) + val parsedToken = service.verifyAndParseAuthToken(token.value) + + check(subject == parsedToken.subject) + check(jwtId == parsedToken.jwtId) + check(parsedToken.isAuthenticated) + } + + @Test + @Throws(JwtExpiredException::class, JwtInvalidException::class) + fun refreshTokenCodec() { + val service = createService() + + val subject = "some user id" + val origin = service.issueRefreshToken(subject, null) + + val parsedToken = service.verifyAndParseRefreshToken(origin.value) + check(subject == parsedToken.subject) + val newToken = service.newRefreshToken(parsedToken, null) + check(!newToken.issuedAt.isBefore(parsedToken.issuedAt)) + check(!newToken.notBefore.isBefore(parsedToken.notBefore)) + check(newToken.expiresAt == parsedToken.expiresAt) + } + + @Test + fun wrongTypeParseShouldFail() { + val service = createService() + val authToken = service.issueAuthToken("some user id", null, ArrayList()) + assertThrows { + service.verifyAndParseRefreshToken(authToken.value) + } + val refreshToken = service.issueRefreshToken("some user id", null) + assertThrows { + service.verifyAndParseAuthToken(refreshToken.value) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/plus/maa/backend/task/CopilotScoreRefreshTaskTest.kt b/src/test/kotlin/plus/maa/backend/task/CopilotScoreRefreshTaskTest.kt new file mode 100644 index 00000000..17326c1d --- /dev/null +++ b/src/test/kotlin/plus/maa/backend/task/CopilotScoreRefreshTaskTest.kt @@ -0,0 +1,132 @@ +package plus.maa.backend.task + +import io.mockk.every +import io.mockk.mockk +import org.bson.Document +import org.junit.jupiter.api.Test +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.Pageable +import org.springframework.data.mongodb.core.MongoTemplate +import org.springframework.data.mongodb.core.aggregation.AggregationResults +import plus.maa.backend.repository.CopilotRepository +import plus.maa.backend.repository.RedisCache +import plus.maa.backend.repository.entity.ArkLevel +import plus.maa.backend.repository.entity.Copilot +import plus.maa.backend.repository.entity.Rating +import plus.maa.backend.service.ArkLevelService +import plus.maa.backend.service.model.RatingCount +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit + +class CopilotScoreRefreshTaskTest { + private val copilotRepository = mockk() + private val mongoTemplate = mockk() + private val redisCache = mockk() + private val arkLevelService = mockk() + private val refreshTask: CopilotScoreRefreshTask = CopilotScoreRefreshTask( + arkLevelService, + redisCache, + copilotRepository, + mongoTemplate + ) + + + @Test + fun testRefreshScores() { + val now = LocalDateTime.now() + val copilot1 = Copilot(doc = Copilot.Doc(title = "test")) + copilot1.copilotId = 1L + copilot1.views = 100L + copilot1.uploadTime = now + copilot1.stageName = "stage1" + val copilot2 = Copilot(doc = Copilot.Doc(title = "test")) + copilot2.copilotId = 2L + copilot2.views = 200L + copilot2.uploadTime = now + copilot2.stageName = "stage2" + val copilot3 = Copilot(doc = Copilot.Doc(title = "test")) + copilot3.copilotId = 3L + copilot3.views = 200L + copilot3.uploadTime = now + copilot3.stageName = "stage3" + val allCopilots = listOf(copilot1, copilot2, copilot3) + + // 配置copilotRepository + every { + copilotRepository.findAllByDeleteIsFalse(any()) + } returns PageImpl(allCopilots) + + // 配置mongoTemplate + every { + mongoTemplate.aggregate(any(), Rating::class.java, RatingCount::class.java) + } returns AggregationResults( + listOf( + RatingCount("1", 1L), + RatingCount("2", 0L), + RatingCount("3", 0L) + ), Document() + ) + + val arkLevel = ArkLevel() + arkLevel.isOpen = true + arkLevel.closeTime = LocalDateTime.now().plus(1, ChronoUnit.DAYS) + every { arkLevelService.findByLevelIdFuzzy(any()) } returns arkLevel + every { copilotRepository.saveAll(any>()) } returns allCopilots + every { redisCache.syncRemoveCacheByPattern(any()) } returns Unit + refreshTask.refreshHotScores() + + check(copilot1.hotScore > 0) + check(copilot2.hotScore > 0) + } + + @Test + fun testRefreshTop100HotScores() { + val now = LocalDateTime.now() + val copilot1 = Copilot(doc = Copilot.Doc(title = "test")) + copilot1.copilotId = 1L + copilot1.views = 100L + copilot1.uploadTime = now + copilot1.stageName = "stage1" + val copilot2 = Copilot(doc = Copilot.Doc(title = "test")) + copilot2.copilotId = 2L + copilot2.views = 200L + copilot2.uploadTime = now + copilot2.stageName = "stage2" + val copilot3 = Copilot(doc = Copilot.Doc(title = "test")) + copilot3.copilotId = 3L + copilot3.views = 200L + copilot3.uploadTime = now + copilot3.stageName = "stage3" + val allCopilots = listOf(copilot1, copilot2, copilot3) + + // 配置 RedisCache + every { redisCache.getZSetReverse("rate:hot:copilotIds", 0, 99) } returns setOf("1", "2", "3") + + // 配置copilotRepository + every { + copilotRepository.findByCopilotIdInAndDeleteIsFalse(any()) + } returns allCopilots + + // 配置mongoTemplate + every { + mongoTemplate.aggregate(any(), Rating::class.java, RatingCount::class.java) + } returns AggregationResults( + listOf( + RatingCount("1", 1L), + RatingCount("2", 0L), + RatingCount("3", 0L) + ), Document() + ) + val arkLevel = ArkLevel() + arkLevel.isOpen = true + arkLevel.closeTime = LocalDateTime.now().plus(1, ChronoUnit.DAYS) + every { arkLevelService.findByLevelIdFuzzy(any()) } returns arkLevel + every { copilotRepository.saveAll(any>()) } returns allCopilots + every { redisCache.removeCache(any()) } returns Unit + every { redisCache.syncRemoveCacheByPattern(any()) } returns Unit + refreshTask.refreshTop100HotScores() + + check(copilot1.hotScore > 0) + check(copilot2.hotScore > 0) + } +}