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;
}

View File

@@ -0,0 +1,3 @@
package com.zl.mjga.dto.scheduler;
public record QueryDto(String name) {}

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
package com.zl.mjga.dto.sign;
import jakarta.validation.constraints.NotEmpty;
import lombok.*;
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class SignInDto {
@NotEmpty private String username;
@NotEmpty private String password;
}

View File

@@ -0,0 +1,15 @@
package com.zl.mjga.dto.sign;
import jakarta.validation.constraints.NotEmpty;
import lombok.*;
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class SignUpDto {
@NotEmpty private String username;
@NotEmpty private String password;
}

View File

@@ -0,0 +1,18 @@
package com.zl.mjga.dto.urp;
import com.zl.mjga.model.urp.BindState;
import java.util.List;
import lombok.*;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class PermissionQueryDto {
private Long roleId;
private Long permissionId;
private String permissionCode;
private String permissionName;
private List<Long> permissionIdList;
private BindState bindState = BindState.ALL;
}

View File

@@ -0,0 +1,14 @@
package com.zl.mjga.dto.urp;
import lombok.*;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class PermissionRespDto {
private Long id;
private String code;
private String name;
private Boolean isBound;
}

View File

@@ -0,0 +1,15 @@
package com.zl.mjga.dto.urp;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class PermissionUpsertDto {
private Long id;
@NotEmpty private String code;
@NotEmpty private String name;
}

View File

@@ -0,0 +1,17 @@
package com.zl.mjga.dto.urp;
import java.util.LinkedList;
import java.util.List;
import lombok.*;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class RoleDto {
private Long id;
private String code;
private String name;
private Boolean isBound;
@Builder.Default List<PermissionRespDto> permissions = new LinkedList<>();
}

View File

@@ -0,0 +1,18 @@
package com.zl.mjga.dto.urp;
import com.zl.mjga.model.urp.BindState;
import java.util.List;
import lombok.*;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class RoleQueryDto {
private Long userId;
private Long roleId;
private String roleCode;
private String roleName;
private List<Long> roleIdList;
private BindState bindState = BindState.ALL;
}

View File

@@ -0,0 +1,15 @@
package com.zl.mjga.dto.urp;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class RoleUpsertDto {
private Long id;
@NotEmpty private String code;
@NotEmpty private String name;
}

View File

@@ -0,0 +1,10 @@
package com.zl.mjga.dto.urp;
import lombok.*;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class UserQueryDto {
private String username;
}

View File

@@ -0,0 +1,34 @@
package com.zl.mjga.dto.urp;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.OffsetDateTime;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.*;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class UserRolePermissionDto {
private Long id;
private String username;
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;
private Boolean enable;
@Builder.Default private List<RoleDto> roles = new LinkedList<>();
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private OffsetDateTime createTime;
public Set<PermissionRespDto> getPermissions() {
return roles.stream()
.flatMap((roleDto) -> roleDto.getPermissions().stream())
.collect(Collectors.toSet());
}
}

View File

@@ -0,0 +1,17 @@
package com.zl.mjga.dto.urp;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class UserUpsertDto {
private Long id;
@NotEmpty private String username;
private String password;
@NotNull private Boolean enable;
}

View File

@@ -0,0 +1,25 @@
package com.zl.mjga.exception;
public class BusinessException extends RuntimeException {
@java.io.Serial private static final long serialVersionUID = -2119302295305964305L;
public BusinessException() {}
public BusinessException(String message) {
super(message);
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
public BusinessException(Throwable cause) {
super(cause);
}
public BusinessException(
String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@@ -0,0 +1,90 @@
package com.zl.mjga.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.lang.Nullable;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.firewall.RequestRejectedException;
import org.springframework.web.ErrorResponseException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(value = {BusinessException.class})
public ResponseEntity<Object> handleBusinessException(BusinessException ex, WebRequest request) {
log.error("Business Error Handled ===> ", ex);
ErrorResponseException errorResponseException =
new ErrorResponseException(
HttpStatus.INTERNAL_SERVER_ERROR,
ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage()),
ex.getCause());
return handleExceptionInternal(
errorResponseException,
errorResponseException.getBody(),
errorResponseException.getHeaders(),
errorResponseException.getStatusCode(),
request);
}
@SuppressWarnings("NullableProblems")
@Override
@Nullable public ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
log.error("MethodArgumentNotValidException Handled ===> ", ex);
ErrorResponseException errorResponseException =
new ErrorResponseException(
status, ProblemDetail.forStatusAndDetail(status, ex.getMessage()), ex.getCause());
return handleExceptionInternal(
errorResponseException,
errorResponseException.getBody(),
errorResponseException.getHeaders(),
errorResponseException.getStatusCode(),
request);
}
@ExceptionHandler(value = {RequestRejectedException.class})
public ResponseEntity<Object> handleRequestRejectedException(
RequestRejectedException ex, WebRequest request) {
log.error("RequestRejectedException Handled ===> ", ex);
ErrorResponseException errorResponseException =
new ErrorResponseException(
HttpStatus.BAD_REQUEST,
ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage()),
ex.getCause());
return handleExceptionInternal(
errorResponseException,
errorResponseException.getBody(),
errorResponseException.getHeaders(),
errorResponseException.getStatusCode(),
request);
}
@ExceptionHandler(value = {AccessDeniedException.class})
public ResponseEntity<Object> handleAccessDenied(AccessDeniedException ex) {
throw ex;
}
@ExceptionHandler(value = {Throwable.class})
public ResponseEntity<Object> handleException(Throwable ex, WebRequest request) {
log.error("System Error Handled ===> ", ex);
ErrorResponseException errorResponseException =
new ErrorResponseException(
HttpStatus.INTERNAL_SERVER_ERROR,
ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "System Error"),
ex.getCause());
return handleExceptionInternal(
errorResponseException,
errorResponseException.getBody(),
errorResponseException.getHeaders(),
errorResponseException.getStatusCode(),
request);
}
}

View File

@@ -0,0 +1,19 @@
package com.zl.mjga.job;
import java.text.MessageFormat;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
@Slf4j
public class DataBackupJob implements Job {
@Override
public void execute(JobExecutionContext context) {
String userId = context.getJobDetail().getJobDataMap().getString("roleId");
log.info(
MessageFormat.format(
"Job execute: JobName {0} Param {1} Thread: {2}",
getClass(), userId, Thread.currentThread().getName()));
}
}

View File

@@ -0,0 +1,19 @@
package com.zl.mjga.job;
import java.text.MessageFormat;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
@Slf4j
public class EmailJob implements Job {
@Override
public void execute(JobExecutionContext context) {
String userEmail = context.getJobDetail().getJobDataMap().getString("userEmail");
log.info(
MessageFormat.format(
"Job execute: JobName {0} Param {1} Thread: {2}",
getClass(), userEmail, Thread.currentThread().getName()));
}
}

View File

@@ -0,0 +1,7 @@
package com.zl.mjga.model.urp;
public enum BindState {
BIND,
UNBIND,
ALL;
}

View File

@@ -0,0 +1,12 @@
package com.zl.mjga.model.urp;
public enum EPermission {
READ_POSITION_PERMISSION,
WRITE_POSITION_PERMISSION,
READ_DEPARTMENT_PERMISSION,
WRITE_DEPARTMENT_PERMISSION,
READ_SCHEDULER_PERMISSION,
WRITE_SCHEDULER_PERMISSION,
WRITE_USER_ROLE_PERMISSION,
READ_USER_ROLE_PERMISSION
}

View File

@@ -0,0 +1,6 @@
package com.zl.mjga.model.urp;
public enum ERole {
ADMIN,
GENERAL
}

View File

@@ -0,0 +1,8 @@
package com.zl.mjga.model.urp;
public enum SchedulerType {
CRON,
SIMPLE,
CALENDAR,
DAILY
}

View File

@@ -0,0 +1,67 @@
package com.zl.mjga.repository;
import static org.jooq.generated.mjga.Tables.*;
import static org.jooq.impl.DSL.noCondition;
import static org.jooq.impl.DSL.noField;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.department.DepartmentQueryDto;
import org.apache.commons.lang3.StringUtils;
import org.jooq.*;
import org.jooq.Record;
import org.jooq.generated.mjga.tables.Department;
import org.jooq.generated.mjga.tables.daos.DepartmentDao;
import org.jooq.impl.DSL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class DepartmentRepository extends DepartmentDao {
@Autowired
public DepartmentRepository(Configuration configuration) {
super(configuration);
}
public Result<Record> pageFetchBy(
PageRequestDto pageRequestDto, DepartmentQueryDto departmentQueryDto) {
Department parent = DEPARTMENT.as("parent");
return ctx()
.select(
DEPARTMENT.asterisk(),
parent.NAME.as("parent_name"),
departmentQueryDto.getUserId() != null
? DSL.when(
DEPARTMENT.ID.in(selectUsersDepartment(departmentQueryDto.getUserId())),
true)
.otherwise(false)
.as("is_bound")
: noField(),
DSL.count().over().as("total_department").convertFrom(Long::valueOf))
.from(DEPARTMENT)
.leftJoin(parent)
.on(parent.ID.eq(DEPARTMENT.PARENT_ID))
.where(
switch (departmentQueryDto.getBindState()) {
case BIND -> DEPARTMENT.ID.in(selectUsersDepartment(departmentQueryDto.getUserId()));
case UNBIND ->
DEPARTMENT.ID.notIn(selectUsersDepartment(departmentQueryDto.getUserId()));
case ALL -> noCondition();
})
.and(
StringUtils.isNotEmpty(departmentQueryDto.getName())
? DEPARTMENT.NAME.like("%" + departmentQueryDto.getName() + "%")
: noCondition())
.orderBy(pageRequestDto.getSortFields())
.limit(pageRequestDto.getSize())
.offset(pageRequestDto.getOffset())
.fetch();
}
private SelectConditionStep<Record1<Long>> selectUsersDepartment(Long userId) {
return DSL.select(USER.department().ID)
.from(USER)
.innerJoin(USER.department())
.where(USER.ID.eq(userId));
}
}

View File

@@ -0,0 +1,81 @@
package com.zl.mjga.repository;
import static org.jooq.generated.mjga.tables.Permission.PERMISSION;
import static org.jooq.generated.mjga.tables.Role.ROLE;
import static org.jooq.impl.DSL.*;
import static org.jooq.impl.DSL.noField;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.urp.PermissionQueryDto;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.jooq.*;
import org.jooq.Record;
import org.jooq.generated.mjga.tables.daos.PermissionDao;
import org.jooq.generated.mjga.tables.pojos.Permission;
import org.jooq.impl.DSL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class PermissionRepository extends PermissionDao {
@Autowired
public PermissionRepository(Configuration configuration) {
super(configuration);
}
public Result<Record> pageFetchBy(
PageRequestDto pageRequestDto, PermissionQueryDto permissionQueryDto) {
return ctx()
.select(
asterisk(),
permissionQueryDto.getRoleId() != null
? when(
PERMISSION.ID.in(selectRolesPermissionIds(permissionQueryDto.getRoleId())),
true)
.otherwise(false)
.as("is_bound")
: noField(),
DSL.count().over().as("total_permission"))
.from(PERMISSION)
.where(
switch (permissionQueryDto.getBindState()) {
case BIND ->
PERMISSION.ID.in(selectRolesPermissionIds(permissionQueryDto.getRoleId()));
case UNBIND ->
PERMISSION.ID.notIn(selectRolesPermissionIds(permissionQueryDto.getRoleId()));
case ALL -> noCondition();
})
.and(
permissionQueryDto.getPermissionId() == null
? noCondition()
: PERMISSION.ID.eq(permissionQueryDto.getPermissionId()))
.and(
StringUtils.isEmpty(permissionQueryDto.getPermissionName())
? noCondition()
: PERMISSION.NAME.like("%" + permissionQueryDto.getPermissionName() + "%"))
.and(
StringUtils.isEmpty(permissionQueryDto.getPermissionName())
? noCondition()
: PERMISSION.CODE.eq(permissionQueryDto.getPermissionCode()))
.orderBy(pageRequestDto.getSortFields())
.limit(pageRequestDto.getSize())
.offset(pageRequestDto.getOffset())
.fetch();
}
private SelectConditionStep<Record1<Long>> selectRolesPermissionIds(Long roleId) {
return DSL.select(ROLE.permission().ID)
.from(ROLE)
.leftJoin(ROLE.permission())
.where(ROLE.ID.eq(roleId));
}
public List<Permission> selectByPermissionIdIn(List<Long> permissionIdList) {
return ctx()
.selectFrom(PERMISSION)
.where(PERMISSION.ID.in(permissionIdList))
.fetchInto(Permission.class);
}
}

View File

@@ -0,0 +1,60 @@
package com.zl.mjga.repository;
import static org.jooq.generated.mjga.Tables.*;
import static org.jooq.impl.DSL.noCondition;
import static org.jooq.impl.DSL.noField;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.position.PositionQueryDto;
import org.apache.commons.lang3.StringUtils;
import org.jooq.*;
import org.jooq.Record;
import org.jooq.generated.mjga.tables.daos.PositionDao;
import org.jooq.impl.DSL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class PositionRepository extends PositionDao {
@Autowired
public PositionRepository(Configuration configuration) {
super(configuration);
}
public Result<Record> pageFetchBy(
PageRequestDto pageRequestDto, PositionQueryDto positionQueryDto) {
return ctx()
.select(
POSITION.asterisk(),
positionQueryDto.getUserId() != null
? DSL.when(POSITION.ID.in(selectUsersPosition(positionQueryDto.getUserId())), true)
.otherwise(false)
.as("is_bound")
: noField(),
DSL.count().over().as("total_position").convertFrom(Long::valueOf))
.from(POSITION)
.where(
switch (positionQueryDto.getBindState()) {
case BIND -> POSITION.ID.in(selectUsersPosition(positionQueryDto.getUserId()));
case UNBIND -> POSITION.ID.notIn(selectUsersPosition(positionQueryDto.getUserId()));
case ALL -> noCondition();
})
.and(
StringUtils.isNotEmpty(positionQueryDto.getName())
? POSITION.NAME.like("%" + positionQueryDto.getName() + "%")
: noCondition())
.orderBy(pageRequestDto.getSortFields())
.limit(pageRequestDto.getSize())
.offset(pageRequestDto.getOffset())
.fetch();
}
private SelectConditionStep<Record1<Long>> selectUsersPosition(Long userId) {
return ctx()
.select(USER.position().ID)
.from(USER)
.innerJoin(USER.position())
.where(USER.ID.eq(userId));
}
}

View File

@@ -0,0 +1,46 @@
package com.zl.mjga.repository;
import static org.jooq.generated.public_.Tables.*;
import static org.jooq.impl.DSL.noCondition;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.scheduler.QueryDto;
import org.apache.commons.lang3.StringUtils;
import org.jooq.*;
import org.jooq.Record;
import org.jooq.generated.public_.tables.daos.QrtzJobDetailsDao;
import org.jooq.impl.DSL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class QrtzJobRepository extends QrtzJobDetailsDao {
@Autowired
public QrtzJobRepository(Configuration configuration) {
super(configuration);
}
public Result<Record> fetchPageWithJobAndTriggerBy(
PageRequestDto pageRequestDto, QueryDto queryDto) {
return ctx()
.select(
QRTZ_JOB_DETAILS.asterisk(),
QRTZ_JOB_DETAILS.qrtzTriggers().asterisk(),
QRTZ_JOB_DETAILS.qrtzTriggers().qrtzCronTriggers().asterisk(),
QRTZ_JOB_DETAILS.qrtzTriggers().qrtzSimpleTriggers().asterisk(),
DSL.count().over().as("total_job"))
.from(QRTZ_JOB_DETAILS)
.leftJoin(QRTZ_JOB_DETAILS.qrtzTriggers())
.leftJoin(QRTZ_JOB_DETAILS.qrtzTriggers().qrtzCronTriggers())
.leftJoin(QRTZ_JOB_DETAILS.qrtzTriggers().qrtzSimpleTriggers())
.where(
StringUtils.isNotEmpty(queryDto.name())
? QRTZ_JOB_DETAILS.SCHED_NAME.eq(queryDto.name())
: noCondition())
.orderBy(pageRequestDto.getSortFields())
.limit(pageRequestDto.getSize())
.offset(pageRequestDto.getOffset())
.fetch();
}
}

View File

@@ -0,0 +1,33 @@
package com.zl.mjga.repository;
import static org.jooq.generated.mjga.tables.RolePermissionMap.ROLE_PERMISSION_MAP;
import java.util.List;
import org.jooq.Configuration;
import org.jooq.generated.mjga.tables.daos.RolePermissionMapDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
@Repository
public class RolePermissionMapRepository extends RolePermissionMapDao {
@Autowired
public RolePermissionMapRepository(Configuration configuration) {
super(configuration);
}
@Transactional
public void deleteByRoleId(Long roleId) {
ctx().deleteFrom(ROLE_PERMISSION_MAP).where(ROLE_PERMISSION_MAP.ROLE_ID.eq(roleId)).execute();
}
@Transactional
public void deleteBy(Long roleId, List<Long> permissionIdList) {
ctx()
.deleteFrom(ROLE_PERMISSION_MAP)
.where(ROLE_PERMISSION_MAP.ROLE_ID.eq(roleId))
.and(ROLE_PERMISSION_MAP.PERMISSION_ID.in(permissionIdList))
.execute();
}
}

View File

@@ -0,0 +1,84 @@
package com.zl.mjga.repository;
import static org.jooq.generated.mjga.Tables.USER;
import static org.jooq.generated.mjga.tables.Role.ROLE;
import static org.jooq.impl.DSL.*;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.urp.RoleQueryDto;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.jooq.*;
import org.jooq.Record;
import org.jooq.generated.mjga.tables.daos.RoleDao;
import org.jooq.generated.mjga.tables.pojos.Permission;
import org.jooq.generated.mjga.tables.pojos.Role;
import org.jooq.impl.DSL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class RoleRepository extends RoleDao {
@Autowired
public RoleRepository(Configuration configuration) {
super(configuration);
}
public List<Role> selectByRoleCodeIn(List<String> roleCodeList) {
return ctx().selectFrom(ROLE).where(ROLE.CODE.in(roleCodeList)).fetchInto(Role.class);
}
public List<Role> selectByRoleIdIn(List<Long> roleIdList) {
return ctx().selectFrom(ROLE).where(ROLE.ID.in(roleIdList)).fetchInto(Role.class);
}
public Result<Record> pageFetchBy(PageRequestDto pageRequestDto, RoleQueryDto roleQueryDto) {
return ctx()
.select(
asterisk(),
roleQueryDto.getUserId() != null
? when(ROLE.ID.in(selectUsersRoleIds(roleQueryDto.getUserId())), true)
.otherwise(false)
.as("is_bound")
: noField(),
multiset(select(ROLE.permission().asterisk()).from(ROLE.permission()))
.convertFrom(r -> r.into(Permission.class))
.as("permissions"),
DSL.count(ROLE.ID).over().as("total_role"))
.from(ROLE)
.where(
switch (roleQueryDto.getBindState()) {
case BIND -> ROLE.ID.in(selectUsersRoleIds(roleQueryDto.getUserId()));
case UNBIND -> ROLE.ID.notIn(selectUsersRoleIds(roleQueryDto.getUserId()));
case ALL -> noCondition();
})
.and(
roleQueryDto.getRoleId() == null ? noCondition() : ROLE.ID.eq(roleQueryDto.getRoleId()))
.and(
StringUtils.isEmpty(roleQueryDto.getRoleName())
? noCondition()
: ROLE.NAME.like("%" + roleQueryDto.getRoleName() + "%"))
.and(
StringUtils.isEmpty(roleQueryDto.getRoleCode())
? noCondition()
: ROLE.CODE.eq(roleQueryDto.getRoleCode()))
.orderBy(pageRequestDto.getSortFields())
.limit(pageRequestDto.getSize())
.offset(pageRequestDto.getOffset())
.fetch();
}
private SelectConditionStep<Record1<Long>> selectUsersRoleIds(Long userId) {
return DSL.select(USER.role().ID).from(USER).innerJoin(USER.role()).where(USER.ID.eq(userId));
}
public Result<Record> fetchUniqueRoleWithPermission(Long roleId) {
return ctx()
.select(ROLE.asterisk(), ROLE.permission().asterisk())
.from(ROLE, ROLE.permission())
.where(ROLE.ID.eq(roleId))
.orderBy(ROLE.ID)
.fetch();
}
}

View File

@@ -0,0 +1,14 @@
package com.zl.mjga.repository;
import org.jooq.Configuration;
import org.jooq.generated.mjga.tables.daos.UserDepartmentMapDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class UserDepartmentMapRepository extends UserDepartmentMapDao {
@Autowired
public UserDepartmentMapRepository(Configuration configuration) {
super(configuration);
}
}

View File

@@ -0,0 +1,14 @@
package com.zl.mjga.repository;
import org.jooq.Configuration;
import org.jooq.generated.mjga.tables.daos.UserPositionMapDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class UserPositionMapRepository extends UserPositionMapDao {
@Autowired
public UserPositionMapRepository(Configuration configuration) {
super(configuration);
}
}

View File

@@ -0,0 +1,151 @@
package com.zl.mjga.repository;
import static org.jooq.generated.mjga.Tables.*;
import static org.jooq.generated.mjga.tables.User.USER;
import static org.jooq.impl.DSL.*;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.urp.PermissionRespDto;
import com.zl.mjga.dto.urp.RoleDto;
import com.zl.mjga.dto.urp.UserQueryDto;
import com.zl.mjga.dto.urp.UserRolePermissionDto;
import org.apache.commons.lang3.StringUtils;
import org.jooq.*;
import org.jooq.Record;
import org.jooq.generated.mjga.tables.daos.*;
import org.jooq.generated.mjga.tables.pojos.User;
import org.jooq.impl.DSL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
@Repository
public class UserRepository extends UserDao {
@Autowired
public UserRepository(Configuration configuration) {
super(configuration);
}
@Transactional
public void mergeWithoutNullFieldBy(User user) {
ctx()
.mergeInto(USER)
.using(
select(
value(user.getId()).as("id"),
value(user.getUsername()).as("username"),
value(user.getPassword()).as("password"),
value(user.getEnable()).as("enable"))
.asTable("newUser"))
.on(USER.ID.eq(DSL.field(DSL.name("newUser", "id"), Long.class)))
.whenMatchedThenUpdate()
.set(USER.USERNAME, DSL.field(DSL.name("newUser", "username"), String.class))
.set(
USER.PASSWORD,
StringUtils.isNotEmpty(user.getPassword())
? DSL.field(DSL.name("newUser", "password"), String.class)
: USER.PASSWORD)
.set(USER.ENABLE, DSL.field(DSL.name("newUser", "enable"), Boolean.class))
.whenNotMatchedThenInsert(USER.USERNAME, USER.PASSWORD, USER.ENABLE)
.values(
DSL.field(DSL.name("newUser", "username"), String.class),
DSL.field(DSL.name("newUser", "password"), String.class),
DSL.field(DSL.name("newUser", "enable"), Boolean.class))
.execute();
}
public Result<Record> pageFetchBy(PageRequestDto pageRequestDto, UserQueryDto userQueryDto) {
return ctx()
.select(asterisk(), DSL.count().over().as("total_user"))
.from(USER)
.where(
userQueryDto.getUsername() != null
? USER.USERNAME.like("%" + userQueryDto.getUsername() + "%")
: noCondition())
.orderBy(pageRequestDto.getSortFields())
.limit(pageRequestDto.getSize())
.offset(pageRequestDto.getOffset())
.fetch();
}
public UserRolePermissionDto fetchUniqueUserDtoWithNestedRolePermissionBy(Long userId) {
return ctx()
.select(
USER.asterisk(),
multiset(
select(
USER.role().asterisk(),
multiset(
select(USER.role().permission().asterisk())
.from(USER.role().permission()))
.convertFrom(
r -> r.map((record) -> record.into(PermissionRespDto.class)))
.as("permissions"))
.from(USER.role()))
.convertFrom(r -> r.map((record) -> record.into(RoleDto.class)))
.as("roles"))
.from(USER)
.where(USER.ID.eq(userId))
.fetchOneInto(UserRolePermissionDto.class);
}
// public UserRolePermissionDto fetchUniqueUserDtoWithNestedRolePermissionBy(Long roleId) {
// return ctx()
// .select(
// USER.asterisk(),
// multiset(
// select(
// ROLE.asterisk(),
// multiset(
// select(PERMISSION.asterisk())
// .from(ROLE_PERMISSION_MAP)
// .leftJoin(PERMISSION)
// .on(ROLE_PERMISSION_MAP.PERMISSION_ID.eq(PERMISSION.ID))
// .where(ROLE_PERMISSION_MAP.ROLE_ID.eq(ROLE.ID)))
// .convertFrom(
// r -> r.map((record) ->
// record.into(PermissionRespDto.class)))
// .as("permissions"))
// .from(USER_ROLE_MAP)
// .leftJoin(ROLE)
// .on(USER_ROLE_MAP.ROLE_ID.eq(ROLE.ID))
// .where(USER.ID.eq(USER_ROLE_MAP.USER_ID)))
// .convertFrom(r -> r.map((record) -> record.into(RoleDto.class)))
// .as("roles"))
// .from(USER)
// .where(USER.ID.eq(roleId))
// .fetchOneInto(UserRolePermissionDto.class);
// }
public Result<Record> fetchUniqueUserWithRolePermissionBy(Long userId) {
return ctx()
.select(USER.asterisk(), USER.role().asterisk(), USER.role().permission().asterisk())
.from(USER)
.leftJoin(USER.role())
.leftJoin(USER.role().permission())
.where(USER.ID.eq(userId))
.fetch();
}
// public Result<Record> fetchUniqueUserWithRolePermissionBy(Long roleId) {
// return ctx()
// .select()
// .from(USER)
// .leftJoin(USER_ROLE_MAP)
// .on(USER.ID.eq(USER_ROLE_MAP.USER_ID))
// .leftJoin(ROLE)
// .on(USER_ROLE_MAP.ROLE_ID.eq(ROLE.ID))
// .leftJoin(ROLE_PERMISSION_MAP)
// .on(ROLE.ID.eq(ROLE_PERMISSION_MAP.ROLE_ID))
// .leftJoin(PERMISSION)
// .on(ROLE_PERMISSION_MAP.PERMISSION_ID.eq(PERMISSION.ID))
// .where(USER.ID.eq(roleId))
// .fetch();
// }
@Transactional
public void deleteByUsername(String username) {
ctx().delete(USER).where(USER.USERNAME.eq(username)).execute();
}
}

View File

@@ -0,0 +1,33 @@
package com.zl.mjga.repository;
import static org.jooq.generated.mjga.tables.UserRoleMap.USER_ROLE_MAP;
import java.util.List;
import org.jooq.Configuration;
import org.jooq.generated.mjga.tables.daos.UserRoleMapDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
@Repository
public class UserRoleMapRepository extends UserRoleMapDao {
@Autowired
public UserRoleMapRepository(Configuration configuration) {
super(configuration);
}
@Transactional
public void deleteByUserId(Long userId) {
ctx().deleteFrom(USER_ROLE_MAP).where(USER_ROLE_MAP.USER_ID.eq(userId)).execute();
}
@Transactional
public void deleteBy(Long userId, List<Long> roleIdList) {
ctx()
.deleteFrom(USER_ROLE_MAP)
.where(USER_ROLE_MAP.USER_ID.eq(userId))
.and(USER_ROLE_MAP.ROLE_ID.in(roleIdList))
.execute();
}
}

View File

@@ -0,0 +1,28 @@
package com.zl.mjga.service;
import com.zl.mjga.config.cache.CacheConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class CacheService {
@Cacheable(value = CacheConfig.VERIFY_CODE, key = "{#identify}", unless = "#result == null")
public String getVerifyCodeBy(String identify) {
return null;
}
@CachePut(value = CacheConfig.VERIFY_CODE, key = "{#identify}")
public String upsertVerifyCodeBy(String identify, String value) {
return value;
}
@CacheEvict(value = CacheConfig.VERIFY_CODE, key = "{#identify}")
public void removeVerifyCodeBy(String identify) {}
@CacheEvict(value = CacheConfig.VERIFY_CODE, allEntries = true)
public void clearAllVerifyCode() {}
}

View File

@@ -0,0 +1,47 @@
package com.zl.mjga.service;
import static org.jooq.generated.mjga.tables.Department.DEPARTMENT;
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 java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jooq.Record;
import org.jooq.Result;
import org.springframework.stereotype.Service;
@Service
@Slf4j
@RequiredArgsConstructor
public class DepartmentService {
private final DepartmentRepository departmentRepository;
public PageResponseDto<List<DepartmentRespDto>> pageQueryDepartment(
PageRequestDto pageRequestDto, DepartmentQueryDto departmentQueryDto) {
Result<Record> records = departmentRepository.pageFetchBy(pageRequestDto, departmentQueryDto);
if (records.isEmpty()) {
return PageResponseDto.empty();
}
List<DepartmentRespDto> departments =
records.map(
record -> {
return DepartmentRespDto.builder()
.id(record.getValue(DEPARTMENT.ID))
.name(record.getValue(DEPARTMENT.NAME))
.parentId(record.getValue(DEPARTMENT.PARENT_ID))
.isBound(
record.field("is_bound") != null
? record.get("is_bound", Boolean.class)
: null)
.parentName(record.get("parent_name", String.class))
.build();
});
Long totalDepartment = records.get(0).getValue("total_department", Long.class);
return new PageResponseDto<>(totalDepartment, departments);
}
}

View File

@@ -0,0 +1,291 @@
package com.zl.mjga.service;
import static org.jooq.generated.mjga.tables.Permission.PERMISSION;
import static org.jooq.generated.mjga.tables.Role.ROLE;
import static org.jooq.generated.mjga.tables.User.USER;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.PageResponseDto;
import com.zl.mjga.dto.department.DepartmentBindDto;
import com.zl.mjga.dto.position.PositionBindDto;
import com.zl.mjga.dto.urp.*;
import com.zl.mjga.exception.BusinessException;
import com.zl.mjga.model.urp.ERole;
import com.zl.mjga.repository.*;
import java.util.*;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.jooq.Record;
import org.jooq.Result;
import org.jooq.generated.mjga.tables.pojos.*;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.BeanUtils;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
@RequiredArgsConstructor
public class IdentityAccessService {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final UserRoleMapRepository userRoleMapRepository;
private final PermissionRepository permissionRepository;
private final RolePermissionMapRepository rolePermissionMapRepository;
private final UserDepartmentMapRepository userDepartmentMapRepository;
private final UserPositionMapRepository userPositionMapRepository;
private final PasswordEncoder passwordEncoder;
public void upsertUser(UserUpsertDto userUpsertDto) {
User user = new User();
BeanUtils.copyProperties(userUpsertDto, user);
if (StringUtils.isNotEmpty(userUpsertDto.getPassword())) {
user.setPassword(passwordEncoder.encode(userUpsertDto.getPassword()));
}
userRepository.mergeWithoutNullFieldBy(user);
}
public void upsertRole(RoleUpsertDto roleUpsertDto) {
Role role = new Role();
BeanUtils.copyProperties(roleUpsertDto, role);
roleRepository.merge(role);
}
public void upsertPermission(PermissionUpsertDto permissionUpsertDto) {
Permission permission = new Permission();
BeanUtils.copyProperties(permissionUpsertDto, permission);
permissionRepository.merge(permission);
}
public PageResponseDto<List<UserRolePermissionDto>> pageQueryUser(
PageRequestDto pageRequestDto, UserQueryDto userQueryDto) {
Result<Record> userRecords = userRepository.pageFetchBy(pageRequestDto, userQueryDto);
if (userRecords.isEmpty()) {
return PageResponseDto.empty();
}
List<UserRolePermissionDto> userRolePermissionDtoList =
userRecords.stream()
.map((record) -> queryUniqueUserWithRolePermission(record.getValue(USER.ID)))
.toList();
return new PageResponseDto<>(
userRecords.get(0).getValue("total_user", Integer.class), userRolePermissionDtoList);
}
public @Nullable UserRolePermissionDto queryUniqueUserWithRolePermission(Long userId) {
return userRepository.fetchUniqueUserDtoWithNestedRolePermissionBy(userId);
}
public PageResponseDto<List<RoleDto>> pageQueryRole(
PageRequestDto pageRequestDto, RoleQueryDto roleQueryDto) {
Result<Record> roleRecords = roleRepository.pageFetchBy(pageRequestDto, roleQueryDto);
if (roleRecords.isEmpty()) {
return PageResponseDto.empty();
}
List<RoleDto> roleDtoList =
roleRecords.stream()
.map(
record -> {
return RoleDto.builder()
.id(record.getValue("id", Long.class))
.code(record.getValue("code", String.class))
.name(record.getValue("name", String.class))
.isBound(
record.field("is_bound", Boolean.class) != null
? record.getValue("is_bound", Boolean.class)
: null)
.permissions(record.getValue("permissions", List.class))
.build();
})
.toList();
return new PageResponseDto<>(
roleRecords.get(0).getValue("total_role", Integer.class), roleDtoList);
}
public @Nullable RoleDto queryUniqueRoleWithPermission(Long roleId) {
Result<Record> roleWithPermissionRecords = roleRepository.fetchUniqueRoleWithPermission(roleId);
if (roleWithPermissionRecords.isEmpty()) {
return null;
}
RoleDto roleDto = createRbacDtoRolePart(roleWithPermissionRecords);
setCurrentRolePermission(roleDto, roleWithPermissionRecords);
return roleDto;
}
public PageResponseDto<List<PermissionRespDto>> pageQueryPermission(
PageRequestDto pageRequestDto, PermissionQueryDto permissionQueryDto) {
Result<Record> permissionRecords =
permissionRepository.pageFetchBy(pageRequestDto, permissionQueryDto);
if (permissionRecords.isEmpty()) {
return PageResponseDto.empty();
}
List<PermissionRespDto> permissionRespDtoList =
permissionRecords.stream()
.map(
record ->
PermissionRespDto.builder()
.id(record.getValue("id", Long.class))
.name(record.getValue("name", String.class))
.code(record.getValue("code", String.class))
.isBound(
record.field("is_bound", Boolean.class) != null
? record.getValue("is_bound", Boolean.class)
: null)
.build())
.toList();
return new PageResponseDto<>(
permissionRecords.get(0).getValue("total_permission", Integer.class),
permissionRespDtoList);
}
public void bindPermissionBy(Long roleId, List<Long> permissionIdList) {
List<RolePermissionMap> permissionMapList =
permissionIdList.stream()
.map(
(permissionId -> {
RolePermissionMap rolePermissionMap = new RolePermissionMap();
rolePermissionMap.setRoleId(roleId);
rolePermissionMap.setPermissionId(permissionId);
return rolePermissionMap;
}))
.collect(Collectors.toList());
rolePermissionMapRepository.merge(permissionMapList);
}
public void unBindPermissionBy(Long roleId, List<Long> permissionIdList) {
if (CollectionUtils.isEmpty(permissionIdList)) {
return;
}
rolePermissionMapRepository.deleteBy(roleId, permissionIdList);
}
public void unBindRoleToUser(Long userId, List<Long> roleIdList) {
if (CollectionUtils.isEmpty(roleIdList)) {
return;
}
List<Role> roles = roleRepository.selectByRoleIdIn(roleIdList);
if (CollectionUtils.isEmpty(roles)) {
throw new BusinessException("unbind role not exist");
}
userRoleMapRepository.deleteBy(userId, roleIdList);
}
public void bindRoleToUser(Long userId, List<Long> roleIdList) {
List<UserRoleMap> userRoleMapList =
roleIdList.stream()
.map(
(roleId -> {
UserRoleMap userRoleMap = new UserRoleMap();
userRoleMap.setUserId(userId);
userRoleMap.setRoleId(roleId);
return userRoleMap;
}))
.collect(Collectors.toList());
userRoleMapRepository.merge(userRoleMapList);
}
@Transactional(rollbackFor = Throwable.class)
public void bindRoleModuleToUser(Long userId, List<ERole> eRoleList) {
bindRoleToUser(
userId,
roleRepository
.selectByRoleCodeIn(eRoleList.stream().map(Enum::name).collect(Collectors.toList()))
.stream()
.map(Role::getId)
.toList());
}
private void setCurrentRolePermission(RoleDto roleDto, List<Record> roleResult) {
if (roleResult.get(0).getValue(PERMISSION.ID) != null) {
roleResult.forEach(
(record) -> {
PermissionRespDto permissionRespDto = createRbacDtoPermissionPart(record);
roleDto.getPermissions().add(permissionRespDto);
});
}
}
private PermissionRespDto createRbacDtoPermissionPart(Record record) {
PermissionRespDto permissionRespDto = new PermissionRespDto();
permissionRespDto.setId(record.getValue(PERMISSION.ID));
permissionRespDto.setCode(record.getValue(PERMISSION.CODE));
permissionRespDto.setName(record.getValue(PERMISSION.NAME));
return permissionRespDto;
}
private RoleDto createRbacDtoRolePart(List<Record> roleResult) {
RoleDto roleDto = new RoleDto();
roleDto.setId(roleResult.get(0).getValue(ROLE.ID));
roleDto.setCode(roleResult.get(0).getValue(ROLE.CODE));
roleDto.setName(roleResult.get(0).getValue(ROLE.NAME));
return roleDto;
}
public boolean isRoleDuplicate(String roleCode, String name) {
return roleRepository.fetchOneByCode(roleCode) != null
|| roleRepository.fetchOneByName(name) != null;
}
public boolean isUsernameDuplicate(String username) {
return userRepository.fetchOneByUsername(username) != null;
}
public boolean isPermissionDuplicate(String code, String name) {
return permissionRepository.fetchOneByCode(code) != null
|| permissionRepository.fetchOneByName(name) != null;
}
@Transactional(rollbackFor = Throwable.class)
public void bindDepartmentBy(DepartmentBindDto departmentBindDto) {
List<UserDepartmentMap> userDepartmentMaps =
departmentBindDto.departmentIds().stream()
.map(
(departmentId) -> {
UserDepartmentMap userDepartmentMap = new UserDepartmentMap();
userDepartmentMap.setUserId(departmentBindDto.userId());
userDepartmentMap.setDepartmentId(departmentId);
return userDepartmentMap;
})
.toList();
userDepartmentMapRepository.merge(userDepartmentMaps);
}
@Transactional(rollbackFor = Throwable.class)
public void unBindDepartmentBy(DepartmentBindDto departmentBindDto) {
for (Long departmentId : departmentBindDto.departmentIds()) {
UserDepartmentMap userDepartmentMap = new UserDepartmentMap();
userDepartmentMap.setUserId(departmentBindDto.userId());
userDepartmentMap.setDepartmentId(departmentId);
userDepartmentMapRepository.delete(userDepartmentMap);
}
}
@Transactional(rollbackFor = Throwable.class)
public void bindPositionBy(PositionBindDto positionBindDto) {
List<UserPositionMap> userPositionMaps =
positionBindDto.positionIds().stream()
.map(
(positionId) -> {
UserPositionMap userPositionMap = new UserPositionMap();
userPositionMap.setUserId(positionBindDto.userId());
userPositionMap.setPositionId(positionId);
return userPositionMap;
})
.toList();
userPositionMapRepository.merge(userPositionMaps);
}
@Transactional(rollbackFor = Throwable.class)
public void unBindPositionBy(PositionBindDto positionBindDto) {
for (Long positionId : positionBindDto.positionIds()) {
UserPositionMap userPositionMap = new UserPositionMap();
userPositionMap.setUserId(positionBindDto.userId());
userPositionMap.setPositionId(positionId);
userPositionMapRepository.delete(userPositionMap);
}
}
}

View File

@@ -0,0 +1,44 @@
package com.zl.mjga.service;
import static org.jooq.generated.mjga.tables.Position.POSITION;
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 java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jooq.Record;
import org.jooq.Result;
import org.springframework.stereotype.Service;
@Service
@Slf4j
@RequiredArgsConstructor
public class PositionService {
private final PositionRepository positionRepository;
public PageResponseDto<List<PositionRespDto>> pageQueryPosition(
PageRequestDto pageRequestDto, PositionQueryDto positionQueryDto) {
Result<Record> records = positionRepository.pageFetchBy(pageRequestDto, positionQueryDto);
if (records.isEmpty()) {
return PageResponseDto.empty();
}
List<PositionRespDto> positions =
records.map(
record ->
PositionRespDto.builder()
.id(record.getValue(POSITION.ID))
.name(record.getValue(POSITION.NAME))
.isBound(
record.field("is_bound", Boolean.class) != null
? record.getValue("is_bound", Boolean.class)
: null)
.build());
Long totalPosition = records.get(0).getValue("total_position", Long.class);
return new PageResponseDto<>(totalPosition, positions);
}
}

View File

@@ -0,0 +1,103 @@
package com.zl.mjga.service;
import static org.jooq.generated.public_.Tables.*;
import static org.quartz.TriggerBuilder.newTrigger;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.PageResponseDto;
import com.zl.mjga.dto.scheduler.JobTriggerDto;
import com.zl.mjga.dto.scheduler.QueryDto;
import com.zl.mjga.repository.QrtzJobRepository;
import jakarta.annotation.Resource;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jooq.Record;
import org.jooq.Result;
import org.quartz.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
@Service
@Slf4j
@RequiredArgsConstructor
public class SchedulerService {
@Resource(name = "emailJobSchedulerFactory")
private Scheduler emailJobScheduler;
@Resource(name = "dataBackupSchedulerFactory")
private Scheduler dataBackupScheduler;
private final QrtzJobRepository qrtzJobRepository;
public PageResponseDto<List<JobTriggerDto>> getJobWithTriggerBy(
PageRequestDto pageRequestDto, QueryDto queryDto) {
Result<Record> records =
qrtzJobRepository.fetchPageWithJobAndTriggerBy(pageRequestDto, queryDto);
if (records.isEmpty()) {
return PageResponseDto.empty();
}
List<JobTriggerDto> jobTriggerDtoList =
records.map(
record -> {
JobTriggerDto jobTriggerDto = new JobTriggerDto();
jobTriggerDto.setName(record.getValue(QRTZ_JOB_DETAILS.JOB_NAME));
jobTriggerDto.setGroup(record.getValue(QRTZ_JOB_DETAILS.JOB_GROUP));
jobTriggerDto.setClassName(record.getValue(QRTZ_JOB_DETAILS.JOB_CLASS_NAME));
jobTriggerDto.setTriggerName(record.getValue(QRTZ_TRIGGERS.TRIGGER_NAME));
jobTriggerDto.setTriggerGroup(record.getValue(QRTZ_TRIGGERS.TRIGGER_GROUP));
jobTriggerDto.setCronExpression(record.getValue(QRTZ_CRON_TRIGGERS.CRON_EXPRESSION));
jobTriggerDto.setStartTime(record.getValue(QRTZ_TRIGGERS.START_TIME));
jobTriggerDto.setEndTime(record.getValue(QRTZ_TRIGGERS.END_TIME));
jobTriggerDto.setNextFireTime(record.getValue(QRTZ_TRIGGERS.NEXT_FIRE_TIME));
jobTriggerDto.setPreviousFireTime(record.getValue(QRTZ_TRIGGERS.PREV_FIRE_TIME));
jobTriggerDto.setSchedulerType(record.getValue(QRTZ_TRIGGERS.TRIGGER_TYPE));
jobTriggerDto.setTriggerState(record.getValue(QRTZ_TRIGGERS.TRIGGER_STATE));
return jobTriggerDto;
});
return new PageResponseDto<>(
records.get(0).getValue("total_job", Integer.class), jobTriggerDtoList);
}
public void resumeTrigger(TriggerKey triggerKey) throws SchedulerException {
emailJobScheduler.resumeTrigger(triggerKey);
dataBackupScheduler.resumeTrigger(triggerKey);
}
public void pauseTrigger(TriggerKey triggerKey) throws SchedulerException {
emailJobScheduler.pauseTrigger(triggerKey);
dataBackupScheduler.pauseTrigger(triggerKey);
}
public void triggerJob(JobKey jobKey, Date startAt) throws SchedulerException {
JobDetail jobDetail = emailJobScheduler.getJobDetail(jobKey);
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Trigger dayLaterTrigger =
newTrigger()
.withIdentity(
String.format(
"%s-%s-%s", "trigger", authentication.getName(), Instant.now().toEpochMilli()),
"job-management")
.startAt(startAt)
.build();
emailJobScheduler.scheduleJob(jobDetail, dayLaterTrigger);
}
public void updateCronTrigger(TriggerKey triggerKey, String cron) throws SchedulerException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Trigger newTrigger =
TriggerBuilder.newTrigger()
.withIdentity(
String.format(
"%s-%s-%s",
"cronTrigger", authentication.getName(), Instant.now().toEpochMilli()),
"job-management")
.withSchedule(CronScheduleBuilder.cronSchedule(cron))
.build();
dataBackupScheduler.rescheduleJob(triggerKey, newTrigger);
}
}

View File

@@ -0,0 +1,51 @@
package com.zl.mjga.service;
import com.zl.mjga.dto.sign.SignInDto;
import com.zl.mjga.dto.sign.SignUpDto;
import com.zl.mjga.exception.BusinessException;
import com.zl.mjga.model.urp.ERole;
import com.zl.mjga.repository.UserRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jooq.generated.mjga.tables.pojos.User;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
@RequiredArgsConstructor
public class SignService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final IdentityAccessService identityAccessService;
public Long signIn(SignInDto signInDto) {
User user = userRepository.fetchOneByUsername(signInDto.getUsername());
if (user == null) {
throw new BusinessException(String.format("%s user not found", signInDto.getUsername()));
}
if (!passwordEncoder.matches(signInDto.getPassword(), user.getPassword())) {
throw new BusinessException("password invalid");
}
return user.getId();
}
@Transactional(rollbackFor = Throwable.class)
public void signUp(SignUpDto signUpDto) {
if (identityAccessService.isUsernameDuplicate(signUpDto.getUsername())) {
throw new BusinessException(
String.format("username %s already exist", signUpDto.getUsername()));
}
User user = new User();
user.setUsername(signUpDto.getUsername());
user.setPassword(passwordEncoder.encode(signUpDto.getPassword()));
userRepository.insert(user);
User insertUser = userRepository.fetchOneByUsername(signUpDto.getUsername());
identityAccessService.bindRoleModuleToUser(insertUser.getId(), List.of(ERole.GENERAL));
}
}

View File

@@ -0,0 +1,31 @@
server:
port: 8080
logging:
file:
path: /var/log
level:
org:
springframework:
security: debug
flywaydb: debug
jooq: debug
cors:
allowedOrigins: ${ALLOWED_ORIGINS}
allowedMethods: ${ALLOWED_METHODS}
allowedHeaders: ${ALLOWED_HEADERS}
allowedExposeHeaders: ${ALLOWED_EXPOSE_HEADERS}
spring:
datasource:
url: jdbc:postgresql://${DATABASE_HOST_PORT}/${DATABASE_DB}
username: ${DATABASE_USER}
password: ${DATABASE_PASSWORD}
flyway:
enabled: true
locations: classpath:db/migration
default-schema: ${DATABASE_DEFAULT_SCHEMA}
springdoc:
swagger-ui:
path: /swagger-ui.html
jwt:
secret: ${JWT_SECRET:secret}
expiration-min: ${JWT_EXPIRATION_MIN:100}

View File

@@ -0,0 +1,67 @@
CREATE SCHEMA IF NOT EXISTS mjga;
CREATE TABLE mjga.user (
id BIGSERIAL PRIMARY KEY,
username VARCHAR NOT NULL UNIQUE,
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
password VARCHAR NOT NULL,
enable BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE mjga.permission (
id BIGSERIAL PRIMARY KEY,
code VARCHAR NOT NULL UNIQUE,
name VARCHAR NOT NULL UNIQUE
);
CREATE TABLE mjga.role (
id BIGSERIAL PRIMARY KEY,
code VARCHAR NOT NULL UNIQUE,
name VARCHAR NOT NULL UNIQUE
);
CREATE TABLE mjga.role_permission_map (
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES mjga.role(id) ON DELETE RESTRICT,
FOREIGN KEY (permission_id) REFERENCES mjga.permission(id) ON DELETE RESTRICT
);
CREATE TABLE mjga.user_role_map (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES mjga.user(id) ON DELETE RESTRICT,
FOREIGN KEY (role_id) REFERENCES mjga.role(id) ON DELETE RESTRICT
);
CREATE TABLE mjga.department (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
parent_id BIGINT,
FOREIGN KEY (parent_id)
REFERENCES mjga.department(id)
ON DELETE RESTRICT
);
CREATE TABLE mjga.user_department_map (
user_id BIGINT NOT NULL,
department_id BIGINT NOT NULL,
PRIMARY KEY (user_id, department_id),
FOREIGN KEY (user_id) REFERENCES mjga.user(id) ON UPDATE NO ACTION ON DELETE RESTRICT,
FOREIGN KEY (department_id) REFERENCES mjga.department(id) ON UPDATE NO ACTION ON DELETE RESTRICT
);
CREATE TABLE mjga.position (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE
);
CREATE TABLE mjga.user_position_map (
user_id BIGINT NOT NULL,
position_id BIGINT NOT NULL,
PRIMARY KEY (user_id, position_id),
FOREIGN KEY (user_id) REFERENCES mjga.user(id) ON UPDATE NO ACTION ON DELETE RESTRICT,
FOREIGN KEY (position_id) REFERENCES mjga.position(id) ON UPDATE NO ACTION ON DELETE RESTRICT
);

View File

@@ -0,0 +1,29 @@
INSERT INTO mjga.user (username, password)
VALUES ('admin', '$2a$10$7zfEdqQYJrBnmDdu7UkgS.zOAsJf4bB1ZYrVhCBAIvIoPbEmeVnVe');
INSERT INTO mjga.role (code, name)
VALUES ('ADMIN', 'ADMIN'),
('GENERAL', 'GENERAL');
INSERT INTO mjga.permission (code, name)
VALUES ('READ_POSITION_PERMISSION', 'READ_POSITION_PERMISSION'),
('WRITE_POSITION_PERMISSION', 'WRITE_POSITION_PERMISSION'),
('READ_DEPARTMENT_PERMISSION', 'READ_DEPARTMENT_PERMISSION'),
('WRITE_DEPARTMENT_PERMISSION', 'WRITE_DEPARTMENT_PERMISSION'),
('READ_SCHEDULER_PERMISSION', 'READ_SCHEDULER_PERMISSION'),
('WRITE_SCHEDULER_PERMISSION', 'WRITE_SCHEDULER_PERMISSION'),
('WRITE_USER_ROLE_PERMISSION', 'WRITE_USER_ROLE_PERMISSION'),
('READ_USER_ROLE_PERMISSION', 'READ_USER_ROLE_PERMISSION');
INSERT INTO mjga.user_role_map (user_id, role_id)
VALUES (1, 1);
INSERT INTO mjga.role_permission_map (role_id, permission_id)
VALUES (1, 1),
(1, 2),
(1, 3),
(1, 4),
(1, 5),
(1, 6),
(1, 7),
(1, 8);

View File

@@ -0,0 +1,194 @@
CREATE SCHEMA IF NOT EXISTS public;
SET search_path TO public;
CREATE TABLE QRTZ_JOB_DETAILS
(
SCHED_NAME VARCHAR(120) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
JOB_CLASS_NAME VARCHAR(250) NOT NULL,
IS_DURABLE BOOL NOT NULL,
IS_NONCONCURRENT BOOL NOT NULL,
IS_UPDATE_DATA BOOL NOT NULL,
REQUESTS_RECOVERY BOOL NOT NULL,
JOB_DATA BYTEA NULL,
PRIMARY KEY (SCHED_NAME, JOB_NAME, JOB_GROUP)
);
CREATE TABLE QRTZ_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
NEXT_FIRE_TIME BIGINT NULL,
PREV_FIRE_TIME BIGINT NULL,
PRIORITY INTEGER NULL,
TRIGGER_STATE VARCHAR(16) NOT NULL,
TRIGGER_TYPE VARCHAR(8) NOT NULL,
START_TIME BIGINT NOT NULL,
END_TIME BIGINT NULL,
CALENDAR_NAME VARCHAR(200) NULL,
MISFIRE_INSTR SMALLINT NULL,
JOB_DATA BYTEA NULL,
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME, JOB_NAME, JOB_GROUP)
REFERENCES QRTZ_JOB_DETAILS (SCHED_NAME, JOB_NAME, JOB_GROUP)
);
CREATE TABLE QRTZ_SIMPLE_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
REPEAT_COUNT BIGINT NOT NULL,
REPEAT_INTERVAL BIGINT NOT NULL,
TIMES_TRIGGERED BIGINT NOT NULL,
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
);
CREATE TABLE QRTZ_CRON_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
CRON_EXPRESSION VARCHAR(120) NOT NULL,
TIME_ZONE_ID VARCHAR(80),
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
);
CREATE TABLE QRTZ_SIMPROP_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
STR_PROP_1 VARCHAR(512) NULL,
STR_PROP_2 VARCHAR(512) NULL,
STR_PROP_3 VARCHAR(512) NULL,
INT_PROP_1 INT NULL,
INT_PROP_2 INT NULL,
LONG_PROP_1 BIGINT NULL,
LONG_PROP_2 BIGINT NULL,
DEC_PROP_1 NUMERIC(13, 4) NULL,
DEC_PROP_2 NUMERIC(13, 4) NULL,
BOOL_PROP_1 BOOL NULL,
BOOL_PROP_2 BOOL NULL,
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
);
CREATE TABLE QRTZ_BLOB_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
BLOB_DATA BYTEA NULL,
PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
);
CREATE TABLE QRTZ_CALENDARS
(
SCHED_NAME VARCHAR(120) NOT NULL,
CALENDAR_NAME VARCHAR(200) NOT NULL,
CALENDAR BYTEA NOT NULL,
PRIMARY KEY (SCHED_NAME, CALENDAR_NAME)
);
CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
PRIMARY KEY (SCHED_NAME, TRIGGER_GROUP)
);
CREATE TABLE QRTZ_FIRED_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
ENTRY_ID VARCHAR(95) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
INSTANCE_NAME VARCHAR(200) NOT NULL,
FIRED_TIME BIGINT NOT NULL,
SCHED_TIME BIGINT NOT NULL,
PRIORITY INTEGER NOT NULL,
STATE VARCHAR(16) NOT NULL,
JOB_NAME VARCHAR(200) NULL,
JOB_GROUP VARCHAR(200) NULL,
IS_NONCONCURRENT BOOL NULL,
REQUESTS_RECOVERY BOOL NULL,
PRIMARY KEY (SCHED_NAME, ENTRY_ID)
);
CREATE TABLE QRTZ_SCHEDULER_STATE
(
SCHED_NAME VARCHAR(120) NOT NULL,
INSTANCE_NAME VARCHAR(200) NOT NULL,
LAST_CHECKIN_TIME BIGINT NOT NULL,
CHECKIN_INTERVAL BIGINT NOT NULL,
PRIMARY KEY (SCHED_NAME, INSTANCE_NAME)
);
CREATE TABLE QRTZ_LOCKS
(
SCHED_NAME VARCHAR(120) NOT NULL,
LOCK_NAME VARCHAR(40) NOT NULL,
PRIMARY KEY (SCHED_NAME, LOCK_NAME)
);
CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY
ON QRTZ_JOB_DETAILS (SCHED_NAME, REQUESTS_RECOVERY);
CREATE INDEX IDX_QRTZ_J_GRP
ON QRTZ_JOB_DETAILS (SCHED_NAME, JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_J
ON QRTZ_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_JG
ON QRTZ_TRIGGERS (SCHED_NAME, JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_C
ON QRTZ_TRIGGERS (SCHED_NAME, CALENDAR_NAME);
CREATE INDEX IDX_QRTZ_T_G
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP);
CREATE INDEX IDX_QRTZ_T_STATE
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_STATE
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_G_STATE
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME
ON QRTZ_TRIGGERS (SCHED_NAME, NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE, NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_GROUP, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME);
CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME, REQUESTS_RECOVERY);
CREATE INDEX IDX_QRTZ_FT_J_G
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP);
CREATE INDEX IDX_QRTZ_FT_JG
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_GROUP);
CREATE INDEX IDX_QRTZ_FT_T_G
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP);
CREATE INDEX IDX_QRTZ_FT_TG
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_GROUP);
COMMIT;

