Loki
3 years ago
commit
b1b46f46fa
2389 changed files with 192283 additions and 0 deletions
@ -0,0 +1,30 @@ |
|||||||
|
# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time |
||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven |
||||||
|
|
||||||
|
name: Java CI with Maven |
||||||
|
|
||||||
|
on: |
||||||
|
push: |
||||||
|
branches: [ master ] |
||||||
|
# pull_request: |
||||||
|
# branches: [ master ] |
||||||
|
|
||||||
|
jobs: |
||||||
|
build: |
||||||
|
|
||||||
|
runs-on: ubuntu-latest |
||||||
|
|
||||||
|
strategy: |
||||||
|
matrix: |
||||||
|
java: [ '8', '11', '17' ] |
||||||
|
|
||||||
|
steps: |
||||||
|
- uses: actions/checkout@v2 |
||||||
|
- name: Set up JDK ${{ matrix.Java }} |
||||||
|
uses: actions/setup-java@v2 |
||||||
|
with: |
||||||
|
java-version: ${{ matrix.java }} |
||||||
|
distribution: 'temurin' |
||||||
|
cache: maven |
||||||
|
- name: Build with Maven |
||||||
|
run: mvn -B package --file pom.xml -Dmaven.test.skip=true |
@ -0,0 +1,51 @@ |
|||||||
|
name: yudao-ui-admin CI |
||||||
|
|
||||||
|
# 在master分支发生push事件时触发。 |
||||||
|
on: |
||||||
|
push: |
||||||
|
branches: [ master ] |
||||||
|
# pull_request: |
||||||
|
# branches: [ master ] |
||||||
|
env: # 设置环境变量 |
||||||
|
TZ: Asia/Shanghai # 时区(设置时区可使页面中的`最近更新时间`使用时区时间) |
||||||
|
WORK_DIR: yudao-ui-admin #工作目录 |
||||||
|
|
||||||
|
defaults: |
||||||
|
run: |
||||||
|
shell: bash |
||||||
|
working-directory: yudao-ui-admin |
||||||
|
|
||||||
|
jobs: |
||||||
|
build: # 自定义名称 |
||||||
|
runs-on: ubuntu-latest # 运行在虚拟机环境ubuntu-latest |
||||||
|
|
||||||
|
strategy: |
||||||
|
matrix: |
||||||
|
node-version: [14.x, 16.x] |
||||||
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/ |
||||||
|
|
||||||
|
steps: |
||||||
|
- name: Checkout # 步骤1 |
||||||
|
uses: actions/checkout@v2 # 使用的动作。格式:userName/repoName。作用:检出仓库,获取源码。 官方actions库:https://github.com/actions |
||||||
|
|
||||||
|
- name: Install pnpm |
||||||
|
uses: pnpm/action-setup@v2.0.1 |
||||||
|
with: |
||||||
|
version: 6.15.1 |
||||||
|
|
||||||
|
- name: Set node version to ${{ matrix.node_version }} |
||||||
|
uses: actions/setup-node@v2 |
||||||
|
with: |
||||||
|
node-version: ${{ matrix.node_version }} |
||||||
|
cache: "yarn" |
||||||
|
cache-dependency-path: yudao-ui-admin/yarn.lock |
||||||
|
|
||||||
|
- name: Install deps |
||||||
|
run: node --version && yarn --version && yarn install |
||||||
|
|
||||||
|
- name: Build |
||||||
|
run: yarn build:prod |
||||||
|
|
||||||
|
# 查看 workflow 的文档来获取更多信息 |
||||||
|
# @see https://github.com/crazy-max/ghaction-github-pages |
||||||
|
|
@ -0,0 +1,47 @@ |
|||||||
|
###################################################################### |
||||||
|
# Build Tools |
||||||
|
|
||||||
|
.gradle |
||||||
|
/build/ |
||||||
|
!gradle/wrapper/gradle-wrapper.jar |
||||||
|
|
||||||
|
target/ |
||||||
|
!.mvn/wrapper/maven-wrapper.jar |
||||||
|
|
||||||
|
###################################################################### |
||||||
|
# IDE |
||||||
|
|
||||||
|
### STS ### |
||||||
|
.apt_generated |
||||||
|
.classpath |
||||||
|
.factorypath |
||||||
|
.project |
||||||
|
.settings |
||||||
|
.springBeans |
||||||
|
|
||||||
|
### IntelliJ IDEA ### |
||||||
|
.idea |
||||||
|
*.iws |
||||||
|
*.iml |
||||||
|
*.ipr |
||||||
|
|
||||||
|
### NetBeans ### |
||||||
|
nbproject/private/ |
||||||
|
build/* |
||||||
|
nbbuild/ |
||||||
|
dist/ |
||||||
|
nbdist/ |
||||||
|
.nb-gradle/ |
||||||
|
|
||||||
|
###################################################################### |
||||||
|
# Others |
||||||
|
*.log |
||||||
|
*.xml.versionsBackup |
||||||
|
*.swp |
||||||
|
|
||||||
|
!*/build/*.java |
||||||
|
!*/build/*.html |
||||||
|
!*/build/*.xml |
||||||
|
|
||||||
|
### JRebel ### |
||||||
|
rebel.xml |
@ -0,0 +1,3 @@ |
|||||||
|
[submodule "yudao-ui-admin"] |
||||||
|
path = yudao-ui-admin |
||||||
|
url = https://git.lotus-wallet.com/Loki/yudao-ui-admin.git |
@ -0,0 +1,60 @@ |
|||||||
|
#!groovy |
||||||
|
pipeline { |
||||||
|
|
||||||
|
agent any |
||||||
|
|
||||||
|
parameters { |
||||||
|
string(name: 'TAG_NAME', defaultValue: '', description: '') |
||||||
|
} |
||||||
|
|
||||||
|
environment { |
||||||
|
// DockerHub 凭证 ID(登录您的 DockerHub) |
||||||
|
DOCKER_CREDENTIAL_ID = 'dockerhub-id' |
||||||
|
// GitHub 凭证 ID (推送 tag 到 GitHub 仓库) |
||||||
|
GITHUB_CREDENTIAL_ID = 'github-id' |
||||||
|
// kubeconfig 凭证 ID (访问接入正在运行的 Kubernetes 集群) |
||||||
|
KUBECONFIG_CREDENTIAL_ID = 'demo-kubeconfig' |
||||||
|
// 镜像的推送 |
||||||
|
REGISTRY = 'docker.io' |
||||||
|
// DockerHub 账号名 |
||||||
|
DOCKERHUB_NAMESPACE = 'docker_username' |
||||||
|
// GitHub 账号名 |
||||||
|
GITHUB_ACCOUNT = 'https://gitee.com/zhijiantianya/ruoyi-vue-pro' |
||||||
|
// 应用名称 |
||||||
|
APP_NAME = 'yudao-server' |
||||||
|
// 应用部署路径 |
||||||
|
APP_DEPLOY_BASE_DIR = '/media/pi/KINGTON/data/work/projects/' |
||||||
|
} |
||||||
|
|
||||||
|
stages { |
||||||
|
stage('检出') { |
||||||
|
steps { |
||||||
|
git url: "https://gitee.com/will-we/ruoyi-vue-pro.git", |
||||||
|
branch: "devops" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
stage('构建') { |
||||||
|
steps { |
||||||
|
// TODO 解决多环境链接、密码不同配置临时方案 |
||||||
|
sh 'if [ ! -d "' + "${env.HOME}" + '/resources" ];then\n' + |
||||||
|
' echo "配置文件不存在无需修改"\n' + |
||||||
|
'else\n' + |
||||||
|
' cp -rf ' + "${env.HOME}" + '/resources/*.yaml ' + "${env.APP_NAME}" + '/src/main/resources\n' + |
||||||
|
' echo "配置文件替换"\n' + |
||||||
|
'fi' |
||||||
|
sh 'mvn clean package -Dmaven.test.skip=true' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
stage('部署') { |
||||||
|
steps { |
||||||
|
sh 'cp -f ' + ' bin/deploy.sh ' + "${env.APP_DEPLOY_BASE_DIR}" + "${env.APP_NAME}" |
||||||
|
sh 'cp -f ' + "${env.APP_NAME}" + '/target/*.jar ' + "${env.APP_DEPLOY_BASE_DIR}" + "${env.APP_NAME}" +'/build/' |
||||||
|
archiveArtifacts "${env.APP_NAME}" + '/target/*.jar' |
||||||
|
sh 'chmod +x ' + "${env.APP_DEPLOY_BASE_DIR}" + "${env.APP_NAME}" + '/deploy.sh' |
||||||
|
sh 'bash ' + "${env.APP_DEPLOY_BASE_DIR}" + "${env.APP_NAME}" + '/deploy.sh' |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
The MIT License (MIT) |
||||||
|
|
||||||
|
Copyright (c) 2021 ruoyi-vue-pro |
||||||
|
|
||||||
|
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. |
@ -0,0 +1,160 @@ |
|||||||
|
#!/bin/bash |
||||||
|
set -e |
||||||
|
|
||||||
|
DATE=$(date +%Y%m%d%H%M) |
||||||
|
# 基础路径 |
||||||
|
BASE_PATH=/work/projects/yudao-server |
||||||
|
# 编译后 jar 的地址。部署时,Jenkins 会上传 jar 包到该目录下 |
||||||
|
SOURCE_PATH=$BASE_PATH/build |
||||||
|
# 服务名称。同时约定部署服务的 jar 包名字也为它。 |
||||||
|
SERVER_NAME=yudao-server |
||||||
|
# 环境 |
||||||
|
PROFILES_ACTIVE=development |
||||||
|
# 健康检查 URL |
||||||
|
HEALTH_CHECK_URL=http://127.0.0.1:48080/actuator/health/ |
||||||
|
|
||||||
|
# heapError 存放路径 |
||||||
|
HEAP_ERROR_PATH=$BASE_PATH/heapError |
||||||
|
# JVM 参数 |
||||||
|
JAVA_OPS="-Xms512m -Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$HEAP_ERROR_PATH" |
||||||
|
|
||||||
|
# SkyWalking Agent 配置 |
||||||
|
#export SW_AGENT_NAME=$SERVER_NAME |
||||||
|
#export SW_AGENT_COLLECTOR_BACKEND_SERVICES=192.168.0.84:11800 |
||||||
|
#export SW_GRPC_LOG_SERVER_HOST=192.168.0.84 |
||||||
|
#export SW_AGENT_TRACE_IGNORE_PATH="Redisson/PING,/actuator/**,/admin/**" |
||||||
|
#export JAVA_AGENT=-javaagent:/work/skywalking/apache-skywalking-apm-bin/agent/skywalking-agent.jar |
||||||
|
|
||||||
|
# 备份 |
||||||
|
function backup() { |
||||||
|
# 如果不存在,则无需备份 |
||||||
|
if [ ! -f "$BASE_PATH/$SERVER_NAME.jar" ]; then |
||||||
|
echo "[backup] $BASE_PATH/$SERVER_NAME.jar 不存在,跳过备份" |
||||||
|
# 如果存在,则备份到 backup 目录下,使用时间作为后缀 |
||||||
|
else |
||||||
|
echo "[backup] 开始备份 $SERVER_NAME ..." |
||||||
|
cp $BASE_PATH/$SERVER_NAME.jar $BASE_PATH/backup/$SERVER_NAME-$DATE.jar |
||||||
|
echo "[backup] 备份 $SERVER_NAME 完成" |
||||||
|
fi |
||||||
|
} |
||||||
|
|
||||||
|
# 最新构建代码 移动到项目环境 |
||||||
|
function transfer() { |
||||||
|
echo "[transfer] 开始转移 $SERVER_NAME.jar" |
||||||
|
|
||||||
|
# 删除原 jar 包 |
||||||
|
if [ ! -f "$BASE_PATH/$SERVER_NAME.jar" ]; then |
||||||
|
echo "[transfer] $BASE_PATH/$SERVER_NAME.jar 不存在,跳过删除" |
||||||
|
else |
||||||
|
echo "[transfer] 移除 $BASE_PATH/$SERVER_NAME.jar 完成" |
||||||
|
rm $BASE_PATH/$SERVER_NAME.jar |
||||||
|
fi |
||||||
|
|
||||||
|
# 复制新 jar 包 |
||||||
|
echo "[transfer] 从 $SOURCE_PATH 中获取 $SERVER_NAME.jar 并迁移至 $BASE_PATH ...." |
||||||
|
cp $SOURCE_PATH/$SERVER_NAME.jar $BASE_PATH |
||||||
|
|
||||||
|
echo "[transfer] 转移 $SERVER_NAME.jar 完成" |
||||||
|
} |
||||||
|
|
||||||
|
# 停止:优雅关闭之前已经启动的服务 |
||||||
|
function stop() { |
||||||
|
echo "[stop] 开始停止 $BASE_PATH/$SERVER_NAME" |
||||||
|
PID=$(ps -ef | grep $BASE_PATH/$SERVER_NAME | grep -v "grep" | awk '{print $2}') |
||||||
|
# 如果 Java 服务启动中,则进行关闭 |
||||||
|
if [ -n "$PID" ]; then |
||||||
|
# 正常关闭 |
||||||
|
echo "[stop] $BASE_PATH/$SERVER_NAME 运行中,开始 kill [$PID]" |
||||||
|
kill -15 $PID |
||||||
|
# 等待最大 120 秒,直到关闭完成。 |
||||||
|
for ((i = 0; i < 120; i++)) |
||||||
|
do |
||||||
|
sleep 1 |
||||||
|
PID=$(ps -ef | grep $BASE_PATH/$SERVER_NAME | grep -v "grep" | awk '{print $2}') |
||||||
|
if [ -n "$PID" ]; then |
||||||
|
echo -e ".\c" |
||||||
|
else |
||||||
|
echo '[stop] 停止 $BASE_PATH/$SERVER_NAME 成功' |
||||||
|
break |
||||||
|
fi |
||||||
|
done |
||||||
|
|
||||||
|
# 如果正常关闭失败,那么进行强制 kill -9 进行关闭 |
||||||
|
if [ -n "$PID" ]; then |
||||||
|
echo "[stop] $BASE_PATH/$SERVER_NAME 失败,强制 kill -9 $PID" |
||||||
|
kill -9 $PID |
||||||
|
fi |
||||||
|
# 如果 Java 服务未启动,则无需关闭 |
||||||
|
else |
||||||
|
echo "[stop] $BASE_PATH/$SERVER_NAME 未启动,无需停止" |
||||||
|
fi |
||||||
|
} |
||||||
|
|
||||||
|
# 启动:启动后端项目 |
||||||
|
function start() { |
||||||
|
# 开启启动前,打印启动参数 |
||||||
|
echo "[start] 开始启动 $BASE_PATH/$SERVER_NAME" |
||||||
|
echo "[start] JAVA_OPS: $JAVA_OPS" |
||||||
|
echo "[start] JAVA_AGENT: $JAVA_AGENT" |
||||||
|
echo "[start] PROFILES: $PROFILES_ACTIVE" |
||||||
|
|
||||||
|
# 开始启动 |
||||||
|
BUILD_ID=dontKillMe nohup java -server $JAVA_OPS $JAVA_AGENT -jar $BASE_PATH/$SERVER_NAME.jar --spring.profiles.active=$PROFILES_ACTIVE & |
||||||
|
echo "[start] 启动 $BASE_PATH/$SERVER_NAME 完成" |
||||||
|
} |
||||||
|
|
||||||
|
# 健康检查:自动判断后端项目是否正常启动 |
||||||
|
function healthCheck() { |
||||||
|
# 如果配置健康检查,则进行健康检查 |
||||||
|
if [ -n "$HEALTH_CHECK_URL" ]; then |
||||||
|
# 健康检查最大 120 秒,直到健康检查通过 |
||||||
|
echo "[healthCheck] 开始通过 $HEALTH_CHECK_URL 地址,进行健康检查"; |
||||||
|
for ((i = 0; i < 120; i++)) |
||||||
|
do |
||||||
|
# 请求健康检查地址,只获取状态码。 |
||||||
|
result=`curl -I -m 10 -o /dev/null -s -w %{http_code} $HEALTH_CHECK_URL || echo "000"` |
||||||
|
# 如果状态码为 200,则说明健康检查通过 |
||||||
|
if [ "$result" == "200" ]; then |
||||||
|
echo "[healthCheck] 健康检查通过"; |
||||||
|
break |
||||||
|
# 如果状态码非 200,则说明未通过。sleep 1 秒后,继续重试 |
||||||
|
else |
||||||
|
echo -e ".\c" |
||||||
|
sleep 1 |
||||||
|
fi |
||||||
|
done |
||||||
|
|
||||||
|
# 健康检查未通过,则异常退出 shell 脚本,不继续部署。 |
||||||
|
if [ ! "$result" == "200" ]; then |
||||||
|
echo "[healthCheck] 健康检查不通过,可能部署失败。查看日志,自行判断是否启动成功"; |
||||||
|
tail -n 10 nohup.out |
||||||
|
exit 1; |
||||||
|
# 健康检查通过,打印最后 10 行日志,可能部署的人想看下日志。 |
||||||
|
else |
||||||
|
tail -n 10 nohup.out |
||||||
|
fi |
||||||
|
# 如果未配置健康检查,则 sleep 120 秒,人工看日志是否部署成功。 |
||||||
|
else |
||||||
|
echo "[healthCheck] HEALTH_CHECK_URL 未配置,开始 sleep 120 秒"; |
||||||
|
sleep 120 |
||||||
|
echo "[healthCheck] sleep 120 秒完成,查看日志,自行判断是否启动成功"; |
||||||
|
tail -n 50 nohup.out |
||||||
|
fi |
||||||
|
} |
||||||
|
|
||||||
|
# 部署 |
||||||
|
function deploy() { |
||||||
|
cd $BASE_PATH |
||||||
|
# 备份原 jar |
||||||
|
backup |
||||||
|
# 停止 Java 服务 |
||||||
|
stop |
||||||
|
# 部署新 jar |
||||||
|
transfer |
||||||
|
# 启动 Java 服务 |
||||||
|
start |
||||||
|
# 健康检查 |
||||||
|
healthCheck |
||||||
|
} |
||||||
|
|
||||||
|
deploy |
@ -0,0 +1,11 @@ |
|||||||
|
{ |
||||||
|
"local": { |
||||||
|
"baseUrl": "http://127.0.0.1:48080/admin-api", |
||||||
|
"token": "test1", |
||||||
|
"adminTenentId": "1", |
||||||
|
|
||||||
|
"appApi": "http://127.0.0.1:48080/app-api", |
||||||
|
"appToken": "test1", |
||||||
|
"appTenentId": "1" |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,4 @@ |
|||||||
|
config.stopBubbling = true |
||||||
|
lombok.tostring.callsuper=CALL |
||||||
|
lombok.equalsandhashcode.callsuper=CALL |
||||||
|
lombok.accessors.chain=true |
@ -0,0 +1,97 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" |
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||||
|
<modelVersion>4.0.0</modelVersion> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
<packaging>pom</packaging> |
||||||
|
<modules> |
||||||
|
<module>yudao-dependencies</module> |
||||||
|
<module>yudao-framework</module> |
||||||
|
<!-- Server 主项目 --> |
||||||
|
<module>yudao-server</module> |
||||||
|
<!-- 各种 module 拓展 --> |
||||||
|
<module>yudao-module-member</module> |
||||||
|
<module>yudao-module-bpm</module> |
||||||
|
<module>yudao-module-system</module> |
||||||
|
<module>yudao-module-infra</module> |
||||||
|
<module>yudao-module-pay</module> |
||||||
|
<module>zsw-farm</module> |
||||||
|
</modules> |
||||||
|
|
||||||
|
<name>${project.artifactId}</name> |
||||||
|
<description>芋道项目基础脚手架</description> |
||||||
|
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
||||||
|
|
||||||
|
<properties> |
||||||
|
<revision>1.6.2-snapshot</revision> |
||||||
|
<!-- Maven 相关 --> |
||||||
|
<java.version>1.8</java.version> |
||||||
|
<maven.compiler.source>${java.version}</maven.compiler.source> |
||||||
|
<maven.compiler.target>${java.version}</maven.compiler.target> |
||||||
|
<maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version> |
||||||
|
<maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version> |
||||||
|
<!-- 看看咋放到 bom 里 --> |
||||||
|
<lombok.version>1.18.20</lombok.version> |
||||||
|
<mapstruct.version>1.4.1.Final</mapstruct.version> |
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
||||||
|
</properties> |
||||||
|
|
||||||
|
<dependencyManagement> |
||||||
|
<dependencies> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-dependencies</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
<type>pom</type> |
||||||
|
<scope>import</scope> |
||||||
|
</dependency> |
||||||
|
</dependencies> |
||||||
|
</dependencyManagement> |
||||||
|
|
||||||
|
<build> |
||||||
|
<pluginManagement> |
||||||
|
<plugins> |
||||||
|
<!-- maven-surefire-plugin 插件,用于运行单元测试。 --> |
||||||
|
<!-- 注意,需要使用 3.0.X+,因为要支持 Junit 5 版本 --> |
||||||
|
<plugin> |
||||||
|
<groupId>org.apache.maven.plugins</groupId> |
||||||
|
<artifactId>maven-surefire-plugin</artifactId> |
||||||
|
<version>${maven-surefire-plugin.version}</version> |
||||||
|
</plugin> |
||||||
|
<!-- maven-compiler-plugin 插件,解决 Lombok + MapStruct 组合 --> |
||||||
|
<plugin> |
||||||
|
<groupId>org.apache.maven.plugins</groupId> |
||||||
|
<artifactId>maven-compiler-plugin</artifactId> |
||||||
|
<version>${maven-compiler-plugin.version}</version> |
||||||
|
<configuration> |
||||||
|
<annotationProcessorPaths> |
||||||
|
<path> |
||||||
|
<groupId>org.projectlombok</groupId> |
||||||
|
<artifactId>lombok</artifactId> |
||||||
|
<version>${lombok.version}</version> |
||||||
|
</path> |
||||||
|
<path> |
||||||
|
<groupId>org.mapstruct</groupId> |
||||||
|
<artifactId>mapstruct-processor</artifactId> |
||||||
|
<version>${mapstruct.version}</version> |
||||||
|
</path> |
||||||
|
</annotationProcessorPaths> |
||||||
|
</configuration> |
||||||
|
</plugin> |
||||||
|
</plugins> |
||||||
|
</pluginManagement> |
||||||
|
</build> |
||||||
|
|
||||||
|
<!-- 使用 aliyun 的 Maven 源,提升下载速度 --> |
||||||
|
<repositories> |
||||||
|
<repository> |
||||||
|
<id>aliyunmaven</id> |
||||||
|
<name>aliyun</name> |
||||||
|
<url>https://maven.aliyun.com/repository/public</url> |
||||||
|
</repository> |
||||||
|
</repositories> |
||||||
|
|
||||||
|
</project> |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,288 @@ |
|||||||
|
/* |
||||||
|
Navicat Premium Data Transfer |
||||||
|
|
||||||
|
Source Server : 127.0.0.1 |
||||||
|
Source Server Type : MySQL |
||||||
|
Source Server Version : 80026 |
||||||
|
Source Host : localhost:3306 |
||||||
|
Source Schema : ruoyi-vue-pro |
||||||
|
|
||||||
|
Target Server Type : MySQL |
||||||
|
Target Server Version : 80026 |
||||||
|
File Encoding : 65001 |
||||||
|
|
||||||
|
Date: 05/02/2022 00:50:30 |
||||||
|
*/ |
||||||
|
|
||||||
|
SET NAMES utf8mb4; |
||||||
|
SET FOREIGN_KEY_CHECKS = 0; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Table structure for QRTZ_BLOB_TRIGGERS |
||||||
|
-- ---------------------------- |
||||||
|
DROP TABLE IF EXISTS `QRTZ_BLOB_TRIGGERS`; |
||||||
|
CREATE TABLE `QRTZ_BLOB_TRIGGERS` ( |
||||||
|
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`TRIGGER_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`TRIGGER_GROUP` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`BLOB_DATA` blob, |
||||||
|
PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`), |
||||||
|
KEY `SCHED_NAME` (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`), |
||||||
|
CONSTRAINT `qrtz_blob_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) |
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Records of QRTZ_BLOB_TRIGGERS |
||||||
|
-- ---------------------------- |
||||||
|
BEGIN; |
||||||
|
COMMIT; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Table structure for QRTZ_CALENDARS |
||||||
|
-- ---------------------------- |
||||||
|
DROP TABLE IF EXISTS `QRTZ_CALENDARS`; |
||||||
|
CREATE TABLE `QRTZ_CALENDARS` ( |
||||||
|
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`CALENDAR_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`CALENDAR` blob NOT NULL, |
||||||
|
PRIMARY KEY (`SCHED_NAME`,`CALENDAR_NAME`) |
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Records of QRTZ_CALENDARS |
||||||
|
-- ---------------------------- |
||||||
|
BEGIN; |
||||||
|
COMMIT; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Table structure for QRTZ_CRON_TRIGGERS |
||||||
|
-- ---------------------------- |
||||||
|
DROP TABLE IF EXISTS `QRTZ_CRON_TRIGGERS`; |
||||||
|
CREATE TABLE `QRTZ_CRON_TRIGGERS` ( |
||||||
|
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`TRIGGER_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`TRIGGER_GROUP` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`CRON_EXPRESSION` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`TIME_ZONE_ID` varchar(80) COLLATE utf8mb4_unicode_ci DEFAULT NULL, |
||||||
|
PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`), |
||||||
|
CONSTRAINT `qrtz_cron_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) |
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Records of QRTZ_CRON_TRIGGERS |
||||||
|
-- ---------------------------- |
||||||
|
BEGIN; |
||||||
|
INSERT INTO `QRTZ_CRON_TRIGGERS` VALUES ('schedulerName', 'payNotifyJob', 'DEFAULT', '* * * * * ?', 'Asia/Shanghai'); |
||||||
|
INSERT INTO `QRTZ_CRON_TRIGGERS` VALUES ('schedulerName', 'userSessionTimeoutJob', 'DEFAULT', '0 * * * * ? *', 'Asia/Shanghai'); |
||||||
|
COMMIT; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Table structure for QRTZ_FIRED_TRIGGERS |
||||||
|
-- ---------------------------- |
||||||
|
DROP TABLE IF EXISTS `QRTZ_FIRED_TRIGGERS`; |
||||||
|
CREATE TABLE `QRTZ_FIRED_TRIGGERS` ( |
||||||
|
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`ENTRY_ID` varchar(95) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`TRIGGER_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`TRIGGER_GROUP` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`INSTANCE_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`FIRED_TIME` bigint NOT NULL, |
||||||
|
`SCHED_TIME` bigint NOT NULL, |
||||||
|
`PRIORITY` int NOT NULL, |
||||||
|
`STATE` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`JOB_NAME` varchar(190) COLLATE utf8mb4_unicode_ci DEFAULT NULL, |
||||||
|
`JOB_GROUP` varchar(190) COLLATE utf8mb4_unicode_ci DEFAULT NULL, |
||||||
|
`IS_NONCONCURRENT` varchar(1) COLLATE utf8mb4_unicode_ci DEFAULT NULL, |
||||||
|
`REQUESTS_RECOVERY` varchar(1) COLLATE utf8mb4_unicode_ci DEFAULT NULL, |
||||||
|
PRIMARY KEY (`SCHED_NAME`,`ENTRY_ID`), |
||||||
|
KEY `IDX_QRTZ_FT_TRIG_INST_NAME` (`SCHED_NAME`,`INSTANCE_NAME`), |
||||||
|
KEY `IDX_QRTZ_FT_INST_JOB_REQ_RCVRY` (`SCHED_NAME`,`INSTANCE_NAME`,`REQUESTS_RECOVERY`), |
||||||
|
KEY `IDX_QRTZ_FT_J_G` (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`), |
||||||
|
KEY `IDX_QRTZ_FT_JG` (`SCHED_NAME`,`JOB_GROUP`), |
||||||
|
KEY `IDX_QRTZ_FT_T_G` (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`), |
||||||
|
KEY `IDX_QRTZ_FT_TG` (`SCHED_NAME`,`TRIGGER_GROUP`) |
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Records of QRTZ_FIRED_TRIGGERS |
||||||
|
-- ---------------------------- |
||||||
|
BEGIN; |
||||||
|
COMMIT; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Table structure for QRTZ_JOB_DETAILS |
||||||
|
-- ---------------------------- |
||||||
|
DROP TABLE IF EXISTS `QRTZ_JOB_DETAILS`; |
||||||
|
CREATE TABLE `QRTZ_JOB_DETAILS` ( |
||||||
|
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`JOB_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`JOB_GROUP` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`DESCRIPTION` varchar(250) COLLATE utf8mb4_unicode_ci DEFAULT NULL, |
||||||
|
`JOB_CLASS_NAME` varchar(250) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`IS_DURABLE` varchar(1) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`IS_NONCONCURRENT` varchar(1) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`IS_UPDATE_DATA` varchar(1) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`REQUESTS_RECOVERY` varchar(1) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`JOB_DATA` blob, |
||||||
|
PRIMARY KEY (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`), |
||||||
|
KEY `IDX_QRTZ_J_REQ_RECOVERY` (`SCHED_NAME`,`REQUESTS_RECOVERY`), |
||||||
|
KEY `IDX_QRTZ_J_GRP` (`SCHED_NAME`,`JOB_GROUP`) |
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Records of QRTZ_JOB_DETAILS |
||||||
|
-- ---------------------------- |
||||||
|
BEGIN; |
||||||
|
INSERT INTO `QRTZ_JOB_DETAILS` VALUES ('schedulerName', 'payNotifyJob', 'DEFAULT', NULL, 'cn.iocoder.yudao.framework.quartz.core.handler.JobHandlerInvoker', '0', '1', '1', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000027400064A4F425F49447372000E6A6176612E6C616E672E4C6F6E673B8BE490CC8F23DF0200014A000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B020000787000000000000000057400104A4F425F48414E444C45525F4E414D4574000C7061794E6F746966794A6F627800); |
||||||
|
INSERT INTO `QRTZ_JOB_DETAILS` VALUES ('schedulerName', 'userSessionTimeoutJob', 'DEFAULT', NULL, 'cn.iocoder.yudao.framework.quartz.core.handler.JobHandlerInvoker', '0', '1', '1', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000027400064A4F425F49447372000E6A6176612E6C616E672E4C6F6E673B8BE490CC8F23DF0200014A000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B0200007870000000000000000D7400104A4F425F48414E444C45525F4E414D457400157573657253657373696F6E54696D656F75744A6F627800); |
||||||
|
COMMIT; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Table structure for QRTZ_LOCKS |
||||||
|
-- ---------------------------- |
||||||
|
DROP TABLE IF EXISTS `QRTZ_LOCKS`; |
||||||
|
CREATE TABLE `QRTZ_LOCKS` ( |
||||||
|
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`LOCK_NAME` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
PRIMARY KEY (`SCHED_NAME`,`LOCK_NAME`) |
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Records of QRTZ_LOCKS |
||||||
|
-- ---------------------------- |
||||||
|
BEGIN; |
||||||
|
INSERT INTO `QRTZ_LOCKS` VALUES ('schedulerName', 'STATE_ACCESS'); |
||||||
|
INSERT INTO `QRTZ_LOCKS` VALUES ('schedulerName', 'TRIGGER_ACCESS'); |
||||||
|
COMMIT; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Table structure for QRTZ_PAUSED_TRIGGER_GRPS |
||||||
|
-- ---------------------------- |
||||||
|
DROP TABLE IF EXISTS `QRTZ_PAUSED_TRIGGER_GRPS`; |
||||||
|
CREATE TABLE `QRTZ_PAUSED_TRIGGER_GRPS` ( |
||||||
|
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`TRIGGER_GROUP` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
PRIMARY KEY (`SCHED_NAME`,`TRIGGER_GROUP`) |
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Records of QRTZ_PAUSED_TRIGGER_GRPS |
||||||
|
-- ---------------------------- |
||||||
|
BEGIN; |
||||||
|
COMMIT; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Table structure for QRTZ_SCHEDULER_STATE |
||||||
|
-- ---------------------------- |
||||||
|
DROP TABLE IF EXISTS `QRTZ_SCHEDULER_STATE`; |
||||||
|
CREATE TABLE `QRTZ_SCHEDULER_STATE` ( |
||||||
|
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`INSTANCE_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`LAST_CHECKIN_TIME` bigint NOT NULL, |
||||||
|
`CHECKIN_INTERVAL` bigint NOT NULL, |
||||||
|
PRIMARY KEY (`SCHED_NAME`,`INSTANCE_NAME`) |
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Records of QRTZ_SCHEDULER_STATE |
||||||
|
-- ---------------------------- |
||||||
|
BEGIN; |
||||||
|
INSERT INTO `QRTZ_SCHEDULER_STATE` VALUES ('schedulerName', 'Yunai.local1635571630493', 1635572537879, 15000); |
||||||
|
COMMIT; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Table structure for QRTZ_SIMPLE_TRIGGERS |
||||||
|
-- ---------------------------- |
||||||
|
DROP TABLE IF EXISTS `QRTZ_SIMPLE_TRIGGERS`; |
||||||
|
CREATE TABLE `QRTZ_SIMPLE_TRIGGERS` ( |
||||||
|
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`TRIGGER_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`TRIGGER_GROUP` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`REPEAT_COUNT` bigint NOT NULL, |
||||||
|
`REPEAT_INTERVAL` bigint NOT NULL, |
||||||
|
`TIMES_TRIGGERED` bigint NOT NULL, |
||||||
|
PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`), |
||||||
|
CONSTRAINT `qrtz_simple_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) |
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Records of QRTZ_SIMPLE_TRIGGERS |
||||||
|
-- ---------------------------- |
||||||
|
BEGIN; |
||||||
|
COMMIT; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Table structure for QRTZ_SIMPROP_TRIGGERS |
||||||
|
-- ---------------------------- |
||||||
|
DROP TABLE IF EXISTS `QRTZ_SIMPROP_TRIGGERS`; |
||||||
|
CREATE TABLE `QRTZ_SIMPROP_TRIGGERS` ( |
||||||
|
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`TRIGGER_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`TRIGGER_GROUP` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`STR_PROP_1` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL, |
||||||
|
`STR_PROP_2` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL, |
||||||
|
`STR_PROP_3` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL, |
||||||
|
`INT_PROP_1` int DEFAULT NULL, |
||||||
|
`INT_PROP_2` int DEFAULT NULL, |
||||||
|
`LONG_PROP_1` bigint DEFAULT NULL, |
||||||
|
`LONG_PROP_2` bigint DEFAULT NULL, |
||||||
|
`DEC_PROP_1` decimal(13,4) DEFAULT NULL, |
||||||
|
`DEC_PROP_2` decimal(13,4) DEFAULT NULL, |
||||||
|
`BOOL_PROP_1` varchar(1) COLLATE utf8mb4_unicode_ci DEFAULT NULL, |
||||||
|
`BOOL_PROP_2` varchar(1) COLLATE utf8mb4_unicode_ci DEFAULT NULL, |
||||||
|
PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`), |
||||||
|
CONSTRAINT `qrtz_simprop_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) |
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Records of QRTZ_SIMPROP_TRIGGERS |
||||||
|
-- ---------------------------- |
||||||
|
BEGIN; |
||||||
|
COMMIT; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Table structure for QRTZ_TRIGGERS |
||||||
|
-- ---------------------------- |
||||||
|
DROP TABLE IF EXISTS `QRTZ_TRIGGERS`; |
||||||
|
CREATE TABLE `QRTZ_TRIGGERS` ( |
||||||
|
`SCHED_NAME` varchar(120) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`TRIGGER_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`TRIGGER_GROUP` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`JOB_NAME` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`JOB_GROUP` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`DESCRIPTION` varchar(250) COLLATE utf8mb4_unicode_ci DEFAULT NULL, |
||||||
|
`NEXT_FIRE_TIME` bigint DEFAULT NULL, |
||||||
|
`PREV_FIRE_TIME` bigint DEFAULT NULL, |
||||||
|
`PRIORITY` int DEFAULT NULL, |
||||||
|
`TRIGGER_STATE` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`TRIGGER_TYPE` varchar(8) COLLATE utf8mb4_unicode_ci NOT NULL, |
||||||
|
`START_TIME` bigint NOT NULL, |
||||||
|
`END_TIME` bigint DEFAULT NULL, |
||||||
|
`CALENDAR_NAME` varchar(190) COLLATE utf8mb4_unicode_ci DEFAULT NULL, |
||||||
|
`MISFIRE_INSTR` smallint DEFAULT NULL, |
||||||
|
`JOB_DATA` blob, |
||||||
|
PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`), |
||||||
|
KEY `IDX_QRTZ_T_J` (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`), |
||||||
|
KEY `IDX_QRTZ_T_JG` (`SCHED_NAME`,`JOB_GROUP`), |
||||||
|
KEY `IDX_QRTZ_T_C` (`SCHED_NAME`,`CALENDAR_NAME`), |
||||||
|
KEY `IDX_QRTZ_T_G` (`SCHED_NAME`,`TRIGGER_GROUP`), |
||||||
|
KEY `IDX_QRTZ_T_STATE` (`SCHED_NAME`,`TRIGGER_STATE`), |
||||||
|
KEY `IDX_QRTZ_T_N_STATE` (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`,`TRIGGER_STATE`), |
||||||
|
KEY `IDX_QRTZ_T_N_G_STATE` (`SCHED_NAME`,`TRIGGER_GROUP`,`TRIGGER_STATE`), |
||||||
|
KEY `IDX_QRTZ_T_NEXT_FIRE_TIME` (`SCHED_NAME`,`NEXT_FIRE_TIME`), |
||||||
|
KEY `IDX_QRTZ_T_NFT_ST` (`SCHED_NAME`,`TRIGGER_STATE`,`NEXT_FIRE_TIME`), |
||||||
|
KEY `IDX_QRTZ_T_NFT_MISFIRE` (`SCHED_NAME`,`MISFIRE_INSTR`,`NEXT_FIRE_TIME`), |
||||||
|
KEY `IDX_QRTZ_T_NFT_ST_MISFIRE` (`SCHED_NAME`,`MISFIRE_INSTR`,`NEXT_FIRE_TIME`,`TRIGGER_STATE`), |
||||||
|
KEY `IDX_QRTZ_T_NFT_ST_MISFIRE_GRP` (`SCHED_NAME`,`MISFIRE_INSTR`,`NEXT_FIRE_TIME`,`TRIGGER_GROUP`,`TRIGGER_STATE`), |
||||||
|
CONSTRAINT `qrtz_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) REFERENCES `QRTZ_JOB_DETAILS` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) |
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; |
||||||
|
|
||||||
|
-- ---------------------------- |
||||||
|
-- Records of QRTZ_TRIGGERS |
||||||
|
-- ---------------------------- |
||||||
|
BEGIN; |
||||||
|
INSERT INTO `QRTZ_TRIGGERS` VALUES ('schedulerName', 'payNotifyJob', 'DEFAULT', 'payNotifyJob', 'DEFAULT', NULL, 1635572540000, 1635572539000, 5, 'WAITING', 'CRON', 1635294882000, 0, NULL, 0, 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000037400114A4F425F48414E444C45525F504152414D707400124A4F425F52455452595F494E54455256414C737200116A6176612E6C616E672E496E746567657212E2A0A4F781873802000149000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B02000078700000000074000F4A4F425F52455452595F434F554E5471007E000B7800); |
||||||
|
INSERT INTO `QRTZ_TRIGGERS` VALUES ('schedulerName', 'userSessionTimeoutJob', 'DEFAULT', 'userSessionTimeoutJob', 'DEFAULT', NULL, 1643993400000, -1, 5, 'WAITING', 'CRON', 1643993386000, 0, NULL, 0, 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000037400114A4F425F48414E444C45525F504152414D707400124A4F425F52455452595F494E54455256414C737200116A6176612E6C616E672E496E746567657212E2A0A4F781873802000149000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B0200007870000007D074000F4A4F425F52455452595F434F554E547371007E0009000000037800); |
||||||
|
COMMIT; |
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1; |
File diff suppressed because one or more lines are too long
@ -0,0 +1,572 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" |
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||||
|
<modelVersion>4.0.0</modelVersion> |
||||||
|
|
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-dependencies</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
<packaging>pom</packaging> |
||||||
|
|
||||||
|
<name>${project.artifactId}</name> |
||||||
|
<description>基础 bom 文件,管理整个项目的依赖版本</description> |
||||||
|
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
||||||
|
|
||||||
|
<properties> |
||||||
|
<revision>1.6.2-snapshot</revision> |
||||||
|
<!-- 统一依赖管理 --> |
||||||
|
<spring.boot.version>2.5.10</spring.boot.version> |
||||||
|
<!-- Web 相关 --> |
||||||
|
<knife4j.version>3.0.2</knife4j.version> |
||||||
|
<swagger-annotations.version>1.5.22</swagger-annotations.version> |
||||||
|
<servlet.versoin>2.5</servlet.versoin> |
||||||
|
<!-- DB 相关 --> |
||||||
|
<mysql.version>5.1.46</mysql.version> |
||||||
|
<druid.version>1.2.8</druid.version> |
||||||
|
<mybatis-plus.version>3.4.3.4</mybatis-plus.version> |
||||||
|
<dynamic-datasource.version>3.5.0</dynamic-datasource.version> |
||||||
|
<redisson.version>3.16.6</redisson.version> |
||||||
|
<!-- Config 配置中心相关 --> |
||||||
|
<apollo.version>1.9.2</apollo.version> |
||||||
|
<!-- Job 定时任务相关 --> |
||||||
|
<!-- 服务保障相关 --> |
||||||
|
<lock4j.version>2.2.0</lock4j.version> |
||||||
|
<resilience4j.version>1.7.0</resilience4j.version> |
||||||
|
<!-- 监控相关 --> |
||||||
|
<skywalking.version>8.7.0</skywalking.version> |
||||||
|
<spring-boot-admin.version>2.6.2</spring-boot-admin.version> |
||||||
|
<opentracing.version>0.31.0</opentracing.version> |
||||||
|
<!-- Test 测试相关 --> |
||||||
|
<podam.version>7.2.6.RELEASE</podam.version> |
||||||
|
<jedis-mock.version>0.1.16</jedis-mock.version> |
||||||
|
<mockito-inline.version>3.9.0</mockito-inline.version> |
||||||
|
<!-- Bpm 工作流相关 --> |
||||||
|
<activiti.version>7.1.0.M6</activiti.version> |
||||||
|
<flowable.version>6.7.0</flowable.version> |
||||||
|
<!-- 工具类相关 --> |
||||||
|
<lombok.version>1.18.20</lombok.version> |
||||||
|
<mapstruct.version>1.4.1.Final</mapstruct.version> |
||||||
|
<hutool.version>5.6.1</hutool.version> |
||||||
|
<easyexcel.verion>2.2.7</easyexcel.verion> |
||||||
|
<velocity.version>2.2</velocity.version> |
||||||
|
<screw.version>1.0.5</screw.version> |
||||||
|
<guava.version>30.1.1-jre</guava.version> |
||||||
|
<guice.version>5.1.0</guice.version> |
||||||
|
<transmittable-thread-local.version>2.12.2</transmittable-thread-local.version> |
||||||
|
<commons-net.version>3.8.0</commons-net.version> |
||||||
|
<jsch.version>0.1.55</jsch.version> |
||||||
|
<!-- 三方云服务相关 --> |
||||||
|
<minio.version>8.2.2</minio.version> |
||||||
|
<aliyun-java-sdk-core.version>4.5.25</aliyun-java-sdk-core.version> |
||||||
|
<aliyun-java-sdk-dysmsapi.version>2.1.0</aliyun-java-sdk-dysmsapi.version> |
||||||
|
<tencentcloud-sdk-java.version>3.1.471</tencentcloud-sdk-java.version> |
||||||
|
<yunpian-java-sdk.version>1.2.7</yunpian-java-sdk.version> |
||||||
|
<justauth.version>1.4.0</justauth.version> |
||||||
|
</properties> |
||||||
|
|
||||||
|
<dependencyManagement> |
||||||
|
<dependencies> |
||||||
|
<!-- 统一依赖管理 --> |
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework.boot</groupId> |
||||||
|
<artifactId>spring-boot-dependencies</artifactId> |
||||||
|
<version>${spring.boot.version}</version> |
||||||
|
<type>pom</type> |
||||||
|
<scope>import</scope> |
||||||
|
<exclusions> |
||||||
|
<exclusion> |
||||||
|
<groupId>mysql</groupId> |
||||||
|
<artifactId>mysql-connector-java</artifactId> |
||||||
|
</exclusion> |
||||||
|
</exclusions> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- 业务组件 --> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-biz-dict</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-biz-sms</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-biz-pay</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-biz-weixin</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-biz-data-permission</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-biz-social</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- Spring 核心 --> |
||||||
|
<dependency> |
||||||
|
<!-- 用于生成自定义的 Spring @ConfigurationProperties 配置类的说明文件 --> |
||||||
|
<groupId>org.springframework.boot</groupId> |
||||||
|
<artifactId>spring-boot-configuration-processor</artifactId> |
||||||
|
<version>${spring.boot.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- Web 相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-web</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-security</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>com.github.xiaoymin</groupId> |
||||||
|
<artifactId>knife4j-spring-boot-starter</artifactId> |
||||||
|
<version>${knife4j.version}</version> |
||||||
|
<exclusions> |
||||||
|
<exclusion> |
||||||
|
<artifactId>mapstruct</artifactId> |
||||||
|
<groupId>org.mapstruct</groupId> <!-- 避免冲突 --> |
||||||
|
</exclusion> |
||||||
|
<exclusion> |
||||||
|
<artifactId>guava</artifactId> |
||||||
|
<groupId>com.google.guava</groupId> |
||||||
|
</exclusion> |
||||||
|
<exclusion> |
||||||
|
<artifactId>swagger-annotations</artifactId> |
||||||
|
<groupId>io.swagger</groupId> |
||||||
|
</exclusion> |
||||||
|
</exclusions> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>io.swagger</groupId> |
||||||
|
<artifactId>swagger-annotations</artifactId> |
||||||
|
<version>${swagger-annotations.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- DB 相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-mybatis</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>mysql</groupId> |
||||||
|
<artifactId>mysql-connector-java</artifactId> |
||||||
|
<version>${mysql.version}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>com.alibaba</groupId> |
||||||
|
<artifactId>druid-spring-boot-starter</artifactId> |
||||||
|
<version>${druid.version}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>com.baomidou</groupId> |
||||||
|
<artifactId>mybatis-plus-boot-starter</artifactId> |
||||||
|
<version>${mybatis-plus.version}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>com.baomidou</groupId> |
||||||
|
<artifactId>dynamic-datasource-spring-boot-starter</artifactId> <!-- 多数据源 --> |
||||||
|
<version>${dynamic-datasource.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-redis</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>org.redisson</groupId> |
||||||
|
<artifactId>redisson-spring-boot-starter</artifactId> |
||||||
|
<version>${redisson.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- Config 配置中心相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-config</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>com.ctrip.framework.apollo</groupId> |
||||||
|
<artifactId>apollo-client</artifactId> <!-- 引入 Apollo Client 库,实现内嵌的配置中心 --> |
||||||
|
<version>${apollo.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- Job 定时任务相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-job</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- 消息队列相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-mq</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- 服务保障相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-protection</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>com.baomidou</groupId> |
||||||
|
<artifactId>lock4j-redisson-spring-boot-starter</artifactId> |
||||||
|
<version>${lock4j.version}</version> |
||||||
|
<exclusions> |
||||||
|
<exclusion> |
||||||
|
<artifactId>redisson-spring-boot-starter</artifactId> |
||||||
|
<groupId>org.redisson</groupId> |
||||||
|
</exclusion> |
||||||
|
</exclusions> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>io.github.resilience4j</groupId> |
||||||
|
<artifactId>resilience4j-ratelimiter</artifactId> |
||||||
|
<version>${resilience4j.version}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>io.github.resilience4j</groupId> |
||||||
|
<artifactId>resilience4j-spring-boot2</artifactId> |
||||||
|
<version>${resilience4j.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- 监控相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-monitor</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>org.apache.skywalking</groupId> |
||||||
|
<artifactId>apm-toolkit-trace</artifactId> |
||||||
|
<version>${skywalking.version}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.apache.skywalking</groupId> |
||||||
|
<artifactId>apm-toolkit-logback-1.x</artifactId> |
||||||
|
<version>${skywalking.version}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.apache.skywalking</groupId> |
||||||
|
<artifactId>apm-toolkit-opentracing</artifactId> |
||||||
|
<version>${skywalking.version}</version> |
||||||
|
<!-- <exclusions>--> |
||||||
|
<!-- <exclusion>--> |
||||||
|
<!-- <artifactId>opentracing-api</artifactId>--> |
||||||
|
<!-- <groupId>io.opentracing</groupId>--> |
||||||
|
<!-- </exclusion>--> |
||||||
|
<!-- <exclusion>--> |
||||||
|
<!-- <artifactId>opentracing-util</artifactId>--> |
||||||
|
<!-- <groupId>io.opentracing</groupId>--> |
||||||
|
<!-- </exclusion>--> |
||||||
|
<!-- </exclusions>--> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>io.opentracing</groupId> |
||||||
|
<artifactId>opentracing-api</artifactId> |
||||||
|
<version>${opentracing.version}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>io.opentracing</groupId> |
||||||
|
<artifactId>opentracing-util</artifactId> |
||||||
|
<version>${opentracing.version}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>io.opentracing</groupId> |
||||||
|
<artifactId>opentracing-noop</artifactId> |
||||||
|
<version>${opentracing.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>de.codecentric</groupId> |
||||||
|
<artifactId>spring-boot-admin-starter-server</artifactId> <!-- 实现 Spring Boot Admin Server 服务端 --> |
||||||
|
<version>${spring-boot-admin.version}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>de.codecentric</groupId> |
||||||
|
<artifactId>spring-boot-admin-starter-client</artifactId> <!-- 实现 Spring Boot Admin Server 服务端 --> |
||||||
|
<version>${spring-boot-admin.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- Test 测试相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-test</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>org.mockito</groupId> |
||||||
|
<artifactId>mockito-inline</artifactId> |
||||||
|
<version>${mockito-inline.version}</version> <!-- 支持 Mockito 的 final 类与 static 方法的 mock --> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework.boot</groupId> |
||||||
|
<artifactId>spring-boot-starter-test</artifactId> |
||||||
|
<version>${spring.boot.version}</version> |
||||||
|
<exclusions> |
||||||
|
<exclusion> |
||||||
|
<artifactId>asm</artifactId> |
||||||
|
<groupId>org.ow2.asm</groupId> |
||||||
|
</exclusion> |
||||||
|
<exclusion> |
||||||
|
<groupId>org.mockito</groupId> |
||||||
|
<artifactId>mockito-core</artifactId> |
||||||
|
</exclusion> |
||||||
|
</exclusions> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>com.github.fppt</groupId> <!-- 单元测试,我们采用内嵌的 Redis 数据库 --> |
||||||
|
<artifactId>jedis-mock</artifactId> |
||||||
|
<version>${jedis-mock.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>uk.co.jemos.podam</groupId> <!-- 单元测试,随机生成 POJO 类 --> |
||||||
|
<artifactId>podam</artifactId> |
||||||
|
<version>${podam.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- 工作流相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>org.activiti</groupId> |
||||||
|
<artifactId>activiti-spring-boot-starter</artifactId> |
||||||
|
<version>${activiti.version}</version> |
||||||
|
<exclusions> |
||||||
|
<exclusion> |
||||||
|
<groupId>de.odysseus.juel</groupId> |
||||||
|
<artifactId>juel-api</artifactId> |
||||||
|
</exclusion> |
||||||
|
<exclusion> |
||||||
|
<groupId>de.odysseus.juel</groupId> |
||||||
|
<artifactId>juel-spi</artifactId> |
||||||
|
</exclusion> |
||||||
|
<exclusion> |
||||||
|
<groupId>org.mybatis</groupId> |
||||||
|
<artifactId>mybatis</artifactId> |
||||||
|
</exclusion> |
||||||
|
<exclusion> |
||||||
|
<artifactId>el-api</artifactId> |
||||||
|
<groupId>javax.el</groupId> |
||||||
|
</exclusion> |
||||||
|
</exclusions> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.activiti</groupId> |
||||||
|
<artifactId>activiti-image-generator</artifactId> |
||||||
|
<version>${activiti.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-activiti</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
<!-- 工作流相关 flowable --> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-flowable</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.flowable</groupId> |
||||||
|
<artifactId>flowable-spring-boot-starter-basic</artifactId> |
||||||
|
<version>${flowable.version}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.flowable</groupId> |
||||||
|
<artifactId>flowable-spring-boot-starter-actuator</artifactId> |
||||||
|
<version>${flowable.version}</version> |
||||||
|
</dependency> |
||||||
|
<!-- 工作流相关结束 --> |
||||||
|
|
||||||
|
<!-- 工具类相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-common</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-excel</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>org.projectlombok</groupId> |
||||||
|
<artifactId>lombok</artifactId> |
||||||
|
<version>${lombok.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>org.mapstruct</groupId> |
||||||
|
<artifactId>mapstruct</artifactId> <!-- use mapstruct-jdk8 for Java 8 or higher --> |
||||||
|
<version>${mapstruct.version}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.mapstruct</groupId> |
||||||
|
<artifactId>mapstruct-jdk8</artifactId> |
||||||
|
<version>${mapstruct.version}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.mapstruct</groupId> |
||||||
|
<artifactId>mapstruct-processor</artifactId> |
||||||
|
<version>${mapstruct.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>cn.hutool</groupId> |
||||||
|
<artifactId>hutool-all</artifactId> |
||||||
|
<version>${hutool.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>com.alibaba</groupId> |
||||||
|
<artifactId>easyexcel</artifactId> |
||||||
|
<version>${easyexcel.verion}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>org.apache.velocity</groupId> |
||||||
|
<artifactId>velocity-engine-core</artifactId> |
||||||
|
<version>${velocity.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>cn.smallbun.screw</groupId> |
||||||
|
<artifactId>screw-core</artifactId> <!-- 实现数据库文档 --> |
||||||
|
<version>${screw.version}</version> |
||||||
|
<exclusions> |
||||||
|
<exclusion> |
||||||
|
<groupId>org.freemarker</groupId> |
||||||
|
<artifactId>freemarker</artifactId> <!-- 移除 Freemarker 依赖,采用 Velocity 作为模板引擎 --> |
||||||
|
</exclusion> |
||||||
|
</exclusions> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>com.google.guava</groupId> |
||||||
|
<artifactId>guava</artifactId> |
||||||
|
<version>${guava.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>com.google.inject</groupId> |
||||||
|
<artifactId>guice</artifactId> |
||||||
|
<version>${guice.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>com.alibaba</groupId> |
||||||
|
<artifactId>transmittable-thread-local</artifactId> <!-- 解决 ThreadLocal 父子线程的传值问题 --> |
||||||
|
<version>${transmittable-thread-local.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>commons-net</groupId> |
||||||
|
<artifactId>commons-net</artifactId> <!-- 解决 ftp 连接 --> |
||||||
|
<version>${commons-net.version}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>com.jcraft</groupId> |
||||||
|
<artifactId>jsch</artifactId> <!-- 解决 sftp 连接 --> |
||||||
|
<version>${jsch.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- 三方云服务相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-file</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>io.minio</groupId> |
||||||
|
<artifactId>minio</artifactId> |
||||||
|
<version>${minio.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- SMS SDK begin --> |
||||||
|
<dependency> |
||||||
|
<groupId>com.yunpian.sdk</groupId> |
||||||
|
<artifactId>yunpian-java-sdk</artifactId> |
||||||
|
<version>${yunpian-java-sdk.version}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>com.aliyun</groupId> |
||||||
|
<artifactId>aliyun-java-sdk-core</artifactId> |
||||||
|
<version>${aliyun-java-sdk-core.version}</version> |
||||||
|
<exclusions> |
||||||
|
<exclusion> |
||||||
|
<artifactId>opentracing-api</artifactId> |
||||||
|
<groupId>io.opentracing</groupId> |
||||||
|
</exclusion> |
||||||
|
<exclusion> |
||||||
|
<artifactId>opentracing-util</artifactId> |
||||||
|
<groupId>io.opentracing</groupId> |
||||||
|
</exclusion> |
||||||
|
</exclusions> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>com.aliyun</groupId> |
||||||
|
<artifactId>aliyun-java-sdk-dysmsapi</artifactId> |
||||||
|
<version>${aliyun-java-sdk-dysmsapi.version}</version> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>com.tencentcloudapi</groupId> |
||||||
|
<artifactId>tencentcloud-sdk-java</artifactId> |
||||||
|
<version>${tencentcloud-sdk-java.version}</version> |
||||||
|
</dependency> |
||||||
|
<!-- SMS SDK end --> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>com.xkcoding.justauth</groupId> |
||||||
|
<artifactId>justauth-spring-boot-starter</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) --> |
||||||
|
<version>${justauth.version}</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
</dependencies> |
||||||
|
</dependencyManagement> |
||||||
|
|
||||||
|
</project> |
@ -0,0 +1,55 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" |
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||||
|
<modelVersion>4.0.0</modelVersion> |
||||||
|
<parent> |
||||||
|
<artifactId>yudao</artifactId> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<version>${revision}</version> |
||||||
|
</parent> |
||||||
|
<packaging>pom</packaging> |
||||||
|
<modules> |
||||||
|
<module>yudao-common</module> |
||||||
|
<module>yudao-spring-boot-starter-mybatis</module> |
||||||
|
<module>yudao-spring-boot-starter-redis</module> |
||||||
|
<module>yudao-spring-boot-starter-web</module> |
||||||
|
<module>yudao-spring-boot-starter-security</module> |
||||||
|
|
||||||
|
<module>yudao-spring-boot-starter-file</module> |
||||||
|
<module>yudao-spring-boot-starter-monitor</module> |
||||||
|
<module>yudao-spring-boot-starter-protection</module> |
||||||
|
<module>yudao-spring-boot-starter-config</module> |
||||||
|
<module>yudao-spring-boot-starter-job</module> |
||||||
|
<module>yudao-spring-boot-starter-mq</module> |
||||||
|
|
||||||
|
<module>yudao-spring-boot-starter-excel</module> |
||||||
|
<module>yudao-spring-boot-starter-test</module> |
||||||
|
<module>yudao-spring-boot-starter-extension</module> |
||||||
|
|
||||||
|
<module>yudao-spring-boot-starter-biz-operatelog</module> |
||||||
|
<module>yudao-spring-boot-starter-biz-dict</module> |
||||||
|
<module>yudao-spring-boot-starter-biz-sms</module> |
||||||
|
<module>yudao-spring-boot-starter-activiti</module> |
||||||
|
<module>yudao-spring-boot-starter-biz-pay</module> |
||||||
|
<module>yudao-spring-boot-starter-biz-weixin</module> |
||||||
|
<module>yudao-spring-boot-starter-biz-social</module> |
||||||
|
<module>yudao-spring-boot-starter-biz-tenant</module> |
||||||
|
<module>yudao-spring-boot-starter-biz-data-permission</module> |
||||||
|
<module>yudao-spring-boot-starter-flowable</module> |
||||||
|
</modules> |
||||||
|
|
||||||
|
<artifactId>yudao-framework</artifactId> |
||||||
|
<description> |
||||||
|
该包是技术组件,每个子包,代表一个组件。每个组件包括两部分: |
||||||
|
1. core 包:是该组件的核心封装 |
||||||
|
2. config 包:是该组件基于 Spring 的配置 |
||||||
|
|
||||||
|
技术组件,也分成两类: |
||||||
|
1. 框架组件:和我们熟悉的 MyBatis、Redis 等等的拓展 |
||||||
|
2. 业务组件:和业务相关的组件的封装,例如说数据字典、操作日志等等。 |
||||||
|
如果是业务组件,Maven 名字会包含 biz |
||||||
|
</description> |
||||||
|
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
||||||
|
|
||||||
|
</project> |
@ -0,0 +1,133 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" |
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||||
|
<parent> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-framework</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</parent> |
||||||
|
<modelVersion>4.0.0</modelVersion> |
||||||
|
<artifactId>yudao-common</artifactId> |
||||||
|
<packaging>jar</packaging> |
||||||
|
|
||||||
|
<name>${project.artifactId}</name> |
||||||
|
<description>定义基础 pojo 类、枚举、工具类等等</description> |
||||||
|
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
||||||
|
|
||||||
|
<dependencies> |
||||||
|
<!-- Spring 核心 --> |
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework</groupId> |
||||||
|
<artifactId>spring-core</artifactId> |
||||||
|
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework</groupId> |
||||||
|
<artifactId>spring-expression</artifactId> |
||||||
|
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework</groupId> |
||||||
|
<artifactId>spring-aop</artifactId> |
||||||
|
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.aspectj</groupId> |
||||||
|
<artifactId>aspectjweaver</artifactId> |
||||||
|
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<!-- 用于生成自定义的 Spring @ConfigurationProperties 配置类的说明文件 --> |
||||||
|
<groupId>org.springframework.boot</groupId> |
||||||
|
<artifactId>spring-boot-configuration-processor</artifactId> |
||||||
|
<optional>true</optional> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- Web 相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework</groupId> |
||||||
|
<artifactId>spring-web</artifactId> |
||||||
|
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>jakarta.servlet</groupId> |
||||||
|
<artifactId>jakarta.servlet-api</artifactId> |
||||||
|
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>io.swagger</groupId> |
||||||
|
<artifactId>swagger-annotations</artifactId> |
||||||
|
<scope>provided</scope> <!-- 设置为 provided,主要是 PageParam 使用到 --> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- 监控相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>org.apache.skywalking</groupId> |
||||||
|
<artifactId>apm-toolkit-trace</artifactId> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- 工具类相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>org.projectlombok</groupId> |
||||||
|
<artifactId>lombok</artifactId> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>org.mapstruct</groupId> |
||||||
|
<artifactId>mapstruct</artifactId> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.mapstruct</groupId> |
||||||
|
<artifactId>mapstruct-jdk8</artifactId> <!-- use mapstruct-jdk8 for Java 8 or higher --> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.mapstruct</groupId> |
||||||
|
<artifactId>mapstruct-processor</artifactId> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>com.google.guava</groupId> |
||||||
|
<artifactId>guava</artifactId> |
||||||
|
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>com.fasterxml.jackson.core</groupId> |
||||||
|
<artifactId>jackson-databind</artifactId> |
||||||
|
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>com.fasterxml.jackson.core</groupId> |
||||||
|
<artifactId>jackson-core</artifactId> |
||||||
|
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>org.slf4j</groupId> |
||||||
|
<artifactId>slf4j-api</artifactId> |
||||||
|
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>jakarta.validation</groupId> |
||||||
|
<artifactId>jakarta.validation-api</artifactId> |
||||||
|
<scope>provided</scope> <!-- 设置为 provided,主要是 PageParam 使用到 --> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>cn.hutool</groupId> |
||||||
|
<artifactId>hutool-all</artifactId> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>com.alibaba</groupId> |
||||||
|
<artifactId>transmittable-thread-local</artifactId> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
</dependencies> |
||||||
|
|
||||||
|
</project> |
@ -0,0 +1,15 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.core; |
||||||
|
|
||||||
|
/** |
||||||
|
* 可生成 Int 数组的接口 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public interface IntArrayValuable { |
||||||
|
|
||||||
|
/** |
||||||
|
* @return int 数组 |
||||||
|
*/ |
||||||
|
int[] array(); |
||||||
|
|
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.core; |
||||||
|
|
||||||
|
import lombok.AllArgsConstructor; |
||||||
|
import lombok.Data; |
||||||
|
import lombok.NoArgsConstructor; |
||||||
|
|
||||||
|
/** |
||||||
|
* Key Value 的键值对 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
@Data |
||||||
|
@NoArgsConstructor |
||||||
|
@AllArgsConstructor |
||||||
|
public class KeyValue<K, V> { |
||||||
|
|
||||||
|
private K key; |
||||||
|
private V value; |
||||||
|
|
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.enums; |
||||||
|
|
||||||
|
import lombok.AllArgsConstructor; |
||||||
|
import lombok.Getter; |
||||||
|
|
||||||
|
/** |
||||||
|
* 通用状态枚举 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
@Getter |
||||||
|
@AllArgsConstructor |
||||||
|
public enum CommonStatusEnum { |
||||||
|
|
||||||
|
ENABLE(0, "开启"), |
||||||
|
DISABLE(1, "关闭"); |
||||||
|
|
||||||
|
/** |
||||||
|
* 状态值 |
||||||
|
*/ |
||||||
|
private final Integer status; |
||||||
|
/** |
||||||
|
* 状态名 |
||||||
|
*/ |
||||||
|
private final String name; |
||||||
|
|
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.enums; |
||||||
|
|
||||||
|
import lombok.AllArgsConstructor; |
||||||
|
import lombok.Getter; |
||||||
|
|
||||||
|
/** |
||||||
|
* 文档地址 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
@Getter |
||||||
|
@AllArgsConstructor |
||||||
|
public enum DocumentEnum { |
||||||
|
|
||||||
|
REDIS_INSTALL("https://gitee.com/zhijiantianya/ruoyi-vue-pro/issues/I4VCSJ", "Redis 安装文档"); |
||||||
|
|
||||||
|
private final String url; |
||||||
|
private final String memo; |
||||||
|
|
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.enums; |
||||||
|
|
||||||
|
import cn.hutool.core.util.ArrayUtil; |
||||||
|
import cn.iocoder.yudao.framework.common.core.IntArrayValuable; |
||||||
|
import lombok.AllArgsConstructor; |
||||||
|
import lombok.Getter; |
||||||
|
|
||||||
|
import java.util.Arrays; |
||||||
|
|
||||||
|
/** |
||||||
|
* 全局用户类型枚举 |
||||||
|
*/ |
||||||
|
@AllArgsConstructor |
||||||
|
@Getter |
||||||
|
public enum UserTypeEnum implements IntArrayValuable { |
||||||
|
|
||||||
|
MEMBER(1, "会员"), // 面向 c 端,普通用户
|
||||||
|
ADMIN(2, "管理员"); // 面向 b 端,管理后台
|
||||||
|
|
||||||
|
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(UserTypeEnum::getValue).toArray(); |
||||||
|
|
||||||
|
/** |
||||||
|
* 类型 |
||||||
|
*/ |
||||||
|
private final Integer value; |
||||||
|
/** |
||||||
|
* 类型名 |
||||||
|
*/ |
||||||
|
private final String name; |
||||||
|
|
||||||
|
public static UserTypeEnum valueOf(Integer value) { |
||||||
|
return ArrayUtil.firstMatch(userType -> userType.getValue().equals(value), UserTypeEnum.values()); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int[] array() { |
||||||
|
return ARRAYS; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.enums; |
||||||
|
|
||||||
|
/** |
||||||
|
* Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期 |
||||||
|
* |
||||||
|
* 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 util 包下 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public interface WebFilterOrderEnum { |
||||||
|
|
||||||
|
int CORS_FILTER = Integer.MIN_VALUE; |
||||||
|
|
||||||
|
int TRACE_FILTER = CORS_FILTER + 1; |
||||||
|
|
||||||
|
int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500; |
||||||
|
|
||||||
|
// OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等
|
||||||
|
|
||||||
|
int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面
|
||||||
|
|
||||||
|
int API_ACCESS_LOG_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面
|
||||||
|
|
||||||
|
int XSS_FILTER = -102; // 需要保证在 RequestBodyCacheFilter 后面
|
||||||
|
|
||||||
|
// Spring Security Filter 默认为 -100,可见 org.springframework.boot.autoconfigure.security.SecurityProperties 配置属性类
|
||||||
|
|
||||||
|
int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器后面
|
||||||
|
|
||||||
|
int ACTIVITI_FILTER = -98; // 需要保证在 Spring Security 过滤后面
|
||||||
|
|
||||||
|
int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面
|
||||||
|
|
||||||
|
int DEMO_FILTER = Integer.MAX_VALUE; |
||||||
|
|
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.exception; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; |
||||||
|
import cn.iocoder.yudao.framework.common.exception.enums.ServiceErrorCodeRange; |
||||||
|
import lombok.Data; |
||||||
|
|
||||||
|
/** |
||||||
|
* 错误码对象 |
||||||
|
* |
||||||
|
* 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants} |
||||||
|
* 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange} |
||||||
|
* |
||||||
|
* TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备 |
||||||
|
*/ |
||||||
|
@Data |
||||||
|
public class ErrorCode { |
||||||
|
|
||||||
|
/** |
||||||
|
* 错误码 |
||||||
|
*/ |
||||||
|
private final Integer code; |
||||||
|
/** |
||||||
|
* 错误提示 |
||||||
|
*/ |
||||||
|
private final String msg; |
||||||
|
|
||||||
|
public ErrorCode(Integer code, String message) { |
||||||
|
this.code = code; |
||||||
|
this.msg = message; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,60 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.exception; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.exception.enums.ServiceErrorCodeRange; |
||||||
|
import lombok.Data; |
||||||
|
import lombok.EqualsAndHashCode; |
||||||
|
|
||||||
|
/** |
||||||
|
* 业务逻辑异常 Exception |
||||||
|
*/ |
||||||
|
@Data |
||||||
|
@EqualsAndHashCode(callSuper = true) |
||||||
|
public final class ServiceException extends RuntimeException { |
||||||
|
|
||||||
|
/** |
||||||
|
* 业务错误码 |
||||||
|
* |
||||||
|
* @see ServiceErrorCodeRange |
||||||
|
*/ |
||||||
|
private Integer code; |
||||||
|
/** |
||||||
|
* 错误提示 |
||||||
|
*/ |
||||||
|
private String message; |
||||||
|
|
||||||
|
/** |
||||||
|
* 空构造方法,避免反序列化问题 |
||||||
|
*/ |
||||||
|
public ServiceException() { |
||||||
|
} |
||||||
|
|
||||||
|
public ServiceException(ErrorCode errorCode) { |
||||||
|
this.code = errorCode.getCode(); |
||||||
|
this.message = errorCode.getMsg(); |
||||||
|
} |
||||||
|
|
||||||
|
public ServiceException(Integer code, String message) { |
||||||
|
this.code = code; |
||||||
|
this.message = message; |
||||||
|
} |
||||||
|
|
||||||
|
public Integer getCode() { |
||||||
|
return code; |
||||||
|
} |
||||||
|
|
||||||
|
public ServiceException setCode(Integer code) { |
||||||
|
this.code = code; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getMessage() { |
||||||
|
return message; |
||||||
|
} |
||||||
|
|
||||||
|
public ServiceException setMessage(String message) { |
||||||
|
this.message = message; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,44 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.exception.enums; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.exception.ErrorCode; |
||||||
|
|
||||||
|
/** |
||||||
|
* 全局错误码枚举 |
||||||
|
* 0-999 系统异常编码保留 |
||||||
|
* |
||||||
|
* 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
|
||||||
|
* 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的 |
||||||
|
* 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public interface GlobalErrorCodeConstants { |
||||||
|
|
||||||
|
ErrorCode SUCCESS = new ErrorCode(0, "成功"); |
||||||
|
|
||||||
|
// ========== 客户端错误段 ==========
|
||||||
|
|
||||||
|
ErrorCode BAD_REQUEST = new ErrorCode(400, "请求参数不正确"); |
||||||
|
ErrorCode UNAUTHORIZED = new ErrorCode(401, "账号未登录"); |
||||||
|
ErrorCode FORBIDDEN = new ErrorCode(403, "没有该操作权限"); |
||||||
|
ErrorCode NOT_FOUND = new ErrorCode(404, "请求未找到"); |
||||||
|
ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确"); |
||||||
|
ErrorCode LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许
|
||||||
|
ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试"); |
||||||
|
|
||||||
|
// ========== 服务端错误段 ==========
|
||||||
|
|
||||||
|
ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常"); |
||||||
|
|
||||||
|
// ========== 自定义错误段 ==========
|
||||||
|
ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求
|
||||||
|
ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作"); |
||||||
|
|
||||||
|
ErrorCode UNKNOWN = new ErrorCode(999, "未知错误"); |
||||||
|
|
||||||
|
static boolean isMatch(Integer code) { |
||||||
|
return code != null |
||||||
|
&& code >= SUCCESS.getCode() && code <= UNKNOWN.getCode(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.exception.enums; |
||||||
|
|
||||||
|
/** |
||||||
|
* 业务异常的错误码区间,解决:解决各模块错误码定义,避免重复,在此只声明不做实际使用 |
||||||
|
* |
||||||
|
* 一共 10 位,分成四段 |
||||||
|
* |
||||||
|
* 第一段,1 位,类型 |
||||||
|
* 1 - 业务级别异常 |
||||||
|
* x - 预留 |
||||||
|
* 第二段,3 位,系统类型 |
||||||
|
* 001 - 用户系统 |
||||||
|
* 002 - 商品系统 |
||||||
|
* 003 - 订单系统 |
||||||
|
* 004 - 支付系统 |
||||||
|
* 005 - 优惠劵系统 |
||||||
|
* ... - ... |
||||||
|
* 第三段,3 位,模块 |
||||||
|
* 不限制规则。 |
||||||
|
* 一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子: |
||||||
|
* 001 - OAuth2 模块 |
||||||
|
* 002 - User 模块 |
||||||
|
* 003 - MobileCode 模块 |
||||||
|
* 第四段,3 位,错误码 |
||||||
|
* 不限制规则。 |
||||||
|
* 一般建议,每个模块自增。 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class ServiceErrorCodeRange { |
||||||
|
|
||||||
|
// 模块 system 错误码区间 [1-000-001-000 ~ 1-000-002-000]
|
||||||
|
|
||||||
|
} |
@ -0,0 +1,122 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.exception.util; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.exception.ErrorCode; |
||||||
|
import cn.iocoder.yudao.framework.common.exception.ServiceException; |
||||||
|
import com.google.common.annotations.VisibleForTesting; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
|
||||||
|
import java.util.Map; |
||||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||||
|
import java.util.concurrent.ConcurrentMap; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link ServiceException} 工具类 |
||||||
|
* |
||||||
|
* 目的在于,格式化异常信息提示。 |
||||||
|
* 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化 |
||||||
|
* |
||||||
|
* 因为 {@link #MESSAGES} 里面默认是没有异常信息提示的模板的,所以需要使用方自己初始化进去。目前想到的有几种方式: |
||||||
|
* |
||||||
|
* 1. 异常提示信息,写在枚举类中,例如说,cn.iocoder.oceans.user.api.constants.ErrorCodeEnum 类 + ServiceExceptionConfiguration |
||||||
|
* 2. 异常提示信息,写在 .properties 等等配置文件 |
||||||
|
* 3. 异常提示信息,写在 Apollo 等等配置中心中,从而实现可动态刷新 |
||||||
|
* 4. 异常提示信息,存储在 db 等等数据库中,从而实现可动态刷新 |
||||||
|
*/ |
||||||
|
@Slf4j |
||||||
|
public class ServiceExceptionUtil { |
||||||
|
|
||||||
|
/** |
||||||
|
* 错误码提示模板 |
||||||
|
*/ |
||||||
|
private static final ConcurrentMap<Integer, String> MESSAGES = new ConcurrentHashMap<>(); |
||||||
|
|
||||||
|
public static void putAll(Map<Integer, String> messages) { |
||||||
|
ServiceExceptionUtil.MESSAGES.putAll(messages); |
||||||
|
} |
||||||
|
|
||||||
|
public static void put(Integer code, String message) { |
||||||
|
ServiceExceptionUtil.MESSAGES.put(code, message); |
||||||
|
} |
||||||
|
|
||||||
|
public static void delete(Integer code, String message) { |
||||||
|
ServiceExceptionUtil.MESSAGES.remove(code, message); |
||||||
|
} |
||||||
|
|
||||||
|
// ========== 和 ServiceException 的集成 ==========
|
||||||
|
|
||||||
|
public static ServiceException exception(ErrorCode errorCode) { |
||||||
|
String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg()); |
||||||
|
return exception0(errorCode.getCode(), messagePattern); |
||||||
|
} |
||||||
|
|
||||||
|
public static ServiceException exception(ErrorCode errorCode, Object... params) { |
||||||
|
String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg()); |
||||||
|
return exception0(errorCode.getCode(), messagePattern, params); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 创建指定编号的 ServiceException 的异常 |
||||||
|
* |
||||||
|
* @param code 编号 |
||||||
|
* @return 异常 |
||||||
|
*/ |
||||||
|
public static ServiceException exception(Integer code) { |
||||||
|
return exception0(code, MESSAGES.get(code)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 创建指定编号的 ServiceException 的异常 |
||||||
|
* |
||||||
|
* @param code 编号 |
||||||
|
* @param params 消息提示的占位符对应的参数 |
||||||
|
* @return 异常 |
||||||
|
*/ |
||||||
|
public static ServiceException exception(Integer code, Object... params) { |
||||||
|
return exception0(code, MESSAGES.get(code), params); |
||||||
|
} |
||||||
|
|
||||||
|
public static ServiceException exception0(Integer code, String messagePattern, Object... params) { |
||||||
|
String message = doFormat(code, messagePattern, params); |
||||||
|
return new ServiceException(code, message); |
||||||
|
} |
||||||
|
|
||||||
|
// ========== 格式化方法 ==========
|
||||||
|
|
||||||
|
/** |
||||||
|
* 将错误编号对应的消息使用 params 进行格式化。 |
||||||
|
* |
||||||
|
* @param code 错误编号 |
||||||
|
* @param messagePattern 消息模版 |
||||||
|
* @param params 参数 |
||||||
|
* @return 格式化后的提示 |
||||||
|
*/ |
||||||
|
@VisibleForTesting |
||||||
|
public static String doFormat(int code, String messagePattern, Object... params) { |
||||||
|
StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50); |
||||||
|
int i = 0; |
||||||
|
int j; |
||||||
|
int l; |
||||||
|
for (l = 0; l < params.length; l++) { |
||||||
|
j = messagePattern.indexOf("{}", i); |
||||||
|
if (j == -1) { |
||||||
|
log.error("[doFormat][参数过多:错误码({})|错误内容({})|参数({})", code, messagePattern, params); |
||||||
|
if (i == 0) { |
||||||
|
return messagePattern; |
||||||
|
} else { |
||||||
|
sbuf.append(messagePattern.substring(i)); |
||||||
|
return sbuf.toString(); |
||||||
|
} |
||||||
|
} else { |
||||||
|
sbuf.append(messagePattern, i, j); |
||||||
|
sbuf.append(params[l]); |
||||||
|
i = j + 2; |
||||||
|
} |
||||||
|
} |
||||||
|
if (messagePattern.indexOf("{}", i) != -1) { |
||||||
|
log.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params); |
||||||
|
} |
||||||
|
sbuf.append(messagePattern.substring(i)); |
||||||
|
return sbuf.toString(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
/** |
||||||
|
* 基础的通用类,和框架无关 |
||||||
|
* |
||||||
|
* 例如说,CommonResult 为通用返回 |
||||||
|
*/ |
||||||
|
package cn.iocoder.yudao.framework.common; |
@ -0,0 +1,102 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.pojo; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.exception.ErrorCode; |
||||||
|
import cn.iocoder.yudao.framework.common.exception.ServiceException; |
||||||
|
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; |
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore; |
||||||
|
import lombok.Data; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
|
||||||
|
import java.io.Serializable; |
||||||
|
import java.util.Objects; |
||||||
|
|
||||||
|
/** |
||||||
|
* 通用返回 |
||||||
|
* |
||||||
|
* @param <T> 数据泛型 |
||||||
|
*/ |
||||||
|
@Data |
||||||
|
public class CommonResult<T> implements Serializable { |
||||||
|
|
||||||
|
/** |
||||||
|
* 错误码 |
||||||
|
* |
||||||
|
* @see ErrorCode#getCode() |
||||||
|
*/ |
||||||
|
private Integer code; |
||||||
|
/** |
||||||
|
* 返回数据 |
||||||
|
*/ |
||||||
|
private T data; |
||||||
|
/** |
||||||
|
* 错误提示,用户可阅读 |
||||||
|
* |
||||||
|
* @see ErrorCode#getMsg() () |
||||||
|
*/ |
||||||
|
private String msg; |
||||||
|
|
||||||
|
/** |
||||||
|
* 将传入的 result 对象,转换成另外一个泛型结果的对象 |
||||||
|
* |
||||||
|
* 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。 |
||||||
|
* |
||||||
|
* @param result 传入的 result 对象 |
||||||
|
* @param <T> 返回的泛型 |
||||||
|
* @return 新的 CommonResult 对象 |
||||||
|
*/ |
||||||
|
public static <T> CommonResult<T> error(CommonResult<?> result) { |
||||||
|
return error(result.getCode(), result.getMsg()); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T> CommonResult<T> error(Integer code, String message) { |
||||||
|
Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code), "code 必须是错误的!"); |
||||||
|
CommonResult<T> result = new CommonResult<>(); |
||||||
|
result.code = code; |
||||||
|
result.msg = message; |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
public static <T> CommonResult<T> error(ErrorCode errorCode) { |
||||||
|
return error(errorCode.getCode(), errorCode.getMsg()); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T> CommonResult<T> success(T data) { |
||||||
|
CommonResult<T> result = new CommonResult<>(); |
||||||
|
result.code = GlobalErrorCodeConstants.SUCCESS.getCode(); |
||||||
|
result.data = data; |
||||||
|
result.msg = ""; |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean isSuccess(Integer code) { |
||||||
|
return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode()); |
||||||
|
} |
||||||
|
|
||||||
|
@JsonIgnore // 避免 jackson 序列化
|
||||||
|
public boolean isSuccess() { |
||||||
|
return isSuccess(code); |
||||||
|
} |
||||||
|
|
||||||
|
@JsonIgnore // 避免 jackson 序列化
|
||||||
|
public boolean isError() { |
||||||
|
return !isSuccess(); |
||||||
|
} |
||||||
|
|
||||||
|
// ========= 和 Exception 异常体系集成 =========
|
||||||
|
|
||||||
|
/** |
||||||
|
* 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常 |
||||||
|
*/ |
||||||
|
public void checkError() throws ServiceException { |
||||||
|
if (isSuccess()) { |
||||||
|
return; |
||||||
|
} |
||||||
|
// 业务异常
|
||||||
|
throw new ServiceException(code, msg); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T> CommonResult<T> error(ServiceException serviceException) { |
||||||
|
return error(serviceException.getCode(), serviceException.getMessage()); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.pojo; |
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel; |
||||||
|
import io.swagger.annotations.ApiModelProperty; |
||||||
|
import lombok.Data; |
||||||
|
|
||||||
|
import javax.validation.constraints.Min; |
||||||
|
import javax.validation.constraints.Max; |
||||||
|
import javax.validation.constraints.NotNull; |
||||||
|
import java.io.Serializable; |
||||||
|
|
||||||
|
@ApiModel("分页参数") |
||||||
|
@Data |
||||||
|
public class PageParam implements Serializable { |
||||||
|
|
||||||
|
private static final Integer PAGE_NO = 1; |
||||||
|
private static final Integer PAGE_SIZE = 10; |
||||||
|
|
||||||
|
@ApiModelProperty(value = "页码,从 1 开始", required = true,example = "1") |
||||||
|
@NotNull(message = "页码不能为空") |
||||||
|
@Min(value = 1, message = "页码最小值为 1") |
||||||
|
private Integer pageNo = PAGE_NO; |
||||||
|
|
||||||
|
@ApiModelProperty(value = "每页条数,最大值为 100", required = true, example = "10") |
||||||
|
@NotNull(message = "每页条数不能为空") |
||||||
|
@Min(value = 1, message = "页码最小值为 1") |
||||||
|
@Max(value = 100, message = "页码最大值为 100") |
||||||
|
private Integer pageSize = PAGE_SIZE; |
||||||
|
|
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.pojo; |
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel; |
||||||
|
import io.swagger.annotations.ApiModelProperty; |
||||||
|
import lombok.Data; |
||||||
|
|
||||||
|
import java.io.Serializable; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
@ApiModel("分页结果") |
||||||
|
@Data |
||||||
|
public final class PageResult<T> implements Serializable { |
||||||
|
|
||||||
|
@ApiModelProperty(value = "数据", required = true) |
||||||
|
private List<T> list; |
||||||
|
|
||||||
|
@ApiModelProperty(value = "总量", required = true) |
||||||
|
private Long total; |
||||||
|
|
||||||
|
public PageResult() { |
||||||
|
} |
||||||
|
|
||||||
|
public PageResult(List<T> list, Long total) { |
||||||
|
this.list = list; |
||||||
|
this.total = total; |
||||||
|
} |
||||||
|
|
||||||
|
public PageResult(Long total) { |
||||||
|
this.list = new ArrayList<>(); |
||||||
|
this.total = total; |
||||||
|
} |
||||||
|
|
||||||
|
public static <T> PageResult<T> empty() { |
||||||
|
return new PageResult<>(0L); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T> PageResult<T> empty(Long total) { |
||||||
|
return new PageResult<>(total); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,56 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.pojo; |
||||||
|
|
||||||
|
import java.io.Serializable; |
||||||
|
|
||||||
|
/** |
||||||
|
* 排序字段 DTO |
||||||
|
* |
||||||
|
* 类名加了 ing 的原因是,避免和 ES SortField 重名。 |
||||||
|
*/ |
||||||
|
public class SortingField implements Serializable { |
||||||
|
|
||||||
|
/** |
||||||
|
* 顺序 - 升序 |
||||||
|
*/ |
||||||
|
public static final String ORDER_ASC = "asc"; |
||||||
|
/** |
||||||
|
* 顺序 - 降序 |
||||||
|
*/ |
||||||
|
public static final String ORDER_DESC = "desc"; |
||||||
|
|
||||||
|
/** |
||||||
|
* 字段 |
||||||
|
*/ |
||||||
|
private String field; |
||||||
|
/** |
||||||
|
* 顺序 |
||||||
|
*/ |
||||||
|
private String order; |
||||||
|
|
||||||
|
// 空构造方法,解决反序列化
|
||||||
|
public SortingField() { |
||||||
|
} |
||||||
|
|
||||||
|
public SortingField(String field, String order) { |
||||||
|
this.field = field; |
||||||
|
this.order = order; |
||||||
|
} |
||||||
|
|
||||||
|
public String getField() { |
||||||
|
return field; |
||||||
|
} |
||||||
|
|
||||||
|
public SortingField setField(String field) { |
||||||
|
this.field = field; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
public String getOrder() { |
||||||
|
return order; |
||||||
|
} |
||||||
|
|
||||||
|
public SortingField setOrder(String order) { |
||||||
|
this.order = order; |
||||||
|
return this; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,56 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.util.collection; |
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollectionUtil; |
||||||
|
import cn.hutool.core.util.ArrayUtil; |
||||||
|
import cn.hutool.core.util.TypeUtil; |
||||||
|
import org.springframework.cglib.core.TypeUtils; |
||||||
|
|
||||||
|
import java.lang.reflect.Array; |
||||||
|
import java.lang.reflect.ParameterizedType; |
||||||
|
import java.lang.reflect.Type; |
||||||
|
import java.util.Collection; |
||||||
|
import java.util.List; |
||||||
|
import java.util.function.Consumer; |
||||||
|
import java.util.function.Function; |
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; |
||||||
|
|
||||||
|
/** |
||||||
|
* Array 工具类 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class ArrayUtils { |
||||||
|
|
||||||
|
/** |
||||||
|
* 将 object 和 newElements 合并成一个数组 |
||||||
|
* |
||||||
|
* @param object 对象 |
||||||
|
* @param newElements 数组 |
||||||
|
* @param <T> 泛型 |
||||||
|
* @return 结果数组 |
||||||
|
*/ |
||||||
|
@SafeVarargs |
||||||
|
public static <T> Consumer<T>[] append(Consumer<T> object, Consumer<T>... newElements) { |
||||||
|
if (object == null) { |
||||||
|
return newElements; |
||||||
|
} |
||||||
|
Consumer<T>[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length); |
||||||
|
result[0] = object; |
||||||
|
System.arraycopy(newElements, 0, result, 1, newElements.length); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
public static <T, V> V[] toArray(Collection<T> from, Function<T, V> mapper) { |
||||||
|
return toArray(convertList(from, mapper)); |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
public static <T> T[] toArray(Collection<T> from) { |
||||||
|
if (CollectionUtil.isEmpty(from)) { |
||||||
|
return (T[]) (new Object[0]); |
||||||
|
} |
||||||
|
return ArrayUtil.toArray(from, (Class<T>) CollectionUtil.getElementType(from.iterator())); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,173 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.util.collection; |
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil; |
||||||
|
import cn.hutool.core.collection.CollectionUtil; |
||||||
|
import com.google.common.collect.ImmutableMap; |
||||||
|
|
||||||
|
import java.util.*; |
||||||
|
import java.util.function.BinaryOperator; |
||||||
|
import java.util.function.Function; |
||||||
|
import java.util.function.Predicate; |
||||||
|
import java.util.function.Supplier; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
/** |
||||||
|
* Collection 工具类 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class CollectionUtils { |
||||||
|
|
||||||
|
public static boolean containsAny(Object source, Object... targets) { |
||||||
|
return Arrays.asList(targets).contains(source); |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean isAnyEmpty(Collection<?>... collections) { |
||||||
|
return Arrays.stream(collections).anyMatch(CollectionUtil::isEmpty); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T> List<T> filterList(Collection<T> from, Predicate<T> predicate) { |
||||||
|
if (CollUtil.isEmpty(from)) { |
||||||
|
return new ArrayList<>(); |
||||||
|
} |
||||||
|
return from.stream().filter(predicate).collect(Collectors.toList()); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper) { |
||||||
|
if (CollUtil.isEmpty(from)) { |
||||||
|
return new ArrayList<>(); |
||||||
|
} |
||||||
|
return distinct(from, keyMapper, (t1, t2) -> t1); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T, R> List<T> distinct(Collection<T> from, Function<T, R> keyMapper, BinaryOperator<T> cover) { |
||||||
|
if (CollUtil.isEmpty(from)) { |
||||||
|
return new ArrayList<>(); |
||||||
|
} |
||||||
|
return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values()); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T, U> List<U> convertList(Collection<T> from, Function<T, U> func) { |
||||||
|
if (CollUtil.isEmpty(from)) { |
||||||
|
return new ArrayList<>(); |
||||||
|
} |
||||||
|
return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toList()); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T, U> Set<U> convertSet(Collection<T> from, Function<T, U> func) { |
||||||
|
if (CollUtil.isEmpty(from)) { |
||||||
|
return new HashSet<>(); |
||||||
|
} |
||||||
|
return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet()); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc) { |
||||||
|
if (CollUtil.isEmpty(from)) { |
||||||
|
return new HashMap<>(); |
||||||
|
} |
||||||
|
return convertMap(from, keyFunc, Function.identity()); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T, K> Map<K, T> convertMap(Collection<T> from, Function<T, K> keyFunc, Supplier<? extends Map<K, T>> supplier) { |
||||||
|
if (CollUtil.isEmpty(from)) { |
||||||
|
return supplier.get(); |
||||||
|
} |
||||||
|
return convertMap(from, keyFunc, Function.identity(), supplier); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) { |
||||||
|
if (CollUtil.isEmpty(from)) { |
||||||
|
return new HashMap<>(); |
||||||
|
} |
||||||
|
return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction) { |
||||||
|
if (CollUtil.isEmpty(from)) { |
||||||
|
return new HashMap<>(); |
||||||
|
} |
||||||
|
return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, Supplier<? extends Map<K, V>> supplier) { |
||||||
|
if (CollUtil.isEmpty(from)) { |
||||||
|
return supplier.get(); |
||||||
|
} |
||||||
|
return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T, K, V> Map<K, V> convertMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc, BinaryOperator<V> mergeFunction, Supplier<? extends Map<K, V>> supplier) { |
||||||
|
if (CollUtil.isEmpty(from)) { |
||||||
|
return new HashMap<>(); |
||||||
|
} |
||||||
|
return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier)); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T, K> Map<K, List<T>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc) { |
||||||
|
if (CollUtil.isEmpty(from)) { |
||||||
|
return new HashMap<>(); |
||||||
|
} |
||||||
|
return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList()))); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T, K, V> Map<K, List<V>> convertMultiMap(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) { |
||||||
|
if (CollUtil.isEmpty(from)) { |
||||||
|
return new HashMap<>(); |
||||||
|
} |
||||||
|
return from.stream() |
||||||
|
.collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList()))); |
||||||
|
} |
||||||
|
|
||||||
|
// 暂时没想好名字,先以 2 结尾噶
|
||||||
|
public static <T, K, V> Map<K, Set<V>> convertMultiMap2(Collection<T> from, Function<T, K> keyFunc, Function<T, V> valueFunc) { |
||||||
|
if (CollUtil.isEmpty(from)) { |
||||||
|
return new HashMap<>(); |
||||||
|
} |
||||||
|
return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet()))); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T, K> Map<K, T> convertImmutableMap(Collection<T> from, Function<T, K> keyFunc) { |
||||||
|
if (CollUtil.isEmpty(from)) { |
||||||
|
return Collections.emptyMap(); |
||||||
|
} |
||||||
|
ImmutableMap.Builder<K, T> builder = ImmutableMap.builder(); |
||||||
|
from.forEach(item -> builder.put(keyFunc.apply(item), item)); |
||||||
|
return builder.build(); |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean containsAny(Collection<?> source, Collection<?> candidates) { |
||||||
|
return org.springframework.util.CollectionUtils.containsAny(source, candidates); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T> T getFirst(List<T> from) { |
||||||
|
return !CollectionUtil.isEmpty(from) ? from.get(0) : null; |
||||||
|
} |
||||||
|
|
||||||
|
public static <T> T findFirst(List<T> from, Predicate<T> predicate) { |
||||||
|
if (CollUtil.isEmpty(from)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return from.stream().filter(predicate).findFirst().orElse(null); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T, V extends Comparable<? super V>> V getMaxValue(List<T> from, Function<T, V> valueFunc) { |
||||||
|
if (CollUtil.isEmpty(from)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
assert from.size() > 0; // 断言,避免告警
|
||||||
|
T t = from.stream().max(Comparator.comparing(valueFunc)).get(); |
||||||
|
return valueFunc.apply(t); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T> void addIfNotNull(Collection<T> coll, T item) { |
||||||
|
if (item == null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
coll.add(item); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T> Collection<T> singleton(T deptId) { |
||||||
|
return deptId == null ? Collections.emptyList() : Collections.singleton(deptId); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,66 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.util.collection; |
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil; |
||||||
|
import cn.hutool.core.collection.CollectionUtil; |
||||||
|
import cn.iocoder.yudao.framework.common.core.KeyValue; |
||||||
|
import com.google.common.collect.Maps; |
||||||
|
import com.google.common.collect.Multimap; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Collection; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.function.Consumer; |
||||||
|
|
||||||
|
/** |
||||||
|
* Map 工具类 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class MapUtils { |
||||||
|
|
||||||
|
/** |
||||||
|
* 从哈希表表中,获得 keys 对应的所有 value 数组 |
||||||
|
* |
||||||
|
* @param multimap 哈希表 |
||||||
|
* @param keys keys |
||||||
|
* @return value 数组 |
||||||
|
*/ |
||||||
|
public static <K, V> List<V> getList(Multimap<K, V> multimap, Collection<K> keys) { |
||||||
|
List<V> result = new ArrayList<>(); |
||||||
|
keys.forEach(k -> { |
||||||
|
Collection<V> values = multimap.get(k); |
||||||
|
if (CollectionUtil.isEmpty(values)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
result.addAll(values); |
||||||
|
}); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 从哈希表查找到 key 对应的 value,然后进一步处理 |
||||||
|
* 注意,如果查找到的 value 为 null 时,不进行处理 |
||||||
|
* |
||||||
|
* @param map 哈希表 |
||||||
|
* @param key key |
||||||
|
* @param consumer 进一步处理的逻辑 |
||||||
|
*/ |
||||||
|
public static <K, V> void findAndThen(Map<K, V> map, K key, Consumer<V> consumer) { |
||||||
|
if (CollUtil.isEmpty(map)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
V value = map.get(key); |
||||||
|
if (value == null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
consumer.accept(value); |
||||||
|
} |
||||||
|
|
||||||
|
public static <K, V> Map<K, V> convertMap(List<KeyValue<K, V>> keyValues) { |
||||||
|
Map<K, V> map = Maps.newLinkedHashMapWithExpectedSize(keyValues.size()); |
||||||
|
keyValues.forEach(keyValue -> map.put(keyValue.getKey(), keyValue.getValue())); |
||||||
|
return map; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.util.collection; |
||||||
|
|
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.HashSet; |
||||||
|
import java.util.Set; |
||||||
|
|
||||||
|
/** |
||||||
|
* Set 工具类 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class SetUtils { |
||||||
|
|
||||||
|
public static <T> Set<T> asSet(T... objs) { |
||||||
|
return new HashSet<>(Arrays.asList(objs)); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,123 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.util.date; |
||||||
|
|
||||||
|
import java.time.Duration; |
||||||
|
import java.util.Calendar; |
||||||
|
import java.util.Date; |
||||||
|
|
||||||
|
/** |
||||||
|
* 时间工具类 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class DateUtils { |
||||||
|
|
||||||
|
/** |
||||||
|
* 时区 - 默认 |
||||||
|
*/ |
||||||
|
public static final String TIME_ZONE_DEFAULT = "GMT+8"; |
||||||
|
|
||||||
|
/** |
||||||
|
* 秒转换成毫秒 |
||||||
|
*/ |
||||||
|
public static final long SECOND_MILLIS = 1000; |
||||||
|
|
||||||
|
public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss"; |
||||||
|
|
||||||
|
public static Date addTime(Duration duration) { |
||||||
|
return new Date(System.currentTimeMillis() + duration.toMillis()); |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean isExpired(Date time) { |
||||||
|
return System.currentTimeMillis() > time.getTime(); |
||||||
|
} |
||||||
|
|
||||||
|
public static long diff(Date endTime, Date startTime) { |
||||||
|
return endTime.getTime() - startTime.getTime(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 创建指定时间 |
||||||
|
* |
||||||
|
* @param year 年 |
||||||
|
* @param mouth 月 |
||||||
|
* @param day 日 |
||||||
|
* @return 指定时间 |
||||||
|
*/ |
||||||
|
public static Date buildTime(int year, int mouth, int day) { |
||||||
|
return buildTime(year, mouth, day, 0, 0, 0); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 创建指定时间 |
||||||
|
* |
||||||
|
* @param year 年 |
||||||
|
* @param mouth 月 |
||||||
|
* @param day 日 |
||||||
|
* @param hour 小时 |
||||||
|
* @param minute 分钟 |
||||||
|
* @param second 秒 |
||||||
|
* @return 指定时间 |
||||||
|
*/ |
||||||
|
public static Date buildTime(int year, int mouth, int day, |
||||||
|
int hour, int minute, int second) { |
||||||
|
Calendar calendar = Calendar.getInstance(); |
||||||
|
calendar.set(Calendar.YEAR, year); |
||||||
|
calendar.set(Calendar.MONTH, mouth - 1); |
||||||
|
calendar.set(Calendar.DAY_OF_MONTH, day); |
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, hour); |
||||||
|
calendar.set(Calendar.MINUTE, minute); |
||||||
|
calendar.set(Calendar.SECOND, second); |
||||||
|
calendar.set(Calendar.MILLISECOND, 0); // 一般情况下,都是 0 毫秒
|
||||||
|
return calendar.getTime(); |
||||||
|
} |
||||||
|
|
||||||
|
public static Date max(Date a, Date b) { |
||||||
|
if (a == null) { |
||||||
|
return b; |
||||||
|
} |
||||||
|
if (b == null) { |
||||||
|
return a; |
||||||
|
} |
||||||
|
return a.compareTo(b) > 0 ? a : b; |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean beforeNow(Date date) { |
||||||
|
return date.getTime() < System.currentTimeMillis(); |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean afterNow(Date date) { |
||||||
|
return date.getTime() >= System.currentTimeMillis(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 计算当期时间相差的日期 |
||||||
|
* |
||||||
|
* @param field 日历字段.<br/>eg:Calendar.MONTH,Calendar.DAY_OF_MONTH,<br/>Calendar.HOUR_OF_DAY等. |
||||||
|
* @param amount 相差的数值 |
||||||
|
* @return 计算后的日志 |
||||||
|
*/ |
||||||
|
public static Date addDate(int field, int amount) { |
||||||
|
return addDate(null, field, amount); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 计算当期时间相差的日期 |
||||||
|
* |
||||||
|
* @param date 设置时间 |
||||||
|
* @param field 日历字段 例如说,{@link Calendar#DAY_OF_MONTH} 等 |
||||||
|
* @param amount 相差的数值 |
||||||
|
* @return 计算后的日志 |
||||||
|
*/ |
||||||
|
public static Date addDate(Date date, int field, int amount) { |
||||||
|
if (amount == 0) { |
||||||
|
return date; |
||||||
|
} |
||||||
|
Calendar c = Calendar.getInstance(); |
||||||
|
if (date != null) { |
||||||
|
c.setTime(date); |
||||||
|
} |
||||||
|
c.add(field, amount); |
||||||
|
return c.getTime(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.util.http; |
||||||
|
|
||||||
|
import cn.hutool.core.map.TableMap; |
||||||
|
import cn.hutool.core.net.url.UrlBuilder; |
||||||
|
import cn.hutool.core.util.ReflectUtil; |
||||||
|
|
||||||
|
import java.nio.charset.Charset; |
||||||
|
|
||||||
|
/** |
||||||
|
* HTTP 工具类 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class HttpUtils { |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
public static String replaceUrlQuery(String url, String key, String value) { |
||||||
|
UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset()); |
||||||
|
// 先移除
|
||||||
|
TableMap<CharSequence, CharSequence> query = (TableMap<CharSequence, CharSequence>) |
||||||
|
ReflectUtil.getFieldValue(builder.getQuery(), "query"); |
||||||
|
query.remove(key); |
||||||
|
// 后添加
|
||||||
|
builder.addQuery(key, value); |
||||||
|
return builder.build(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,61 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.util.io; |
||||||
|
|
||||||
|
import cn.hutool.core.io.FileUtil; |
||||||
|
import cn.hutool.core.util.IdUtil; |
||||||
|
import lombok.SneakyThrows; |
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
|
||||||
|
/** |
||||||
|
* 文件工具类 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class FileUtils { |
||||||
|
|
||||||
|
/** |
||||||
|
* 创建临时文件 |
||||||
|
* 该文件会在 JVM 退出时,进行删除 |
||||||
|
* |
||||||
|
* @param data 文件内容 |
||||||
|
* @return 文件 |
||||||
|
*/ |
||||||
|
@SneakyThrows |
||||||
|
public static File createTempFile(String data) { |
||||||
|
File file = createTempFile(); |
||||||
|
// 写入内容
|
||||||
|
FileUtil.writeUtf8String(data, file); |
||||||
|
return file; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 创建临时文件 |
||||||
|
* 该文件会在 JVM 退出时,进行删除 |
||||||
|
* |
||||||
|
* @param data 文件内容 |
||||||
|
* @return 文件 |
||||||
|
*/ |
||||||
|
@SneakyThrows |
||||||
|
public static File createTempFile(byte[] data) { |
||||||
|
File file = createTempFile(); |
||||||
|
// 写入内容
|
||||||
|
FileUtil.writeBytes(data, file); |
||||||
|
return file; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 创建临时文件,无内容 |
||||||
|
* 该文件会在 JVM 退出时,进行删除 |
||||||
|
* |
||||||
|
* @return 文件 |
||||||
|
*/ |
||||||
|
@SneakyThrows |
||||||
|
public static File createTempFile() { |
||||||
|
// 创建文件,通过 UUID 保证唯一
|
||||||
|
File file = File.createTempFile(IdUtil.simpleUUID(), null); |
||||||
|
// 标记 JVM 退出时,自动删除
|
||||||
|
file.deleteOnExit(); |
||||||
|
return file; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.util.io; |
||||||
|
|
||||||
|
import cn.hutool.core.io.IORuntimeException; |
||||||
|
import cn.hutool.core.io.IoUtil; |
||||||
|
import cn.hutool.core.util.StrUtil; |
||||||
|
|
||||||
|
import java.io.InputStream; |
||||||
|
|
||||||
|
/** |
||||||
|
* IO 工具类,用于 {@link cn.hutool.core.io.IoUtil} 缺失的方法 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class IoUtils { |
||||||
|
|
||||||
|
/** |
||||||
|
* 从流中读取 UTF8 编码的内容 |
||||||
|
* |
||||||
|
* @param in 输入流 |
||||||
|
* @param isClose 是否关闭 |
||||||
|
* @return 内容 |
||||||
|
* @throws IORuntimeException IO 异常 |
||||||
|
*/ |
||||||
|
public static String readUtf8(InputStream in, boolean isClose) throws IORuntimeException { |
||||||
|
return StrUtil.utf8Str(IoUtil.read(in, isClose)); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,135 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.util.json; |
||||||
|
|
||||||
|
import cn.hutool.core.util.ArrayUtil; |
||||||
|
import cn.hutool.core.util.StrUtil; |
||||||
|
import cn.hutool.json.JSONUtil; |
||||||
|
import com.fasterxml.jackson.core.type.TypeReference; |
||||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature; |
||||||
|
import lombok.SneakyThrows; |
||||||
|
import lombok.experimental.UtilityClass; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
/** |
||||||
|
* JSON 工具类 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
@UtilityClass |
||||||
|
@Slf4j |
||||||
|
public class JsonUtils { |
||||||
|
|
||||||
|
private static ObjectMapper objectMapper = new ObjectMapper(); |
||||||
|
|
||||||
|
static { |
||||||
|
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 初始化 objectMapper 属性 |
||||||
|
* <p> |
||||||
|
* 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean |
||||||
|
* |
||||||
|
* @param objectMapper ObjectMapper 对象 |
||||||
|
*/ |
||||||
|
public static void init(ObjectMapper objectMapper) { |
||||||
|
JsonUtils.objectMapper = objectMapper; |
||||||
|
} |
||||||
|
|
||||||
|
@SneakyThrows |
||||||
|
public static String toJsonString(Object object) { |
||||||
|
return objectMapper.writeValueAsString(object); |
||||||
|
} |
||||||
|
|
||||||
|
@SneakyThrows |
||||||
|
public static byte[] toJsonByte(Object object) { |
||||||
|
return objectMapper.writeValueAsBytes(object); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public static <T> T parseObject(String text, Class<T> clazz) { |
||||||
|
if (StrUtil.isEmpty(text)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
try { |
||||||
|
return objectMapper.readValue(text, clazz); |
||||||
|
} catch (IOException e) { |
||||||
|
log.error("json parse err,json:{}", text, e); |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 将字符串解析成指定类型的对象 |
||||||
|
* 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下, |
||||||
|
* 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。 |
||||||
|
* |
||||||
|
* @param text 字符串 |
||||||
|
* @param clazz 类型 |
||||||
|
* @return 对象 |
||||||
|
*/ |
||||||
|
public static <T> T parseObject2(String text, Class<T> clazz) { |
||||||
|
if (StrUtil.isEmpty(text)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return JSONUtil.toBean(text, clazz); |
||||||
|
} |
||||||
|
|
||||||
|
public static <T> T parseObject(byte[] bytes, Class<T> clazz) { |
||||||
|
if (ArrayUtil.isEmpty(bytes)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
try { |
||||||
|
return objectMapper.readValue(bytes, clazz); |
||||||
|
} catch (IOException e) { |
||||||
|
log.error("json parse err,json:{}", bytes, e); |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public static <T> T parseObject(String text, TypeReference<T> typeReference) { |
||||||
|
try { |
||||||
|
return objectMapper.readValue(text, typeReference); |
||||||
|
} catch (IOException e) { |
||||||
|
log.error("json parse err,json:{}", text, e); |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public static <T> List<T> parseArray(String text, Class<T> clazz) { |
||||||
|
if (StrUtil.isEmpty(text)) { |
||||||
|
return new ArrayList<>(); |
||||||
|
} |
||||||
|
try { |
||||||
|
return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); |
||||||
|
} catch (IOException e) { |
||||||
|
log.error("json parse err,json:{}", text, e); |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// TODO @Li:和上面的风格保持一致哈。parseTree
|
||||||
|
public static JsonNode readTree(String text) { |
||||||
|
try { |
||||||
|
return objectMapper.readTree(text); |
||||||
|
} catch (IOException e) { |
||||||
|
log.error("json parse err,json:{}", text, e); |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public static JsonNode readTree(byte[] text) { |
||||||
|
try { |
||||||
|
return objectMapper.readTree(text); |
||||||
|
} catch (IOException e) { |
||||||
|
log.error("json parse err,json:{}", text, e); |
||||||
|
throw new RuntimeException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.util.monitor; |
||||||
|
|
||||||
|
import org.apache.skywalking.apm.toolkit.trace.TraceContext; |
||||||
|
|
||||||
|
/** |
||||||
|
* 链路追踪工具类 |
||||||
|
* |
||||||
|
* 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 util 包下 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class TracerUtils { |
||||||
|
|
||||||
|
/** |
||||||
|
* 私有化构造方法 |
||||||
|
*/ |
||||||
|
private TracerUtils() { |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获得链路追踪编号,直接返回 SkyWalking 的 TraceId。 |
||||||
|
* 如果不存在的话为空字符串!!! |
||||||
|
* |
||||||
|
* @return 链路追踪编号 |
||||||
|
*/ |
||||||
|
public static String getTraceId() { |
||||||
|
return TraceContext.traceId(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.util.number; |
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil; |
||||||
|
|
||||||
|
/** |
||||||
|
* 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class NumberUtils { |
||||||
|
|
||||||
|
public static Long parseLong(String str) { |
||||||
|
return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,63 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.util.object; |
||||||
|
|
||||||
|
import cn.hutool.core.util.ArrayUtil; |
||||||
|
import cn.hutool.core.util.ObjectUtil; |
||||||
|
import cn.hutool.core.util.ReflectUtil; |
||||||
|
|
||||||
|
import java.lang.reflect.Field; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Objects; |
||||||
|
import java.util.function.Consumer; |
||||||
|
|
||||||
|
/** |
||||||
|
* Object 工具类 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class ObjectUtils { |
||||||
|
|
||||||
|
/** |
||||||
|
* 复制对象,并忽略 Id 编号 |
||||||
|
* |
||||||
|
* @param object 被复制对象 |
||||||
|
* @param consumer 消费者,可以二次编辑被复制对象 |
||||||
|
* @return 复制后的对象 |
||||||
|
*/ |
||||||
|
public static <T> T cloneIgnoreId(T object, Consumer<T> consumer) { |
||||||
|
T result = ObjectUtil.clone(object); |
||||||
|
// 忽略 id 编号
|
||||||
|
Field field = ReflectUtil.getField(object.getClass(), "id"); |
||||||
|
if (field != null) { |
||||||
|
ReflectUtil.setFieldValue(result, field, null); |
||||||
|
} |
||||||
|
// 二次编辑
|
||||||
|
if (result != null) { |
||||||
|
consumer.accept(result); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
public static <T extends Comparable<T>> T max(T obj1, T obj2) { |
||||||
|
if (obj1 == null) { |
||||||
|
return obj2; |
||||||
|
} |
||||||
|
if (obj2 == null) { |
||||||
|
return obj1; |
||||||
|
} |
||||||
|
return obj1.compareTo(obj2) > 0 ? obj1 : obj2; |
||||||
|
} |
||||||
|
|
||||||
|
public static <T> T defaultIfNull(T... array) { |
||||||
|
for (T item : array) { |
||||||
|
if (item != null) { |
||||||
|
return item; |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
public static <T> boolean equalsAny(T obj, T... array) { |
||||||
|
return Arrays.asList(array).contains(obj); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.util.object; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.PageParam; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link cn.iocoder.yudao.framework.common.pojo.PageParam} 工具类 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class PageUtils { |
||||||
|
|
||||||
|
public static int getStart(PageParam pageParam) { |
||||||
|
return (pageParam.getPageNo() - 1) * pageParam.getPageSize(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
/** |
||||||
|
* 对于工具类的选择,优先查找 Hutool 中有没对应的方法 |
||||||
|
* 如果没有,则自己封装对应的工具类,以 Utils 结尾,用于区分 |
||||||
|
* |
||||||
|
* ps:如果担心 Hutool 存在坑的问题,可以阅读 Hutool 的实现源码,以确保可靠性。并且,可以补充相关的单元测试。 |
||||||
|
*/ |
||||||
|
package cn.iocoder.yudao.framework.common.util; |
@ -0,0 +1,95 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.util.servlet; |
||||||
|
|
||||||
|
import cn.hutool.core.io.IoUtil; |
||||||
|
import cn.hutool.core.util.StrUtil; |
||||||
|
import cn.hutool.extra.servlet.ServletUtil; |
||||||
|
import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
||||||
|
import org.springframework.http.MediaType; |
||||||
|
import org.springframework.web.context.request.RequestAttributes; |
||||||
|
import org.springframework.web.context.request.RequestContextHolder; |
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes; |
||||||
|
|
||||||
|
import javax.servlet.ServletRequest; |
||||||
|
import javax.servlet.http.HttpServletRequest; |
||||||
|
import javax.servlet.http.HttpServletResponse; |
||||||
|
import java.io.IOException; |
||||||
|
import java.net.URLEncoder; |
||||||
|
|
||||||
|
/** |
||||||
|
* 客户端工具类 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class ServletUtils { |
||||||
|
|
||||||
|
/** |
||||||
|
* 返回 JSON 字符串 |
||||||
|
* |
||||||
|
* @param response 响应 |
||||||
|
* @param object 对象,会序列化成 JSON 字符串 |
||||||
|
*/ |
||||||
|
@SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码
|
||||||
|
public static void writeJSON(HttpServletResponse response, Object object) { |
||||||
|
String content = JsonUtils.toJsonString(object); |
||||||
|
ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 返回附件 |
||||||
|
* |
||||||
|
* @param response 响应 |
||||||
|
* @param filename 文件名 |
||||||
|
* @param content 附件内容 |
||||||
|
* @throws IOException |
||||||
|
*/ |
||||||
|
public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException { |
||||||
|
// 设置 header 和 contentType
|
||||||
|
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8")); |
||||||
|
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); |
||||||
|
// 输出附件
|
||||||
|
IoUtil.write(response.getOutputStream(), false, content); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param request 请求 |
||||||
|
* @return ua |
||||||
|
*/ |
||||||
|
public static String getUserAgent(HttpServletRequest request) { |
||||||
|
String ua = request.getHeader("User-Agent"); |
||||||
|
return ua != null ? ua : ""; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获得请求 |
||||||
|
* |
||||||
|
* @return HttpServletRequest |
||||||
|
*/ |
||||||
|
public static HttpServletRequest getRequest() { |
||||||
|
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); |
||||||
|
if (!(requestAttributes instanceof ServletRequestAttributes)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return ((ServletRequestAttributes) requestAttributes).getRequest(); |
||||||
|
} |
||||||
|
|
||||||
|
public static String getUserAgent() { |
||||||
|
HttpServletRequest request = getRequest(); |
||||||
|
if (request == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return getUserAgent(request); |
||||||
|
} |
||||||
|
|
||||||
|
public static String getClientIP() { |
||||||
|
HttpServletRequest request = getRequest(); |
||||||
|
if (request == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return ServletUtil.getClientIP(request); |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean isJsonRequest(ServletRequest request) { |
||||||
|
return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,46 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.util.spring; |
||||||
|
|
||||||
|
import cn.hutool.core.bean.BeanUtil; |
||||||
|
import org.springframework.aop.framework.AdvisedSupport; |
||||||
|
import org.springframework.aop.framework.AopProxy; |
||||||
|
import org.springframework.aop.support.AopUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* Spring AOP 工具类 |
||||||
|
* |
||||||
|
* 参考波克尔 http://www.bubuko.com/infodetail-3471885.html 实现
|
||||||
|
*/ |
||||||
|
public class SpringAopUtils { |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取代理的目标对象 |
||||||
|
* |
||||||
|
* @param proxy 代理对象 |
||||||
|
* @return 目标对象 |
||||||
|
*/ |
||||||
|
public static Object getTarget(Object proxy) throws Exception { |
||||||
|
// 不是代理对象
|
||||||
|
if (!AopUtils.isAopProxy(proxy)) { |
||||||
|
return proxy; |
||||||
|
} |
||||||
|
// Jdk 代理
|
||||||
|
if (AopUtils.isJdkDynamicProxy(proxy)) { |
||||||
|
return getJdkDynamicProxyTargetObject(proxy); |
||||||
|
} |
||||||
|
// Cglib 代理
|
||||||
|
return getCglibProxyTargetObject(proxy); |
||||||
|
} |
||||||
|
|
||||||
|
private static Object getCglibProxyTargetObject(Object proxy) throws Exception { |
||||||
|
Object dynamicAdvisedInterceptor = BeanUtil.getFieldValue(proxy, "CGLIB$CALLBACK_0"); |
||||||
|
AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(dynamicAdvisedInterceptor, "advised"); |
||||||
|
return advisedSupport.getTargetSource().getTarget(); |
||||||
|
} |
||||||
|
|
||||||
|
private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception { |
||||||
|
AopProxy aopProxy = (AopProxy) BeanUtil.getFieldValue(proxy, "h"); |
||||||
|
AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(aopProxy, "advised"); |
||||||
|
return advisedSupport.getTargetSource().getTarget(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,82 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.util.spring; |
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil; |
||||||
|
import cn.hutool.core.map.MapUtil; |
||||||
|
import cn.hutool.core.util.ArrayUtil; |
||||||
|
import org.aspectj.lang.ProceedingJoinPoint; |
||||||
|
import org.aspectj.lang.reflect.MethodSignature; |
||||||
|
import org.springframework.core.DefaultParameterNameDiscoverer; |
||||||
|
import org.springframework.core.ParameterNameDiscoverer; |
||||||
|
import org.springframework.expression.EvaluationContext; |
||||||
|
import org.springframework.expression.ExpressionParser; |
||||||
|
import org.springframework.expression.spel.standard.SpelExpressionParser; |
||||||
|
import org.springframework.expression.spel.support.StandardEvaluationContext; |
||||||
|
|
||||||
|
import java.lang.reflect.Method; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* Spring EL 表达式的工具类 |
||||||
|
* |
||||||
|
* @author mashu |
||||||
|
*/ |
||||||
|
public class SpringExpressionUtils { |
||||||
|
|
||||||
|
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); |
||||||
|
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); |
||||||
|
|
||||||
|
private SpringExpressionUtils() { |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 从切面中,单个解析 EL 表达式的结果 |
||||||
|
* |
||||||
|
* @param joinPoint 切面点 |
||||||
|
* @param expressionString EL 表达式数组 |
||||||
|
* @return 执行界面 |
||||||
|
*/ |
||||||
|
public static Object parseExpression(ProceedingJoinPoint joinPoint, String expressionString) { |
||||||
|
Map<String, Object> result = parseExpressions(joinPoint, Collections.singletonList(expressionString)); |
||||||
|
return result.get(expressionString); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 从切面中,批量解析 EL 表达式的结果 |
||||||
|
* |
||||||
|
* @param joinPoint 切面点 |
||||||
|
* @param expressionStrings EL 表达式数组 |
||||||
|
* @return 结果,key 为表达式,value 为对应值 |
||||||
|
*/ |
||||||
|
public static Map<String, Object> parseExpressions(ProceedingJoinPoint joinPoint, List<String> expressionStrings) { |
||||||
|
// 如果为空,则不进行解析
|
||||||
|
if (CollUtil.isEmpty(expressionStrings)) { |
||||||
|
return MapUtil.newHashMap(); |
||||||
|
} |
||||||
|
|
||||||
|
// 第一步,构建解析的上下文 EvaluationContext
|
||||||
|
// 通过 joinPoint 获取被注解方法
|
||||||
|
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); |
||||||
|
Method method = methodSignature.getMethod(); |
||||||
|
// 使用 spring 的 ParameterNameDiscoverer 获取方法形参名数组
|
||||||
|
String[] paramNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method); |
||||||
|
// Spring 的表达式上下文对象
|
||||||
|
EvaluationContext context = new StandardEvaluationContext(); |
||||||
|
// 给上下文赋值
|
||||||
|
if (ArrayUtil.isNotEmpty(paramNames)) { |
||||||
|
Object[] args = joinPoint.getArgs(); |
||||||
|
for (int i = 0; i < paramNames.length; i++) { |
||||||
|
context.setVariable(paramNames[i], args[i]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 第二步,逐个参数解析
|
||||||
|
Map<String, Object> result = MapUtil.newHashMap(expressionStrings.size(), true); |
||||||
|
expressionStrings.forEach(key -> { |
||||||
|
Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context); |
||||||
|
result.put(key, value); |
||||||
|
}); |
||||||
|
return result; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.util.string; |
||||||
|
|
||||||
|
import cn.hutool.core.util.ObjectUtil; |
||||||
|
import cn.hutool.core.util.StrUtil; |
||||||
|
|
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* 字符串工具类 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class StrUtils { |
||||||
|
|
||||||
|
public static String maxLength(CharSequence str, int maxLength) { |
||||||
|
return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 指定字符串的 |
||||||
|
* @param str |
||||||
|
* @param replaceMap |
||||||
|
* @return |
||||||
|
*/ |
||||||
|
public static String replace(String str, Map<String, String> replaceMap) { |
||||||
|
assert StrUtil.isNotBlank(str); |
||||||
|
if (ObjectUtil.isEmpty(replaceMap)) { |
||||||
|
return str; |
||||||
|
} |
||||||
|
String result = null; |
||||||
|
for (String key : replaceMap.keySet()) { |
||||||
|
result = str.replace(key, replaceMap.get(key)); |
||||||
|
} |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,49 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.util.validation; |
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil; |
||||||
|
import cn.hutool.core.util.StrUtil; |
||||||
|
import org.springframework.util.StringUtils; |
||||||
|
|
||||||
|
import javax.validation.ConstraintViolation; |
||||||
|
import javax.validation.ConstraintViolationException; |
||||||
|
import javax.validation.Validator; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.regex.Pattern; |
||||||
|
|
||||||
|
/** |
||||||
|
* 校验工具类 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class ValidationUtils { |
||||||
|
|
||||||
|
private static final Pattern PATTERN_URL = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"); |
||||||
|
|
||||||
|
private static final Pattern PATTERN_XML_NCNAME = Pattern.compile("[a-zA-Z_][\\-_.0-9_a-zA-Z$]*"); |
||||||
|
|
||||||
|
public static boolean isMobile(String mobile) { |
||||||
|
if (StrUtil.length(mobile) != 11) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
// TODO 芋艿,后面完善手机校验
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean isURL(String url) { |
||||||
|
return StringUtils.hasText(url) |
||||||
|
&& PATTERN_URL.matcher(url).matches(); |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean isXmlNCName(String str) { |
||||||
|
return StringUtils.hasText(str) |
||||||
|
&& PATTERN_XML_NCNAME.matcher(str).matches(); |
||||||
|
} |
||||||
|
|
||||||
|
public static void validate(Validator validator, Object object, Class<?>... groups) { |
||||||
|
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups); |
||||||
|
if (CollUtil.isNotEmpty(constraintViolations)) { |
||||||
|
throw new ConstraintViolationException(constraintViolations); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.validation; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.core.IntArrayValuable; |
||||||
|
|
||||||
|
import javax.validation.Constraint; |
||||||
|
import javax.validation.Payload; |
||||||
|
import java.lang.annotation.*; |
||||||
|
|
||||||
|
@Target({ |
||||||
|
ElementType.METHOD, |
||||||
|
ElementType.FIELD, |
||||||
|
ElementType.ANNOTATION_TYPE, |
||||||
|
ElementType.CONSTRUCTOR, |
||||||
|
ElementType.PARAMETER, |
||||||
|
ElementType.TYPE_USE |
||||||
|
}) |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Documented |
||||||
|
@Constraint( |
||||||
|
validatedBy = InEnumValidator.class |
||||||
|
) |
||||||
|
public @interface InEnum { |
||||||
|
|
||||||
|
/** |
||||||
|
* @return 实现 EnumValuable 接口的 |
||||||
|
*/ |
||||||
|
Class<? extends IntArrayValuable> value(); |
||||||
|
|
||||||
|
String message() default "必须在指定范围 {value}"; |
||||||
|
|
||||||
|
Class<?>[] groups() default {}; |
||||||
|
|
||||||
|
Class<? extends Payload>[] payload() default {}; |
||||||
|
|
||||||
|
} |
@ -0,0 +1,44 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.validation; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.core.IntArrayValuable; |
||||||
|
|
||||||
|
import javax.validation.ConstraintValidator; |
||||||
|
import javax.validation.ConstraintValidatorContext; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.List; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
public class InEnumValidator implements ConstraintValidator<InEnum, Integer> { |
||||||
|
|
||||||
|
private List<Integer> values; |
||||||
|
|
||||||
|
@Override |
||||||
|
public void initialize(InEnum annotation) { |
||||||
|
IntArrayValuable[] values = annotation.value().getEnumConstants(); |
||||||
|
if (values.length == 0) { |
||||||
|
this.values = Collections.emptyList(); |
||||||
|
} else { |
||||||
|
this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toList()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isValid(Integer value, ConstraintValidatorContext context) { |
||||||
|
// 为空时,默认不校验,即认为通过
|
||||||
|
if (value == null) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
// 校验通过
|
||||||
|
if (values.contains(value)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
// 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值)
|
||||||
|
context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
|
||||||
|
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate() |
||||||
|
.replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
@ -0,0 +1,28 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.validation; |
||||||
|
|
||||||
|
import javax.validation.Constraint; |
||||||
|
import javax.validation.Payload; |
||||||
|
import java.lang.annotation.*; |
||||||
|
|
||||||
|
@Target({ |
||||||
|
ElementType.METHOD, |
||||||
|
ElementType.FIELD, |
||||||
|
ElementType.ANNOTATION_TYPE, |
||||||
|
ElementType.CONSTRUCTOR, |
||||||
|
ElementType.PARAMETER, |
||||||
|
ElementType.TYPE_USE |
||||||
|
}) |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Documented |
||||||
|
@Constraint( |
||||||
|
validatedBy = MobileValidator.class |
||||||
|
) |
||||||
|
public @interface Mobile { |
||||||
|
|
||||||
|
String message() default "手机号格式不正确"; |
||||||
|
|
||||||
|
Class<?>[] groups() default {}; |
||||||
|
|
||||||
|
Class<? extends Payload>[] payload() default {}; |
||||||
|
|
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
package cn.iocoder.yudao.framework.common.validation; |
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil; |
||||||
|
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; |
||||||
|
|
||||||
|
import javax.validation.ConstraintValidator; |
||||||
|
import javax.validation.ConstraintValidatorContext; |
||||||
|
|
||||||
|
public class MobileValidator implements ConstraintValidator<Mobile, String> { |
||||||
|
|
||||||
|
@Override |
||||||
|
public void initialize(Mobile annotation) { |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isValid(String value, ConstraintValidatorContext context) { |
||||||
|
// 如果手机号为空,默认不校验,即校验通过
|
||||||
|
if (StrUtil.isEmpty(value)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
// 校验手机
|
||||||
|
return ValidationUtils.isMobile(value); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,4 @@ |
|||||||
|
/** |
||||||
|
* 使用 Hibernate Validator 实现参数校验 |
||||||
|
*/ |
||||||
|
package cn.iocoder.yudao.framework.common.validation; |
@ -0,0 +1 @@ |
|||||||
|
<http://www.iocoder.cn/Spring-Boot/Validation/?yudao> |
@ -0,0 +1,48 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" |
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||||
|
<parent> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-framework</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</parent> |
||||||
|
<modelVersion>4.0.0</modelVersion> |
||||||
|
<artifactId>yudao-spring-boot-starter-activiti</artifactId> |
||||||
|
<packaging>jar</packaging> |
||||||
|
|
||||||
|
<name>${project.artifactId}</name> |
||||||
|
<description>Activiti 拓展</description> |
||||||
|
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
||||||
|
|
||||||
|
<dependencies> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-common</artifactId> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- Web 相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-security</artifactId> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- DB 相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-mybatis</artifactId> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- 工作流相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>org.activiti</groupId> |
||||||
|
<artifactId>activiti-spring-boot-starter</artifactId> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.activiti</groupId> |
||||||
|
<artifactId>activiti-image-generator</artifactId> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
</dependencies> |
||||||
|
|
||||||
|
</project> |
@ -0,0 +1,45 @@ |
|||||||
|
package cn.iocoder.yudao.framework.activiti.config; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.activiti.core.web.ActivitiWebFilter; |
||||||
|
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; |
||||||
|
import org.activiti.image.ProcessDiagramGenerator; |
||||||
|
import org.activiti.image.impl.DefaultProcessDiagramGenerator; |
||||||
|
import org.activiti.spring.SpringProcessEngineConfiguration; |
||||||
|
import org.activiti.spring.boot.ProcessEngineConfigurationConfigurer; |
||||||
|
import org.apache.ibatis.session.SqlSessionFactory; |
||||||
|
import org.apache.ibatis.transaction.TransactionFactory; |
||||||
|
import org.mybatis.spring.transaction.SpringManagedTransactionFactory; |
||||||
|
import org.springframework.boot.web.servlet.FilterRegistrationBean; |
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
import org.springframework.transaction.PlatformTransactionManager; |
||||||
|
|
||||||
|
@Configuration |
||||||
|
public class YudaoActivitiConfiguration { |
||||||
|
|
||||||
|
/** |
||||||
|
* Activiti 流程图的生成器。目前管理后台的流程图 svg,通过它绘制生成。 |
||||||
|
*/ |
||||||
|
@Bean |
||||||
|
public ProcessDiagramGenerator processDiagramGenerator() { |
||||||
|
return new DefaultProcessDiagramGenerator(); |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
public FilterRegistrationBean<ActivitiWebFilter> activitiWebFilter() { |
||||||
|
FilterRegistrationBean<ActivitiWebFilter> registrationBean = new FilterRegistrationBean<>(); |
||||||
|
registrationBean.setFilter(new ActivitiWebFilter()); |
||||||
|
registrationBean.setOrder(WebFilterOrderEnum.ACTIVITI_FILTER); |
||||||
|
return registrationBean; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* ProcessEngineConfigurationConfigurer 实现类,设置事务管理器,保证 ACT_ 表和自己的表的事务一致性 |
||||||
|
*/ |
||||||
|
@Bean |
||||||
|
public ProcessEngineConfigurationConfigurer processEngineConfigurationConfigurer( |
||||||
|
PlatformTransactionManager platformTransactionManager) { |
||||||
|
return processEngineConfiguration -> processEngineConfiguration.setTransactionManager(platformTransactionManager); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,109 @@ |
|||||||
|
package cn.iocoder.yudao.framework.activiti.core.util; |
||||||
|
|
||||||
|
import cn.hutool.core.util.ArrayUtil; |
||||||
|
import cn.hutool.core.util.ObjectUtil; |
||||||
|
import cn.hutool.core.util.ReflectUtil; |
||||||
|
import cn.hutool.core.util.StrUtil; |
||||||
|
import cn.iocoder.yudao.framework.common.util.number.NumberUtils; |
||||||
|
import com.alibaba.ttl.TransmittableThreadLocal; |
||||||
|
import org.activiti.bpmn.converter.BpmnXMLConverter; |
||||||
|
import org.activiti.bpmn.model.BpmnModel; |
||||||
|
import org.activiti.bpmn.model.FlowElement; |
||||||
|
import org.activiti.bpmn.model.Process; |
||||||
|
import org.activiti.engine.impl.identity.Authentication; |
||||||
|
import org.activiti.engine.impl.util.io.BytesStreamSource; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Objects; |
||||||
|
import java.util.function.Consumer; |
||||||
|
|
||||||
|
/** |
||||||
|
* Activiti 工具类 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class ActivitiUtils { |
||||||
|
|
||||||
|
static { |
||||||
|
setAuthenticationThreadLocal(); |
||||||
|
} |
||||||
|
|
||||||
|
// ========== Authentication 相关 ==========
|
||||||
|
|
||||||
|
/** |
||||||
|
* 反射修改 Authentication 的 authenticatedUserIdThreadLocal 静态变量,使用 TTL 线程变量 |
||||||
|
* 目的:保证 @Async 等异步执行时,变量丢失的问题 |
||||||
|
*/ |
||||||
|
private static void setAuthenticationThreadLocal() { |
||||||
|
ReflectUtil.setFieldValue(Authentication.class, "authenticatedUserIdThreadLocal", |
||||||
|
new TransmittableThreadLocal<String>()); |
||||||
|
} |
||||||
|
|
||||||
|
public static void setAuthenticatedUserId(Long userId) { |
||||||
|
Authentication.setAuthenticatedUserId(String.valueOf(userId)); |
||||||
|
} |
||||||
|
|
||||||
|
public static void clearAuthenticatedUserId() { |
||||||
|
Authentication.setAuthenticatedUserId(null); |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean equals(String userIdStr, Long userId) { |
||||||
|
return Objects.equals(userId, NumberUtils.parseLong(userIdStr)); |
||||||
|
} |
||||||
|
|
||||||
|
// ========== BPMN XML 相关 ==========
|
||||||
|
|
||||||
|
/** |
||||||
|
* 构建对应的 BPMN Model |
||||||
|
* |
||||||
|
* @param bpmnBytes 原始的 BPMN XML 字节数组 |
||||||
|
* @return BPMN Model |
||||||
|
*/ |
||||||
|
public static BpmnModel buildBpmnModel(byte[] bpmnBytes) { |
||||||
|
// 转换成 BpmnModel 对象
|
||||||
|
BpmnXMLConverter converter = new BpmnXMLConverter(); |
||||||
|
return converter.convertToBpmnModel(new BytesStreamSource(bpmnBytes), true, true); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获得 BPMN 流程中,指定的元素们 |
||||||
|
* |
||||||
|
* @param model |
||||||
|
* @param clazz 指定元素。例如说,{@link org.activiti.bpmn.model.UserTask}、{@link org.activiti.bpmn.model.Gateway} 等等 |
||||||
|
* @return 元素们 |
||||||
|
*/ |
||||||
|
public static <T extends FlowElement> List<T> getBpmnModelElements(BpmnModel model, Class<T> clazz) { |
||||||
|
List<T> result = new ArrayList<>(); |
||||||
|
model.getProcesses().forEach(process -> { |
||||||
|
process.getFlowElements().forEach(flowElement -> { |
||||||
|
if (flowElement.getClass().isAssignableFrom(clazz)) { |
||||||
|
result.add((T) flowElement); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
public static String getBpmnXml(BpmnModel model) { |
||||||
|
if (model == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return StrUtil.utf8Str(getBpmnBytes(model)); |
||||||
|
} |
||||||
|
|
||||||
|
public static byte[] getBpmnBytes(BpmnModel model) { |
||||||
|
if (model == null) { |
||||||
|
return new byte[0]; |
||||||
|
} |
||||||
|
BpmnXMLConverter converter = new BpmnXMLConverter(); |
||||||
|
return converter.convertToXML(model); |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean equals(BpmnModel oldModel, BpmnModel newModel) { |
||||||
|
// 由于 BpmnModel 未提供 equals 方法,所以只能转成字节数组,进行比较
|
||||||
|
return Arrays.equals(getBpmnBytes(oldModel), getBpmnBytes(newModel)); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
package cn.iocoder.yudao.framework.activiti.core.web; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.activiti.core.util.ActivitiUtils; |
||||||
|
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; |
||||||
|
import org.springframework.web.filter.OncePerRequestFilter; |
||||||
|
|
||||||
|
import javax.servlet.FilterChain; |
||||||
|
import javax.servlet.ServletException; |
||||||
|
import javax.servlet.http.HttpServletRequest; |
||||||
|
import javax.servlet.http.HttpServletResponse; |
||||||
|
import java.io.IOException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Activiti Web 过滤器,将 userId 设置到 {@link org.activiti.engine.impl.identity.Authentication} 中 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class ActivitiWebFilter extends OncePerRequestFilter { |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) |
||||||
|
throws ServletException, IOException { |
||||||
|
try { |
||||||
|
// 设置工作流的用户
|
||||||
|
Long userId = SecurityFrameworkUtils.getLoginUserId(); |
||||||
|
if (userId != null) { |
||||||
|
ActivitiUtils.setAuthenticatedUserId(userId); |
||||||
|
} |
||||||
|
// 过滤
|
||||||
|
chain.doFilter(request, response); |
||||||
|
} finally { |
||||||
|
// 清理
|
||||||
|
ActivitiUtils.clearAuthenticatedUserId(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
package cn.iocoder.yudao.framework.activiti; |
@ -0,0 +1,2 @@ |
|||||||
|
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
||||||
|
cn.iocoder.yudao.framework.activiti.config.YudaoActivitiConfiguration |
@ -0,0 +1,45 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" |
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||||
|
<parent> |
||||||
|
<artifactId>yudao-framework</artifactId> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<version>${revision}</version> |
||||||
|
</parent> |
||||||
|
<modelVersion>4.0.0</modelVersion> |
||||||
|
<artifactId>yudao-spring-boot-starter-biz-data-permission</artifactId> |
||||||
|
<packaging>jar</packaging> |
||||||
|
|
||||||
|
<name>${project.artifactId}</name> |
||||||
|
<description>数据权限</description> |
||||||
|
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
||||||
|
|
||||||
|
<dependencies> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-common</artifactId> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- Web 相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-security</artifactId> |
||||||
|
<optional>true</optional> <!-- 可选,如果使用 DeptDataPermissionRule 必须提供 --> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- DB 相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-mybatis</artifactId> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- Test 测试相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-test</artifactId> |
||||||
|
<scope>test</scope> |
||||||
|
</dependency> |
||||||
|
</dependencies> |
||||||
|
|
||||||
|
</project> |
@ -0,0 +1,45 @@ |
|||||||
|
package cn.iocoder.yudao.framework.datapermission.config; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor; |
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.db.DataPermissionDatabaseInterceptor; |
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; |
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory; |
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl; |
||||||
|
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; |
||||||
|
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; |
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
/** |
||||||
|
* 数据全新啊的自动配置类 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
@Configuration |
||||||
|
public class YudaoDataPermissionAutoConfiguration { |
||||||
|
|
||||||
|
@Bean |
||||||
|
public DataPermissionRuleFactory dataPermissionRuleFactory(List<DataPermissionRule> rules) { |
||||||
|
return new DataPermissionRuleFactoryImpl(rules); |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
public DataPermissionDatabaseInterceptor dataPermissionDatabaseInterceptor(MybatisPlusInterceptor interceptor, |
||||||
|
List<DataPermissionRule> rules) { |
||||||
|
// 创建 DataPermissionDatabaseInterceptor 拦截器
|
||||||
|
DataPermissionRuleFactory ruleFactory = dataPermissionRuleFactory(rules); |
||||||
|
DataPermissionDatabaseInterceptor inner = new DataPermissionDatabaseInterceptor(ruleFactory); |
||||||
|
// 添加到 interceptor 中
|
||||||
|
// 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
|
||||||
|
MyBatisUtils.addInterceptor(interceptor, inner, 0); |
||||||
|
return inner; |
||||||
|
} |
||||||
|
|
||||||
|
@Bean |
||||||
|
public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() { |
||||||
|
return new DataPermissionAnnotationAdvisor(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
package cn.iocoder.yudao.framework.datapermission.config; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.dept.rule.DeptDataPermissionRule; |
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.dept.rule.DeptDataPermissionRuleCustomizer; |
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.dept.service.DeptDataPermissionFrameworkService; |
||||||
|
import cn.iocoder.yudao.framework.security.core.LoginUser; |
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; |
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
/** |
||||||
|
* 基于部门的数据权限 AutoConfiguration |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
@Configuration |
||||||
|
@ConditionalOnClass(LoginUser.class) |
||||||
|
@ConditionalOnBean(value = {DeptDataPermissionFrameworkService.class, DeptDataPermissionRuleCustomizer.class}) |
||||||
|
public class YudaoDeptDataPermissionAutoConfiguration { |
||||||
|
|
||||||
|
@Bean |
||||||
|
public DeptDataPermissionRule deptDataPermissionRule(DeptDataPermissionFrameworkService service, |
||||||
|
List<DeptDataPermissionRuleCustomizer> customizers) { |
||||||
|
// 创建 DeptDataPermissionRule 对象
|
||||||
|
DeptDataPermissionRule rule = new DeptDataPermissionRule(service); |
||||||
|
// 补全表配置
|
||||||
|
customizers.forEach(customizer -> customizer.customize(rule)); |
||||||
|
return rule; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
package cn.iocoder.yudao.framework.datapermission.core.annotation; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; |
||||||
|
|
||||||
|
import java.lang.annotation.*; |
||||||
|
|
||||||
|
/** |
||||||
|
* 数据权限注解 |
||||||
|
* 可声明在类或者方法上,标识使用的数据权限规则 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
@Target({ElementType.TYPE, ElementType.METHOD}) |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Documented |
||||||
|
public @interface DataPermission { |
||||||
|
|
||||||
|
/** |
||||||
|
* 当前类或方法是否开启数据权限 |
||||||
|
* 即使不添加 @DataPermission 注解,默认是开启状态 |
||||||
|
* 可通过设置 enable 为 false 禁用 |
||||||
|
*/ |
||||||
|
boolean enable() default true; |
||||||
|
|
||||||
|
/** |
||||||
|
* 生效的数据权限规则数组,优先级高于 {@link #excludeRules()} |
||||||
|
*/ |
||||||
|
Class<? extends DataPermissionRule>[] includeRules() default {}; |
||||||
|
|
||||||
|
/** |
||||||
|
* 排除的数据权限规则数组,优先级最低 |
||||||
|
*/ |
||||||
|
Class<? extends DataPermissionRule>[] excludeRules() default {}; |
||||||
|
|
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
package cn.iocoder.yudao.framework.datapermission.core.aop; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; |
||||||
|
import lombok.EqualsAndHashCode; |
||||||
|
import lombok.Getter; |
||||||
|
import org.aopalliance.aop.Advice; |
||||||
|
import org.springframework.aop.Pointcut; |
||||||
|
import org.springframework.aop.support.AbstractPointcutAdvisor; |
||||||
|
import org.springframework.aop.support.ComposablePointcut; |
||||||
|
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission} 注解的 Advisor 实现类 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
@Getter |
||||||
|
@EqualsAndHashCode(callSuper = true) |
||||||
|
public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor { |
||||||
|
|
||||||
|
private final Advice advice; |
||||||
|
|
||||||
|
private final Pointcut pointcut; |
||||||
|
|
||||||
|
public DataPermissionAnnotationAdvisor() { |
||||||
|
this.advice = new DataPermissionAnnotationInterceptor(); |
||||||
|
this.pointcut = this.buildPointcut(); |
||||||
|
} |
||||||
|
|
||||||
|
protected Pointcut buildPointcut() { |
||||||
|
Pointcut classPointcut = new AnnotationMatchingPointcut(DataPermission.class, true); |
||||||
|
Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataPermission.class, true); |
||||||
|
return new ComposablePointcut(classPointcut).union(methodPointcut); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,72 @@ |
|||||||
|
package cn.iocoder.yudao.framework.datapermission.core.aop; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; |
||||||
|
import lombok.Getter; |
||||||
|
import org.aopalliance.intercept.MethodInterceptor; |
||||||
|
import org.aopalliance.intercept.MethodInvocation; |
||||||
|
import org.springframework.core.MethodClassKey; |
||||||
|
import org.springframework.core.annotation.AnnotationUtils; |
||||||
|
|
||||||
|
import java.lang.reflect.Method; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link DataPermission} 注解的拦截器 |
||||||
|
* 1. 在执行方法前,将 @DataPermission 注解入栈 |
||||||
|
* 2. 在执行方法后,将 @DataPermission 注解出栈 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
@DataPermission // 该注解,用于 {@link DATA_PERMISSION_NULL} 的空对象
|
||||||
|
public class DataPermissionAnnotationInterceptor implements MethodInterceptor { |
||||||
|
|
||||||
|
/** |
||||||
|
* DataPermission 空对象,用于方法无 {@link DataPermission} 注解时,使用 DATA_PERMISSION_NULL 进行占位 |
||||||
|
*/ |
||||||
|
static final DataPermission DATA_PERMISSION_NULL = DataPermissionAnnotationInterceptor.class.getAnnotation(DataPermission.class); |
||||||
|
|
||||||
|
@Getter |
||||||
|
private final Map<MethodClassKey, DataPermission> dataPermissionCache = new ConcurrentHashMap<>(); |
||||||
|
|
||||||
|
@Override |
||||||
|
public Object invoke(MethodInvocation methodInvocation) throws Throwable { |
||||||
|
// 入栈
|
||||||
|
DataPermission dataPermission = this.findAnnotation(methodInvocation); |
||||||
|
if (dataPermission != null) { |
||||||
|
DataPermissionContextHolder.add(dataPermission); |
||||||
|
} |
||||||
|
try { |
||||||
|
// 执行逻辑
|
||||||
|
return methodInvocation.proceed(); |
||||||
|
} finally { |
||||||
|
// 出栈
|
||||||
|
if (dataPermission != null) { |
||||||
|
DataPermissionContextHolder.remove(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private DataPermission findAnnotation(MethodInvocation methodInvocation) { |
||||||
|
// 1. 从缓存中获取
|
||||||
|
Method method = methodInvocation.getMethod(); |
||||||
|
Object targetObject = methodInvocation.getThis(); |
||||||
|
Class<?> clazz = targetObject != null ? targetObject.getClass() : method.getDeclaringClass(); |
||||||
|
MethodClassKey methodClassKey = new MethodClassKey(method, clazz); |
||||||
|
DataPermission dataPermission = dataPermissionCache.get(methodClassKey); |
||||||
|
if (dataPermission != null) { |
||||||
|
return dataPermission != DATA_PERMISSION_NULL ? dataPermission : null; |
||||||
|
} |
||||||
|
|
||||||
|
// 2.1 从方法中获取
|
||||||
|
dataPermission = AnnotationUtils.findAnnotation(method, DataPermission.class); |
||||||
|
// 2.2 从类上获取
|
||||||
|
if (dataPermission == null) { |
||||||
|
dataPermission = AnnotationUtils.findAnnotation(clazz, DataPermission.class); |
||||||
|
} |
||||||
|
// 2.3 添加到缓存中
|
||||||
|
dataPermissionCache.put(methodClassKey, dataPermission != null ? dataPermission : DATA_PERMISSION_NULL); |
||||||
|
return dataPermission; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,72 @@ |
|||||||
|
package cn.iocoder.yudao.framework.datapermission.core.aop; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; |
||||||
|
import com.alibaba.ttl.TransmittableThreadLocal; |
||||||
|
|
||||||
|
import java.util.LinkedList; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link DataPermission} 注解的 Context 上下文 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class DataPermissionContextHolder { |
||||||
|
|
||||||
|
/** |
||||||
|
* 使用 List 的原因,可能存在方法的嵌套调用 |
||||||
|
*/ |
||||||
|
private static final ThreadLocal<LinkedList<DataPermission>> DATA_PERMISSIONS = |
||||||
|
TransmittableThreadLocal.withInitial(LinkedList::new); |
||||||
|
|
||||||
|
/** |
||||||
|
* 获得当前的 DataPermission 注解 |
||||||
|
* |
||||||
|
* @return DataPermission 注解 |
||||||
|
*/ |
||||||
|
public static DataPermission get() { |
||||||
|
return DATA_PERMISSIONS.get().peekLast(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 入栈 DataPermission 注解 |
||||||
|
* |
||||||
|
* @param dataPermission DataPermission 注解 |
||||||
|
*/ |
||||||
|
public static void add(DataPermission dataPermission) { |
||||||
|
DATA_PERMISSIONS.get().addLast(dataPermission); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 出栈 DataPermission 注解 |
||||||
|
* |
||||||
|
* @return DataPermission 注解 |
||||||
|
*/ |
||||||
|
public static DataPermission remove() { |
||||||
|
DataPermission dataPermission = DATA_PERMISSIONS.get().removeLast(); |
||||||
|
// 无元素时,清空 ThreadLocal
|
||||||
|
if (DATA_PERMISSIONS.get().isEmpty()) { |
||||||
|
DATA_PERMISSIONS.remove(); |
||||||
|
} |
||||||
|
return dataPermission; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获得所有 DataPermission |
||||||
|
* |
||||||
|
* @return DataPermission 队列 |
||||||
|
*/ |
||||||
|
public static List<DataPermission> getAll() { |
||||||
|
return DATA_PERMISSIONS.get(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 清空上下文 |
||||||
|
* |
||||||
|
* 目前仅仅用于单测 |
||||||
|
*/ |
||||||
|
public static void clear() { |
||||||
|
DATA_PERMISSIONS.remove(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,508 @@ |
|||||||
|
package cn.iocoder.yudao.framework.datapermission.core.db; |
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil; |
||||||
|
import cn.iocoder.yudao.framework.common.util.collection.SetUtils; |
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; |
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory; |
||||||
|
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; |
||||||
|
import com.alibaba.ttl.TransmittableThreadLocal; |
||||||
|
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; |
||||||
|
import com.baomidou.mybatisplus.core.toolkit.PluginUtils; |
||||||
|
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport; |
||||||
|
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; |
||||||
|
import lombok.Getter; |
||||||
|
import lombok.RequiredArgsConstructor; |
||||||
|
import net.sf.jsqlparser.expression.*; |
||||||
|
import net.sf.jsqlparser.expression.operators.conditional.AndExpression; |
||||||
|
import net.sf.jsqlparser.expression.operators.conditional.OrExpression; |
||||||
|
import net.sf.jsqlparser.expression.operators.relational.ExistsExpression; |
||||||
|
import net.sf.jsqlparser.expression.operators.relational.ExpressionList; |
||||||
|
import net.sf.jsqlparser.expression.operators.relational.InExpression; |
||||||
|
import net.sf.jsqlparser.expression.operators.relational.ItemsList; |
||||||
|
import net.sf.jsqlparser.schema.Table; |
||||||
|
import net.sf.jsqlparser.statement.delete.Delete; |
||||||
|
import net.sf.jsqlparser.statement.select.*; |
||||||
|
import net.sf.jsqlparser.statement.update.Update; |
||||||
|
import org.apache.ibatis.executor.Executor; |
||||||
|
import org.apache.ibatis.executor.statement.StatementHandler; |
||||||
|
import org.apache.ibatis.mapping.BoundSql; |
||||||
|
import org.apache.ibatis.mapping.MappedStatement; |
||||||
|
import org.apache.ibatis.mapping.SqlCommandType; |
||||||
|
import org.apache.ibatis.session.ResultHandler; |
||||||
|
import org.apache.ibatis.session.RowBounds; |
||||||
|
|
||||||
|
import java.sql.Connection; |
||||||
|
import java.util.*; |
||||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||||
|
|
||||||
|
/** |
||||||
|
* 数据权限拦截器,通过 {@link DataPermissionRule} 数据权限规则,重写 SQL 的方式来实现 |
||||||
|
* 主要的 SQL 重写方法,可见 {@link #builderExpression(Expression, Table)} 方法 |
||||||
|
* |
||||||
|
* 整体的代码实现上,参考 {@link com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor} 实现。 |
||||||
|
* 所以每次 MyBatis Plus 升级时,需要 Review 下其具体的实现是否有变更! |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
@RequiredArgsConstructor |
||||||
|
public class DataPermissionDatabaseInterceptor extends JsqlParserSupport implements InnerInterceptor { |
||||||
|
|
||||||
|
private final DataPermissionRuleFactory ruleFactory; |
||||||
|
|
||||||
|
@Getter |
||||||
|
private final MappedStatementCache mappedStatementCache = new MappedStatementCache(); |
||||||
|
|
||||||
|
@Override // SELECT 场景
|
||||||
|
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, |
||||||
|
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { |
||||||
|
// 获得 Mapper 对应的数据权限的规则
|
||||||
|
List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(ms.getId()); |
||||||
|
if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写,则跳过
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql); |
||||||
|
try { |
||||||
|
// 初始化上下文
|
||||||
|
ContextHolder.init(rules); |
||||||
|
// 处理 SQL
|
||||||
|
mpBs.sql(parserSingle(mpBs.sql(), null)); |
||||||
|
} finally { |
||||||
|
addMappedStatementCache(ms); |
||||||
|
ContextHolder.clear(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override // 只处理 UPDATE / DELETE 场景,不处理 INSERT 场景
|
||||||
|
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) { |
||||||
|
PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh); |
||||||
|
MappedStatement ms = mpSh.mappedStatement(); |
||||||
|
SqlCommandType sct = ms.getSqlCommandType(); |
||||||
|
if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) { |
||||||
|
// 获得 Mapper 对应的数据权限的规则
|
||||||
|
List<DataPermissionRule> rules = ruleFactory.getDataPermissionRule(ms.getId()); |
||||||
|
if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写,则跳过
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql(); |
||||||
|
try { |
||||||
|
// 初始化上下文
|
||||||
|
ContextHolder.init(rules); |
||||||
|
// 处理 SQL
|
||||||
|
mpBs.sql(parserMulti(mpBs.sql(), null)); |
||||||
|
} finally { |
||||||
|
addMappedStatementCache(ms); |
||||||
|
ContextHolder.clear(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void processSelect(Select select, int index, String sql, Object obj) { |
||||||
|
processSelectBody(select.getSelectBody()); |
||||||
|
List<WithItem> withItemsList = select.getWithItemsList(); |
||||||
|
if (!CollectionUtils.isEmpty(withItemsList)) { |
||||||
|
withItemsList.forEach(this::processSelectBody); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected void processSelectBody(SelectBody selectBody) { |
||||||
|
if (selectBody == null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if (selectBody instanceof PlainSelect) { |
||||||
|
processPlainSelect((PlainSelect) selectBody); |
||||||
|
} else if (selectBody instanceof WithItem) { |
||||||
|
WithItem withItem = (WithItem) selectBody; |
||||||
|
processSelectBody(withItem.getSubSelect().getSelectBody()); |
||||||
|
} else { |
||||||
|
SetOperationList operationList = (SetOperationList) selectBody; |
||||||
|
List<SelectBody> selectBodys = operationList.getSelects(); |
||||||
|
if (CollectionUtils.isNotEmpty(selectBodys)) { |
||||||
|
selectBodys.forEach(this::processSelectBody); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* update 语句处理 |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
protected void processUpdate(Update update, int index, String sql, Object obj) { |
||||||
|
final Table table = update.getTable(); |
||||||
|
update.setWhere(this.builderExpression(update.getWhere(), table)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* delete 语句处理 |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
protected void processDelete(Delete delete, int index, String sql, Object obj) { |
||||||
|
delete.setWhere(this.builderExpression(delete.getWhere(), delete.getTable())); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 处理 PlainSelect |
||||||
|
*/ |
||||||
|
protected void processPlainSelect(PlainSelect plainSelect) { |
||||||
|
FromItem fromItem = plainSelect.getFromItem(); |
||||||
|
Expression where = plainSelect.getWhere(); |
||||||
|
processWhereSubSelect(where); |
||||||
|
if (fromItem instanceof Table) { |
||||||
|
Table fromTable = (Table) fromItem; |
||||||
|
plainSelect.setWhere(builderExpression(where, fromTable)); |
||||||
|
} else { |
||||||
|
processFromItem(fromItem); |
||||||
|
} |
||||||
|
//#3087 github
|
||||||
|
List<SelectItem> selectItems = plainSelect.getSelectItems(); |
||||||
|
if (CollectionUtils.isNotEmpty(selectItems)) { |
||||||
|
selectItems.forEach(this::processSelectItem); |
||||||
|
} |
||||||
|
List<Join> joins = plainSelect.getJoins(); |
||||||
|
if (CollectionUtils.isNotEmpty(joins)) { |
||||||
|
processJoins(joins); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 处理where条件内的子查询 |
||||||
|
* <p> |
||||||
|
* 支持如下: |
||||||
|
* 1. in |
||||||
|
* 2. = |
||||||
|
* 3. > |
||||||
|
* 4. < |
||||||
|
* 5. >= |
||||||
|
* 6. <= |
||||||
|
* 7. <> |
||||||
|
* 8. EXISTS |
||||||
|
* 9. NOT EXISTS |
||||||
|
* <p> |
||||||
|
* 前提条件: |
||||||
|
* 1. 子查询必须放在小括号中 |
||||||
|
* 2. 子查询一般放在比较操作符的右边 |
||||||
|
* |
||||||
|
* @param where where 条件 |
||||||
|
*/ |
||||||
|
protected void processWhereSubSelect(Expression where) { |
||||||
|
if (where == null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if (where instanceof FromItem) { |
||||||
|
processFromItem((FromItem) where); |
||||||
|
return; |
||||||
|
} |
||||||
|
if (where.toString().indexOf("SELECT") > 0) { |
||||||
|
// 有子查询
|
||||||
|
if (where instanceof BinaryExpression) { |
||||||
|
// 比较符号 , and , or , 等等
|
||||||
|
BinaryExpression expression = (BinaryExpression) where; |
||||||
|
processWhereSubSelect(expression.getLeftExpression()); |
||||||
|
processWhereSubSelect(expression.getRightExpression()); |
||||||
|
} else if (where instanceof InExpression) { |
||||||
|
// in
|
||||||
|
InExpression expression = (InExpression) where; |
||||||
|
ItemsList itemsList = expression.getRightItemsList(); |
||||||
|
if (itemsList instanceof SubSelect) { |
||||||
|
processSelectBody(((SubSelect) itemsList).getSelectBody()); |
||||||
|
} |
||||||
|
} else if (where instanceof ExistsExpression) { |
||||||
|
// exists
|
||||||
|
ExistsExpression expression = (ExistsExpression) where; |
||||||
|
processWhereSubSelect(expression.getRightExpression()); |
||||||
|
} else if (where instanceof NotExpression) { |
||||||
|
// not exists
|
||||||
|
NotExpression expression = (NotExpression) where; |
||||||
|
processWhereSubSelect(expression.getExpression()); |
||||||
|
} else if (where instanceof Parenthesis) { |
||||||
|
Parenthesis expression = (Parenthesis) where; |
||||||
|
processWhereSubSelect(expression.getExpression()); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected void processSelectItem(SelectItem selectItem) { |
||||||
|
if (selectItem instanceof SelectExpressionItem) { |
||||||
|
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem; |
||||||
|
if (selectExpressionItem.getExpression() instanceof SubSelect) { |
||||||
|
processSelectBody(((SubSelect) selectExpressionItem.getExpression()).getSelectBody()); |
||||||
|
} else if (selectExpressionItem.getExpression() instanceof Function) { |
||||||
|
processFunction((Function) selectExpressionItem.getExpression()); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 处理函数 |
||||||
|
* <p>支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)<p> |
||||||
|
* <p> fixed gitee pulls/141</p> |
||||||
|
* |
||||||
|
* @param function 函数 |
||||||
|
*/ |
||||||
|
protected void processFunction(Function function) { |
||||||
|
ExpressionList parameters = function.getParameters(); |
||||||
|
if (parameters != null) { |
||||||
|
parameters.getExpressions().forEach(expression -> { |
||||||
|
if (expression instanceof SubSelect) { |
||||||
|
processSelectBody(((SubSelect) expression).getSelectBody()); |
||||||
|
} else if (expression instanceof Function) { |
||||||
|
processFunction((Function) expression); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 处理子查询等 |
||||||
|
*/ |
||||||
|
protected void processFromItem(FromItem fromItem) { |
||||||
|
if (fromItem instanceof SubJoin) { |
||||||
|
SubJoin subJoin = (SubJoin) fromItem; |
||||||
|
if (subJoin.getJoinList() != null) { |
||||||
|
processJoins(subJoin.getJoinList()); |
||||||
|
} |
||||||
|
if (subJoin.getLeft() != null) { |
||||||
|
processFromItem(subJoin.getLeft()); |
||||||
|
} |
||||||
|
} else if (fromItem instanceof SubSelect) { |
||||||
|
SubSelect subSelect = (SubSelect) fromItem; |
||||||
|
if (subSelect.getSelectBody() != null) { |
||||||
|
processSelectBody(subSelect.getSelectBody()); |
||||||
|
} |
||||||
|
} else if (fromItem instanceof ValuesList) { |
||||||
|
logger.debug("Perform a subquery, if you do not give us feedback"); |
||||||
|
} else if (fromItem instanceof LateralSubSelect) { |
||||||
|
LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem; |
||||||
|
if (lateralSubSelect.getSubSelect() != null) { |
||||||
|
SubSelect subSelect = lateralSubSelect.getSubSelect(); |
||||||
|
if (subSelect.getSelectBody() != null) { |
||||||
|
processSelectBody(subSelect.getSelectBody()); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 处理 joins |
||||||
|
* |
||||||
|
* @param joins join 集合 |
||||||
|
*/ |
||||||
|
private void processJoins(List<Join> joins) { |
||||||
|
//对于 on 表达式写在最后的 join,需要记录下前面多个 on 的表名
|
||||||
|
Deque<Table> tables = new LinkedList<>(); |
||||||
|
for (Join join : joins) { |
||||||
|
// 处理 on 表达式
|
||||||
|
FromItem fromItem = join.getRightItem(); |
||||||
|
if (fromItem instanceof Table) { |
||||||
|
Table fromTable = (Table) fromItem; |
||||||
|
// 获取 join 尾缀的 on 表达式列表
|
||||||
|
Collection<Expression> originOnExpressions = join.getOnExpressions(); |
||||||
|
// 正常 join on 表达式只有一个,立刻处理
|
||||||
|
if (originOnExpressions.size() == 1) { |
||||||
|
processJoin(join); |
||||||
|
continue; |
||||||
|
} |
||||||
|
tables.push(fromTable); |
||||||
|
// 尾缀多个 on 表达式的时候统一处理
|
||||||
|
if (originOnExpressions.size() > 1) { |
||||||
|
Collection<Expression> onExpressions = new LinkedList<>(); |
||||||
|
for (Expression originOnExpression : originOnExpressions) { |
||||||
|
Table currentTable = tables.poll(); |
||||||
|
onExpressions.add(builderExpression(originOnExpression, currentTable)); |
||||||
|
} |
||||||
|
join.setOnExpressions(onExpressions); |
||||||
|
} |
||||||
|
} else { |
||||||
|
// 处理右边连接的子表达式
|
||||||
|
processFromItem(fromItem); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 处理联接语句 |
||||||
|
*/ |
||||||
|
protected void processJoin(Join join) { |
||||||
|
if (join.getRightItem() instanceof Table) { |
||||||
|
Table fromTable = (Table) join.getRightItem(); |
||||||
|
Expression originOnExpression = CollUtil.getFirst(join.getOnExpressions()); |
||||||
|
originOnExpression = builderExpression(originOnExpression, fromTable); |
||||||
|
join.setOnExpressions(CollUtil.newArrayList(originOnExpression)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 处理条件 |
||||||
|
*/ |
||||||
|
protected Expression builderExpression(Expression currentExpression, Table table) { |
||||||
|
// 获得 Table 对应的数据权限条件
|
||||||
|
Expression equalsTo = buildDataPermissionExpression(table); |
||||||
|
if (equalsTo == null) { // 如果没条件,则返回 currentExpression 默认
|
||||||
|
return currentExpression; |
||||||
|
} |
||||||
|
|
||||||
|
// 表达式为空,则直接返回 equalsTo
|
||||||
|
if (currentExpression == null) { |
||||||
|
return equalsTo; |
||||||
|
} |
||||||
|
// 如果表达式为 Or,则需要 (currentExpression) AND equalsTo
|
||||||
|
if (currentExpression instanceof OrExpression) { |
||||||
|
return new AndExpression(new Parenthesis(currentExpression), equalsTo); |
||||||
|
} |
||||||
|
// 如果表达式为 And,则直接返回 currentExpression AND equalsTo
|
||||||
|
return new AndExpression(currentExpression, equalsTo); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 构建指定表的数据权限的 Expression 过滤条件 |
||||||
|
* |
||||||
|
* @param table 表 |
||||||
|
* @return Expression 过滤条件 |
||||||
|
*/ |
||||||
|
private Expression buildDataPermissionExpression(Table table) { |
||||||
|
// 生成条件
|
||||||
|
Expression allExpression = null; |
||||||
|
for (DataPermissionRule rule : ContextHolder.getRules()) { |
||||||
|
// 判断表名是否匹配
|
||||||
|
if (!rule.getTableNames().contains(table.getName())) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
// 如果有匹配的规则,说明可重写。
|
||||||
|
// 为什么不是有 allExpression 非空才重写呢?在生成 column = value 过滤条件时,会因为 value 不存在,导致未重写。
|
||||||
|
// 这样导致第一次无 value,被标记成无需重写;但是第二次有 value,此时会需要重写。
|
||||||
|
ContextHolder.setRewrite(true); |
||||||
|
|
||||||
|
// 单条规则的条件
|
||||||
|
String tableName = MyBatisUtils.getTableName(table); |
||||||
|
Expression oneExpress = rule.getExpression(tableName, table.getAlias()); |
||||||
|
// 拼接到 allExpression 中
|
||||||
|
allExpression = allExpression == null ? oneExpress |
||||||
|
: new AndExpression(allExpression, oneExpress); |
||||||
|
} |
||||||
|
|
||||||
|
return allExpression; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 判断 SQL 是否重写。如果没有重写,则添加到 {@link MappedStatementCache} 中 |
||||||
|
* |
||||||
|
* @param ms MappedStatement |
||||||
|
*/ |
||||||
|
private void addMappedStatementCache(MappedStatement ms) { |
||||||
|
if (ContextHolder.getRewrite()) { |
||||||
|
return; |
||||||
|
} |
||||||
|
// 无重写,进行添加
|
||||||
|
mappedStatementCache.addNoRewritable(ms, ContextHolder.getRules()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* SQL 解析上下文,方便透传 {@link DataPermissionRule} 规则 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
static final class ContextHolder { |
||||||
|
|
||||||
|
/** |
||||||
|
* 该 {@link MappedStatement} 对应的规则 |
||||||
|
*/ |
||||||
|
private static final ThreadLocal<List<DataPermissionRule>> RULES = new TransmittableThreadLocal<>(); |
||||||
|
/** |
||||||
|
* SQL 是否进行重写 |
||||||
|
*/ |
||||||
|
private static final ThreadLocal<Boolean> REWRITE = new TransmittableThreadLocal<>(); |
||||||
|
|
||||||
|
public static void init(List<DataPermissionRule> rules) { |
||||||
|
RULES.set(rules); |
||||||
|
REWRITE.set(false); |
||||||
|
} |
||||||
|
|
||||||
|
public static void clear() { |
||||||
|
RULES.remove(); |
||||||
|
REWRITE.remove(); |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean getRewrite() { |
||||||
|
return REWRITE.get(); |
||||||
|
} |
||||||
|
|
||||||
|
public static void setRewrite(boolean rewrite) { |
||||||
|
REWRITE.set(rewrite); |
||||||
|
} |
||||||
|
|
||||||
|
public static List<DataPermissionRule> getRules() { |
||||||
|
return RULES.get(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link MappedStatement} 缓存 |
||||||
|
* 目前主要用于,记录 {@link DataPermissionRule} 是否对指定 {@link MappedStatement} 无效 |
||||||
|
* 如果无效,则可以避免 SQL 的解析,加快速度 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
static final class MappedStatementCache { |
||||||
|
|
||||||
|
/** |
||||||
|
* 指定数据权限规则,对指定 MappedStatement 无需重写(不生效)的缓存 |
||||||
|
* |
||||||
|
* value:{@link MappedStatement#getId()} 编号 |
||||||
|
*/ |
||||||
|
@Getter |
||||||
|
private final Map<Class<? extends DataPermissionRule>, Set<String>> noRewritableMappedStatements = new ConcurrentHashMap<>(); |
||||||
|
|
||||||
|
/** |
||||||
|
* 判断是否无需重写 |
||||||
|
* ps:虽然有点中文式英语,但是容易读懂即可 |
||||||
|
* |
||||||
|
* @param ms MappedStatement |
||||||
|
* @param rules 数据权限规则数组 |
||||||
|
* @return 是否无需重写 |
||||||
|
*/ |
||||||
|
public boolean noRewritable(MappedStatement ms, List<DataPermissionRule> rules) { |
||||||
|
// 如果规则为空,说明无需重写
|
||||||
|
if (CollUtil.isEmpty(rules)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
// 任一规则不在 noRewritableMap 中,则说明可能需要重写
|
||||||
|
for (DataPermissionRule rule : rules) { |
||||||
|
Set<String> mappedStatementIds = noRewritableMappedStatements.get(rule.getClass()); |
||||||
|
if (!CollUtil.contains(mappedStatementIds, ms.getId())) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 添加无需重写的 MappedStatement |
||||||
|
* |
||||||
|
* @param ms MappedStatement |
||||||
|
* @param rules 数据权限规则数组 |
||||||
|
*/ |
||||||
|
public void addNoRewritable(MappedStatement ms, List<DataPermissionRule> rules) { |
||||||
|
for (DataPermissionRule rule : rules) { |
||||||
|
Set<String> mappedStatementIds = noRewritableMappedStatements.get(rule.getClass()); |
||||||
|
if (CollUtil.isNotEmpty(mappedStatementIds)) { |
||||||
|
mappedStatementIds.add(ms.getId()); |
||||||
|
} else { |
||||||
|
noRewritableMappedStatements.put(rule.getClass(), SetUtils.asSet(ms.getId())); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 清空缓存 |
||||||
|
* 目前主要提供给单元测试 |
||||||
|
*/ |
||||||
|
public void clear() { |
||||||
|
noRewritableMappedStatements.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
/** |
||||||
|
* 基于部门的数据权限规则 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
package cn.iocoder.yudao.framework.datapermission.core.dept; |
@ -0,0 +1,192 @@ |
|||||||
|
package cn.iocoder.yudao.framework.datapermission.core.dept.rule; |
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil; |
||||||
|
import cn.hutool.core.util.StrUtil; |
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.dept.service.DeptDataPermissionFrameworkService; |
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; |
||||||
|
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; |
||||||
|
import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; |
||||||
|
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
||||||
|
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; |
||||||
|
import cn.iocoder.yudao.framework.security.core.LoginUser; |
||||||
|
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; |
||||||
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; |
||||||
|
import lombok.AllArgsConstructor; |
||||||
|
import lombok.Getter; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
import net.sf.jsqlparser.expression.Alias; |
||||||
|
import net.sf.jsqlparser.expression.Expression; |
||||||
|
import net.sf.jsqlparser.expression.LongValue; |
||||||
|
import net.sf.jsqlparser.expression.NullValue; |
||||||
|
import net.sf.jsqlparser.expression.operators.conditional.OrExpression; |
||||||
|
import net.sf.jsqlparser.expression.operators.relational.EqualsTo; |
||||||
|
import net.sf.jsqlparser.expression.operators.relational.ExpressionList; |
||||||
|
import net.sf.jsqlparser.expression.operators.relational.InExpression; |
||||||
|
|
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.HashSet; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Set; |
||||||
|
|
||||||
|
/** |
||||||
|
* 基于部门的 {@link DataPermissionRule} 数据权限规则实现 |
||||||
|
* |
||||||
|
* 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。 |
||||||
|
* |
||||||
|
* 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改? |
||||||
|
* 1. 一般情况下,dept_id 不进行修改,则会导致用户看到之前的数据。【yudao-server 采用该方案】 |
||||||
|
* 2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 DeptDataPermissionRule 的实现代码】 |
||||||
|
* 1)编写洗数据的脚本,将 dept_id 修改成新部门的编号;【建议】 |
||||||
|
* 最终过滤条件是 WHERE dept_id = ? |
||||||
|
* 2)洗数据的话,可能涉及的数据量较大,也可以采用 user_id 进行过滤的方式,此时需要获取到 dept_id 对应的所有 user_id 用户编号; |
||||||
|
* 最终过滤条件是 WHERE user_id IN (?, ?, ? ...) |
||||||
|
* 3)想要保证原 dept_id 和 user_id 都可以看的到,此时使用 dept_id 和 user_id 一起过滤; |
||||||
|
* 最终过滤条件是 WHERE dept_id = ? OR user_id IN (?, ?, ? ...) |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
@AllArgsConstructor |
||||||
|
@Slf4j |
||||||
|
public class DeptDataPermissionRule implements DataPermissionRule { |
||||||
|
|
||||||
|
private static final String DEPT_COLUMN_NAME = "dept_id"; |
||||||
|
private static final String USER_COLUMN_NAME = "user_id"; |
||||||
|
|
||||||
|
static final Expression EXPRESSION_NULL = new NullValue(); |
||||||
|
|
||||||
|
private final DeptDataPermissionFrameworkService deptDataPermissionService; |
||||||
|
|
||||||
|
/** |
||||||
|
* 基于部门的表字段配置 |
||||||
|
* 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 |
||||||
|
* |
||||||
|
* key:表名 |
||||||
|
* value:字段名 |
||||||
|
*/ |
||||||
|
private final Map<String, String> deptColumns = new HashMap<>(); |
||||||
|
/** |
||||||
|
* 基于用户的表字段配置 |
||||||
|
* 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 |
||||||
|
* |
||||||
|
* key:表名 |
||||||
|
* value:字段名 |
||||||
|
*/ |
||||||
|
private final Map<String, String> userColumns = new HashMap<>(); |
||||||
|
/** |
||||||
|
* 所有表名,是 {@link #deptColumns} 和 {@link #userColumns} 的合集 |
||||||
|
*/ |
||||||
|
private final Set<String> TABLE_NAMES = new HashSet<>(); |
||||||
|
|
||||||
|
@Override |
||||||
|
public Set<String> getTableNames() { |
||||||
|
return TABLE_NAMES; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Expression getExpression(String tableName, Alias tableAlias) { |
||||||
|
// 只有有登陆用户的情况下,才进行数据权限的处理
|
||||||
|
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); |
||||||
|
if (loginUser == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
// 获得数据权限
|
||||||
|
DeptDataPermissionRespDTO deptDataPermission = deptDataPermissionService.getDeptDataPermission(loginUser); |
||||||
|
if (deptDataPermission == null) { |
||||||
|
log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser)); |
||||||
|
throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限", |
||||||
|
loginUser.getId(), tableName, tableAlias.getName())); |
||||||
|
} |
||||||
|
|
||||||
|
// 情况一,如果是 ALL 可查看全部,则无需拼接条件
|
||||||
|
if (deptDataPermission.getAll()) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
// 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限
|
||||||
|
if (CollUtil.isEmpty(deptDataPermission.getDeptIds()) |
||||||
|
&& Boolean.FALSE.equals(deptDataPermission.getSelf())) { |
||||||
|
return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空
|
||||||
|
} |
||||||
|
|
||||||
|
// 情况三,拼接 Dept 和 User 的条件,最后组合
|
||||||
|
Expression deptExpression = this.buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds()); |
||||||
|
Expression userExpression = this.buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId()); |
||||||
|
if (deptExpression == null && userExpression == null) { |
||||||
|
// TODO 芋艿:获得不到条件的时候,暂时不抛出异常,而是不返回数据
|
||||||
|
log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]", |
||||||
|
JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission)); |
||||||
|
// throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空",
|
||||||
|
// loginUser.getId(), tableName, tableAlias.getName()));
|
||||||
|
return EXPRESSION_NULL; |
||||||
|
} |
||||||
|
if (deptExpression == null) { |
||||||
|
return userExpression; |
||||||
|
} |
||||||
|
if (userExpression == null) { |
||||||
|
return deptExpression; |
||||||
|
} |
||||||
|
// 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE dept_id IN ? OR user_id = ?
|
||||||
|
return new OrExpression(deptExpression, userExpression); |
||||||
|
} |
||||||
|
|
||||||
|
private Expression buildDeptExpression(String tableName, Alias tableAlias, Set<Long> deptIds) { |
||||||
|
// 如果不存在配置,则无需作为条件
|
||||||
|
String columnName = deptColumns.get(tableName); |
||||||
|
if (StrUtil.isEmpty(columnName)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
// 如果为空,则无条件
|
||||||
|
if (CollUtil.isEmpty(deptIds)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
// 拼接条件
|
||||||
|
return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), |
||||||
|
new ExpressionList(CollectionUtils.convertList(deptIds, LongValue::new))); |
||||||
|
} |
||||||
|
|
||||||
|
private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) { |
||||||
|
// 如果不查看自己,则无需作为条件
|
||||||
|
if (Boolean.FALSE.equals(self)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
String columnName = userColumns.get(tableName); |
||||||
|
if (StrUtil.isEmpty(columnName)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
// 拼接条件
|
||||||
|
return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId)); |
||||||
|
} |
||||||
|
|
||||||
|
// ==================== 添加配置 ====================
|
||||||
|
|
||||||
|
public void addDeptColumn(Class<? extends BaseDO> entityClass) { |
||||||
|
addDeptColumn(entityClass, DEPT_COLUMN_NAME); |
||||||
|
} |
||||||
|
|
||||||
|
public void addDeptColumn(Class<? extends BaseDO> entityClass, String columnName) { |
||||||
|
String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); |
||||||
|
addDeptColumn(tableName, columnName); |
||||||
|
} |
||||||
|
|
||||||
|
public void addDeptColumn(String tableName, String columnName) { |
||||||
|
deptColumns.put(tableName, columnName); |
||||||
|
TABLE_NAMES.add(tableName); |
||||||
|
} |
||||||
|
|
||||||
|
public void addUserColumn(Class<? extends BaseDO> entityClass) { |
||||||
|
addUserColumn(entityClass, USER_COLUMN_NAME); |
||||||
|
} |
||||||
|
|
||||||
|
public void addUserColumn(Class<? extends BaseDO> entityClass, String columnName) { |
||||||
|
String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); |
||||||
|
addUserColumn(tableName, columnName); |
||||||
|
} |
||||||
|
|
||||||
|
public void addUserColumn(String tableName, String columnName) { |
||||||
|
userColumns.put(tableName, columnName); |
||||||
|
TABLE_NAMES.add(tableName); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
package cn.iocoder.yudao.framework.datapermission.core.dept.rule; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link DeptDataPermissionRule} 的自定义配置接口 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
@FunctionalInterface |
||||||
|
public interface DeptDataPermissionRuleCustomizer { |
||||||
|
|
||||||
|
/** |
||||||
|
* 自定义该权限规则 |
||||||
|
* 1. 调用 {@link DeptDataPermissionRule#addDeptColumn(Class, String)} 方法,配置基于 dept_id 的过滤规则 |
||||||
|
* 2. 调用 {@link DeptDataPermissionRule#addUserColumn(Class, String)} 方法,配置基于 user_id 的过滤规则 |
||||||
|
* |
||||||
|
* @param rule 权限规则 |
||||||
|
*/ |
||||||
|
void customize(DeptDataPermissionRule rule); |
||||||
|
|
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
package cn.iocoder.yudao.framework.datapermission.core.dept.service; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; |
||||||
|
import cn.iocoder.yudao.framework.security.core.LoginUser; |
||||||
|
|
||||||
|
/** |
||||||
|
* 基于部门的数据权限 Framework Service 接口 |
||||||
|
* 目前的实现类是 SysPermissionServiceImpl 类 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public interface DeptDataPermissionFrameworkService { |
||||||
|
|
||||||
|
/** |
||||||
|
* 获得登陆用户的部门数据权限 |
||||||
|
* |
||||||
|
* @param loginUser 登陆用户 |
||||||
|
* @return 部门数据权限 |
||||||
|
*/ |
||||||
|
DeptDataPermissionRespDTO getDeptDataPermission(LoginUser loginUser); |
||||||
|
|
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
package cn.iocoder.yudao.framework.datapermission.core.dept.service.dto; |
||||||
|
|
||||||
|
import lombok.Data; |
||||||
|
|
||||||
|
import java.util.HashSet; |
||||||
|
import java.util.Set; |
||||||
|
|
||||||
|
/** |
||||||
|
* 部门的数据权限 Response DTO |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
@Data |
||||||
|
public class DeptDataPermissionRespDTO { |
||||||
|
|
||||||
|
/** |
||||||
|
* 是否可查看全部数据 |
||||||
|
*/ |
||||||
|
private Boolean all; |
||||||
|
/** |
||||||
|
* 是否可查看自己的数据 |
||||||
|
*/ |
||||||
|
private Boolean self; |
||||||
|
/** |
||||||
|
* 可查看的部门编号数组 |
||||||
|
*/ |
||||||
|
private Set<Long> deptIds; |
||||||
|
|
||||||
|
public DeptDataPermissionRespDTO() { |
||||||
|
this.all = false; |
||||||
|
this.self = false; |
||||||
|
this.deptIds = new HashSet<>(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
package cn.iocoder.yudao.framework.datapermission.core.rule; |
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; |
||||||
|
import net.sf.jsqlparser.expression.Alias; |
||||||
|
import net.sf.jsqlparser.expression.Expression; |
||||||
|
|
||||||
|
import java.util.Set; |
||||||
|
|
||||||
|
/** |
||||||
|
* 数据权限规则接口 |
||||||
|
* 通过实现接口,自定义数据规则。例如说, |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public interface DataPermissionRule { |
||||||
|
|
||||||
|
/** |
||||||
|
* 返回需要生效的表名数组 |
||||||
|
* 为什么需要该方法?Data Permission 数组基于 SQL 重写,通过 Where 返回只有权限的数据 |
||||||
|
* |
||||||
|
* 如果需要基于实体名获得表名,可调用 {@link TableInfoHelper#getTableInfo(Class)} 获得 |
||||||
|
* |
||||||
|
* @return 表名数组 |
||||||
|
*/ |
||||||
|
Set<String> getTableNames(); |
||||||
|
|
||||||
|
/** |
||||||
|
* 根据表名和别名,生成对应的 WHERE / OR 过滤条件 |
||||||
|
* |
||||||
|
* @param tableName 表名 |
||||||
|
* @param tableAlias 别名,可能为空 |
||||||
|
* @return 过滤条件 Expression 表达式 |
||||||
|
*/ |
||||||
|
Expression getExpression(String tableName, Alias tableAlias); |
||||||
|
|
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
package cn.iocoder.yudao.framework.datapermission.core.rule; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link DataPermissionRule} 工厂接口 |
||||||
|
* 作为 {@link DataPermissionRule} 的容器,提供管理能力 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public interface DataPermissionRuleFactory { |
||||||
|
|
||||||
|
/** |
||||||
|
* 获得所有数据权限规则数组 |
||||||
|
* |
||||||
|
* @return 数据权限规则数组 |
||||||
|
*/ |
||||||
|
List<DataPermissionRule> getDataPermissionRules(); |
||||||
|
|
||||||
|
/** |
||||||
|
* 获得指定 Mapper 的数据权限规则数组 |
||||||
|
* |
||||||
|
* @param mappedStatementId 指定 Mapper 的编号 |
||||||
|
* @return 数据权限规则数组 |
||||||
|
*/ |
||||||
|
List<DataPermissionRule> getDataPermissionRule(String mappedStatementId); |
||||||
|
|
||||||
|
} |
@ -0,0 +1,62 @@ |
|||||||
|
package cn.iocoder.yudao.framework.datapermission.core.rule; |
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil; |
||||||
|
import cn.hutool.core.util.ArrayUtil; |
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; |
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder; |
||||||
|
import lombok.RequiredArgsConstructor; |
||||||
|
|
||||||
|
import java.util.Collections; |
||||||
|
import java.util.List; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
/** |
||||||
|
* 默认的 DataPermissionRuleFactoryImpl 实现类 |
||||||
|
* 支持通过 {@link DataPermissionContextHolder} 过滤数据权限 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
@RequiredArgsConstructor |
||||||
|
public class DataPermissionRuleFactoryImpl implements DataPermissionRuleFactory { |
||||||
|
|
||||||
|
/** |
||||||
|
* 数据权限规则数组 |
||||||
|
*/ |
||||||
|
private final List<DataPermissionRule> rules; |
||||||
|
|
||||||
|
@Override |
||||||
|
public List<DataPermissionRule> getDataPermissionRules() { |
||||||
|
return rules; |
||||||
|
} |
||||||
|
|
||||||
|
@Override // mappedStatementId 参数,暂时没有用。以后,可以基于 mappedStatementId + DataPermission 进行缓存
|
||||||
|
public List<DataPermissionRule> getDataPermissionRule(String mappedStatementId) { |
||||||
|
// 1. 无数据权限
|
||||||
|
if (CollUtil.isEmpty(rules)) { |
||||||
|
return Collections.emptyList(); |
||||||
|
} |
||||||
|
// 2. 未配置,则默认开启
|
||||||
|
DataPermission dataPermission = DataPermissionContextHolder.get(); |
||||||
|
if (dataPermission == null) { |
||||||
|
return rules; |
||||||
|
} |
||||||
|
// 3. 已配置,但禁用
|
||||||
|
if (!dataPermission.enable()) { |
||||||
|
return Collections.emptyList(); |
||||||
|
} |
||||||
|
|
||||||
|
// 4. 已配置,只选择部分规则
|
||||||
|
if (ArrayUtil.isNotEmpty(dataPermission.includeRules())) { |
||||||
|
return rules.stream().filter(rule -> ArrayUtil.contains(dataPermission.includeRules(), rule.getClass())) |
||||||
|
.collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询
|
||||||
|
} |
||||||
|
// 5. 已配置,只排除部分规则
|
||||||
|
if (ArrayUtil.isNotEmpty(dataPermission.excludeRules())) { |
||||||
|
return rules.stream().filter(rule -> !ArrayUtil.contains(dataPermission.excludeRules(), rule.getClass())) |
||||||
|
.collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询
|
||||||
|
} |
||||||
|
// 6. 已配置,全部规则
|
||||||
|
return rules; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,4 @@ |
|||||||
|
/** |
||||||
|
* 基于 JSqlParser 解析 SQL,增加数据权限的 WHERE 条件 |
||||||
|
*/ |
||||||
|
package cn.iocoder.yudao.framework.datapermission; |
@ -0,0 +1,3 @@ |
|||||||
|
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
||||||
|
cn.iocoder.yudao.framework.datapermission.config.YudaoDataPermissionAutoConfiguration,\ |
||||||
|
cn.iocoder.yudao.framework.datapermission.config.YudaoDeptDataPermissionAutoConfiguration |
@ -0,0 +1,108 @@ |
|||||||
|
package cn.iocoder.yudao.framework.datapermission.core.aop; |
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil; |
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; |
||||||
|
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; |
||||||
|
import org.aopalliance.intercept.MethodInvocation; |
||||||
|
import org.junit.jupiter.api.BeforeEach; |
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.mockito.InjectMocks; |
||||||
|
import org.mockito.Mock; |
||||||
|
|
||||||
|
import java.lang.reflect.Method; |
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*; |
||||||
|
import static org.mockito.Mockito.when; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link DataPermissionAnnotationInterceptor} 的单元测试 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class DataPermissionAnnotationInterceptorTest extends BaseMockitoUnitTest { |
||||||
|
|
||||||
|
@InjectMocks |
||||||
|
private DataPermissionAnnotationInterceptor interceptor; |
||||||
|
|
||||||
|
@Mock |
||||||
|
private MethodInvocation methodInvocation; |
||||||
|
|
||||||
|
@BeforeEach |
||||||
|
public void setUp() { |
||||||
|
interceptor.getDataPermissionCache().clear(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test // 无 @DataPermission 注解
|
||||||
|
public void testInvoke_none() throws Throwable { |
||||||
|
// 参数
|
||||||
|
mockMethodInvocation(TestNone.class); |
||||||
|
|
||||||
|
// 调用
|
||||||
|
Object result = interceptor.invoke(methodInvocation); |
||||||
|
// 断言
|
||||||
|
assertEquals("none", result); |
||||||
|
assertEquals(1, interceptor.getDataPermissionCache().size()); |
||||||
|
assertTrue(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test // 在 Method 上有 @DataPermission 注解
|
||||||
|
public void testInvoke_method() throws Throwable { |
||||||
|
// 参数
|
||||||
|
mockMethodInvocation(TestMethod.class); |
||||||
|
|
||||||
|
// 调用
|
||||||
|
Object result = interceptor.invoke(methodInvocation); |
||||||
|
// 断言
|
||||||
|
assertEquals("method", result); |
||||||
|
assertEquals(1, interceptor.getDataPermissionCache().size()); |
||||||
|
assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test // 在 Class 上有 @DataPermission 注解
|
||||||
|
public void testInvoke_class() throws Throwable { |
||||||
|
// 参数
|
||||||
|
mockMethodInvocation(TestClass.class); |
||||||
|
|
||||||
|
// 调用
|
||||||
|
Object result = interceptor.invoke(methodInvocation); |
||||||
|
// 断言
|
||||||
|
assertEquals("class", result); |
||||||
|
assertEquals(1, interceptor.getDataPermissionCache().size()); |
||||||
|
assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); |
||||||
|
} |
||||||
|
|
||||||
|
private void mockMethodInvocation(Class<?> clazz) throws Throwable { |
||||||
|
Object targetObject = clazz.newInstance(); |
||||||
|
Method method = targetObject.getClass().getMethod("echo"); |
||||||
|
when(methodInvocation.getThis()).thenReturn(targetObject); |
||||||
|
when(methodInvocation.getMethod()).thenReturn(method); |
||||||
|
when(methodInvocation.proceed()).then(invocationOnMock -> method.invoke(targetObject)); |
||||||
|
} |
||||||
|
|
||||||
|
static class TestMethod { |
||||||
|
|
||||||
|
@DataPermission(enable = false) |
||||||
|
public String echo() { |
||||||
|
return "method"; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@DataPermission(enable = false) |
||||||
|
static class TestClass { |
||||||
|
|
||||||
|
public String echo() { |
||||||
|
return "class"; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static class TestNone { |
||||||
|
|
||||||
|
public String echo() { |
||||||
|
return "none"; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,66 @@ |
|||||||
|
package cn.iocoder.yudao.framework.datapermission.core.aop; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; |
||||||
|
import org.junit.jupiter.api.BeforeEach; |
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals; |
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame; |
||||||
|
import static org.mockito.Mockito.mock; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link DataPermissionContextHolder} 的单元测试 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
class DataPermissionContextHolderTest { |
||||||
|
|
||||||
|
@BeforeEach |
||||||
|
public void setUp() { |
||||||
|
DataPermissionContextHolder.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testGet() { |
||||||
|
// mock 方法
|
||||||
|
DataPermission dataPermission01 = mock(DataPermission.class); |
||||||
|
DataPermissionContextHolder.add(dataPermission01); |
||||||
|
DataPermission dataPermission02 = mock(DataPermission.class); |
||||||
|
DataPermissionContextHolder.add(dataPermission02); |
||||||
|
|
||||||
|
// 调用
|
||||||
|
DataPermission result = DataPermissionContextHolder.get(); |
||||||
|
// 断言
|
||||||
|
assertSame(result, dataPermission02); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testPush() { |
||||||
|
// 调用
|
||||||
|
DataPermission dataPermission01 = mock(DataPermission.class); |
||||||
|
DataPermissionContextHolder.add(dataPermission01); |
||||||
|
DataPermission dataPermission02 = mock(DataPermission.class); |
||||||
|
DataPermissionContextHolder.add(dataPermission02); |
||||||
|
// 断言
|
||||||
|
DataPermission first = DataPermissionContextHolder.getAll().get(0); |
||||||
|
DataPermission second = DataPermissionContextHolder.getAll().get(1); |
||||||
|
assertSame(dataPermission01, first); |
||||||
|
assertSame(dataPermission02, second); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testRemove() { |
||||||
|
// mock 方法
|
||||||
|
DataPermission dataPermission01 = mock(DataPermission.class); |
||||||
|
DataPermissionContextHolder.add(dataPermission01); |
||||||
|
DataPermission dataPermission02 = mock(DataPermission.class); |
||||||
|
DataPermissionContextHolder.add(dataPermission02); |
||||||
|
|
||||||
|
// 调用
|
||||||
|
DataPermission result = DataPermissionContextHolder.remove(); |
||||||
|
// 断言
|
||||||
|
assertSame(result, dataPermission02); |
||||||
|
assertEquals(1, DataPermissionContextHolder.getAll().size()); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,190 @@ |
|||||||
|
package cn.iocoder.yudao.framework.datapermission.core.db; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.util.collection.SetUtils; |
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; |
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory; |
||||||
|
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; |
||||||
|
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; |
||||||
|
import com.baomidou.mybatisplus.core.toolkit.PluginUtils; |
||||||
|
import net.sf.jsqlparser.expression.Alias; |
||||||
|
import net.sf.jsqlparser.expression.Expression; |
||||||
|
import net.sf.jsqlparser.expression.LongValue; |
||||||
|
import net.sf.jsqlparser.expression.operators.relational.EqualsTo; |
||||||
|
import net.sf.jsqlparser.schema.Column; |
||||||
|
import org.apache.ibatis.executor.Executor; |
||||||
|
import org.apache.ibatis.executor.statement.StatementHandler; |
||||||
|
import org.apache.ibatis.mapping.BoundSql; |
||||||
|
import org.apache.ibatis.mapping.MappedStatement; |
||||||
|
import org.junit.jupiter.api.BeforeEach; |
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.mockito.InjectMocks; |
||||||
|
import org.mockito.Mock; |
||||||
|
import org.mockito.MockedStatic; |
||||||
|
|
||||||
|
import java.sql.Connection; |
||||||
|
import java.util.*; |
||||||
|
|
||||||
|
import static java.util.Collections.singletonList; |
||||||
|
import static org.junit.jupiter.api.Assertions.*; |
||||||
|
import static org.mockito.Mockito.*; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link DataPermissionDatabaseInterceptor} 的单元测试 |
||||||
|
* 主要测试 {@link DataPermissionDatabaseInterceptor#beforePrepare(StatementHandler, Connection, Integer)} |
||||||
|
* 和 {@link DataPermissionDatabaseInterceptor#beforeUpdate(Executor, MappedStatement, Object)} |
||||||
|
* 以及在这个过程中,ContextHolder 和 MappedStatementCache |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class DataPermissionDatabaseInterceptorTest extends BaseMockitoUnitTest { |
||||||
|
|
||||||
|
@InjectMocks |
||||||
|
private DataPermissionDatabaseInterceptor interceptor; |
||||||
|
|
||||||
|
@Mock |
||||||
|
private DataPermissionRuleFactory ruleFactory; |
||||||
|
|
||||||
|
@BeforeEach |
||||||
|
public void setUp() { |
||||||
|
// 清理上下文
|
||||||
|
DataPermissionDatabaseInterceptor.ContextHolder.clear(); |
||||||
|
// 清空缓存
|
||||||
|
interceptor.getMappedStatementCache().clear(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test // 不存在规则,且不匹配
|
||||||
|
public void testBeforeQuery_withoutRule() { |
||||||
|
try (MockedStatic<PluginUtils> pluginUtilsMock = mockStatic(PluginUtils.class)) { |
||||||
|
// 准备参数
|
||||||
|
MappedStatement mappedStatement = mock(MappedStatement.class); |
||||||
|
BoundSql boundSql = mock(BoundSql.class); |
||||||
|
|
||||||
|
// 调用
|
||||||
|
interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql); |
||||||
|
// 断言
|
||||||
|
pluginUtilsMock.verify(never(), () -> PluginUtils.mpBoundSql(boundSql)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test // 存在规则,且不匹配
|
||||||
|
public void testBeforeQuery_withMatchRule() { |
||||||
|
try (MockedStatic<PluginUtils> pluginUtilsMock = mockStatic(PluginUtils.class)) { |
||||||
|
// 准备参数
|
||||||
|
MappedStatement mappedStatement = mock(MappedStatement.class); |
||||||
|
BoundSql boundSql = mock(BoundSql.class); |
||||||
|
// mock 方法(数据权限)
|
||||||
|
when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId()))) |
||||||
|
.thenReturn(singletonList(new DeptDataPermissionRule())); |
||||||
|
// mock 方法(MPBoundSql)
|
||||||
|
PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class); |
||||||
|
pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs); |
||||||
|
// mock 方法(SQL)
|
||||||
|
String sql = "select * from t_user where id = 1"; |
||||||
|
when(mpBs.sql()).thenReturn(sql); |
||||||
|
// 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确
|
||||||
|
|
||||||
|
// 调用
|
||||||
|
interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql); |
||||||
|
// 断言
|
||||||
|
verify(mpBs, times(1)).sql( |
||||||
|
eq("SELECT * FROM t_user WHERE id = 1 AND dept_id = 100")); |
||||||
|
// 断言缓存
|
||||||
|
assertTrue(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test // 存在规则,但不匹配
|
||||||
|
public void testBeforeQuery_withoutMatchRule() { |
||||||
|
try (MockedStatic<PluginUtils> pluginUtilsMock = mockStatic(PluginUtils.class)) { |
||||||
|
// 准备参数
|
||||||
|
MappedStatement mappedStatement = mock(MappedStatement.class); |
||||||
|
BoundSql boundSql = mock(BoundSql.class); |
||||||
|
// mock 方法(数据权限)
|
||||||
|
when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId()))) |
||||||
|
.thenReturn(singletonList(new DeptDataPermissionRule())); |
||||||
|
// mock 方法(MPBoundSql)
|
||||||
|
PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class); |
||||||
|
pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs); |
||||||
|
// mock 方法(SQL)
|
||||||
|
String sql = "select * from t_role where id = 1"; |
||||||
|
when(mpBs.sql()).thenReturn(sql); |
||||||
|
// 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确
|
||||||
|
|
||||||
|
// 调用
|
||||||
|
interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql); |
||||||
|
// 断言
|
||||||
|
verify(mpBs, times(1)).sql( |
||||||
|
eq("SELECT * FROM t_role WHERE id = 1")); |
||||||
|
// 断言缓存
|
||||||
|
assertFalse(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testAddNoRewritable() { |
||||||
|
// 准备参数
|
||||||
|
MappedStatement ms = mock(MappedStatement.class); |
||||||
|
List<DataPermissionRule> rules = singletonList(new DeptDataPermissionRule()); |
||||||
|
// mock 方法
|
||||||
|
when(ms.getId()).thenReturn("selectById"); |
||||||
|
|
||||||
|
// 调用
|
||||||
|
interceptor.getMappedStatementCache().addNoRewritable(ms, rules); |
||||||
|
// 断言
|
||||||
|
Map<Class<? extends DataPermissionRule>, Set<String>> noRewritableMappedStatements = |
||||||
|
interceptor.getMappedStatementCache().getNoRewritableMappedStatements(); |
||||||
|
assertEquals(1, noRewritableMappedStatements.size()); |
||||||
|
assertEquals(SetUtils.asSet("selectById"), noRewritableMappedStatements.get(DeptDataPermissionRule.class)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testNoRewritable() { |
||||||
|
// 准备参数
|
||||||
|
MappedStatement ms = mock(MappedStatement.class); |
||||||
|
// mock 方法
|
||||||
|
when(ms.getId()).thenReturn("selectById"); |
||||||
|
// mock 数据
|
||||||
|
List<DataPermissionRule> rules = singletonList(new DeptDataPermissionRule()); |
||||||
|
interceptor.getMappedStatementCache().addNoRewritable(ms, rules); |
||||||
|
|
||||||
|
// 场景一,rules 为空
|
||||||
|
assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, null)); |
||||||
|
// 场景二,rules 非空,可重写
|
||||||
|
assertFalse(interceptor.getMappedStatementCache().noRewritable(ms, singletonList(new EmptyDataPermissionRule()))); |
||||||
|
// 场景三,rule 非空,不可重写
|
||||||
|
assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, rules)); |
||||||
|
} |
||||||
|
|
||||||
|
private static class DeptDataPermissionRule implements DataPermissionRule { |
||||||
|
|
||||||
|
private static final String COLUMN = "dept_id"; |
||||||
|
|
||||||
|
@Override |
||||||
|
public Set<String> getTableNames() { |
||||||
|
return SetUtils.asSet("t_user"); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Expression getExpression(String tableName, Alias tableAlias) { |
||||||
|
Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); |
||||||
|
LongValue value = new LongValue(100L); |
||||||
|
return new EqualsTo(column, value); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
private static class EmptyDataPermissionRule implements DataPermissionRule { |
||||||
|
|
||||||
|
@Override |
||||||
|
public Set<String> getTableNames() { |
||||||
|
return Collections.emptySet(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Expression getExpression(String tableName, Alias tableAlias) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,370 @@ |
|||||||
|
package cn.iocoder.yudao.framework.datapermission.core.db; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; |
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRuleFactory; |
||||||
|
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; |
||||||
|
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; |
||||||
|
import net.sf.jsqlparser.expression.Alias; |
||||||
|
import net.sf.jsqlparser.expression.Expression; |
||||||
|
import net.sf.jsqlparser.expression.LongValue; |
||||||
|
import net.sf.jsqlparser.expression.operators.relational.EqualsTo; |
||||||
|
import net.sf.jsqlparser.expression.operators.relational.ExpressionList; |
||||||
|
import net.sf.jsqlparser.expression.operators.relational.InExpression; |
||||||
|
import net.sf.jsqlparser.schema.Column; |
||||||
|
import org.junit.jupiter.api.BeforeEach; |
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.mockito.InjectMocks; |
||||||
|
import org.mockito.Mock; |
||||||
|
|
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Set; |
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; |
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link DataPermissionDatabaseInterceptor} 的单元测试 |
||||||
|
* 主要复用了 MyBatis Plus 的 TenantLineInnerInterceptorTest 的单元测试 |
||||||
|
* 不过它的单元测试不是很规范,考虑到是复用的,所以暂时不进行修改~ |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
public class DataPermissionDatabaseInterceptorTest2 extends BaseMockitoUnitTest { |
||||||
|
|
||||||
|
@InjectMocks |
||||||
|
private DataPermissionDatabaseInterceptor interceptor; |
||||||
|
|
||||||
|
@Mock |
||||||
|
private DataPermissionRuleFactory ruleFactory; |
||||||
|
|
||||||
|
@BeforeEach |
||||||
|
public void setUp() { |
||||||
|
// 租户的数据权限规则
|
||||||
|
DataPermissionRule tenantRule = new DataPermissionRule() { |
||||||
|
|
||||||
|
private static final String COLUMN = "tenant_id"; |
||||||
|
|
||||||
|
@Override |
||||||
|
public Set<String> getTableNames() { |
||||||
|
return asSet("entity", "entity1", "entity2", "t1", "t2", // 支持 MyBatis Plus 的单元测试
|
||||||
|
"t_user", "t_role"); // 满足自己的单元测试
|
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Expression getExpression(String tableName, Alias tableAlias) { |
||||||
|
Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); |
||||||
|
LongValue value = new LongValue(1L); |
||||||
|
return new EqualsTo(column, value); |
||||||
|
} |
||||||
|
|
||||||
|
}; |
||||||
|
// 部门的数据权限规则
|
||||||
|
DataPermissionRule deptRule = new DataPermissionRule() { |
||||||
|
|
||||||
|
private static final String COLUMN = "dept_id"; |
||||||
|
|
||||||
|
@Override |
||||||
|
public Set<String> getTableNames() { |
||||||
|
return asSet("t_user"); // 满足自己的单元测试
|
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Expression getExpression(String tableName, Alias tableAlias) { |
||||||
|
Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); |
||||||
|
ExpressionList values = new ExpressionList(new LongValue(10L), |
||||||
|
new LongValue(20L)); |
||||||
|
return new InExpression(column, values); |
||||||
|
} |
||||||
|
|
||||||
|
}; |
||||||
|
// 设置到上下文,保证
|
||||||
|
DataPermissionDatabaseInterceptor.ContextHolder.init(Arrays.asList(tenantRule, deptRule)); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void delete() { |
||||||
|
assertSql("delete from entity where id = ?", |
||||||
|
"DELETE FROM entity WHERE id = ? AND tenant_id = 1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void update() { |
||||||
|
assertSql("update entity set name = ? where id = ?", |
||||||
|
"UPDATE entity SET name = ? WHERE id = ? AND tenant_id = 1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void selectSingle() { |
||||||
|
// 单表
|
||||||
|
assertSql("select * from entity where id = ?", |
||||||
|
"SELECT * FROM entity WHERE id = ? AND tenant_id = 1"); |
||||||
|
|
||||||
|
assertSql("select * from entity where id = ? or name = ?", |
||||||
|
"SELECT * FROM entity WHERE (id = ? OR name = ?) AND tenant_id = 1"); |
||||||
|
|
||||||
|
assertSql("SELECT * FROM entity WHERE (id = ? OR name = ?)", |
||||||
|
"SELECT * FROM entity WHERE (id = ? OR name = ?) AND tenant_id = 1"); |
||||||
|
|
||||||
|
/* not */ |
||||||
|
assertSql("SELECT * FROM entity WHERE not (id = ? OR name = ?)", |
||||||
|
"SELECT * FROM entity WHERE NOT (id = ? OR name = ?) AND tenant_id = 1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void selectSubSelectIn() { |
||||||
|
/* in */ |
||||||
|
assertSql("SELECT * FROM entity e WHERE e.id IN (select e1.id from entity1 e1 where e1.id = ?)", |
||||||
|
"SELECT * FROM entity e WHERE e.id IN (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); |
||||||
|
// 在最前
|
||||||
|
assertSql("SELECT * FROM entity e WHERE e.id IN " + |
||||||
|
"(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?", |
||||||
|
"SELECT * FROM entity e WHERE e.id IN " + |
||||||
|
"(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1"); |
||||||
|
// 在最后
|
||||||
|
assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " + |
||||||
|
"(select e1.id from entity1 e1 where e1.id = ?)", |
||||||
|
"SELECT * FROM entity e WHERE e.id = ? AND e.id IN " + |
||||||
|
"(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); |
||||||
|
// 在中间
|
||||||
|
assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " + |
||||||
|
"(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?", |
||||||
|
"SELECT * FROM entity e WHERE e.id = ? AND e.id IN " + |
||||||
|
"(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void selectSubSelectEq() { |
||||||
|
/* = */ |
||||||
|
assertSql("SELECT * FROM entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?)", |
||||||
|
"SELECT * FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void selectSubSelectInnerNotEq() { |
||||||
|
/* inner not = */ |
||||||
|
assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?))", |
||||||
|
"SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1)) AND e.tenant_id = 1"); |
||||||
|
|
||||||
|
assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?) and e.id = ?)", |
||||||
|
"SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ?) AND e.tenant_id = 1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void selectSubSelectExists() { |
||||||
|
/* EXISTS */ |
||||||
|
assertSql("SELECT * FROM entity e WHERE EXISTS (select e1.id from entity1 e1 where e1.id = ?)", |
||||||
|
"SELECT * FROM entity e WHERE EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); |
||||||
|
|
||||||
|
|
||||||
|
/* NOT EXISTS */ |
||||||
|
assertSql("SELECT * FROM entity e WHERE NOT EXISTS (select e1.id from entity1 e1 where e1.id = ?)", |
||||||
|
"SELECT * FROM entity e WHERE NOT EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void selectSubSelect() { |
||||||
|
/* >= */ |
||||||
|
assertSql("SELECT * FROM entity e WHERE e.id >= (select e1.id from entity1 e1 where e1.id = ?)", |
||||||
|
"SELECT * FROM entity e WHERE e.id >= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); |
||||||
|
|
||||||
|
/* <= */ |
||||||
|
assertSql("SELECT * FROM entity e WHERE e.id <= (select e1.id from entity1 e1 where e1.id = ?)", |
||||||
|
"SELECT * FROM entity e WHERE e.id <= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); |
||||||
|
|
||||||
|
/* <> */ |
||||||
|
assertSql("SELECT * FROM entity e WHERE e.id <> (select e1.id from entity1 e1 where e1.id = ?)", |
||||||
|
"SELECT * FROM entity e WHERE e.id <> (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void selectFromSelect() { |
||||||
|
assertSql("SELECT * FROM (select e.id from entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?))", |
||||||
|
"SELECT * FROM (SELECT e.id FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1)"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void selectBodySubSelect() { |
||||||
|
assertSql("select t1.col1,(select t2.col2 from t2 t2 where t1.col1=t2.col1) from t1 t1", |
||||||
|
"SELECT t1.col1, (SELECT t2.col2 FROM t2 t2 WHERE t1.col1 = t2.col1 AND t2.tenant_id = 1) FROM t1 t1 WHERE t1.tenant_id = 1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void selectLeftJoin() { |
||||||
|
// left join
|
||||||
|
assertSql("SELECT * FROM entity e " + |
||||||
|
"left join entity1 e1 on e1.id = e.id " + |
||||||
|
"WHERE e.id = ? OR e.name = ?", |
||||||
|
"SELECT * FROM entity e " + |
||||||
|
"LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + |
||||||
|
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); |
||||||
|
|
||||||
|
assertSql("SELECT * FROM entity e " + |
||||||
|
"left join entity1 e1 on e1.id = e.id " + |
||||||
|
"WHERE (e.id = ? OR e.name = ?)", |
||||||
|
"SELECT * FROM entity e " + |
||||||
|
"LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + |
||||||
|
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void selectRightJoin() { |
||||||
|
// right join
|
||||||
|
assertSql("SELECT * FROM entity e " + |
||||||
|
"right join entity1 e1 on e1.id = e.id", |
||||||
|
"SELECT * FROM entity e " + |
||||||
|
"RIGHT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + |
||||||
|
"WHERE e.tenant_id = 1"); |
||||||
|
|
||||||
|
assertSql("SELECT * FROM entity e " + |
||||||
|
"right join entity1 e1 on e1.id = e.id " + |
||||||
|
"WHERE e.id = ? OR e.name = ?", |
||||||
|
"SELECT * FROM entity e " + |
||||||
|
"RIGHT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + |
||||||
|
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void selectLeftJoinMultipleTrailingOn() { |
||||||
|
// 多个 on 尾缀的
|
||||||
|
assertSql("SELECT * FROM entity e " + |
||||||
|
"LEFT JOIN entity1 e1 " + |
||||||
|
"LEFT JOIN entity2 e2 ON e2.id = e1.id " + |
||||||
|
"ON e1.id = e.id " + |
||||||
|
"WHERE (e.id = ? OR e.NAME = ?)", |
||||||
|
"SELECT * FROM entity e " + |
||||||
|
"LEFT JOIN entity1 e1 " + |
||||||
|
"LEFT JOIN entity2 e2 ON e2.id = e1.id AND e2.tenant_id = 1 " + |
||||||
|
"ON e1.id = e.id AND e1.tenant_id = 1 " + |
||||||
|
"WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1"); |
||||||
|
|
||||||
|
assertSql("SELECT * FROM entity e " + |
||||||
|
"LEFT JOIN entity1 e1 " + |
||||||
|
"LEFT JOIN with_as_A e2 ON e2.id = e1.id " + |
||||||
|
"ON e1.id = e.id " + |
||||||
|
"WHERE (e.id = ? OR e.NAME = ?)", |
||||||
|
"SELECT * FROM entity e " + |
||||||
|
"LEFT JOIN entity1 e1 " + |
||||||
|
"LEFT JOIN with_as_A e2 ON e2.id = e1.id " + |
||||||
|
"ON e1.id = e.id AND e1.tenant_id = 1 " + |
||||||
|
"WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void selectInnerJoin() { |
||||||
|
// inner join
|
||||||
|
assertSql("SELECT * FROM entity e " + |
||||||
|
"inner join entity1 e1 on e1.id = e.id " + |
||||||
|
"WHERE e.id = ? OR e.name = ?", |
||||||
|
"SELECT * FROM entity e " + |
||||||
|
"INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + |
||||||
|
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); |
||||||
|
|
||||||
|
assertSql("SELECT * FROM entity e " + |
||||||
|
"inner join entity1 e1 on e1.id = e.id " + |
||||||
|
"WHERE (e.id = ? OR e.name = ?)", |
||||||
|
"SELECT * FROM entity e " + |
||||||
|
"INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + |
||||||
|
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); |
||||||
|
|
||||||
|
// 垃圾 inner join todo
|
||||||
|
// assertSql("SELECT * FROM entity,entity1 " +
|
||||||
|
// "WHERE entity.id = entity1.id",
|
||||||
|
// "SELECT * FROM entity e " +
|
||||||
|
// "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||||
|
// "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void selectWithAs() { |
||||||
|
assertSql("with with_as_A as (select * from entity) select * from with_as_A", |
||||||
|
"WITH with_as_A AS (SELECT * FROM entity WHERE tenant_id = 1) SELECT * FROM with_as_A"); |
||||||
|
} |
||||||
|
|
||||||
|
private void assertSql(String sql, String targetSql) { |
||||||
|
assertEquals(targetSql, interceptor.parserSingle(sql, null)); |
||||||
|
} |
||||||
|
|
||||||
|
// ========== 额外的测试 ==========
|
||||||
|
|
||||||
|
@Test |
||||||
|
public void testSelectSingle() { |
||||||
|
// 单表
|
||||||
|
assertSql("select * from t_user where id = ?", |
||||||
|
"SELECT * FROM t_user WHERE id = ? AND tenant_id = 1 AND dept_id IN (10, 20)"); |
||||||
|
|
||||||
|
assertSql("select * from t_user where id = ? or name = ?", |
||||||
|
"SELECT * FROM t_user WHERE (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)"); |
||||||
|
|
||||||
|
assertSql("SELECT * FROM t_user WHERE (id = ? OR name = ?)", |
||||||
|
"SELECT * FROM t_user WHERE (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)"); |
||||||
|
|
||||||
|
/* not */ |
||||||
|
assertSql("SELECT * FROM t_user WHERE not (id = ? OR name = ?)", |
||||||
|
"SELECT * FROM t_user WHERE NOT (id = ? OR name = ?) AND tenant_id = 1 AND dept_id IN (10, 20)"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testSelectLeftJoin() { |
||||||
|
// left join
|
||||||
|
assertSql("SELECT * FROM t_user e " + |
||||||
|
"left join t_role e1 on e1.id = e.id " + |
||||||
|
"WHERE e.id = ? OR e.name = ?", |
||||||
|
"SELECT * FROM t_user e " + |
||||||
|
"LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + |
||||||
|
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); |
||||||
|
|
||||||
|
// 条件 e.id = ? OR e.name = ? 带括号
|
||||||
|
assertSql("SELECT * FROM t_user e " + |
||||||
|
"left join t_role e1 on e1.id = e.id " + |
||||||
|
"WHERE (e.id = ? OR e.name = ?)", |
||||||
|
"SELECT * FROM t_user e " + |
||||||
|
"LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + |
||||||
|
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testSelectRightJoin() { |
||||||
|
// right join
|
||||||
|
assertSql("SELECT * FROM t_user e " + |
||||||
|
"right join t_role e1 on e1.id = e.id " + |
||||||
|
"WHERE e.id = ? OR e.name = ?", |
||||||
|
"SELECT * FROM t_user e " + |
||||||
|
"RIGHT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + |
||||||
|
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); |
||||||
|
|
||||||
|
// 条件 e.id = ? OR e.name = ? 带括号
|
||||||
|
assertSql("SELECT * FROM t_user e " + |
||||||
|
"right join t_role e1 on e1.id = e.id " + |
||||||
|
"WHERE (e.id = ? OR e.name = ?)", |
||||||
|
"SELECT * FROM t_user e " + |
||||||
|
"RIGHT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + |
||||||
|
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testSelectInnerJoin() { |
||||||
|
// inner join
|
||||||
|
assertSql("SELECT * FROM t_user e " + |
||||||
|
"inner join entity1 e1 on e1.id = e.id " + |
||||||
|
"WHERE e.id = ? OR e.name = ?", |
||||||
|
"SELECT * FROM t_user e " + |
||||||
|
"INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + |
||||||
|
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); |
||||||
|
|
||||||
|
// 条件 e.id = ? OR e.name = ? 带括号
|
||||||
|
assertSql("SELECT * FROM t_user e " + |
||||||
|
"inner join t_role e1 on e1.id = e.id " + |
||||||
|
"WHERE (e.id = ? OR e.name = ?)", |
||||||
|
"SELECT * FROM t_user e " + |
||||||
|
"INNER JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + |
||||||
|
"WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); |
||||||
|
|
||||||
|
// 垃圾 inner join todo
|
||||||
|
// assertSql("SELECT * FROM entity,entity1 " +
|
||||||
|
// "WHERE entity.id = entity1.id",
|
||||||
|
// "SELECT * FROM entity e " +
|
||||||
|
// "INNER JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " +
|
||||||
|
// "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1");
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,221 @@ |
|||||||
|
package cn.iocoder.yudao.framework.datapermission.core.dept.rule; |
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil; |
||||||
|
import cn.hutool.core.util.ReflectUtil; |
||||||
|
import cn.iocoder.yudao.framework.common.util.collection.SetUtils; |
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.dept.service.DeptDataPermissionFrameworkService; |
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.dept.service.dto.DeptDataPermissionRespDTO; |
||||||
|
import cn.iocoder.yudao.framework.security.core.LoginUser; |
||||||
|
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; |
||||||
|
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; |
||||||
|
import net.sf.jsqlparser.expression.Alias; |
||||||
|
import net.sf.jsqlparser.expression.Expression; |
||||||
|
import org.junit.jupiter.api.BeforeEach; |
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.mockito.InjectMocks; |
||||||
|
import org.mockito.Mock; |
||||||
|
import org.mockito.MockedStatic; |
||||||
|
|
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.datapermission.core.dept.rule.DeptDataPermissionRule.EXPRESSION_NULL; |
||||||
|
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; |
||||||
|
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; |
||||||
|
import static org.junit.jupiter.api.Assertions.*; |
||||||
|
import static org.mockito.ArgumentMatchers.same; |
||||||
|
import static org.mockito.Mockito.mockStatic; |
||||||
|
import static org.mockito.Mockito.when; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link DeptDataPermissionRule} 的单元测试 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { |
||||||
|
|
||||||
|
@InjectMocks |
||||||
|
private DeptDataPermissionRule rule; |
||||||
|
|
||||||
|
@Mock |
||||||
|
private DeptDataPermissionFrameworkService deptDataPermissionFrameworkService; |
||||||
|
|
||||||
|
@BeforeEach |
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
public void setUp() { |
||||||
|
// 清空 rule
|
||||||
|
rule.getTableNames().clear(); |
||||||
|
((Map<String, String>) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); |
||||||
|
((Map<String, String>) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test // 无 LoginUser
|
||||||
|
public void testGetExpression_noLoginUser() { |
||||||
|
// 准备参数
|
||||||
|
String tableName = randomString(); |
||||||
|
Alias tableAlias = new Alias(randomString()); |
||||||
|
// mock 方法
|
||||||
|
|
||||||
|
// 调用
|
||||||
|
Expression expression = rule.getExpression(tableName, tableAlias); |
||||||
|
// 断言
|
||||||
|
assertNull(expression); |
||||||
|
} |
||||||
|
|
||||||
|
@Test // 无数据权限时
|
||||||
|
public void testGetExpression_noDeptDataPermission() { |
||||||
|
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock |
||||||
|
= mockStatic(SecurityFrameworkUtils.class)) { |
||||||
|
// 准备参数
|
||||||
|
String tableName = "t_user"; |
||||||
|
Alias tableAlias = new Alias("u"); |
||||||
|
// mock 方法
|
||||||
|
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); |
||||||
|
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); |
||||||
|
|
||||||
|
// 调用
|
||||||
|
NullPointerException exception = assertThrows(NullPointerException.class, |
||||||
|
() -> rule.getExpression(tableName, tableAlias)); |
||||||
|
// 断言
|
||||||
|
assertEquals("LoginUser(1) Table(t_user/u) 未返回数据权限", exception.getMessage()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test // 全部数据权限
|
||||||
|
public void testGetExpression_allDeptDataPermission() { |
||||||
|
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock |
||||||
|
= mockStatic(SecurityFrameworkUtils.class)) { |
||||||
|
// 准备参数
|
||||||
|
String tableName = "t_user"; |
||||||
|
Alias tableAlias = new Alias("u"); |
||||||
|
// mock 方法(LoginUser)
|
||||||
|
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); |
||||||
|
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); |
||||||
|
// mock 方法(DeptDataPermissionRespDTO)
|
||||||
|
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO().setAll(true); |
||||||
|
when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); |
||||||
|
|
||||||
|
// 调用
|
||||||
|
Expression expression = rule.getExpression(tableName, tableAlias); |
||||||
|
// 断言
|
||||||
|
assertNull(expression); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test // 即不能查看部门,又不能查看自己,则说明 100% 无权限
|
||||||
|
public void testGetExpression_noDept_noSelf() { |
||||||
|
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock |
||||||
|
= mockStatic(SecurityFrameworkUtils.class)) { |
||||||
|
// 准备参数
|
||||||
|
String tableName = "t_user"; |
||||||
|
Alias tableAlias = new Alias("u"); |
||||||
|
// mock 方法(LoginUser)
|
||||||
|
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); |
||||||
|
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); |
||||||
|
// mock 方法(DeptDataPermissionRespDTO)
|
||||||
|
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO(); |
||||||
|
when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); |
||||||
|
|
||||||
|
// 调用
|
||||||
|
Expression expression = rule.getExpression(tableName, tableAlias); |
||||||
|
// 断言
|
||||||
|
assertEquals("null = null", expression.toString()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test // 拼接 Dept 和 User 的条件(字段都不符合)
|
||||||
|
public void testGetExpression_noDeptColumn_noSelfColumn() { |
||||||
|
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock |
||||||
|
= mockStatic(SecurityFrameworkUtils.class)) { |
||||||
|
// 准备参数
|
||||||
|
String tableName = "t_user"; |
||||||
|
Alias tableAlias = new Alias("u"); |
||||||
|
// mock 方法(LoginUser)
|
||||||
|
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); |
||||||
|
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); |
||||||
|
// mock 方法(DeptDataPermissionRespDTO)
|
||||||
|
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() |
||||||
|
.setDeptIds(SetUtils.asSet(10L, 20L)).setSelf(true); |
||||||
|
when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); |
||||||
|
|
||||||
|
// 调用
|
||||||
|
Expression expression = rule.getExpression(tableName, tableAlias); |
||||||
|
// 断言
|
||||||
|
assertSame(EXPRESSION_NULL, expression); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test // 拼接 Dept 和 User 的条件(self 符合)
|
||||||
|
public void testGetExpression_noDeptColumn_yesSelfColumn() { |
||||||
|
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock |
||||||
|
= mockStatic(SecurityFrameworkUtils.class)) { |
||||||
|
// 准备参数
|
||||||
|
String tableName = "t_user"; |
||||||
|
Alias tableAlias = new Alias("u"); |
||||||
|
// mock 方法(LoginUser)
|
||||||
|
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); |
||||||
|
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); |
||||||
|
// mock 方法(DeptDataPermissionRespDTO)
|
||||||
|
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() |
||||||
|
.setSelf(true); |
||||||
|
when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); |
||||||
|
// 添加 user 字段配置
|
||||||
|
rule.addUserColumn("t_user", "id"); |
||||||
|
|
||||||
|
// 调用
|
||||||
|
Expression expression = rule.getExpression(tableName, tableAlias); |
||||||
|
// 断言
|
||||||
|
assertEquals("u.id = 1", expression.toString()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test // 拼接 Dept 和 User 的条件(dept 符合)
|
||||||
|
public void testGetExpression_yesDeptColumn_noSelfColumn() { |
||||||
|
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock |
||||||
|
= mockStatic(SecurityFrameworkUtils.class)) { |
||||||
|
// 准备参数
|
||||||
|
String tableName = "t_user"; |
||||||
|
Alias tableAlias = new Alias("u"); |
||||||
|
// mock 方法(LoginUser)
|
||||||
|
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); |
||||||
|
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); |
||||||
|
// mock 方法(DeptDataPermissionRespDTO)
|
||||||
|
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() |
||||||
|
.setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)); |
||||||
|
when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); |
||||||
|
// 添加 dept 字段配置
|
||||||
|
rule.addDeptColumn("t_user", "dept_id"); |
||||||
|
|
||||||
|
// 调用
|
||||||
|
Expression expression = rule.getExpression(tableName, tableAlias); |
||||||
|
// 断言
|
||||||
|
assertEquals("u.dept_id IN (10, 20)", expression.toString()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test // 拼接 Dept 和 User 的条件(dept + self 符合)
|
||||||
|
public void testGetExpression_yesDeptColumn_yesSelfColumn() { |
||||||
|
try (MockedStatic<SecurityFrameworkUtils> securityFrameworkUtilsMock |
||||||
|
= mockStatic(SecurityFrameworkUtils.class)) { |
||||||
|
// 准备参数
|
||||||
|
String tableName = "t_user"; |
||||||
|
Alias tableAlias = new Alias("u"); |
||||||
|
// mock 方法(LoginUser)
|
||||||
|
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)); |
||||||
|
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); |
||||||
|
// mock 方法(DeptDataPermissionRespDTO)
|
||||||
|
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() |
||||||
|
.setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)).setSelf(true); |
||||||
|
when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission); |
||||||
|
// 添加 user 字段配置
|
||||||
|
rule.addUserColumn("t_user", "id"); |
||||||
|
// 添加 dept 字段配置
|
||||||
|
rule.addDeptColumn("t_user", "dept_id"); |
||||||
|
|
||||||
|
// 调用
|
||||||
|
Expression expression = rule.getExpression(tableName, tableAlias); |
||||||
|
// 断言
|
||||||
|
assertEquals("u.dept_id IN (10, 20) OR u.id = 1", expression.toString()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,145 @@ |
|||||||
|
package cn.iocoder.yudao.framework.datapermission.core.rule; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; |
||||||
|
import cn.iocoder.yudao.framework.datapermission.core.aop.DataPermissionContextHolder; |
||||||
|
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; |
||||||
|
import net.sf.jsqlparser.expression.Alias; |
||||||
|
import net.sf.jsqlparser.expression.Expression; |
||||||
|
import org.junit.jupiter.api.BeforeEach; |
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import org.mockito.InjectMocks; |
||||||
|
import org.mockito.Spy; |
||||||
|
import org.springframework.core.annotation.AnnotationUtils; |
||||||
|
|
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Set; |
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; |
||||||
|
import static org.junit.jupiter.api.Assertions.*; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link DataPermissionRuleFactoryImpl} 单元测试 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
class DataPermissionRuleFactoryImplTest extends BaseMockitoUnitTest { |
||||||
|
|
||||||
|
@InjectMocks |
||||||
|
private DataPermissionRuleFactoryImpl dataPermissionRuleFactory; |
||||||
|
|
||||||
|
@Spy |
||||||
|
private List<DataPermissionRule> rules = Arrays.asList(new DataPermissionRule01(), |
||||||
|
new DataPermissionRule02()); |
||||||
|
|
||||||
|
@BeforeEach |
||||||
|
public void setUp() { |
||||||
|
DataPermissionContextHolder.clear(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testGetDataPermissionRule_02() { |
||||||
|
// 准备参数
|
||||||
|
String mappedStatementId = randomString(); |
||||||
|
|
||||||
|
// 调用
|
||||||
|
List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); |
||||||
|
// 断言
|
||||||
|
assertSame(rules, result); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testGetDataPermissionRule_03() { |
||||||
|
// 准备参数
|
||||||
|
String mappedStatementId = randomString(); |
||||||
|
// mock 方法
|
||||||
|
DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass03.class, DataPermission.class)); |
||||||
|
|
||||||
|
// 调用
|
||||||
|
List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); |
||||||
|
// 断言
|
||||||
|
assertTrue(result.isEmpty()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testGetDataPermissionRule_04() { |
||||||
|
// 准备参数
|
||||||
|
String mappedStatementId = randomString(); |
||||||
|
// mock 方法
|
||||||
|
DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass04.class, DataPermission.class)); |
||||||
|
|
||||||
|
// 调用
|
||||||
|
List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); |
||||||
|
// 断言
|
||||||
|
assertEquals(1, result.size()); |
||||||
|
assertEquals(DataPermissionRule01.class, result.get(0).getClass()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testGetDataPermissionRule_05() { |
||||||
|
// 准备参数
|
||||||
|
String mappedStatementId = randomString(); |
||||||
|
// mock 方法
|
||||||
|
DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass05.class, DataPermission.class)); |
||||||
|
|
||||||
|
// 调用
|
||||||
|
List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); |
||||||
|
// 断言
|
||||||
|
assertEquals(1, result.size()); |
||||||
|
assertEquals(DataPermissionRule02.class, result.get(0).getClass()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testGetDataPermissionRule_06() { |
||||||
|
// 准备参数
|
||||||
|
String mappedStatementId = randomString(); |
||||||
|
// mock 方法
|
||||||
|
DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass06.class, DataPermission.class)); |
||||||
|
|
||||||
|
// 调用
|
||||||
|
List<DataPermissionRule> result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); |
||||||
|
// 断言
|
||||||
|
assertSame(rules, result); |
||||||
|
} |
||||||
|
|
||||||
|
@DataPermission(enable = false) |
||||||
|
static class TestClass03 {} |
||||||
|
|
||||||
|
@DataPermission(includeRules = DataPermissionRule01.class) |
||||||
|
static class TestClass04 {} |
||||||
|
|
||||||
|
@DataPermission(excludeRules = DataPermissionRule01.class) |
||||||
|
static class TestClass05 {} |
||||||
|
|
||||||
|
@DataPermission |
||||||
|
static class TestClass06 {} |
||||||
|
|
||||||
|
static class DataPermissionRule01 implements DataPermissionRule { |
||||||
|
|
||||||
|
@Override |
||||||
|
public Set<String> getTableNames() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Expression getExpression(String tableName, Alias tableAlias) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
static class DataPermissionRule02 implements DataPermissionRule { |
||||||
|
|
||||||
|
@Override |
||||||
|
public Set<String> getTableNames() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Expression getExpression(String tableName, Alias tableAlias) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" |
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||||
|
<parent> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-framework</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</parent> |
||||||
|
<modelVersion>4.0.0</modelVersion> |
||||||
|
<artifactId>yudao-spring-boot-starter-biz-dict</artifactId> |
||||||
|
<packaging>jar</packaging> |
||||||
|
|
||||||
|
<name>${project.artifactId}</name> |
||||||
|
<description>字典类型、数据</description> |
||||||
|
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
||||||
|
|
||||||
|
<dependencies> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-common</artifactId> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- Spring 核心 --> |
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework.boot</groupId> |
||||||
|
<artifactId>spring-boot-starter</artifactId> |
||||||
|
</dependency> |
||||||
|
</dependencies> |
||||||
|
</project> |
@ -0,0 +1,18 @@ |
|||||||
|
package cn.iocoder.yudao.framework.dict.config; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.dict.core.service.DictDataFrameworkService; |
||||||
|
import cn.iocoder.yudao.framework.dict.core.util.DictFrameworkUtils; |
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
|
||||||
|
@Configuration |
||||||
|
public class YudaoDictAutoConfiguration { |
||||||
|
|
||||||
|
@Bean |
||||||
|
@SuppressWarnings("InstantiationOfUtilityClass") |
||||||
|
public DictFrameworkUtils dictUtils(DictDataFrameworkService service) { |
||||||
|
DictFrameworkUtils.init(service); |
||||||
|
return new DictFrameworkUtils(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
package cn.iocoder.yudao.framework.dict.core.dto; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
||||||
|
import lombok.Data; |
||||||
|
|
||||||
|
/** |
||||||
|
* 字典数据 Response DTO |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
@Data |
||||||
|
public class DictDataRespDTO { |
||||||
|
|
||||||
|
/** |
||||||
|
* 字典标签 |
||||||
|
*/ |
||||||
|
private String label; |
||||||
|
/** |
||||||
|
* 字典值 |
||||||
|
*/ |
||||||
|
private String value; |
||||||
|
/** |
||||||
|
* 字典类型 |
||||||
|
*/ |
||||||
|
private String dictType; |
||||||
|
/** |
||||||
|
* 状态 |
||||||
|
* |
||||||
|
* 枚举 {@link CommonStatusEnum} |
||||||
|
*/ |
||||||
|
private Integer status; |
||||||
|
|
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
package cn.iocoder.yudao.framework.dict.core.service; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.dict.core.dto.DictDataRespDTO; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
public interface DictDataFrameworkService { |
||||||
|
|
||||||
|
/** |
||||||
|
* 获得指定的字典数据,从缓存中 |
||||||
|
* |
||||||
|
* @param type 字典类型 |
||||||
|
* @param value 字典数据值 |
||||||
|
* @return 字典数据 |
||||||
|
*/ |
||||||
|
DictDataRespDTO getDictDataFromCache(String type, String value); |
||||||
|
|
||||||
|
/** |
||||||
|
* 解析获得指定的字典数据,从缓存中 |
||||||
|
* |
||||||
|
* @param type 字典类型 |
||||||
|
* @param label 字典数据标签 |
||||||
|
* @return 字典数据 |
||||||
|
*/ |
||||||
|
DictDataRespDTO parseDictDataFromCache(String type, String label); |
||||||
|
|
||||||
|
/** |
||||||
|
* 获得指定类型的字典数据,从缓存中 |
||||||
|
* |
||||||
|
* @param type 字典类型 |
||||||
|
* @return 字典数据列表 |
||||||
|
*/ |
||||||
|
List<DictDataRespDTO> listDictDatasFromCache(String type); |
||||||
|
|
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
package cn.iocoder.yudao.framework.dict.core.util; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.dict.core.dto.DictDataRespDTO; |
||||||
|
import cn.iocoder.yudao.framework.dict.core.service.DictDataFrameworkService; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
|
||||||
|
/** |
||||||
|
* 字典工具类 |
||||||
|
*/ |
||||||
|
@Slf4j |
||||||
|
public class DictFrameworkUtils { |
||||||
|
|
||||||
|
private static DictDataFrameworkService service; |
||||||
|
|
||||||
|
public static void init(DictDataFrameworkService service) { |
||||||
|
DictFrameworkUtils.service = service; |
||||||
|
log.info("[init][初始化 DictFrameworkUtils 成功]"); |
||||||
|
} |
||||||
|
|
||||||
|
public static DictDataRespDTO getDictDataFromCache(String type, String value) { |
||||||
|
return service.getDictDataFromCache(type, value); |
||||||
|
} |
||||||
|
|
||||||
|
public static DictDataRespDTO parseDictDataFromCache(String type, String label) { |
||||||
|
return service.parseDictDataFromCache(type, label); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
/** |
||||||
|
* 字典数据模块,提供 {@link cn.iocoder.yudao.framework.dict.core.util.DictFrameworkUtils} 工具类 |
||||||
|
* |
||||||
|
* 通过将字典缓存在内存中,保证性能 |
||||||
|
*/ |
||||||
|
package cn.iocoder.yudao.framework.dict; |
@ -0,0 +1,2 @@ |
|||||||
|
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
||||||
|
cn.iocoder.yudao.framework.dict.config.YudaoDictAutoConfiguration |
@ -0,0 +1,44 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" |
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||||
|
<parent> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-framework</artifactId> |
||||||
|
<version>${revision}</version> |
||||||
|
</parent> |
||||||
|
<modelVersion>4.0.0</modelVersion> |
||||||
|
<artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId> |
||||||
|
<packaging>jar</packaging> |
||||||
|
|
||||||
|
<name>${project.artifactId}</name> |
||||||
|
<description>操作日志</description> |
||||||
|
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
||||||
|
|
||||||
|
<dependencies> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-common</artifactId> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- Spring 核心 --> |
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework.boot</groupId> |
||||||
|
<artifactId>spring-boot-starter-aop</artifactId> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- Web 相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>cn.iocoder.boot</groupId> |
||||||
|
<artifactId>yudao-spring-boot-starter-web</artifactId> |
||||||
|
<scope>provided</scope> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<!-- 工具类相关 --> |
||||||
|
<dependency> |
||||||
|
<groupId>com.google.guava</groupId> |
||||||
|
<artifactId>guava</artifactId> |
||||||
|
</dependency> |
||||||
|
</dependencies> |
||||||
|
|
||||||
|
</project> |
@ -0,0 +1,15 @@ |
|||||||
|
package cn.iocoder.yudao.framework.operatelog.config; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.operatelog.core.aop.OperateLogAspect; |
||||||
|
import org.springframework.context.annotation.Bean; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
|
||||||
|
@Configuration |
||||||
|
public class YudaoOperateLogAutoConfiguration { |
||||||
|
|
||||||
|
@Bean |
||||||
|
public OperateLogAspect operateLogAspect() { |
||||||
|
return new OperateLogAspect(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,57 @@ |
|||||||
|
package cn.iocoder.yudao.framework.operatelog.core.annotations; |
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum; |
||||||
|
import io.swagger.annotations.Api; |
||||||
|
import io.swagger.annotations.ApiOperation; |
||||||
|
|
||||||
|
import java.lang.annotation.ElementType; |
||||||
|
import java.lang.annotation.Retention; |
||||||
|
import java.lang.annotation.RetentionPolicy; |
||||||
|
import java.lang.annotation.Target; |
||||||
|
|
||||||
|
/** |
||||||
|
* 操作日志注解 |
||||||
|
* |
||||||
|
* @author 芋道源码 |
||||||
|
*/ |
||||||
|
@Target({ElementType.METHOD}) |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
public @interface OperateLog { |
||||||
|
|
||||||
|
// ========== 模块字段 ==========
|
||||||
|
|
||||||
|
/** |
||||||
|
* 操作模块 |
||||||
|
* |
||||||
|
* 为空时,会尝试读取 {@link Api#value()} 属性 |
||||||
|
*/ |
||||||
|
String module() default ""; |
||||||
|
/** |
||||||
|
* 操作名 |
||||||
|
* |
||||||
|
* 为空时,会尝试读取 {@link ApiOperation#value()} 属性 |
||||||
|
*/ |
||||||
|
String name() default ""; |
||||||
|
/** |
||||||
|
* 操作分类 |
||||||
|
* |
||||||
|
* 实际并不是数组,因为枚举不能设置 null 作为默认值 |
||||||
|
*/ |
||||||
|
OperateTypeEnum[] type() default {}; |
||||||
|
|
||||||
|
// ========== 开关字段 ==========
|
||||||
|
|
||||||
|
/** |
||||||
|
* 是否记录操作日志 |
||||||
|
*/ |
||||||
|
boolean enable() default true; |
||||||
|
/** |
||||||
|
* 是否记录方法参数 |
||||||
|
*/ |
||||||
|
boolean logArgs() default true; |
||||||
|
/** |
||||||
|
* 是否记录方法结果的数据 |
||||||
|
*/ |
||||||
|
boolean logResultData() default true; |
||||||
|
|
||||||
|
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue