commit
e8e149c2cb
1257 changed files with 111311 additions and 0 deletions
@ -0,0 +1,53 @@ |
|||||
|
###################################################################### |
||||
|
# Build Tools |
||||
|
|
||||
|
.gradle |
||||
|
/build/ |
||||
|
!gradle/wrapper/gradle-wrapper.jar |
||||
|
|
||||
|
target/ |
||||
|
!.mvn/wrapper/maven-wrapper.jar |
||||
|
|
||||
|
.flattened-pom.xml |
||||
|
|
||||
|
###################################################################### |
||||
|
# IDE |
||||
|
|
||||
|
### STS ### |
||||
|
.apt_generated |
||||
|
.classpath |
||||
|
.factorypath |
||||
|
.project |
||||
|
.settings |
||||
|
.springBeans |
||||
|
|
||||
|
### IntelliJ IDEA ### |
||||
|
.idea |
||||
|
*.iws |
||||
|
*.iml |
||||
|
*.ipr |
||||
|
|
||||
|
### NetBeans ### |
||||
|
nbproject/private/ |
||||
|
build/* |
||||
|
nbbuild/ |
||||
|
dist/ |
||||
|
nbdist/ |
||||
|
.nb-gradle/ |
||||
|
|
||||
|
###################################################################### |
||||
|
# Others |
||||
|
*.log |
||||
|
*.xml.versionsBackup |
||||
|
*.swp |
||||
|
|
||||
|
!*/build/*.java |
||||
|
!*/build/*.html |
||||
|
!*/build/*.xml |
||||
|
|
||||
|
### JRebel ### |
||||
|
rebel.xml |
||||
|
|
||||
|
application-my.yaml |
||||
|
|
||||
|
/platform-ui-app/unpackage/ |
||||
@ -0,0 +1,4 @@ |
|||||
|
# UDI云平台-后端 |
||||
|
|
||||
|
|
||||
|
|
||||
@ -0,0 +1,4 @@ |
|||||
|
config.stopBubbling = true |
||||
|
lombok.tostring.callsuper=CALL |
||||
|
lombok.equalsandhashcode.callsuper=CALL |
||||
|
lombok.accessors.chain=true |
||||
@ -0,0 +1,642 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
|
xmlns="http://maven.apache.org/POM/4.0.0" |
||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
|
<modelVersion>4.0.0</modelVersion> |
||||
|
|
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-dependencies</artifactId> |
||||
|
<version>${revision}</version> |
||||
|
<packaging>pom</packaging> |
||||
|
|
||||
|
<name>${project.artifactId}</name> |
||||
|
<description>基础 bom 文件,管理整个项目的依赖版本</description> |
||||
|
|
||||
|
<properties> |
||||
|
<revision>2.1.0-snapshot</revision> |
||||
|
<flatten-maven-plugin.version>1.5.0</flatten-maven-plugin.version> |
||||
|
<!-- 统一依赖管理 --> |
||||
|
<spring.boot.version>3.2.2</spring.boot.version> |
||||
|
<!-- Web 相关 --> |
||||
|
<springdoc.version>2.2.0</springdoc.version> |
||||
|
<knife4j.version>4.3.0</knife4j.version> |
||||
|
<!-- DB 相关 --> |
||||
|
<druid.version>1.2.21</druid.version> |
||||
|
<mybatis-plus.version>3.5.5</mybatis-plus.version> |
||||
|
<mybatis-plus-generator.version>3.5.5</mybatis-plus-generator.version> |
||||
|
<dynamic-datasource.version>4.3.0</dynamic-datasource.version> |
||||
|
<mybatis-plus-join.version>1.4.10</mybatis-plus-join.version> |
||||
|
<easy-trans.version>2.2.11</easy-trans.version> |
||||
|
<redisson.version>3.26.0</redisson.version> |
||||
|
<dm8.jdbc.version>8.1.3.62</dm8.jdbc.version> |
||||
|
<!-- 消息队列 --> |
||||
|
<rocketmq-spring.version>2.3.0</rocketmq-spring.version> |
||||
|
<!-- 服务保障相关 --> |
||||
|
<lock4j.version>2.2.7</lock4j.version> |
||||
|
<!-- 监控相关 --> |
||||
|
<skywalking.version>9.0.0</skywalking.version> |
||||
|
<spring-boot-admin.version>3.2.1</spring-boot-admin.version> |
||||
|
<opentracing.version>0.33.0</opentracing.version> |
||||
|
<!-- Test 测试相关 --> |
||||
|
<podam.version>8.0.1.RELEASE</podam.version> |
||||
|
<jedis-mock.version>1.0.13</jedis-mock.version> |
||||
|
<mockito-inline.version>5.2.0</mockito-inline.version> |
||||
|
<!-- Bpm 工作流相关 --> |
||||
|
<flowable.version>7.0.1</flowable.version> |
||||
|
<!-- 工具类相关 --> |
||||
|
<captcha-plus.version>2.0.3</captcha-plus.version> |
||||
|
<jsoup.version>1.17.2</jsoup.version> |
||||
|
<lombok.version>1.18.38</lombok.version> |
||||
|
<mapstruct.version>1.5.5.Final</mapstruct.version> |
||||
|
<hutool-5.version>5.8.25</hutool-5.version> |
||||
|
<hutool-6.version>6.0.0-M10</hutool-6.version> |
||||
|
<easyexcel.verion>3.3.3</easyexcel.verion> |
||||
|
<velocity.version>2.3</velocity.version> |
||||
|
<screw.version>1.0.5</screw.version> |
||||
|
<fastjson.version>1.2.83</fastjson.version> |
||||
|
<guava.version>33.0.0-jre</guava.version> |
||||
|
<guice.version>5.1.0</guice.version> |
||||
|
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version> |
||||
|
<commons-net.version>3.10.0</commons-net.version> |
||||
|
<jsch.version>0.1.55</jsch.version> |
||||
|
<tika-core.version>2.9.1</tika-core.version> |
||||
|
<ip2region.version>2.7.0</ip2region.version> |
||||
|
<bizlog-sdk.version>3.0.6</bizlog-sdk.version> |
||||
|
<!-- 三方云服务相关 --> |
||||
|
<okio.version>3.5.0</okio.version> |
||||
|
<okhttp3.version>4.11.0</okhttp3.version> |
||||
|
<commons-io.version>2.15.1</commons-io.version> |
||||
|
<minio.version>8.5.7</minio.version> |
||||
|
<aliyun-java-sdk-core.version>4.6.4</aliyun-java-sdk-core.version> |
||||
|
<aliyun-java-sdk-dysmsapi.version>2.2.1</aliyun-java-sdk-dysmsapi.version> |
||||
|
<tencentcloud-sdk-java.version>3.1.880</tencentcloud-sdk-java.version> |
||||
|
<justauth.version>2.0.5</justauth.version> |
||||
|
<jimureport.version>1.6.6-beta2</jimureport.version> |
||||
|
<xercesImpl.version>2.12.2</xercesImpl.version> |
||||
|
<weixin-java.version>4.6.0</weixin-java.version> |
||||
|
</properties> |
||||
|
|
||||
|
<dependencyManagement> |
||||
|
<dependencies> |
||||
|
<!-- 统一依赖管理 --> |
||||
|
<dependency> |
||||
|
<groupId>org.springframework.boot</groupId> |
||||
|
<artifactId>spring-boot-dependencies</artifactId> |
||||
|
<version>${spring.boot.version}</version> |
||||
|
<type>pom</type> |
||||
|
<scope>import</scope> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- 业务组件 --> |
||||
|
<dependency> |
||||
|
<groupId>io.github.mouzt</groupId> |
||||
|
<artifactId>bizlog-sdk</artifactId> |
||||
|
<version>${bizlog-sdk.version}</version> |
||||
|
<exclusions> |
||||
|
<exclusion> <!-- 排除掉springboot依赖使用项目的 --> |
||||
|
<groupId>org.springframework.boot</groupId> |
||||
|
<artifactId>spring-boot-starter</artifactId> |
||||
|
</exclusion> |
||||
|
</exclusions> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-spring-boot-starter-biz-tenant</artifactId> |
||||
|
<version>${revision}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-spring-boot-starter-biz-data-permission</artifactId> |
||||
|
<version>${revision}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-spring-boot-starter-biz-ip</artifactId> |
||||
|
<version>${revision}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- Spring 核心 --> |
||||
|
<dependency> |
||||
|
<!-- 用于生成自定义的 Spring @ConfigurationProperties 配置类的说明文件 --> |
||||
|
<groupId>org.springframework.boot</groupId> |
||||
|
<artifactId>spring-boot-configuration-processor</artifactId> |
||||
|
<version>${spring.boot.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- Web 相关 --> |
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-spring-boot-starter-web</artifactId> |
||||
|
<version>${revision}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-spring-boot-starter-security</artifactId> |
||||
|
<version>${revision}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-spring-boot-starter-websocket</artifactId> |
||||
|
<version>${revision}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.github.xiaoymin</groupId> |
||||
|
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId> |
||||
|
<version>${knife4j.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.springdoc</groupId> |
||||
|
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId> |
||||
|
<version>${springdoc.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.springdoc</groupId> |
||||
|
<artifactId>springdoc-openapi-ui</artifactId> |
||||
|
<version>${springdoc.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- DB 相关 --> |
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-spring-boot-starter-mybatis</artifactId> |
||||
|
<version>${revision}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.alibaba</groupId> |
||||
|
<artifactId>druid-spring-boot-3-starter</artifactId> |
||||
|
<version>${druid.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>com.baomidou</groupId> |
||||
|
<artifactId>mybatis-plus-spring-boot3-starter</artifactId> |
||||
|
<version>${mybatis-plus.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>com.baomidou</groupId> |
||||
|
<artifactId>mybatis-plus-generator</artifactId> <!-- 代码生成器,使用它解析表结构 --> |
||||
|
<version>${mybatis-plus-generator.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>com.baomidou</groupId> |
||||
|
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId> <!-- 多数据源 --> |
||||
|
<version>${dynamic-datasource.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>com.github.yulichang</groupId> |
||||
|
<artifactId>mybatis-plus-join-boot-starter</artifactId> <!-- MyBatis 联表查询 --> |
||||
|
<version>${mybatis-plus-join.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.fhs-opensource</groupId> <!-- VO 数据翻译 --> |
||||
|
<artifactId>easy-trans-spring-boot-starter</artifactId> |
||||
|
<version>${easy-trans.version}</version> |
||||
|
<exclusions> |
||||
|
<exclusion> |
||||
|
<groupId>org.springframework</groupId> |
||||
|
<artifactId>spring-context</artifactId> |
||||
|
</exclusion> |
||||
|
<exclusion> |
||||
|
<groupId>org.springframework.cloud</groupId> |
||||
|
<artifactId>spring-cloud-commons</artifactId> |
||||
|
</exclusion> |
||||
|
</exclusions> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>com.fhs-opensource</groupId> |
||||
|
<artifactId>easy-trans-mybatis-plus-extend</artifactId> |
||||
|
<version>${easy-trans.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>com.fhs-opensource</groupId> |
||||
|
<artifactId>easy-trans-anno</artifactId> |
||||
|
<version>${easy-trans.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-spring-boot-starter-redis</artifactId> |
||||
|
<version>${revision}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>org.redisson</groupId> |
||||
|
<artifactId>redisson-spring-boot-starter</artifactId> |
||||
|
<version>${redisson.version}</version> |
||||
|
<exclusions> |
||||
|
<exclusion> |
||||
|
<groupId>org.springframework.boot</groupId> |
||||
|
<artifactId>spring-boot-starter-actuator</artifactId> |
||||
|
</exclusion> |
||||
|
</exclusions> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.dameng</groupId> |
||||
|
<artifactId>DmJdbcDriver18</artifactId> |
||||
|
<version>${dm8.jdbc.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- Job 定时任务相关 --> |
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-spring-boot-starter-job</artifactId> |
||||
|
<version>${revision}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- 消息队列相关 --> |
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-spring-boot-starter-mq</artifactId> |
||||
|
<version>${revision}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>org.apache.rocketmq</groupId> |
||||
|
<artifactId>rocketmq-spring-boot-starter</artifactId> |
||||
|
<version>${rocketmq-spring.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- 服务保障相关 --> |
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-spring-boot-starter-protection</artifactId> |
||||
|
<version>${revision}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.baomidou</groupId> |
||||
|
<artifactId>lock4j-redisson-spring-boot-starter</artifactId> |
||||
|
<version>${lock4j.version}</version> |
||||
|
<exclusions> |
||||
|
<exclusion> |
||||
|
<artifactId>redisson-spring-boot-starter</artifactId> |
||||
|
<groupId>org.redisson</groupId> |
||||
|
</exclusion> |
||||
|
</exclusions> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- 监控相关 --> |
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-spring-boot-starter-monitor</artifactId> |
||||
|
<version>${revision}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>org.apache.skywalking</groupId> |
||||
|
<artifactId>apm-toolkit-trace</artifactId> |
||||
|
<version>${skywalking.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.apache.skywalking</groupId> |
||||
|
<artifactId>apm-toolkit-logback-1.x</artifactId> |
||||
|
<version>${skywalking.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.apache.skywalking</groupId> |
||||
|
<artifactId>apm-toolkit-opentracing</artifactId> |
||||
|
<version>${skywalking.version}</version> |
||||
|
<!-- <exclusions>--> |
||||
|
<!-- <exclusion>--> |
||||
|
<!-- <artifactId>opentracing-api</artifactId>--> |
||||
|
<!-- <groupId>io.opentracing</groupId>--> |
||||
|
<!-- </exclusion>--> |
||||
|
<!-- <exclusion>--> |
||||
|
<!-- <artifactId>opentracing-util</artifactId>--> |
||||
|
<!-- <groupId>io.opentracing</groupId>--> |
||||
|
<!-- </exclusion>--> |
||||
|
<!-- </exclusions>--> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>io.opentracing</groupId> |
||||
|
<artifactId>opentracing-api</artifactId> |
||||
|
<version>${opentracing.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>io.opentracing</groupId> |
||||
|
<artifactId>opentracing-util</artifactId> |
||||
|
<version>${opentracing.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>io.opentracing</groupId> |
||||
|
<artifactId>opentracing-noop</artifactId> |
||||
|
<version>${opentracing.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>de.codecentric</groupId> |
||||
|
<artifactId>spring-boot-admin-starter-server</artifactId> <!-- 实现 Spring Boot Admin Server 服务端 --> |
||||
|
<version>${spring-boot-admin.version}</version> |
||||
|
<exclusions> |
||||
|
<exclusion> |
||||
|
<groupId>de.codecentric</groupId> |
||||
|
<artifactId>spring-boot-admin-server-cloud</artifactId> |
||||
|
</exclusion> |
||||
|
</exclusions> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>de.codecentric</groupId> |
||||
|
<artifactId>spring-boot-admin-starter-client</artifactId> <!-- 实现 Spring Boot Admin Server 服务端 --> |
||||
|
<version>${spring-boot-admin.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>org.mockito</groupId> |
||||
|
<artifactId>mockito-inline</artifactId> |
||||
|
<version>${mockito-inline.version}</version> <!-- 支持 Mockito 的 final 类与 static 方法的 mock --> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>org.springframework.boot</groupId> |
||||
|
<artifactId>spring-boot-starter-test</artifactId> |
||||
|
<version>${spring.boot.version}</version> |
||||
|
<exclusions> |
||||
|
<exclusion> |
||||
|
<artifactId>asm</artifactId> |
||||
|
<groupId>org.ow2.asm</groupId> |
||||
|
</exclusion> |
||||
|
<exclusion> |
||||
|
<groupId>org.mockito</groupId> |
||||
|
<artifactId>mockito-core</artifactId> |
||||
|
</exclusion> |
||||
|
</exclusions> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.github.fppt</groupId> <!-- 单元测试,我们采用内嵌的 Redis 数据库 --> |
||||
|
<artifactId>jedis-mock</artifactId> |
||||
|
<version>${jedis-mock.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>uk.co.jemos.podam</groupId> <!-- 单元测试,随机生成 POJO 类 --> |
||||
|
<artifactId>podam</artifactId> |
||||
|
<version>${podam.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- 工作流相关 --> |
||||
|
<dependency> |
||||
|
<groupId>org.flowable</groupId> |
||||
|
<artifactId>flowable-spring-boot-starter-process</artifactId> |
||||
|
<version>${flowable.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.flowable</groupId> |
||||
|
<artifactId>flowable-spring-boot-starter-actuator</artifactId> |
||||
|
<version>${flowable.version}</version> |
||||
|
</dependency> |
||||
|
<!-- 工作流相关结束 --> |
||||
|
|
||||
|
<!-- 工具类相关 --> |
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-common</artifactId> |
||||
|
<version>${revision}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-spring-boot-starter-excel</artifactId> |
||||
|
<version>${revision}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>org.projectlombok</groupId> |
||||
|
<artifactId>lombok</artifactId> |
||||
|
<version>${lombok.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>org.mapstruct</groupId> |
||||
|
<artifactId>mapstruct</artifactId> <!-- use mapstruct-jdk8 for Java 8 or higher --> |
||||
|
<version>${mapstruct.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.mapstruct</groupId> |
||||
|
<artifactId>mapstruct-jdk8</artifactId> |
||||
|
<version>${mapstruct.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.mapstruct</groupId> |
||||
|
<artifactId>mapstruct-processor</artifactId> |
||||
|
<version>${mapstruct.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>cn.hutool</groupId> |
||||
|
<artifactId>hutool-all</artifactId> |
||||
|
<version>${hutool-5.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.dromara.hutool</groupId> |
||||
|
<artifactId>hutool-extra</artifactId> |
||||
|
<version>${hutool-6.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.alibaba</groupId> |
||||
|
<artifactId>easyexcel</artifactId> |
||||
|
<version>${easyexcel.verion}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>commons-io</groupId> |
||||
|
<artifactId>commons-io</artifactId> |
||||
|
<version>${commons-io.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.apache.tika</groupId> |
||||
|
<artifactId>tika-core</artifactId> <!-- 文件类型的识别 --> |
||||
|
<version>${tika-core.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>org.apache.velocity</groupId> |
||||
|
<artifactId>velocity-engine-core</artifactId> |
||||
|
<version>${velocity.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.alibaba</groupId> |
||||
|
<artifactId>fastjson</artifactId> |
||||
|
<version>${fastjson.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.google.guava</groupId> |
||||
|
<artifactId>guava</artifactId> |
||||
|
<version>${guava.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.google.inject</groupId> |
||||
|
<artifactId>guice</artifactId> |
||||
|
<version>${guice.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.alibaba</groupId> |
||||
|
<artifactId>transmittable-thread-local</artifactId> <!-- 解决 ThreadLocal 父子线程的传值问题 --> |
||||
|
<version>${transmittable-thread-local.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>commons-net</groupId> |
||||
|
<artifactId>commons-net</artifactId> <!-- 解决 ftp 连接 --> |
||||
|
<version>${commons-net.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.jcraft</groupId> |
||||
|
<artifactId>jsch</artifactId> <!-- 解决 sftp 连接 --> |
||||
|
<version>${jsch.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.xingyuv</groupId> |
||||
|
<artifactId>spring-boot-starter-captcha-plus</artifactId> |
||||
|
<version>${captcha-plus.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>org.lionsoul</groupId> |
||||
|
<artifactId>ip2region</artifactId> |
||||
|
<version>${ip2region.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>org.jsoup</groupId> |
||||
|
<artifactId>jsoup</artifactId> |
||||
|
<version>${jsoup.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- 三方云服务相关 --> |
||||
|
<dependency> |
||||
|
<groupId>com.squareup.okio</groupId> |
||||
|
<artifactId>okio</artifactId> |
||||
|
<version>${okio.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>com.squareup.okhttp3</groupId> |
||||
|
<artifactId>okhttp</artifactId> |
||||
|
<version>${okhttp3.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>io.minio</groupId> |
||||
|
<artifactId>minio</artifactId> |
||||
|
<version>${minio.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- SMS SDK begin --> |
||||
|
<dependency> |
||||
|
<groupId>com.aliyun</groupId> |
||||
|
<artifactId>aliyun-java-sdk-core</artifactId> |
||||
|
<version>${aliyun-java-sdk-core.version}</version> |
||||
|
<exclusions> |
||||
|
<exclusion> |
||||
|
<artifactId>opentracing-api</artifactId> |
||||
|
<groupId>io.opentracing</groupId> |
||||
|
</exclusion> |
||||
|
<exclusion> |
||||
|
<artifactId>opentracing-util</artifactId> |
||||
|
<groupId>io.opentracing</groupId> |
||||
|
</exclusion> |
||||
|
</exclusions> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>com.aliyun</groupId> |
||||
|
<artifactId>aliyun-java-sdk-dysmsapi</artifactId> |
||||
|
<version>${aliyun-java-sdk-dysmsapi.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>com.tencentcloudapi</groupId> |
||||
|
<artifactId>tencentcloud-sdk-java-sms</artifactId> |
||||
|
<version>${tencentcloud-sdk-java.version}</version> |
||||
|
</dependency> |
||||
|
<!-- SMS SDK end --> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.xingyuv</groupId> |
||||
|
<artifactId>spring-boot-starter-justauth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) --> |
||||
|
<version>${justauth.version}</version> |
||||
|
<exclusions> |
||||
|
<exclusion> |
||||
|
<groupId>cn.hutool</groupId> |
||||
|
<artifactId>hutool-core</artifactId> |
||||
|
</exclusion> |
||||
|
</exclusions> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.github.binarywang</groupId> |
||||
|
<artifactId>weixin-java-pay</artifactId> |
||||
|
<version>${weixin-java.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>com.github.binarywang</groupId> |
||||
|
<artifactId>wx-java-mp-spring-boot-starter</artifactId> |
||||
|
<version>${weixin-java.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>com.github.binarywang</groupId> |
||||
|
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId> |
||||
|
<version>${weixin-java.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- 积木报表--> |
||||
|
<dependency> |
||||
|
<groupId>org.jeecgframework.jimureport</groupId> |
||||
|
<artifactId>jimureport-spring-boot3-starter</artifactId> |
||||
|
<version>${jimureport.version}</version> |
||||
|
<exclusions> |
||||
|
<exclusion> |
||||
|
<groupId>com.alibaba</groupId> |
||||
|
<artifactId>druid</artifactId> |
||||
|
</exclusion> |
||||
|
</exclusions> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>xerces</groupId> |
||||
|
<artifactId>xercesImpl</artifactId> |
||||
|
<version>${xercesImpl.version}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
</dependencies> |
||||
|
</dependencyManagement> |
||||
|
|
||||
|
<build> |
||||
|
<plugins> |
||||
|
<!-- 统一 revision 版本 --> |
||||
|
<plugin> |
||||
|
<groupId>org.codehaus.mojo</groupId> |
||||
|
<artifactId>flatten-maven-plugin</artifactId> |
||||
|
<version>${flatten-maven-plugin.version}</version> |
||||
|
<configuration> |
||||
|
<flattenMode>resolveCiFriendliesOnly</flattenMode> |
||||
|
<updatePomFile>true</updatePomFile> |
||||
|
</configuration> |
||||
|
<executions> |
||||
|
<execution> |
||||
|
<goals> |
||||
|
<goal>flatten</goal> |
||||
|
</goals> |
||||
|
<id>flatten</id> |
||||
|
<phase>process-resources</phase> |
||||
|
</execution> |
||||
|
<execution> |
||||
|
<goals> |
||||
|
<goal>clean</goal> |
||||
|
</goals> |
||||
|
<id>flatten.clean</id> |
||||
|
<phase>clean</phase> |
||||
|
</execution> |
||||
|
</executions> |
||||
|
</plugin> |
||||
|
</plugins> |
||||
|
</build> |
||||
|
|
||||
|
</project> |
||||
@ -0,0 +1,148 @@ |
|||||
|
<?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>com.qiantoon</groupId> |
||||
|
<artifactId>platform-framework</artifactId> |
||||
|
<version>${revision}</version> |
||||
|
</parent> |
||||
|
<modelVersion>4.0.0</modelVersion> |
||||
|
<artifactId>platform-common</artifactId> |
||||
|
<packaging>jar</packaging> |
||||
|
|
||||
|
<name>${project.artifactId}</name> |
||||
|
<description>定义基础 pojo 类、枚举、工具类等等</description> |
||||
|
|
||||
|
<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>org.springdoc</groupId> |
||||
|
<artifactId>springdoc-openapi-starter-webmvc-api</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>com.fasterxml.jackson.datatype</groupId> |
||||
|
<artifactId>jackson-datatype-jsr310</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> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.fhs-opensource</groupId> <!-- VO 数据翻译 --> |
||||
|
<artifactId>easy-trans-anno</artifactId> <!-- 默认引入的原因,方便 xxx-module-api 包使用 --> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- Test 测试相关 --> |
||||
|
<dependency> |
||||
|
<groupId>org.springframework.boot</groupId> |
||||
|
<artifactId>spring-boot-starter-test</artifactId> |
||||
|
<scope>test</scope> |
||||
|
</dependency> |
||||
|
</dependencies> |
||||
|
|
||||
|
</project> |
||||
@ -0,0 +1,15 @@ |
|||||
|
package com.qiantoon.platform.framework.common.core; |
||||
|
|
||||
|
/** |
||||
|
* 可生成 Int 数组的接口 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public interface IntArrayValuable { |
||||
|
|
||||
|
/** |
||||
|
* @return int 数组 |
||||
|
*/ |
||||
|
int[] array(); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
package com.qiantoon.platform.framework.common.core; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Data; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
|
||||
|
import java.io.Serializable; |
||||
|
|
||||
|
/** |
||||
|
* Key Value 的键值对 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
@Data |
||||
|
@NoArgsConstructor |
||||
|
@AllArgsConstructor |
||||
|
public class KeyValue<K, V> implements Serializable { |
||||
|
|
||||
|
private K key; |
||||
|
private V value; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
package com.qiantoon.platform.framework.common.enums; |
||||
|
|
||||
|
import cn.hutool.core.util.ObjUtil; |
||||
|
import com.qiantoon.platform.framework.common.core.IntArrayValuable; |
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Getter; |
||||
|
|
||||
|
import java.util.Arrays; |
||||
|
|
||||
|
/** |
||||
|
* 通用状态枚举 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
@Getter |
||||
|
@AllArgsConstructor |
||||
|
public enum CommonStatusEnum implements IntArrayValuable { |
||||
|
|
||||
|
ENABLE(0, "开启"), |
||||
|
DISABLE(1, "关闭"); |
||||
|
|
||||
|
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CommonStatusEnum::getStatus).toArray(); |
||||
|
|
||||
|
/** |
||||
|
* 状态值 |
||||
|
*/ |
||||
|
private final Integer status; |
||||
|
/** |
||||
|
* 状态名 |
||||
|
*/ |
||||
|
private final String name; |
||||
|
|
||||
|
@Override |
||||
|
public int[] array() { |
||||
|
return ARRAYS; |
||||
|
} |
||||
|
|
||||
|
public static boolean isEnable(Integer status) { |
||||
|
return ObjUtil.equal(ENABLE.status, status); |
||||
|
} |
||||
|
|
||||
|
public static boolean isDisable(Integer status) { |
||||
|
return ObjUtil.equal(DISABLE.status, status); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
package com.qiantoon.platform.framework.common.enums; |
||||
|
|
||||
|
import cn.hutool.core.util.ArrayUtil; |
||||
|
import com.qiantoon.platform.framework.common.core.IntArrayValuable; |
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Getter; |
||||
|
|
||||
|
import java.util.Arrays; |
||||
|
|
||||
|
/** |
||||
|
* 时间间隔的枚举 |
||||
|
* |
||||
|
* @author dhb52 |
||||
|
*/ |
||||
|
@Getter |
||||
|
@AllArgsConstructor |
||||
|
public enum DateIntervalEnum implements IntArrayValuable { |
||||
|
|
||||
|
DAY(1, "天"), |
||||
|
WEEK(2, "周"), |
||||
|
MONTH(3, "月"), |
||||
|
QUARTER(4, "季度"), |
||||
|
YEAR(5, "年") |
||||
|
; |
||||
|
|
||||
|
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(DateIntervalEnum::getInterval).toArray(); |
||||
|
|
||||
|
/** |
||||
|
* 类型 |
||||
|
*/ |
||||
|
private final Integer interval; |
||||
|
/** |
||||
|
* 名称 |
||||
|
*/ |
||||
|
private final String name; |
||||
|
|
||||
|
@Override |
||||
|
public int[] array() { |
||||
|
return ARRAYS; |
||||
|
} |
||||
|
|
||||
|
public static DateIntervalEnum valueOf(Integer interval) { |
||||
|
return ArrayUtil.firstMatch(item -> item.getInterval().equals(interval), DateIntervalEnum.values()); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
package com.qiantoon.platform.framework.common.enums; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Getter; |
||||
|
|
||||
|
/** |
||||
|
* 文档地址 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
@Getter |
||||
|
@AllArgsConstructor |
||||
|
public enum DocumentEnum { |
||||
|
|
||||
|
REDIS_INSTALL("https://redis.io", "Redis 安装文档"), |
||||
|
TENANT("https://www.baidu.com", "SaaS 多租户文档"); |
||||
|
|
||||
|
private final String url; |
||||
|
private final String memo; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
package com.qiantoon.platform.framework.common.enums; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.common.core.IntArrayValuable; |
||||
|
import lombok.Getter; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
|
||||
|
import java.util.Arrays; |
||||
|
|
||||
|
/** |
||||
|
* 终端的枚举 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
@RequiredArgsConstructor |
||||
|
@Getter |
||||
|
public enum TerminalEnum implements IntArrayValuable { |
||||
|
|
||||
|
UNKNOWN(0, "未知"), // 目的:在无法解析到 terminal 时,使用它
|
||||
|
WECHAT_MINI_PROGRAM(10, "微信小程序"), |
||||
|
WECHAT_WAP(11, "微信公众号"), |
||||
|
H5(20, "H5 网页"), |
||||
|
APP(31, "手机 App"), |
||||
|
; |
||||
|
|
||||
|
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TerminalEnum::getTerminal).toArray(); |
||||
|
|
||||
|
/** |
||||
|
* 终端 |
||||
|
*/ |
||||
|
private final Integer terminal; |
||||
|
/** |
||||
|
* 终端名 |
||||
|
*/ |
||||
|
private final String name; |
||||
|
|
||||
|
@Override |
||||
|
public int[] array() { |
||||
|
return ARRAYS; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
package com.qiantoon.platform.framework.common.enums; |
||||
|
|
||||
|
import cn.hutool.core.util.ArrayUtil; |
||||
|
import com.qiantoon.platform.framework.common.core.IntArrayValuable; |
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Getter; |
||||
|
|
||||
|
import java.util.Arrays; |
||||
|
|
||||
|
/** |
||||
|
* 全局用户类型枚举 |
||||
|
*/ |
||||
|
@AllArgsConstructor |
||||
|
@Getter |
||||
|
public enum UserTypeEnum implements IntArrayValuable { |
||||
|
|
||||
|
MEMBER(1, "会员"), // 面向 c 端,普通用户
|
||||
|
ADMIN(2, "管理员"); // 面向 b 端,管理后台
|
||||
|
|
||||
|
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(UserTypeEnum::getValue).toArray(); |
||||
|
|
||||
|
/** |
||||
|
* 类型 |
||||
|
*/ |
||||
|
private final Integer value; |
||||
|
/** |
||||
|
* 类型名 |
||||
|
*/ |
||||
|
private final String name; |
||||
|
|
||||
|
public static UserTypeEnum valueOf(Integer value) { |
||||
|
return ArrayUtil.firstMatch(userType -> userType.getValue().equals(value), UserTypeEnum.values()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public int[] array() { |
||||
|
return ARRAYS; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
package com.qiantoon.platform.framework.common.enums; |
||||
|
|
||||
|
/** |
||||
|
* Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期 |
||||
|
* |
||||
|
* 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 enums 包下 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
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 FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面
|
||||
|
|
||||
|
int DEMO_FILTER = Integer.MAX_VALUE; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
package com.qiantoon.platform.framework.common.exception; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.common.exception.enums.GlobalErrorCodeConstants; |
||||
|
import com.qiantoon.platform.framework.common.exception.enums.ServiceErrorCodeRange; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
/** |
||||
|
* 错误码对象 |
||||
|
* |
||||
|
* 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants} |
||||
|
* 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange} |
||||
|
* |
||||
|
* TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备 |
||||
|
*/ |
||||
|
@Data |
||||
|
public class ErrorCode { |
||||
|
|
||||
|
/** |
||||
|
* 错误码 |
||||
|
*/ |
||||
|
private final Integer code; |
||||
|
/** |
||||
|
* 错误提示 |
||||
|
*/ |
||||
|
private final String msg; |
||||
|
|
||||
|
public ErrorCode(Integer code, String message) { |
||||
|
this.code = code; |
||||
|
this.msg = message; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,60 @@ |
|||||
|
package com.qiantoon.platform.framework.common.exception; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.common.exception.enums.GlobalErrorCodeConstants; |
||||
|
import lombok.Data; |
||||
|
import lombok.EqualsAndHashCode; |
||||
|
|
||||
|
/** |
||||
|
* 服务器异常 Exception |
||||
|
*/ |
||||
|
@Data |
||||
|
@EqualsAndHashCode(callSuper = true) |
||||
|
public final class ServerException extends RuntimeException { |
||||
|
|
||||
|
/** |
||||
|
* 全局错误码 |
||||
|
* |
||||
|
* @see GlobalErrorCodeConstants |
||||
|
*/ |
||||
|
private Integer code; |
||||
|
/** |
||||
|
* 错误提示 |
||||
|
*/ |
||||
|
private String message; |
||||
|
|
||||
|
/** |
||||
|
* 空构造方法,避免反序列化问题 |
||||
|
*/ |
||||
|
public ServerException() { |
||||
|
} |
||||
|
|
||||
|
public ServerException(ErrorCode errorCode) { |
||||
|
this.code = errorCode.getCode(); |
||||
|
this.message = errorCode.getMsg(); |
||||
|
} |
||||
|
|
||||
|
public ServerException(Integer code, String message) { |
||||
|
this.code = code; |
||||
|
this.message = message; |
||||
|
} |
||||
|
|
||||
|
public Integer getCode() { |
||||
|
return code; |
||||
|
} |
||||
|
|
||||
|
public ServerException setCode(Integer code) { |
||||
|
this.code = code; |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getMessage() { |
||||
|
return message; |
||||
|
} |
||||
|
|
||||
|
public ServerException setMessage(String message) { |
||||
|
this.message = message; |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,60 @@ |
|||||
|
package com.qiantoon.platform.framework.common.exception; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.common.exception.enums.ServiceErrorCodeRange; |
||||
|
import lombok.Data; |
||||
|
import lombok.EqualsAndHashCode; |
||||
|
|
||||
|
/** |
||||
|
* 业务逻辑异常 Exception |
||||
|
*/ |
||||
|
@Data |
||||
|
@EqualsAndHashCode(callSuper = true) |
||||
|
public final class ServiceException extends RuntimeException { |
||||
|
|
||||
|
/** |
||||
|
* 业务错误码 |
||||
|
* |
||||
|
* @see ServiceErrorCodeRange |
||||
|
*/ |
||||
|
private Integer code; |
||||
|
/** |
||||
|
* 错误提示 |
||||
|
*/ |
||||
|
private String message; |
||||
|
|
||||
|
/** |
||||
|
* 空构造方法,避免反序列化问题 |
||||
|
*/ |
||||
|
public ServiceException() { |
||||
|
} |
||||
|
|
||||
|
public ServiceException(ErrorCode errorCode) { |
||||
|
this.code = errorCode.getCode(); |
||||
|
this.message = errorCode.getMsg(); |
||||
|
} |
||||
|
|
||||
|
public ServiceException(Integer code, String message) { |
||||
|
this.code = code; |
||||
|
this.message = message; |
||||
|
} |
||||
|
|
||||
|
public Integer getCode() { |
||||
|
return code; |
||||
|
} |
||||
|
|
||||
|
public ServiceException setCode(Integer code) { |
||||
|
this.code = code; |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getMessage() { |
||||
|
return message; |
||||
|
} |
||||
|
|
||||
|
public ServiceException setMessage(String message) { |
||||
|
this.message = message; |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
package com.qiantoon.platform.framework.common.exception.enums; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.common.exception.ErrorCode; |
||||
|
|
||||
|
/** |
||||
|
* 全局错误码枚举 |
||||
|
* 0-999 系统异常编码保留 |
||||
|
* |
||||
|
* 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
|
||||
|
* 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的 |
||||
|
* 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
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 NOT_IMPLEMENTED = new ErrorCode(501, "功能未实现/未开启"); |
||||
|
ErrorCode ERROR_CONFIGURATION = new ErrorCode(502, "错误的配置项"); |
||||
|
|
||||
|
// ========== 自定义错误段 ==========
|
||||
|
ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求
|
||||
|
ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作"); |
||||
|
|
||||
|
ErrorCode UNKNOWN = new ErrorCode(999, "未知错误"); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
package com.qiantoon.platform.framework.common.exception.enums; |
||||
|
|
||||
|
/** |
||||
|
* 业务异常的错误码区间,解决:解决各模块错误码定义,避免重复,在此只声明不做实际使用 |
||||
|
* |
||||
|
* 一共 10 位,分成四段 |
||||
|
* |
||||
|
* 第一段,1 位,类型 |
||||
|
* 1 - 业务级别异常 |
||||
|
* x - 预留 |
||||
|
* 第二段,3 位,系统类型 |
||||
|
* 001 - 用户系统 |
||||
|
* 002 - 商品系统 |
||||
|
* 003 - 订单系统 |
||||
|
* 004 - 支付系统 |
||||
|
* 005 - 优惠劵系统 |
||||
|
* ... - ... |
||||
|
* 第三段,3 位,模块 |
||||
|
* 不限制规则。 |
||||
|
* 一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子: |
||||
|
* 001 - OAuth2 模块 |
||||
|
* 002 - User 模块 |
||||
|
* 003 - MobileCode 模块 |
||||
|
* 第四段,3 位,错误码 |
||||
|
* 不限制规则。 |
||||
|
* 一般建议,每个模块自增。 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class ServiceErrorCodeRange { |
||||
|
|
||||
|
// 模块 infra 错误码区间 [1-001-000-000 ~ 1-002-000-000)
|
||||
|
// 模块 system 错误码区间 [1-002-000-000 ~ 1-003-000-000)
|
||||
|
// 模块 report 错误码区间 [1-003-000-000 ~ 1-004-000-000)
|
||||
|
// 模块 member 错误码区间 [1-004-000-000 ~ 1-005-000-000)
|
||||
|
// 模块 mp 错误码区间 [1-006-000-000 ~ 1-007-000-000)
|
||||
|
// 模块 pay 错误码区间 [1-007-000-000 ~ 1-008-000-000)
|
||||
|
// 模块 bpm 错误码区间 [1-009-000-000 ~ 1-010-000-000)
|
||||
|
|
||||
|
// 模块 product 错误码区间 [1-008-000-000 ~ 1-009-000-000)
|
||||
|
// 模块 trade 错误码区间 [1-011-000-000 ~ 1-012-000-000)
|
||||
|
// 模块 promotion 错误码区间 [1-013-000-000 ~ 1-014-000-000)
|
||||
|
|
||||
|
// 模块 crm 错误码区间 [1-020-000-000 ~ 1-021-000-000)
|
||||
|
|
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
package com.qiantoon.platform.framework.common.exception.util; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.common.exception.ErrorCode; |
||||
|
import com.qiantoon.platform.framework.common.exception.ServiceException; |
||||
|
import com.qiantoon.platform.framework.common.exception.enums.GlobalErrorCodeConstants; |
||||
|
import com.google.common.annotations.VisibleForTesting; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
|
||||
|
/** |
||||
|
* {@link ServiceException} 工具类 |
||||
|
* |
||||
|
* 目的在于,格式化异常信息提示。 |
||||
|
* 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化 |
||||
|
* |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
public class ServiceExceptionUtil { |
||||
|
|
||||
|
// ========== 和 ServiceException 的集成 ==========
|
||||
|
|
||||
|
public static ServiceException exception(ErrorCode errorCode) { |
||||
|
return exception0(errorCode.getCode(), errorCode.getMsg()); |
||||
|
} |
||||
|
|
||||
|
public static ServiceException exception(ErrorCode errorCode, Object... params) { |
||||
|
return exception0(errorCode.getCode(), errorCode.getMsg(), params); |
||||
|
} |
||||
|
|
||||
|
public static ServiceException exception0(Integer code, String messagePattern, Object... params) { |
||||
|
String message = doFormat(code, messagePattern, params); |
||||
|
return new ServiceException(code, message); |
||||
|
} |
||||
|
|
||||
|
public static ServiceException invalidParamException(String messagePattern, Object... params) { |
||||
|
return exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), messagePattern, params); |
||||
|
} |
||||
|
|
||||
|
// ========== 格式化方法 ==========
|
||||
|
|
||||
|
/** |
||||
|
* 将错误编号对应的消息使用 params 进行格式化。 |
||||
|
* |
||||
|
* @param code 错误编号 |
||||
|
* @param messagePattern 消息模版 |
||||
|
* @param params 参数 |
||||
|
* @return 格式化后的提示 |
||||
|
*/ |
||||
|
@VisibleForTesting |
||||
|
public static String doFormat(int code, String messagePattern, Object... params) { |
||||
|
StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50); |
||||
|
int i = 0; |
||||
|
int j; |
||||
|
int l; |
||||
|
for (l = 0; l < params.length; l++) { |
||||
|
j = messagePattern.indexOf("{}", i); |
||||
|
if (j == -1) { |
||||
|
log.error("[doFormat][参数过多:错误码({})|错误内容({})|参数({})", code, messagePattern, params); |
||||
|
if (i == 0) { |
||||
|
return messagePattern; |
||||
|
} else { |
||||
|
sbuf.append(messagePattern.substring(i)); |
||||
|
return sbuf.toString(); |
||||
|
} |
||||
|
} else { |
||||
|
sbuf.append(messagePattern, i, j); |
||||
|
sbuf.append(params[l]); |
||||
|
i = j + 2; |
||||
|
} |
||||
|
} |
||||
|
if (messagePattern.indexOf("{}", i) != -1) { |
||||
|
log.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params); |
||||
|
} |
||||
|
sbuf.append(messagePattern.substring(i)); |
||||
|
return sbuf.toString(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
/** |
||||
|
* 基础的通用类,和框架无关 |
||||
|
* |
||||
|
* 例如说,CommonResult 为通用返回 |
||||
|
*/ |
||||
|
package com.qiantoon.platform.framework.common; |
||||
@ -0,0 +1,112 @@ |
|||||
|
package com.qiantoon.platform.framework.common.pojo; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.common.exception.ErrorCode; |
||||
|
import com.qiantoon.platform.framework.common.exception.ServiceException; |
||||
|
import com.qiantoon.platform.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); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常 |
||||
|
* 如果没有,则返回 {@link #data} 数据 |
||||
|
*/ |
||||
|
@JsonIgnore // 避免 jackson 序列化
|
||||
|
public T getCheckedData() { |
||||
|
checkError(); |
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
public static <T> CommonResult<T> error(ServiceException serviceException) { |
||||
|
return error(serviceException.getCode(), serviceException.getMessage()); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
package com.qiantoon.platform.framework.common.pojo; |
||||
|
|
||||
|
import io.swagger.v3.oas.annotations.media.Schema; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
import jakarta.validation.constraints.Min; |
||||
|
import jakarta.validation.constraints.Max; |
||||
|
import jakarta.validation.constraints.NotNull; |
||||
|
import java.io.Serializable; |
||||
|
|
||||
|
@Schema(description="分页参数") |
||||
|
@Data |
||||
|
public class PageParam implements Serializable { |
||||
|
|
||||
|
private static final Integer PAGE_NO = 1; |
||||
|
private static final Integer PAGE_SIZE = 10; |
||||
|
|
||||
|
/** |
||||
|
* 每页条数 - 不分页 |
||||
|
* |
||||
|
* 例如说,导出接口,可以设置 {@link #pageSize} 为 -1 不分页,查询所有数据。 |
||||
|
*/ |
||||
|
public static final Integer PAGE_SIZE_NONE = -1; |
||||
|
|
||||
|
@Schema(description = "页码,从 1 开始", requiredMode = Schema.RequiredMode.REQUIRED,example = "1") |
||||
|
@NotNull(message = "页码不能为空") |
||||
|
@Min(value = 1, message = "页码最小值为 1") |
||||
|
private Integer pageNo = PAGE_NO; |
||||
|
|
||||
|
@Schema(description = "每页条数,最大值为 100", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") |
||||
|
@NotNull(message = "每页条数不能为空") |
||||
|
@Min(value = 1, message = "每页条数最小值为 1") |
||||
|
@Max(value = 100, message = "每页条数最大值为 100") |
||||
|
private Integer pageSize = PAGE_SIZE; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
package com.qiantoon.platform.framework.common.pojo; |
||||
|
|
||||
|
import io.swagger.v3.oas.annotations.media.Schema; |
||||
|
import lombok.Data; |
||||
|
|
||||
|
import java.io.Serializable; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
@Schema(description = "分页结果") |
||||
|
@Data |
||||
|
public final class PageResult<T> implements Serializable { |
||||
|
|
||||
|
@Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED) |
||||
|
private List<T> list; |
||||
|
|
||||
|
@Schema(description = "总量", requiredMode = Schema.RequiredMode.REQUIRED) |
||||
|
private Long total; |
||||
|
|
||||
|
public PageResult() { |
||||
|
} |
||||
|
|
||||
|
public PageResult(List<T> list, Long total) { |
||||
|
this.list = list; |
||||
|
this.total = total; |
||||
|
} |
||||
|
|
||||
|
public PageResult(Long total) { |
||||
|
this.list = new ArrayList<>(); |
||||
|
this.total = total; |
||||
|
} |
||||
|
|
||||
|
public static <T> PageResult<T> empty() { |
||||
|
return new PageResult<>(0L); |
||||
|
} |
||||
|
|
||||
|
public static <T> PageResult<T> empty(Long total) { |
||||
|
return new PageResult<>(total); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
package com.qiantoon.platform.framework.common.pojo; |
||||
|
|
||||
|
import io.swagger.v3.oas.annotations.media.Schema; |
||||
|
import lombok.Data; |
||||
|
import lombok.EqualsAndHashCode; |
||||
|
import lombok.ToString; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
@Schema(description = "可排序的分页参数") |
||||
|
@Data |
||||
|
@EqualsAndHashCode(callSuper = true) |
||||
|
@ToString(callSuper = true) |
||||
|
public class SortablePageParam extends PageParam { |
||||
|
|
||||
|
@Schema(description = "排序字段") |
||||
|
private List<SortingField> sortingFields; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
package com.qiantoon.platform.framework.common.pojo; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Data; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
|
||||
|
import java.io.Serializable; |
||||
|
|
||||
|
/** |
||||
|
* 排序字段 DTO |
||||
|
* |
||||
|
* 类名加了 ing 的原因是,避免和 ES SortField 重名。 |
||||
|
*/ |
||||
|
@Data |
||||
|
@NoArgsConstructor |
||||
|
@AllArgsConstructor |
||||
|
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; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
package com.qiantoon.platform.framework.common.util.cache; |
||||
|
|
||||
|
import com.google.common.cache.CacheBuilder; |
||||
|
import com.google.common.cache.CacheLoader; |
||||
|
import com.google.common.cache.LoadingCache; |
||||
|
|
||||
|
import java.time.Duration; |
||||
|
import java.util.concurrent.Executors; |
||||
|
|
||||
|
/** |
||||
|
* Cache 工具类 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class CacheUtils { |
||||
|
|
||||
|
/** |
||||
|
* 构建异步刷新的 LoadingCache 对象 |
||||
|
* |
||||
|
* 注意:如果你的缓存和 ThreadLocal 有关系,要么自己处理 ThreadLocal 的传递,要么使用 {@link #buildCache(Duration, CacheLoader)} 方法 |
||||
|
* |
||||
|
* 或者简单理解: |
||||
|
* 1、和“人”相关的,使用 {@link #buildCache(Duration, CacheLoader)} 方法 |
||||
|
* 2、和“全局”、“系统”相关的,使用当前缓存方法 |
||||
|
* |
||||
|
* @param duration 过期时间 |
||||
|
* @param loader CacheLoader 对象 |
||||
|
* @return LoadingCache 对象 |
||||
|
*/ |
||||
|
public static <K, V> LoadingCache<K, V> buildAsyncReloadingCache(Duration duration, CacheLoader<K, V> loader) { |
||||
|
return CacheBuilder.newBuilder() |
||||
|
// 只阻塞当前数据加载线程,其他线程返回旧值
|
||||
|
.refreshAfterWrite(duration) |
||||
|
// 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程
|
||||
|
.build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool())); // TODO qt:可能要思考下,未来要不要做成可配置
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 构建同步刷新的 LoadingCache 对象 |
||||
|
* |
||||
|
* @param duration 过期时间 |
||||
|
* @param loader CacheLoader 对象 |
||||
|
* @return LoadingCache 对象 |
||||
|
*/ |
||||
|
public static <K, V> LoadingCache<K, V> buildCache(Duration duration, CacheLoader<K, V> loader) { |
||||
|
return CacheBuilder.newBuilder().refreshAfterWrite(duration).build(loader); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,58 @@ |
|||||
|
package com.qiantoon.platform.framework.common.util.collection; |
||||
|
|
||||
|
import cn.hutool.core.collection.CollectionUtil; |
||||
|
import cn.hutool.core.collection.IterUtil; |
||||
|
import cn.hutool.core.util.ArrayUtil; |
||||
|
|
||||
|
import java.util.Collection; |
||||
|
import java.util.function.Consumer; |
||||
|
import java.util.function.Function; |
||||
|
|
||||
|
import static com.qiantoon.platform.framework.common.util.collection.CollectionUtils.convertList; |
||||
|
|
||||
|
/** |
||||
|
* Array 工具类 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
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>) IterUtil.getElementType(from.iterator())); |
||||
|
} |
||||
|
|
||||
|
public static <T> T get(T[] array, int index) { |
||||
|
if (null == array || index >= array.length) { |
||||
|
return null; |
||||
|
} |
||||
|
return array[index]; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,322 @@ |
|||||
|
package com.qiantoon.platform.framework.common.util.collection; |
||||
|
|
||||
|
import cn.hutool.core.collection.CollUtil; |
||||
|
import cn.hutool.core.collection.CollectionUtil; |
||||
|
import cn.hutool.core.util.ArrayUtil; |
||||
|
import com.google.common.collect.ImmutableMap; |
||||
|
|
||||
|
import java.util.*; |
||||
|
import java.util.function.*; |
||||
|
import java.util.stream.Collectors; |
||||
|
import java.util.stream.Stream; |
||||
|
|
||||
|
import static java.util.Arrays.asList; |
||||
|
|
||||
|
/** |
||||
|
* Collection 工具类 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class CollectionUtils { |
||||
|
|
||||
|
public static boolean containsAny(Object source, Object... targets) { |
||||
|
return asList(targets).contains(source); |
||||
|
} |
||||
|
|
||||
|
public static boolean isAnyEmpty(Collection<?>... collections) { |
||||
|
return Arrays.stream(collections).anyMatch(CollectionUtil::isEmpty); |
||||
|
} |
||||
|
|
||||
|
public static <T> boolean anyMatch(Collection<T> from, Predicate<T> predicate) { |
||||
|
return from.stream().anyMatch(predicate); |
||||
|
} |
||||
|
|
||||
|
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(T[] from, Function<T, U> func) { |
||||
|
if (ArrayUtil.isEmpty(from)) { |
||||
|
return new ArrayList<>(); |
||||
|
} |
||||
|
return convertList(Arrays.asList(from), func); |
||||
|
} |
||||
|
|
||||
|
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> List<U> convertListByFlatMap(Collection<T> from, |
||||
|
Function<T, ? extends Stream<? extends U>> func) { |
||||
|
if (CollUtil.isEmpty(from)) { |
||||
|
return new ArrayList<>(); |
||||
|
} |
||||
|
return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList()); |
||||
|
} |
||||
|
|
||||
|
public static <T, U, R> List<R> convertListByFlatMap(Collection<T> from, |
||||
|
Function<? super T, ? extends U> mapper, |
||||
|
Function<U, ? extends Stream<? extends R>> func) { |
||||
|
if (CollUtil.isEmpty(from)) { |
||||
|
return new ArrayList<>(); |
||||
|
} |
||||
|
return from.stream().map(mapper).filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toList()); |
||||
|
} |
||||
|
|
||||
|
public static <K, V> List<V> mergeValuesFromMap(Map<K, List<V>> map) { |
||||
|
return map.values() |
||||
|
.stream() |
||||
|
.flatMap(List::stream) |
||||
|
.collect(Collectors.toList()); |
||||
|
} |
||||
|
|
||||
|
public static <T> Set<T> convertSet(Collection<T> from) { |
||||
|
return convertSet(from, v -> v); |
||||
|
} |
||||
|
|
||||
|
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> convertMapByFilter(Collection<T> from, Predicate<T> filter, Function<T, K> keyFunc) { |
||||
|
if (CollUtil.isEmpty(from)) { |
||||
|
return new HashMap<>(); |
||||
|
} |
||||
|
return from.stream().filter(filter).collect(Collectors.toMap(keyFunc, v -> v)); |
||||
|
} |
||||
|
|
||||
|
public static <T, U> Set<U> convertSetByFlatMap(Collection<T> from, |
||||
|
Function<T, ? extends Stream<? extends U>> func) { |
||||
|
if (CollUtil.isEmpty(from)) { |
||||
|
return new HashSet<>(); |
||||
|
} |
||||
|
return from.stream().filter(Objects::nonNull).flatMap(func).filter(Objects::nonNull).collect(Collectors.toSet()); |
||||
|
} |
||||
|
|
||||
|
public static <T, U, R> Set<R> convertSetByFlatMap(Collection<T> from, |
||||
|
Function<? super T, ? extends U> mapper, |
||||
|
Function<U, ? extends Stream<? extends R>> func) { |
||||
|
if (CollUtil.isEmpty(from)) { |
||||
|
return new HashSet<>(); |
||||
|
} |
||||
|
return from.stream().map(mapper).filter(Objects::nonNull).flatMap(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(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 对比老、新两个列表,找出新增、修改、删除的数据 |
||||
|
* |
||||
|
* @param oldList 老列表 |
||||
|
* @param newList 新列表 |
||||
|
* @param sameFunc 对比函数,返回 true 表示相同,返回 false 表示不同 |
||||
|
* 注意,same 是通过每个元素的“标识”,判断它们是不是同一个数据 |
||||
|
* @return [新增列表、修改列表、删除列表] |
||||
|
*/ |
||||
|
public static <T> List<List<T>> diffList(Collection<T> oldList, Collection<T> newList, |
||||
|
BiFunction<T, T, Boolean> sameFunc) { |
||||
|
List<T> createList = new LinkedList<>(newList); // 默认都认为是新增的,后续会进行移除
|
||||
|
List<T> updateList = new ArrayList<>(); |
||||
|
List<T> deleteList = new ArrayList<>(); |
||||
|
|
||||
|
// 通过以 oldList 为主遍历,找出 updateList 和 deleteList
|
||||
|
for (T oldObj : oldList) { |
||||
|
// 1. 寻找是否有匹配的
|
||||
|
T foundObj = null; |
||||
|
for (Iterator<T> iterator = createList.iterator(); iterator.hasNext(); ) { |
||||
|
T newObj = iterator.next(); |
||||
|
// 1.1 不匹配,则直接跳过
|
||||
|
if (!sameFunc.apply(oldObj, newObj)) { |
||||
|
continue; |
||||
|
} |
||||
|
// 1.2 匹配,则移除,并结束寻找
|
||||
|
iterator.remove(); |
||||
|
foundObj = newObj; |
||||
|
break; |
||||
|
} |
||||
|
// 2. 匹配添加到 updateList;不匹配则添加到 deleteList 中
|
||||
|
if (foundObj != null) { |
||||
|
updateList.add(foundObj); |
||||
|
} else { |
||||
|
deleteList.add(oldObj); |
||||
|
} |
||||
|
} |
||||
|
return asList(createList, updateList, deleteList); |
||||
|
} |
||||
|
|
||||
|
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(Collection<T> from, Predicate<T> predicate) { |
||||
|
return findFirst(from, predicate, Function.identity()); |
||||
|
} |
||||
|
|
||||
|
public static <T, U> U findFirst(Collection<T> from, Predicate<T> predicate, Function<T, U> func) { |
||||
|
if (CollUtil.isEmpty(from)) { |
||||
|
return null; |
||||
|
} |
||||
|
return from.stream().filter(predicate).findFirst().map(func).orElse(null); |
||||
|
} |
||||
|
|
||||
|
public static <T, V extends Comparable<? super V>> V getMaxValue(Collection<T> from, Function<T, V> valueFunc) { |
||||
|
if (CollUtil.isEmpty(from)) { |
||||
|
return null; |
||||
|
} |
||||
|
assert !from.isEmpty(); // 断言,避免告警
|
||||
|
T t = from.stream().max(Comparator.comparing(valueFunc)).get(); |
||||
|
return valueFunc.apply(t); |
||||
|
} |
||||
|
|
||||
|
public static <T, V extends Comparable<? super V>> V getMinValue(List<T> from, Function<T, V> valueFunc) { |
||||
|
if (CollUtil.isEmpty(from)) { |
||||
|
return null; |
||||
|
} |
||||
|
assert from.size() > 0; // 断言,避免告警
|
||||
|
T t = from.stream().min(Comparator.comparing(valueFunc)).get(); |
||||
|
return valueFunc.apply(t); |
||||
|
} |
||||
|
|
||||
|
public static <T, V extends Comparable<? super V>> V getSumValue(List<T> from, Function<T, V> valueFunc, |
||||
|
BinaryOperator<V> accumulator) { |
||||
|
return getSumValue(from, valueFunc, accumulator, null); |
||||
|
} |
||||
|
|
||||
|
public static <T, V extends Comparable<? super V>> V getSumValue(Collection<T> from, Function<T, V> valueFunc, |
||||
|
BinaryOperator<V> accumulator, V defaultValue) { |
||||
|
if (CollUtil.isEmpty(from)) { |
||||
|
return defaultValue; |
||||
|
} |
||||
|
assert !from.isEmpty(); // 断言,避免告警
|
||||
|
return from.stream().map(valueFunc).filter(Objects::nonNull).reduce(accumulator).orElse(defaultValue); |
||||
|
} |
||||
|
|
||||
|
public static <T> void addIfNotNull(Collection<T> coll, T item) { |
||||
|
if (item == null) { |
||||
|
return; |
||||
|
} |
||||
|
coll.add(item); |
||||
|
} |
||||
|
|
||||
|
public static <T> Collection<T> singleton(T obj) { |
||||
|
return obj == null ? Collections.emptyList() : Collections.singleton(obj); |
||||
|
} |
||||
|
|
||||
|
public static <T> List<T> newArrayList(List<List<T>> list) { |
||||
|
return list.stream().flatMap(Collection::stream).collect(Collectors.toList()); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,68 @@ |
|||||
|
package com.qiantoon.platform.framework.common.util.collection; |
||||
|
|
||||
|
import cn.hutool.core.collection.CollUtil; |
||||
|
import cn.hutool.core.collection.CollectionUtil; |
||||
|
import cn.hutool.core.util.ObjUtil; |
||||
|
import com.qiantoon.platform.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 qt |
||||
|
*/ |
||||
|
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,然后进一步处理 |
||||
|
* key 为 null 时, 不处理 |
||||
|
* 注意,如果查找到的 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 (ObjUtil.isNull(key) || CollUtil.isEmpty(map)) { |
||||
|
return; |
||||
|
} |
||||
|
V value = map.get(key); |
||||
|
if (value == null) { |
||||
|
return; |
||||
|
} |
||||
|
consumer.accept(value); |
||||
|
} |
||||
|
|
||||
|
public static <K, V> Map<K, V> convertMap(List<KeyValue<K, V>> keyValues) { |
||||
|
Map<K, V> map = Maps.newLinkedHashMapWithExpectedSize(keyValues.size()); |
||||
|
keyValues.forEach(keyValue -> map.put(keyValue.getKey(), keyValue.getValue())); |
||||
|
return map; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
package com.qiantoon.platform.framework.common.util.collection; |
||||
|
|
||||
|
import cn.hutool.core.collection.CollUtil; |
||||
|
|
||||
|
import java.util.Set; |
||||
|
|
||||
|
/** |
||||
|
* Set 工具类 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class SetUtils { |
||||
|
|
||||
|
@SafeVarargs |
||||
|
public static <T> Set<T> asSet(T... objs) { |
||||
|
return CollUtil.newHashSet(objs); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,149 @@ |
|||||
|
package com.qiantoon.platform.framework.common.util.date; |
||||
|
|
||||
|
import cn.hutool.core.date.LocalDateTimeUtil; |
||||
|
|
||||
|
import java.time.*; |
||||
|
import java.util.Calendar; |
||||
|
import java.util.Date; |
||||
|
|
||||
|
/** |
||||
|
* 时间工具类 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
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 = "yyyy-MM-dd"; |
||||
|
|
||||
|
public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss"; |
||||
|
|
||||
|
/** |
||||
|
* 将 LocalDateTime 转换成 Date |
||||
|
* |
||||
|
* @param date LocalDateTime |
||||
|
* @return LocalDateTime |
||||
|
*/ |
||||
|
public static Date of(LocalDateTime date) { |
||||
|
if (date == null) { |
||||
|
return null; |
||||
|
} |
||||
|
// 将此日期时间与时区相结合以创建 ZonedDateTime
|
||||
|
ZonedDateTime zonedDateTime = date.atZone(ZoneId.systemDefault()); |
||||
|
// 本地时间线 LocalDateTime 到即时时间线 Instant 时间戳
|
||||
|
Instant instant = zonedDateTime.toInstant(); |
||||
|
// UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间
|
||||
|
return Date.from(instant); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 将 Date 转换成 LocalDateTime |
||||
|
* |
||||
|
* @param date Date |
||||
|
* @return LocalDateTime |
||||
|
*/ |
||||
|
public static LocalDateTime of(Date date) { |
||||
|
if (date == null) { |
||||
|
return null; |
||||
|
} |
||||
|
// 转为时间戳
|
||||
|
Instant instant = date.toInstant(); |
||||
|
// UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间
|
||||
|
return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); |
||||
|
} |
||||
|
|
||||
|
public static Date addTime(Duration duration) { |
||||
|
return new Date(System.currentTimeMillis() + duration.toMillis()); |
||||
|
} |
||||
|
|
||||
|
public static boolean isExpired(LocalDateTime time) { |
||||
|
LocalDateTime now = LocalDateTime.now(); |
||||
|
return now.isAfter(time); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建指定时间 |
||||
|
* |
||||
|
* @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 LocalDateTime max(LocalDateTime a, LocalDateTime b) { |
||||
|
if (a == null) { |
||||
|
return b; |
||||
|
} |
||||
|
if (b == null) { |
||||
|
return a; |
||||
|
} |
||||
|
return a.isAfter(b) ? a : b; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 是否今天 |
||||
|
* |
||||
|
* @param date 日期 |
||||
|
* @return 是否 |
||||
|
*/ |
||||
|
public static boolean isToday(LocalDateTime date) { |
||||
|
return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 是否昨天 |
||||
|
* |
||||
|
* @param date 日期 |
||||
|
* @return 是否 |
||||
|
*/ |
||||
|
public static boolean isYesterday(LocalDateTime date) { |
||||
|
return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now().minusDays(1)); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,309 @@ |
|||||
|
package com.qiantoon.platform.framework.common.util.date; |
||||
|
|
||||
|
import cn.hutool.core.collection.CollUtil; |
||||
|
import cn.hutool.core.date.DatePattern; |
||||
|
import cn.hutool.core.date.LocalDateTimeUtil; |
||||
|
import cn.hutool.core.lang.Assert; |
||||
|
import cn.hutool.core.util.StrUtil; |
||||
|
import com.qiantoon.platform.framework.common.enums.DateIntervalEnum; |
||||
|
|
||||
|
import java.time.*; |
||||
|
import java.time.format.DateTimeParseException; |
||||
|
import java.time.temporal.ChronoUnit; |
||||
|
import java.time.temporal.TemporalAdjusters; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 时间工具类,用于 {@link java.time.LocalDateTime} |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class LocalDateTimeUtils { |
||||
|
|
||||
|
/** |
||||
|
* 空的 LocalDateTime 对象,主要用于 DB 唯一索引的默认值 |
||||
|
*/ |
||||
|
public static LocalDateTime EMPTY = buildTime(1970, 1, 1); |
||||
|
|
||||
|
/** |
||||
|
* 解析时间 |
||||
|
* |
||||
|
* 相比 {@link LocalDateTimeUtil#parse(CharSequence)} 方法来说,会尽量去解析,直到成功 |
||||
|
* |
||||
|
* @param time 时间 |
||||
|
* @return 时间字符串 |
||||
|
*/ |
||||
|
public static LocalDateTime parse(String time) { |
||||
|
try { |
||||
|
return LocalDateTimeUtil.parse(time, DatePattern.NORM_DATE_PATTERN); |
||||
|
} catch (DateTimeParseException e) { |
||||
|
return LocalDateTimeUtil.parse(time); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static LocalDateTime addTime(Duration duration) { |
||||
|
return LocalDateTime.now().plus(duration); |
||||
|
} |
||||
|
|
||||
|
public static LocalDateTime minusTime(Duration duration) { |
||||
|
return LocalDateTime.now().minus(duration); |
||||
|
} |
||||
|
|
||||
|
public static boolean beforeNow(LocalDateTime date) { |
||||
|
return date.isBefore(LocalDateTime.now()); |
||||
|
} |
||||
|
|
||||
|
public static boolean afterNow(LocalDateTime date) { |
||||
|
return date.isAfter(LocalDateTime.now()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建指定时间 |
||||
|
* |
||||
|
* @param year 年 |
||||
|
* @param mouth 月 |
||||
|
* @param day 日 |
||||
|
* @return 指定时间 |
||||
|
*/ |
||||
|
public static LocalDateTime buildTime(int year, int mouth, int day) { |
||||
|
return LocalDateTime.of(year, mouth, day, 0, 0, 0); |
||||
|
} |
||||
|
|
||||
|
public static LocalDateTime[] buildBetweenTime(int year1, int mouth1, int day1, |
||||
|
int year2, int mouth2, int day2) { |
||||
|
return new LocalDateTime[]{buildTime(year1, mouth1, day1), buildTime(year2, mouth2, day2)}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 判指定断时间,是否在该时间范围内 |
||||
|
* |
||||
|
* @param startTime 开始时间 |
||||
|
* @param endTime 结束时间 |
||||
|
* @param time 指定时间 |
||||
|
* @return 是否 |
||||
|
*/ |
||||
|
public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime, String time) { |
||||
|
if (startTime == null || endTime == null || time == null) { |
||||
|
return false; |
||||
|
} |
||||
|
return LocalDateTimeUtil.isIn(parse(time), startTime, endTime); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 判断当前时间是否在该时间范围内 |
||||
|
* |
||||
|
* @param startTime 开始时间 |
||||
|
* @param endTime 结束时间 |
||||
|
* @return 是否 |
||||
|
*/ |
||||
|
public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime) { |
||||
|
if (startTime == null || endTime == null) { |
||||
|
return false; |
||||
|
} |
||||
|
return LocalDateTimeUtil.isIn(LocalDateTime.now(), startTime, endTime); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 判断当前时间是否在该时间范围内 |
||||
|
* |
||||
|
* @param startTime 开始时间 |
||||
|
* @param endTime 结束时间 |
||||
|
* @return 是否 |
||||
|
*/ |
||||
|
public static boolean isBetween(String startTime, String endTime) { |
||||
|
if (startTime == null || endTime == null) { |
||||
|
return false; |
||||
|
} |
||||
|
LocalDate nowDate = LocalDate.now(); |
||||
|
return LocalDateTimeUtil.isIn(LocalDateTime.now(), |
||||
|
LocalDateTime.of(nowDate, LocalTime.parse(startTime)), |
||||
|
LocalDateTime.of(nowDate, LocalTime.parse(endTime))); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 判断时间段是否重叠 |
||||
|
* |
||||
|
* @param startTime1 开始 time1 |
||||
|
* @param endTime1 结束 time1 |
||||
|
* @param startTime2 开始 time2 |
||||
|
* @param endTime2 结束 time2 |
||||
|
* @return 重叠:true 不重叠:false |
||||
|
*/ |
||||
|
public static boolean isOverlap(LocalTime startTime1, LocalTime endTime1, LocalTime startTime2, LocalTime endTime2) { |
||||
|
LocalDate nowDate = LocalDate.now(); |
||||
|
return LocalDateTimeUtil.isOverlap(LocalDateTime.of(nowDate, startTime1), LocalDateTime.of(nowDate, endTime1), |
||||
|
LocalDateTime.of(nowDate, startTime2), LocalDateTime.of(nowDate, endTime2)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取指定日期所在的月份的开始时间 |
||||
|
* 例如:2023-09-30 00:00:00,000 |
||||
|
* |
||||
|
* @param date 日期 |
||||
|
* @return 月份的开始时间 |
||||
|
*/ |
||||
|
public static LocalDateTime beginOfMonth(LocalDateTime date) { |
||||
|
return date.with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取指定日期所在的月份的最后时间 |
||||
|
* 例如:2023-09-30 23:59:59,999 |
||||
|
* |
||||
|
* @param date 日期 |
||||
|
* @return 月份的结束时间 |
||||
|
*/ |
||||
|
public static LocalDateTime endOfMonth(LocalDateTime date) { |
||||
|
return date.with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获得指定日期所在季度 |
||||
|
* |
||||
|
* @param date 日期 |
||||
|
* @return 所在季度 |
||||
|
*/ |
||||
|
public static int getQuarterOfYear(LocalDateTime date) { |
||||
|
return (date.getMonthValue() - 1) / 3 + 1; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取指定日期到现在过了几天,如果指定日期在当前日期之后,获取结果为负 |
||||
|
* |
||||
|
* @param dateTime 日期 |
||||
|
* @return 相差天数 |
||||
|
*/ |
||||
|
public static Long between(LocalDateTime dateTime) { |
||||
|
return LocalDateTimeUtil.between(dateTime, LocalDateTime.now(), ChronoUnit.DAYS); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取今天的开始时间 |
||||
|
* |
||||
|
* @return 今天 |
||||
|
*/ |
||||
|
public static LocalDateTime getToday() { |
||||
|
return LocalDateTimeUtil.beginOfDay(LocalDateTime.now()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取昨天的开始时间 |
||||
|
* |
||||
|
* @return 昨天 |
||||
|
*/ |
||||
|
public static LocalDateTime getYesterday() { |
||||
|
return LocalDateTimeUtil.beginOfDay(LocalDateTime.now().minusDays(1)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取本月的开始时间 |
||||
|
* |
||||
|
* @return 本月 |
||||
|
*/ |
||||
|
public static LocalDateTime getMonth() { |
||||
|
return beginOfMonth(LocalDateTime.now()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取本年的开始时间 |
||||
|
* |
||||
|
* @return 本年 |
||||
|
*/ |
||||
|
public static LocalDateTime getYear() { |
||||
|
return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN); |
||||
|
} |
||||
|
|
||||
|
public static List<LocalDateTime[]> getDateRangeList(LocalDateTime startTime, |
||||
|
LocalDateTime endTime, |
||||
|
Integer interval) { |
||||
|
// 1.1 找到枚举
|
||||
|
DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval); |
||||
|
Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval); |
||||
|
// 1.2 将时间对齐
|
||||
|
startTime = LocalDateTimeUtil.beginOfDay(startTime); |
||||
|
endTime = LocalDateTimeUtil.endOfDay(endTime); |
||||
|
|
||||
|
// 2. 循环,生成时间范围
|
||||
|
List<LocalDateTime[]> timeRanges = new ArrayList<>(); |
||||
|
switch (intervalEnum) { |
||||
|
case DAY: |
||||
|
while (startTime.isBefore(endTime)) { |
||||
|
timeRanges.add(new LocalDateTime[]{startTime, startTime.plusDays(1).minusNanos(1)}); |
||||
|
startTime = startTime.plusDays(1); |
||||
|
} |
||||
|
break; |
||||
|
case WEEK: |
||||
|
while (startTime.isBefore(endTime)) { |
||||
|
LocalDateTime endOfWeek = startTime.with(DayOfWeek.SUNDAY).plusDays(1).minusNanos(1); |
||||
|
timeRanges.add(new LocalDateTime[]{startTime, endOfWeek}); |
||||
|
startTime = endOfWeek.plusNanos(1); |
||||
|
} |
||||
|
break; |
||||
|
case MONTH: |
||||
|
while (startTime.isBefore(endTime)) { |
||||
|
LocalDateTime endOfMonth = startTime.with(TemporalAdjusters.lastDayOfMonth()).plusDays(1).minusNanos(1); |
||||
|
timeRanges.add(new LocalDateTime[]{startTime, endOfMonth}); |
||||
|
startTime = endOfMonth.plusNanos(1); |
||||
|
} |
||||
|
break; |
||||
|
case QUARTER: |
||||
|
while (startTime.isBefore(endTime)) { |
||||
|
int quarterOfYear = getQuarterOfYear(startTime); |
||||
|
LocalDateTime quarterEnd = quarterOfYear == 4 |
||||
|
? startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1) |
||||
|
: startTime.withMonth(quarterOfYear * 3 + 1).withDayOfMonth(1).minusNanos(1); |
||||
|
timeRanges.add(new LocalDateTime[]{startTime, quarterEnd}); |
||||
|
startTime = quarterEnd.plusNanos(1); |
||||
|
} |
||||
|
break; |
||||
|
case YEAR: |
||||
|
while (startTime.isBefore(endTime)) { |
||||
|
LocalDateTime endOfYear = startTime.with(TemporalAdjusters.lastDayOfYear()).plusDays(1).minusNanos(1); |
||||
|
timeRanges.add(new LocalDateTime[]{startTime, endOfYear}); |
||||
|
startTime = endOfYear.plusNanos(1); |
||||
|
} |
||||
|
break; |
||||
|
default: |
||||
|
throw new IllegalArgumentException("Invalid interval: " + interval); |
||||
|
} |
||||
|
// 3. 兜底,最后一个时间,需要保持在 endTime 之前
|
||||
|
LocalDateTime[] lastTimeRange = CollUtil.getLast(timeRanges); |
||||
|
if (lastTimeRange != null) { |
||||
|
lastTimeRange[1] = endTime; |
||||
|
} |
||||
|
return timeRanges; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 格式化时间范围 |
||||
|
* |
||||
|
* @param startTime 开始时间 |
||||
|
* @param endTime 结束时间 |
||||
|
* @param interval 时间间隔 |
||||
|
* @return 时间范围 |
||||
|
*/ |
||||
|
public static String formatDateRange(LocalDateTime startTime, LocalDateTime endTime, Integer interval) { |
||||
|
// 1. 找到枚举
|
||||
|
DateIntervalEnum intervalEnum = DateIntervalEnum.valueOf(interval); |
||||
|
Assert.notNull(intervalEnum, "interval({}} 找不到对应的枚举", interval); |
||||
|
|
||||
|
// 2. 循环,生成时间范围
|
||||
|
switch (intervalEnum) { |
||||
|
case DAY: |
||||
|
return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN); |
||||
|
case WEEK: |
||||
|
return LocalDateTimeUtil.format(startTime, DatePattern.NORM_DATE_PATTERN) |
||||
|
+ StrUtil.format("(第 {} 周)", LocalDateTimeUtil.weekOfYear(startTime)); |
||||
|
case MONTH: |
||||
|
return LocalDateTimeUtil.format(startTime, DatePattern.NORM_MONTH_PATTERN); |
||||
|
case QUARTER: |
||||
|
return StrUtil.format("{}-Q{}", startTime.getYear(), getQuarterOfYear(startTime)); |
||||
|
case YEAR: |
||||
|
return LocalDateTimeUtil.format(startTime, DatePattern.NORM_YEAR_PATTERN); |
||||
|
default: |
||||
|
throw new IllegalArgumentException("Invalid interval: " + interval); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,126 @@ |
|||||
|
package com.qiantoon.platform.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 jakarta.servlet.http.HttpServletRequest; |
||||
|
import java.net.URI; |
||||
|
import java.nio.charset.Charset; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* HTTP 工具类 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -0,0 +1,84 @@ |
|||||
|
package com.qiantoon.platform.framework.common.util.io; |
||||
|
|
||||
|
import cn.hutool.core.io.FileTypeUtil; |
||||
|
import cn.hutool.core.io.FileUtil; |
||||
|
import cn.hutool.core.io.file.FileNameUtil; |
||||
|
import cn.hutool.core.util.IdUtil; |
||||
|
import cn.hutool.core.util.StrUtil; |
||||
|
import cn.hutool.crypto.digest.DigestUtil; |
||||
|
import lombok.SneakyThrows; |
||||
|
|
||||
|
import java.io.ByteArrayInputStream; |
||||
|
import java.io.File; |
||||
|
|
||||
|
/** |
||||
|
* 文件工具类 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 生成文件路径 |
||||
|
* |
||||
|
* @param content 文件内容 |
||||
|
* @param originalName 原始文件名 |
||||
|
* @return path,唯一不可重复 |
||||
|
*/ |
||||
|
public static String generatePath(byte[] content, String originalName) { |
||||
|
String sha256Hex = DigestUtil.sha256Hex(content); |
||||
|
// 情况一:如果存在 name,则优先使用 name 的后缀
|
||||
|
if (StrUtil.isNotBlank(originalName)) { |
||||
|
String extName = FileNameUtil.extName(originalName); |
||||
|
return StrUtil.isBlank(extName) ? sha256Hex : sha256Hex + "." + extName; |
||||
|
} |
||||
|
// 情况二:基于 content 计算
|
||||
|
return sha256Hex + '.' + FileTypeUtil.getType(new ByteArrayInputStream(content)); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
package com.qiantoon.platform.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 qt |
||||
|
*/ |
||||
|
public class IoUtils { |
||||
|
|
||||
|
/** |
||||
|
* 从流中读取 UTF8 编码的内容 |
||||
|
* |
||||
|
* @param in 输入流 |
||||
|
* @param isClose 是否关闭 |
||||
|
* @return 内容 |
||||
|
* @throws IORuntimeException IO 异常 |
||||
|
*/ |
||||
|
public static String readUtf8(InputStream in, boolean isClose) throws IORuntimeException { |
||||
|
return StrUtil.utf8Str(IoUtil.read(in, isClose)); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,202 @@ |
|||||
|
package com.qiantoon.platform.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.annotation.JsonInclude; |
||||
|
import com.fasterxml.jackson.core.type.TypeReference; |
||||
|
import com.fasterxml.jackson.databind.DeserializationFeature; |
||||
|
import com.fasterxml.jackson.databind.JsonNode; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import com.fasterxml.jackson.databind.SerializationFeature; |
||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; |
||||
|
import lombok.SneakyThrows; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.lang.reflect.Type; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* JSON 工具类 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
public class JsonUtils { |
||||
|
|
||||
|
private static ObjectMapper objectMapper = new ObjectMapper(); |
||||
|
|
||||
|
static { |
||||
|
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); |
||||
|
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); |
||||
|
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 忽略 null 值
|
||||
|
objectMapper.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 初始化 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); |
||||
|
} |
||||
|
|
||||
|
@SneakyThrows |
||||
|
public static String toJsonPrettyString(Object object) { |
||||
|
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static <T> T parseObject(String text, String path, Class<T> clazz) { |
||||
|
if (StrUtil.isEmpty(text)) { |
||||
|
return null; |
||||
|
} |
||||
|
try { |
||||
|
JsonNode treeNode = objectMapper.readTree(text); |
||||
|
JsonNode pathNode = treeNode.path(path); |
||||
|
return objectMapper.readValue(pathNode.toString(), clazz); |
||||
|
} catch (IOException e) { |
||||
|
log.error("json parse err,json:{}", text, e); |
||||
|
throw new RuntimeException(e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static <T> T parseObject(String text, Type type) { |
||||
|
if (StrUtil.isEmpty(text)) { |
||||
|
return null; |
||||
|
} |
||||
|
try { |
||||
|
return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type)); |
||||
|
} 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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 解析 JSON 字符串成指定类型的对象,如果解析失败,则返回 null |
||||
|
* |
||||
|
* @param text 字符串 |
||||
|
* @param typeReference 类型引用 |
||||
|
* @return 指定类型的对象 |
||||
|
*/ |
||||
|
public static <T> T parseObjectQuietly(String text, TypeReference<T> typeReference) { |
||||
|
try { |
||||
|
return objectMapper.readValue(text, typeReference); |
||||
|
} catch (IOException e) { |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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 <T> List<T> parseArray(String text, String path, Class<T> clazz) { |
||||
|
if (StrUtil.isEmpty(text)) { |
||||
|
return null; |
||||
|
} |
||||
|
try { |
||||
|
JsonNode treeNode = objectMapper.readTree(text); |
||||
|
JsonNode pathNode = treeNode.path(path); |
||||
|
return objectMapper.readValue(pathNode.toString(), 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.isTypeJSON(text); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
package com.qiantoon.platform.framework.common.util.monitor; |
||||
|
|
||||
|
import org.apache.skywalking.apm.toolkit.trace.TraceContext; |
||||
|
|
||||
|
/** |
||||
|
* 链路追踪工具类 |
||||
|
* |
||||
|
* 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 util 包下 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class TracerUtils { |
||||
|
|
||||
|
/** |
||||
|
* 私有化构造方法 |
||||
|
*/ |
||||
|
private TracerUtils() { |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获得链路追踪编号,直接返回 SkyWalking 的 TraceId。 |
||||
|
* 如果不存在的话为空字符串!!! |
||||
|
* |
||||
|
* @return 链路追踪编号 |
||||
|
*/ |
||||
|
public static String getTraceId() { |
||||
|
return TraceContext.traceId(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,131 @@ |
|||||
|
package com.qiantoon.platform.framework.common.util.number; |
||||
|
|
||||
|
import cn.hutool.core.math.Money; |
||||
|
import cn.hutool.core.util.NumberUtil; |
||||
|
|
||||
|
import java.math.BigDecimal; |
||||
|
import java.math.RoundingMode; |
||||
|
|
||||
|
/** |
||||
|
* 金额工具类 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class MoneyUtils { |
||||
|
|
||||
|
/** |
||||
|
* 金额的小数位数 |
||||
|
*/ |
||||
|
private static final int PRICE_SCALE = 2; |
||||
|
|
||||
|
/** |
||||
|
* 百分比对应的 BigDecimal 对象 |
||||
|
*/ |
||||
|
public static final BigDecimal PERCENT_100 = BigDecimal.valueOf(100); |
||||
|
|
||||
|
/** |
||||
|
* 计算百分比金额,四舍五入 |
||||
|
* |
||||
|
* @param price 金额 |
||||
|
* @param rate 百分比,例如说 56.77% 则传入 56.77 |
||||
|
* @return 百分比金额 |
||||
|
*/ |
||||
|
public static Integer calculateRatePrice(Integer price, Double rate) { |
||||
|
return calculateRatePrice(price, rate, 0, RoundingMode.HALF_UP).intValue(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算百分比金额,向下传入 |
||||
|
* |
||||
|
* @param price 金额 |
||||
|
* @param rate 百分比,例如说 56.77% 则传入 56.77 |
||||
|
* @return 百分比金额 |
||||
|
*/ |
||||
|
public static Integer calculateRatePriceFloor(Integer price, Double rate) { |
||||
|
return calculateRatePrice(price, rate, 0, RoundingMode.FLOOR).intValue(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算百分比金额 |
||||
|
* |
||||
|
* @param price 金额(单位分) |
||||
|
* @param count 数量 |
||||
|
* @param percent 折扣(单位分),列如 60.2%,则传入 6020 |
||||
|
* @return 商品总价 |
||||
|
*/ |
||||
|
public static Integer calculator(Integer price, Integer count, Integer percent) { |
||||
|
price = price * count; |
||||
|
if (percent == null) { |
||||
|
return price; |
||||
|
} |
||||
|
return MoneyUtils.calculateRatePriceFloor(price, (double) (percent / 100)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算百分比金额 |
||||
|
* |
||||
|
* @param price 金额 |
||||
|
* @param rate 百分比,例如说 56.77% 则传入 56.77 |
||||
|
* @param scale 保留小数位数 |
||||
|
* @param roundingMode 舍入模式 |
||||
|
*/ |
||||
|
public static BigDecimal calculateRatePrice(Number price, Number rate, int scale, RoundingMode roundingMode) { |
||||
|
return NumberUtil.toBigDecimal(price).multiply(NumberUtil.toBigDecimal(rate)) // 乘以
|
||||
|
.divide(BigDecimal.valueOf(100), scale, roundingMode); // 除以 100
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 分转元 |
||||
|
* |
||||
|
* @param fen 分 |
||||
|
* @return 元 |
||||
|
*/ |
||||
|
public static BigDecimal fenToYuan(int fen) { |
||||
|
return new Money(0, fen).getAmount(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 分转元(字符串) |
||||
|
* |
||||
|
* 例如说 fen 为 1 时,则结果为 0.01 |
||||
|
* |
||||
|
* @param fen 分 |
||||
|
* @return 元 |
||||
|
*/ |
||||
|
public static String fenToYuanStr(int fen) { |
||||
|
return new Money(0, fen).toString(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 金额相乘,默认进行四舍五入 |
||||
|
* |
||||
|
* 位数:{@link #PRICE_SCALE} |
||||
|
* |
||||
|
* @param price 金额 |
||||
|
* @param count 数量 |
||||
|
* @return 金额相乘结果 |
||||
|
*/ |
||||
|
public static BigDecimal priceMultiply(BigDecimal price, BigDecimal count) { |
||||
|
if (price == null || count == null) { |
||||
|
return null; |
||||
|
} |
||||
|
return price.multiply(count).setScale(PRICE_SCALE, RoundingMode.HALF_UP); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 金额相乘(百分比),默认进行四舍五入 |
||||
|
* |
||||
|
* 位数:{@link #PRICE_SCALE} |
||||
|
* |
||||
|
* @param price 金额 |
||||
|
* @param percent 百分比 |
||||
|
* @return 金额相乘结果 |
||||
|
*/ |
||||
|
public static BigDecimal priceMultiplyPercent(BigDecimal price, BigDecimal percent) { |
||||
|
if (price == null || percent == null) { |
||||
|
return null; |
||||
|
} |
||||
|
return price.multiply(percent).divide(PERCENT_100, PRICE_SCALE, RoundingMode.HALF_UP); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,64 @@ |
|||||
|
package com.qiantoon.platform.framework.common.util.number; |
||||
|
|
||||
|
import cn.hutool.core.util.NumberUtil; |
||||
|
import cn.hutool.core.util.StrUtil; |
||||
|
|
||||
|
import java.math.BigDecimal; |
||||
|
|
||||
|
/** |
||||
|
* 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class NumberUtils { |
||||
|
|
||||
|
public static Long parseLong(String str) { |
||||
|
return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null; |
||||
|
} |
||||
|
|
||||
|
public static Integer parseInt(String str) { |
||||
|
return StrUtil.isNotEmpty(str) ? Integer.valueOf(str) : null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 通过经纬度获取地球上两点之间的距离 |
||||
|
* |
||||
|
* 参考 <<a href="https://gitee.com/dromara/hutool/blob/1caabb586b1f95aec66a21d039c5695df5e0f4c1/hutool-core/src/main/java/cn/hutool/core/util/DistanceUtil.java">DistanceUtil</a>> 实现,目前它已经被 hutool 删除 |
||||
|
* |
||||
|
* @param lat1 经度1 |
||||
|
* @param lng1 纬度1 |
||||
|
* @param lat2 经度2 |
||||
|
* @param lng2 纬度2 |
||||
|
* @return 距离,单位:千米 |
||||
|
*/ |
||||
|
public static double getDistance(double lat1, double lng1, double lat2, double lng2) { |
||||
|
double radLat1 = lat1 * Math.PI / 180.0; |
||||
|
double radLat2 = lat2 * Math.PI / 180.0; |
||||
|
double a = radLat1 - radLat2; |
||||
|
double b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0; |
||||
|
double distance = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) |
||||
|
+ Math.cos(radLat1) * Math.cos(radLat2) |
||||
|
* Math.pow(Math.sin(b / 2), 2))); |
||||
|
distance = distance * 6378.137; |
||||
|
distance = Math.round(distance * 10000d) / 10000d; |
||||
|
return distance; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 提供精确的乘法运算 |
||||
|
* |
||||
|
* 和 hutool {@link NumberUtil#mul(BigDecimal...)} 的差别是,如果存在 null,则返回 null |
||||
|
* |
||||
|
* @param values 多个被乘值 |
||||
|
* @return 积 |
||||
|
*/ |
||||
|
public static BigDecimal mul(BigDecimal... values) { |
||||
|
for (BigDecimal value : values) { |
||||
|
if (value == null) { |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
return NumberUtil.mul(values); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,62 @@ |
|||||
|
package com.qiantoon.platform.framework.common.util.object; |
||||
|
|
||||
|
import cn.hutool.core.bean.BeanUtil; |
||||
|
import com.qiantoon.platform.framework.common.pojo.PageResult; |
||||
|
import com.qiantoon.platform.framework.common.util.collection.CollectionUtils; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.function.Consumer; |
||||
|
|
||||
|
/** |
||||
|
* Bean 工具类 |
||||
|
* |
||||
|
* 1. 默认使用 {@link cn.hutool.core.bean.BeanUtil} 作为实现类,虽然不同 bean 工具的性能有差别,但是对绝大多数同学的项目,不用在意这点性能 |
||||
|
* 2. 针对复杂的对象转换,可以搜参考 AuthConvert 实现,通过 mapstruct + default 配合实现 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class BeanUtils { |
||||
|
|
||||
|
public static <T> T toBean(Object source, Class<T> targetClass) { |
||||
|
return BeanUtil.toBean(source, targetClass); |
||||
|
} |
||||
|
|
||||
|
public static <T> T toBean(Object source, Class<T> targetClass, Consumer<T> peek) { |
||||
|
T target = toBean(source, targetClass); |
||||
|
if (target != null) { |
||||
|
peek.accept(target); |
||||
|
} |
||||
|
return target; |
||||
|
} |
||||
|
|
||||
|
public static <S, T> List<T> toBean(List<S> source, Class<T> targetType) { |
||||
|
if (source == null) { |
||||
|
return null; |
||||
|
} |
||||
|
return CollectionUtils.convertList(source, s -> toBean(s, targetType)); |
||||
|
} |
||||
|
|
||||
|
public static <S, T> List<T> toBean(List<S> source, Class<T> targetType, Consumer<T> peek) { |
||||
|
List<T> list = toBean(source, targetType); |
||||
|
if (list != null) { |
||||
|
list.forEach(peek); |
||||
|
} |
||||
|
return list; |
||||
|
} |
||||
|
|
||||
|
public static <S, T> PageResult<T> toBean(PageResult<S> source, Class<T> targetType) { |
||||
|
return toBean(source, targetType, null); |
||||
|
} |
||||
|
|
||||
|
public static <S, T> PageResult<T> toBean(PageResult<S> source, Class<T> targetType, Consumer<T> peek) { |
||||
|
if (source == null) { |
||||
|
return null; |
||||
|
} |
||||
|
List<T> list = toBean(source.getList(), targetType); |
||||
|
if (peek != null) { |
||||
|
list.forEach(peek); |
||||
|
} |
||||
|
return new PageResult<>(list, source.getTotal()); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
package com.qiantoon.platform.framework.common.util.object; |
||||
|
|
||||
|
import cn.hutool.core.util.ObjectUtil; |
||||
|
import cn.hutool.core.util.ReflectUtil; |
||||
|
|
||||
|
import java.lang.reflect.Field; |
||||
|
import java.util.Arrays; |
||||
|
import java.util.function.Consumer; |
||||
|
|
||||
|
/** |
||||
|
* Object 工具类 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
@SafeVarargs |
||||
|
public static <T> T defaultIfNull(T... array) { |
||||
|
for (T item : array) { |
||||
|
if (item != null) { |
||||
|
return item; |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
@SafeVarargs |
||||
|
public static <T> boolean equalsAny(T obj, T... array) { |
||||
|
return Arrays.asList(array).contains(obj); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
package com.qiantoon.platform.framework.common.util.object; |
||||
|
|
||||
|
import cn.hutool.core.collection.CollUtil; |
||||
|
import cn.hutool.core.lang.func.Func1; |
||||
|
import cn.hutool.core.lang.func.LambdaUtil; |
||||
|
import cn.hutool.core.util.ArrayUtil; |
||||
|
import com.qiantoon.platform.framework.common.pojo.PageParam; |
||||
|
import com.qiantoon.platform.framework.common.pojo.SortablePageParam; |
||||
|
import com.qiantoon.platform.framework.common.pojo.SortingField; |
||||
|
import org.springframework.util.Assert; |
||||
|
|
||||
|
import static java.util.Collections.singletonList; |
||||
|
|
||||
|
/** |
||||
|
* {@link com.qiantoon.platform.framework.common.pojo.PageParam} 工具类 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class PageUtils { |
||||
|
|
||||
|
private static final Object[] ORDER_TYPES = new String[]{SortingField.ORDER_ASC, SortingField.ORDER_DESC}; |
||||
|
|
||||
|
public static int getStart(PageParam pageParam) { |
||||
|
return (pageParam.getPageNo() - 1) * pageParam.getPageSize(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 构建排序字段(默认倒序) |
||||
|
* |
||||
|
* @param func 排序字段的 Lambda 表达式 |
||||
|
* @param <T> 排序字段所属的类型 |
||||
|
* @return 排序字段 |
||||
|
*/ |
||||
|
public static <T> SortingField buildSortingField(Func1<T, ?> func) { |
||||
|
return buildSortingField(func, SortingField.ORDER_DESC); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 构建排序字段 |
||||
|
* |
||||
|
* @param func 排序字段的 Lambda 表达式 |
||||
|
* @param order 排序类型 {@link SortingField#ORDER_ASC} {@link SortingField#ORDER_DESC} |
||||
|
* @param <T> 排序字段所属的类型 |
||||
|
* @return 排序字段 |
||||
|
*/ |
||||
|
public static <T> SortingField buildSortingField(Func1<T, ?> func, String order) { |
||||
|
Assert.isTrue(ArrayUtil.contains(ORDER_TYPES, order), String.format("字段的排序类型只能是 %s/%s", ORDER_TYPES)); |
||||
|
|
||||
|
String fieldName = LambdaUtil.getFieldName(func); |
||||
|
return new SortingField(fieldName, order); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 构建默认的排序字段 |
||||
|
* 如果排序字段为空,则设置排序字段;否则忽略 |
||||
|
* |
||||
|
* @param sortablePageParam 排序分页查询参数 |
||||
|
* @param func 排序字段的 Lambda 表达式 |
||||
|
* @param <T> 排序字段所属的类型 |
||||
|
*/ |
||||
|
public static <T> void buildDefaultSortingField(SortablePageParam sortablePageParam, Func1<T, ?> func) { |
||||
|
if (sortablePageParam != null && CollUtil.isEmpty(sortablePageParam.getSortingFields())) { |
||||
|
sortablePageParam.setSortingFields(singletonList(buildSortingField(func))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
/** |
||||
|
* 对于工具类的选择,优先查找 Hutool 中有没对应的方法 |
||||
|
* 如果没有,则自己封装对应的工具类,以 Utils 结尾,用于区分 |
||||
|
* |
||||
|
* ps:如果担心 Hutool 存在坑的问题,可以阅读 Hutool 的实现源码,以确保可靠性。并且,可以补充相关的单元测试。 |
||||
|
*/ |
||||
|
package com.qiantoon.platform.framework.common.util; |
||||
@ -0,0 +1,101 @@ |
|||||
|
package com.qiantoon.platform.framework.common.util.servlet; |
||||
|
|
||||
|
import cn.hutool.core.util.StrUtil; |
||||
|
import cn.hutool.extra.servlet.JakartaServletUtil; |
||||
|
import com.qiantoon.platform.framework.common.util.json.JsonUtils; |
||||
|
import jakarta.servlet.ServletRequest; |
||||
|
import jakarta.servlet.http.HttpServletRequest; |
||||
|
import jakarta.servlet.http.HttpServletResponse; |
||||
|
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 java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* 客户端工具类 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
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); |
||||
|
JakartaServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @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 JakartaServletUtil.getClientIP(request); |
||||
|
} |
||||
|
|
||||
|
public static boolean isJsonRequest(ServletRequest request) { |
||||
|
return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE); |
||||
|
} |
||||
|
|
||||
|
public static String getBody(HttpServletRequest request) { |
||||
|
// 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取
|
||||
|
if (isJsonRequest(request)) { |
||||
|
return JakartaServletUtil.getBody(request); |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
public static byte[] getBodyBytes(HttpServletRequest request) { |
||||
|
// 只有在 json 请求在读取,因为只有 CacheRequestBodyFilter 才会进行缓存,支持重复读取
|
||||
|
if (isJsonRequest(request)) { |
||||
|
return JakartaServletUtil.getBodyBytes(request); |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
public static String getClientIP(HttpServletRequest request) { |
||||
|
return JakartaServletUtil.getClientIP(request); |
||||
|
} |
||||
|
|
||||
|
public static Map<String, String> getParamMap(HttpServletRequest request) { |
||||
|
return JakartaServletUtil.getParamMap(request); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,89 @@ |
|||||
|
package com.qiantoon.platform.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.JoinPoint; |
||||
|
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 { |
||||
|
|
||||
|
/** |
||||
|
* Spring EL 表达式解析器 |
||||
|
*/ |
||||
|
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(JoinPoint 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(JoinPoint joinPoint, List<String> expressionStrings) { |
||||
|
// 如果为空,则不进行解析
|
||||
|
if (CollUtil.isEmpty(expressionStrings)) { |
||||
|
return MapUtil.newHashMap(); |
||||
|
} |
||||
|
|
||||
|
// 第一步,构建解析的上下文 EvaluationContext
|
||||
|
// 通过 joinPoint 获取被注解方法
|
||||
|
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); |
||||
|
Method method = methodSignature.getMethod(); |
||||
|
// 使用 spring 的 ParameterNameDiscoverer 获取方法形参名数组
|
||||
|
String[] paramNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method); |
||||
|
// Spring 的表达式上下文对象
|
||||
|
EvaluationContext context = new StandardEvaluationContext(); |
||||
|
// 给上下文赋值
|
||||
|
if (ArrayUtil.isNotEmpty(paramNames)) { |
||||
|
Object[] args = joinPoint.getArgs(); |
||||
|
for (int i = 0; i < paramNames.length; i++) { |
||||
|
context.setVariable(paramNames[i], args[i]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 第二步,逐个参数解析
|
||||
|
Map<String, Object> result = MapUtil.newHashMap(expressionStrings.size(), true); |
||||
|
expressionStrings.forEach(key -> { |
||||
|
Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context); |
||||
|
result.put(key, value); |
||||
|
}); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
package com.qiantoon.platform.framework.common.util.spring; |
||||
|
|
||||
|
import cn.hutool.extra.spring.SpringUtil; |
||||
|
|
||||
|
import java.util.Objects; |
||||
|
|
||||
|
/** |
||||
|
* Spring 工具类 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class SpringUtils extends SpringUtil { |
||||
|
|
||||
|
/** |
||||
|
* 是否为生产环境 |
||||
|
* |
||||
|
* @return 是否生产环境 |
||||
|
*/ |
||||
|
public static boolean isProd() { |
||||
|
String activeProfile = getActiveProfile(); |
||||
|
return Objects.equals("prod", activeProfile); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
package com.qiantoon.platform.framework.common.util.string; |
||||
|
|
||||
|
import cn.hutool.core.text.StrPool; |
||||
|
import cn.hutool.core.util.ArrayUtil; |
||||
|
import cn.hutool.core.util.StrUtil; |
||||
|
|
||||
|
import java.util.Arrays; |
||||
|
import java.util.Collection; |
||||
|
import java.util.List; |
||||
|
import java.util.Set; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
/** |
||||
|
* 字符串工具类 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
public static List<Long> splitToLong(String value, CharSequence separator) { |
||||
|
long[] longs = StrUtil.splitToLong(value, separator); |
||||
|
return Arrays.stream(longs).boxed().collect(Collectors.toList()); |
||||
|
} |
||||
|
|
||||
|
public static Set<Long> splitToLongSet(String value) { |
||||
|
return splitToLongSet(value, StrPool.COMMA); |
||||
|
} |
||||
|
|
||||
|
public static Set<Long> splitToLongSet(String value, CharSequence separator) { |
||||
|
long[] longs = StrUtil.splitToLong(value, separator); |
||||
|
return Arrays.stream(longs).boxed().collect(Collectors.toSet()); |
||||
|
} |
||||
|
|
||||
|
public static List<Integer> splitToInteger(String value, CharSequence separator) { |
||||
|
int[] integers = StrUtil.splitToInt(value, separator); |
||||
|
return Arrays.stream(integers).boxed().collect(Collectors.toList()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 移除字符串中,包含指定字符串的行 |
||||
|
* |
||||
|
* @param content 字符串 |
||||
|
* @param sequence 包含的字符串 |
||||
|
* @return 移除后的字符串 |
||||
|
*/ |
||||
|
public static String removeLineContains(String content, String sequence) { |
||||
|
if (StrUtil.isEmpty(content) || StrUtil.isEmpty(sequence)) { |
||||
|
return content; |
||||
|
} |
||||
|
return Arrays.stream(content.split("\n")) |
||||
|
.filter(line -> !line.contains(sequence)) |
||||
|
.collect(Collectors.joining("\n")); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,55 @@ |
|||||
|
package com.qiantoon.platform.framework.common.util.validation; |
||||
|
|
||||
|
import cn.hutool.core.collection.CollUtil; |
||||
|
import cn.hutool.core.lang.Assert; |
||||
|
import org.springframework.util.StringUtils; |
||||
|
|
||||
|
import jakarta.validation.ConstraintViolation; |
||||
|
import jakarta.validation.ConstraintViolationException; |
||||
|
import jakarta.validation.Validation; |
||||
|
import jakarta.validation.Validator; |
||||
|
import java.util.Set; |
||||
|
import java.util.regex.Pattern; |
||||
|
|
||||
|
/** |
||||
|
* 校验工具类 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class ValidationUtils { |
||||
|
|
||||
|
private static final Pattern PATTERN_MOBILE = Pattern.compile("^(?:(?:\\+|00)86)?1(?:(?:3[\\d])|(?:4[0,1,4-9])|(?:5[0-3,5-9])|(?:6[2,5-7])|(?:7[0-8])|(?:8[\\d])|(?:9[0-3,5-9]))\\d{8}$"); |
||||
|
|
||||
|
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) { |
||||
|
return StringUtils.hasText(mobile) |
||||
|
&& PATTERN_MOBILE.matcher(mobile).matches(); |
||||
|
} |
||||
|
|
||||
|
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(Object object, Class<?>... groups) { |
||||
|
Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); |
||||
|
Assert.notNull(validator); |
||||
|
validate(validator, object, groups); |
||||
|
} |
||||
|
|
||||
|
public static void validate(Validator validator, Object object, Class<?>... groups) { |
||||
|
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups); |
||||
|
if (CollUtil.isNotEmpty(constraintViolations)) { |
||||
|
throw new ConstraintViolationException(constraintViolations); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
package com.qiantoon.platform.framework.common.validation; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.common.core.IntArrayValuable; |
||||
|
|
||||
|
import jakarta.validation.Constraint; |
||||
|
import jakarta.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, InEnumCollectionValidator.class} |
||||
|
) |
||||
|
public @interface InEnum { |
||||
|
|
||||
|
/** |
||||
|
* @return 实现 EnumValuable 接口的 |
||||
|
*/ |
||||
|
Class<? extends IntArrayValuable> value(); |
||||
|
|
||||
|
String message() default "必须在指定范围 {value}"; |
||||
|
|
||||
|
Class<?>[] groups() default {}; |
||||
|
|
||||
|
Class<? extends Payload>[] payload() default {}; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
package com.qiantoon.platform.framework.common.validation; |
||||
|
|
||||
|
import cn.hutool.core.collection.CollUtil; |
||||
|
import com.qiantoon.platform.framework.common.core.IntArrayValuable; |
||||
|
|
||||
|
import jakarta.validation.ConstraintValidator; |
||||
|
import jakarta.validation.ConstraintValidatorContext; |
||||
|
import java.util.Arrays; |
||||
|
import java.util.Collection; |
||||
|
import java.util.Collections; |
||||
|
import java.util.List; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
public class InEnumCollectionValidator implements ConstraintValidator<InEnum, Collection<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(Collection<Integer> list, ConstraintValidatorContext context) { |
||||
|
// 校验通过
|
||||
|
if (CollUtil.containsAll(values, list)) { |
||||
|
return true; |
||||
|
} |
||||
|
// 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值)
|
||||
|
context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
|
||||
|
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate() |
||||
|
.replaceAll("\\{value}", CollUtil.join(list, ","))).addConstraintViolation(); // 重新添加错误提示语句
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
@ -0,0 +1,44 @@ |
|||||
|
package com.qiantoon.platform.framework.common.validation; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.common.core.IntArrayValuable; |
||||
|
|
||||
|
import jakarta.validation.ConstraintValidator; |
||||
|
import jakarta.validation.ConstraintValidatorContext; |
||||
|
import java.util.Arrays; |
||||
|
import java.util.Collections; |
||||
|
import java.util.List; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
public class InEnumValidator implements ConstraintValidator<InEnum, Integer> { |
||||
|
|
||||
|
private List<Integer> values; |
||||
|
|
||||
|
@Override |
||||
|
public void initialize(InEnum annotation) { |
||||
|
IntArrayValuable[] values = annotation.value().getEnumConstants(); |
||||
|
if (values.length == 0) { |
||||
|
this.values = Collections.emptyList(); |
||||
|
} else { |
||||
|
this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toList()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean isValid(Integer value, ConstraintValidatorContext context) { |
||||
|
// 为空时,默认不校验,即认为通过
|
||||
|
if (value == null) { |
||||
|
return true; |
||||
|
} |
||||
|
// 校验通过
|
||||
|
if (values.contains(value)) { |
||||
|
return true; |
||||
|
} |
||||
|
// 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值)
|
||||
|
context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
|
||||
|
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate() |
||||
|
.replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
@ -0,0 +1,28 @@ |
|||||
|
package com.qiantoon.platform.framework.common.validation; |
||||
|
|
||||
|
import jakarta.validation.Constraint; |
||||
|
import jakarta.validation.Payload; |
||||
|
import java.lang.annotation.*; |
||||
|
|
||||
|
@Target({ |
||||
|
ElementType.METHOD, |
||||
|
ElementType.FIELD, |
||||
|
ElementType.ANNOTATION_TYPE, |
||||
|
ElementType.CONSTRUCTOR, |
||||
|
ElementType.PARAMETER, |
||||
|
ElementType.TYPE_USE |
||||
|
}) |
||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||
|
@Documented |
||||
|
@Constraint( |
||||
|
validatedBy = MobileValidator.class |
||||
|
) |
||||
|
public @interface Mobile { |
||||
|
|
||||
|
String message() default "手机号格式不正确"; |
||||
|
|
||||
|
Class<?>[] groups() default {}; |
||||
|
|
||||
|
Class<? extends Payload>[] payload() default {}; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
package com.qiantoon.platform.framework.common.validation; |
||||
|
|
||||
|
import cn.hutool.core.util.StrUtil; |
||||
|
import com.qiantoon.platform.framework.common.util.validation.ValidationUtils; |
||||
|
|
||||
|
import jakarta.validation.ConstraintValidator; |
||||
|
import jakarta.validation.ConstraintValidatorContext; |
||||
|
|
||||
|
public class MobileValidator implements ConstraintValidator<Mobile, String> { |
||||
|
|
||||
|
@Override |
||||
|
public void initialize(Mobile annotation) { |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean isValid(String value, ConstraintValidatorContext context) { |
||||
|
// 如果手机号为空,默认不校验,即校验通过
|
||||
|
if (StrUtil.isEmpty(value)) { |
||||
|
return true; |
||||
|
} |
||||
|
// 校验手机
|
||||
|
return ValidationUtils.isMobile(value); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
package com.qiantoon.platform.framework.common.validation; |
||||
|
|
||||
|
import jakarta.validation.Constraint; |
||||
|
import jakarta.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 = TelephoneValidator.class |
||||
|
) |
||||
|
public @interface Telephone { |
||||
|
|
||||
|
String message() default "电话格式不正确"; |
||||
|
|
||||
|
Class<?>[] groups() default {}; |
||||
|
|
||||
|
Class<? extends Payload>[] payload() default {}; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
package com.qiantoon.platform.framework.common.validation; |
||||
|
|
||||
|
import cn.hutool.core.text.CharSequenceUtil; |
||||
|
import cn.hutool.core.util.PhoneUtil; |
||||
|
|
||||
|
import jakarta.validation.ConstraintValidator; |
||||
|
import jakarta.validation.ConstraintValidatorContext; |
||||
|
|
||||
|
public class TelephoneValidator implements ConstraintValidator<Telephone, String> { |
||||
|
|
||||
|
@Override |
||||
|
public void initialize(Telephone annotation) { |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean isValid(String value, ConstraintValidatorContext context) { |
||||
|
// 如果手机号为空,默认不校验,即校验通过
|
||||
|
if (CharSequenceUtil.isEmpty(value)) { |
||||
|
return true; |
||||
|
} |
||||
|
// 校验手机
|
||||
|
return PhoneUtil.isTel(value) || PhoneUtil.isPhone(value); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
/** |
||||
|
* 使用 Hibernate Validator 实现参数校验 |
||||
|
*/ |
||||
|
package com.qiantoon.platform.framework.common.validation; |
||||
@ -0,0 +1,45 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" |
||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
|
<parent> |
||||
|
<artifactId>platform-framework</artifactId> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<version>${revision}</version> |
||||
|
</parent> |
||||
|
<modelVersion>4.0.0</modelVersion> |
||||
|
<artifactId>platform-spring-boot-starter-biz-data-permission</artifactId> |
||||
|
<packaging>jar</packaging> |
||||
|
|
||||
|
<name>${project.artifactId}</name> |
||||
|
<description>数据权限</description> |
||||
|
|
||||
|
<dependencies> |
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-common</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- Web 相关 --> |
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-spring-boot-starter-security</artifactId> |
||||
|
<optional>true</optional> <!-- 可选,如果使用 DeptDataPermissionRule 必须提供 --> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- DB 相关 --> |
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-spring-boot-starter-mybatis</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- 业务组件 --> |
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-module-system-api</artifactId> <!-- 需要使用它,进行数据权限的获取 --> |
||||
|
<version>${revision}</version> |
||||
|
</dependency> |
||||
|
|
||||
|
</dependencies> |
||||
|
|
||||
|
</project> |
||||
@ -0,0 +1,44 @@ |
|||||
|
package com.qiantoon.platform.framework.datapermission.config; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor; |
||||
|
import com.qiantoon.platform.framework.datapermission.core.db.DataPermissionDatabaseInterceptor; |
||||
|
import com.qiantoon.platform.framework.datapermission.core.rule.DataPermissionRule; |
||||
|
import com.qiantoon.platform.framework.datapermission.core.rule.DataPermissionRuleFactory; |
||||
|
import com.qiantoon.platform.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl; |
||||
|
import com.qiantoon.platform.framework.mybatis.core.util.MyBatisUtils; |
||||
|
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; |
||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration; |
||||
|
import org.springframework.context.annotation.Bean; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 数据权限的自动配置类 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
@AutoConfiguration |
||||
|
public class PlatformDataPermissionAutoConfiguration { |
||||
|
|
||||
|
@Bean |
||||
|
public DataPermissionRuleFactory dataPermissionRuleFactory(List<DataPermissionRule> rules) { |
||||
|
return new DataPermissionRuleFactoryImpl(rules); |
||||
|
} |
||||
|
|
||||
|
@Bean |
||||
|
public DataPermissionDatabaseInterceptor dataPermissionDatabaseInterceptor(MybatisPlusInterceptor interceptor, |
||||
|
DataPermissionRuleFactory ruleFactory) { |
||||
|
// 创建 DataPermissionDatabaseInterceptor 拦截器
|
||||
|
DataPermissionDatabaseInterceptor inner = new DataPermissionDatabaseInterceptor(ruleFactory); |
||||
|
// 添加到 interceptor 中
|
||||
|
// 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
|
||||
|
MyBatisUtils.addInterceptor(interceptor, inner, 0); |
||||
|
return inner; |
||||
|
} |
||||
|
|
||||
|
@Bean |
||||
|
public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() { |
||||
|
return new DataPermissionAnnotationAdvisor(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
package com.qiantoon.platform.framework.datapermission.config; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.datapermission.core.rule.dept.DeptDataPermissionRule; |
||||
|
import com.qiantoon.platform.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer; |
||||
|
import com.qiantoon.platform.framework.security.core.LoginUser; |
||||
|
import com.qiantoon.platform.module.system.api.permission.PermissionApi; |
||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration; |
||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; |
||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
||||
|
import org.springframework.context.annotation.Bean; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 基于部门的数据权限 AutoConfiguration |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
@AutoConfiguration |
||||
|
@ConditionalOnClass(LoginUser.class) |
||||
|
@ConditionalOnBean(value = {PermissionApi.class, DeptDataPermissionRuleCustomizer.class}) |
||||
|
public class PlatformDeptDataPermissionAutoConfiguration { |
||||
|
|
||||
|
@Bean |
||||
|
public DeptDataPermissionRule deptDataPermissionRule(PermissionApi permissionApi, |
||||
|
List<DeptDataPermissionRuleCustomizer> customizers) { |
||||
|
// 创建 DeptDataPermissionRule 对象
|
||||
|
DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi); |
||||
|
// 补全表配置
|
||||
|
customizers.forEach(customizer -> customizer.customize(rule)); |
||||
|
return rule; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
package com.qiantoon.platform.framework.datapermission.core.annotation; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.datapermission.core.rule.DataPermissionRule; |
||||
|
|
||||
|
import java.lang.annotation.*; |
||||
|
|
||||
|
/** |
||||
|
* 数据权限注解 |
||||
|
* 可声明在类或者方法上,标识使用的数据权限规则 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
@Target({ElementType.TYPE, ElementType.METHOD}) |
||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||
|
@Documented |
||||
|
public @interface DataPermission { |
||||
|
|
||||
|
/** |
||||
|
* 当前类或方法是否开启数据权限 |
||||
|
* 即使不添加 @DataPermission 注解,默认是开启状态 |
||||
|
* 可通过设置 enable 为 false 禁用 |
||||
|
*/ |
||||
|
boolean enable() default true; |
||||
|
|
||||
|
/** |
||||
|
* 生效的数据权限规则数组,优先级高于 {@link #excludeRules()} |
||||
|
*/ |
||||
|
Class<? extends DataPermissionRule>[] includeRules() default {}; |
||||
|
|
||||
|
/** |
||||
|
* 排除的数据权限规则数组,优先级最低 |
||||
|
*/ |
||||
|
Class<? extends DataPermissionRule>[] excludeRules() default {}; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
package com.qiantoon.platform.framework.datapermission.core.aop; |
||||
|
|
||||
|
import com.qiantoon.platform.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 com.qiantoon.platform.framework.datapermission.core.annotation.DataPermission} 注解的 Advisor 实现类 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
@Getter |
||||
|
@EqualsAndHashCode(callSuper = true) |
||||
|
public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor { |
||||
|
|
||||
|
private final Advice advice; |
||||
|
|
||||
|
private final Pointcut pointcut; |
||||
|
|
||||
|
public DataPermissionAnnotationAdvisor() { |
||||
|
this.advice = new DataPermissionAnnotationInterceptor(); |
||||
|
this.pointcut = this.buildPointcut(); |
||||
|
} |
||||
|
|
||||
|
protected Pointcut buildPointcut() { |
||||
|
Pointcut classPointcut = new AnnotationMatchingPointcut(DataPermission.class, true); |
||||
|
Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataPermission.class, true); |
||||
|
return new ComposablePointcut(classPointcut).union(methodPointcut); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,72 @@ |
|||||
|
package com.qiantoon.platform.framework.datapermission.core.aop; |
||||
|
|
||||
|
import com.qiantoon.platform.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 qt |
||||
|
*/ |
||||
|
@DataPermission // 该注解,用于 {@link DATA_PERMISSION_NULL} 的空对象
|
||||
|
public class DataPermissionAnnotationInterceptor implements MethodInterceptor { |
||||
|
|
||||
|
/** |
||||
|
* DataPermission 空对象,用于方法无 {@link DataPermission} 注解时,使用 DATA_PERMISSION_NULL 进行占位 |
||||
|
*/ |
||||
|
static final DataPermission DATA_PERMISSION_NULL = DataPermissionAnnotationInterceptor.class.getAnnotation(DataPermission.class); |
||||
|
|
||||
|
@Getter |
||||
|
private final Map<MethodClassKey, DataPermission> dataPermissionCache = new ConcurrentHashMap<>(); |
||||
|
|
||||
|
@Override |
||||
|
public Object invoke(MethodInvocation methodInvocation) throws Throwable { |
||||
|
// 入栈
|
||||
|
DataPermission dataPermission = this.findAnnotation(methodInvocation); |
||||
|
if (dataPermission != null) { |
||||
|
DataPermissionContextHolder.add(dataPermission); |
||||
|
} |
||||
|
try { |
||||
|
// 执行逻辑
|
||||
|
return methodInvocation.proceed(); |
||||
|
} finally { |
||||
|
// 出栈
|
||||
|
if (dataPermission != null) { |
||||
|
DataPermissionContextHolder.remove(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private DataPermission findAnnotation(MethodInvocation methodInvocation) { |
||||
|
// 1. 从缓存中获取
|
||||
|
Method method = methodInvocation.getMethod(); |
||||
|
Object targetObject = methodInvocation.getThis(); |
||||
|
Class<?> clazz = targetObject != null ? targetObject.getClass() : method.getDeclaringClass(); |
||||
|
MethodClassKey methodClassKey = new MethodClassKey(method, clazz); |
||||
|
DataPermission dataPermission = dataPermissionCache.get(methodClassKey); |
||||
|
if (dataPermission != null) { |
||||
|
return dataPermission != DATA_PERMISSION_NULL ? dataPermission : null; |
||||
|
} |
||||
|
|
||||
|
// 2.1 从方法中获取
|
||||
|
dataPermission = AnnotationUtils.findAnnotation(method, DataPermission.class); |
||||
|
// 2.2 从类上获取
|
||||
|
if (dataPermission == null) { |
||||
|
dataPermission = AnnotationUtils.findAnnotation(clazz, DataPermission.class); |
||||
|
} |
||||
|
// 2.3 添加到缓存中
|
||||
|
dataPermissionCache.put(methodClassKey, dataPermission != null ? dataPermission : DATA_PERMISSION_NULL); |
||||
|
return dataPermission; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,72 @@ |
|||||
|
package com.qiantoon.platform.framework.datapermission.core.aop; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.datapermission.core.annotation.DataPermission; |
||||
|
import com.alibaba.ttl.TransmittableThreadLocal; |
||||
|
|
||||
|
import java.util.LinkedList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* {@link DataPermission} 注解的 Context 上下文 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class DataPermissionContextHolder { |
||||
|
|
||||
|
/** |
||||
|
* 使用 List 的原因,可能存在方法的嵌套调用 |
||||
|
*/ |
||||
|
private static final ThreadLocal<LinkedList<DataPermission>> DATA_PERMISSIONS = |
||||
|
TransmittableThreadLocal.withInitial(LinkedList::new); |
||||
|
|
||||
|
/** |
||||
|
* 获得当前的 DataPermission 注解 |
||||
|
* |
||||
|
* @return DataPermission 注解 |
||||
|
*/ |
||||
|
public static DataPermission get() { |
||||
|
return DATA_PERMISSIONS.get().peekLast(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 入栈 DataPermission 注解 |
||||
|
* |
||||
|
* @param dataPermission DataPermission 注解 |
||||
|
*/ |
||||
|
public static void add(DataPermission dataPermission) { |
||||
|
DATA_PERMISSIONS.get().addLast(dataPermission); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 出栈 DataPermission 注解 |
||||
|
* |
||||
|
* @return DataPermission 注解 |
||||
|
*/ |
||||
|
public static DataPermission remove() { |
||||
|
DataPermission dataPermission = DATA_PERMISSIONS.get().removeLast(); |
||||
|
// 无元素时,清空 ThreadLocal
|
||||
|
if (DATA_PERMISSIONS.get().isEmpty()) { |
||||
|
DATA_PERMISSIONS.remove(); |
||||
|
} |
||||
|
return dataPermission; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获得所有 DataPermission |
||||
|
* |
||||
|
* @return DataPermission 队列 |
||||
|
*/ |
||||
|
public static List<DataPermission> getAll() { |
||||
|
return DATA_PERMISSIONS.get(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 清空上下文 |
||||
|
* |
||||
|
* 目前仅仅用于单测 |
||||
|
*/ |
||||
|
public static void clear() { |
||||
|
DATA_PERMISSIONS.remove(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,641 @@ |
|||||
|
package com.qiantoon.platform.framework.datapermission.core.db; |
||||
|
|
||||
|
import cn.hutool.core.collection.CollUtil; |
||||
|
import com.qiantoon.platform.framework.common.util.collection.SetUtils; |
||||
|
import com.qiantoon.platform.framework.datapermission.core.rule.DataPermissionRule; |
||||
|
import com.qiantoon.platform.framework.datapermission.core.rule.DataPermissionRuleFactory; |
||||
|
import com.qiantoon.platform.framework.mybatis.core.util.MyBatisUtils; |
||||
|
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.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, List)} 方法 |
||||
|
* |
||||
|
* 整体的代码实现上,参考 {@link com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor} 实现。 |
||||
|
* 所以每次 MyBatis Plus 升级时,需要 Review 下其具体的实现是否有变更! |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
@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 场景(因为 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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 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())); |
||||
|
} |
||||
|
|
||||
|
// ========== 和 TenantLineInnerInterceptor 一致的逻辑 ==========
|
||||
|
|
||||
|
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> selectBodyList = operationList.getSelects(); |
||||
|
if (CollectionUtils.isNotEmpty(selectBodyList)) { |
||||
|
selectBodyList.forEach(this::processSelectBody); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理 PlainSelect |
||||
|
*/ |
||||
|
protected void processPlainSelect(PlainSelect plainSelect) { |
||||
|
//#3087 github
|
||||
|
List<SelectItem> selectItems = plainSelect.getSelectItems(); |
||||
|
if (CollectionUtils.isNotEmpty(selectItems)) { |
||||
|
selectItems.forEach(this::processSelectItem); |
||||
|
} |
||||
|
|
||||
|
// 处理 where 中的子查询
|
||||
|
Expression where = plainSelect.getWhere(); |
||||
|
processWhereSubSelect(where); |
||||
|
|
||||
|
// 处理 fromItem
|
||||
|
FromItem fromItem = plainSelect.getFromItem(); |
||||
|
List<Table> list = processFromItem(fromItem); |
||||
|
List<Table> mainTables = new ArrayList<>(list); |
||||
|
|
||||
|
// 处理 join
|
||||
|
List<Join> joins = plainSelect.getJoins(); |
||||
|
if (CollectionUtils.isNotEmpty(joins)) { |
||||
|
mainTables = processJoins(mainTables, joins); |
||||
|
} |
||||
|
|
||||
|
// 当有 mainTable 时,进行 where 条件追加
|
||||
|
if (CollectionUtils.isNotEmpty(mainTables)) { |
||||
|
plainSelect.setWhere(builderExpression(where, mainTables)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private List<Table> processFromItem(FromItem fromItem) { |
||||
|
// 处理括号括起来的表达式
|
||||
|
while (fromItem instanceof ParenthesisFromItem) { |
||||
|
fromItem = ((ParenthesisFromItem) fromItem).getFromItem(); |
||||
|
} |
||||
|
|
||||
|
List<Table> mainTables = new ArrayList<>(); |
||||
|
// 无 join 时的处理逻辑
|
||||
|
if (fromItem instanceof Table) { |
||||
|
Table fromTable = (Table) fromItem; |
||||
|
mainTables.add(fromTable); |
||||
|
} else if (fromItem instanceof SubJoin) { |
||||
|
// SubJoin 类型则还需要添加上 where 条件
|
||||
|
List<Table> tables = processSubJoin((SubJoin) fromItem); |
||||
|
mainTables.addAll(tables); |
||||
|
} else { |
||||
|
// 处理下 fromItem
|
||||
|
processOtherFromItem(fromItem); |
||||
|
} |
||||
|
return mainTables; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理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) { |
||||
|
processOtherFromItem((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; |
||||
|
Expression inExpression = expression.getRightExpression(); |
||||
|
if (inExpression instanceof SubSelect) { |
||||
|
processSelectBody(((SubSelect) inExpression).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 processOtherFromItem(FromItem fromItem) { |
||||
|
// 去除括号
|
||||
|
while (fromItem instanceof ParenthesisFromItem) { |
||||
|
fromItem = ((ParenthesisFromItem) fromItem).getFromItem(); |
||||
|
} |
||||
|
|
||||
|
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()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理 sub join |
||||
|
* |
||||
|
* @param subJoin subJoin |
||||
|
* @return Table subJoin 中的主表 |
||||
|
*/ |
||||
|
private List<Table> processSubJoin(SubJoin subJoin) { |
||||
|
List<Table> mainTables = new ArrayList<>(); |
||||
|
if (subJoin.getJoinList() != null) { |
||||
|
List<Table> list = processFromItem(subJoin.getLeft()); |
||||
|
mainTables.addAll(list); |
||||
|
mainTables = processJoins(mainTables, subJoin.getJoinList()); |
||||
|
} |
||||
|
return mainTables; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理 joins |
||||
|
* |
||||
|
* @param mainTables 可以为 null |
||||
|
* @param joins join 集合 |
||||
|
* @return List<Table> 右连接查询的 Table 列表 |
||||
|
*/ |
||||
|
private List<Table> processJoins(List<Table> mainTables, List<Join> joins) { |
||||
|
// join 表达式中最终的主表
|
||||
|
Table mainTable = null; |
||||
|
// 当前 join 的左表
|
||||
|
Table leftTable = null; |
||||
|
|
||||
|
if (mainTables == null) { |
||||
|
mainTables = new ArrayList<>(); |
||||
|
} else if (mainTables.size() == 1) { |
||||
|
mainTable = mainTables.get(0); |
||||
|
leftTable = mainTable; |
||||
|
} |
||||
|
|
||||
|
//对于 on 表达式写在最后的 join,需要记录下前面多个 on 的表名
|
||||
|
Deque<List<Table>> onTableDeque = new LinkedList<>(); |
||||
|
for (Join join : joins) { |
||||
|
// 处理 on 表达式
|
||||
|
FromItem joinItem = join.getRightItem(); |
||||
|
|
||||
|
// 获取当前 join 的表,subJoint 可以看作是一张表
|
||||
|
List<Table> joinTables = null; |
||||
|
if (joinItem instanceof Table) { |
||||
|
joinTables = new ArrayList<>(); |
||||
|
joinTables.add((Table) joinItem); |
||||
|
} else if (joinItem instanceof SubJoin) { |
||||
|
joinTables = processSubJoin((SubJoin) joinItem); |
||||
|
} |
||||
|
|
||||
|
if (joinTables != null) { |
||||
|
|
||||
|
// 如果是隐式内连接
|
||||
|
if (join.isSimple()) { |
||||
|
mainTables.addAll(joinTables); |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// 当前表是否忽略
|
||||
|
Table joinTable = joinTables.get(0); |
||||
|
|
||||
|
List<Table> onTables = null; |
||||
|
// 如果不要忽略,且是右连接,则记录下当前表
|
||||
|
if (join.isRight()) { |
||||
|
mainTable = joinTable; |
||||
|
if (leftTable != null) { |
||||
|
onTables = Collections.singletonList(leftTable); |
||||
|
} |
||||
|
} else if (join.isLeft()) { |
||||
|
onTables = Collections.singletonList(joinTable); |
||||
|
} else if (join.isInner()) { |
||||
|
if (mainTable == null) { |
||||
|
onTables = Collections.singletonList(joinTable); |
||||
|
} else { |
||||
|
onTables = Arrays.asList(mainTable, joinTable); |
||||
|
} |
||||
|
mainTable = null; |
||||
|
} |
||||
|
|
||||
|
mainTables = new ArrayList<>(); |
||||
|
if (mainTable != null) { |
||||
|
mainTables.add(mainTable); |
||||
|
} |
||||
|
|
||||
|
// 获取 join 尾缀的 on 表达式列表
|
||||
|
Collection<Expression> originOnExpressions = join.getOnExpressions(); |
||||
|
// 正常 join on 表达式只有一个,立刻处理
|
||||
|
if (originOnExpressions.size() == 1 && onTables != null) { |
||||
|
List<Expression> onExpressions = new LinkedList<>(); |
||||
|
onExpressions.add(builderExpression(originOnExpressions.iterator().next(), onTables)); |
||||
|
join.setOnExpressions(onExpressions); |
||||
|
leftTable = joinTable; |
||||
|
continue; |
||||
|
} |
||||
|
// 表名压栈,忽略的表压入 null,以便后续不处理
|
||||
|
onTableDeque.push(onTables); |
||||
|
// 尾缀多个 on 表达式的时候统一处理
|
||||
|
if (originOnExpressions.size() > 1) { |
||||
|
Collection<Expression> onExpressions = new LinkedList<>(); |
||||
|
for (Expression originOnExpression : originOnExpressions) { |
||||
|
List<Table> currentTableList = onTableDeque.poll(); |
||||
|
if (CollectionUtils.isEmpty(currentTableList)) { |
||||
|
onExpressions.add(originOnExpression); |
||||
|
} else { |
||||
|
onExpressions.add(builderExpression(originOnExpression, currentTableList)); |
||||
|
} |
||||
|
} |
||||
|
join.setOnExpressions(onExpressions); |
||||
|
} |
||||
|
leftTable = joinTable; |
||||
|
} else { |
||||
|
processOtherFromItem(joinItem); |
||||
|
leftTable = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return mainTables; |
||||
|
} |
||||
|
|
||||
|
// ========== 和 TenantLineInnerInterceptor 存在差异的逻辑:关键,实现权限条件的拼接 ==========
|
||||
|
|
||||
|
/** |
||||
|
* 处理条件 |
||||
|
* |
||||
|
* @param currentExpression 当前 where 条件 |
||||
|
* @param table 单个表 |
||||
|
*/ |
||||
|
protected Expression builderExpression(Expression currentExpression, Table table) { |
||||
|
return this.builderExpression(currentExpression, Collections.singletonList(table)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理条件 |
||||
|
* |
||||
|
* @param currentExpression 当前 where 条件 |
||||
|
* @param tables 多个表 |
||||
|
*/ |
||||
|
protected Expression builderExpression(Expression currentExpression, List<Table> tables) { |
||||
|
// 没有表需要处理直接返回
|
||||
|
if (CollectionUtils.isEmpty(tables)) { |
||||
|
return currentExpression; |
||||
|
} |
||||
|
|
||||
|
// 第一步,获得 Table 对应的数据权限条件
|
||||
|
Expression dataPermissionExpression = null; |
||||
|
for (Table table : tables) { |
||||
|
// 构建每个表的权限 Expression 条件
|
||||
|
Expression expression = buildDataPermissionExpression(table); |
||||
|
if (expression == null) { |
||||
|
continue; |
||||
|
} |
||||
|
// 合并到 dataPermissionExpression 中
|
||||
|
dataPermissionExpression = dataPermissionExpression == null ? expression |
||||
|
: new AndExpression(dataPermissionExpression, expression); |
||||
|
} |
||||
|
|
||||
|
// 第二步,合并多个 Expression 条件
|
||||
|
if (dataPermissionExpression == null) { |
||||
|
return currentExpression; |
||||
|
} |
||||
|
if (currentExpression == null) { |
||||
|
return dataPermissionExpression; |
||||
|
} |
||||
|
// ① 如果表达式为 Or,则需要 (currentExpression) AND dataPermissionExpression
|
||||
|
if (currentExpression instanceof OrExpression) { |
||||
|
return new AndExpression(new Parenthesis(currentExpression), dataPermissionExpression); |
||||
|
} |
||||
|
// ② 如果表达式为 And,则直接返回 where AND dataPermissionExpression
|
||||
|
return new AndExpression(currentExpression, dataPermissionExpression); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 构建指定表的数据权限的 Expression 过滤条件 |
||||
|
* |
||||
|
* @param table 表 |
||||
|
* @return Expression 过滤条件 |
||||
|
*/ |
||||
|
private Expression buildDataPermissionExpression(Table table) { |
||||
|
// 生成条件
|
||||
|
Expression allExpression = null; |
||||
|
for (DataPermissionRule rule : ContextHolder.getRules()) { |
||||
|
// 判断表名是否匹配
|
||||
|
String tableName = MyBatisUtils.getTableName(table); |
||||
|
if (!rule.getTableNames().contains(tableName)) { |
||||
|
continue; |
||||
|
} |
||||
|
// 如果有匹配的规则,说明可重写。
|
||||
|
// 为什么不是有 allExpression 非空才重写呢?在生成 column = value 过滤条件时,会因为 value 不存在,导致未重写。
|
||||
|
// 这样导致第一次无 value,被标记成无需重写;但是第二次有 value,此时会需要重写。
|
||||
|
ContextHolder.setRewrite(true); |
||||
|
|
||||
|
// 单条规则的条件
|
||||
|
Expression oneExpress = rule.getExpression(tableName, table.getAlias()); |
||||
|
if (oneExpress == null){ |
||||
|
continue; |
||||
|
} |
||||
|
// 拼接到 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 qt |
||||
|
*/ |
||||
|
static final class ContextHolder { |
||||
|
|
||||
|
/** |
||||
|
* 该 {@link MappedStatement} 对应的规则 |
||||
|
*/ |
||||
|
private static final ThreadLocal<List<DataPermissionRule>> RULES = ThreadLocal.withInitial(Collections::emptyList); |
||||
|
/** |
||||
|
* SQL 是否进行重写 |
||||
|
*/ |
||||
|
private static final ThreadLocal<Boolean> REWRITE = ThreadLocal.withInitial(() -> Boolean.FALSE); |
||||
|
|
||||
|
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 qt |
||||
|
*/ |
||||
|
static final class MappedStatementCache { |
||||
|
|
||||
|
/** |
||||
|
* 指定数据权限规则,对指定 MappedStatement 无需重写(不生效)的缓存 |
||||
|
* |
||||
|
* value:{@link MappedStatement#getId()} 编号 |
||||
|
*/ |
||||
|
@Getter |
||||
|
private final Map<Class<? extends DataPermissionRule>, Set<String>> noRewritableMappedStatements = new ConcurrentHashMap<>(); |
||||
|
|
||||
|
/** |
||||
|
* 判断是否无需重写 |
||||
|
* ps:虽然有点中文式英语,但是容易读懂即可 |
||||
|
* |
||||
|
* @param ms MappedStatement |
||||
|
* @param rules 数据权限规则数组 |
||||
|
* @return 是否无需重写 |
||||
|
*/ |
||||
|
public boolean noRewritable(MappedStatement ms, List<DataPermissionRule> rules) { |
||||
|
// 如果规则为空,说明无需重写
|
||||
|
if (CollUtil.isEmpty(rules)) { |
||||
|
return true; |
||||
|
} |
||||
|
// 任一规则不在 noRewritableMap 中,则说明可能需要重写
|
||||
|
for (DataPermissionRule rule : rules) { |
||||
|
Set<String> mappedStatementIds = noRewritableMappedStatements.get(rule.getClass()); |
||||
|
if (!CollUtil.contains(mappedStatementIds, ms.getId())) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 添加无需重写的 MappedStatement |
||||
|
* |
||||
|
* @param ms MappedStatement |
||||
|
* @param rules 数据权限规则数组 |
||||
|
*/ |
||||
|
public void addNoRewritable(MappedStatement ms, List<DataPermissionRule> rules) { |
||||
|
for (DataPermissionRule rule : rules) { |
||||
|
Set<String> mappedStatementIds = noRewritableMappedStatements.get(rule.getClass()); |
||||
|
if (CollUtil.isNotEmpty(mappedStatementIds)) { |
||||
|
mappedStatementIds.add(ms.getId()); |
||||
|
} else { |
||||
|
noRewritableMappedStatements.put(rule.getClass(), SetUtils.asSet(ms.getId())); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 清空缓存 |
||||
|
* 目前主要提供给单元测试 |
||||
|
*/ |
||||
|
public void clear() { |
||||
|
noRewritableMappedStatements.clear(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
package com.qiantoon.platform.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 qt |
||||
|
*/ |
||||
|
public interface DataPermissionRule { |
||||
|
|
||||
|
/** |
||||
|
* 返回需要生效的表名数组 |
||||
|
* 为什么需要该方法?Data Permission 数组基于 SQL 重写,通过 Where 返回只有权限的数据 |
||||
|
* |
||||
|
* 如果需要基于实体名获得表名,可调用 {@link TableInfoHelper#getTableInfo(Class)} 获得 |
||||
|
* |
||||
|
* @return 表名数组 |
||||
|
*/ |
||||
|
Set<String> getTableNames(); |
||||
|
|
||||
|
/** |
||||
|
* 根据表名和别名,生成对应的 WHERE / OR 过滤条件 |
||||
|
* |
||||
|
* @param tableName 表名 |
||||
|
* @param tableAlias 别名,可能为空 |
||||
|
* @return 过滤条件 Expression 表达式 |
||||
|
*/ |
||||
|
Expression getExpression(String tableName, Alias tableAlias); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
package com.qiantoon.platform.framework.datapermission.core.rule; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* {@link DataPermissionRule} 工厂接口 |
||||
|
* 作为 {@link DataPermissionRule} 的容器,提供管理能力 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public interface DataPermissionRuleFactory { |
||||
|
|
||||
|
/** |
||||
|
* 获得所有数据权限规则数组 |
||||
|
* |
||||
|
* @return 数据权限规则数组 |
||||
|
*/ |
||||
|
List<DataPermissionRule> getDataPermissionRules(); |
||||
|
|
||||
|
/** |
||||
|
* 获得指定 Mapper 的数据权限规则数组 |
||||
|
* |
||||
|
* @param mappedStatementId 指定 Mapper 的编号 |
||||
|
* @return 数据权限规则数组 |
||||
|
*/ |
||||
|
List<DataPermissionRule> getDataPermissionRule(String mappedStatementId); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,62 @@ |
|||||
|
package com.qiantoon.platform.framework.datapermission.core.rule; |
||||
|
|
||||
|
import cn.hutool.core.collection.CollUtil; |
||||
|
import cn.hutool.core.util.ArrayUtil; |
||||
|
import com.qiantoon.platform.framework.datapermission.core.annotation.DataPermission; |
||||
|
import com.qiantoon.platform.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 qt |
||||
|
*/ |
||||
|
@RequiredArgsConstructor |
||||
|
public class DataPermissionRuleFactoryImpl implements DataPermissionRuleFactory { |
||||
|
|
||||
|
/** |
||||
|
* 数据权限规则数组 |
||||
|
*/ |
||||
|
private final List<DataPermissionRule> rules; |
||||
|
|
||||
|
@Override |
||||
|
public List<DataPermissionRule> getDataPermissionRules() { |
||||
|
return rules; |
||||
|
} |
||||
|
|
||||
|
@Override // mappedStatementId 参数,暂时没有用。以后,可以基于 mappedStatementId + DataPermission 进行缓存
|
||||
|
public List<DataPermissionRule> getDataPermissionRule(String mappedStatementId) { |
||||
|
// 1. 无数据权限
|
||||
|
if (CollUtil.isEmpty(rules)) { |
||||
|
return Collections.emptyList(); |
||||
|
} |
||||
|
// 2. 未配置,则默认开启
|
||||
|
DataPermission dataPermission = DataPermissionContextHolder.get(); |
||||
|
if (dataPermission == null) { |
||||
|
return rules; |
||||
|
} |
||||
|
// 3. 已配置,但禁用
|
||||
|
if (!dataPermission.enable()) { |
||||
|
return Collections.emptyList(); |
||||
|
} |
||||
|
|
||||
|
// 4. 已配置,只选择部分规则
|
||||
|
if (ArrayUtil.isNotEmpty(dataPermission.includeRules())) { |
||||
|
return rules.stream().filter(rule -> ArrayUtil.contains(dataPermission.includeRules(), rule.getClass())) |
||||
|
.collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询
|
||||
|
} |
||||
|
// 5. 已配置,只排除部分规则
|
||||
|
if (ArrayUtil.isNotEmpty(dataPermission.excludeRules())) { |
||||
|
return rules.stream().filter(rule -> !ArrayUtil.contains(dataPermission.excludeRules(), rule.getClass())) |
||||
|
.collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询
|
||||
|
} |
||||
|
// 6. 已配置,全部规则
|
||||
|
return rules; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,205 @@ |
|||||
|
package com.qiantoon.platform.framework.datapermission.core.rule.dept; |
||||
|
|
||||
|
import cn.hutool.core.collection.CollUtil; |
||||
|
import cn.hutool.core.util.ObjectUtil; |
||||
|
import cn.hutool.core.util.StrUtil; |
||||
|
import com.qiantoon.platform.framework.common.enums.UserTypeEnum; |
||||
|
import com.qiantoon.platform.framework.common.util.collection.CollectionUtils; |
||||
|
import com.qiantoon.platform.framework.common.util.json.JsonUtils; |
||||
|
import com.qiantoon.platform.framework.datapermission.core.rule.DataPermissionRule; |
||||
|
import com.qiantoon.platform.framework.mybatis.core.dataobject.BaseDO; |
||||
|
import com.qiantoon.platform.framework.mybatis.core.util.MyBatisUtils; |
||||
|
import com.qiantoon.platform.framework.security.core.LoginUser; |
||||
|
import com.qiantoon.platform.framework.security.core.util.SecurityFrameworkUtils; |
||||
|
import com.qiantoon.platform.module.system.api.permission.PermissionApi; |
||||
|
import com.qiantoon.platform.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.*; |
||||
|
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 不进行修改,则会导致用户看不到之前的数据。【platform-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 qt |
||||
|
*/ |
||||
|
@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 qt:获得不到条件的时候,暂时不抛出异常,而是不返回数据
|
||||
|
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 Parenthesis(new OrExpression(deptExpression, userExpression)); |
||||
|
} |
||||
|
|
||||
|
private Expression buildDeptExpression(String tableName, Alias tableAlias, Set<Long> deptIds) { |
||||
|
// 如果不存在配置,则无需作为条件
|
||||
|
String columnName = deptColumns.get(tableName); |
||||
|
if (StrUtil.isEmpty(columnName)) { |
||||
|
return null; |
||||
|
} |
||||
|
// 如果为空,则无条件
|
||||
|
if (CollUtil.isEmpty(deptIds)) { |
||||
|
return null; |
||||
|
} |
||||
|
// 拼接条件
|
||||
|
return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), |
||||
|
new ExpressionList(CollectionUtils.convertList(deptIds, LongValue::new))); |
||||
|
} |
||||
|
|
||||
|
private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) { |
||||
|
// 如果不查看自己,则无需作为条件
|
||||
|
if (Boolean.FALSE.equals(self)) { |
||||
|
return null; |
||||
|
} |
||||
|
String columnName = userColumns.get(tableName); |
||||
|
if (StrUtil.isEmpty(columnName)) { |
||||
|
return null; |
||||
|
} |
||||
|
// 拼接条件
|
||||
|
return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId)); |
||||
|
} |
||||
|
|
||||
|
// ==================== 添加配置 ====================
|
||||
|
|
||||
|
public void addDeptColumn(Class<? extends BaseDO> entityClass) { |
||||
|
addDeptColumn(entityClass, DEPT_COLUMN_NAME); |
||||
|
} |
||||
|
|
||||
|
public void addDeptColumn(Class<? extends BaseDO> entityClass, String columnName) { |
||||
|
String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); |
||||
|
addDeptColumn(tableName, columnName); |
||||
|
} |
||||
|
|
||||
|
public void addDeptColumn(String tableName, String columnName) { |
||||
|
deptColumns.put(tableName, columnName); |
||||
|
TABLE_NAMES.add(tableName); |
||||
|
} |
||||
|
|
||||
|
public void addUserColumn(Class<? extends BaseDO> entityClass) { |
||||
|
addUserColumn(entityClass, USER_COLUMN_NAME); |
||||
|
} |
||||
|
|
||||
|
public void addUserColumn(Class<? extends BaseDO> entityClass, String columnName) { |
||||
|
String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); |
||||
|
addUserColumn(tableName, columnName); |
||||
|
} |
||||
|
|
||||
|
public void addUserColumn(String tableName, String columnName) { |
||||
|
userColumns.put(tableName, columnName); |
||||
|
TABLE_NAMES.add(tableName); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
package com.qiantoon.platform.framework.datapermission.core.rule.dept; |
||||
|
|
||||
|
/** |
||||
|
* {@link DeptDataPermissionRule} 的自定义配置接口 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
@FunctionalInterface |
||||
|
public interface DeptDataPermissionRuleCustomizer { |
||||
|
|
||||
|
/** |
||||
|
* 自定义该权限规则 |
||||
|
* 1. 调用 {@link DeptDataPermissionRule#addDeptColumn(Class, String)} 方法,配置基于 dept_id 的过滤规则 |
||||
|
* 2. 调用 {@link DeptDataPermissionRule#addUserColumn(Class, String)} 方法,配置基于 user_id 的过滤规则 |
||||
|
* |
||||
|
* @param rule 权限规则 |
||||
|
*/ |
||||
|
void customize(DeptDataPermissionRule rule); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
/** |
||||
|
* 基于部门的数据权限规则 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
package com.qiantoon.platform.framework.datapermission.core.rule.dept; |
||||
@ -0,0 +1,63 @@ |
|||||
|
package com.qiantoon.platform.framework.datapermission.core.util; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.datapermission.core.annotation.DataPermission; |
||||
|
import com.qiantoon.platform.framework.datapermission.core.aop.DataPermissionContextHolder; |
||||
|
import lombok.SneakyThrows; |
||||
|
|
||||
|
import java.util.concurrent.Callable; |
||||
|
|
||||
|
/** |
||||
|
* 数据权限 Util |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class DataPermissionUtils { |
||||
|
|
||||
|
private static DataPermission DATA_PERMISSION_DISABLE; |
||||
|
|
||||
|
@DataPermission(enable = false) |
||||
|
@SneakyThrows |
||||
|
private static DataPermission getDisableDataPermissionDisable() { |
||||
|
if (DATA_PERMISSION_DISABLE == null) { |
||||
|
DATA_PERMISSION_DISABLE = DataPermissionUtils.class |
||||
|
.getDeclaredMethod("getDisableDataPermissionDisable") |
||||
|
.getAnnotation(DataPermission.class); |
||||
|
} |
||||
|
return DATA_PERMISSION_DISABLE; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 忽略数据权限,执行对应的逻辑 |
||||
|
* |
||||
|
* @param runnable 逻辑 |
||||
|
*/ |
||||
|
public static void executeIgnore(Runnable runnable) { |
||||
|
DataPermission dataPermission = getDisableDataPermissionDisable(); |
||||
|
DataPermissionContextHolder.add(dataPermission); |
||||
|
try { |
||||
|
// 执行 runnable
|
||||
|
runnable.run(); |
||||
|
} finally { |
||||
|
DataPermissionContextHolder.remove(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 忽略数据权限,执行对应的逻辑 |
||||
|
* |
||||
|
* @param callable 逻辑 |
||||
|
* @return 执行结果 |
||||
|
*/ |
||||
|
@SneakyThrows |
||||
|
public static <T> T executeIgnore(Callable<T> callable) { |
||||
|
DataPermission dataPermission = getDisableDataPermissionDisable(); |
||||
|
DataPermissionContextHolder.add(dataPermission); |
||||
|
try { |
||||
|
// 执行 callable
|
||||
|
return callable.call(); |
||||
|
} finally { |
||||
|
DataPermissionContextHolder.remove(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
/** |
||||
|
* 基于 JSqlParser 解析 SQL,增加数据权限的 WHERE 条件 |
||||
|
*/ |
||||
|
package com.qiantoon.platform.framework.datapermission; |
||||
@ -0,0 +1,2 @@ |
|||||
|
com.qiantoon.platform.framework.datapermission.config.PlatformDataPermissionAutoConfiguration |
||||
|
com.qiantoon.platform.framework.datapermission.config.PlatformDeptDataPermissionAutoConfiguration |
||||
@ -0,0 +1,47 @@ |
|||||
|
<?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>platform-framework</artifactId> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<version>${revision}</version> |
||||
|
</parent> |
||||
|
<modelVersion>4.0.0</modelVersion> |
||||
|
<artifactId>platform-spring-boot-starter-biz-ip</artifactId> |
||||
|
<packaging>jar</packaging> |
||||
|
|
||||
|
<name>${project.artifactId}</name> |
||||
|
<description>IP 拓展,支持如下功能: |
||||
|
1. IP 功能:查询 IP 对应的城市信息 |
||||
|
基于 https://gitee.com/lionsoul/ip2region 实现 |
||||
|
2. 城市功能:查询城市编码对应的城市信息 |
||||
|
基于 https://github.com/modood/Administrative-divisions-of-China 实现 |
||||
|
</description> |
||||
|
|
||||
|
<dependencies> |
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-common</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- IP地址检索 --> |
||||
|
<dependency> |
||||
|
<groupId>org.lionsoul</groupId> |
||||
|
<artifactId>ip2region</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>org.projectlombok</groupId> |
||||
|
<artifactId>lombok</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>org.slf4j</groupId> |
||||
|
<artifactId>slf4j-api</artifactId> |
||||
|
<scope>provided</scope> <!-- 设置为 provided,只有工具类需要使用到 --> |
||||
|
</dependency> |
||||
|
|
||||
|
</dependencies> |
||||
|
|
||||
|
</project> |
||||
@ -0,0 +1,55 @@ |
|||||
|
package com.qiantoon.platform.framework.ip.core; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.ip.core.enums.AreaTypeEnum; |
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Data; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
/** |
||||
|
* 区域节点,包括国家、省份、城市、地区等信息 |
||||
|
* |
||||
|
* 数据可见 resources/area.csv 文件 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
@NoArgsConstructor |
||||
|
public class Area { |
||||
|
|
||||
|
/** |
||||
|
* 编号 - 全球,即根目录 |
||||
|
*/ |
||||
|
public static final Integer ID_GLOBAL = 0; |
||||
|
/** |
||||
|
* 编号 - 中国 |
||||
|
*/ |
||||
|
public static final Integer ID_CHINA = 1; |
||||
|
|
||||
|
/** |
||||
|
* 编号 |
||||
|
*/ |
||||
|
private Integer id; |
||||
|
/** |
||||
|
* 名字 |
||||
|
*/ |
||||
|
private String name; |
||||
|
/** |
||||
|
* 类型 |
||||
|
* |
||||
|
* 枚举 {@link AreaTypeEnum} |
||||
|
*/ |
||||
|
private Integer type; |
||||
|
|
||||
|
/** |
||||
|
* 父节点 |
||||
|
*/ |
||||
|
private Area parent; |
||||
|
/** |
||||
|
* 子节点 |
||||
|
*/ |
||||
|
private List<Area> children; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
package com.qiantoon.platform.framework.ip.core.enums; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.common.core.IntArrayValuable; |
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Getter; |
||||
|
|
||||
|
import java.util.Arrays; |
||||
|
|
||||
|
/** |
||||
|
* 区域类型枚举 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
@AllArgsConstructor |
||||
|
@Getter |
||||
|
public enum AreaTypeEnum implements IntArrayValuable { |
||||
|
|
||||
|
COUNTRY(1, "国家"), |
||||
|
PROVINCE(2, "省份"), |
||||
|
CITY(3, "城市"), |
||||
|
DISTRICT(4, "地区"), // 县、镇、区等
|
||||
|
; |
||||
|
|
||||
|
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AreaTypeEnum::getType).toArray(); |
||||
|
|
||||
|
/** |
||||
|
* 类型 |
||||
|
*/ |
||||
|
private final Integer type; |
||||
|
/** |
||||
|
* 名字 |
||||
|
*/ |
||||
|
private final String name; |
||||
|
|
||||
|
@Override |
||||
|
public int[] array() { |
||||
|
return ARRAYS; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,214 @@ |
|||||
|
package com.qiantoon.platform.framework.ip.core.utils; |
||||
|
|
||||
|
import cn.hutool.core.io.resource.ResourceUtil; |
||||
|
import cn.hutool.core.lang.Assert; |
||||
|
import cn.hutool.core.text.csv.CsvRow; |
||||
|
import cn.hutool.core.text.csv.CsvUtil; |
||||
|
import com.qiantoon.platform.framework.common.util.object.ObjectUtils; |
||||
|
import com.qiantoon.platform.framework.ip.core.Area; |
||||
|
import com.qiantoon.platform.framework.ip.core.enums.AreaTypeEnum; |
||||
|
import lombok.NonNull; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.function.Function; |
||||
|
|
||||
|
import static com.qiantoon.platform.framework.common.util.collection.CollectionUtils.convertList; |
||||
|
import static com.qiantoon.platform.framework.common.util.collection.CollectionUtils.findFirst; |
||||
|
|
||||
|
/** |
||||
|
* 区域工具类 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
public class AreaUtils { |
||||
|
|
||||
|
/** |
||||
|
* 初始化 SEARCHER |
||||
|
*/ |
||||
|
@SuppressWarnings("InstantiationOfUtilityClass") |
||||
|
private final static AreaUtils INSTANCE = new AreaUtils(); |
||||
|
|
||||
|
/** |
||||
|
* Area 内存缓存,提升访问速度 |
||||
|
*/ |
||||
|
private static Map<Integer, Area> areas; |
||||
|
|
||||
|
private AreaUtils() { |
||||
|
long now = System.currentTimeMillis(); |
||||
|
areas = new HashMap<>(); |
||||
|
areas.put(Area.ID_GLOBAL, new Area(Area.ID_GLOBAL, "全球", 0, |
||||
|
null, new ArrayList<>())); |
||||
|
// 从 csv 中加载数据
|
||||
|
List<CsvRow> rows = CsvUtil.getReader().read(ResourceUtil.getUtf8Reader("area.csv")).getRows(); |
||||
|
rows.remove(0); // 删除 header
|
||||
|
for (CsvRow row : rows) { |
||||
|
// 创建 Area 对象
|
||||
|
Area area = new Area(Integer.valueOf(row.get(0)), row.get(1), Integer.valueOf(row.get(2)), |
||||
|
null, new ArrayList<>()); |
||||
|
// 添加到 areas 中
|
||||
|
areas.put(area.getId(), area); |
||||
|
} |
||||
|
|
||||
|
// 构建父子关系:因为 Area 中没有 parentId 字段,所以需要重复读取
|
||||
|
for (CsvRow row : rows) { |
||||
|
Area area = areas.get(Integer.valueOf(row.get(0))); // 自己
|
||||
|
Area parent = areas.get(Integer.valueOf(row.get(3))); // 父
|
||||
|
Assert.isTrue(area != parent, "{}:父子节点相同", area.getName()); |
||||
|
area.setParent(parent); |
||||
|
parent.getChildren().add(area); |
||||
|
} |
||||
|
log.info("启动加载 AreaUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获得指定编号对应的区域 |
||||
|
* |
||||
|
* @param id 区域编号 |
||||
|
* @return 区域 |
||||
|
*/ |
||||
|
public static Area getArea(Integer id) { |
||||
|
return areas.get(id); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获得指定区域对应的编号 |
||||
|
* |
||||
|
* @param pathStr 区域路径,例如说:河南省/石家庄市/新华区 |
||||
|
* @return 区域 |
||||
|
*/ |
||||
|
public static Area parseArea(String pathStr) { |
||||
|
String[] paths = pathStr.split("/"); |
||||
|
Area area = null; |
||||
|
for (String path : paths) { |
||||
|
if (area == null) { |
||||
|
area = findFirst(areas.values(), item -> item.getName().equals(path)); |
||||
|
} else { |
||||
|
area = findFirst(area.getChildren(), item -> item.getName().equals(path)); |
||||
|
} |
||||
|
} |
||||
|
return area; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取所有节点的全路径名称如:河南省/石家庄市/新华区 |
||||
|
* |
||||
|
* @param areas 地区树 |
||||
|
* @return 所有节点的全路径名称 |
||||
|
*/ |
||||
|
public static List<String> getAreaNodePathList(List<Area> areas) { |
||||
|
List<String> paths = new ArrayList<>(); |
||||
|
areas.forEach(area -> getAreaNodePathList(area, "", paths)); |
||||
|
return paths; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 构建一棵树的所有节点的全路径名称,并将其存储为 "祖先/父级/子级" 的形式 |
||||
|
* |
||||
|
* @param node 父节点 |
||||
|
* @param path 全路径名称 |
||||
|
* @param paths 全路径名称列表,省份/城市/地区 |
||||
|
*/ |
||||
|
private static void getAreaNodePathList(Area node, String path, List<String> paths) { |
||||
|
if (node == null) { |
||||
|
return; |
||||
|
} |
||||
|
// 构建当前节点的路径
|
||||
|
String currentPath = path.isEmpty() ? node.getName() : path + "/" + node.getName(); |
||||
|
paths.add(currentPath); |
||||
|
// 递归遍历子节点
|
||||
|
for (Area child : node.getChildren()) { |
||||
|
getAreaNodePathList(child, currentPath, paths); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 格式化区域 |
||||
|
* |
||||
|
* @param id 区域编号 |
||||
|
* @return 格式化后的区域 |
||||
|
*/ |
||||
|
public static String format(Integer id) { |
||||
|
return format(id, " "); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 格式化区域 |
||||
|
* |
||||
|
* 例如说: |
||||
|
* 1. id = “静安区”时:上海 上海市 静安区 |
||||
|
* 2. id = “上海市”时:上海 上海市 |
||||
|
* 3. id = “上海”时:上海 |
||||
|
* 4. id = “美国”时:美国 |
||||
|
* 当区域在中国时,默认不显示中国 |
||||
|
* |
||||
|
* @param id 区域编号 |
||||
|
* @param separator 分隔符 |
||||
|
* @return 格式化后的区域 |
||||
|
*/ |
||||
|
public static String format(Integer id, String separator) { |
||||
|
// 获得区域
|
||||
|
Area area = areas.get(id); |
||||
|
if (area == null) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// 格式化
|
||||
|
StringBuilder sb = new StringBuilder(); |
||||
|
for (int i = 0; i < AreaTypeEnum.values().length; i++) { // 避免死循环
|
||||
|
sb.insert(0, area.getName()); |
||||
|
// “递归”父节点
|
||||
|
area = area.getParent(); |
||||
|
if (area == null |
||||
|
|| ObjectUtils.equalsAny(area.getId(), Area.ID_GLOBAL, Area.ID_CHINA)) { // 跳过父节点为中国的情况
|
||||
|
break; |
||||
|
} |
||||
|
sb.insert(0, separator); |
||||
|
} |
||||
|
return sb.toString(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取指定类型的区域列表 |
||||
|
* |
||||
|
* @param type 区域类型 |
||||
|
* @param func 转换函数 |
||||
|
* @param <T> 结果类型 |
||||
|
* @return 区域列表 |
||||
|
*/ |
||||
|
public static <T> List<T> getByType(AreaTypeEnum type, Function<Area, T> func) { |
||||
|
return convertList(areas.values(), func, area -> type.getType().equals(area.getType())); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据区域编号、上级区域类型,获取上级区域编号 |
||||
|
* |
||||
|
* @param id 区域编号 |
||||
|
* @param type 区域类型 |
||||
|
* @return 上级区域编号 |
||||
|
*/ |
||||
|
public static Integer getParentIdByType(Integer id, @NonNull AreaTypeEnum type) { |
||||
|
for (int i = 0; i < Byte.MAX_VALUE; i++) { |
||||
|
Area area = AreaUtils.getArea(id); |
||||
|
if (area == null) { |
||||
|
return null; |
||||
|
} |
||||
|
// 情况一:匹配到,返回它
|
||||
|
if (type.getType().equals(area.getType())) { |
||||
|
return area.getId(); |
||||
|
} |
||||
|
// 情况二:找到根节点,返回空
|
||||
|
if (area.getParent() == null || area.getParent().getId() == null) { |
||||
|
return null; |
||||
|
} |
||||
|
// 其它:继续向上查找
|
||||
|
id = area.getParent().getId(); |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,87 @@ |
|||||
|
package com.qiantoon.platform.framework.ip.core.utils; |
||||
|
|
||||
|
import cn.hutool.core.io.resource.ResourceUtil; |
||||
|
import com.qiantoon.platform.framework.ip.core.Area; |
||||
|
import lombok.SneakyThrows; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.lionsoul.ip2region.xdb.Searcher; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
|
||||
|
/** |
||||
|
* IP 工具类 |
||||
|
* |
||||
|
* IP 数据源来自 ip2region.xdb 精简版,基于 <a href="https://gitee.com/zhijiantianya/ip2region"/> 项目 |
||||
|
* |
||||
|
* @author wanglhup |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
public class IPUtils { |
||||
|
|
||||
|
/** |
||||
|
* 初始化 SEARCHER |
||||
|
*/ |
||||
|
@SuppressWarnings("InstantiationOfUtilityClass") |
||||
|
private final static IPUtils INSTANCE = new IPUtils(); |
||||
|
|
||||
|
/** |
||||
|
* IP 查询器,启动加载到内存中 |
||||
|
*/ |
||||
|
private static Searcher SEARCHER; |
||||
|
|
||||
|
/** |
||||
|
* 私有化构造 |
||||
|
*/ |
||||
|
private IPUtils() { |
||||
|
try { |
||||
|
long now = System.currentTimeMillis(); |
||||
|
byte[] bytes = ResourceUtil.readBytes("ip2region.xdb"); |
||||
|
SEARCHER = Searcher.newWithBuffer(bytes); |
||||
|
log.info("启动加载 IPUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now); |
||||
|
} catch (IOException e) { |
||||
|
log.error("启动加载 IPUtils 失败", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 查询 IP 对应的地区编号 |
||||
|
* |
||||
|
* @param ip IP 地址,格式为 127.0.0.1 |
||||
|
* @return 地区id |
||||
|
*/ |
||||
|
@SneakyThrows |
||||
|
public static Integer getAreaId(String ip) { |
||||
|
return Integer.parseInt(SEARCHER.search(ip.trim())); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 查询 IP 对应的地区编号 |
||||
|
* |
||||
|
* @param ip IP 地址的时间戳,格式参考{@link Searcher#checkIP(String)} 的返回 |
||||
|
* @return 地区编号 |
||||
|
*/ |
||||
|
@SneakyThrows |
||||
|
public static Integer getAreaId(long ip) { |
||||
|
return Integer.parseInt(SEARCHER.search(ip)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 查询 IP 对应的地区 |
||||
|
* |
||||
|
* @param ip IP 地址,格式为 127.0.0.1 |
||||
|
* @return 地区 |
||||
|
*/ |
||||
|
public static Area getArea(String ip) { |
||||
|
return AreaUtils.getArea(getAreaId(ip)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 查询 IP 对应的地区 |
||||
|
* |
||||
|
* @param ip IP 地址的时间戳,格式参考{@link Searcher#checkIP(String)} 的返回 |
||||
|
* @return 地区 |
||||
|
*/ |
||||
|
public static Area getArea(long ip) { |
||||
|
return AreaUtils.getArea(getAreaId(ip)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
/** |
||||
|
* IP 拓展,支持如下功能: |
||||
|
* |
||||
|
* 1. IP 功能:查询 IP 对应的城市信息 |
||||
|
* 基于 https://gitee.com/lionsoul/ip2region 实现
|
||||
|
* 2. 城市功能:查询城市编码对应的城市信息 |
||||
|
* 基于 https://github.com/modood/Administrative-divisions-of-China 实现
|
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
package com.qiantoon.platform.framework.ip; |
||||
File diff suppressed because it is too large
Binary file not shown.
@ -0,0 +1,75 @@ |
|||||
|
<?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>platform-framework</artifactId> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<version>${revision}</version> |
||||
|
</parent> |
||||
|
<modelVersion>4.0.0</modelVersion> |
||||
|
<artifactId>platform-spring-boot-starter-biz-tenant</artifactId> |
||||
|
<packaging>jar</packaging> |
||||
|
|
||||
|
<name>${project.artifactId}</name> |
||||
|
<description>多租户</description> |
||||
|
|
||||
|
<dependencies> |
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-common</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- Web 相关 --> |
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-spring-boot-starter-security</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- DB 相关 --> |
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-spring-boot-starter-mybatis</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-spring-boot-starter-redis</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- Job 定时任务相关 --> |
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-spring-boot-starter-job</artifactId> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- 消息队列相关 --> |
||||
|
<dependency> |
||||
|
<groupId>com.qiantoon</groupId> |
||||
|
<artifactId>platform-spring-boot-starter-mq</artifactId> |
||||
|
<optional>true</optional> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.springframework.kafka</groupId> |
||||
|
<artifactId>spring-kafka</artifactId> |
||||
|
<optional>true</optional> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.springframework.amqp</groupId> |
||||
|
<artifactId>spring-rabbit</artifactId> |
||||
|
<optional>true</optional> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.apache.rocketmq</groupId> |
||||
|
<artifactId>rocketmq-spring-boot-starter</artifactId> |
||||
|
<optional>true</optional> |
||||
|
</dependency> |
||||
|
|
||||
|
<!-- 工具类相关 --> |
||||
|
<dependency> |
||||
|
<groupId>com.google.guava</groupId> |
||||
|
<artifactId>guava</artifactId> |
||||
|
</dependency> |
||||
|
</dependencies> |
||||
|
|
||||
|
</project> |
||||
@ -0,0 +1,132 @@ |
|||||
|
package com.qiantoon.platform.framework.tenant.config; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.common.enums.WebFilterOrderEnum; |
||||
|
import com.qiantoon.platform.framework.mybatis.core.util.MyBatisUtils; |
||||
|
import com.qiantoon.platform.framework.redis.config.PlatformCacheProperties; |
||||
|
import com.qiantoon.platform.framework.tenant.core.aop.TenantIgnoreAspect; |
||||
|
import com.qiantoon.platform.framework.tenant.core.db.TenantDatabaseInterceptor; |
||||
|
import com.qiantoon.platform.framework.tenant.core.job.TenantJobAspect; |
||||
|
import com.qiantoon.platform.framework.tenant.core.mq.rabbitmq.TenantRabbitMQInitializer; |
||||
|
import com.qiantoon.platform.framework.tenant.core.mq.redis.TenantRedisMessageInterceptor; |
||||
|
import com.qiantoon.platform.framework.tenant.core.mq.rocketmq.TenantRocketMQInitializer; |
||||
|
import com.qiantoon.platform.framework.tenant.core.redis.TenantRedisCacheManager; |
||||
|
import com.qiantoon.platform.framework.tenant.core.security.TenantSecurityWebFilter; |
||||
|
import com.qiantoon.platform.framework.tenant.core.service.TenantFrameworkService; |
||||
|
import com.qiantoon.platform.framework.tenant.core.service.TenantFrameworkServiceImpl; |
||||
|
import com.qiantoon.platform.framework.tenant.core.web.TenantContextWebFilter; |
||||
|
import com.qiantoon.platform.framework.web.config.WebProperties; |
||||
|
import com.qiantoon.platform.framework.web.core.handler.GlobalExceptionHandler; |
||||
|
import com.qiantoon.platform.module.system.api.tenant.TenantApi; |
||||
|
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; |
||||
|
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; |
||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration; |
||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties; |
||||
|
import org.springframework.boot.web.servlet.FilterRegistrationBean; |
||||
|
import org.springframework.context.annotation.Bean; |
||||
|
import org.springframework.context.annotation.Primary; |
||||
|
import org.springframework.data.redis.cache.BatchStrategies; |
||||
|
import org.springframework.data.redis.cache.RedisCacheConfiguration; |
||||
|
import org.springframework.data.redis.cache.RedisCacheManager; |
||||
|
import org.springframework.data.redis.cache.RedisCacheWriter; |
||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory; |
||||
|
import org.springframework.data.redis.core.RedisTemplate; |
||||
|
|
||||
|
import java.util.Objects; |
||||
|
|
||||
|
@AutoConfiguration |
||||
|
@ConditionalOnProperty(prefix = "platform.tenant", value = "enable", matchIfMissing = true) // 允许使用 platform.tenant.enable=false 禁用多租户
|
||||
|
@EnableConfigurationProperties(TenantProperties.class) |
||||
|
public class PlatformTenantAutoConfiguration { |
||||
|
|
||||
|
@Bean |
||||
|
public TenantFrameworkService tenantFrameworkService(TenantApi tenantApi) { |
||||
|
return new TenantFrameworkServiceImpl(tenantApi); |
||||
|
} |
||||
|
|
||||
|
// ========== AOP ==========
|
||||
|
|
||||
|
@Bean |
||||
|
public TenantIgnoreAspect tenantIgnoreAspect() { |
||||
|
return new TenantIgnoreAspect(); |
||||
|
} |
||||
|
|
||||
|
// ========== DB ==========
|
||||
|
|
||||
|
@Bean |
||||
|
public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties, |
||||
|
MybatisPlusInterceptor interceptor) { |
||||
|
TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties)); |
||||
|
// 添加到 interceptor 中
|
||||
|
// 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
|
||||
|
MyBatisUtils.addInterceptor(interceptor, inner, 0); |
||||
|
return inner; |
||||
|
} |
||||
|
|
||||
|
// ========== WEB ==========
|
||||
|
|
||||
|
@Bean |
||||
|
public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter() { |
||||
|
FilterRegistrationBean<TenantContextWebFilter> registrationBean = new FilterRegistrationBean<>(); |
||||
|
registrationBean.setFilter(new TenantContextWebFilter()); |
||||
|
registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER); |
||||
|
return registrationBean; |
||||
|
} |
||||
|
|
||||
|
// ========== Security ==========
|
||||
|
|
||||
|
@Bean |
||||
|
public FilterRegistrationBean<TenantSecurityWebFilter> tenantSecurityWebFilter(TenantProperties tenantProperties, |
||||
|
WebProperties webProperties, |
||||
|
GlobalExceptionHandler globalExceptionHandler, |
||||
|
TenantFrameworkService tenantFrameworkService) { |
||||
|
FilterRegistrationBean<TenantSecurityWebFilter> registrationBean = new FilterRegistrationBean<>(); |
||||
|
registrationBean.setFilter(new TenantSecurityWebFilter(tenantProperties, webProperties, |
||||
|
globalExceptionHandler, tenantFrameworkService)); |
||||
|
registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER); |
||||
|
return registrationBean; |
||||
|
} |
||||
|
|
||||
|
// ========== MQ ==========
|
||||
|
|
||||
|
@Bean |
||||
|
public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() { |
||||
|
return new TenantRedisMessageInterceptor(); |
||||
|
} |
||||
|
|
||||
|
@Bean |
||||
|
@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate") |
||||
|
public TenantRabbitMQInitializer tenantRabbitMQInitializer() { |
||||
|
return new TenantRabbitMQInitializer(); |
||||
|
} |
||||
|
|
||||
|
@Bean |
||||
|
@ConditionalOnClass(name = "org.apache.rocketmq.spring.core.RocketMQTemplate") |
||||
|
public TenantRocketMQInitializer tenantRocketMQInitializer() { |
||||
|
return new TenantRocketMQInitializer(); |
||||
|
} |
||||
|
|
||||
|
// ========== Job ==========
|
||||
|
|
||||
|
@Bean |
||||
|
public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) { |
||||
|
return new TenantJobAspect(tenantFrameworkService); |
||||
|
} |
||||
|
|
||||
|
// ========== Redis ==========
|
||||
|
|
||||
|
@Bean |
||||
|
@Primary // 引入租户时,tenantRedisCacheManager 为主 Bean
|
||||
|
public RedisCacheManager tenantRedisCacheManager(RedisTemplate<String, Object> redisTemplate, |
||||
|
RedisCacheConfiguration redisCacheConfiguration, |
||||
|
PlatformCacheProperties platformCacheProperties) { |
||||
|
// 创建 RedisCacheWriter 对象
|
||||
|
RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory()); |
||||
|
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, |
||||
|
BatchStrategies.scan(platformCacheProperties.getRedisScanBatchSize())); |
||||
|
// 创建 TenantRedisCacheManager 对象
|
||||
|
return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
package com.qiantoon.platform.framework.tenant.config; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import org.springframework.boot.context.properties.ConfigurationProperties; |
||||
|
|
||||
|
import java.util.Collections; |
||||
|
import java.util.Set; |
||||
|
|
||||
|
/** |
||||
|
* 多租户配置 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
@ConfigurationProperties(prefix = "platform.tenant") |
||||
|
@Data |
||||
|
public class TenantProperties { |
||||
|
|
||||
|
/** |
||||
|
* 租户是否开启 |
||||
|
*/ |
||||
|
private static final Boolean ENABLE_DEFAULT = true; |
||||
|
|
||||
|
/** |
||||
|
* 是否开启 |
||||
|
*/ |
||||
|
private Boolean enable = ENABLE_DEFAULT; |
||||
|
|
||||
|
/** |
||||
|
* 需要忽略多租户的请求 |
||||
|
* |
||||
|
* 默认情况下,每个请求需要带上 tenant-id 的请求头。但是,部分请求是无需带上的,例如说短信回调、支付回调等 Open API! |
||||
|
*/ |
||||
|
private Set<String> ignoreUrls = Collections.emptySet(); |
||||
|
|
||||
|
/** |
||||
|
* 需要忽略多租户的表 |
||||
|
* |
||||
|
* 即默认所有表都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟 |
||||
|
*/ |
||||
|
private Set<String> ignoreTables = Collections.emptySet(); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
package com.qiantoon.platform.framework.tenant.core.aop; |
||||
|
|
||||
|
import java.lang.annotation.*; |
||||
|
|
||||
|
/** |
||||
|
* 忽略租户,标记指定方法不进行租户的自动过滤 |
||||
|
* |
||||
|
* 注意,只有 DB 的场景会过滤,其它场景暂时不过滤: |
||||
|
* 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的 |
||||
|
* 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
@Target({ElementType.METHOD}) |
||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||
|
@Inherited |
||||
|
public @interface TenantIgnore { |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
package com.qiantoon.platform.framework.tenant.core.aop; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.tenant.core.context.TenantContextHolder; |
||||
|
import com.qiantoon.platform.framework.tenant.core.util.TenantUtils; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.aspectj.lang.ProceedingJoinPoint; |
||||
|
import org.aspectj.lang.annotation.Around; |
||||
|
import org.aspectj.lang.annotation.Aspect; |
||||
|
|
||||
|
/** |
||||
|
* 忽略多租户的 Aspect,基于 {@link TenantIgnore} 注解实现,用于一些全局的逻辑。 |
||||
|
* 例如说,一个定时任务,读取所有数据,进行处理。 |
||||
|
* 又例如说,读取所有数据,进行缓存。 |
||||
|
* |
||||
|
* 整体逻辑的实现,和 {@link TenantUtils#executeIgnore(Runnable)} 需要保持一致 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
@Aspect |
||||
|
@Slf4j |
||||
|
public class TenantIgnoreAspect { |
||||
|
|
||||
|
@Around("@annotation(tenantIgnore)") |
||||
|
public Object around(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable { |
||||
|
Boolean oldIgnore = TenantContextHolder.isIgnore(); |
||||
|
try { |
||||
|
TenantContextHolder.setIgnore(true); |
||||
|
// 执行逻辑
|
||||
|
return joinPoint.proceed(); |
||||
|
} finally { |
||||
|
TenantContextHolder.setIgnore(oldIgnore); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,68 @@ |
|||||
|
package com.qiantoon.platform.framework.tenant.core.context; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.common.enums.DocumentEnum; |
||||
|
import com.alibaba.ttl.TransmittableThreadLocal; |
||||
|
|
||||
|
/** |
||||
|
* 多租户上下文 Holder |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class TenantContextHolder { |
||||
|
|
||||
|
/** |
||||
|
* 当前租户编号 |
||||
|
*/ |
||||
|
private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>(); |
||||
|
|
||||
|
/** |
||||
|
* 是否忽略租户 |
||||
|
*/ |
||||
|
private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>(); |
||||
|
|
||||
|
/** |
||||
|
* 获得租户编号 |
||||
|
* |
||||
|
* @return 租户编号 |
||||
|
*/ |
||||
|
public static Long getTenantId() { |
||||
|
return TENANT_ID.get(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获得租户编号。如果不存在,则抛出 NullPointerException 异常 |
||||
|
* |
||||
|
* @return 租户编号 |
||||
|
*/ |
||||
|
public static Long getRequiredTenantId() { |
||||
|
Long tenantId = getTenantId(); |
||||
|
if (tenantId == null) { |
||||
|
throw new NullPointerException("TenantContextHolder 不存在租户编号!可参考文档:" |
||||
|
+ DocumentEnum.TENANT.getUrl()); |
||||
|
} |
||||
|
return tenantId; |
||||
|
} |
||||
|
|
||||
|
public static void setTenantId(Long tenantId) { |
||||
|
TENANT_ID.set(tenantId); |
||||
|
} |
||||
|
|
||||
|
public static void setIgnore(Boolean ignore) { |
||||
|
IGNORE.set(ignore); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 当前是否忽略租户 |
||||
|
* |
||||
|
* @return 是否忽略 |
||||
|
*/ |
||||
|
public static boolean isIgnore() { |
||||
|
return Boolean.TRUE.equals(IGNORE.get()); |
||||
|
} |
||||
|
|
||||
|
public static void clear() { |
||||
|
TENANT_ID.remove(); |
||||
|
IGNORE.remove(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
package com.qiantoon.platform.framework.tenant.core.db; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.mybatis.core.dataobject.BaseDO; |
||||
|
import lombok.Data; |
||||
|
import lombok.EqualsAndHashCode; |
||||
|
|
||||
|
/** |
||||
|
* 拓展多租户的 BaseDO 基类 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
@Data |
||||
|
@EqualsAndHashCode(callSuper = true) |
||||
|
public abstract class TenantBaseDO extends BaseDO { |
||||
|
|
||||
|
/** |
||||
|
* 多租户编号 |
||||
|
*/ |
||||
|
private Long tenantId; |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
package com.qiantoon.platform.framework.tenant.core.db; |
||||
|
|
||||
|
import cn.hutool.core.collection.CollUtil; |
||||
|
import com.qiantoon.platform.framework.tenant.config.TenantProperties; |
||||
|
import com.qiantoon.platform.framework.tenant.core.context.TenantContextHolder; |
||||
|
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; |
||||
|
import net.sf.jsqlparser.expression.Expression; |
||||
|
import net.sf.jsqlparser.expression.LongValue; |
||||
|
|
||||
|
import java.util.HashSet; |
||||
|
import java.util.Set; |
||||
|
|
||||
|
/** |
||||
|
* 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class TenantDatabaseInterceptor implements TenantLineHandler { |
||||
|
|
||||
|
private final Set<String> ignoreTables = new HashSet<>(); |
||||
|
|
||||
|
public TenantDatabaseInterceptor(TenantProperties properties) { |
||||
|
// 不同 DB 下,大小写的习惯不同,所以需要都添加进去
|
||||
|
properties.getIgnoreTables().forEach(table -> { |
||||
|
ignoreTables.add(table.toLowerCase()); |
||||
|
ignoreTables.add(table.toUpperCase()); |
||||
|
}); |
||||
|
// 在 OracleKeyGenerator 中,生成主键时,会查询这个表,查询这个表后,会自动拼接 TENANT_ID 导致报错
|
||||
|
ignoreTables.add("DUAL"); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Expression getTenantId() { |
||||
|
return new LongValue(TenantContextHolder.getRequiredTenantId()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean ignoreTable(String tableName) { |
||||
|
return TenantContextHolder.isIgnore() // 情况一,全局忽略多租户
|
||||
|
|| CollUtil.contains(ignoreTables, tableName); // 情况二,忽略多租户的表
|
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
package com.qiantoon.platform.framework.tenant.core.job; |
||||
|
|
||||
|
import java.lang.annotation.ElementType; |
||||
|
import java.lang.annotation.Retention; |
||||
|
import java.lang.annotation.RetentionPolicy; |
||||
|
import java.lang.annotation.Target; |
||||
|
|
||||
|
/** |
||||
|
* 多租户 Job 注解 |
||||
|
*/ |
||||
|
@Target({ElementType.METHOD}) |
||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||
|
public @interface TenantJob { |
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
package com.qiantoon.platform.framework.tenant.core.job; |
||||
|
|
||||
|
import cn.hutool.core.collection.CollUtil; |
||||
|
import cn.hutool.core.exceptions.ExceptionUtil; |
||||
|
import com.qiantoon.platform.framework.common.util.json.JsonUtils; |
||||
|
import com.qiantoon.platform.framework.tenant.core.service.TenantFrameworkService; |
||||
|
import com.qiantoon.platform.framework.tenant.core.util.TenantUtils; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.aspectj.lang.ProceedingJoinPoint; |
||||
|
import org.aspectj.lang.annotation.Around; |
||||
|
import org.aspectj.lang.annotation.Aspect; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||
|
|
||||
|
/** |
||||
|
* 多租户 JobHandler AOP |
||||
|
* 任务执行时,会按照租户逐个执行 Job 的逻辑 |
||||
|
* |
||||
|
* 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
@Aspect |
||||
|
@RequiredArgsConstructor |
||||
|
@Slf4j |
||||
|
public class TenantJobAspect { |
||||
|
|
||||
|
private final TenantFrameworkService tenantFrameworkService; |
||||
|
|
||||
|
@Around("@annotation(tenantJob)") |
||||
|
public String around(ProceedingJoinPoint joinPoint, TenantJob tenantJob) { |
||||
|
// 获得租户列表
|
||||
|
List<Long> tenantIds = tenantFrameworkService.getTenantIds(); |
||||
|
if (CollUtil.isEmpty(tenantIds)) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// 逐个租户,执行 Job
|
||||
|
Map<Long, String> results = new ConcurrentHashMap<>(); |
||||
|
tenantIds.parallelStream().forEach(tenantId -> { |
||||
|
// TODO qt:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况
|
||||
|
TenantUtils.execute(tenantId, () -> { |
||||
|
try { |
||||
|
joinPoint.proceed(); |
||||
|
} catch (Throwable e) { |
||||
|
results.put(tenantId, ExceptionUtil.getRootCauseMessage(e)); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
return JsonUtils.toJsonString(results); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
package com.qiantoon.platform.framework.tenant.core.mq.kafka; |
||||
|
|
||||
|
import cn.hutool.core.util.StrUtil; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.boot.SpringApplication; |
||||
|
import org.springframework.boot.env.EnvironmentPostProcessor; |
||||
|
import org.springframework.core.env.ConfigurableEnvironment; |
||||
|
|
||||
|
/** |
||||
|
* 多租户的 Kafka 的 {@link EnvironmentPostProcessor} 实现类 |
||||
|
* |
||||
|
* Kafka Producer 发送消息时,增加 {@link TenantKafkaProducerInterceptor} 拦截器 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
public class TenantKafkaEnvironmentPostProcessor implements EnvironmentPostProcessor { |
||||
|
|
||||
|
private static final String PROPERTY_KEY_INTERCEPTOR_CLASSES = "spring.kafka.producer.properties.interceptor.classes"; |
||||
|
|
||||
|
@Override |
||||
|
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { |
||||
|
// 添加 TenantKafkaProducerInterceptor 拦截器
|
||||
|
try { |
||||
|
String value = environment.getProperty(PROPERTY_KEY_INTERCEPTOR_CLASSES); |
||||
|
if (StrUtil.isEmpty(value)) { |
||||
|
value = TenantKafkaProducerInterceptor.class.getName(); |
||||
|
} else { |
||||
|
value += "," + TenantKafkaProducerInterceptor.class.getName(); |
||||
|
} |
||||
|
environment.getSystemProperties().put(PROPERTY_KEY_INTERCEPTOR_CLASSES, value); |
||||
|
} catch (NoClassDefFoundError ignore) { |
||||
|
// 如果触发 NoClassDefFoundError 异常,说明 TenantKafkaProducerInterceptor 类不存在,即没引入 kafka-spring 依赖
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
package com.qiantoon.platform.framework.tenant.core.mq.kafka; |
||||
|
|
||||
|
import cn.hutool.core.util.ReflectUtil; |
||||
|
import com.qiantoon.platform.framework.tenant.core.context.TenantContextHolder; |
||||
|
import org.apache.kafka.clients.producer.ProducerInterceptor; |
||||
|
import org.apache.kafka.clients.producer.ProducerRecord; |
||||
|
import org.apache.kafka.clients.producer.RecordMetadata; |
||||
|
import org.apache.kafka.common.header.Headers; |
||||
|
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
|
||||
|
import static com.qiantoon.platform.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
||||
|
|
||||
|
/** |
||||
|
* Kafka 消息队列的多租户 {@link ProducerInterceptor} 实现类 |
||||
|
* |
||||
|
* 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 |
||||
|
* 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class TenantKafkaProducerInterceptor implements ProducerInterceptor<Object, Object> { |
||||
|
|
||||
|
@Override |
||||
|
public ProducerRecord<Object, Object> onSend(ProducerRecord<Object, Object> record) { |
||||
|
Long tenantId = TenantContextHolder.getTenantId(); |
||||
|
if (tenantId != null) { |
||||
|
Headers headers = (Headers) ReflectUtil.getFieldValue(record, "headers"); // private 属性,没有 get 方法,智能反射
|
||||
|
headers.add(HEADER_TENANT_ID, tenantId.toString().getBytes()); |
||||
|
} |
||||
|
return record; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onAcknowledgement(RecordMetadata metadata, Exception exception) { |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void close() { |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void configure(Map<String, ?> configs) { |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
package com.qiantoon.platform.framework.tenant.core.mq.rabbitmq; |
||||
|
|
||||
|
import org.springframework.amqp.rabbit.core.RabbitTemplate; |
||||
|
import org.springframework.beans.BeansException; |
||||
|
import org.springframework.beans.factory.config.BeanPostProcessor; |
||||
|
|
||||
|
/** |
||||
|
* 多租户的 RabbitMQ 初始化器 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class TenantRabbitMQInitializer implements BeanPostProcessor { |
||||
|
|
||||
|
@Override |
||||
|
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { |
||||
|
if (bean instanceof RabbitTemplate) { |
||||
|
RabbitTemplate rabbitTemplate = (RabbitTemplate) bean; |
||||
|
rabbitTemplate.addBeforePublishPostProcessors(new TenantRabbitMQMessagePostProcessor()); |
||||
|
} |
||||
|
return bean; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
package com.qiantoon.platform.framework.tenant.core.mq.rabbitmq; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.tenant.core.context.TenantContextHolder; |
||||
|
import org.apache.kafka.clients.producer.ProducerInterceptor; |
||||
|
import org.springframework.amqp.AmqpException; |
||||
|
import org.springframework.amqp.core.Message; |
||||
|
import org.springframework.amqp.core.MessagePostProcessor; |
||||
|
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; |
||||
|
|
||||
|
import static com.qiantoon.platform.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
||||
|
|
||||
|
/** |
||||
|
* RabbitMQ 消息队列的多租户 {@link ProducerInterceptor} 实现类 |
||||
|
* |
||||
|
* 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 |
||||
|
* 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class TenantRabbitMQMessagePostProcessor implements MessagePostProcessor { |
||||
|
|
||||
|
@Override |
||||
|
public Message postProcessMessage(Message message) throws AmqpException { |
||||
|
Long tenantId = TenantContextHolder.getTenantId(); |
||||
|
if (tenantId != null) { |
||||
|
message.getMessageProperties().getHeaders().put(HEADER_TENANT_ID, tenantId); |
||||
|
} |
||||
|
return message; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
package com.qiantoon.platform.framework.tenant.core.mq.redis; |
||||
|
|
||||
|
import cn.hutool.core.util.StrUtil; |
||||
|
import com.qiantoon.platform.framework.mq.redis.core.interceptor.RedisMessageInterceptor; |
||||
|
import com.qiantoon.platform.framework.mq.redis.core.message.AbstractRedisMessage; |
||||
|
import com.qiantoon.platform.framework.tenant.core.context.TenantContextHolder; |
||||
|
|
||||
|
import static com.qiantoon.platform.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
||||
|
|
||||
|
/** |
||||
|
* 多租户 {@link AbstractRedisMessage} 拦截器 |
||||
|
* |
||||
|
* 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 |
||||
|
* 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class TenantRedisMessageInterceptor implements RedisMessageInterceptor { |
||||
|
|
||||
|
@Override |
||||
|
public void sendMessageBefore(AbstractRedisMessage message) { |
||||
|
Long tenantId = TenantContextHolder.getTenantId(); |
||||
|
if (tenantId != null) { |
||||
|
message.addHeader(HEADER_TENANT_ID, tenantId.toString()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void consumeMessageBefore(AbstractRedisMessage message) { |
||||
|
String tenantIdStr = message.getHeader(HEADER_TENANT_ID); |
||||
|
if (StrUtil.isNotEmpty(tenantIdStr)) { |
||||
|
TenantContextHolder.setTenantId(Long.valueOf(tenantIdStr)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void consumeMessageAfter(AbstractRedisMessage message) { |
||||
|
// 注意,Consumer 是一个逻辑的入口,所以不考虑原本上下文就存在租户编号的情况
|
||||
|
TenantContextHolder.clear(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
package com.qiantoon.platform.framework.tenant.core.mq.rocketmq; |
||||
|
|
||||
|
import cn.hutool.core.lang.Assert; |
||||
|
import cn.hutool.core.util.StrUtil; |
||||
|
import com.qiantoon.platform.framework.tenant.core.context.TenantContextHolder; |
||||
|
import org.apache.rocketmq.client.hook.ConsumeMessageContext; |
||||
|
import org.apache.rocketmq.client.hook.ConsumeMessageHook; |
||||
|
import org.apache.rocketmq.common.message.MessageExt; |
||||
|
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
import static com.qiantoon.platform.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
||||
|
|
||||
|
/** |
||||
|
* RocketMQ 消息队列的多租户 {@link ConsumeMessageHook} 实现类 |
||||
|
* |
||||
|
* Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class TenantRocketMQConsumeMessageHook implements ConsumeMessageHook { |
||||
|
|
||||
|
@Override |
||||
|
public String hookName() { |
||||
|
return getClass().getSimpleName(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void consumeMessageBefore(ConsumeMessageContext context) { |
||||
|
// 校验,消息必须是单条,不然设置租户可能不正确
|
||||
|
List<MessageExt> messages = context.getMsgList(); |
||||
|
Assert.isTrue(messages.size() == 1, "消息条数({})不正确", messages.size()); |
||||
|
// 设置租户编号
|
||||
|
String tenantId = messages.get(0).getUserProperty(HEADER_TENANT_ID); |
||||
|
if (StrUtil.isNotEmpty(tenantId)) { |
||||
|
TenantContextHolder.setTenantId(Long.parseLong(tenantId)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void consumeMessageAfter(ConsumeMessageContext context) { |
||||
|
TenantContextHolder.clear(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,53 @@ |
|||||
|
package com.qiantoon.platform.framework.tenant.core.mq.rocketmq; |
||||
|
|
||||
|
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; |
||||
|
import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl; |
||||
|
import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; |
||||
|
import org.apache.rocketmq.client.producer.DefaultMQProducer; |
||||
|
import org.apache.rocketmq.spring.core.RocketMQTemplate; |
||||
|
import org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer; |
||||
|
import org.springframework.beans.BeansException; |
||||
|
import org.springframework.beans.factory.config.BeanPostProcessor; |
||||
|
|
||||
|
/** |
||||
|
* 多租户的 RocketMQ 初始化器 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class TenantRocketMQInitializer implements BeanPostProcessor { |
||||
|
|
||||
|
@Override |
||||
|
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { |
||||
|
if (bean instanceof DefaultRocketMQListenerContainer) { |
||||
|
DefaultRocketMQListenerContainer container = (DefaultRocketMQListenerContainer) bean; |
||||
|
initTenantConsumer(container.getConsumer()); |
||||
|
} else if (bean instanceof RocketMQTemplate) { |
||||
|
RocketMQTemplate template = (RocketMQTemplate) bean; |
||||
|
initTenantProducer(template.getProducer()); |
||||
|
} |
||||
|
return bean; |
||||
|
} |
||||
|
|
||||
|
private void initTenantProducer(DefaultMQProducer producer) { |
||||
|
if (producer == null) { |
||||
|
return; |
||||
|
} |
||||
|
DefaultMQProducerImpl producerImpl = producer.getDefaultMQProducerImpl(); |
||||
|
if (producerImpl == null) { |
||||
|
return; |
||||
|
} |
||||
|
producerImpl.registerSendMessageHook(new TenantRocketMQSendMessageHook()); |
||||
|
} |
||||
|
|
||||
|
private void initTenantConsumer(DefaultMQPushConsumer consumer) { |
||||
|
if (consumer == null) { |
||||
|
return; |
||||
|
} |
||||
|
DefaultMQPushConsumerImpl consumerImpl = consumer.getDefaultMQPushConsumerImpl(); |
||||
|
if (consumerImpl == null) { |
||||
|
return; |
||||
|
} |
||||
|
consumerImpl.registerConsumeMessageHook(new TenantRocketMQConsumeMessageHook()); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
package com.qiantoon.platform.framework.tenant.core.mq.rocketmq; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.tenant.core.context.TenantContextHolder; |
||||
|
import org.apache.rocketmq.client.hook.SendMessageContext; |
||||
|
import org.apache.rocketmq.client.hook.SendMessageHook; |
||||
|
|
||||
|
import static com.qiantoon.platform.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; |
||||
|
|
||||
|
/** |
||||
|
* RocketMQ 消息队列的多租户 {@link SendMessageHook} 实现类 |
||||
|
* |
||||
|
* Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 |
||||
|
* |
||||
|
* @author qt |
||||
|
*/ |
||||
|
public class TenantRocketMQSendMessageHook implements SendMessageHook { |
||||
|
|
||||
|
@Override |
||||
|
public String hookName() { |
||||
|
return getClass().getSimpleName(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void sendMessageBefore(SendMessageContext sendMessageContext) { |
||||
|
Long tenantId = TenantContextHolder.getTenantId(); |
||||
|
if (tenantId == null) { |
||||
|
return; |
||||
|
} |
||||
|
sendMessageContext.getMessage().putUserProperty(HEADER_TENANT_ID, tenantId.toString()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void sendMessageAfter(SendMessageContext sendMessageContext) { |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
package com.qiantoon.platform.framework.tenant.core.redis; |
||||
|
|
||||
|
import com.qiantoon.platform.framework.redis.core.TimeoutRedisCacheManager; |
||||
|
import com.qiantoon.platform.framework.tenant.core.context.TenantContextHolder; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.cache.Cache; |
||||
|
import org.springframework.data.redis.cache.RedisCacheConfiguration; |
||||
|
import org.springframework.data.redis.cache.RedisCacheManager; |
||||
|
import org.springframework.data.redis.cache.RedisCacheWriter; |
||||
|
|
||||
|
/** |
||||
|
* 多租户的 {@link RedisCacheManager} 实现类 |
||||
|
* |
||||
|
* 操作指定 name 的 {@link Cache} 时,自动拼接租户后缀,格式为 name + ":" + tenantId + 后缀 |
||||
|
* |
||||
|
* @author airhead |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
public class TenantRedisCacheManager extends TimeoutRedisCacheManager { |
||||
|
|
||||
|
public TenantRedisCacheManager(RedisCacheWriter cacheWriter, |
||||
|
RedisCacheConfiguration defaultCacheConfiguration) { |
||||
|
super(cacheWriter, defaultCacheConfiguration); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Cache getCache(String name) { |
||||
|
// 如果开启多租户,则 name 拼接租户后缀
|
||||
|
if (!TenantContextHolder.isIgnore() |
||||
|
&& TenantContextHolder.getTenantId() != null) { |
||||
|
name = name + ":" + TenantContextHolder.getTenantId(); |
||||
|
} |
||||
|
|
||||
|
// 继续基于父方法
|
||||
|
return super.getCache(name); |
||||
|
} |
||||
|
|
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue