初始化 spring cloud 项目的 system 和 infra 模块

This commit is contained in:
YunaiV 2022-06-01 23:59:01 +08:00
parent fcc5180ebf
commit 12e2ce99e1
1138 changed files with 65636 additions and 134 deletions

4
lombok.config Normal file
View File

@ -0,0 +1,4 @@
config.stopBubbling = true
lombok.tostring.callsuper=CALL
lombok.equalsandhashcode.callsuper=CALL
lombok.accessors.chain=true

View File

@ -10,14 +10,12 @@
<modules>
<module>yudao-dependencies</module>
<module>yudao-gateway</module>
<!-- <module>yudao-framework</module>-->
<!-- Server 主项目 -->
<!-- <module>yudao-server</module>-->
<module>yudao-framework</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-system</module>
<module>yudao-module-infra</module>
<!-- <module>yudao-module-pay</module>-->
</modules>

View File

@ -16,33 +16,33 @@
<properties>
<revision>1.6.2-snapshot</revision>
<!-- 统一依赖管理 -->
<spring.boot.version>2.6.7</spring.boot.version>
<spring.boot.version>2.6.8</spring.boot.version>
<spring.cloud.version>2021.0.1</spring.cloud.version>
<spring.cloud.alibaba.version>2021.0.1.0</spring.cloud.alibaba.version>
<!-- Web 相关 -->
<knife4j.version>3.0.2</knife4j.version>
<swagger-annotations.version>1.5.22</swagger-annotations.version>
<knife4j.version>3.0.3</knife4j.version>
<swagger-annotations.version>1.6.6</swagger-annotations.version>
<servlet.versoin>2.5</servlet.versoin>
<!-- DB 相关 -->
<druid.version>1.2.8</druid.version>
<mybatis-plus.version>3.4.3.4</mybatis-plus.version>
<mybatis-plus-generator.version>3.5.2</mybatis-plus-generator.version>
<dynamic-datasource.version>3.5.0</dynamic-datasource.version>
<redisson.version>3.17.0</redisson.version>
<redisson.version>3.17.3</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>
<resilience4j.version>1.7.1</resilience4j.version>
<!-- 监控相关 -->
<skywalking.version>8.7.0</skywalking.version>
<spring-boot-admin.version>2.6.2</spring-boot-admin.version>
<spring-boot-admin.version>2.6.7</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>
<mockito-inline.version>4.0.0</mockito-inline.version>
<!-- Bpm 工作流相关 -->
<activiti.version>7.1.0.M6</activiti.version>
<flowable.version>6.7.0</flowable.version>
@ -54,6 +54,7 @@
<easyexcel.verion>2.2.7</easyexcel.verion>
<velocity.version>2.2</velocity.version>
<screw.version>1.0.5</screw.version>
<fastjson.version>2.0.5</fastjson.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>
@ -94,46 +95,46 @@
</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>-->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-biz-dict</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-biz-sms</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-biz-pay</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-biz-weixin</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-biz-data-permission</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-biz-social</artifactId>
<version>${revision}</version>
</dependency>
<!-- Spring 核心 -->
<dependency>
@ -145,13 +146,13 @@
<!-- Web 相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-web</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-security</artifactId>
<version>${revision}</version>
</dependency>
@ -182,17 +183,12 @@
</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>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-mybatis</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
@ -214,11 +210,11 @@
<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>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-redis</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
@ -227,11 +223,11 @@
</dependency>
<!-- Config 配置中心相关 -->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-spring-boot-starter-config</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-config</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
@ -240,25 +236,25 @@
</dependency>
<!-- Job 定时任务相关 -->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-spring-boot-starter-job</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<dependency>
<groupId>cn.iocoder.cloud</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.cloud</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>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-protection</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
@ -284,11 +280,11 @@
</dependency>
<!-- 监控相关 -->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-spring-boot-starter-monitor</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-monitor</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.apache.skywalking</groupId>
@ -304,16 +300,16 @@
<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>-->
<!-- <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>
@ -343,12 +339,12 @@
</dependency>
<!-- Test 测试相关 -->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-spring-boot-starter-test</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- <scope>test</scope>-->
<!-- </dependency>-->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-test</artifactId>
<version>${revision}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
@ -414,17 +410,17 @@
<version>${activiti.version}</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-spring-boot-starter-activiti</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<dependency>
<groupId>cn.iocoder.cloud</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>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-flowable</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter-basic</artifactId>
@ -438,11 +434,11 @@
<!-- 工作流相关结束 -->
<!-- 工具类相关 -->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-common</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-common</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
@ -450,11 +446,11 @@
<version>${jasypt-spring-boot-starter.version}</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-spring-boot-starter-excel</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-excel</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
@ -496,6 +492,12 @@
<version>${velocity.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>cn.smallbun.screw</groupId>
<artifactId>screw-core</artifactId> <!-- 实现数据库文档 -->
@ -505,6 +507,10 @@
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId> <!-- 移除 Freemarker 依赖,采用 Velocity 作为模板引擎 -->
</exclusion>
<exclusion>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId> <!-- 最新版screw-core1.0.5依赖fastjson1.2.73存在漏洞,移除。 -->
</exclusion>
</exclusions>
</dependency>
@ -538,11 +544,11 @@
</dependency>
<!-- 三方云服务相关 -->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-spring-boot-starter-file</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-file</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>

55
yudao-framework/pom.xml Normal file
View File

@ -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.cloud</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>

View File

@ -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.cloud</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>

View File

@ -0,0 +1,15 @@
package cn.iocoder.yudao.framework.common.core;
/**
* 可生成 Int 数组的接口
*
* @author 芋道源码
*/
public interface IntArrayValuable {
/**
* @return int 数组
*/
int[] array();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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();
}
}

View File

@ -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]
}

View File

@ -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();
}
}

View File

@ -0,0 +1,6 @@
/**
* 基础的通用类和框架无关
*
* 例如说CommonResult 为通用返回
*/
package cn.iocoder.yudao.framework.common;

View File

@ -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());
}
}

View File

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

View File

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

View File

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

View File

@ -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()));
}
}

View File

@ -0,0 +1,187 @@
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> List<U> convertList(Collection<T> from, Function<T, U> func, Predicate<T> filter) {
if (CollUtil.isEmpty(from)) {
return new ArrayList<>();
}
return from.stream().filter(filter).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, U> Set<U> convertSet(Collection<T> from, Function<T, U> func, Predicate<T> filter) {
if (CollUtil.isEmpty(from)) {
return new HashSet<>();
}
return from.stream().filter(filter).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);
}
}

View File

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

View File

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

View File

@ -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();
}
}

View File

@ -0,0 +1,126 @@
package cn.iocoder.yudao.framework.common.util.http;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.map.TableMap;
import cn.hutool.core.net.url.UrlBuilder;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.Map;
/**
* 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();
}
private String append(String base, Map<String, ?> query, boolean fragment) {
return append(base, query, null, fragment);
}
/**
* 拼接 URL
*
* copy from Spring Security OAuth2 AuthorizationEndpoint 类的 append 方法
*
* @param base 基础 URL
* @param query 查询参数
* @param keys query key对应的原本的 key 的映射例如说 query 里有个 key xx实际它的 key extra_xx则通过 keys 里添加这个映射
* @param fragment URL fragment即拼接到 #
* @return 拼接后的 URL
*/
public static String append(String base, Map<String, ?> query, Map<String, String> keys, boolean fragment) {
UriComponentsBuilder template = UriComponentsBuilder.newInstance();
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base);
URI redirectUri;
try {
// assume it's encoded to start with (if it came in over the wire)
redirectUri = builder.build(true).toUri();
} catch (Exception e) {
// ... but allow client registrations to contain hard-coded non-encoded values
redirectUri = builder.build().toUri();
builder = UriComponentsBuilder.fromUri(redirectUri);
}
template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost())
.userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath());
if (fragment) {
StringBuilder values = new StringBuilder();
if (redirectUri.getFragment() != null) {
String append = redirectUri.getFragment();
values.append(append);
}
for (String key : query.keySet()) {
if (values.length() > 0) {
values.append("&");
}
String name = key;
if (keys != null && keys.containsKey(key)) {
name = keys.get(key);
}
values.append(name).append("={").append(key).append("}");
}
if (values.length() > 0) {
template.fragment(values.toString());
}
UriComponents encoded = template.build().expand(query).encode();
builder.fragment(encoded.getFragment());
} else {
for (String key : query.keySet()) {
String name = key;
if (keys != null && keys.containsKey(key)) {
name = keys.get(key);
}
template.queryParam(name, "{" + key + "}");
}
template.fragment(redirectUri.getFragment());
UriComponents encoded = template.build().expand(query).encode();
builder.query(encoded.getQuery());
}
return builder.build().toUriString();
}
public static String[] obtainBasicAuthorization(HttpServletRequest request) {
String clientId;
String clientSecret;
// 先从 Header 中获取
String authorization = request.getHeader("Authorization");
authorization = StrUtil.subAfter(authorization, "Basic ", true);
if (StringUtils.hasText(authorization)) {
authorization = Base64.decodeStr(authorization);
clientId = StrUtil.subBefore(authorization, ":", false);
clientSecret = StrUtil.subAfter(authorization, ":", false);
// 再从 Param 中获取
} else {
clientId = request.getParameter("client_id");
clientSecret = request.getParameter("client_secret");
}
// 如果两者非空则返回
if (StrUtil.isNotEmpty(clientId) && StrUtil.isNotEmpty(clientSecret)) {
return new String[]{clientId, clientSecret};
}
return null;
}
}

View File

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

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

View File

@ -0,0 +1,138 @@
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);
}
}
public static JsonNode parseTree(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 parseTree(byte[] text) {
try {
return objectMapper.readTree(text);
} catch (IOException e) {
log.error("json parse err,json:{}", text, e);
throw new RuntimeException(e);
}
}
public static boolean isJson(String text) {
return JSONUtil.isJson(text);
}
}

View File

@ -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();
}
}

View File

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

View File

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

View File

@ -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();
}
}

View File

@ -0,0 +1,7 @@
/**
* 对于工具类的选择优先查找 Hutool 中有没对应的方法
* 如果没有则自己封装对应的工具类 Utils 结尾用于区分
*
* ps如果担心 Hutool 存在坑的问题可以阅读 Hutool 的实现源码以确保可靠性并且可以补充相关的单元测试
*/
package cn.iocoder.yudao.framework.common.util;

View File

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

View File

@ -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();
}
}

View File

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

View File

@ -0,0 +1,40 @@
package cn.iocoder.yudao.framework.common.util.string;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import java.util.Collection;
/**
* 字符串工具类
*
* @author 芋道源码
*/
public class StrUtils {
public static String maxLength(CharSequence str, int maxLength) {
return StrUtil.maxLength(str, maxLength - 3); // -3 的原因是该方法会补充 ... 恰好
}
/**
* 给定字符串是否以任何一个字符串开始
* 给定字符串和数组为空都返回 false
*
* @param str 给定字符串
* @param prefixes 需要检测的开始字符串
* @since 3.0.6
*/
public static boolean startWithAny(String str, Collection<String> prefixes) {
if (StrUtil.isEmpty(str) || ArrayUtil.isEmpty(prefixes)) {
return false;
}
for (CharSequence suffix : prefixes) {
if (StrUtil.startWith(str, suffix, false)) {
return true;
}
}
return false;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
/**
* 使用 Hibernate Validator 实现参数校验
*/
package cn.iocoder.yudao.framework.common.validation;

View File

@ -0,0 +1 @@
<http://www.iocoder.cn/Spring-Boot/Validation/?yudao>

View File

@ -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.cloud</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.cloud</groupId>
<artifactId>yudao-common</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-security</artifactId>
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>cn.iocoder.cloud</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>

View File

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

View File

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

View File

@ -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();
}
}
}

View File

@ -0,0 +1 @@
package cn.iocoder.yudao.framework.activiti;

View File

@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.yudao.framework.activiti.config.YudaoActivitiConfiguration

View File

@ -0,0 +1,52 @@
<?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.cloud</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.cloud</groupId>
<artifactId>yudao-common</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-security</artifactId>
<optional>true</optional> <!-- 可选,如果使用 DeptDataPermissionRule 必须提供 -->
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-mybatis</artifactId>
</dependency>
<!-- 业务组件 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-module-system-api</artifactId> <!-- 需要使用它,进行数据权限的获取 -->
<version>${revision}</version>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -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();
}
}

View File

@ -0,0 +1,34 @@
package cn.iocoder.yudao.framework.datapermission.config;
import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRule;
import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
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 = {PermissionApi.class, DeptDataPermissionRuleCustomizer.class})
public class YudaoDeptDataPermissionAutoConfiguration {
@Bean
public DeptDataPermissionRule deptDataPermissionRule(PermissionApi permissionApi,
List<DeptDataPermissionRuleCustomizer> customizers) {
// 创建 DeptDataPermissionRule 对象
DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi);
// 补全表配置
customizers.forEach(customizer -> customizer.customize(rule));
return rule;
}
}

View File

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

View File

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

View File

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

View File

@ -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();
}
}

View File

@ -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();
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,208 @@
package cn.iocoder.yudao.framework.datapermission.core.rule.dept;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
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 cn.iocoder.yudao.module.system.api.permission.PermissionApi;
import cn.iocoder.yudao.module.system.api.permission.dto.DeptDataPermissionRespDTO;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import lombok.AllArgsConstructor;
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 {
/**
* LoginUser Context 缓存 Key
*/
protected static final String CONTEXT_KEY = DeptDataPermissionRule.class.getSimpleName();
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 PermissionApi permissionApi;
/**
* 基于部门的表字段配置
* 一般情况下每个表的部门编号字段是 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;
}
// 只有管理员类型的用户才进行数据权限的处理
if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) {
return null;
}
// 获得数据权限
DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class);
// 从上下文中拿不到则调用逻辑进行获取
if (deptDataPermission == null) {
deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId());
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()));
}
// 添加到上下文中避免重复计算
loginUser.setContext(CONTEXT_KEY, deptDataPermission);
}
// 情况一如果是 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 = buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds());
Expression userExpression = 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);
}
}

View File

@ -0,0 +1,20 @@
package cn.iocoder.yudao.framework.datapermission.core.rule.dept;
/**
* {@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);
}

View File

@ -0,0 +1,6 @@
/**
* 基于部门的数据权限规则
*
* @author 芋道源码
*/
package cn.iocoder.yudao.framework.datapermission.core.rule.dept;

View File

@ -0,0 +1,4 @@
/**
* 基于 JSqlParser 解析 SQL增加数据权限的 WHERE 条件
*/
package cn.iocoder.yudao.framework.datapermission;

View File

@ -0,0 +1,3 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.yudao.framework.datapermission.config.YudaoDataPermissionAutoConfiguration,\
cn.iocoder.yudao.framework.datapermission.config.YudaoDeptDataPermissionAutoConfiguration

View File

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

View File

@ -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());
}
}

View File

@ -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(() -> PluginUtils.mpBoundSql(boundSql), never());
}
}
@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;
}
}
}

View File

@ -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");
}
}

View File

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

View File

@ -0,0 +1,235 @@
package cn.iocoder.yudao.framework.datapermission.core.rule.dept;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
import cn.iocoder.yudao.module.system.api.permission.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.rule.dept.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 PermissionApi permissionApi;
@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)
.setUserType(UserTypeEnum.ADMIN.getValue()));
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)
.setUserType(UserTypeEnum.ADMIN.getValue()));
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
// mock 方法DeptDataPermissionRespDTO
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO().setAll(true);
when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
// 调用
Expression expression = rule.getExpression(tableName, tableAlias);
// 断言
assertNull(expression);
assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
}
}
@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)
.setUserType(UserTypeEnum.ADMIN.getValue()));
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
// mock 方法DeptDataPermissionRespDTO
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO();
when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
// 调用
Expression expression = rule.getExpression(tableName, tableAlias);
// 断言
assertEquals("null = null", expression.toString());
assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
}
}
@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)
.setUserType(UserTypeEnum.ADMIN.getValue()));
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
// mock 方法DeptDataPermissionRespDTO
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
.setDeptIds(SetUtils.asSet(10L, 20L)).setSelf(true);
when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
// 调用
Expression expression = rule.getExpression(tableName, tableAlias);
// 断言
assertSame(EXPRESSION_NULL, expression);
assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
}
}
@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)
.setUserType(UserTypeEnum.ADMIN.getValue()));
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
// mock 方法DeptDataPermissionRespDTO
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
.setSelf(true);
when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
// 添加 user 字段配置
rule.addUserColumn("t_user", "id");
// 调用
Expression expression = rule.getExpression(tableName, tableAlias);
// 断言
assertEquals("u.id = 1", expression.toString());
assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
}
}
@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)
.setUserType(UserTypeEnum.ADMIN.getValue()));
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
// mock 方法DeptDataPermissionRespDTO
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
.setDeptIds(CollUtil.newLinkedHashSet(10L, 20L));
when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission);
// 添加 dept 字段配置
rule.addDeptColumn("t_user", "dept_id");
// 调用
Expression expression = rule.getExpression(tableName, tableAlias);
// 断言
assertEquals("u.dept_id IN (10, 20)", expression.toString());
assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
}
}
@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)
.setUserType(UserTypeEnum.ADMIN.getValue()));
securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
// mock 方法DeptDataPermissionRespDTO
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
.setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)).setSelf(true);
when(permissionApi.getDeptDataPermission(same(1L))).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());
assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
}
}
}

View File

@ -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.cloud</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.cloud</groupId>
<artifactId>yudao-common</artifactId>
</dependency>
<!-- Spring 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -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();
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
/**
* 字典数据模块提供 {@link cn.iocoder.yudao.framework.dict.core.util.DictFrameworkUtils} 工具类
*
* 通过将字典缓存在内存中保证性能
*/
package cn.iocoder.yudao.framework.dict;

View File

@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.yudao.framework.dict.config.YudaoDictAutoConfiguration

View File

@ -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.cloud</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.cloud</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.cloud</groupId>
<artifactId>yudao-spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -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();
}
}

View File

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

View File

@ -0,0 +1,364 @@
package cn.iocoder.yudao.framework.operatelog.core.aop;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.framework.operatelog.core.dto.OperateLogCreateReqDTO;
import cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum;
import cn.iocoder.yudao.framework.operatelog.core.service.OperateLogFrameworkService;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import com.google.common.collect.Maps;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.IntStream;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.SUCCESS;
/**
* 拦截使用 @OperateLog 注解如果满足条件则生成操作日志
* 满足如下任一条件则会进行记录
* 1. 使用 @ApiOperation + @GetMapping
* 2. 使用 @OperateLog 注解
*
* 但是如果声明 @OperateLog 注解时 enable 属性设置为 false 强制不记录
*
* @author 芋道源码
*/
@Aspect
@Slf4j
public class OperateLogAspect {
/**
* 用于记录操作内容的上下文
*
* @see OperateLogCreateReqDTO#getContent()
*/
private static final ThreadLocal<String> CONTENT = new ThreadLocal<>();
/**
* 用于记录拓展字段的上下文
*
* @see OperateLogCreateReqDTO#getExts()
*/
private static final ThreadLocal<Map<String, Object>> EXTS = new ThreadLocal<>();
@Resource
private OperateLogFrameworkService operateLogFrameworkService;
@Around("@annotation(apiOperation)")
public Object around(ProceedingJoinPoint joinPoint, ApiOperation apiOperation) throws Throwable {
// 可能也添加了 @ApiOperation 注解
OperateLog operateLog = getMethodAnnotation(joinPoint, OperateLog.class);
return around0(joinPoint, operateLog, apiOperation);
}
@Around("!@annotation(io.swagger.annotations.ApiOperation) && @annotation(operateLog)") // 兼容处理只添加 @OperateLog 注解的情况
public Object around(ProceedingJoinPoint joinPoint, OperateLog operateLog) throws Throwable {
return around0(joinPoint, operateLog, null);
}
private Object around0(ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation) throws Throwable {
// 目前只有管理员才记录操作日志所以非管理员直接调用不进行记录
Integer userType = WebFrameworkUtils.getLoginUserType();
if (!Objects.equals(userType, UserTypeEnum.ADMIN.getValue())) {
return joinPoint.proceed();
}
// 记录开始时间
Date startTime = new Date();
try {
// 执行原有方法
Object result = joinPoint.proceed();
// 记录正常执行时的操作日志
this.log(joinPoint, operateLog, apiOperation, startTime, result, null);
return result;
} catch (Throwable exception) {
this.log(joinPoint, operateLog, apiOperation, startTime, null, exception);
throw exception;
} finally {
clearThreadLocal();
}
}
public static void setContent(String content) {
CONTENT.set(content);
}
public static void addExt(String key, Object value) {
if (EXTS.get() == null) {
EXTS.set(new HashMap<>());
}
EXTS.get().put(key, value);
}
private static void clearThreadLocal() {
CONTENT.remove();
EXTS.remove();
}
private void log(ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation,
Date startTime, Object result, Throwable exception) {
try {
// 判断不记录的情况
if (!isLogEnable(joinPoint, operateLog)) {
return;
}
// 真正记录操作日志
this.log0(joinPoint, operateLog, apiOperation, startTime, result, exception);
} catch (Throwable ex) {
log.error("[log][记录操作日志时,发生异常,其中参数是 joinPoint({}) operateLog({}) apiOperation({}) result({}) exception({}) ]",
joinPoint, operateLog, apiOperation, result, exception, ex);
}
}
private void log0(ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation,
Date startTime, Object result, Throwable exception) {
OperateLogCreateReqDTO operateLogDTO = new OperateLogCreateReqDTO();
// 补全通用字段
operateLogDTO.setTraceId(TracerUtils.getTraceId());
operateLogDTO.setStartTime(startTime);
// 补充用户信息
fillUserFields(operateLogDTO);
// 补全模块信息
fillModuleFields(operateLogDTO, joinPoint, operateLog, apiOperation);
// 补全请求信息
fillRequestFields(operateLogDTO);
// 补全方法信息
fillMethodFields(operateLogDTO, joinPoint, operateLog, startTime, result, exception);
// 异步记录日志
operateLogFrameworkService.createOperateLogAsync(operateLogDTO);
}
private static void fillUserFields(OperateLogCreateReqDTO operateLogDTO) {
operateLogDTO.setUserId(WebFrameworkUtils.getLoginUserId());
operateLogDTO.setUserType(WebFrameworkUtils.getLoginUserType());
}
private static void fillModuleFields(OperateLogCreateReqDTO operateLogDTO,
ProceedingJoinPoint joinPoint, OperateLog operateLog, ApiOperation apiOperation) {
// module 属性
if (operateLog != null) {
operateLogDTO.setModule(operateLog.module());
}
if (StrUtil.isEmpty(operateLogDTO.getModule())) {
Api api = getClassAnnotation(joinPoint, Api.class);
if (api != null) {
// 优先读取 @API name 属性
if (StrUtil.isNotEmpty(api.value())) {
operateLogDTO.setModule(api.value());
}
// 没有的话读取 @API tags 属性
if (StrUtil.isEmpty(operateLogDTO.getModule()) && ArrayUtil.isNotEmpty(api.tags())) {
operateLogDTO.setModule(api.tags()[0]);
}
}
}
// name 属性
if (operateLog != null) {
operateLogDTO.setName(operateLog.name());
}
if (StrUtil.isEmpty(operateLogDTO.getName()) && apiOperation != null) {
operateLogDTO.setName(apiOperation.value());
}
// type 属性
if (operateLog != null && ArrayUtil.isNotEmpty(operateLog.type())) {
operateLogDTO.setType(operateLog.type()[0].getType());
}
if (operateLogDTO.getType() == null) {
RequestMethod requestMethod = obtainFirstMatchRequestMethod(obtainRequestMethod(joinPoint));
OperateTypeEnum operateLogType = convertOperateLogType(requestMethod);
operateLogDTO.setType(operateLogType != null ? operateLogType.getType() : null);
}
// content exts 属性
operateLogDTO.setContent(CONTENT.get());
operateLogDTO.setExts(EXTS.get());
}
private static void fillRequestFields(OperateLogCreateReqDTO operateLogDTO) {
// 获得 Request 对象
HttpServletRequest request = ServletUtils.getRequest();
if (request == null) {
return;
}
// 补全请求信息
operateLogDTO.setRequestMethod(request.getMethod());
operateLogDTO.setRequestUrl(request.getRequestURI());
operateLogDTO.setUserIp(ServletUtil.getClientIP(request));
operateLogDTO.setUserAgent(ServletUtils.getUserAgent(request));
}
private static void fillMethodFields(OperateLogCreateReqDTO operateLogDTO,
ProceedingJoinPoint joinPoint, OperateLog operateLog,
Date startTime, Object result, Throwable exception) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
operateLogDTO.setJavaMethod(methodSignature.toString());
if (operateLog == null || operateLog.logArgs()) {
operateLogDTO.setJavaMethodArgs(obtainMethodArgs(joinPoint));
}
if (operateLog == null || operateLog.logResultData()) {
operateLogDTO.setResultData(obtainResultData(result));
}
operateLogDTO.setDuration((int) (System.currentTimeMillis() - startTime.getTime()));
// 正常处理 resultCode resultMsg 字段
if (result != null) {
if (result instanceof CommonResult) {
CommonResult<?> commonResult = (CommonResult<?>) result;
operateLogDTO.setResultCode(commonResult.getCode());
operateLogDTO.setResultMsg(commonResult.getMsg());
} else {
operateLogDTO.setResultCode(SUCCESS.getCode());
}
}
// 异常处理 resultCode resultMsg 字段
if (exception != null) {
operateLogDTO.setResultCode(INTERNAL_SERVER_ERROR.getCode());
operateLogDTO.setResultMsg(ExceptionUtil.getRootCauseMessage(exception));
}
}
private static boolean isLogEnable(ProceedingJoinPoint joinPoint, OperateLog operateLog) {
// @OperateLog 注解的情况下
if (operateLog != null) {
return operateLog.enable();
}
// 没有 @ApiOperation 注解的情况下只记录 POSTPUTDELETE 的情况
return obtainFirstLogRequestMethod(obtainRequestMethod(joinPoint)) != null;
}
private static RequestMethod obtainFirstLogRequestMethod(RequestMethod[] requestMethods) {
if (ArrayUtil.isEmpty(requestMethods)) {
return null;
}
return Arrays.stream(requestMethods).filter(requestMethod ->
requestMethod == RequestMethod.POST
|| requestMethod == RequestMethod.PUT
|| requestMethod == RequestMethod.DELETE)
.findFirst().orElse(null);
}
private static RequestMethod obtainFirstMatchRequestMethod(RequestMethod[] requestMethods) {
if (ArrayUtil.isEmpty(requestMethods)) {
return null;
}
// 优先匹配最优的 POSTPUTDELETE
RequestMethod result = obtainFirstLogRequestMethod(requestMethods);
if (result != null) {
return result;
}
// 然后匹配次优的 GET
result = Arrays.stream(requestMethods).filter(requestMethod -> requestMethod == RequestMethod.GET)
.findFirst().orElse(null);
if (result != null) {
return result;
}
// 兜底获得第一个
return requestMethods[0];
}
private static OperateTypeEnum convertOperateLogType(RequestMethod requestMethod) {
if (requestMethod == null) {
return null;
}
switch (requestMethod) {
case GET:
return OperateTypeEnum.GET;
case POST:
return OperateTypeEnum.CREATE;
case PUT:
return OperateTypeEnum.UPDATE;
case DELETE:
return OperateTypeEnum.DELETE;
default:
return OperateTypeEnum.OTHER;
}
}
private static RequestMethod[] obtainRequestMethod(ProceedingJoinPoint joinPoint) {
RequestMapping requestMapping = AnnotationUtils.getAnnotation( // 使用 Spring 的工具类可以处理 @RequestMapping 别名注解
((MethodSignature) joinPoint.getSignature()).getMethod(), RequestMapping.class);
return requestMapping != null ? requestMapping.method() : new RequestMethod[]{};
}
@SuppressWarnings("SameParameterValue")
private static <T extends Annotation> T getMethodAnnotation(ProceedingJoinPoint joinPoint, Class<T> annotationClass) {
return ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(annotationClass);
}
@SuppressWarnings("SameParameterValue")
private static <T extends Annotation> T getClassAnnotation(ProceedingJoinPoint joinPoint, Class<T> annotationClass) {
return ((MethodSignature) joinPoint.getSignature()).getMethod().getDeclaringClass().getAnnotation(annotationClass);
}
private static String obtainMethodArgs(ProceedingJoinPoint joinPoint) {
// TODO 提升参数脱敏和忽略
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String[] argNames = methodSignature.getParameterNames();
Object[] argValues = joinPoint.getArgs();
// 拼接参数
Map<String, Object> args = Maps.newHashMapWithExpectedSize(argValues.length);
for (int i = 0; i < argNames.length; i++) {
String argName = argNames[i];
Object argValue = argValues[i];
// 被忽略时标记为 ignore 字符串避免和 null 混在一起
args.put(argName, !isIgnoreArgs(argValue) ? argValue : "[ignore]");
}
return JsonUtils.toJsonString(args);
}
private static String obtainResultData(Object result) {
// TODO 提升结果脱敏和忽略
if (result instanceof CommonResult) {
result = ((CommonResult<?>) result).getData();
}
return JsonUtils.toJsonString(result);
}
private static boolean isIgnoreArgs(Object object) {
Class<?> clazz = object.getClass();
// 处理数组的情况
if (clazz.isArray()) {
return IntStream.range(0, Array.getLength(object))
.anyMatch(index -> isIgnoreArgs(Array.get(object, index)));
}
// 递归处理数组CollectionMap 的情况
if (Collection.class.isAssignableFrom(clazz)) {
return ((Collection<?>) object).stream()
.anyMatch((Predicate<Object>) OperateLogAspect::isIgnoreArgs);
}
if (Map.class.isAssignableFrom(clazz)) {
return isIgnoreArgs(((Map<?, ?>) object).values());
}
// obj
return object instanceof MultipartFile
|| object instanceof HttpServletRequest
|| object instanceof HttpServletResponse
|| object instanceof BindingResult;
}
}

View File

@ -0,0 +1,87 @@
package cn.iocoder.yudao.framework.operatelog.core.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Date;
import java.util.Map;
/**
* 操作日志创建 Request DTO
*/
@Data
public class OperateLogCreateReqDTO {
@ApiModelProperty(value = "链路追踪编号", required = true, example = "89aca178-a370-411c-ae02-3f0d672be4ab")
@NotEmpty(message = "链路追踪编号不能为空")
private String traceId;
@ApiModelProperty(value = "用户编号", required = true, example = "1024")
@NotNull(message = "用户编号不能为空")
private Long userId;
@ApiModelProperty(value = "用户类型", required = true, example = "1")
@NotNull(message = "用户类型不能为空")
private Integer userType;
@ApiModelProperty(value = "操作模块", required = true, example = "订单")
@NotEmpty(message = "操作模块不能为空")
private String module;
@ApiModelProperty(value = "操作名", required = true, example = "创建订单")
@NotEmpty(message = "操作名")
private String name;
@ApiModelProperty(value = "操作分类", required = true, example = "1", notes = "参见 SysOperateLogTypeEnum 枚举类")
@NotNull(message = "操作分类不能为空")
private Integer type;
@ApiModelProperty(value = "操作明细", example = "修改编号为 1 的用户信息,将性别从男改成女,将姓名从芋道改成源码。")
private String content;
@ApiModelProperty(value = "拓展字段", example = "{'orderId': 1}")
private Map<String, Object> exts;
@ApiModelProperty(value = "请求方法名", required = true, example = "GET")
@NotEmpty(message = "请求方法名不能为空")
private String requestMethod;
@ApiModelProperty(value = "请求地址", required = true, example = "/xxx/yyy")
@NotEmpty(message = "请求地址不能为空")
private String requestUrl;
@ApiModelProperty(value = "用户 IP", required = true, example = "127.0.0.1")
@NotEmpty(message = "用户 IP 不能为空")
private String userIp;
@ApiModelProperty(value = "浏览器 UserAgent", required = true, example = "Mozilla/5.0")
@NotEmpty(message = "浏览器 UserAgent 不能为空")
private String userAgent;
@ApiModelProperty(value = "Java 方法名", required = true, example = "cn.iocoder.yudao.UserController.save(...)")
@NotEmpty(message = "Java 方法名不能为空")
private String javaMethod;
@ApiModelProperty(value = "Java 方法的参数")
private String javaMethodArgs;
@ApiModelProperty(value = "开始时间", required = true)
@NotNull(message = "开始时间不能为空")
private Date startTime;
@ApiModelProperty(value = "执行时长,单位:毫秒", required = true)
@NotNull(message = "执行时长不能为空")
private Integer duration;
@ApiModelProperty(value = "结果码", required = true)
@NotNull(message = "结果码不能为空")
private Integer resultCode;
@ApiModelProperty(value = "结果提示")
private String resultMsg;
@ApiModelProperty(value = "结果数据")
private String resultData;
}

View File

@ -0,0 +1,55 @@
package cn.iocoder.yudao.framework.operatelog.core.enums;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 操作日志的操作类型
*
* @author ruoyi
*/
@Getter
@AllArgsConstructor
public enum OperateTypeEnum {
/**
* 查询
*
* 绝大多数情况下不会记录查询动作因为过于大量显得没有意义
* 在有需要的时候通过声明 {@link OperateLog} 注解来记录
*/
GET(1),
/**
* 新增
*/
CREATE(2),
/**
* 修改
*/
UPDATE(3),
/**
* 删除
*/
DELETE(4),
/**
* 导出
*/
EXPORT(5),
/**
* 导入
*/
IMPORT(6),
/**
* 其它
*
* 在无法归类时可以选择使用其它因为还有操作名可以进一步标识
*/
OTHER(0);
/**
* 类型
*/
private final Integer type;
}

View File

@ -0,0 +1 @@
package cn.iocoder.yudao.framework.operatelog.core;

View File

@ -0,0 +1,17 @@
package cn.iocoder.yudao.framework.operatelog.core.service;
import cn.iocoder.yudao.framework.operatelog.core.dto.OperateLogCreateReqDTO;
import java.util.concurrent.Future;
public interface OperateLogFrameworkService {
/**
* 异步记录操作日志
*
* @param reqVO 操作日志请求
* @return true: 记录成功,false: 记录失败
*/
Future<Boolean> createOperateLogAsync(OperateLogCreateReqDTO reqVO);
}

View File

@ -0,0 +1,21 @@
package cn.iocoder.yudao.framework.operatelog.core.util;
import cn.iocoder.yudao.framework.operatelog.core.aop.OperateLogAspect;
/**
* 操作日志工具类
* 目前主要的作用是提供给业务代码记录操作明细和拓展字段
*
* @author 芋道源码
*/
public class OperateLogUtils {
public static void setContent(String content) {
OperateLogAspect.setContent(content);
}
public static void addExt(String key, Object value) {
OperateLogAspect.addExt(key, value);
}
}

View File

@ -0,0 +1,6 @@
/**
* 用户操作日志记录用户的操作用于对用户的操作的审计与追溯永久保存
*
* @author 芋道源码
*/
package cn.iocoder.yudao.framework.operatelog;

View File

@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.yudao.framework.operatelog.config.YudaoOperateLogAutoConfiguration

View File

@ -0,0 +1,72 @@
<?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.cloud</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-spring-boot-starter-biz-pay</artifactId>
<name>${project.artifactId}</name>
<description>支付拓展,接入国内多个支付渠道
1. 支付宝,基于官方 SDK 接入
2. 微信支付,基于 weixin-java-pay 接入
</description>
<dependencies>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-common</artifactId>
</dependency>
<!-- Spring 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<!-- 三方云服务相关 -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.17.9.ALL</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>4.1.9.B</version>
</dependency>
<!-- TODO 芋艿:清理 -->
<!-- Test 测试相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,38 @@
package cn.iocoder.yudao.framework.pay.config;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotEmpty;
@ConfigurationProperties(prefix = "yudao.pay")
@Validated
@Data
public class PayProperties {
/**
* 支付回调地址
* 注意支付渠道统一回调到 payNotifyUrl 地址由支付模块统一处理然后自己的支付模块在回调 PayAppDO.payNotifyUrl 地址
*/
@NotEmpty(message = "支付回调地址不能为空")
@URL(message = "支付回调地址的格式必须是 URL")
private String payNotifyUrl;
/**
* 退款回调地址
* 注意点 {@link #payNotifyUrl} 属性
*/
@NotEmpty(message = "退款回调地址不能为空")
@URL(message = "退款回调地址的格式必须是 URL")
private String refundNotifyUrl;
/**
* 支付完成的返回地址
*/
@URL(message = "支付返回的地址的格式必须是 URL")
@NotEmpty(message = "支付返回的地址不能为空")
private String payReturnUrl;
}

View File

@ -0,0 +1,23 @@
package cn.iocoder.yudao.framework.pay.config;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.impl.PayClientFactoryImpl;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 支付配置类
*
* @author 芋道源码
*/
@Configuration
@EnableConfigurationProperties(PayProperties.class)
public class YudaoPayAutoConfiguration {
@Bean
public PayClientFactory payClientFactory() {
return new PayClientFactoryImpl();
}
}

View File