View File

@@ -0,0 +1,51 @@
package com.zl.mjga.integration.cache;
import static org.assertj.core.api.Assertions.assertThat;
import com.zl.mjga.config.cache.CacheConfig;
import com.zl.mjga.service.CacheService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
@SpringJUnitConfig(classes = {CacheConfig.class, CacheService.class})
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
public class CacheTest {
@Autowired private CacheService cacheService;
@Test
void
getVerifyCodeBy_upsertVerifyCodeBy_whenSetCacheValue_subsequentGetCacheShouldReturnUpdatedValue() {
cacheService.upsertVerifyCodeBy("WsxOtE0d6Vc1glZ", "ej1x8T4XiluV8D216");
String verifyCode = cacheService.getVerifyCodeBy("WsxOtE0d6Vc1glZ");
assertThat(verifyCode).isEqualTo("ej1x8T4XiluV8D216");
}
@Test
void removeVerifyCodeBy_whenRemoveCacheValue_subsequentGetCacheShouldReturnNull() {
cacheService.upsertVerifyCodeBy("WsxOtE0d6Vc1glZ", "ej1x8T4XiluV8D216");
String verifyCode = cacheService.getVerifyCodeBy("WsxOtE0d6Vc1glZ");
cacheService.removeVerifyCodeBy("WsxOtE0d6Vc1glZ");
String verifyCode2 = cacheService.getVerifyCodeBy("WsxOtE0d6Vc1glZ");
assertThat(verifyCode).isEqualTo("ej1x8T4XiluV8D216");
assertThat(verifyCode2).isNull();
}
@Test
void clearAllVerifyCode_whenCleanCache_subsequentGetCacheShouldReturnNewValue() {
cacheService.upsertVerifyCodeBy("WsxOtE0d6Vc1glZ", "ej1x8T4XiluV8D216");
cacheService.upsertVerifyCodeBy("hNYcK0MDjX4197", "Ll1v93jiXwHLji");
String verifyCode1 = cacheService.getVerifyCodeBy("WsxOtE0d6Vc1glZ");
String verifyCode2 = cacheService.getVerifyCodeBy("hNYcK0MDjX4197");
cacheService.clearAllVerifyCode();
String verifyCode3 = cacheService.getVerifyCodeBy("WsxOtE0d6Vc1glZ");
String verifyCode4 = cacheService.getVerifyCodeBy("hNYcK0MDjX4197");
assertThat(verifyCode1).isEqualTo("ej1x8T4XiluV8D216");
assertThat(verifyCode2).isEqualTo("Ll1v93jiXwHLji");
assertThat(verifyCode3).isNull();
assertThat(verifyCode4).isNull();
}
}

View File

@@ -0,0 +1,121 @@
package com.zl.mjga.integration.e2e;
import com.zl.mjga.repository.UserRepository;
import com.zl.mjga.repository.UserRoleMapRepository;
import java.time.Duration;
import org.jooq.generated.mjga.tables.pojos.User;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.web.reactive.server.WebTestClient;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
@Disabled
public class SignE2ETest {
@Value("${jwt.cookie-name}")
private String jwtCookieName;
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration-min}")
private int expirationMin;
@Autowired private WebTestClient webTestClient;
@Autowired private UserRepository userRepository;
@Autowired private UserRoleMapRepository userRoleMapRepository;
@Autowired private PasswordEncoder passwordEncoder;
@Autowired private TestRestTemplate testRestTemplate;
@AfterEach
void cleanUp() {
User user = userRepository.fetchOneByUsername("test_5fab32c22a3e");
userRoleMapRepository.deleteByUserId(user.getId());
userRepository.deleteByUsername("test_5fab32c22a3e");
}
@Test
void signUp() {
webTestClient
.post()
.uri("/auth/sign-up")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(
"""
{
"username": "test_5fab32c22a3e",
"password": "test_eab28b939ba1"
}
""")
.exchange()
.expectStatus()
.isCreated();
}
@Test
void signIn() {
User stubUser = new User();
stubUser.setUsername("test_5fab32c22a3e");
stubUser.setPassword(passwordEncoder.encode("test_eab28b939ba1"));
userRepository.insert(stubUser);
webTestClient
.post()
.uri("/auth/sign-in")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(
"""
{
"username": "test_5fab32c22a3e",
"password": "test_eab28b939ba1"
}
""")
.exchange()
.expectCookie()
.exists(jwtCookieName)
.expectCookie()
.maxAge(jwtCookieName, Duration.ofSeconds(expirationMin * 60L))
.expectStatus()
.isOk();
}
@Test
void signOut() {
User stubUser = new User();
stubUser.setUsername("test_5fab32c22a3e");
stubUser.setPassword(passwordEncoder.encode("test_eab28b939ba1"));
userRepository.insert(stubUser);
User loginUser = new User();
loginUser.setUsername("test_5fab32c22a3e");
loginUser.setPassword("test_eab28b939ba1");
HttpHeaders headers =
testRestTemplate.postForEntity("/auth/sign-in", loginUser, String.class).getHeaders();
headers
.get("Set-Cookie")
.forEach(
cookie -> {
if (cookie.startsWith(jwtCookieName)) {
webTestClient
.post()
.uri("/auth/sign-out")
.header("Cookie", cookie)
.exchange()
.expectCookie()
.maxAge(jwtCookieName, Duration.ofSeconds(0L))
.expectStatus()
.isOk();
}
});
}
}

View File

@@ -0,0 +1,78 @@
package com.zl.mjga.integration.mvc;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.zl.mjga.config.security.HttpFireWallConfig;
import com.zl.mjga.controller.IdentityAccessController;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.PageResponseDto;
import com.zl.mjga.dto.urp.UserQueryDto;
import com.zl.mjga.dto.urp.UserRolePermissionDto;
import com.zl.mjga.repository.PermissionRepository;
import com.zl.mjga.repository.RoleRepository;
import com.zl.mjga.repository.UserRepository;
import com.zl.mjga.service.IdentityAccessService;
import java.time.OffsetDateTime;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(value = {IdentityAccessController.class})
@Import({HttpFireWallConfig.class})
public class JacksonAnnotationMvcTest {
@MockBean private IdentityAccessService identityAccessService;
@Autowired private MockMvc mockMvc;
@MockBean private UserRepository userRepository;
@MockBean private RoleRepository roleRepository;
@MockBean private PermissionRepository permissionRepository;
@Test
@WithMockUser
void fieldWithJsonWriteOnlyAnnotation_whenResponseIncludeField_responseJsonShouldNotExist()
throws Exception {
String stubUsername = "test_04cb017e1fe6";
String stubPassword = "y1hxAC0V0e4B3s8sJ";
UserRolePermissionDto stubUserRolePermissionDto = new UserRolePermissionDto();
stubUserRolePermissionDto.setId(1L);
stubUserRolePermissionDto.setUsername(stubUsername);
stubUserRolePermissionDto.setPassword(stubPassword);
when(identityAccessService.pageQueryUser(
PageRequestDto.of(1, 5), new UserQueryDto(stubUsername)))
.thenReturn(new PageResponseDto<>(1, List.of(stubUserRolePermissionDto)));
mockMvc
.perform(
get(String.format("/iam/users?page=1&size=5&username=%s", stubUsername))
.contentType(MediaType.APPLICATION_FORM_URLENCODED))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data[0].username").value(stubUsername))
.andExpect(jsonPath("$.data[0].password").doesNotExist());
}
@Test
@WithMockUser
void dateFieldWithFormatAnnotation_whenResponseIncludeField_fieldShouldBeExpectDataFormat()
throws Exception {
OffsetDateTime stubCreateDateTime =
OffsetDateTime.of(2023, 12, 2, 1, 1, 1, 0, OffsetDateTime.now().getOffset());
UserRolePermissionDto stubUserRolePermissionDto = new UserRolePermissionDto();
stubUserRolePermissionDto.setCreateTime(stubCreateDateTime);
when(identityAccessService.pageQueryUser(any(PageRequestDto.class), any(UserQueryDto.class)))
.thenReturn(new PageResponseDto<>(1, List.of(stubUserRolePermissionDto)));
mockMvc
.perform(
get(String.format("/iam/users?page=1&size=5&username=%s", "7bF3mcNVTj6P6v2"))
.contentType(MediaType.APPLICATION_FORM_URLENCODED))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data[0].createTime").value("2023-12-02 01:01:01"));
}
}

View File

@@ -0,0 +1,144 @@
package com.zl.mjga.integration.mvc;
import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.zl.mjga.config.security.HttpFireWallConfig;
import com.zl.mjga.config.security.Jwt;
import com.zl.mjga.controller.SignController;
import com.zl.mjga.dto.sign.SignInDto;
import com.zl.mjga.service.SignService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(value = {SignController.class})
@Import({HttpFireWallConfig.class})
class SignMvcTest {
@MockBean private SignService signService;
@MockBean private Jwt jwt;
@Autowired private MockMvc mockMvc;
@Test
@WithMockUser
void signIn_givenValidHttpRequest_shouldSucceedWith200() throws Exception {
String stubUsername = "test_04cb017e1fe6";
String stubPassword = "test_567472858b8c";
SignInDto signInDto = new SignInDto();
signInDto.setUsername(stubUsername);
signInDto.setPassword(stubPassword);
when(signService.signIn(signInDto)).thenReturn(1L);
mockMvc
.perform(
post("/auth/sign-in")
.contentType(MediaType.APPLICATION_JSON)
.content(
"""
{
"username": "test_04cb017e1fe6",
"password": "test_567472858b8c"
}
""")
.with(csrf()))
.andExpect(status().isOk());
}
@Test
@WithMockUser
void signIn_givenInValidHttpRequest_shouldFailedWith400() throws Exception {
String stubUsername = "test_04cb017e1fe6";
String stubPassword = "test_567472858b8c";
SignInDto signInDto = new SignInDto();
signInDto.setUsername(stubUsername);
signInDto.setPassword(stubPassword);
when(signService.signIn(signInDto)).thenReturn(1L);
mockMvc
.perform(
post("/auth/sign-in")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.content(
"""
{
"username": "test_04cb017e1fe6",
"password": "test_567472858b8c"
}
""")
.with(csrf()))
.andExpect(status().isBadRequest());
when(signService.signIn(signInDto)).thenReturn(1L);
mockMvc
.perform(
post("/auth/sign-in")
.contentType(MediaType.APPLICATION_JSON)
.content(
"""
{
"username": "test_04cb017e1fe6"
}
""")
.with(csrf()))
.andExpect(status().isBadRequest());
}
@Test
@WithMockUser
void signUp_givenValidHttpRequest_shouldSucceedWith200() throws Exception {
mockMvc
.perform(
post("/auth/sign-up")
.contentType(MediaType.APPLICATION_JSON)
.content(
"""
{
"username": "test_04cb017e1fe6",
"password": "test_567472858b8c"
}
""")
.with(csrf()))
.andExpect(status().isCreated());
}
@Test
@WithMockUser
void signUp_givenInValidHttpRequest_shouldFailedWith400() throws Exception {
mockMvc
.perform(
post("/auth/sign-up")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.content(
"""
{
"username": "test_04cb017e1fe6",
"password": "test_567472858b8c"
}
""")
.with(csrf()))
.andExpect(status().isBadRequest());
mockMvc
.perform(
post("/auth/sign-up")
.contentType(MediaType.APPLICATION_JSON)
.content(
"""
{
"username": "test_04cb017e1fe6"
}
""")
.with(csrf()))
.andExpect(status().isBadRequest());
}
}

View File

@@ -0,0 +1,225 @@
package com.zl.mjga.integration.mvc;
import static org.mockito.Mockito.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zl.mjga.config.security.HttpFireWallConfig;
import com.zl.mjga.controller.IdentityAccessController;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.PageResponseDto;
import com.zl.mjga.dto.urp.*;
import com.zl.mjga.repository.PermissionRepository;
import com.zl.mjga.repository.RoleRepository;
import com.zl.mjga.repository.UserRepository;
import com.zl.mjga.service.IdentityAccessService;
import java.util.List;
import org.jooq.generated.mjga.tables.pojos.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(value = {IdentityAccessController.class})
@Import({HttpFireWallConfig.class})
class UserRolePermissionMvcTest {
@MockBean private IdentityAccessService identityAccessService;
@Autowired private MockMvc mockMvc;
@MockBean private UserRepository userRepository;
@MockBean private RoleRepository roleRepository;
@MockBean private PermissionRepository permissionRepository;
@Test
@WithMockUser
void currentUser_givenValidHttpRequest_shouldSucceedWith200AndReturnJson() throws Exception {
String stubUsername = "test_04cb017e1fe6";
UserRolePermissionDto stubUserRolePermissionDto = new UserRolePermissionDto();
stubUserRolePermissionDto.setId(1L);
stubUserRolePermissionDto.setUsername(stubUsername);
User stubUser = new User();
stubUser.setId(1L);
when(userRepository.fetchOneByUsername(anyString())).thenReturn(stubUser);
when(identityAccessService.queryUniqueUserWithRolePermission(anyLong()))
.thenReturn(stubUserRolePermissionDto);
mockMvc
.perform(get("/iam/me"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value(stubUsername));
}
@Test
@WithMockUser
void deleteUser_givenValidHttpRequest_shouldSucceedWith200() throws Exception {
Long stubUserId = 1L;
mockMvc
.perform(
delete(String.format("/iam/user?userId=%s", stubUserId))
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.with(csrf()))
.andExpect(status().isOk());
}
@Test
@WithMockUser
void upsertUser_givenValidHttpRequest_shouldSucceedWith200() throws Exception {
UserUpsertDto userUpsertDto = new UserUpsertDto();
userUpsertDto.setUsername("username");
userUpsertDto.setPassword("password");
userUpsertDto.setEnable(true);
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(userUpsertDto);
mockMvc
.perform(
post("/iam/user").contentType(MediaType.APPLICATION_JSON).content(json).with(csrf()))
.andExpect(status().isOk());
}
@Test
@WithMockUser
void upsertRole_givenValidHttpRequest_shouldSucceedWith200() throws Exception {
RoleUpsertDto roleUpsertDto = new RoleUpsertDto();
roleUpsertDto.setCode("roleCode");
roleUpsertDto.setName("name");
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(roleUpsertDto);
mockMvc
.perform(
post("/iam/role").contentType(MediaType.APPLICATION_JSON).content(json).with(csrf()))
.andExpect(status().isOk());
}
@Test
@WithMockUser
void deleteRole_givenValidHttpRequest_shouldSucceedWith200() throws Exception {
Long stubRoleId = 1L;
mockMvc
.perform(
delete(String.format("/iam/role?roleId=%s", stubRoleId))
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.with(csrf()))
.andExpect(status().isOk());
}
@Test
@WithMockUser
void upsertPermission_givenValidHttpRequest_shouldSucceedWith200() throws Exception {
PermissionUpsertDto permissionUpsertDto = new PermissionUpsertDto();
permissionUpsertDto.setCode("roleCode");
permissionUpsertDto.setName("name");
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(permissionUpsertDto);
mockMvc
.perform(
post("/iam/permission")
.contentType(MediaType.APPLICATION_JSON)
.content(json)
.with(csrf()))
.andExpect(status().isOk());
}
@Test
@WithMockUser
void deletePermission_givenValidHttpRequest_shouldSucceedWith200() throws Exception {
Long permissionId = 1L;
mockMvc
.perform(
delete(String.format("/iam/permission?permissionId=%s", permissionId))
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.with(csrf()))
.andExpect(status().isOk());
}
@Test
@WithMockUser
void pageQueryUser_givenValidHttpRequest_shouldSucceedWith200AndReturnJson() throws Exception {
String stubUsername = "test_04cb017e1fe6";
UserRolePermissionDto stubUserRolePermissionDto = new UserRolePermissionDto();
stubUserRolePermissionDto.setId(1L);
stubUserRolePermissionDto.setUsername(stubUsername);
when(identityAccessService.pageQueryUser(
PageRequestDto.of(1, 5), new UserQueryDto(stubUsername)))
.thenReturn(new PageResponseDto<>(1, List.of(stubUserRolePermissionDto)));
mockMvc
.perform(
get(String.format("/iam/users?page=1&size=5&username=%s", stubUsername))
.contentType(MediaType.APPLICATION_FORM_URLENCODED))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data[0].username").value(stubUsername));
}
@Test
@WithMockUser
void pageQueryRole_givenValidHttpRequest_shouldSucceedWith200AndReturnJson() throws Exception {
Long stubUserId = 1L;
Long stubRoleId = 1L;
String stubRoleCode = "UZ1Ej9vx5y8L4";
String stubRoleName = "B90KM9Pw2ZH9P8OAS";
RoleQueryDto stubRoleQueryDto = new RoleQueryDto();
stubRoleQueryDto.setUserId(stubUserId);
stubRoleQueryDto.setRoleId(stubRoleId);
stubRoleQueryDto.setRoleCode(stubRoleCode);
stubRoleQueryDto.setRoleName(stubRoleName);
RoleDto stubRoleDto = new RoleDto();
stubRoleDto.setId(1L);
stubRoleDto.setName(stubRoleName);
stubRoleDto.setCode(stubRoleCode);
stubRoleDto.setPermissions(
List.of(new PermissionRespDto(1L, "9VWU1nmU89zEVH", "9VWU1nmU89zEVH", false)));
when(identityAccessService.pageQueryRole(PageRequestDto.of(1, 5), stubRoleQueryDto))
.thenReturn(new PageResponseDto<>(1, List.of(stubRoleDto)));
mockMvc
.perform(
get(String.format(
"/iam/roles?page=1&size=5&userId=%s&roleId=%s&roleCode=%s&roleName=%s",
stubUserId, stubRoleId, stubRoleCode, stubRoleName))
.contentType(MediaType.APPLICATION_FORM_URLENCODED))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data[0].name").value(stubRoleName));
}
@Test
@WithMockUser
void pageQueryPermission_givenValidHttpRequest_shouldSucceedWith200AndReturnJson()
throws Exception {
Long stubRoleId = 1L;
Long stubPermissionId = 1L;
String stubPermissionCode = "UZ1Ej9vx5y8L4";
String stubPermissionName = "B90KM9Pw2ZH9P8OAS";
PermissionQueryDto stubPermissionQueryDto = new PermissionQueryDto();
stubPermissionQueryDto.setRoleId(stubRoleId);
stubPermissionQueryDto.setPermissionId(stubPermissionId);
stubPermissionQueryDto.setPermissionCode(stubPermissionCode);
stubPermissionQueryDto.setPermissionName(stubPermissionName);
PermissionRespDto stubPermissionRespDto = new PermissionRespDto();
stubPermissionRespDto.setId(stubPermissionId);
stubPermissionRespDto.setName(stubPermissionName);
stubPermissionRespDto.setCode(stubPermissionCode);
when(identityAccessService.pageQueryPermission(PageRequestDto.of(1, 5), stubPermissionQueryDto))
.thenReturn(new PageResponseDto<>(1, List.of(stubPermissionRespDto)));
mockMvc
.perform(
get(String.format(
"/iam/permissions?page=1&size=5&roleId=%s&permissionId=%s&permissionCode=%s&permissionName=%s",
stubRoleId, stubPermissionId, stubPermissionCode, stubPermissionName))
.contentType(MediaType.APPLICATION_FORM_URLENCODED))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data[0].name").value(stubPermissionName));
}
}

View File

@@ -0,0 +1,33 @@
package com.zl.mjga.integration.persistence;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jooq.JooqTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScans;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Testcontainers;
@JooqTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ComponentScans({@ComponentScan("jooq.tables.daos"), @ComponentScan("com.zl.mjga.repository")})
@Testcontainers
public class AbstractDataAccessLayerTest {
public static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:17.3-alpine").withDatabaseName("mjga");
@DynamicPropertySource
static void postgresProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
registry.add("spring.flyway.locations", () -> "classpath:db/migration/test");
registry.add("spring.flyway.default-schema", () -> "public");
}
static {
postgres.start();
}
}

View File

@@ -0,0 +1,160 @@
package com.zl.mjga.integration.persistence;
import static org.assertj.core.api.Assertions.assertThat;
import static org.jooq.generated.mjga.tables.User.USER;
import static org.jooq.impl.DSL.asterisk;
import java.util.List;
import org.jooq.DSLContext;
import org.jooq.Record;
import org.jooq.Result;
import org.jooq.generated.mjga.tables.pojos.User;
import org.jooq.generated.mjga.tables.records.UserRecord;
import org.jooq.impl.DSL;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.jdbc.Sql;
public class JooqTutorialsTest extends AbstractDataAccessLayerTest {
@Autowired private DSLContext dsl;
@Test
@Sql(
statements = {
"INSERT INTO mjga.user (id, username, password) VALUES (1, 'testUserA','5EUX1AIlV09n2o')",
"INSERT INTO mjga.user (id, username, password) VALUES (2, 'testUserB','lbHHwHjTzpOiRHTs')"
})
void queryWithDsl() {
List<User> users = dsl.selectFrom(USER).fetchInto(User.class);
assertThat(users.size()).isEqualTo(2);
assertThat(users.get(0).getUsername()).isEqualTo("testUserA");
assertThat(users.get(1).getUsername()).isEqualTo("testUserB");
}
@Test
void insertWithOrmFeel() {
UserRecord userRecord = dsl.newRecord(USER);
userRecord.setUsername("9hrb5Fv@gmail.com");
userRecord.setPassword("falr2b9nCVY5hS1o");
userRecord.store();
UserRecord fetchedOne = dsl.fetchOne(USER, USER.USERNAME.eq("9hrb5Fv@gmail.com"));
assertThat(fetchedOne.getPassword()).isEqualTo("falr2b9nCVY5hS1o");
}
@Test
void updateWithOrmFeel() {
UserRecord userRecord = dsl.newRecord(USER);
userRecord.setUsername("9hrb5Fv@gmail.com");
userRecord.setPassword("falr2b9nCVY5hS1o");
userRecord.store();
UserRecord fetchedOne = dsl.fetchOne(USER, USER.USERNAME.eq("9hrb5Fv@gmail.com"));
assertThat(fetchedOne.getPassword()).isEqualTo("falr2b9nCVY5hS1o");
userRecord.setPassword("JHMDoQPKuEcgILE6");
userRecord.store();
fetchedOne.refresh();
assertThat(fetchedOne.getPassword()).isEqualTo("JHMDoQPKuEcgILE6");
}
@Test
@Sql(
statements = {
"INSERT INTO mjga.user (id, username, password) VALUES (1, 'testUserA','5EUX1AIlV09n2o')",
"INSERT INTO mjga.user (id, username, password) VALUES (2, 'testUserB','lbHHwHjTzpOiRHTs')"
})
void deleteWithOrmFeel() {
UserRecord userRecord1 = dsl.fetchOne(USER, USER.USERNAME.eq("testUserA"));
assertThat(userRecord1.get(USER.USERNAME)).isEqualTo("testUserA");
userRecord1.delete();
UserRecord userRecord2 = dsl.fetchOne(USER, USER.USERNAME.eq("testUserA"));
assertThat(userRecord2).isNull();
}
@Test
@Sql(
statements = {
"INSERT INTO mjga.user (id, username, password) VALUES (1, 'testUserA','5EUX1AIlV09n2o')",
"INSERT INTO mjga.user (id, username, password) VALUES (2, 'testUserB','lbHHwHjTzpOiRHTs')",
"INSERT INTO mjga.user (id, username, password) VALUES (3, 'testUserC','yF25WscLYmA8')",
"INSERT INTO mjga.user (id, username, password) VALUES (4, 'testUserD','yF25WscLYmA8')",
"INSERT INTO mjga.user (id, username, password) VALUES (5, 'testUserE','x60FelJjyd0B')"
})
void pagingQuery() {
List<User> users =
dsl.select(USER.USERNAME).from(USER).limit(2).offset(1).fetchInto(User.class);
assertThat(users.size()).isEqualTo(2);
assertThat(users.get(0).getUsername()).isEqualTo("testUserB");
assertThat(users.get(1).getUsername()).isEqualTo("testUserC");
}
@Test
@Sql(
statements = {
"INSERT INTO mjga.user (id, username, password) VALUES (1, 'testUserA','5EUX1AIlV09n2o')",
"INSERT INTO mjga.user (id, username, password) VALUES (2, 'testUserB','lbHHwHjTzpOiRHTs')",
"INSERT INTO mjga.user (id, username, password) VALUES (3, 'testUserC','yF25WscLYmA8')",
"INSERT INTO mjga.user (id, username, password) VALUES (4, 'testUserD','yF25WscLYmA8')",
"INSERT INTO mjga.user (id, username, password) VALUES (5, 'testUserE','x60FelJjyd0B')"
})
void pagingSortQuery() {
List<User> users =
dsl.select(USER.USERNAME)
.from(USER)
.orderBy(USER.ID.desc())
.limit(3)
.offset(1)
.fetchInto(User.class);
assertThat(users.size()).isEqualTo(3);
assertThat(users.get(0).getUsername()).isEqualTo("testUserD");
assertThat(users.get(1).getUsername()).isEqualTo("testUserC");
}
@Test
@Sql(
statements = {
"INSERT INTO mjga.user (id, username, password) VALUES (1, 'testUserA','a')",
"INSERT INTO mjga.user (id, username, password) VALUES (2, 'testUserB','b')",
"INSERT INTO mjga.user (id, username, password) VALUES (3, 'testUserC','c')",
"INSERT INTO mjga.user (id, username, password) VALUES (4, 'testUserD','c')",
"INSERT INTO mjga.user (id, username, password) VALUES (5, 'testUserE','c')"
})
void fetchAndTiesQuery() {
List<User> users =
dsl.select(USER.USERNAME)
.from(USER)
.orderBy(USER.PASSWORD.asc())
.limit(3)
.withTies()
.offset(0)
.fetchInto(User.class);
assertThat(users.size()).isEqualTo(5);
assertThat(users.get(0).getUsername()).isEqualTo("testUserA");
assertThat(users.get(4).getUsername()).isEqualTo("testUserE");
}
@Test
@Sql(
statements = {
"INSERT INTO mjga.user (id, username, password) VALUES (1, 'testUserA','a')",
"INSERT INTO mjga.user (id, username, password) VALUES (2, 'testUserB','b')",
"INSERT INTO mjga.user (id, username, password) VALUES (3, 'testUserC','c')",
"INSERT INTO mjga.user (id, username, password) VALUES (4, 'testUserD','e')",
"INSERT INTO mjga.user (id, username, password) VALUES (5, 'testUserE','f')"
})
void windowFunctionQuery() {
Result<Record> resultWithWindow =
dsl.select(asterisk(), DSL.count().over().as("total_user"))
.from(USER)
.orderBy(USER.ID.asc())
.limit(4)
.offset(0)
.fetch();
assertThat(resultWithWindow.size()).isEqualTo(4);
assertThat(resultWithWindow.get(0).getValue("total_user")).isEqualTo(5);
assertThat(resultWithWindow.get(0).getValue(USER.USERNAME)).isEqualTo("testUserA");
assertThat(resultWithWindow.get(1).getValue(USER.USERNAME)).isEqualTo("testUserB");
}
}

View File

@@ -0,0 +1,71 @@
package com.zl.mjga.integration.persistence;
import static org.assertj.core.api.Assertions.assertThat;
import static org.jooq.generated.mjga.tables.User.USER;
import com.zl.mjga.dto.PageRequestDto;
import com.zl.mjga.dto.urp.UserQueryDto;
import com.zl.mjga.repository.*;
import java.util.HashMap;
import org.jooq.Record;
import org.jooq.Result;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.jdbc.Sql;
public class SortByDALTest extends AbstractDataAccessLayerTest {
@Autowired private UserRoleMapRepository userRoleMapRepository;
@Autowired private RolePermissionMapRepository rolePermissionMapRepository;
@Autowired private UserRepository userRepository;
@Autowired private RoleRepository roleRepository;
@Autowired private PermissionRepository permissionRepository;
@Test
@Sql(
statements = {
"INSERT INTO mjga.user (id, username, password) VALUES (1, 'testA','5EUX1AIlV09n2o')",
"INSERT INTO mjga.user (id, username,password) VALUES (2, 'testB','NTjRCeUq2EqCy')",
"INSERT INTO mjga.user (id, username,password) VALUES (3, 'testC','qFVVFvPqs291k10')",
})
void userPageFetchWithNoSort() {
UserQueryDto rbacQueryDto = new UserQueryDto("test");
Result<Record> records = userRepository.pageFetchBy(PageRequestDto.of(1, 10), rbacQueryDto);
assertThat(records.get(0).get(USER.ID)).isEqualTo(1);
assertThat(records.get(1).get(USER.ID)).isEqualTo(2);
assertThat(records.get(2).get(USER.ID)).isEqualTo(3);
}
@Test
@Sql(
statements = {
"INSERT INTO mjga.user (id, username, password) VALUES (1, 'testA','1')",
"INSERT INTO mjga.user (id, username,password) VALUES (2, 'testB','2')",
"INSERT INTO mjga.user (id, username,password) VALUES (3, 'testC','3')",
"INSERT INTO mjga.user (id, username,password) VALUES (4, 'testD','3')",
})
void userPageFetchWithSort() {
UserQueryDto rbacQueryDto = new UserQueryDto("test");
HashMap<String, PageRequestDto.Direction> sortByIdDesc = new HashMap<>();
sortByIdDesc.put("id", PageRequestDto.Direction.DESC);
Result<Record> records =
userRepository.pageFetchBy(PageRequestDto.of(1, 10, sortByIdDesc), rbacQueryDto);
assertThat(records.get(0).get(USER.ID)).isEqualTo(4);
assertThat(records.get(1).get(USER.ID)).isEqualTo(3);
assertThat(records.get(2).get(USER.ID)).isEqualTo(2);
assertThat(records.get(3).get(USER.ID)).isEqualTo(1);
HashMap<String, PageRequestDto.Direction> sortByPasswordAndId = new HashMap<>();
sortByPasswordAndId.put("password", PageRequestDto.Direction.DESC);
sortByIdDesc.put("id", PageRequestDto.Direction.ASC);
Result<Record> records2 =
userRepository.pageFetchBy(PageRequestDto.of(1, 10, sortByPasswordAndId), rbacQueryDto);
assertThat(records2.get(0).get(USER.ID)).isEqualTo(3);
assertThat(records2.get(1).get(USER.ID)).isEqualTo(4);
assertThat(records2.get(2).get(USER.ID)).isEqualTo(2);
assertThat(records2.get(3).get(USER.ID)).isEqualTo(1);
}
}

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