This commit is contained in:
Chuck1sn
2025-05-14 10:16:48 +08:00
commit 3cd59337e7
220 changed files with 23768 additions and 0 deletions

171
.gitignore vendored Normal file
View File

@@ -0,0 +1,171 @@
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
# 如何在这个地方引用 backend 和 frontend 下级目录中的 .gitignore 中的规则?我需要全部给他们前面加上 backend/ 和 frontend/ 前缀吗?

2
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,2 @@
{
}

21
LICENSE.txt Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Chuck
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

133
README.md Normal file
View File

@@ -0,0 +1,133 @@
# 知路管理后台
一个重新构思、重新设计、重新开发的现代化 Java 前后端脚手架。整合大量高效现代化技术栈,全网唯一。
- [知路管理后台](#%E7%9F%A5%E8%B7%AF%E7%AE%A1%E7%90%86%E5%90%8E%E5%8F%B0)
- [尊重设计与编码规范](#%E5%B0%8A%E9%87%8D%E8%AE%BE%E8%AE%A1%E4%B8%8E%E7%BC%96%E7%A0%81%E8%A7%84%E8%8C%83)
- [正确的业务设计与建模](#%E6%AD%A3%E7%A1%AE%E7%9A%84%E4%B8%9A%E5%8A%A1%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%BB%BA%E6%A8%A1)
- [专属业务功能](#%E4%B8%93%E5%B1%9E%E4%B8%9A%E5%8A%A1%E5%8A%9F%E8%83%BD)
- [云原生开发与秒级部署](#%E4%BA%91%E5%8E%9F%E7%94%9F%E5%BC%80%E5%8F%91%E4%B8%8E%E7%A7%92%E7%BA%A7%E9%83%A8%E7%BD%B2)
- [自动免费的 HTTPS 证书](#%E8%87%AA%E5%8A%A8%E5%85%8D%E8%B4%B9%E7%9A%84-https-%E8%AF%81%E4%B9%A6)
- [自动化数据库管理](#%E8%87%AA%E5%8A%A8%E5%8C%96%E6%95%B0%E6%8D%AE%E5%BA%93%E7%AE%A1%E7%90%86)
- [其他更多特性](#%E5%85%B6%E4%BB%96%E6%9B%B4%E5%A4%9A%E7%89%B9%E6%80%A7)
- [技术选型](#%E6%8A%80%E6%9C%AF%E9%80%89%E5%9E%8B)
## 产品社群
**加 QQ 群,获取一键部署脚本**
QQ群638254979
**加微信,获取 VIP 客户支持(喂饭级技术讲解+预购所有课程)**
微信Chuck9996
**相关课程**
已上线:
[《重构方法论与单元测试的艺术》](https://www.bilibili.com/cheese/play/ep1615343)
敬请期待:(加群获取)
[《知路脚手架喂饭级教程》]()
[《领域驱动没那么复杂-贫民项目的领域架构实战》]()
## 尊重设计与编码规范
本系统在开发过程中以《TDD测试驱动开发》为指导思想在业务代码中贯彻落实了严格、规范、优良的编码与设计并编写了大量的单元测试、集成测试、切片测试为你的应用保驾护航。
本系统的测试代码全网独一无二,内容无可挑剔;其中包含大量编码设计的哲学理念。
具体内容请参考 test 目录,并辅以视频教程进行阅读,它将会使你受益匪浅。
[👉《重构方法论与单元测试的艺术》👈](https://www.bilibili.com/cheese/play/ep1615343)
![class](/assets/class.png)
吃透这款脚手架与配套课程,从今以后就不是别人 Review 你的代码,而是你对别人的代码进行指摘。
## 正确的业务设计与建模
大部分同类产品,为了规避某些复杂的业务逻辑,强行改变本来的业务形态:如强行把某些多对多关系的业务设计为一对多等等。**这叫做鸵鸟战术:即把头埋进沙子里面假装这件事情不存在。**
这样的设计不仅没有解决问题,反而增加了问题。一个无法解决问题的管理系统是没有价值的。
而知路管理后台从一开始就致力于正确的业务建模——当面对复杂的产品逻辑时,我们不采取鸵鸟战术,而是采取以下两个手段直面困难:
- 在框架层面进行抽象与封装
- 使用现代化的技术选型从根本上解决
使用本系统构建的系统,只需要几行代码即可轻松编写之前难以实现的复杂业务逻辑,让开发者倍感轻松。
### 专属业务功能
得益于上述设计思想,在本系统中用户不仅可以担任多个岗位,还能够隶属于多个部门,同时还可拥有多个部门的数据权限。另外,岗位和部门还可以相互配合,提供更加复杂的产品逻辑的实现支持。
![depbind](/assets/depbind.png)
![posbind](/assets/posbind.png)
今后还将推出更多复杂业务逻辑的解决方案,敬请期待。
## 云原生开发与零配置部署
知路管理后台是完全围绕云原生进行设计开发的。这意味着你只需要三个步骤就可以部署完整个前后端系统:
1. 拥有一台安装了操作系统的电脑
2. 安装 Docker
3. 下载代码,一键运行部署脚本
你不需要安装 Java不需要安装 Javascript不需要安装 Vue不需要安装 mysql不需要配置这个修改那个。
无论你是 Linux 还是 Mac 还是 Windows都能够在 2 分钟内一键部署好整个系统。
**获取部署脚本,请加 QQ 群638254979**
## 自动免费的 HTTPS 证书
本系统会在「开发环境->测试环境->生产环境」自动生成并配置免费的 Https 证书供你使用。不需要任何配置,只要运行部署脚本即可马上获取这项功能。
如果你是一个有经验的开发者,尤其是前端开发者,就应该能明白在开发和测试环境使用 Https 调试是多么的重要。
>注意,开发环境和测试环境的证书是自签名的,这意味着访问网站时需要手动点击信任按钮
## 自动化数据库管理
本系统会自动管理将所有数据库功能,包括自动建表、自动修改、删除字段、自动增加索引等;
不仅如此,脚手架还会在代码库中对你的修改历史进行版本管理。从而方便你对任意时间点的数据库修改进行回朔。
总而言之,你唯一需要做的就是业务编码,然后把其他复杂的事情交给脚手架。
## 更多特性
- 开发、测试、生产全生态链云原生环境
- 通过 .env 管理开发、测试、生产环境所有账号密码。
- 在框架层面集成的代码格式化与规范检查
- 自动生成数据库建模、DAO、单表 CRUD
- 其他更多功能
## 部分技术选型
**前端**
| 框架 | 版本 |
|---|---|
| node | lts/jod |
| vue-family | ^3.5 |
| vite-family | ^6.2.1 |
| tailwindcss | ^4.0 |
| zod | ^3.2 |
| pinia |^3.0 |
| biome |^1.9.4 |
| playwright |^1.51.1 |
| msw |^1.51.1 |
| openapi-typescript | ^7.6.1 |
| typescript | ~5.8.0 |
| docker | ^27 |
**后端**
| 技术 | 版本 |
|---|---|
| java | 21 |
| spring-boot | 3.3.9 |
| spring-security | 3.3.9 |
| spring-cache | 3.3.9 |
| spring-doc | 2.6.0 |
| test-containers | 1.20.6 |
| jooq | 3.19.18 |
| postgresql | 17.3 |
| flyway | 11.4 |
| spotless | 7.0.2 |
| pmd | 7.9.0 |
| gradle | 8.13 |
| docker | ^27 |

BIN
assets/class.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

BIN
assets/depbind.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

BIN
assets/posbind.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

BIN
assets/qq.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB

217
backend/.dockerignore Normal file
View File

@@ -0,0 +1,217 @@
### Java template
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
### VisualStudioCode template
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### dotenv template
.env
### Git template
# Created by git for backups. To disable backups in Git:
# $ git config --global mergetool.keepBackup false
*.orig
# Created by git when using merge tools for conflicts
*.BACKUP.*
*.BASE.*
*.LOCAL.*
*.REMOTE.*
*_BACKUP_*.txt
*_BASE_*.txt
*_LOCAL_*.txt
*_REMOTE_*.txt
### Gradle template
.gradle
**/build/
!src/**/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Avoid ignore Gradle wrappper properties
!gradle-wrapper.properties
# Cache of project
.gradletasknamecache
# Eclipse Gradle plugin generated files
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
### Windows template
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
.git
.gitignore
README.md

17
backend/.env Normal file
View File

@@ -0,0 +1,17 @@
COMPOSE_PROJECT_NAME=mjga
# database
DATABASE_HOST_PORT=host.docker.internal:5432
DATABASE_STORE=~/docker/store/mjga/db/data
DATABASE_DB=mjga
DATABASE_DEFAULT_SCHEMA=public
DATABASE_USER=mjga
DATABASE_PASSWORD=mjga
DATABASE_EXPOSE_PORT=5432
# web
WEB_EXPOSE_PORT=8080
LOG_PATH=~/docker/store/mjga/log
# cors
ALLOWED_ORIGINS=http://localhost,https://localhost,http://localhost:8080,http://localhost:5173
ALLOWED_METHODS=*
ALLOWED_HEADERS=*
ALLOWED_EXPOSE_HEADERS=*

229
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,229 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
assets
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Java template
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
### VisualStudioCode template
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Gradle template
**/build/
!src/**/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Avoid ignore Gradle wrappper properties
!gradle-wrapper.properties
# Cache of project
.gradletasknamecache
### Windows template
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
logs
db.d/store/*
.git

194
backend/build.gradle.kts Normal file
View File

@@ -0,0 +1,194 @@
import org.springframework.boot.gradle.tasks.bundling.BootJar
val jooqVersion by extra("3.19.22")
val testcontainersVersion by extra("1.20.6")
val flywayVersion by extra("11.4.0")
plugins {
java
`java-library`
jacoco
id("org.springframework.boot") version "3.3.9"
id("io.spring.dependency-management") version "1.1.7"
id("org.springdoc.openapi-gradle-plugin") version "1.9.0"
id("pmd")
id("org.jooq.jooq-codegen-gradle") version "3.19.22"
id("com.diffplug.spotless") version "7.0.2"
}
sourceSets {
main {
java {
srcDir("build/generated-sources/jooq")
}
}
test {
java {
srcDir("build/generated-sources/jooq")
}
}
}
group = "com.zl.mjga"
version = "1.0.0"
description = "make java great again!"
java.sourceCompatibility = JavaVersion.VERSION_17
configurations {
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-jooq")
implementation("org.springframework.boot:spring-boot-starter-mail")
implementation("org.springframework.boot:spring-boot-starter-cache")
implementation("org.springframework.boot:spring-boot-starter-quartz")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-aop")
implementation("org.apache.commons:commons-lang3:3.17.0")
implementation("org.apache.commons:commons-collections4:4.4")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0")
implementation("org.jooq:jooq-meta:$jooqVersion")
implementation("com.auth0:java-jwt:4.4.0")
implementation("org.flywaydb:flyway-core:$flywayVersion")
implementation("org.flywaydb:flyway-database-postgresql:$flywayVersion")
implementation("com.github.ben-manes.caffeine:caffeine:3.2.0")
implementation("org.springframework.boot:spring-boot-starter-quartz")
testImplementation("org.testcontainers:junit-jupiter:$testcontainersVersion")
testImplementation("org.testcontainers:postgresql:$testcontainersVersion")
testImplementation("org.testcontainers:testcontainers-bom:$testcontainersVersion")
runtimeOnly("org.postgresql:postgresql")
compileOnly("org.projectlombok:lombok")
developmentOnly("org.springframework.boot:spring-boot-devtools")
testImplementation("org.springframework.boot:spring-boot-testcontainers")
testImplementation("org.springframework.boot:spring-boot-starter-webflux")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
jooqCodegen("org.postgresql:postgresql")
jooqCodegen("org.jooq:jooq-codegen:$jooqVersion")
jooqCodegen("org.jooq:jooq-meta-extensions:$jooqVersion")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
annotationProcessor("org.projectlombok:lombok")
api("org.jspecify:jspecify:1.0.0")
}
tasks.withType<BootJar> {
archiveFileName.set("dbfirst.jar")
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.test {
finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run
}
tasks.jacocoTestReport {
dependsOn(tasks.test) // tests are required to run before generating the report
}
jacoco {
toolVersion = "0.8.12"
reportsDirectory.set(layout.buildDirectory.dir("reports/jacoco"))
}
pmd {
sourceSets = listOf(java.sourceSets.findByName("main"))
isConsoleOutput = true
toolVersion = "7.9.0"
rulesMinimumPriority.set(5)
ruleSetFiles = files("pmd-rules.xml")
}
spotless {
format("misc") {
// define the files to apply `misc` to
target("*.gradle.kts", "*.md", ".gitignore")
// define the steps to apply to those files
trimTrailingWhitespace()
leadingTabsToSpaces()
endWithNewline()
}
java {
googleJavaFormat("1.25.2").reflowLongStrings()
formatAnnotations()
}
kotlinGradle {
target("*.gradle.kts") // default target for kotlinGradle
ktlint() // or ktfmt() or prettier()
}
}
jooq {
configuration {
generator {
database {
includes = ".*"
// excludes = "qrtz_.*"
name = "org.jooq.meta.extensions.ddl.DDLDatabase"
properties {
property {
key = "scripts"
value = "src/main/resources/db/migration/*.sql"
}
property {
key = "sort"
value = "semantic"
}
property {
key = "unqualifiedSchema"
value = "none"
}
property {
key = "defaultNameCase"
value = "lower"
}
property {
key = "logExecutedQueries"
value = "true"
}
property {
key = "logExecutionResults"
value = "true"
}
}
forcedTypes {
forcedType {
name = "varchar"
includeExpression = ".*"
includeTypes = "JSONB?"
}
forcedType {
name = "varchar"
includeExpression = ".*"
includeTypes = "INET"
}
}
}
generate {
isDaos = true
isRecords = true
isDeprecated = false
isImmutablePojos = false
isFluentSetters = true
isSpringAnnotations = true
isSpringDao = true
}
target {
packageName = "org.jooq.generated"
}
}
}
}

View File

@@ -0,0 +1,3 @@
org.gradle.configuration-cache=false
org.gradle.parallel=true
org.gradle.caching=true

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
backend/gradlew vendored Executable file
View File

@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
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=SC2039,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=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
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, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
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.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
backend/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

17
backend/pmd-rules.xml Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0"?>
<ruleset name="Custom Rules"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
description="xxx"
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
<description>
This is a custom ruleset for our project.
</description>
<rule ref="category/java/bestpractices.xml">
<exclude name="GuardLogStatement"/>
</rule>
<rule ref="category/java/errorprone.xml">
<exclude name="AvoidLiteralsInIfCondition"/>
</rule>
</ruleset>

View File

@@ -0,0 +1 @@
rootProject.name = "dbfirst"

View File

@@ -0,0 +1,12 @@
package com.zl.mjga;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = {"com.zl.mjga", "org.jooq.generated"})
public class ApplicationService {
public static void main(String[] args) {
SpringApplication.run(ApplicationService.class, args);
}
}

View File

@@ -0,0 +1,45 @@
package com.zl.mjga.config;
import java.util.Arrays;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Value("${cors.allowedOrigins}")
private String allowedOrigins;
@Value("${cors.allowedMethods}")
private String allowedMethods;
@Value("${cors.allowedHeaders}")
private String allowedHeaders;
@Value("${cors.allowedExposeHeaders}")
private String allowedExposeHeaders;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList(allowedOrigins.split(",")));
configuration.setAllowedMethods(Arrays.asList(allowedMethods.split(",")));
configuration.setAllowedHeaders(Arrays.asList(allowedHeaders.split(",")));
configuration.setExposedHeaders(Arrays.asList(allowedExposeHeaders.split(",")));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

View File

@@ -0,0 +1,79 @@
package com.zl.mjga.config;
import com.zl.mjga.job.DataBackupJob;
import java.util.Map;
import java.util.Properties;
import javax.sql.DataSource;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.Trigger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
@Configuration
public class QuartzConfig {
@Value("${spring.flyway.default-schema}")
private String defaultSchema;
@Bean("emailJobSchedulerFactory")
public SchedulerFactoryBean emailJobSchedulerFactory(DataSource dataSource) {
SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean();
schedulerFactory.setSchedulerName("email-scheduler");
schedulerFactory.setDataSource(dataSource);
Properties props = getCommonProps();
props.setProperty("org.quartz.threadPool.threadCount", "10");
schedulerFactory.setQuartzProperties(props);
return schedulerFactory;
}
@Bean("dataBackupJobDetail")
public JobDetailFactoryBean dataBackupJobDetail() {
JobDetailFactoryBean factory = new JobDetailFactoryBean();
factory.setJobClass(DataBackupJob.class);
factory.setJobDataMap(new JobDataMap(Map.of("roleId", "Gh2mxa")));
factory.setName("data-backup-job");
factory.setGroup("batch-service");
factory.setDurability(true);
return factory;
}
@Bean("dataBackupSchedulerFactory")
public SchedulerFactoryBean dataBackupSchedulerFactory(
Trigger dataBackupTrigger, JobDetail dataBackupJobDetail, DataSource dataSource) {
SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean();
schedulerFactory.setSchedulerName("data-backup-scheduler");
Properties props = getCommonProps();
props.setProperty("org.quartz.threadPool.threadCount", "10");
schedulerFactory.setQuartzProperties(props);
schedulerFactory.setJobDetails(dataBackupJobDetail);
schedulerFactory.setTriggers(dataBackupTrigger);
schedulerFactory.setDataSource(dataSource);
return schedulerFactory;
}
private Properties getCommonProps() {
Properties props = new Properties();
props.setProperty(
"org.quartz.jobStore.class",
"org.springframework.scheduling.quartz.LocalDataSourceJobStore");
props.setProperty(
"org.quartz.jobStore.driverDelegateClass",
"org.quartz.impl.jdbcjobstore.PostgreSQLDelegate");
props.setProperty("org.quartz.jobStore.tablePrefix", String.format("%s.qrtz_", defaultSchema));
return props;
}
@Bean("dataBackupTrigger")
public CronTriggerFactoryBean dataBackupTrigger(JobDetail dataBackupJobDetail) {
CronTriggerFactoryBean factory = new CronTriggerFactoryBean();
factory.setJobDetail(dataBackupJobDetail);
factory.setCronExpression("0 0/5 * * * ?");
return factory;
}
}

View File

@@ -0,0 +1,31 @@
package com.zl.mjga.config.cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@EnableCaching
@Configuration
public class CacheConfig {
public static final String VERIFY_CODE = "verifyCode";
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(List.of(verifyCodeCache()));
return cacheManager;
}
private CaffeineCache verifyCodeCache() {
return new CaffeineCache(
VERIFY_CODE,
Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(60, TimeUnit.SECONDS).build());
}
}

View File

@@ -0,0 +1,30 @@
package com.zl.mjga.config.security;
import jakarta.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.RequestRejectedHandler;
import org.springframework.security.web.firewall.StrictHttpFirewall;
@Configuration
public class HttpFireWallConfig {
@Bean
public HttpFirewall getHttpFirewall() {
return new StrictHttpFirewall();
}
@Bean
public RequestRejectedHandler requestRejectedHandler() {
return (request, response, requestRejectedException) -> {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setContentType(MediaType.TEXT_PLAIN_VALUE);
try (PrintWriter writer = response.getWriter()) {
writer.write(requestRejectedException.getMessage());
}
};
}
}

View File

@@ -0,0 +1,79 @@
package com.zl.mjga.config.security;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Getter
public class Jwt {
private final String secret;
private final int expirationMin;
private final JWTVerifier verifier;
public Jwt(
@Value("${jwt.secret}") String secret, @Value("${jwt.expiration-min}") int expirationMin) {
this.verifier = JWT.require(Algorithm.HMAC256(secret)).build();
this.secret = secret;
this.expirationMin = expirationMin;
}
public String getSubject(String token) {
return JWT.decode(token).getSubject();
}
public Boolean verify(String token) {
try {
verifier.verify(token);
return Boolean.TRUE;
} catch (JWTVerificationException e) {
return Boolean.FALSE;
}
}
public String extract(HttpServletRequest request) {
String authorization = request.getHeader("Authorization");
if (StringUtils.isNotEmpty(authorization) && authorization.startsWith("Bearer")) {
return authorization.substring(7);
} else {
return null;
}
}
public String create(String userIdentify) {
return JWT.create()
.withSubject(String.valueOf(userIdentify))
.withIssuedAt(new Date())
.withExpiresAt(
Date.from(
LocalDateTime.now()
.plusMinutes(expirationMin)
.atZone(ZoneId.systemDefault())
.toInstant()))
.sign(Algorithm.HMAC256(secret));
}
public void makeToken(
HttpServletRequest request, HttpServletResponse response, String userIdentify) {
response.addHeader("Authorization", String.format("Bearer %s", create(userIdentify)));
}
public void removeToken(HttpServletRequest request, HttpServletResponse response) {
response.addHeader("Authorization", null);
}
}

View File

@@ -0,0 +1,44 @@
package com.zl.mjga.config.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
@Slf4j
@Setter
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final Jwt jwt;
private final UserDetailsServiceImpl userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = jwt.extract(request);
if (StringUtils.isNotEmpty(token) && jwt.verify(token)) {
try {
UserDetails userDetails = userDetailsService.loadUserByUsername(jwt.getSubject(token));
JwtAuthenticationToken authenticated =
JwtAuthenticationToken.authenticated(userDetails, token, userDetails.getAuthorities());
authenticated.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticated);
} catch (Exception e) {
log.error("jwt with invalid user id {}", jwt.getSubject(token), e);
}
}
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,57 @@
package com.zl.mjga.config.security;
import java.io.Serial;
import java.util.Collection;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.core.userdetails.UserDetails;
@Setter
@Getter
@ToString
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
@Serial private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private String credentials;
public JwtAuthenticationToken(Object principal, String credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(false);
}
public JwtAuthenticationToken(
Object principal, String credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
public static JwtAuthenticationToken unauthenticated(String userIdentify, String token) {
return new JwtAuthenticationToken(userIdentify, token);
}
public static JwtAuthenticationToken authenticated(
UserDetails principal, String token, Collection<? extends GrantedAuthority> authorities) {
return new JwtAuthenticationToken(principal, token, authorities);
}
@Override
public String getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
}

View File

@@ -0,0 +1,14 @@
package com.zl.mjga.config.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@@ -0,0 +1,28 @@
package com.zl.mjga.config.security;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
public class RestfulAuthenticationEntryPointHandler
implements AccessDeniedHandler, AuthenticationEntryPoint {
@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
@Override
public void handle(
HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
}
}

View File

@@ -0,0 +1,38 @@
package com.zl.mjga.config.security;
import com.zl.mjga.dto.urp.UserRolePermissionDto;
import com.zl.mjga.service.IdentityAccessService;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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;
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final IdentityAccessService identityAccessService;
@Override
public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException {
UserRolePermissionDto userRolePermissionDto =
identityAccessService.queryUniqueUserWithRolePermission(Long.valueOf(id));
if (userRolePermissionDto == null) {
throw new UsernameNotFoundException(String.format("uid %s user not found", id));
}
return new User(
userRolePermissionDto.getUsername(),
userRolePermissionDto.getPassword(),
userRolePermissionDto.getEnable(),
true,
true,
true,
userRolePermissionDto.getPermissions().stream()
.map((permission) -> new SimpleGrantedAuthority(permission.getCode()))
.collect(Collectors.toSet()));
}
}

View File

@@ -0,0 +1,77 @@
package com.zl.mjga.config.security;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.*;
import org.springframework.web.cors.CorsConfigurationSource;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
private final Jwt jwt;
private final CorsConfigurationSource corsConfigurationSource;
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public RequestMatcher publicEndPointMatcher() {
return new OrRequestMatcher(
new AntPathRequestMatcher("/auth/sign-in", HttpMethod.POST.name()),
new AntPathRequestMatcher("/auth/sign-up", HttpMethod.POST.name()),
new AntPathRequestMatcher("/v3/api-docs/**", HttpMethod.GET.name()),
new AntPathRequestMatcher("/swagger-ui/**", HttpMethod.GET.name()),
new AntPathRequestMatcher("/swagger-ui.html", HttpMethod.GET.name()),
new AntPathRequestMatcher("/error"));
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
RestfulAuthenticationEntryPointHandler restfulAuthenticationEntryPointHandler =
new RestfulAuthenticationEntryPointHandler();
/*
<Stateless API CSRF protection>
http.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
*/
http.cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource));
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(
authorize ->
authorize
.requestMatchers(publicEndPointMatcher())
.permitAll()
.anyRequest()
.authenticated())
.exceptionHandling(
(exceptionHandling) ->
exceptionHandling
.accessDeniedHandler(restfulAuthenticationEntryPointHandler)
.authenticationEntryPoint(restfulAuthenticationEntryPointHandler))
.sessionManagement(
session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterAt(
new JwtAuthenticationFilter(jwt, userDetailsService),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}

View File

@@ -0,0 +1,51 @@
package com.zl.mjga.controller;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.PageResponseDto;
import com.zl.mjga.dto.department.DepartmentQueryDto;
import com.zl.mjga.dto.department.DepartmentRespDto;
import com.zl.mjga.repository.DepartmentRepository;
import com.zl.mjga.service.DepartmentService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.jooq.generated.mjga.tables.pojos.Department;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
@RestController
@RequestMapping("/department")
@RequiredArgsConstructor
public class DepartmentController {
private final DepartmentService departmentService;
private final DepartmentRepository departmentRepository;
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_DEPARTMENT_PERMISSION)")
@GetMapping("/page-query")
@ResponseStatus(HttpStatus.OK)
PageResponseDto<List<DepartmentRespDto>> pageQueryDepartments(
@ModelAttribute PageRequestDto pageRequestDto,
@ModelAttribute DepartmentQueryDto departmentQueryDto) {
return departmentService.pageQueryDepartment(pageRequestDto, departmentQueryDto);
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_DEPARTMENT_PERMISSION)")
@GetMapping("/query")
List<Department> queryDepartments() {
return departmentRepository.findAll();
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_DEPARTMENT_PERMISSION)")
@DeleteMapping()
void deleteDepartment(@RequestParam Long id) {
departmentRepository.deleteById(id);
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_DEPARTMENT_PERMISSION)")
@PostMapping()
void upsertDepartment(@RequestBody Department department) {
departmentRepository.merge(department);
}
}

View File

@@ -0,0 +1,177 @@
package com.zl.mjga.controller;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.PageResponseDto;
import com.zl.mjga.dto.department.DepartmentBindDto;
import com.zl.mjga.dto.permission.PermissionBindDto;
import com.zl.mjga.dto.position.PositionBindDto;
import com.zl.mjga.dto.role.RoleBindDto;
import com.zl.mjga.dto.urp.*;
import com.zl.mjga.repository.RoleRepository;
import com.zl.mjga.repository.UserRepository;
import com.zl.mjga.service.IdentityAccessService;
import jakarta.validation.Valid;
import java.security.Principal;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.jooq.generated.mjga.tables.pojos.User;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
@RestController
@RequestMapping("/iam")
@RequiredArgsConstructor
public class IdentityAccessController {
private final IdentityAccessService identityAccessService;
private final UserRepository userRepository;
private final RoleRepository roleRepository;
@GetMapping("/me")
UserRolePermissionDto currentUser(Principal principal) {
String name = principal.getName();
User user = userRepository.fetchOneByUsername(name);
return identityAccessService.queryUniqueUserWithRolePermission(user.getId());
}
@PostMapping("/me")
void upsertMe(Principal principal, @RequestBody UserUpsertDto userUpsertDto) {
String name = principal.getName();
User user = userRepository.fetchOneByUsername(name);
userUpsertDto.setId(user.getId());
identityAccessService.upsertUser(userUpsertDto);
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
@PostMapping("/user")
void upsertUser(@RequestBody UserUpsertDto userUpsertDto) {
identityAccessService.upsertUser(userUpsertDto);
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_USER_ROLE_PERMISSION)")
@GetMapping("/user")
UserRolePermissionDto queryUserWithRolePermission(@RequestParam Long userId) {
return identityAccessService.queryUniqueUserWithRolePermission(userId);
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
@DeleteMapping("/user")
void deleteUser(@RequestParam Long userId) {
userRepository.deleteById(userId);
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
@PostMapping("/role")
void upsertRole(@RequestBody RoleUpsertDto roleUpsertDto) {
identityAccessService.upsertRole(roleUpsertDto);
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
@DeleteMapping("/role")
void deleteRole(@RequestParam Long roleId) {
roleRepository.deleteById(roleId);
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
@GetMapping("/role")
RoleDto queryRoleWithPermission(@RequestParam Long roleId) {
return identityAccessService.queryUniqueRoleWithPermission(roleId);
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
@PostMapping("/permission")
void upsertPermission(@RequestBody PermissionUpsertDto permissionUpsertDto) {
identityAccessService.upsertPermission(permissionUpsertDto);
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
@DeleteMapping("/permission")
void deletePermission(@RequestParam Long permissionId) {
roleRepository.deleteById(permissionId);
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_USER_ROLE_PERMISSION)")
@GetMapping("/users")
@ResponseStatus(HttpStatus.OK)
PageResponseDto<List<UserRolePermissionDto>> queryUsers(
@ModelAttribute PageRequestDto pageRequestDto, @ModelAttribute UserQueryDto userQueryDto) {
return identityAccessService.pageQueryUser(pageRequestDto, userQueryDto);
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_USER_ROLE_PERMISSION)")
@GetMapping("/roles")
@ResponseStatus(HttpStatus.OK)
PageResponseDto<List<RoleDto>> queryRoles(
@ModelAttribute PageRequestDto pageRequestDto, @ModelAttribute RoleQueryDto roleQueryDto) {
return identityAccessService.pageQueryRole(pageRequestDto, roleQueryDto);
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_USER_ROLE_PERMISSION)")
@GetMapping("/permissions")
@ResponseStatus(HttpStatus.OK)
PageResponseDto<List<PermissionRespDto>> queryPermissions(
@ModelAttribute PageRequestDto pageRequestDto,
@ModelAttribute PermissionQueryDto permissionQueryDto) {
return identityAccessService.pageQueryPermission(pageRequestDto, permissionQueryDto);
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
@PostMapping("/role/bind")
@ResponseStatus(HttpStatus.OK)
void bindRoleBy(@RequestBody @Valid RoleBindDto roleBindDto) {
identityAccessService.bindRoleToUser(roleBindDto.userId(), roleBindDto.roleIds());
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
@PostMapping("/role/unbind")
@ResponseStatus(HttpStatus.OK)
void unBindRoleBy(@RequestBody @Valid RoleBindDto roleBindDto) {
identityAccessService.unBindRoleToUser(roleBindDto.userId(), roleBindDto.roleIds());
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
@PostMapping("/permission/bind")
@ResponseStatus(HttpStatus.OK)
void bindPermissionBy(@RequestBody @Valid PermissionBindDto permissionBindDto) {
identityAccessService.bindPermissionBy(
permissionBindDto.roleId(), permissionBindDto.permissionIds());
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
@PostMapping("/permission/unbind")
@ResponseStatus(HttpStatus.OK)
void unBindPermissionBy(@RequestBody @Valid PermissionBindDto permissionBindDto) {
identityAccessService.unBindPermissionBy(
permissionBindDto.roleId(), permissionBindDto.permissionIds());
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_DEPARTMENT_PERMISSION)")
@PostMapping("/department/bind")
@ResponseStatus(HttpStatus.OK)
public void bindDepartmentBy(@RequestBody @Valid DepartmentBindDto departmentBindDto) {
identityAccessService.bindDepartmentBy(departmentBindDto);
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_DEPARTMENT_PERMISSION)")
@PostMapping("/department/unbind")
@ResponseStatus(HttpStatus.OK)
public void unBindDepartmentBy(@RequestBody @Valid DepartmentBindDto departmentBindDto) {
identityAccessService.unBindDepartmentBy(departmentBindDto);
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_DEPARTMENT_PERMISSION)")
@PostMapping("/position/bind")
@ResponseStatus(HttpStatus.OK)
public void bindPositionBy(@RequestBody @Valid PositionBindDto positionBindDto) {
identityAccessService.bindPositionBy(positionBindDto);
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_DEPARTMENT_PERMISSION)")
@PostMapping("/position/unbind")
@ResponseStatus(HttpStatus.OK)
public void unBindPositionBy(@RequestBody @Valid PositionBindDto positionBindDto) {
identityAccessService.unBindPositionBy(positionBindDto);
}
}

View File

@@ -0,0 +1,51 @@
package com.zl.mjga.controller;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.PageResponseDto;
import com.zl.mjga.dto.position.PositionQueryDto;
import com.zl.mjga.dto.position.PositionRespDto;
import com.zl.mjga.repository.PositionRepository;
import com.zl.mjga.service.PositionService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.jooq.generated.mjga.tables.pojos.Position;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
@RestController
@RequestMapping("/position")
@RequiredArgsConstructor
public class PositionController {
private final PositionService positionService;
private final PositionRepository positionRepository;
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_POSITION_PERMISSION)")
@GetMapping("/page-query")
@ResponseStatus(HttpStatus.OK)
PageResponseDto<List<PositionRespDto>> pageQueryPositions(
@ModelAttribute PageRequestDto pageRequestDto,
@ModelAttribute PositionQueryDto positionQueryDto) {
return positionService.pageQueryPosition(pageRequestDto, positionQueryDto);
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_POSITION_PERMISSION)")
@GetMapping("/query")
List<Position> queryPositions() {
return positionRepository.findAll();
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_POSITION_PERMISSION)")
@DeleteMapping()
void deletePosition(@RequestParam Long id) {
positionRepository.deleteById(id);
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_POSITION_PERMISSION)")
@PostMapping()
void upsertPosition(@RequestBody Position position) {
positionRepository.merge(position);
}
}

View File

@@ -0,0 +1,58 @@
package com.zl.mjga.controller;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.PageResponseDto;
import com.zl.mjga.dto.scheduler.JobKeyDto;
import com.zl.mjga.dto.scheduler.JobTriggerDto;
import com.zl.mjga.dto.scheduler.QueryDto;
import com.zl.mjga.dto.scheduler.TriggerKeyDto;
import com.zl.mjga.service.SchedulerService;
import java.util.Date;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.quartz.JobKey;
import org.quartz.SchedulerException;
import org.quartz.TriggerKey;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/scheduler")
@RequiredArgsConstructor
public class SchedulerController {
private final SchedulerService schedulerService;
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_SCHEDULER_PERMISSION)")
@GetMapping("/page-query")
public PageResponseDto<List<JobTriggerDto>> pageQuery(
@ModelAttribute PageRequestDto pageRequestDto, @ModelAttribute QueryDto queryDto) {
return schedulerService.getJobWithTriggerBy(pageRequestDto, queryDto);
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_SCHEDULER_PERMISSION)")
@PostMapping("/trigger/resume")
public void resumeTrigger(@RequestBody TriggerKeyDto triggerKey) throws SchedulerException {
schedulerService.resumeTrigger(new TriggerKey(triggerKey.name(), triggerKey.group()));
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_SCHEDULER_PERMISSION)")
@PostMapping("/trigger/pause")
public void pauseTrigger(@RequestBody TriggerKeyDto triggerKey) throws SchedulerException {
schedulerService.pauseTrigger(new TriggerKey(triggerKey.name(), triggerKey.group()));
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_SCHEDULER_PERMISSION)")
@PostMapping("/job/trigger")
public void triggerJob(@RequestBody JobKeyDto jobKeyDto, @RequestParam Long startAt)
throws SchedulerException {
schedulerService.triggerJob(new JobKey(jobKeyDto.name(), jobKeyDto.group()), new Date(startAt));
}
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_SCHEDULER_PERMISSION)")
@PutMapping("/job/update")
public void updateJob(@RequestBody TriggerKeyDto triggerKey, @RequestParam String cron)
throws SchedulerException {
schedulerService.updateCronTrigger(new TriggerKey(triggerKey.name(), triggerKey.group()), cron);
}
}

View File

@@ -0,0 +1,43 @@
package com.zl.mjga.controller;
import com.zl.mjga.config.security.Jwt;
import com.zl.mjga.dto.sign.SignInDto;
import com.zl.mjga.dto.sign.SignUpDto;
import com.zl.mjga.service.SignService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class SignController {
private final SignService signService;
private final Jwt jwt;
@ResponseStatus(HttpStatus.OK)
@PostMapping("/sign-in")
void signIn(
HttpServletRequest request,
HttpServletResponse response,
@RequestBody @Valid SignInDto signInDto) {
jwt.makeToken(request, response, String.valueOf(signService.signIn(signInDto)));
}
@ResponseStatus(HttpStatus.CREATED)
@PostMapping("/sign-up")
void signUp(@RequestBody @Valid SignUpDto signUpDto) {
signService.signUp(signUpDto);
}
@ResponseStatus(HttpStatus.OK)
@PostMapping("/sign-out")
void signOut(HttpServletRequest request, HttpServletResponse response) {
jwt.removeToken(request, response);
}
}

View File

@@ -0,0 +1,129 @@
package com.zl.mjga.dto;
import static org.jooq.impl.DSL.field;
import static org.jooq.impl.DSL.name;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.*;
import org.apache.commons.lang3.StringUtils;
import org.jooq.SortField;
import org.jooq.SortOrder;
@Data
@NoArgsConstructor
public class PageRequestDto {
public static final String REGEX = "^[a-zA-Z][a-zA-Z0-9_]*$";
public static final String SPACE = " ";
private long page;
private long size;
private Map<String, Direction> sortBy = new HashMap<>();
public PageRequestDto(int page, int size) {
checkPageAndSize(page, size);
this.page = page;
this.size = size;
}
public PageRequestDto(int page, int size, Map<String, Direction> sortBy) {
checkPageAndSize(page, size);
this.page = page;
this.size = size;
this.sortBy = sortBy;
}
@AllArgsConstructor
@Getter
public enum Direction {
ASC("ASC"),
DESC("DESC");
private final String keyword;
public static Direction fromString(String value) {
try {
return Direction.valueOf(value.toUpperCase(Locale.US));
} catch (Exception e) {
throw new IllegalArgumentException(
String.format(
"Invalid value '%s' for orders given; Has to be either 'desc' or 'asc' (case"
+ " insensitive)",
value),
e);
}
}
}
public static PageRequestDto of(int page, int size) {
return new PageRequestDto(page, size);
}
public static PageRequestDto of(int page, int size, Map<String, Direction> sortBy) {
return new PageRequestDto(page, size, sortBy);
}
public List<SortField<Object>> getSortFields() {
return sortBy.entrySet().stream()
.map(
(entry) ->
field(name(entry.getKey())).sort(SortOrder.valueOf(entry.getValue().getKeyword())))
.toList();
}
private void checkPageAndSize(int page, int size) {
if (page < 0) {
throw new IllegalArgumentException("Page index must not be less than zero");
}
if (size < 1) {
throw new IllegalArgumentException("Page size must not be less than one");
}
}
public long getOffset() {
if (page == 0) {
return 0;
} else {
return (page - 1) * size;
}
}
public void setSortBy(String sortBy) {
this.sortBy = convertSortBy(sortBy);
}
private Map<String, Direction> convertSortBy(String sortBy) {
Map<String, Direction> result = new HashMap<>();
if (StringUtils.isEmpty(sortBy)) {
return result;
}
for (String fieldSpaceDirection : sortBy.split(",")) {
String[] fieldDirectionArray = fieldSpaceDirection.split(SPACE);
if (fieldDirectionArray.length != 2) {
throw new IllegalArgumentException(
String.format(
"Invalid sortBy field format %s. The expect format is [col1 asc,col2 desc]",
sortBy));
}
String field = fieldDirectionArray[0];
if (!verifySortField(field)) {
throw new IllegalArgumentException(
String.format("Invalid Sort field %s. Sort field must match %s", sortBy, REGEX));
}
String direction = fieldDirectionArray[1];
result.put(field, Direction.fromString(direction));
}
return result;
}
private static boolean verifySortField(String sortField) {
Pattern pattern = Pattern.compile(REGEX);
Matcher matcher = pattern.matcher(sortField);
return matcher.matches();
}
}

View File

@@ -0,0 +1,22 @@
package com.zl.mjga.dto;
import jakarta.annotation.Nullable;
import lombok.*;
@Data
public class PageResponseDto<T> {
private long total;
private T data;
public PageResponseDto(long total, @Nullable T data) {
if (total < 0) {
throw new IllegalArgumentException("total must not be less than zero");
}
this.total = total;
this.data = data;
}
public static <T> PageResponseDto<T> empty() {
return new PageResponseDto<>(0, null);
}
}

View File

@@ -0,0 +1,6 @@
package com.zl.mjga.dto.department;
import jakarta.validation.constraints.NotNull;
import java.util.List;
public record DepartmentBindDto(@NotNull Long userId, @NotNull List<Long> departmentIds) {}

View File

@@ -0,0 +1,16 @@
package com.zl.mjga.dto.department;
import com.zl.mjga.model.urp.BindState;
import lombok.*;
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class DepartmentQueryDto {
private Long userId;
private String name;
private Boolean enable;
private BindState bindState = BindState.ALL;
}

View File

@@ -0,0 +1,20 @@
package com.zl.mjga.dto.department;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DepartmentRespDto {
@NotNull private Long id;
@NotEmpty private String name;
private Long parentId;
private String parentName;
private Boolean isBound;
}

View File

@@ -0,0 +1,17 @@
package com.zl.mjga.dto.department;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.*;
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class DepartmentUpsertDto {
private Long id;
@NotEmpty private String name;
private Long parentId;
@NotNull private Boolean enable;
}

View File

@@ -0,0 +1,8 @@
package com.zl.mjga.dto.permission;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.List;
public record PermissionBindDto(
@NotNull Long roleId, @NotEmpty(message = "权限不能为空") List<Long> permissionIds) {}

View File

@@ -0,0 +1,6 @@
package com.zl.mjga.dto.position;
import jakarta.validation.constraints.NotNull;
import java.util.List;
public record PositionBindDto(@NotNull Long userId, @NotNull List<Long> positionIds) {}

View File

@@ -0,0 +1,15 @@
package com.zl.mjga.dto.position;
import com.zl.mjga.model.urp.BindState;
import lombok.*;
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class PositionQueryDto {
private Long userId;
private String name;
private BindState bindState = BindState.ALL;
}

View File

@@ -0,0 +1,19 @@
package com.zl.mjga.dto.position;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class PositionRespDto {
@NotNull private Long id;
@NotEmpty private String name;
private Long parentId;
private Boolean isBound;
}

View File

@@ -0,0 +1,17 @@
package com.zl.mjga.dto.position;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.*;
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class PositionUpsertDto {
private Long id;
@NotEmpty private String name;
private Long parentId;
@NotNull private Boolean enable;
}

View File

@@ -0,0 +1,8 @@
package com.zl.mjga.dto.role;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.List;
public record RoleBindDto(
@NotNull(message = "用户不能为空") Long userId, @NotEmpty(message = "角色不能为空") List<Long> roleIds) {}

View File

@@ -0,0 +1,5 @@
package com.zl.mjga.dto.scheduler;
import jakarta.validation.constraints.NotEmpty;
public record JobKeyDto(@NotEmpty String name, @NotEmpty String group) {}

View File

@@ -0,0 +1,26 @@
package com.zl.mjga.dto.scheduler;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class JobTriggerDto {
private String name;
private String group;
private String className;
private Map jobDataMap;
private String triggerName;
private String triggerGroup;
private String schedulerType;
private String cronExpression;
private long startTime;
private long endTime;
private long nextFireTime;
private long previousFireTime;
private String triggerState;
private Map triggerJobDataMap;
}

Some files were not shown because too many files have changed in this diff Show More