@ -0,0 +1,33 @@
package cn.iocoder.yudao.framework.pay.core.client;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants;
import lombok.extern.slf4j.Slf4j;
/**
* API 的错误码转换为通用的错误码
*
* @see PayCommonResult
* @see PayFrameworkErrorCodeConstants
*
* @author 芋道源码
*/
@Slf4j
public abstract class AbstractPayCodeMapping {
public final ErrorCode apply(String apiCode, String apiMsg) {
if (apiCode == null) {
log.error("[apply][API 错误码为空,请排查]");
return PayFrameworkErrorCodeConstants.EXCEPTION;
}
ErrorCode errorCode = this.apply0(apiCode, apiMsg);
if (errorCode == null) {
log.error("[apply][API 错误码({}) 错误提示({}) 无法匹配]", apiCode, apiMsg);
return PayFrameworkErrorCodeConstants.PAY_UNKNOWN;
}
return errorCode;
}
protected abstract ErrorCode apply0(String apiCode, String apiMsg);
}

View File

@ -0,0 +1,73 @@
package cn.iocoder.yudao.framework.pay.core.client;
import cn.iocoder.yudao.framework.pay.core.client.dto.*;
/**
* 支付客户端用于对接各支付渠道的 SDK实现发起支付退款等功能
*
* @author 芋道源码
*/
public interface PayClient {
/**
* 获得渠道编号
*
* @return 渠道编号
*/
Long getId();
/**
* 调用支付渠道统一下单
*
* @param reqDTO 下单信息
* @return 各支付渠道的返回结果
*/
PayCommonResult<?> unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
/**
* 解析支付单的通知结果
*
* @param data 通知结果
* @return 解析结果
* @throws Exception 解析失败抛出异常
*/
PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception;
/**
* 调用支付渠道进行退款
* @param reqDTO 统一退款请求信息
* @return 各支付渠道的统一返回结果
*/
PayCommonResult<PayRefundUnifiedRespDTO> unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
/**
* 解析支付退款通知数据
* @param notifyData 支付退款通知请求数据
* @return 支付退款通知的Notify DTO
*/
PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData);
// TODO @芋艿后续改成非 default避免不知道去实现
/**
* 验证是否渠道通知
*
* @param notifyData 通知数据
* @return 默认是 true
*/
default boolean verifyNotifyData(PayNotifyDataDTO notifyData) {
return true;
}
// TODO @芋艿后续改成非 default避免不知道去实现
/**
* 判断是否为退款通知
*
* @param notifyData 通知数据
* @return 默认是 false
*/
default boolean isRefundNotify(PayNotifyDataDTO notifyData){
return false;
}
}

View File

@ -0,0 +1,42 @@
package cn.iocoder.yudao.framework.pay.core.client;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validator;
import java.util.Set;
/**
* 支付客户端的配置本质是支付渠道的配置
* 每个不同的渠道需要不同的配置通过子类来定义
*
* @author 芋道源码
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
// @JsonTypeInfo 注解的作用Jackson 多态
// 1. 序列化到时数据库时增加 @class 属性
// 2. 反序列化到内存对象时通过 @class 属性可以创建出正确的类型
public interface PayClientConfig {
/**
* 配置验证参数是
*
* @param validator 校验对象
* @return 配置好的验证参数
*/
Set<ConstraintViolation<PayClientConfig>> verifyParam(Validator validator);
// TODO @aquan貌似抽象一个 validation group 就好了
/**
* 参数校验
*
* @param validator 校验对象
*/
default void validate(Validator validator) {
Set<ConstraintViolation<PayClientConfig>> violations = verifyParam(validator);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
}

View File

@ -0,0 +1,28 @@
package cn.iocoder.yudao.framework.pay.core.client;
/**
* 支付客户端的工厂接口
*
* @author 芋道源码
*/
public interface PayClientFactory {
/**
* 获得支付客户端
*
* @param channelId 渠道编号
* @return 支付客户端
*/
PayClient getPayClient(Long channelId);
/**
* 创建支付客户端
*
* @param channelId 渠道编号
* @param channelCode 渠道编码
* @param config 支付配置
*/
<Config extends PayClientConfig> void createOrUpdatePayClient(Long channelId, String channelCode,
Config config);
}

View File

@ -0,0 +1,57 @@
package cn.iocoder.yudao.framework.pay.core.client;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* 支付的 CommonResult 拓展类
*
* 考虑到不同的平台返回的 code msg 是不同的所以统一额外返回 {@link #apiCode} {@link #apiMsg} 字段
*
* @author 芋道源码
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class PayCommonResult<T> extends CommonResult<T> {
/**
* API 返回错误码
*
* 由于第三方的错误码可能是字符串所以使用 String 类型
*/
private String apiCode;
/**
* API 返回提示
*/
private String apiMsg;
private PayCommonResult() {
}
public static <T> PayCommonResult<T> build(String apiCode, String apiMsg, T data, AbstractPayCodeMapping codeMapping) {
Assert.notNull(codeMapping, "参数 codeMapping 不能为空");
PayCommonResult<T> result = new PayCommonResult<T>().setApiCode(apiCode).setApiMsg(apiMsg);
result.setData(data);
// 翻译错误码
if (codeMapping != null) {
ErrorCode errorCode = codeMapping.apply(apiCode, apiMsg);
result.setCode(errorCode.getCode()).setMsg(errorCode.getMsg());
}
return result;
}
public static <T> PayCommonResult<T> error(Throwable ex) {
PayCommonResult<T> result = new PayCommonResult<>();
result.setCode(PayFrameworkErrorCodeConstants.EXCEPTION.getCode());
result.setMsg(ExceptionUtil.getRootCauseMessage(ex));
return result;
}
}

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