17 Commits

Author SHA1 Message Date
57dd6831d3 !664 发布 5.3.1 正式版
Merge pull request !664 from 疯狂的狮子Li/dev
2025-03-27 02:54:00 +00:00
8aa60abb1f !663 回退 'Pull Request !662 : 发布 5.3.1 正式版'
* 回退 'Pull Request !662 : 发布 5.3.1 正式版'
2025-03-27 02:53:23 +00:00
7a9f51fc7a !662 发布 5.3.1 正式版
* 🐳发布 5.3.1 正式版
* update 优化 删除无用配置
* fix 修复 excel模板导出数据被覆盖的问题
* update 优化 统一用户密码校验长度
* update mybatis-plus 3.5.10.1 => 3.5.11
* fix 修复 跨域未设置请求头问题(cloud版本不需要 vue版本需要)
2025-03-27 02:51:57 +00:00
159e30c982 !661 发布 5.3.1-BETA2 公测版本
Merge pull request !661 from 疯狂的狮子Li/dev
2025-03-21 07:25:25 +00:00
7334d91d6b !652 发布 5.3.1-BETA 公测版本
Merge pull request !652 from 疯狂的狮子Li/dev
2025-03-13 05:27:36 +00:00
95c01301f6 !644 同步修复一些问题
Merge pull request !644 from 疯狂的狮子Li/dev
2025-02-07 06:19:28 +00:00
296466fa13 !640 发布 5.3.0 新春版 祝大家新年快乐
Merge pull request !640 from 疯狂的狮子Li/dev
2025-01-24 05:08:28 +00:00
3c8d864b5f !639 发布 5.3.0-BETA 公测版本
Merge pull request !639 from 疯狂的狮子Li/dev
2025-01-20 03:35:45 +00:00
ea50a57602 update 优化 xss包装器 Parameter 处理 兼容某些容器不允许改参数的情况 2024-11-21 10:17:34 +08:00
7e14b98676 reset 回滚错误修改
Signed-off-by: 疯狂的狮子Li <15040126243@163.com>
2024-10-28 09:46:28 +00:00
015b406001 !591 发布 5.2.3 正式版
Merge pull request !591 from 疯狂的狮子Li/dev
2024-10-25 03:09:23 +00:00
098d3347a0 !577 发布 5.2.2 正式版 安全性提升
Merge pull request !577 from 疯狂的狮子Li/dev
2024-08-26 03:43:59 +00:00
08d4493994 update 优化 bug 模板 2024-07-15 15:19:22 +08:00
367d739e2d Merge remote-tracking branch 'origin/5.X' into 5.X 2024-07-09 16:38:43 +08:00
d6688a367d !562 ♥️发布 5.2.1 正式版本
Merge pull request !562 from 疯狂的狮子Li/dev
2024-07-09 02:42:40 +00:00
0b331796e2 !551 ♥️发布 5.2.0 正式版本
Merge pull request !551 from 疯狂的狮子Li/dev
2024-06-20 02:10:15 +00:00
456620b638 !549 ♥️发布 5.2.0-BETA2 公测版本
Merge pull request !549 from 疯狂的狮子Li/dev
2024-06-06 03:13:46 +00:00
345 changed files with 3632 additions and 9586 deletions

View File

@ -2,7 +2,7 @@
<configuration default="false" name="ruoyi-monitor-admin" type="docker-deploy" factoryName="dockerfile" server-name="Docker"> <configuration default="false" name="ruoyi-monitor-admin" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
<deployment type="dockerfile"> <deployment type="dockerfile">
<settings> <settings>
<option name="imageTag" value="ruoyi/ruoyi-monitor-admin:5.4.1" /> <option name="imageTag" value="ruoyi/ruoyi-monitor-admin:5.3.1" />
<option name="buildOnly" value="true" /> <option name="buildOnly" value="true" />
<option name="sourceFilePath" value="ruoyi-extend/ruoyi-monitor-admin/Dockerfile" /> <option name="sourceFilePath" value="ruoyi-extend/ruoyi-monitor-admin/Dockerfile" />
</settings> </settings>

View File

@ -2,7 +2,7 @@
<configuration default="false" name="ruoyi-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker"> <configuration default="false" name="ruoyi-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
<deployment type="dockerfile"> <deployment type="dockerfile">
<settings> <settings>
<option name="imageTag" value="ruoyi/ruoyi-server:5.4.1" /> <option name="imageTag" value="ruoyi/ruoyi-server:5.3.1" />
<option name="buildOnly" value="true" /> <option name="buildOnly" value="true" />
<option name="sourceFilePath" value="ruoyi-admin/Dockerfile" /> <option name="sourceFilePath" value="ruoyi-admin/Dockerfile" />
</settings> </settings>

View File

@ -2,7 +2,7 @@
<configuration default="false" name="ruoyi-snailjob-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker"> <configuration default="false" name="ruoyi-snailjob-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
<deployment type="dockerfile"> <deployment type="dockerfile">
<settings> <settings>
<option name="imageTag" value="ruoyi/ruoyi-snailjob-server:5.4.1" /> <option name="imageTag" value="ruoyi/ruoyi-snailjob-server:5.3.1" />
<option name="buildOnly" value="true" /> <option name="buildOnly" value="true" />
<option name="sourceFilePath" value="ruoyi-extend/ruoyi-snailjob-server/Dockerfile" /> <option name="sourceFilePath" value="ruoyi-extend/ruoyi-snailjob-server/Dockerfile" />
</settings> </settings>

View File

@ -7,10 +7,10 @@
[![码云Gitee](https://gitee.com/dromara/RuoYi-Vue-Plus/badge/star.svg?theme=blue)](https://gitee.com/dromara/RuoYi-Vue-Plus) [![码云Gitee](https://gitee.com/dromara/RuoYi-Vue-Plus/badge/star.svg?theme=blue)](https://gitee.com/dromara/RuoYi-Vue-Plus)
[![GitHub](https://img.shields.io/github/stars/dromara/RuoYi-Vue-Plus.svg?style=social&label=Stars)](https://github.com/dromara/RuoYi-Vue-Plus) [![GitHub](https://img.shields.io/github/stars/dromara/RuoYi-Vue-Plus.svg?style=social&label=Stars)](https://github.com/dromara/RuoYi-Vue-Plus)
[![Star](https://gitcode.com/dromara/RuoYi-Vue-Plus/star/badge.svg)](https://gitcode.com/dromara/RuoYi-Vue-Plus) [![Star](https://gitcode.com/dromara/RuoYi-Vue-Plus/star/badge.svg)](https://gitcode.com/dromara/RuoYi-Vue-Plus)
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/5.X/LICENSE) [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/master/LICENSE)
[![使用IntelliJ IDEA开发维护](https://img.shields.io/badge/IntelliJ%20IDEA-提供支持-blue.svg)](https://www.jetbrains.com/?from=RuoYi-Vue-Plus) [![使用IntelliJ IDEA开发维护](https://img.shields.io/badge/IntelliJ%20IDEA-提供支持-blue.svg)](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
<br> <br>
[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.4.1-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus) [![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.3.0-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.4-blue.svg)]() [![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.4-blue.svg)]()
[![JDK-17](https://img.shields.io/badge/JDK-17-green.svg)]() [![JDK-17](https://img.shields.io/badge/JDK-17-green.svg)]()
[![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)]() [![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)]()
@ -22,12 +22,10 @@
> 系统演示: [传送门](https://plus-doc.dromara.org/#/common/demo_system) > 系统演示: [传送门](https://plus-doc.dromara.org/#/common/demo_system)
> 官方前端项目地址: [gitee](https://gitee.com/JavaLionLi/plus-ui) - [github](https://github.com/JavaLionLi/plus-ui) - [gitcode](https://gitcode.com/dromara/plus-ui)<br> > 官方前端项目地址: [plus-ui](https://gitee.com/JavaLionLi/plus-ui)<br>
> 成员前端项目地址: 基于vben5 [ruoyi-plus-vben5](https://gitee.com/dapppp/ruoyi-plus-vben5)<br> > 成员前端项目地址: 基于vben5 [ruoyi-plus-vben5](https://gitee.com/dapppp/ruoyi-plus-vben5)
> 成员前端项目地址: 基于soybean [ruoyi-plus-soybean](https://gitee.com/xlsea/ruoyi-plus-soybean)<br>
> 成员项目地址: 删除多租户与工作流 [RuoYi-Vue-Plus-Single](https://gitee.com/ColorDreams/RuoYi-Vue-Plus-Single)<br>
> 文档地址: [plus-doc](https://plus-doc.dromara.org) 国内加速: [plus-doc.top](https://plus-doc.top) > 文档地址: [plus-doc](https://plus-doc.dromara.org)
## 赞助商 ## 赞助商
@ -36,11 +34,7 @@ CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
数舵科技 软件定制开发APP小程序等 - http://www.shuduokeji.com/ <br> 数舵科技 软件定制开发APP小程序等 - http://www.shuduokeji.com/ <br>
引迈信息 软件开发平台 - https://www.jnpfsoft.com/index.html?from=plus-doc <br> 引迈信息 软件开发平台 - https://www.jnpfsoft.com/index.html?from=plus-doc <br>
<font color="red">**启山商城系统 多租户商城源码可免费商用可二次开发 - https://www.73app.cn/** </font><br> <font color="red">**启山商城系统 多租户商城源码可免费商用可二次开发 - https://www.73app.cn/** </font><br>
Mall4J 高质量Java商城系统 - https://www.mall4j.com/cn/?statId=11 <br> [如何成为赞助商 加群联系作者详谈](https://plus-doc.dromara.org/#/common/add_group)
aizuda flowlong 工作流 - https://gitee.com/aizuda/flowlong <br>
Ruoyi-Plus-Uniapp - https://ruoyi.plus <br>
[如何成为赞助商 加群联系作者详谈 每日PV2500-3000 IP1700-2500](https://plus-doc.dromara.org/#/common/add_group)
# 本框架与RuoYi的功能差异 # 本框架与RuoYi的功能差异
@ -81,7 +75,7 @@ Ruoyi-Plus-Uniapp - https://ruoyi.plus <br>
| 邮件 | 采用 mail-api 通用协议支持大部分邮件厂商 | 不支持 | | 邮件 | 采用 mail-api 通用协议支持大部分邮件厂商 | 不支持 |
| 接口文档 | 采用 SpringDoc、javadoc 无注解零入侵基于java注释<br/>只需把注释写好 无需再写一大堆的文档注解了 | 采用 Springfox 已停止维护 需要编写大量的注解来支持文档生成 | | 接口文档 | 采用 SpringDoc、javadoc 无注解零入侵基于java注释<br/>只需把注释写好 无需再写一大堆的文档注解了 | 采用 Springfox 已停止维护 需要编写大量的注解来支持文档生成 |
| 校验框架 | 采用 Validation 支持注解与工具类校验 注解支持国际化 | 仅支持注解 且注解不支持国际化 | | 校验框架 | 采用 Validation 支持注解与工具类校验 注解支持国际化 | 仅支持注解 且注解不支持国际化 |
| Excel框架 | 采用 FastExcel(原Alibaba EasyExcel) 基于插件化<br/>框架对其增加了很多功能 例如 自动合并相同内容 自动排列布局 字典翻译等 | 基于 POI 手写实现 功能有限 复杂 扩展性差 | | Excel框架 | 采用 Alibaba EasyExcel 基于插件化<br/>框架对其增加了很多功能 例如 自动合并相同内容 自动排列布局 字典翻译等 | 基于 POI 手写实现 功能有限 复杂 扩展性差 |
| 工作流支持 | 支持各种复杂审批 转办 委派 加减签 会签 或签 票签 等功能 | 无 | | 工作流支持 | 支持各种复杂审批 转办 委派 加减签 会签 或签 票签 等功能 | 无 |
| 工具类框架 | 采用 Hutool、Lombok 上百种工具覆盖90%的使用需求 基于注解自动生成 get set 等简化框架大量代码 | 手写工具稳定性差易出问题 工具数量有限 代码臃肿需自己手写 get set 等 | | 工具类框架 | 采用 Hutool、Lombok 上百种工具覆盖90%的使用需求 基于注解自动生成 get set 等简化框架大量代码 | 手写工具稳定性差易出问题 工具数量有限 代码臃肿需自己手写 get set 等 |
| 监控框架 | 采用 SpringBoot-Admin 基于SpringBoot官方 actuator 探针机制<br/>实时监控服务状态 框架还为其扩展了在线日志查看监控 | 无 | | 监控框架 | 采用 SpringBoot-Admin 基于SpringBoot官方 actuator 探针机制<br/>实时监控服务状态 框架还为其扩展了在线日志查看监控 | 无 |
@ -119,6 +113,7 @@ Ruoyi-Plus-Uniapp - https://ruoyi.plus <br>
| 系统接口 | 根据业务代码自动生成相关的api接口文档 | 支持 | 支持 | | 系统接口 | 根据业务代码自动生成相关的api接口文档 | 支持 | 支持 |
| 服务监控 | 监视集群系统CPU、内存、磁盘、堆栈、在线日志、Spring相关配置等 | 支持 | 仅支持单机CPU、内存、磁盘监控 | | 服务监控 | 监视集群系统CPU、内存、磁盘、堆栈、在线日志、Spring相关配置等 | 支持 | 仅支持单机CPU、内存、磁盘监控 |
| 缓存监控 | 对系统的缓存信息查询,命令统计等。 | 支持 | 支持 | | 缓存监控 | 对系统的缓存信息查询,命令统计等。 | 支持 | 支持 |
| 在线构建器 | 拖动表单元素生成相应的HTML代码。 | 支持 | 支持 |
| 使用案例 | 系统的一些功能案例 | 支持 | 不支持 | | 使用案例 | 系统的一些功能案例 | 支持 | 不支持 |
## 参考文档 ## 参考文档

93
pom.xml
View File

@ -13,48 +13,49 @@
<description>Dromara RuoYi-Vue-Plus多租户管理系统</description> <description>Dromara RuoYi-Vue-Plus多租户管理系统</description>
<properties> <properties>
<revision>5.4.1</revision> <revision>5.3.1</revision>
<spring-boot.version>3.5.4</spring-boot.version> <spring-boot.version>3.4.4</spring-boot.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version> <java.version>17</java.version>
<mybatis.version>3.5.16</mybatis.version> <mybatis.version>3.5.16</mybatis.version>
<springdoc.version>2.8.13</springdoc.version> <springdoc.version>2.8.5</springdoc.version>
<therapi-javadoc.version>0.15.0</therapi-javadoc.version> <therapi-javadoc.version>0.15.0</therapi-javadoc.version>
<fastexcel.version>1.3.0</fastexcel.version> <easyexcel.version>4.0.3</easyexcel.version>
<velocity.version>2.3</velocity.version> <velocity.version>2.3</velocity.version>
<satoken.version>1.44.0</satoken.version> <satoken.version>1.40.0</satoken.version>
<mybatis-plus.version>3.5.14</mybatis-plus.version> <mybatis-plus.version>3.5.11</mybatis-plus.version>
<p6spy.version>3.9.1</p6spy.version> <p6spy.version>3.9.1</p6spy.version>
<hutool.version>5.8.40</hutool.version> <hutool.version>5.8.35</hutool.version>
<spring-boot-admin.version>3.5.3</spring-boot-admin.version> <spring-boot-admin.version>3.4.5</spring-boot-admin.version>
<redisson.version>3.51.0</redisson.version> <redisson.version>3.45.1</redisson.version>
<lock4j.version>2.2.7</lock4j.version> <lock4j.version>2.2.7</lock4j.version>
<dynamic-ds.version>4.3.1</dynamic-ds.version> <dynamic-ds.version>4.3.1</dynamic-ds.version>
<snailjob.version>1.7.2</snailjob.version> <snailjob.version>1.4.0</snailjob.version>
<mapstruct-plus.version>1.5.0</mapstruct-plus.version> <mapstruct-plus.version>1.4.6</mapstruct-plus.version>
<mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version> <mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
<lombok.version>1.18.40</lombok.version> <lombok.version>1.18.36</lombok.version>
<bouncycastle.version>1.80</bouncycastle.version> <bouncycastle.version>1.76</bouncycastle.version>
<justauth.version>1.16.7</justauth.version> <justauth.version>1.16.7</justauth.version>
<!-- 离线IP地址定位库 --> <!-- 离线IP地址定位库 -->
<ip2region.version>2.7.0</ip2region.version> <ip2region.version>2.7.0</ip2region.version>
<!-- OSS 配置 --> <!-- OSS 配置 -->
<aws.sdk.version>2.28.22</aws.sdk.version> <aws.sdk.version>2.28.22</aws.sdk.version>
<!-- SMS 配置 --> <!-- SMS 配置 -->
<sms4j.version>3.3.5</sms4j.version> <sms4j.version>3.3.4</sms4j.version>
<!-- 限制框架中的fastjson版本 --> <!-- 限制框架中的fastjson版本 -->
<fastjson.version>1.2.83</fastjson.version> <fastjson.version>1.2.83</fastjson.version>
<!-- 面向运行时的D-ORM依赖 --> <!-- 面向运行时的D-ORM依赖 -->
<anyline.version>8.7.2-20250603</anyline.version> <anyline.version>8.7.2-20250101</anyline.version>
<!-- 工作流配置 --> <!--工作流配置-->
<warm-flow.version>1.8.2-m2</warm-flow.version> <warm-flow.version>1.6.8</warm-flow.version>
<!-- 插件版本 --> <!-- 插件版本 -->
<maven-jar-plugin.version>3.4.2</maven-jar-plugin.version> <maven-jar-plugin.version>3.2.2</maven-jar-plugin.version>
<maven-war-plugin.version>3.4.0</maven-war-plugin.version> <maven-war-plugin.version>3.2.2</maven-war-plugin.version>
<maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version> <maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
<maven-surefire-plugin.version>3.5.3</maven-surefire-plugin.version> <maven-surefire-plugin.version>3.1.2</maven-surefire-plugin.version>
<flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version> <flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version>
<!-- 打包默认跳过测试 --> <!-- 打包默认跳过测试 -->
<skipTests>true</skipTests> <skipTests>true</skipTests>
@ -118,6 +119,25 @@
<scope>import</scope> <scope>import</scope>
</dependency> </dependency>
<!-- Warm-Flow国产工作流引擎, 在线文档http://warm-flow.cn/ -->
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-mybatis-plus-sb3-starter</artifactId>
<version>${warm-flow.version}</version>
</dependency>
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-plugin-ui-sb-web</artifactId>
<version>${warm-flow.version}</version>
</dependency>
<!-- JustAuth 的依赖配置-->
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
<version>${justauth.version}</version>
</dependency>
<!-- common 的依赖配置--> <!-- common 的依赖配置-->
<dependency> <dependency>
<groupId>org.dromara</groupId> <groupId>org.dromara</groupId>
@ -146,9 +166,9 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>cn.idev.excel</groupId> <groupId>com.alibaba</groupId>
<artifactId>fastexcel</artifactId> <artifactId>easyexcel</artifactId>
<version>${fastexcel.version}</version> <version>${easyexcel.version}</version>
</dependency> </dependency>
<!-- velocity代码生成使用模板 --> <!-- velocity代码生成使用模板 -->
@ -294,25 +314,6 @@
<version>${mapstruct-plus.version}</version> <version>${mapstruct-plus.version}</version>
</dependency> </dependency>
<!-- Warm-Flow国产工作流引擎, 在线文档http://warm-flow.cn/ -->
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-mybatis-plus-sb3-starter</artifactId>
<version>${warm-flow.version}</version>
</dependency>
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-plugin-ui-sb-web</artifactId>
<version>${warm-flow.version}</version>
</dependency>
<!-- JustAuth 的依赖配置-->
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
<version>${justauth.version}</version>
</dependency>
<!-- 离线IP地址定位库 ip2region --> <!-- 离线IP地址定位库 ip2region -->
<dependency> <dependency>
<groupId>org.lionsoul</groupId> <groupId>org.lionsoul</groupId>
@ -320,6 +321,12 @@
<version>${ip2region.version}</version> <version>${ip2region.version}</version>
</dependency> </dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.15.0</version>
</dependency>
<dependency> <dependency>
<groupId>com.alibaba</groupId> <groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId> <artifactId>fastjson</artifactId>

View File

@ -1,6 +1,6 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/ # 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds FROM bellsoft/liberica-openjdk-debian:17.0.11-cds
#FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds #FROM bellsoft/liberica-openjdk-debian:21.0.5-cds
#FROM findepi/graalvm:java17-native #FROM findepi/graalvm:java17-native
LABEL maintainer="Lion Li" LABEL maintainer="Lion Li"
@ -11,18 +11,17 @@ RUN mkdir -p /ruoyi/server/logs \
WORKDIR /ruoyi/server WORKDIR /ruoyi/server
ENV SERVER_PORT=8080 SNAIL_PORT=28080 LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS="" ENV SERVER_PORT=8080 LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS=""
EXPOSE ${SERVER_PORT} EXPOSE ${SERVER_PORT}
# 暴露 snail job 客户端端口 用于定时任务调度中心通信
EXPOSE ${SNAIL_PORT}
ADD ./target/ruoyi-admin.jar ./app.jar ADD ./target/ruoyi-admin.jar ./app.jar
# 工作流字体文件
ADD ./zhFonts/ /usr/share/fonts/zhFonts/
SHELL ["/bin/bash", "-c"] SHELL ["/bin/bash", "-c"]
ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -Dserver.port=${SERVER_PORT} \ ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -Dserver.port=${SERVER_PORT} \
-Dsnail-job.port=${SNAIL_PORT} \
# 应用名称 如果想区分集群节点监控 改成不同的名称即可 # 应用名称 如果想区分集群节点监控 改成不同的名称即可
#-Dskywalking.agent.service_name=ruoyi-server \ #-Dskywalking.agent.service_name=ruoyi-server \
#-javaagent:/ruoyi/skywalking/agent/skywalking-agent.jar \ #-javaagent:/ruoyi/skywalking/agent/skywalking-agent.jar \

View File

@ -21,8 +21,6 @@ import org.dromara.common.core.domain.model.SocialLoginBody;
import org.dromara.common.core.utils.*; import org.dromara.common.core.utils.*;
import org.dromara.common.encrypt.annotation.ApiEncrypt; import org.dromara.common.encrypt.annotation.ApiEncrypt;
import org.dromara.common.json.utils.JsonUtils; import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.ratelimiter.annotation.RateLimiter;
import org.dromara.common.ratelimiter.enums.LimitType;
import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.social.config.properties.SocialLoginConfigProperties; import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
import org.dromara.common.social.config.properties.SocialProperties; import org.dromara.common.social.config.properties.SocialProperties;
@ -200,7 +198,6 @@ public class AuthController {
* *
* @return 租户列表 * @return 租户列表
*/ */
@RateLimiter(time = 60, count = 20, limitType = LimitType.IP)
@GetMapping("/tenant/list") @GetMapping("/tenant/list")
public R<LoginTenantVo> tenantList(HttpServletRequest request) throws Exception { public R<LoginTenantVo> tenantList(HttpServletRequest request) throws Exception {
// 返回对象 // 返回对象

View File

@ -131,18 +131,15 @@ public class CaptchaController {
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid; String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
// 生成验证码 // 生成验证码
CaptchaType captchaType = captchaProperties.getType(); CaptchaType captchaType = captchaProperties.getType();
CodeGenerator codeGenerator; boolean isMath = CaptchaType.MATH == captchaType;
if (CaptchaType.MATH == captchaType) { Integer length = isMath ? captchaProperties.getNumberLength() : captchaProperties.getCharLength();
codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getNumberLength(), false); CodeGenerator codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), length);
} else {
codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getCharLength());
}
AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz()); AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz());
captcha.setGenerator(codeGenerator); captcha.setGenerator(codeGenerator);
captcha.createCode(); captcha.createCode();
// 如果是数学验证码使用SpEL表达式处理验证码结果 // 如果是数学验证码使用SpEL表达式处理验证码结果
String code = captcha.getCode(); String code = captcha.getCode();
if (CaptchaType.MATH == captchaType) { if (isMath) {
ExpressionParser parser = new SpelExpressionParser(); ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(StringUtils.remove(code, "=")); Expression exp = parser.parseExpression(StringUtils.remove(code, "="));
code = exp.getValue(String.class); code = exp.getValue(String.class);

View File

@ -1,8 +1,9 @@
package org.dromara.web.listener; package org.dromara.web.listener;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.listener.SaTokenListener; import cn.dev33.satoken.listener.SaTokenListener;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.hutool.http.useragent.UserAgent; import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil; import cn.hutool.http.useragent.UserAgentUtil;
@ -34,13 +35,14 @@ import java.time.Duration;
@Slf4j @Slf4j
public class UserActionListener implements SaTokenListener { public class UserActionListener implements SaTokenListener {
private final SaTokenConfig tokenConfig;
private final SysLoginService loginService; private final SysLoginService loginService;
/** /**
* 每次登录时触发 * 每次登录时触发
*/ */
@Override @Override
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) { public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent")); UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = ServletUtils.getClientIP(); String ip = ServletUtils.getClientIP();
UserOnlineDTO dto = new UserOnlineDTO(); UserOnlineDTO dto = new UserOnlineDTO();
@ -50,17 +52,17 @@ public class UserActionListener implements SaTokenListener {
dto.setOs(userAgent.getOs().getName()); dto.setOs(userAgent.getOs().getName());
dto.setLoginTime(System.currentTimeMillis()); dto.setLoginTime(System.currentTimeMillis());
dto.setTokenId(tokenValue); dto.setTokenId(tokenValue);
String username = (String) loginParameter.getExtra(LoginHelper.USER_NAME_KEY); String username = (String) loginModel.getExtra(LoginHelper.USER_NAME_KEY);
String tenantId = (String) loginParameter.getExtra(LoginHelper.TENANT_KEY); String tenantId = (String) loginModel.getExtra(LoginHelper.TENANT_KEY);
dto.setUserName(username); dto.setUserName(username);
dto.setClientKey((String) loginParameter.getExtra(LoginHelper.CLIENT_KEY)); dto.setClientKey((String) loginModel.getExtra(LoginHelper.CLIENT_KEY));
dto.setDeviceType(loginParameter.getDeviceType()); dto.setDeviceType(loginModel.getDevice());
dto.setDeptName((String) loginParameter.getExtra(LoginHelper.DEPT_NAME_KEY)); dto.setDeptName((String) loginModel.getExtra(LoginHelper.DEPT_NAME_KEY));
TenantHelper.dynamic(tenantId, () -> { TenantHelper.dynamic(tenantId, () -> {
if(loginParameter.getTimeout() == -1) { if(tokenConfig.getTimeout() == -1) {
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto); RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto);
} else { } else {
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto, Duration.ofSeconds(loginParameter.getTimeout())); RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, dto, Duration.ofSeconds(tokenConfig.getTimeout()));
} }
}); });
// 记录登录日志 // 记录登录日志
@ -72,7 +74,7 @@ public class UserActionListener implements SaTokenListener {
logininforEvent.setRequest(ServletUtils.getRequest()); logininforEvent.setRequest(ServletUtils.getRequest());
SpringUtils.context().publishEvent(logininforEvent); SpringUtils.context().publishEvent(logininforEvent);
// 更新登录信息 // 更新登录信息
loginService.recordLoginInfo((Long) loginParameter.getExtra(LoginHelper.USER_KEY), ip); loginService.recordLoginInfo((Long) loginModel.getExtra(LoginHelper.USER_KEY), ip);
log.info("user doLogin, userId:{}, token:{}", loginId, tokenValue); log.info("user doLogin, userId:{}, token:{}", loginId, tokenValue);
} }
@ -158,6 +160,6 @@ public class UserActionListener implements SaTokenListener {
* 每次Token续期时触发 * 每次Token续期时触发
*/ */
@Override @Override
public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) { public void doRenewTimeout(String tokenValue, Object loginId, long timeout) {
} }
} }

View File

@ -1,6 +1,6 @@
package org.dromara.web.service; package org.dromara.web.service;
import cn.hutool.crypto.digest.BCrypt; import cn.dev33.satoken.secure.BCrypt;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.Constants;
@ -87,7 +87,7 @@ public class SysRegisterService {
recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")); recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
throw new CaptchaExpireException(); throw new CaptchaExpireException();
} }
if (!StringUtils.equalsIgnoreCase(code, captcha)) { if (!code.equalsIgnoreCase(captcha)) {
recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")); recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
throw new CaptchaException(); throw new CaptchaException();
} }

View File

@ -1,7 +1,7 @@
package org.dromara.web.service.impl; package org.dromara.web.service.impl;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -58,8 +58,8 @@ public class EmailAuthStrategy implements IAuthStrategy {
}); });
loginUser.setClientKey(client.getClientKey()); loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType()); loginUser.setDeviceType(client.getDeviceType());
SaLoginParameter model = new SaLoginParameter(); SaLoginModel model = new SaLoginModel();
model.setDeviceType(client.getDeviceType()); model.setDevice(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置 // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期 // 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout()); model.setTimeout(client.getTimeout());

View File

@ -1,9 +1,9 @@
package org.dromara.web.service.impl; package org.dromara.web.service.impl;
import cn.dev33.satoken.secure.BCrypt;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.digest.BCrypt;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -70,8 +70,8 @@ public class PasswordAuthStrategy implements IAuthStrategy {
}); });
loginUser.setClientKey(client.getClientKey()); loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType()); loginUser.setDeviceType(client.getDeviceType());
SaLoginParameter model = new SaLoginParameter(); SaLoginModel model = new SaLoginModel();
model.setDeviceType(client.getDeviceType()); model.setDevice(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置 // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期 // 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout()); model.setTimeout(client.getTimeout());
@ -102,7 +102,7 @@ public class PasswordAuthStrategy implements IAuthStrategy {
loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")); loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
throw new CaptchaExpireException(); throw new CaptchaExpireException();
} }
if (!StringUtils.equalsIgnoreCase(code, captcha)) { if (!code.equalsIgnoreCase(captcha)) {
loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")); loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
throw new CaptchaException(); throw new CaptchaException();
} }

View File

@ -1,7 +1,7 @@
package org.dromara.web.service.impl; package org.dromara.web.service.impl;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -58,8 +58,8 @@ public class SmsAuthStrategy implements IAuthStrategy {
}); });
loginUser.setClientKey(client.getClientKey()); loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType()); loginUser.setDeviceType(client.getDeviceType());
SaLoginParameter model = new SaLoginParameter(); SaLoginModel model = new SaLoginModel();
model.setDeviceType(client.getDeviceType()); model.setDevice(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置 // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期 // 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout()); model.setTimeout(client.getTimeout());

View File

@ -1,9 +1,12 @@
package org.dromara.web.service.impl; package org.dromara.web.service.impl;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.http.Method;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthResponse; import me.zhyd.oauth.model.AuthResponse;
@ -65,6 +68,15 @@ public class SocialAuthStrategy implements IAuthStrategy {
throw new ServiceException(response.getMsg()); throw new ServiceException(response.getMsg());
} }
AuthUser authUserData = response.getData(); AuthUser authUserData = response.getData();
if ("GITEE".equals(authUserData.getSource())) {
// 如用户使用 gitee 登录顺手 star 给作者一点支持 拒绝白嫖
HttpUtil.createRequest(Method.PUT, "https://gitee.com/api/v5/user/starred/dromara/RuoYi-Vue-Plus")
.formStr(MapUtil.of("access_token", authUserData.getToken().getAccessToken()))
.executeAsync();
HttpUtil.createRequest(Method.PUT, "https://gitee.com/api/v5/user/starred/dromara/RuoYi-Cloud-Plus")
.formStr(MapUtil.of("access_token", authUserData.getToken().getAccessToken()))
.executeAsync();
}
List<SysSocialVo> list = sysSocialService.selectByAuthId(authUserData.getSource() + authUserData.getUuid()); List<SysSocialVo> list = sysSocialService.selectByAuthId(authUserData.getSource() + authUserData.getUuid());
if (CollUtil.isEmpty(list)) { if (CollUtil.isEmpty(list)) {
@ -87,8 +99,8 @@ public class SocialAuthStrategy implements IAuthStrategy {
}); });
loginUser.setClientKey(client.getClientKey()); loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType()); loginUser.setDeviceType(client.getDeviceType());
SaLoginParameter model = new SaLoginParameter(); SaLoginModel model = new SaLoginModel();
model.setDeviceType(client.getDeviceType()); model.setDevice(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置 // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期 // 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout()); model.setTimeout(client.getTimeout());

View File

@ -1,7 +1,7 @@
package org.dromara.web.service.impl; package org.dromara.web.service.impl;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -76,8 +76,8 @@ public class XcxAuthStrategy implements IAuthStrategy {
loginUser.setDeviceType(client.getDeviceType()); loginUser.setDeviceType(client.getDeviceType());
loginUser.setOpenid(openid); loginUser.setOpenid(openid);
SaLoginParameter model = new SaLoginParameter(); SaLoginModel model = new SaLoginModel();
model.setDeviceType(client.getDeviceType()); model.setDevice(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置 // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期 // 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout()); model.setTimeout(client.getTimeout());

View File

@ -27,6 +27,8 @@ snail-job:
port: 2${server.port} port: 2${server.port}
# 客户端ip指定 # 客户端ip指定
host: host:
# RPC类型: netty, grpc
rpc-type: grpc
--- # 数据源配置 --- # 数据源配置
spring: spring:
@ -261,10 +263,3 @@ justauth:
client-id: 10**********6 client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=gitlab redirect-uri: ${justauth.address}/social-callback?source=gitlab
gitea:
# 前端改动 https://gitee.com/JavaLionLi/plus-ui/pulls/204
# gitea 服务器地址
server-url: https://demo.gitea.com
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=gitea

View File

@ -30,6 +30,8 @@ snail-job:
port: 2${server.port} port: 2${server.port}
# 客户端ip指定 # 客户端ip指定
host: host:
# RPC类型: netty, grpc
rpc-type: grpc
--- # 数据源配置 --- # 数据源配置
spring: spring:
@ -263,10 +265,3 @@ justauth:
client-id: 10**********6 client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=gitlab redirect-uri: ${justauth.address}/social-callback?source=gitlab
gitea:
# 前端改动 https://gitee.com/JavaLionLi/plus-ui/pulls/204
# gitea 服务器地址
server-url: https://demo.gitea.com
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=gitea

View File

@ -21,8 +21,8 @@ server:
worker: 256 worker: 256
captcha: captcha:
# 是否启用验证码校验
enable: true enable: true
# 页面 <参数设置> 可开启关闭 验证码校验
# 验证码类型 math 数组计算 char 字符验证 # 验证码类型 math 数组计算 char 字符验证
type: MATH type: MATH
# line 线段干扰 circle 圆圈干扰 shear 扭曲干扰 # line 线段干扰 circle 圆圈干扰 shear 扭曲干扰
@ -57,13 +57,6 @@ spring:
# 开启虚拟线程 仅jdk21可用 # 开启虚拟线程 仅jdk21可用
virtual: virtual:
enabled: false enabled: false
task:
execution:
# 从 springboot 3.5 开始 spring自带线程池
# 不再需要 AsyncConfig与ThreadPoolConfig 可直接注入线程池使用
thread-name-prefix: async-
# 由spring自己初始化线程池
mode: force
# 资源信息 # 资源信息
messages: messages:
# 国际化资源文件路径 # 国际化资源文件路径
@ -117,7 +110,7 @@ security:
- /error - /error
- /*/api-docs - /*/api-docs
- /*/api-docs/** - /*/api-docs/**
- /warm-flow-ui/config - /warm-flow-ui/token-name
# 多租户配置 # 多租户配置
tenant: tenant:
@ -134,7 +127,6 @@ tenant:
- sys_user_role - sys_user_role
- sys_client - sys_client
- sys_oss_config - sys_oss_config
- flow_spel
# MyBatisPlus配置 # MyBatisPlus配置
# https://baomidou.com/config/ # https://baomidou.com/config/
@ -185,18 +177,28 @@ springdoc:
api-docs: api-docs:
# 是否开启接口文档 # 是否开启接口文档
enabled: true enabled: true
# swagger-ui:
# # 持久化认证数据
# persistAuthorization: true
info: info:
# 标题 # 标题
title: '标题RuoYi-Vue-Plus多租户管理系统_接口文档' title: '标题RuoYi-Vue-Plus多租户管理系统_接口文档'
# 描述 # 描述
description: '描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...' description: '描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...'
# 版本 # 版本
version: '版本号: ${project.version}' version: '版本号: ${ruoyi.version}'
# 作者信息 # 作者信息
contact: contact:
name: Lion Li name: Lion Li
email: crazylionli@163.com email: crazylionli@163.com
url: https://gitee.com/dromara/RuoYi-Vue-Plus url: https://gitee.com/dromara/RuoYi-Vue-Plus
components:
# 鉴权方式配置
security-schemes:
apiKey:
type: APIKEY
in: HEADER
name: ${sa-token.token-name}
#这里定义了两个分组,可定义多个,也可以不定义 #这里定义了两个分组,可定义多个,也可以不定义
group-configs: group-configs:
- group: 1.演示模块 - group: 1.演示模块
@ -214,10 +216,20 @@ springdoc:
xss: xss:
# 过滤开关 # 过滤开关
enabled: true enabled: true
# 排除链接 # 排除链接(多个用逗号分隔)
excludeUrls: excludeUrls:
- /system/notice - /system/notice
# 全局线程池相关配置
# 如使用JDK21请直接使用虚拟线程 不要开启此配置
thread-pool:
# 是否开启线程池
enabled: false
# 队列最大长度
queueCapacity: 128
# 线程池维护线程所允许的空闲时间
keepAliveSeconds: 300
--- # 分布式锁 lock4j 全局配置 --- # 分布式锁 lock4j 全局配置
lock4j: lock4j:
# 获取分布式锁超时时间,默认为 3000 毫秒 # 获取分布式锁超时时间,默认为 3000 毫秒
@ -257,9 +269,13 @@ warm-flow:
enabled: true enabled: true
# 是否开启设计器ui # 是否开启设计器ui
ui: true ui: true
# 是否显示流程图顶部文字
top-text-show: true
# 是否渲染节点悬浮提示默认true
node-tooltip: true
# 默认Authorization如果有多个token用逗号分隔 # 默认Authorization如果有多个token用逗号分隔
token-name: ${sa-token.token-name},clientid token-name: ${sa-token.token-name},clientid
# 流程状态对应的三元色
chart-status-color:
## 未办理
- 62,62,62
## 待办理
- 255,205,23
## 已办理
- 157,255,0

View File

@ -17,7 +17,6 @@ user.username.length.valid=账户长度必须在{min}到{max}个字符之间
user.password.not.blank=用户密码不能为空 user.password.not.blank=用户密码不能为空
user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间 user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间
user.password.not.valid=* 5-50个字符 user.password.not.valid=* 5-50个字符
user.password.format.valid=密码必须包含大写字母、小写字母、数字和特殊字符
user.email.not.valid=邮箱格式错误 user.email.not.valid=邮箱格式错误
user.email.not.blank=邮箱不能为空 user.email.not.blank=邮箱不能为空
user.phonenumber.not.blank=用户手机号不能为空 user.phonenumber.not.blank=用户手机号不能为空

View File

@ -17,7 +17,6 @@ user.username.length.valid=Account length must be between {min} and {max} charac
user.password.not.blank=Password cannot be empty user.password.not.blank=Password cannot be empty
user.password.length.valid=Password length must be between {min} and {max} characters user.password.length.valid=Password length must be between {min} and {max} characters
user.password.not.valid=* 5-50 characters user.password.not.valid=* 5-50 characters
user.password.format.valid=Password must contain uppercase, lowercase, digit, and special character
user.email.not.valid=Mailbox format error user.email.not.valid=Mailbox format error
user.email.not.blank=Mailbox cannot be blank user.email.not.blank=Mailbox cannot be blank
user.phonenumber.not.blank=Phone number cannot be blank user.phonenumber.not.blank=Phone number cannot be blank

View File

@ -17,7 +17,6 @@ user.username.length.valid=账户长度必须在{min}到{max}个字符之间
user.password.not.blank=用户密码不能为空 user.password.not.blank=用户密码不能为空
user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间 user.password.length.valid=用户密码长度必须在{min}到{max}个字符之间
user.password.not.valid=* 5-50个字符 user.password.not.valid=* 5-50个字符
user.password.format.valid=密码必须包含大写字母、小写字母、数字和特殊字符
user.email.not.valid=邮箱格式错误 user.email.not.valid=邮箱格式错误
user.email.not.blank=邮箱不能为空 user.email.not.blank=邮箱不能为空
user.phonenumber.not.blank=用户手机号不能为空 user.phonenumber.not.blank=用户手机号不能为空

View File

@ -0,0 +1 @@
3f2ee348-0303-40ca-bf03-03f48d2d2141

Binary file not shown.

View File

@ -0,0 +1,4 @@
3
SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-iso10646-1
SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-iso8859-1
SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-koi8-r

View File

@ -0,0 +1,4 @@
3
SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-iso10646-1
SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-iso8859-1
SIMSUN.TTC -misc-simsun-medium-r-normal--0-0-0-0-p-0-koi8-r

View File

@ -14,7 +14,7 @@
</description> </description>
<properties> <properties>
<revision>5.4.1</revision> <revision>5.3.1</revision>
</properties> </properties>
<dependencyManagement> <dependencyManagement>

View File

@ -0,0 +1,52 @@
package org.dromara.common.core.config;
import cn.hutool.core.util.ArrayUtil;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.core.task.VirtualThreadTaskExecutor;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import java.util.Arrays;
import java.util.concurrent.Executor;
/**
* 异步配置
* <p>
* 如果未使用虚拟线程则生效
*
* @author Lion Li
*/
@AutoConfiguration
public class AsyncConfig implements AsyncConfigurer {
/**
* 自定义 @Async 注解使用系统线程池
*/
@Override
public Executor getAsyncExecutor() {
if(SpringUtils.isVirtual()) {
return new VirtualThreadTaskExecutor("async-");
}
return SpringUtils.getBean("scheduledExecutorService");
}
/**
* 异步执行异常处理
*/
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, objects) -> {
throwable.printStackTrace();
StringBuilder sb = new StringBuilder();
sb.append("Exception message - ").append(throwable.getMessage())
.append(", Method name - ").append(method.getName());
if (ArrayUtil.isNotEmpty(objects)) {
sb.append(", Parameter value - ").append(Arrays.toString(objects));
}
throw new ServiceException(sb.toString());
};
}
}

View File

@ -7,9 +7,11 @@ import org.dromara.common.core.config.properties.ThreadPoolProperties;
import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.Threads; import org.dromara.common.core.utils.Threads;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.core.task.VirtualThreadTaskExecutor; import org.springframework.core.task.VirtualThreadTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ScheduledThreadPoolExecutor;
@ -32,6 +34,18 @@ public class ThreadPoolConfig {
private ScheduledExecutorService scheduledExecutorService; private ScheduledExecutorService scheduledExecutorService;
@Bean(name = "threadPoolTaskExecutor")
@ConditionalOnProperty(prefix = "thread-pool", name = "enabled", havingValue = "true")
public ThreadPoolTaskExecutor threadPoolTaskExecutor(ThreadPoolProperties threadPoolProperties) {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(core);
executor.setMaxPoolSize(core * 2);
executor.setQueueCapacity(threadPoolProperties.getQueueCapacity());
executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds());
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
/** /**
* 执行周期性或定时任务 * 执行周期性或定时任务
*/ */

View File

@ -3,14 +3,13 @@ package org.dromara.common.core.constant;
/** /**
* 缓存组名称常量 * 缓存组名称常量
* <p> * <p>
* key 格式为 cacheNames#ttl#maxIdleTime#maxSize#local * key 格式为 cacheNames#ttl#maxIdleTime#maxSize
* <p> * <p>
* ttl 过期时间 如果设置为0则不过期 默认为0 * ttl 过期时间 如果设置为0则不过期 默认为0
* maxIdleTime 最大空闲时间 根据LRU算法清理空闲数据 如果设置为0则不检测 默认为0 * maxIdleTime 最大空闲时间 根据LRU算法清理空闲数据 如果设置为0则不检测 默认为0
* maxSize 组最大长度 根据LRU算法清理溢出数据 如果设置为0则无限长 默认为0 * maxSize 组最大长度 根据LRU算法清理溢出数据 如果设置为0则无限长 默认为0
* local 默认开启本地缓存为1 关闭本地缓存为0
* <p> * <p>
* 例子: test#60s、test#0#60s、test#0#1m#1000、test#1h#0#500、test#1h#0#500#0 * 例子: test#60s、test#0#60s、test#0#1m#1000、test#1h#0#500
* *
* @author Lion Li * @author Lion Li
*/ */

View File

@ -72,10 +72,5 @@ public interface Constants {
*/ */
Long TOP_PARENT_ID = 0L; Long TOP_PARENT_ID = 0L;
/**
* 加密头
*/
String ENCRYPT_HEADER = "ENC_";
} }

View File

@ -77,9 +77,4 @@ public interface SystemConstants {
*/ */
String ROOT_DEPT_ANCESTORS = "0"; String ROOT_DEPT_ANCESTORS = "0";
/**
* 默认部门 ID
*/
Long DEFAULT_DEPT_ID = 100L;
} }

View File

@ -50,11 +50,6 @@ public class CompleteTaskDTO implements Serializable {
*/ */
private String notice; private String notice;
/**
* 办理人(可不填 用于覆盖当前节点办理人)
*/
private String handler;
/** /**
* 流程变量 * 流程变量
*/ */

View File

@ -30,11 +30,6 @@ public class StartProcessDTO implements Serializable {
*/ */
private String flowCode; private String flowCode;
/**
* 办理人(可不填 用于覆盖当前节点办理人)
*/
private String handler;
/** /**
* 流程变量,前端会提交一个元素{'entity': {业务详情数据对象}} * 流程变量,前端会提交一个元素{'entity': {业务详情数据对象}}
*/ */

View File

@ -52,17 +52,17 @@ public class TaskAssigneeDTO implements Serializable {
*/ */
public static <T> List<TaskHandler> convertToHandlerList( public static <T> List<TaskHandler> convertToHandlerList(
List<T> sourceList, List<T> sourceList,
Function<T, String> storageId, Function<T, Long> storageId,
Function<T, String> handlerCode, Function<T, String> handlerCode,
Function<T, String> handlerName, Function<T, String> handlerName,
Function<T, String> groupName, Function<T, Long> groupName,
Function<T, Date> createTimeMapper) { Function<T, Date> createTimeMapper) {
return sourceList.stream() return sourceList.stream()
.map(item -> new TaskHandler( .map(item -> new TaskHandler(
storageId.apply(item), String.valueOf(storageId.apply(item)),
handlerCode.apply(item), handlerCode.apply(item),
handlerName.apply(item), handlerName.apply(item),
groupName.apply(item), groupName != null ? String.valueOf(groupName.apply(item)) : null,
createTimeMapper.apply(item) createTimeMapper.apply(item)
)).collect(Collectors.toList()); )).collect(Collectors.toList());
} }

View File

@ -0,0 +1,44 @@
package org.dromara.common.core.domain.event;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 流程创建任务监听
*
* @author may
*/
@Data
public class ProcessCreateTaskEvent implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 租户ID
*/
private String tenantId;
/**
* 流程定义编码
*/
private String flowCode;
/**
* 审批节点编码
*/
private String nodeCode;
/**
* 任务id
*/
private Long taskId;
/**
* 业务id
*/
private String businessId;
}

View File

@ -27,33 +27,13 @@ public class ProcessEvent implements Serializable {
*/ */
private String flowCode; private String flowCode;
/**
* 实例id
*/
private Long instanceId;
/** /**
* 业务id * 业务id
*/ */
private String businessId; private String businessId;
/** /**
* 节点类型0开始节点 1中间节点 2结束节点 3互斥网关 4并行网关 * 状态
*/
private Integer nodeType;
/**
* 流程节点编码
*/
private String nodeCode;
/**
* 流程节点名称
*/
private String nodeName;
/**
* 流程状态
*/ */
private String status; private String status;
@ -65,6 +45,6 @@ public class ProcessEvent implements Serializable {
/** /**
* 当为true时为申请人节点办理 * 当为true时为申请人节点办理
*/ */
private Boolean submit; private boolean submit;
} }

View File

@ -1,70 +0,0 @@
package org.dromara.common.core.domain.event;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Map;
/**
* 流程任务监听
*
* @author may
*/
@Data
public class ProcessTaskEvent implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 租户ID
*/
private String tenantId;
/**
* 流程定义编码
*/
private String flowCode;
/**
* 节点类型0开始节点 1中间节点 2结束节点 3互斥网关 4并行网关
*/
private Integer nodeType;
/**
* 流程节点编码
*/
private String nodeCode;
/**
* 流程节点名称
*/
private String nodeName;
/**
* 任务id
*/
private Long taskId;
/**
* 实例id
*/
private Long instanceId;
/**
* 业务id
*/
private String businessId;
/**
* 流程状态
*/
private String status;
/**
* 办理参数
*/
private Map<String, Object> params;
}

View File

@ -26,7 +26,6 @@ public class PasswordLoginBody extends LoginBody {
*/ */
@NotBlank(message = "{user.password.not.blank}") @NotBlank(message = "{user.password.not.blank}")
@Length(min = 5, max = 30, message = "{user.password.length.valid}") @Length(min = 5, max = 30, message = "{user.password.length.valid}")
// @Pattern(regexp = RegexConstants.PASSWORD, message = "{user.password.format.valid}")
private String password; private String password;
} }

View File

@ -18,20 +18,16 @@ public class RegisterBody extends LoginBody {
* 用户名 * 用户名
*/ */
@NotBlank(message = "{user.username.not.blank}") @NotBlank(message = "{user.username.not.blank}")
@Length(min = 2, max = 30, message = "{user.username.length.valid}") @Length(min = 2, max = 20, message = "{user.username.length.valid}")
private String username; private String username;
/** /**
* 用户密码 * 用户密码
*/ */
@NotBlank(message = "{user.password.not.blank}") @NotBlank(message = "{user.password.not.blank}")
@Length(min = 5, max = 30, message = "{user.password.length.valid}") @Length(min = 5, max = 20, message = "{user.password.length.valid}")
// @Pattern(regexp = RegexConstants.PASSWORD, message = "{user.password.format.valid}")
private String password; private String password;
/**
* 用户类型
*/
private String userType; private String userType;
} }

View File

@ -5,6 +5,7 @@ import lombok.Getter;
/** /**
* 设备类型 * 设备类型
* 针对一套 用户体系
* *
* @author Lion Li * @author Lion Li
*/ */
@ -28,12 +29,9 @@ public enum DeviceType {
XCX("xcx"), XCX("xcx"),
/** /**
* 第三方社交登录平台 * social第三方端
*/ */
SOCIAL("social"); SOCIAL("social");
/**
* 设备标识
*/
private final String device; private final String device;
} }

View File

@ -1,11 +1,12 @@
package org.dromara.common.core.enums; package org.dromara.common.core.enums;
import org.dromara.common.core.utils.StringUtils;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import org.dromara.common.core.utils.StringUtils;
/** /**
* 用户类型 * 设备类型
* 针对多套 用户体系
* *
* @author Lion Li * @author Lion Li
*/ */
@ -14,18 +15,15 @@ import org.dromara.common.core.utils.StringUtils;
public enum UserType { public enum UserType {
/** /**
* 后台系统用户 * pc端
*/ */
SYS_USER("sys_user"), SYS_USER("sys_user"),
/** /**
* 移动客户端用户 * app端
*/ */
APP_USER("app_user"); APP_USER("app_user");
/**
* 用户类型标识(用于 token、权限识别等
*/
private final String userType; private final String userType;
public static UserType getUserType(String str) { public static UserType getUserType(String str) {

View File

@ -1,15 +1,11 @@
package org.dromara.common.core.exception; package org.dromara.common.core.exception;
import cn.hutool.core.text.StrFormatter; import lombok.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.io.Serial; import java.io.Serial;
/** /**
* 业务异常(支持占位符 {} * 业务异常
* *
* @author ruoyi * @author ruoyi
*/ */
@ -46,10 +42,6 @@ public final class ServiceException extends RuntimeException {
this.code = code; this.code = code;
} }
public ServiceException(String message, Object... args) {
this.message = StrFormatter.format(message, args);
}
@Override @Override
public String getMessage() { public String getMessage() {
return message; return message;

View File

@ -3,7 +3,6 @@ package org.dromara.common.core.service;
import org.dromara.common.core.domain.dto.DeptDTO; import org.dromara.common.core.domain.dto.DeptDTO;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 通用 部门服务 * 通用 部门服务
@ -35,12 +34,4 @@ public interface DeptService {
*/ */
List<DeptDTO> selectDeptsByList(); List<DeptDTO> selectDeptsByList();
/**
* 根据部门 ID 列表查询部门名称映射关系
*
* @param deptIds 部门 ID 列表
* @return Map其中 key 为部门 IDvalue 为对应的部门名称
*/
Map<Long, String> selectDeptNamesByIds(List<Long> deptIds);
} }

View File

@ -1,28 +0,0 @@
package org.dromara.common.core.service;
import java.util.Set;
/**
* 用户权限处理
*
* @author Lion Li
*/
public interface PermissionService {
/**
* 获取角色数据权限
*
* @param userId 用户id
* @return 角色权限信息
*/
Set<String> getRolePermission(Long userId);
/**
* 获取菜单数据权限
*
* @param userId 用户id
* @return 菜单权限信息
*/
Set<String> getMenuPermission(Long userId);
}

View File

@ -1,8 +1,5 @@
package org.dromara.common.core.service; package org.dromara.common.core.service;
import java.util.List;
import java.util.Map;
/** /**
* 通用 岗位服务 * 通用 岗位服务
* *
@ -10,12 +7,4 @@ import java.util.Map;
*/ */
public interface PostService { public interface PostService {
/**
* 根据岗位 ID 列表查询岗位名称映射关系
*
* @param postIds 岗位 ID 列表
* @return Map其中 key 为岗位 IDvalue 为对应的岗位名称
*/
Map<Long, String> selectPostNamesByIds(List<Long> postIds);
} }

View File

@ -1,8 +1,5 @@
package org.dromara.common.core.service; package org.dromara.common.core.service;
import java.util.List;
import java.util.Map;
/** /**
* 通用 角色服务 * 通用 角色服务
* *
@ -10,12 +7,4 @@ import java.util.Map;
*/ */
public interface RoleService { public interface RoleService {
/**
* 根据角色 ID 列表查询角色名称映射关系
*
* @param roleIds 角色 ID 列表
* @return Map其中 key 为角色 IDvalue 为对应的角色名称
*/
Map<Long, String> selectRoleNamesByIds(List<Long> roleIds);
} }

View File

@ -3,7 +3,6 @@ package org.dromara.common.core.service;
import org.dromara.common.core.domain.dto.UserDTO; import org.dromara.common.core.domain.dto.UserDTO;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 通用 用户服务 * 通用 用户服务
@ -92,12 +91,4 @@ public interface UserService {
*/ */
List<UserDTO> selectUsersByPostIds(List<Long> postIds); List<UserDTO> selectUsersByPostIds(List<Long> postIds);
/**
* 根据用户 ID 列表查询用户名称映射关系
*
* @param userIds 用户 ID 列表
* @return Map其中 key 为用户 IDvalue 为对应的用户名称
*/
Map<Long, String> selectUserNamesByIds(List<Long> userIds);
} }

View File

@ -78,28 +78,9 @@ public interface WorkflowService {
/** /**
* 办理任务 * 办理任务
* 系统后台发起审批 无用户信息 需要忽略权限
* completeTask.getVariables().put("ignore", true);
* *
* @param completeTask 参数 * @param completeTask 参数
* @return 结果 * @return 结果
*/ */
boolean completeTask(CompleteTaskDTO completeTask); boolean completeTask(CompleteTaskDTO completeTask);
/**
* 办理任务
*
* @param taskId 任务ID
* @param message 办理意见
* @return 结果
*/
boolean completeTask(Long taskId, String message);
/**
* 启动流程并办理第一个任务
*
* @param startProcess 参数
* @return 结果
*/
boolean startCompleteTask(StartProcessDTO startProcess);
} }

View File

@ -175,27 +175,14 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
} }
/** /**
* 计算两个时间之间的时间差,并以指定单位返回(绝对值 * 计算两个日期之间的天数差(以毫秒为单位
* *
* @param start 起始时间 * @param date1 第一个日期
* @param end 结束时间 * @param date2 第二个日期
* @param unit 所需返回的时间单位DAYS、HOURS、MINUTES、SECONDS、MILLISECONDS、MICROSECONDS、NANOSECONDS * @return 两个日期之间的天数差的绝对值
* @return 时间差的绝对值,以指定单位表示
*/ */
public static long difference(Date start, Date end, TimeUnit unit) { public static int differentDaysByMillisecond(Date date1, Date date2) {
// 计算时间差,单位为毫秒,取绝对值避免负数 return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24)));
long diffInMillis = Math.abs(end.getTime() - start.getTime());
// 根据目标单位转换时间差
return switch (unit) {
case DAYS -> diffInMillis / TimeUnit.DAYS.toMillis(1);
case HOURS -> diffInMillis / TimeUnit.HOURS.toMillis(1);
case MINUTES -> diffInMillis / TimeUnit.MINUTES.toMillis(1);
case SECONDS -> diffInMillis / TimeUnit.SECONDS.toMillis(1);
case MILLISECONDS -> diffInMillis;
case MICROSECONDS -> TimeUnit.MILLISECONDS.toMicros(diffInMillis);
case NANOSECONDS -> TimeUnit.MILLISECONDS.toNanos(diffInMillis);
};
} }
/** /**
@ -293,7 +280,7 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
// 校验时间跨度不超过最大限制 // 校验时间跨度不超过最大限制
if (diff > maxValue) { if (diff > maxValue) {
throw new ServiceException("最大时间跨度为 {} {}", maxValue, unit.toString().toLowerCase()); throw new ServiceException("最大时间跨度为 " + maxValue + " " + unit.toString().toLowerCase());
} }
} }

View File

@ -1,84 +0,0 @@
package org.dromara.common.core.utils;
import cn.hutool.core.lang.PatternPool;
import cn.hutool.core.net.NetUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.regex.RegexUtils;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* 增强网络相关工具类
*
* @author 秋辞未寒
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class NetUtils extends NetUtil {
/**
* 判断是否为IPv6地址
*
* @param ip IP地址
* @return 是否为IPv6地址
*/
public static boolean isIPv6(String ip) {
try {
// 判断是否为IPv6地址
return InetAddress.getByName(ip) instanceof Inet6Address;
} catch (UnknownHostException e) {
return false;
}
}
/**
* 判断IPv6地址是否为内网地址
* <br><br>
* 以下地址将归类为本地地址,如有业务场景有需要,请根据需求自行处理:
* <pre>
* 通配符地址 0:0:0:0:0:0:0:0
* 链路本地地址 fe80::/10
* 唯一本地地址 fec0::/10
* 环回地址 ::1
* </pre>
*
* @param ip IP地址
* @return 是否为内网地址
*/
public static boolean isInnerIPv6(String ip) {
try {
// 判断是否为IPv6地址
if (InetAddress.getByName(ip) instanceof Inet6Address inet6Address) {
// isAnyLocalAddress 判断是否为通配符地址,通常不会将其视为内网地址,根据业务场景自行处理判断
// isLinkLocalAddress 判断是否为链路本地地址,通常不算内网地址,是否划分归属于内网需要根据业务场景自行处理判断
// isLoopbackAddress 判断是否为环回地址与IPv4的 127.0.0.1 同理,用于表示本机
// isSiteLocalAddress 判断是否为本地站点地址IPv6唯一本地地址Unique Local Addresses简称ULA
if (inet6Address.isAnyLocalAddress()
|| inet6Address.isLinkLocalAddress()
|| inet6Address.isLoopbackAddress()
|| inet6Address.isSiteLocalAddress()) {
return true;
}
}
} catch (UnknownHostException e) {
// 注意isInnerIPv6方法和isIPv6方法的适用范围不同所以此处不能忽略其异常信息。
throw new IllegalArgumentException("Invalid IPv6 address!", e);
}
return false;
}
/**
* 判断是否为IPv4地址
*
* @param ip IP地址
* @return 是否为IPv4地址
*/
public static boolean isIPv4(String ip) {
return RegexUtils.isMatch(PatternPool.IPV4, ip);
}
}

View File

@ -115,7 +115,7 @@ public class ServletUtils extends JakartaServletUtil {
public static Map<String, String> getParamMap(ServletRequest request) { public static Map<String, String> getParamMap(ServletRequest request) {
Map<String, String> params = new HashMap<>(); Map<String, String> params = new HashMap<>();
for (Map.Entry<String, String[]> entry : getParams(request).entrySet()) { for (Map.Entry<String, String[]> entry : getParams(request).entrySet()) {
params.put(entry.getKey(), StringUtils.joinComma(entry.getValue())); params.put(entry.getKey(), StringUtils.join(entry.getValue(), StringUtils.SEPARATOR));
} }
return params; return params;
} }

View File

@ -7,6 +7,7 @@ import lombok.NoArgsConstructor;
import java.util.*; import java.util.*;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -30,10 +31,8 @@ public class StreamUtils {
if (CollUtil.isEmpty(collection)) { if (CollUtil.isEmpty(collection)) {
return CollUtil.newArrayList(); return CollUtil.newArrayList();
} }
return collection.stream()
.filter(function)
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题 // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
.collect(Collectors.toList()); return collection.stream().filter(function).collect(Collectors.toList());
} }
/** /**
@ -41,26 +40,13 @@ public class StreamUtils {
* *
* @param collection 需要查询的集合 * @param collection 需要查询的集合
* @param function 过滤方法 * @param function 过滤方法
* @return 找到符合条件的第一个元素,没有则返回 Optional.empty() * @return 找到符合条件的第一个元素,没有则返回null
*/ */
public static <E> Optional<E> findFirst(Collection<E> collection, Predicate<E> function) { public static <E> E findFirst(Collection<E> collection, Predicate<E> function) {
if (CollUtil.isEmpty(collection)) { if (CollUtil.isEmpty(collection)) {
return Optional.empty(); return null;
} }
return collection.stream() return collection.stream().filter(function).findFirst().orElse(null);
.filter(function)
.findFirst();
}
/**
* 找到流中满足条件的第一个元素值
*
* @param collection 需要查询的集合
* @param function 过滤方法
* @return 找到符合条件的第一个元素,没有则返回 null
*/
public static <E> E findFirstValue(Collection<E> collection, Predicate<E> function) {
return findFirst(collection,function).orElse(null);
} }
/** /**
@ -68,26 +54,13 @@ public class StreamUtils {
* *
* @param collection 需要查询的集合 * @param collection 需要查询的集合
* @param function 过滤方法 * @param function 过滤方法
* @return 找到符合条件的任意一个元素,没有则返回 Optional.empty() * @return 找到符合条件的任意一个元素,没有则返回null
*/ */
public static <E> Optional<E> findAny(Collection<E> collection, Predicate<E> function) { public static <E> Optional<E> findAny(Collection<E> collection, Predicate<E> function) {
if (CollUtil.isEmpty(collection)) { if (CollUtil.isEmpty(collection)) {
return Optional.empty(); return Optional.empty();
} }
return collection.stream() return collection.stream().filter(function).findAny();
.filter(function)
.findAny();
}
/**
* 找到流中任意一个满足条件的元素值
*
* @param collection 需要查询的集合
* @param function 过滤方法
* @return 找到符合条件的任意一个元素没有则返回null
*/
public static <E> E findAnyValue(Collection<E> collection, Predicate<E> function) {
return findAny(collection,function).orElse(null);
} }
/** /**
@ -113,10 +86,7 @@ public class StreamUtils {
if (CollUtil.isEmpty(collection)) { if (CollUtil.isEmpty(collection)) {
return StringUtils.EMPTY; return StringUtils.EMPTY;
} }
return collection.stream() return collection.stream().map(function).filter(Objects::nonNull).collect(Collectors.joining(delimiter));
.map(function)
.filter(Objects::nonNull)
.collect(Collectors.joining(delimiter));
} }
/** /**
@ -130,11 +100,8 @@ public class StreamUtils {
if (CollUtil.isEmpty(collection)) { if (CollUtil.isEmpty(collection)) {
return CollUtil.newArrayList(); return CollUtil.newArrayList();
} }
return collection.stream()
.filter(Objects::nonNull)
.sorted(comparing)
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题 // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
.collect(Collectors.toList()); return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList());
} }
/** /**
@ -151,9 +118,7 @@ public class StreamUtils {
if (CollUtil.isEmpty(collection)) { if (CollUtil.isEmpty(collection)) {
return MapUtil.newHashMap(); return MapUtil.newHashMap();
} }
return collection.stream() return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
.filter(Objects::nonNull)
.collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
} }
/** /**
@ -172,25 +137,7 @@ public class StreamUtils {
if (CollUtil.isEmpty(collection)) { if (CollUtil.isEmpty(collection)) {
return MapUtil.newHashMap(); return MapUtil.newHashMap();
} }
return collection.stream() return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, value, (l, r) -> l));
.filter(Objects::nonNull)
.collect(Collectors.toMap(key, value, (l, r) -> l));
}
/**
* 获取 map 中的数据作为新 Map 的 value key 不变
* @param map 需要处理的map
* @param take 取值函数
* @param <K> map中的key类型
* @param <E> map中的value类型
* @param <V> 新map中的value类型
* @return 新的map
*/
public static <K, E, V> Map<K, V> toMap(Map<K, E> map, BiFunction<K, E, V> take) {
if (CollUtil.isEmpty(map)) {
return MapUtil.newHashMap();
}
return toMap(map.entrySet(), Map.Entry::getKey, entry -> take.apply(entry.getKey(), entry.getValue()));
} }
/** /**
@ -207,8 +154,8 @@ public class StreamUtils {
if (CollUtil.isEmpty(collection)) { if (CollUtil.isEmpty(collection)) {
return MapUtil.newHashMap(); return MapUtil.newHashMap();
} }
return collection.stream() return collection
.filter(Objects::nonNull) .stream().filter(Objects::nonNull)
.collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList())); .collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList()));
} }
@ -228,8 +175,8 @@ public class StreamUtils {
if (CollUtil.isEmpty(collection)) { if (CollUtil.isEmpty(collection)) {
return MapUtil.newHashMap(); return MapUtil.newHashMap();
} }
return collection.stream() return collection
.filter(Objects::nonNull) .stream().filter(Objects::nonNull)
.collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList()))); .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList())));
} }
@ -246,11 +193,11 @@ public class StreamUtils {
* @return 分类后的map * @return 分类后的map
*/ */
public static <E, T, U> Map<T, Map<U, E>> group2Map(Collection<E> collection, Function<E, T> key1, Function<E, U> key2) { public static <E, T, U> Map<T, Map<U, E>> group2Map(Collection<E> collection, Function<E, T> key1, Function<E, U> key2) {
if (CollUtil.isEmpty(collection)) { if (CollUtil.isEmpty(collection) || key1 == null || key2 == null) {
return MapUtil.newHashMap(); return MapUtil.newHashMap();
} }
return collection.stream() return collection
.filter(Objects::nonNull) .stream().filter(Objects::nonNull)
.collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l))); .collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l)));
} }
@ -268,7 +215,8 @@ public class StreamUtils {
if (CollUtil.isEmpty(collection)) { if (CollUtil.isEmpty(collection)) {
return CollUtil.newArrayList(); return CollUtil.newArrayList();
} }
return collection.stream() return collection
.stream()
.map(function) .map(function)
.filter(Objects::nonNull) .filter(Objects::nonNull)
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题 // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
@ -286,10 +234,11 @@ public class StreamUtils {
* @return 转化后的Set * @return 转化后的Set
*/ */
public static <E, T> Set<T> toSet(Collection<E> collection, Function<E, T> function) { public static <E, T> Set<T> toSet(Collection<E> collection, Function<E, T> function) {
if (CollUtil.isEmpty(collection)) { if (CollUtil.isEmpty(collection) || function == null) {
return CollUtil.newHashSet(); return CollUtil.newHashSet();
} }
return collection.stream() return collection
.stream()
.map(function) .map(function)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
@ -309,20 +258,26 @@ public class StreamUtils {
* @return 合并后的map * @return 合并后的map
*/ */
public static <K, X, Y, V> Map<K, V> merge(Map<K, X> map1, Map<K, Y> map2, BiFunction<X, Y, V> merge) { public static <K, X, Y, V> Map<K, V> merge(Map<K, X> map1, Map<K, Y> map2, BiFunction<X, Y, V> merge) {
if (CollUtil.isEmpty(map1) && CollUtil.isEmpty(map2)) { if (MapUtil.isEmpty(map1) && MapUtil.isEmpty(map2)) {
// 如果两个 map 都为空,则直接返回空的 map
return MapUtil.newHashMap(); return MapUtil.newHashMap();
} else if (CollUtil.isEmpty(map1)) { } else if (MapUtil.isEmpty(map1)) {
// 如果 map1 为空,则直接处理返回 map2 map1 = MapUtil.newHashMap();
return toMap(map2.entrySet(), Map.Entry::getKey, entry -> merge.apply(null, entry.getValue())); } else if (MapUtil.isEmpty(map2)) {
} else if (CollUtil.isEmpty(map2)) { map2 = MapUtil.newHashMap();
// 如果 map2 为空,则直接处理返回 map1
return toMap(map1.entrySet(), Map.Entry::getKey, entry -> merge.apply(entry.getValue(), null));
} }
Set<K> keySet = new HashSet<>(); Set<K> key = new HashSet<>();
keySet.addAll(map1.keySet()); key.addAll(map1.keySet());
keySet.addAll(map2.keySet()); key.addAll(map2.keySet());
return toMap(keySet, key -> key, key -> merge.apply(map1.get(key), map2.get(key))); Map<K, V> map = new HashMap<>();
for (K t : key) {
X x = map1.get(t);
Y y = map2.get(t);
V z = merge.apply(x, y);
if (z != null) {
map.put(t, z);
}
}
return map;
} }
} }

View File

@ -6,7 +6,6 @@ import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import org.springframework.util.AntPathMatcher; import org.springframework.util.AntPathMatcher;
import java.nio.charset.Charset;
import java.util.*; import java.util.*;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -260,13 +259,13 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils {
if (s != null) { if (s != null) {
final int len = s.length(); final int len = s.length();
if (s.length() <= size) { if (s.length() <= size) {
sb.append(Convert.toStr(c).repeat(size - len)); sb.append(String.valueOf(c).repeat(size - len));
sb.append(s); sb.append(s);
} else { } else {
return s.substring(len - size, len); return s.substring(len - size, len);
} }
} else { } else {
sb.append(Convert.toStr(c).repeat(Math.max(0, size))); sb.append(String.valueOf(c).repeat(Math.max(0, size)));
} }
return sb.toString(); return sb.toString();
} }
@ -340,45 +339,4 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils {
return false; return false;
} }
/**
* 将字符串从源字符集转换为目标字符集
*
* @param input 原始字符串
* @param fromCharset 源字符集
* @param toCharset 目标字符集
* @return 转换后的字符串
*/
public static String convert(String input, Charset fromCharset, Charset toCharset) {
if (isBlank(input)) {
return input;
}
try {
// 从源字符集获取字节
byte[] bytes = input.getBytes(fromCharset);
// 使用目标字符集解码
return new String(bytes, toCharset);
} catch (Exception e) {
return input;
}
}
/**
* 将可迭代对象中的元素使用逗号拼接成字符串
*
* @param iterable 可迭代对象,如 List、Set 等
* @return 拼接后的字符串
*/
public static String joinComma(Iterable<?> iterable) {
return StringUtils.join(iterable, SEPARATOR);
}
/**
* 将数组中的元素使用逗号拼接成字符串
*
* @param array 任意类型的数组
* @return 拼接后的字符串
*/
public static String joinComma(Object[] array) {
return StringUtils.join(array, SEPARATOR);
}
} }

View File

@ -10,8 +10,6 @@ import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.reflect.ReflectUtils; import org.dromara.common.core.utils.reflect.ReflectUtils;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -62,31 +60,6 @@ public class TreeBuildUtils extends TreeUtil {
return TreeUtil.build(list, parentId, DEFAULT_CONFIG, nodeParser); return TreeUtil.build(list, parentId, DEFAULT_CONFIG, nodeParser);
} }
/**
* 构建多根节点的树结构(支持多个顶级节点)
*
* @param list 原始数据列表
* @param getId 获取节点 ID 的方法引用例如node -> node.getId()
* @param getParentId 获取节点父级 ID 的方法引用例如node -> node.getParentId()
* @param parser 树节点属性映射器,用于将原始节点 T 转为 Tree 节点
* @param <T> 原始数据类型如实体类、DTO 等)
* @param <K> 节点 ID 类型(如 Long、String
* @return 构建完成的树形结构(可能包含多个顶级根节点)
*/
public static <T, K> List<Tree<K>> buildMultiRoot(List<T> list, Function<T, K> getId, Function<T, K> getParentId, NodeParser<T, K> parser) {
if (CollUtil.isEmpty(list)) {
return CollUtil.newArrayList();
}
Set<K> rootParentIds = StreamUtils.toSet(list, getParentId);
rootParentIds.removeAll(StreamUtils.toSet(list, getId));
// 构建每一个根 parentId 下的树,并合并成最终结果列表
return rootParentIds.stream()
.flatMap(rootParentId -> TreeUtil.build(list, rootParentId, parser).stream())
.collect(Collectors.toList());
}
/** /**
* 获取节点列表中所有节点的叶子节点 * 获取节点列表中所有节点的叶子节点
* *

View File

@ -1,11 +1,11 @@
package org.dromara.common.core.utils.ip; package org.dromara.common.core.utils.ip;
import cn.hutool.core.net.NetUtil;
import cn.hutool.http.HtmlUtil; import cn.hutool.http.HtmlUtil;
import org.dromara.common.core.utils.StringUtils;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.NetUtils;
import org.dromara.common.core.utils.StringUtils;
/** /**
* 获取地址类 * 获取地址类
@ -16,55 +16,18 @@ import org.dromara.common.core.utils.StringUtils;
@NoArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PRIVATE)
public class AddressUtils { public class AddressUtils {
// 未知IP
public static final String UNKNOWN_IP = "XX XX";
// 内网地址
public static final String LOCAL_ADDRESS = "内网IP";
// 未知地址 // 未知地址
public static final String UNKNOWN_ADDRESS = "未知"; public static final String UNKNOWN = "XX XX";
public static String getRealAddressByIP(String ip) { public static String getRealAddressByIP(String ip) {
// 处理空串并过滤HTML标签 if (StringUtils.isBlank(ip)) {
ip = HtmlUtil.cleanHtmlTag(StringUtils.blankToDefault(ip,"")); return UNKNOWN;
// 判断是否为IPv4
if (NetUtils.isIPv4(ip)) {
return resolverIPv4Region(ip);
} }
// 判断是否为IPv6
if (NetUtils.isIPv6(ip)) {
return resolverIPv6Region(ip);
}
// 如果不是IPv4或IPv6则返回未知IP
return UNKNOWN_IP;
}
/**
* 根据IPv4地址查询IP归属行政区域
* @param ip ipv4地址
* @return 归属行政区域
*/
private static String resolverIPv4Region(String ip){
// 内网不查询 // 内网不查询
if (NetUtils.isInnerIP(ip)) { ip = StringUtils.contains(ip, "0:0:0:0:0:0:0:1") ? "127.0.0.1" : HtmlUtil.cleanHtmlTag(ip);
return LOCAL_ADDRESS; if (NetUtil.isInnerIP(ip)) {
return "内网IP";
} }
return RegionUtils.getCityInfo(ip); return RegionUtils.getCityInfo(ip);
} }
/**
* 根据IPv6地址查询IP归属行政区域
* @param ip ipv6地址
* @return 归属行政区域
*/
private static String resolverIPv6Region(String ip){
// 内网不查询
if (NetUtils.isInnerIPv6(ip)) {
return LOCAL_ADDRESS;
}
log.warn("ip2region不支持IPV6地址解析{}", ip);
// 不支持IPv6不再进行没有必要的IP地址信息的解析直接返回
// 如有需要可自行实现IPv6地址信息解析逻辑并在这里返回
return UNKNOWN_ADDRESS;
}
} }

View File

@ -1,12 +1,15 @@
package org.dromara.common.core.utils.ip; package org.dromara.common.core.utils.ip;
import cn.hutool.core.io.resource.NoResourceException; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ResourceUtil; import cn.hutool.core.io.resource.ClassPathResource;
import lombok.extern.slf4j.Slf4j; import cn.hutool.core.util.ObjectUtil;
import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.file.FileUtils;
import lombok.extern.slf4j.Slf4j;
import org.lionsoul.ip2region.xdb.Searcher; import org.lionsoul.ip2region.xdb.Searcher;
import java.io.File;
/** /**
* 根据ip地址定位工具类离线方式 * 根据ip地址定位工具类离线方式
* 参考地址:<a href="https://gitee.com/lionsoul/ip2region/tree/master/binding/java">集成 ip2region 实现离线IP地址定位库</a> * 参考地址:<a href="https://gitee.com/lionsoul/ip2region/tree/master/binding/java">集成 ip2region 实现离线IP地址定位库</a>
@ -16,19 +19,31 @@ import org.lionsoul.ip2region.xdb.Searcher;
@Slf4j @Slf4j
public class RegionUtils { public class RegionUtils {
// IP地址库文件名称
public static final String IP_XDB_FILENAME = "ip2region.xdb";
private static final Searcher SEARCHER; private static final Searcher SEARCHER;
static { static {
try { String fileName = "/ip2region.xdb";
// 1、将 ip2region 数据库文件 xdb 从 ClassPath 加载到内存。 File existFile = FileUtils.file(FileUtil.getTmpDir() + FileUtil.FILE_SEPARATOR + fileName);
// 2、基于加载到内存的 xdb 数据创建一个 Searcher 查询对象。 if (!FileUtils.exist(existFile)) {
SEARCHER = Searcher.newWithBuffer(ResourceUtil.readBytes(IP_XDB_FILENAME)); ClassPathResource fileStream = new ClassPathResource(fileName);
log.info("RegionUtils初始化成功加载IP地址库数据成功"); if (ObjectUtil.isEmpty(fileStream.getStream())) {
} catch (NoResourceException e) {
throw new ServiceException("RegionUtils初始化失败原因IP地址库数据不存在"); throw new ServiceException("RegionUtils初始化失败原因IP地址库数据不存在");
}
FileUtils.writeFromStream(fileStream.getStream(), existFile);
}
String dbPath = existFile.getPath();
// 1、从 dbPath 加载整个 xdb 到内存。
byte[] cBuff;
try {
cBuff = Searcher.loadContentFromFile(dbPath);
} catch (Exception e) {
throw new ServiceException("RegionUtils初始化失败原因从ip2region.xdb文件加载内容失败" + e.getMessage());
}
// 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。
try {
SEARCHER = Searcher.newWithBuffer(cBuff);
} catch (Exception e) { } catch (Exception e) {
throw new ServiceException("RegionUtils初始化失败原因" + e.getMessage()); throw new ServiceException("RegionUtils初始化失败原因" + e.getMessage());
} }
@ -39,8 +54,9 @@ public class RegionUtils {
*/ */
public static String getCityInfo(String ip) { public static String getCityInfo(String ip) {
try { try {
ip = ip.trim();
// 3、执行查询 // 3、执行查询
String region = SEARCHER.search(StringUtils.trim(ip)); String region = SEARCHER.search(ip);
return region.replace("0|", "").replace("|0", ""); return region.replace("0|", "").replace("|0", "");
} catch (Exception e) { } catch (Exception e) {
log.error("IP地址离线获取城市异常 {}", ip); log.error("IP地址离线获取城市异常 {}", ip);

View File

@ -1,40 +0,0 @@
package org.dromara.common.core.validate.dicts;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 字典项校验注解
*
* @author AprilWind
*/
@Constraint(validatedBy = DictPatternValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface DictPattern {
/**
* 字典类型,如 "sys_user_sex"
*/
String dictType();
/**
* 分隔符
*/
String separator();
/**
* 默认校验失败提示信息
*/
String message() default "字典值无效";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@ -1,55 +0,0 @@
package org.dromara.common.core.validate.dicts;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.dromara.common.core.service.DictService;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
/**
* 自定义字典值校验器
*
* @author AprilWind
*/
public class DictPatternValidator implements ConstraintValidator<DictPattern, String> {
/**
* 字典类型
*/
private String dictType;
/**
* 分隔符
*/
private String separator = ",";
/**
* 初始化校验器,提取注解上的字典类型
*
* @param annotation 注解实例
*/
@Override
public void initialize(DictPattern annotation) {
this.dictType = annotation.dictType();
if (StringUtils.isNotBlank(annotation.separator())) {
this.separator = annotation.separator();
}
}
/**
* 校验字段值是否为指定字典类型中的合法值
*
* @param value 被校验的字段值
* @param context 校验上下文(可用于构建错误信息)
* @return true 表示校验通过合法字典值false 表示不通过
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (StringUtils.isBlank(dictType) || StringUtils.isBlank(value)) {
return false;
}
String dictLabel = SpringUtils.getBean(DictService.class).getDictLabel(dictType, value, separator);
return StringUtils.isNotBlank(dictLabel);
}
}

View File

@ -13,7 +13,7 @@ import org.dromara.common.core.utils.reflect.ReflectUtils;
*/ */
public class EnumPatternValidator implements ConstraintValidator<EnumPattern, String> { public class EnumPatternValidator implements ConstraintValidator<EnumPattern, String> {
private EnumPattern annotation; private EnumPattern annotation;;
@Override @Override
public void initialize(EnumPattern annotation) { public void initialize(EnumPattern annotation) {

View File

@ -1,4 +1,5 @@
org.dromara.common.core.config.ApplicationConfig org.dromara.common.core.config.ApplicationConfig
org.dromara.common.core.config.AsyncConfig
org.dromara.common.core.config.ThreadPoolConfig org.dromara.common.core.config.ThreadPoolConfig
org.dromara.common.core.config.ValidatorConfig org.dromara.common.core.config.ValidatorConfig
org.dromara.common.core.utils.SpringUtils org.dromara.common.core.utils.SpringUtils

View File

@ -30,7 +30,7 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
/** /**
* 接口文档配置 * Swagger 文档配置
* *
* @author Lion Li * @author Lion Li
*/ */
@ -54,7 +54,6 @@ public class SpringDocConfig {
openApi.externalDocs(properties.getExternalDocs()); openApi.externalDocs(properties.getExternalDocs());
openApi.tags(properties.getTags()); openApi.tags(properties.getTags());
openApi.paths(properties.getPaths()); openApi.paths(properties.getPaths());
if (properties.getComponents() != null) {
openApi.components(properties.getComponents()); openApi.components(properties.getComponents());
Set<String> keySet = properties.getComponents().getSecuritySchemes().keySet(); Set<String> keySet = properties.getComponents().getSecuritySchemes().keySet();
List<SecurityRequirement> list = new ArrayList<>(); List<SecurityRequirement> list = new ArrayList<>();
@ -62,7 +61,7 @@ public class SpringDocConfig {
keySet.forEach(securityRequirement::addList); keySet.forEach(securityRequirement::addList);
list.add(securityRequirement); list.add(securityRequirement);
openApi.security(list); openApi.security(list);
}
return openApi; return openApi;
} }

View File

@ -6,7 +6,6 @@ import org.dromara.common.encrypt.properties.ApiDecryptProperties;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistration;
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -21,14 +20,13 @@ import org.springframework.context.annotation.Bean;
public class ApiDecryptAutoConfiguration { public class ApiDecryptAutoConfiguration {
@Bean @Bean
@FilterRegistration( public FilterRegistrationBean<CryptoFilter> cryptoFilterRegistration(ApiDecryptProperties properties) {
name = "cryptoFilter", FilterRegistrationBean<CryptoFilter> registration = new FilterRegistrationBean<>();
urlPatterns = "/*", registration.setDispatcherTypes(DispatcherType.REQUEST);
order = FilterRegistrationBean.HIGHEST_PRECEDENCE, registration.setFilter(new CryptoFilter(properties));
dispatcherTypes = DispatcherType.REQUEST registration.addUrlPatterns("/*");
) registration.setName("cryptoFilter");
public CryptoFilter cryptoFilter(ApiDecryptProperties properties) { registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
return new CryptoFilter(properties); return registration;
} }
} }

View File

@ -5,7 +5,6 @@ import cn.hutool.core.util.ReflectUtil;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.io.Resources; import org.apache.ibatis.io.Resources;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.utils.ObjectUtils; import org.dromara.common.core.utils.ObjectUtils;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.encrypt.annotation.EncryptField; import org.dromara.common.encrypt.annotation.EncryptField;
@ -93,12 +92,8 @@ public class EncryptorManager {
* @param encryptContext 加密相关的配置信息 * @param encryptContext 加密相关的配置信息
*/ */
public String encrypt(String value, EncryptContext encryptContext) { public String encrypt(String value, EncryptContext encryptContext) {
if (StringUtils.startsWith(value, Constants.ENCRYPT_HEADER)) {
return value;
}
IEncryptor encryptor = this.registAndGetEncryptor(encryptContext); IEncryptor encryptor = this.registAndGetEncryptor(encryptContext);
String encrypt = encryptor.encrypt(value, encryptContext.getEncode()); return encryptor.encrypt(value, encryptContext.getEncode());
return Constants.ENCRYPT_HEADER + encrypt;
} }
/** /**
@ -108,12 +103,8 @@ public class EncryptorManager {
* @param encryptContext 加密相关的配置信息 * @param encryptContext 加密相关的配置信息
*/ */
public String decrypt(String value, EncryptContext encryptContext) { public String decrypt(String value, EncryptContext encryptContext) {
if (!StringUtils.startsWith(value, Constants.ENCRYPT_HEADER)) {
return value;
}
IEncryptor encryptor = this.registAndGetEncryptor(encryptContext); IEncryptor encryptor = this.registAndGetEncryptor(encryptContext);
String str = StringUtils.removeStart(value, Constants.ENCRYPT_HEADER); return encryptor.decrypt(value);
return encryptor.decrypt(str);
} }
/** /**

View File

@ -5,7 +5,6 @@ import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.resultset.ResultSetHandler; import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*; import org.apache.ibatis.plugin.*;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.StringUtils;
@ -40,23 +39,12 @@ public class MybatisDecryptInterceptor implements Interceptor {
@Override @Override
public Object intercept(Invocation invocation) throws Throwable { public Object intercept(Invocation invocation) throws Throwable {
// 开始进行参数解密
ResultSetHandler resultSetHandler = (ResultSetHandler) invocation.getTarget();
Field parameterHandlerField = resultSetHandler.getClass().getDeclaredField("parameterHandler");
parameterHandlerField.setAccessible(true);
Object target = parameterHandlerField.get(resultSetHandler);
if (target instanceof ParameterHandler parameterHandler) {
Object parameterObject = parameterHandler.getParameterObject();
if (ObjectUtil.isNotNull(parameterObject) && !(parameterObject instanceof String)) {
this.decryptHandler(parameterObject);
}
}
// 获取执行mysql执行结果 // 获取执行mysql执行结果
Object result = invocation.proceed(); Object result = invocation.proceed();
if (result == null) { if (result == null) {
return null; return null;
} }
this.decryptHandler(result); decryptHandler(result);
return result; return result;
} }

View File

@ -108,7 +108,7 @@ public class EncryptUtils {
} }
/** /**
* SM4加密Base64编码 * sm4加密
* *
* @param data 待加密数据 * @param data 待加密数据
* @param password 秘钥字符串 * @param password 秘钥字符串
@ -127,11 +127,11 @@ public class EncryptUtils {
} }
/** /**
* SM4加密Hex编码 * sm4加密
* *
* @param data 待加密数据 * @param data 待加密数据
* @param password 秘钥字符串 * @param password 秘钥字符串
* @return 加密后字符串, 采用Hex编码 * @return 加密后字符串, 采用Base64编码
*/ */
public static String encryptBySm4Hex(String data, String password) { public static String encryptBySm4Hex(String data, String password) {
if (StrUtil.isBlank(password)) { if (StrUtil.isBlank(password)) {
@ -148,7 +148,7 @@ public class EncryptUtils {
/** /**
* sm4解密 * sm4解密
* *
* @param data 待解密数据可以是Base64或Hex编码 * @param data 待解密数据
* @param password 秘钥字符串 * @param password 秘钥字符串
* @return 解密后字符串 * @return 解密后字符串
*/ */

View File

@ -22,8 +22,8 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>cn.idev.excel</groupId> <groupId>com.alibaba</groupId>
<artifactId>fastexcel</artifactId> <artifactId>easyexcel</artifactId>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@ -6,13 +6,17 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
/** /**
* 批注 此注解仅用于单表头 不支持多层级表头 * 批注
* @author guzhouyanyu * @author guzhouyanyu
*/ */
@Target({ElementType.FIELD}) @Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface ExcelNotation { public @interface ExcelNotation {
/**
* col index
*/
int index() default -1;
/** /**
* 批注内容 * 批注内容
*/ */

View File

@ -8,13 +8,17 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
/** /**
* 是否必填 此注解仅用于单表头 不支持多层级表头 * 是否必填
* @author guzhouyanyu * @author guzhouyanyu
*/ */
@Target({ElementType.FIELD}) @Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface ExcelRequired { public @interface ExcelRequired {
/**
* col index
*/
int index() default -1;
/** /**
* 字体颜色 * 字体颜色
*/ */

View File

@ -2,12 +2,12 @@ package org.dromara.common.excel.convert;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.idev.excel.converters.Converter; import com.alibaba.excel.converters.Converter;
import cn.idev.excel.enums.CellDataTypeEnum; import com.alibaba.excel.enums.CellDataTypeEnum;
import cn.idev.excel.metadata.GlobalConfiguration; import com.alibaba.excel.metadata.GlobalConfiguration;
import cn.idev.excel.metadata.data.ReadCellData; import com.alibaba.excel.metadata.data.ReadCellData;
import cn.idev.excel.metadata.data.WriteCellData; import com.alibaba.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty; import com.alibaba.excel.metadata.property.ExcelContentProperty;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal; import java.math.BigDecimal;

View File

@ -3,12 +3,12 @@ package org.dromara.common.excel.convert;
import cn.hutool.core.annotation.AnnotationUtil; import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.idev.excel.converters.Converter; import com.alibaba.excel.converters.Converter;
import cn.idev.excel.enums.CellDataTypeEnum; import com.alibaba.excel.enums.CellDataTypeEnum;
import cn.idev.excel.metadata.GlobalConfiguration; import com.alibaba.excel.metadata.GlobalConfiguration;
import cn.idev.excel.metadata.data.ReadCellData; import com.alibaba.excel.metadata.data.ReadCellData;
import cn.idev.excel.metadata.data.WriteCellData; import com.alibaba.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty; import com.alibaba.excel.metadata.property.ExcelContentProperty;
import org.dromara.common.excel.annotation.ExcelDictFormat; import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.core.service.DictService; import org.dromara.common.core.service.DictService;
import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.SpringUtils;

View File

@ -3,12 +3,12 @@ package org.dromara.common.excel.convert;
import cn.hutool.core.annotation.AnnotationUtil; import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.idev.excel.converters.Converter; import com.alibaba.excel.converters.Converter;
import cn.idev.excel.enums.CellDataTypeEnum; import com.alibaba.excel.enums.CellDataTypeEnum;
import cn.idev.excel.metadata.GlobalConfiguration; import com.alibaba.excel.metadata.GlobalConfiguration;
import cn.idev.excel.metadata.data.ReadCellData; import com.alibaba.excel.metadata.data.ReadCellData;
import cn.idev.excel.metadata.data.WriteCellData; import com.alibaba.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty; import com.alibaba.excel.metadata.property.ExcelContentProperty;
import org.dromara.common.core.utils.reflect.ReflectUtils; import org.dromara.common.core.utils.reflect.ReflectUtils;
import org.dromara.common.excel.annotation.ExcelEnumFormat; import org.dromara.common.excel.annotation.ExcelEnumFormat;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;

View File

@ -1,200 +0,0 @@
package org.dromara.common.excel.core;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.SneakyThrows;
import org.apache.poi.ss.util.CellRangeAddress;
import org.dromara.common.core.utils.reflect.ReflectUtils;
import org.dromara.common.excel.annotation.CellMerge;
import java.lang.reflect.Field;
import java.util.*;
/**
* 单元格合并处理器
*
* @author Lion Li
*/
public class CellMergeHandler {
private final boolean hasTitle;
private int rowIndex;
private CellMergeHandler(final boolean hasTitle) {
this.hasTitle = hasTitle;
// 行合并开始下标
this.rowIndex = hasTitle ? 1 : 0;
}
@SneakyThrows
public List<CellRangeAddress> handle(List<?> rows) {
// 如果入参为空集合则返回空集
if (CollUtil.isEmpty(rows)) {
return Collections.emptyList();
}
// 获取有合并注解的字段
Map<Field, FieldColumnIndex> mergeFields = getFieldColumnIndexMap(rows.get(0).getClass());
// 如果没有需要合并的字段则返回空集
if (CollUtil.isEmpty(mergeFields)) {
return Collections.emptyList();
}
// 结果集
List<CellRangeAddress> result = new ArrayList<>();
// 生成两两合并单元格
Map<Field, RepeatCell> rowRepeatCellMap = new HashMap<>();
for (Map.Entry<Field, FieldColumnIndex> item : mergeFields.entrySet()) {
Field field = item.getKey();
FieldColumnIndex itemValue = item.getValue();
int colNum = itemValue.colIndex();
CellMerge cellMerge = itemValue.cellMerge();
for (int i = 0; i < rows.size(); i++) {
// 当前行数据
Object currentRowObj = rows.get(i);
// 当前行数据字段值
Object currentRowObjFieldVal = ReflectUtils.invokeGetter(currentRowObj, field.getName());
// 空值跳过不处理
if (currentRowObjFieldVal == null || "".equals(currentRowObjFieldVal)) {
continue;
}
// 单元格合并Map是否存在数据如果不存在则添加当前行的字段值
if (!rowRepeatCellMap.containsKey(field)) {
rowRepeatCellMap.put(field, RepeatCell.of(currentRowObjFieldVal, i));
continue;
}
// 获取 单元格合并Map 中字段值
RepeatCell repeatCell = rowRepeatCellMap.get(field);
Object cellValue = repeatCell.value();
int current = repeatCell.current();
// 检查是否满足合并条件
// currentRowObj 当前行数据
// rows.get(i - 1) 上一行数据 注:由于 if (!rowRepeatCellMap.containsKey(field)) 条件的存在,所以该 i 必不可能小于1
// cellMerge 当前行字段合并注解
boolean merge = isMerge(currentRowObj, rows.get(i - 1), cellMerge);
// 是否添加到结果集
boolean isAddResult = false;
// 最新行
int lastRow = i + rowIndex - 1;
// 如果当前行字段值和缓存中的字段值不相等,或不满足合并条件,则替换
if (!currentRowObjFieldVal.equals(cellValue) || !merge) {
rowRepeatCellMap.put(field, RepeatCell.of(currentRowObjFieldVal, i));
isAddResult = true;
}
// 如果最后一行不能合并,检查之前的数据是否需要合并;如果最后一行可以合并,则直接合并到最后
if (i == rows.size() - 1) {
isAddResult = true;
if (i > current) {
lastRow = i + rowIndex;
}
}
if (isAddResult && i > current) {
result.add(new CellRangeAddress(current + rowIndex, lastRow, colNum, colNum));
}
}
}
return result;
}
/**
* 获取带有合并注解的字段列索引和合并注解信息Map集
*/
private Map<Field, FieldColumnIndex> getFieldColumnIndexMap(Class<?> clazz) {
boolean annotationPresent = clazz.isAnnotationPresent(ExcelIgnoreUnannotated.class);
Field[] fields = ReflectUtils.getFields(clazz, field -> {
if ("serialVersionUID".equals(field.getName())) {
return false;
}
if (field.isAnnotationPresent(ExcelIgnore.class)) {
return false;
}
return !annotationPresent || field.isAnnotationPresent(ExcelProperty.class);
});
// 有注解的字段
Map<Field, FieldColumnIndex> mergeFields = new HashMap<>();
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
if (!field.isAnnotationPresent(CellMerge.class)) {
continue;
}
CellMerge cm = field.getAnnotation(CellMerge.class);
int index = cm.index() == -1 ? i : cm.index();
mergeFields.put(field, FieldColumnIndex.of(index, cm));
if (hasTitle) {
ExcelProperty property = field.getAnnotation(ExcelProperty.class);
rowIndex = Math.max(rowIndex, property.value().length);
}
}
return mergeFields;
}
private boolean isMerge(Object currentRow, Object preRow, CellMerge cellMerge) {
final String[] mergeBy = cellMerge.mergeBy();
if (StrUtil.isAllNotBlank(mergeBy)) {
//比对当前行和上一行的各个属性值一一比对 如果全为真 则为真
for (String fieldName : mergeBy) {
final Object valCurrent = ReflectUtil.getFieldValue(currentRow, fieldName);
final Object valPre = ReflectUtil.getFieldValue(preRow, fieldName);
if (!Objects.equals(valPre, valCurrent)) {
//依赖字段如有任一不等值,则标记为不可合并
return false;
}
}
}
return true;
}
/**
* 单元格合并
*/
record RepeatCell(Object value, int current) {
static RepeatCell of(Object value, int current) {
return new RepeatCell(value, current);
}
}
/**
* 字段列索引和合并注解信息
*/
record FieldColumnIndex(int colIndex, CellMerge cellMerge) {
static FieldColumnIndex of(int colIndex, CellMerge cellMerge) {
return new FieldColumnIndex(colIndex, cellMerge);
}
}
/**
* 创建一个单元格合并处理器实例
*
* @param hasTitle 是否合并标题
* @return 单元格合并处理器
*/
public static CellMergeHandler of(final boolean hasTitle) {
return new CellMergeHandler(hasTitle);
}
/**
* 创建一个单元格合并处理器实例(默认不合并标题)
*
* @return 单元格合并处理器
*/
public static CellMergeHandler of() {
return new CellMergeHandler(false);
}
}

View File

@ -1,15 +1,24 @@
package org.dromara.common.excel.core; package org.dromara.common.excel.core;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.idev.excel.metadata.Head; import cn.hutool.core.util.ReflectUtil;
import cn.idev.excel.write.handler.WorkbookWriteHandler; import cn.hutool.core.util.StrUtil;
import cn.idev.excel.write.handler.context.WorkbookWriteHandlerContext; import com.alibaba.excel.annotation.ExcelProperty;
import cn.idev.excel.write.merge.AbstractMergeStrategy; import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.handler.WorkbookWriteHandler;
import com.alibaba.excel.write.handler.context.WorkbookWriteHandlerContext;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.ss.util.CellRangeAddress;
import org.dromara.common.core.utils.reflect.ReflectUtils;
import org.dromara.common.excel.annotation.CellMerge;
import java.lang.reflect.Field;
import java.util.*; import java.util.*;
/** /**
@ -21,22 +30,21 @@ import java.util.*;
public class CellMergeStrategy extends AbstractMergeStrategy implements WorkbookWriteHandler { public class CellMergeStrategy extends AbstractMergeStrategy implements WorkbookWriteHandler {
private final List<CellRangeAddress> cellList; private final List<CellRangeAddress> cellList;
private final boolean hasTitle;
public CellMergeStrategy(List<CellRangeAddress> cellList) { private int rowIndex;
this.cellList = cellList;
}
public CellMergeStrategy(List<?> list, boolean hasTitle) { public CellMergeStrategy(List<?> list, boolean hasTitle) {
this.cellList = CellMergeHandler.of(hasTitle).handle(list); this.hasTitle = hasTitle;
// 行合并开始下标
this.rowIndex = hasTitle ? 1 : 0;
this.cellList = handle(list, hasTitle);
} }
@Override @Override
protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) { protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
if (CollUtil.isEmpty(cellList)){
return;
}
//单元格写入了,遍历合并区域,如果该Cell在区域内,但非首行,则清空 //单元格写入了,遍历合并区域,如果该Cell在区域内,但非首行,则清空
final int rowIndex = cell.getRowIndex(); final int rowIndex = cell.getRowIndex();
if (CollUtil.isNotEmpty(cellList)){
for (CellRangeAddress cellAddresses : cellList) { for (CellRangeAddress cellAddresses : cellList) {
final int firstRow = cellAddresses.getFirstRow(); final int firstRow = cellAddresses.getFirstRow();
if (cellAddresses.isInRange(cell) && rowIndex != firstRow){ if (cellAddresses.isInRange(cell) && rowIndex != firstRow){
@ -44,16 +52,106 @@ public class CellMergeStrategy extends AbstractMergeStrategy implements Workbook
} }
} }
} }
}
@Override @Override
public void afterWorkbookDispose(final WorkbookWriteHandlerContext context) { public void afterWorkbookDispose(final WorkbookWriteHandlerContext context) {
if (CollUtil.isEmpty(cellList)){
return;
}
//当前表格写完后,统一写入 //当前表格写完后,统一写入
if (CollUtil.isNotEmpty(cellList)){
for (CellRangeAddress item : cellList) { for (CellRangeAddress item : cellList) {
context.getWriteContext().writeSheetHolder().getSheet().addMergedRegion(item); context.getWriteContext().writeSheetHolder().getSheet().addMergedRegion(item);
} }
} }
}
@SneakyThrows
private List<CellRangeAddress> handle(List<?> list, boolean hasTitle) {
List<CellRangeAddress> cellList = new ArrayList<>();
if (CollUtil.isEmpty(list)) {
return cellList;
}
Field[] fields = ReflectUtils.getFields(list.get(0).getClass(), field -> !"serialVersionUID".equals(field.getName()));
// 有注解的字段
List<Field> mergeFields = new ArrayList<>();
List<Integer> mergeFieldsIndex = new ArrayList<>();
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
if (field.isAnnotationPresent(CellMerge.class)) {
CellMerge cm = field.getAnnotation(CellMerge.class);
mergeFields.add(field);
mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index());
if (hasTitle) {
ExcelProperty property = field.getAnnotation(ExcelProperty.class);
rowIndex = Math.max(rowIndex, property.value().length);
}
}
}
Map<Field, RepeatCell> map = new HashMap<>();
// 生成两两合并单元格
for (int i = 0; i < list.size(); i++) {
for (int j = 0; j < mergeFields.size(); j++) {
Field field = mergeFields.get(j);
Object val = ReflectUtils.invokeGetter(list.get(i), field.getName());
int colNum = mergeFieldsIndex.get(j);
if (!map.containsKey(field)) {
map.put(field, new RepeatCell(val, i));
} else {
RepeatCell repeatCell = map.get(field);
Object cellValue = repeatCell.getValue();
if (cellValue == null || "".equals(cellValue)) {
// 空值跳过不合并
continue;
}
if (!cellValue.equals(val)) {
if ((i - repeatCell.getCurrent() > 1)) {
cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
}
map.put(field, new RepeatCell(val, i));
} else if (i == list.size() - 1) {
if (i > repeatCell.getCurrent() && isMerge(list, i, field)) {
cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
}
} else if (!isMerge(list, i, field)) {
if ((i - repeatCell.getCurrent() > 1)) {
cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
}
map.put(field, new RepeatCell(val, i));
}
}
}
}
return cellList;
}
private boolean isMerge(List<?> list, int i, Field field) {
boolean isMerge = true;
CellMerge cm = field.getAnnotation(CellMerge.class);
final String[] mergeBy = cm.mergeBy();
if (StrUtil.isAllNotBlank(mergeBy)) {
//比对当前list(i)和list(i - 1)的各个属性值一一比对 如果全为真 则为真
for (String fieldName : mergeBy) {
final Object valCurrent = ReflectUtil.getFieldValue(list.get(i), fieldName);
final Object valPre = ReflectUtil.getFieldValue(list.get(i - 1), fieldName);
if (!Objects.equals(valPre, valCurrent)) {
//依赖字段如有任一不等值,则标记为不可合并
isMerge = false;
}
}
}
return isMerge;
}
@Data
@AllArgsConstructor
static class RepeatCell {
private Object value;
private int current;
}
} }

View File

@ -1,10 +1,10 @@
package org.dromara.common.excel.core; package org.dromara.common.excel.core;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.idev.excel.context.AnalysisContext; import com.alibaba.excel.context.AnalysisContext;
import cn.idev.excel.event.AnalysisEventListener; import com.alibaba.excel.event.AnalysisEventListener;
import cn.idev.excel.exception.ExcelAnalysisException; import com.alibaba.excel.exception.ExcelAnalysisException;
import cn.idev.excel.exception.ExcelDataConvertException; import com.alibaba.excel.exception.ExcelDataConvertException;
import org.dromara.common.core.utils.StreamUtils; import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.ValidatorUtils; import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.json.utils.JsonUtils; import org.dromara.common.json.utils.JsonUtils;

View File

@ -1,6 +1,5 @@
package org.dromara.common.excel.core; package org.dromara.common.excel.core;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
@ -66,7 +65,7 @@ public class DropDownOptions {
StringBuilder stringBuffer = new StringBuilder(); StringBuilder stringBuffer = new StringBuilder();
String regex = "^[\\S\\d\\u4e00-\\u9fa5]+$"; String regex = "^[\\S\\d\\u4e00-\\u9fa5]+$";
for (int i = 0; i < vars.length; i++) { for (int i = 0; i < vars.length; i++) {
String var = StrUtil.trimToEmpty(Convert.toStr(vars[i])); String var = StrUtil.trimToEmpty(String.valueOf(vars[i]));
if (!var.matches(regex)) { if (!var.matches(regex)) {
throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字"); throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字");
} }

View File

@ -1,17 +1,16 @@
package org.dromara.common.excel.core; package org.dromara.common.excel.core;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.EnumUtil; import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.idev.excel.metadata.FieldCache; import com.alibaba.excel.metadata.FieldCache;
import cn.idev.excel.metadata.FieldWrapper; import com.alibaba.excel.metadata.FieldWrapper;
import cn.idev.excel.util.ClassUtils; import com.alibaba.excel.util.ClassUtils;
import cn.idev.excel.write.handler.SheetWriteHandler; import com.alibaba.excel.write.handler.SheetWriteHandler;
import cn.idev.excel.write.metadata.holder.WriteSheetHolder; import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import cn.idev.excel.write.metadata.holder.WriteWorkbookHolder; import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList; import org.apache.poi.ss.util.CellRangeAddressList;
@ -104,7 +103,7 @@ public class ExcelDownHandler implements SheetWriteHandler {
if (StringUtils.isNotBlank(dictType)) { if (StringUtils.isNotBlank(dictType)) {
// 如果传递了字典名,则依据字典建立下拉 // 如果传递了字典名,则依据字典建立下拉
Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType)) Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType))
.orElseThrow(() -> new ServiceException("字典 {} 不存在", dictType)) .orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType)))
.values(); .values();
options = new ArrayList<>(values); options = new ArrayList<>(values);
} else if (StringUtils.isNotBlank(converterExp)) { } else if (StringUtils.isNotBlank(converterExp)) {
@ -116,7 +115,7 @@ public class ExcelDownHandler implements SheetWriteHandler {
// 否则如果指定了@ExcelEnumFormat则使用枚举的逻辑 // 否则如果指定了@ExcelEnumFormat则使用枚举的逻辑
ExcelEnumFormat format = field.getDeclaredAnnotation(ExcelEnumFormat.class); ExcelEnumFormat format = field.getDeclaredAnnotation(ExcelEnumFormat.class);
List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField()); List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField());
options = StreamUtils.toList(values, Convert::toStr); options = StreamUtils.toList(values, String::valueOf);
} }
if (ObjectUtil.isNotEmpty(options)) { if (ObjectUtil.isNotEmpty(options)) {
// 仅当下拉可选项不为空时执行 // 仅当下拉可选项不为空时执行
@ -176,7 +175,7 @@ public class ExcelDownHandler implements SheetWriteHandler {
List<String> firstOptions = options.getOptions(); List<String> firstOptions = options.getOptions();
Map<String, List<String>> secoundOptionsMap = options.getNextOptions(); Map<String, List<String>> secoundOptionsMap = options.getNextOptions();
// 采用按行填充数据的方式,避免出现数据无法写入的问题 // 采用按行填充数据的方式,避免EasyExcel出现数据无法写入的问题
// Attempting to write a row in the range that is already written to disk // Attempting to write a row in the range that is already written to disk
// 使用ArrayList记载数据防止乱序 // 使用ArrayList记载数据防止乱序
@ -292,11 +291,9 @@ public class ExcelDownHandler implements SheetWriteHandler {
* @param value 下拉选可选值 * @param value 下拉选可选值
*/ */
private void dropDownWithSheet(DataValidationHelper helper, Workbook workbook, Sheet sheet, Integer celIndex, List<String> value) { private void dropDownWithSheet(DataValidationHelper helper, Workbook workbook, Sheet sheet, Integer celIndex, List<String> value) {
//由于poi的写出相关问题超过100个会被临时写进硬盘导致后续内存合并会出Attempting to write a row[] in the range [] that is already written to disk
String tmpOptionsSheetName = OPTIONS_SHEET_NAME + "_" + currentOptionsColumnIndex;
// 创建下拉数据表 // 创建下拉数据表
Sheet simpleDataSheet = Optional.ofNullable(workbook.getSheet(WorkbookUtil.createSafeSheetName(tmpOptionsSheetName))) Sheet simpleDataSheet = Optional.ofNullable(workbook.getSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)))
.orElseGet(() -> workbook.createSheet(WorkbookUtil.createSafeSheetName(tmpOptionsSheetName))); .orElseGet(() -> workbook.createSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)));
// 将下拉表隐藏 // 将下拉表隐藏
workbook.setSheetHidden(workbook.getSheetIndex(simpleDataSheet), true); workbook.setSheetHidden(workbook.getSheetIndex(simpleDataSheet), true);
// 完善纵向的一级选项数据表 // 完善纵向的一级选项数据表
@ -305,9 +302,9 @@ public class ExcelDownHandler implements SheetWriteHandler {
// 获取每一选项行,如果没有则创建 // 获取每一选项行,如果没有则创建
Row row = Optional.ofNullable(simpleDataSheet.getRow(i)) Row row = Optional.ofNullable(simpleDataSheet.getRow(i))
.orElseGet(() -> simpleDataSheet.createRow(finalI)); .orElseGet(() -> simpleDataSheet.createRow(finalI));
// 获取本级选项对应的选项列,如果没有则创建。上述采用多个sheet,默认索引为1列 // 获取本级选项对应的选项列,如果没有则创建
Cell cell = Optional.ofNullable(row.getCell(0)) Cell cell = Optional.ofNullable(row.getCell(currentOptionsColumnIndex))
.orElseGet(() -> row.createCell(0)); .orElseGet(() -> row.createCell(currentOptionsColumnIndex));
// 设置值 // 设置值
cell.setCellValue(value.get(i)); cell.setCellValue(value.get(i));
} }
@ -315,13 +312,13 @@ public class ExcelDownHandler implements SheetWriteHandler {
// 创建名称管理器 // 创建名称管理器
Name name = workbook.createName(); Name name = workbook.createName();
// 设置名称管理器的别名 // 设置名称管理器的别名
String nameName = String.format("%s_%d", tmpOptionsSheetName, celIndex); String nameName = String.format("%s_%d", OPTIONS_SHEET_NAME, celIndex);
name.setNameName(nameName); name.setNameName(nameName);
// 以纵向第一列创建一级下拉拼接引用位置 // 以纵向第一列创建一级下拉拼接引用位置
String function = String.format("%s!$%s$1:$%s$%d", String function = String.format("%s!$%s$1:$%s$%d",
tmpOptionsSheetName, OPTIONS_SHEET_NAME,
getExcelColumnName(0), getExcelColumnName(currentOptionsColumnIndex),
getExcelColumnName(0), getExcelColumnName(currentOptionsColumnIndex),
value.size()); value.size());
// 设置名称管理器的引用位置 // 设置名称管理器的引用位置
name.setRefersToFormula(function); name.setRefersToFormula(function);

View File

@ -1,6 +1,6 @@
package org.dromara.common.excel.core; package org.dromara.common.excel.core;
import cn.idev.excel.read.listener.ReadListener; import com.alibaba.excel.read.listener.ReadListener;
/** /**
* Excel 导入监听 * Excel 导入监听

View File

@ -1,19 +1,19 @@
package org.dromara.common.excel.handler; package org.dromara.common.excel.handler;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.idev.excel.annotation.ExcelProperty; import com.alibaba.excel.metadata.data.DataFormatData;
import cn.idev.excel.metadata.data.DataFormatData; import com.alibaba.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.data.WriteCellData; import com.alibaba.excel.util.StyleUtil;
import cn.idev.excel.util.StyleUtil; import com.alibaba.excel.write.handler.CellWriteHandler;
import cn.idev.excel.write.handler.CellWriteHandler; import com.alibaba.excel.write.handler.SheetWriteHandler;
import cn.idev.excel.write.handler.SheetWriteHandler; import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
import cn.idev.excel.write.handler.context.CellWriteHandlerContext; import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import cn.idev.excel.write.metadata.holder.WriteSheetHolder; import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import cn.idev.excel.write.metadata.style.WriteCellStyle; import com.alibaba.excel.write.metadata.style.WriteFont;
import cn.idev.excel.write.metadata.style.WriteFont;
import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFClientAnchor; import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
import org.apache.poi.xssf.usermodel.XSSFRichTextString; import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.dromara.common.core.utils.reflect.ReflectUtils;
import org.dromara.common.excel.annotation.ExcelNotation; import org.dromara.common.excel.annotation.ExcelNotation;
import org.dromara.common.excel.annotation.ExcelRequired; import org.dromara.common.excel.annotation.ExcelRequired;
@ -31,12 +31,12 @@ public class DataWriteHandler implements SheetWriteHandler, CellWriteHandler {
/** /**
* 批注 * 批注
*/ */
private final Map<String, String> notationMap; private final Map<Integer, String> notationMap;
/** /**
* 头列字体颜色 * 头列字体颜色
*/ */
private final Map<String, Short> headColumnMap; private final Map<Integer, Short> headColumnMap;
public DataWriteHandler(Class<?> clazz) { public DataWriteHandler(Class<?> clazz) {
@ -49,16 +49,15 @@ public class DataWriteHandler implements SheetWriteHandler, CellWriteHandler {
if (CollUtil.isEmpty(notationMap) && CollUtil.isEmpty(headColumnMap)) { if (CollUtil.isEmpty(notationMap) && CollUtil.isEmpty(headColumnMap)) {
return; return;
} }
// 第一行
WriteCellData<?> cellData = context.getFirstCellData(); WriteCellData<?> cellData = context.getFirstCellData();
// 第一个格子
WriteCellStyle writeCellStyle = cellData.getOrCreateStyle(); WriteCellStyle writeCellStyle = cellData.getOrCreateStyle();
if (context.getHead()) {
DataFormatData dataFormatData = new DataFormatData(); DataFormatData dataFormatData = new DataFormatData();
// 单元格设置为文本格式 // 单元格设置为文本格式
dataFormatData.setIndex((short) 49); dataFormatData.setIndex((short) 49);
writeCellStyle.setDataFormatData(dataFormatData); writeCellStyle.setDataFormatData(dataFormatData);
if (context.getHead()) {
Cell cell = context.getCell(); Cell cell = context.getCell();
WriteSheetHolder writeSheetHolder = context.getWriteSheetHolder(); WriteSheetHolder writeSheetHolder = context.getWriteSheetHolder();
Sheet sheet = writeSheetHolder.getSheet(); Sheet sheet = writeSheetHolder.getSheet();
@ -68,17 +67,17 @@ public class DataWriteHandler implements SheetWriteHandler, CellWriteHandler {
WriteFont headWriteFont = new WriteFont(); WriteFont headWriteFont = new WriteFont();
// 加粗 // 加粗
headWriteFont.setBold(true); headWriteFont.setBold(true);
if (CollUtil.isNotEmpty(headColumnMap) && headColumnMap.containsKey(cell.getStringCellValue())) { if (CollUtil.isNotEmpty(headColumnMap) && headColumnMap.containsKey(cell.getColumnIndex())) {
// 设置字体颜色 // 设置字体颜色
headWriteFont.setColor(headColumnMap.get(cell.getStringCellValue())); headWriteFont.setColor(headColumnMap.get(cell.getColumnIndex()));
} }
writeCellStyle.setWriteFont(headWriteFont); writeCellStyle.setWriteFont(headWriteFont);
CellStyle cellStyle = StyleUtil.buildCellStyle(workbook, null, writeCellStyle); CellStyle cellStyle = StyleUtil.buildCellStyle(workbook, null, writeCellStyle);
cell.setCellStyle(cellStyle); cell.setCellStyle(cellStyle);
if (CollUtil.isNotEmpty(notationMap) && notationMap.containsKey(cell.getStringCellValue())) { if (CollUtil.isNotEmpty(notationMap) && notationMap.containsKey(cell.getColumnIndex())) {
// 批注内容 // 批注内容
String notationContext = notationMap.get(cell.getStringCellValue()); String notationContext = notationMap.get(cell.getColumnIndex());
// 创建绘图对象 // 创建绘图对象
Comment comment = drawing.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), 0, (short) 5, 5)); Comment comment = drawing.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) cell.getColumnIndex(), 0, (short) 5, 5));
comment.setString(new XSSFRichTextString(notationContext)); comment.setString(new XSSFRichTextString(notationContext));
@ -90,16 +89,23 @@ public class DataWriteHandler implements SheetWriteHandler, CellWriteHandler {
/** /**
* 获取必填列 * 获取必填列
*/ */
private static Map<String, Short> getRequiredMap(Class<?> clazz) { private static Map<Integer, Short> getRequiredMap(Class<?> clazz) {
Map<String, Short> requiredMap = new HashMap<>(); Map<Integer, Short> requiredMap = new HashMap<>();
Field[] fields = clazz.getDeclaredFields(); Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) { // 检查 fields 数组是否为空
if (fields.length == 0) {
return requiredMap;
}
Field[] filteredFields = ReflectUtils.getFields(clazz, field -> !"serialVersionUID".equals(field.getName()));
for (int i = 0; i < filteredFields.length; i++) {
Field field = filteredFields[i];
if (!field.isAnnotationPresent(ExcelRequired.class)) { if (!field.isAnnotationPresent(ExcelRequired.class)) {
continue; continue;
} }
ExcelRequired excelRequired = field.getAnnotation(ExcelRequired.class); ExcelRequired excelRequired = field.getAnnotation(ExcelRequired.class);
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); int columnIndex = excelRequired.index() == -1 ? i : excelRequired.index();
requiredMap.put(excelProperty.value()[0], excelRequired.fontColor().getIndex()); requiredMap.put(columnIndex, excelRequired.fontColor().getIndex());
} }
return requiredMap; return requiredMap;
} }
@ -107,16 +113,22 @@ public class DataWriteHandler implements SheetWriteHandler, CellWriteHandler {
/** /**
* 获取批注 * 获取批注
*/ */
private static Map<String, String> getNotationMap(Class<?> clazz) { private static Map<Integer, String> getNotationMap(Class<?> clazz) {
Map<String, String> notationMap = new HashMap<>(); Map<Integer, String> notationMap = new HashMap<>();
Field[] fields = clazz.getDeclaredFields(); Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) { // 检查 fields 数组是否为空
if (fields.length == 0) {
return notationMap;
}
Field[] filteredFields = ReflectUtils.getFields(clazz, field -> !"serialVersionUID".equals(field.getName()));
for (int i = 0; i < filteredFields.length; i++) {
Field field = filteredFields[i];
if (!field.isAnnotationPresent(ExcelNotation.class)) { if (!field.isAnnotationPresent(ExcelNotation.class)) {
continue; continue;
} }
ExcelNotation excelNotation = field.getAnnotation(ExcelNotation.class); ExcelNotation excelNotation = field.getAnnotation(ExcelNotation.class);
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); int columnIndex = excelNotation.index() == -1 ? i : excelNotation.index();
notationMap.put(excelProperty.value()[0], excelNotation.value()); notationMap.put(columnIndex, excelNotation.value());
} }
return notationMap; return notationMap;
} }

View File

@ -3,13 +3,13 @@ package org.dromara.common.excel.utils;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.resource.ClassPathResource; import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.idev.excel.FastExcel; import com.alibaba.excel.EasyExcel;
import cn.idev.excel.ExcelWriter; import com.alibaba.excel.ExcelWriter;
import cn.idev.excel.write.builder.ExcelWriterSheetBuilder; import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
import cn.idev.excel.write.metadata.WriteSheet; import com.alibaba.excel.write.metadata.WriteSheet;
import cn.idev.excel.write.metadata.fill.FillConfig; import com.alibaba.excel.write.metadata.fill.FillConfig;
import cn.idev.excel.write.metadata.fill.FillWrapper; import com.alibaba.excel.write.metadata.fill.FillWrapper;
import cn.idev.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import jakarta.servlet.ServletOutputStream; import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.AccessLevel; import lombok.AccessLevel;
@ -27,7 +27,6 @@ import java.io.UnsupportedEncodingException;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer;
/** /**
* Excel相关处理 * Excel相关处理
@ -44,7 +43,7 @@ public class ExcelUtil {
* @return 转换后集合 * @return 转换后集合
*/ */
public static <T> List<T> importExcel(InputStream is, Class<T> clazz) { public static <T> List<T> importExcel(InputStream is, Class<T> clazz) {
return FastExcel.read(is).head(clazz).autoCloseStream(false).sheet().doReadSync(); return EasyExcel.read(is).head(clazz).autoCloseStream(false).sheet().doReadSync();
} }
@ -58,7 +57,7 @@ public class ExcelUtil {
*/ */
public static <T> ExcelResult<T> importExcel(InputStream is, Class<T> clazz, boolean isValidate) { public static <T> ExcelResult<T> importExcel(InputStream is, Class<T> clazz, boolean isValidate) {
DefaultExcelListener<T> listener = new DefaultExcelListener<>(isValidate); DefaultExcelListener<T> listener = new DefaultExcelListener<>(isValidate);
FastExcel.read(is, clazz, listener).sheet().doRead(); EasyExcel.read(is, clazz, listener).sheet().doRead();
return listener.getExcelResult(); return listener.getExcelResult();
} }
@ -71,7 +70,7 @@ public class ExcelUtil {
* @return 转换后集合 * @return 转换后集合
*/ */
public static <T> ExcelResult<T> importExcel(InputStream is, Class<T> clazz, ExcelListener<T> listener) { public static <T> ExcelResult<T> importExcel(InputStream is, Class<T> clazz, ExcelListener<T> listener) {
FastExcel.read(is, clazz, listener).sheet().doRead(); EasyExcel.read(is, clazz, listener).sheet().doRead();
return listener.getExcelResult(); return listener.getExcelResult();
} }
@ -187,7 +186,7 @@ public class ExcelUtil {
*/ */
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge,
OutputStream os, List<DropDownOptions> options) { OutputStream os, List<DropDownOptions> options) {
ExcelWriterSheetBuilder builder = FastExcel.write(os, clazz) ExcelWriterSheetBuilder builder = EasyExcel.write(os, clazz)
.autoCloseStream(false) .autoCloseStream(false)
// 自动适配 // 自动适配
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
@ -204,44 +203,6 @@ public class ExcelUtil {
builder.doWrite(list); builder.doWrite(list);
} }
/**
* 导出excel
*
* @param headType 带Excel注解的类型
* @param os 输出流
* @param options Excel下拉可选项
* @param consumer 导出助手消费函数
*/
public static <T> void exportExcel(Class<T> headType, OutputStream os, List<DropDownOptions> options, Consumer<ExcelWriterWrapper<T>> consumer) {
try (ExcelWriter writer = FastExcel.write(os, headType)
.autoCloseStream(false)
// 自动适配
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
// 大数值自动转换 防止失真
.registerConverter(new ExcelBigNumberConvert())
// 批注必填项处理
.registerWriteHandler(new DataWriteHandler(headType))
// 添加下拉框操作
.registerWriteHandler(new ExcelDownHandler(options))
.build()) {
// 执行消费函数
consumer.accept(ExcelWriterWrapper.of(writer));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 导出excel
*
* @param headType 带Excel注解的类型
* @param os 输出流
* @param consumer 导出助手消费函数
*/
public static <T> void exportExcel(Class<T> headType, OutputStream os, Consumer<ExcelWriterWrapper<T>> consumer) {
exportExcel(headType, os, null, consumer);
}
/** /**
* 单表多数据模板导出 模板格式为 {.属性} * 单表多数据模板导出 模板格式为 {.属性}
* *
@ -254,9 +215,6 @@ public class ExcelUtil {
*/ */
public static <T> void exportTemplate(List<T> data, String filename, String templatePath, HttpServletResponse response) { public static <T> void exportTemplate(List<T> data, String filename, String templatePath, HttpServletResponse response) {
try { try {
if (CollUtil.isEmpty(data)) {
throw new IllegalArgumentException("数据为空");
}
resetResponse(filename, response); resetResponse(filename, response);
ServletOutputStream os = response.getOutputStream(); ServletOutputStream os = response.getOutputStream();
exportTemplate(data, templatePath, os); exportTemplate(data, templatePath, os);
@ -275,15 +233,18 @@ public class ExcelUtil {
* @param os 输出流 * @param os 输出流
*/ */
public static <T> void exportTemplate(List<T> data, String templatePath, OutputStream os) { public static <T> void exportTemplate(List<T> data, String templatePath, OutputStream os) {
if (CollUtil.isEmpty(data)) {
throw new IllegalArgumentException("数据为空");
}
ClassPathResource templateResource = new ClassPathResource(templatePath); ClassPathResource templateResource = new ClassPathResource(templatePath);
ExcelWriter excelWriter = FastExcel.write(os) ExcelWriter excelWriter = EasyExcel.write(os)
.withTemplate(templateResource.getStream()) .withTemplate(templateResource.getStream())
.autoCloseStream(false) .autoCloseStream(false)
// 大数值自动转换 防止失真 // 大数值自动转换 防止失真
.registerConverter(new ExcelBigNumberConvert()) .registerConverter(new ExcelBigNumberConvert())
.registerWriteHandler(new DataWriteHandler(data.get(0).getClass())) .registerWriteHandler(new DataWriteHandler(data.get(0).getClass()))
.build(); .build();
WriteSheet writeSheet = FastExcel.writerSheet().build(); WriteSheet writeSheet = EasyExcel.writerSheet().build();
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build(); FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
// 单表多数据导出 模板格式为 {.属性} // 单表多数据导出 模板格式为 {.属性}
for (T d : data) { for (T d : data) {
@ -304,9 +265,6 @@ public class ExcelUtil {
*/ */
public static void exportTemplateMultiList(Map<String, Object> data, String filename, String templatePath, HttpServletResponse response) { public static void exportTemplateMultiList(Map<String, Object> data, String filename, String templatePath, HttpServletResponse response) {
try { try {
if (CollUtil.isEmpty(data)) {
throw new IllegalArgumentException("数据为空");
}
resetResponse(filename, response); resetResponse(filename, response);
ServletOutputStream os = response.getOutputStream(); ServletOutputStream os = response.getOutputStream();
exportTemplateMultiList(data, templatePath, os); exportTemplateMultiList(data, templatePath, os);
@ -327,9 +285,6 @@ public class ExcelUtil {
*/ */
public static void exportTemplateMultiSheet(List<Map<String, Object>> data, String filename, String templatePath, HttpServletResponse response) { public static void exportTemplateMultiSheet(List<Map<String, Object>> data, String filename, String templatePath, HttpServletResponse response) {
try { try {
if (CollUtil.isEmpty(data)) {
throw new IllegalArgumentException("数据为空");
}
resetResponse(filename, response); resetResponse(filename, response);
ServletOutputStream os = response.getOutputStream(); ServletOutputStream os = response.getOutputStream();
exportTemplateMultiSheet(data, templatePath, os); exportTemplateMultiSheet(data, templatePath, os);
@ -348,14 +303,17 @@ public class ExcelUtil {
* @param os 输出流 * @param os 输出流
*/ */
public static void exportTemplateMultiList(Map<String, Object> data, String templatePath, OutputStream os) { public static void exportTemplateMultiList(Map<String, Object> data, String templatePath, OutputStream os) {
if (CollUtil.isEmpty(data)) {
throw new IllegalArgumentException("数据为空");
}
ClassPathResource templateResource = new ClassPathResource(templatePath); ClassPathResource templateResource = new ClassPathResource(templatePath);
ExcelWriter excelWriter = FastExcel.write(os) ExcelWriter excelWriter = EasyExcel.write(os)
.withTemplate(templateResource.getStream()) .withTemplate(templateResource.getStream())
.autoCloseStream(false) .autoCloseStream(false)
// 大数值自动转换 防止失真 // 大数值自动转换 防止失真
.registerConverter(new ExcelBigNumberConvert()) .registerConverter(new ExcelBigNumberConvert())
.build(); .build();
WriteSheet writeSheet = FastExcel.writerSheet().build(); WriteSheet writeSheet = EasyExcel.writerSheet().build();
for (Map.Entry<String, Object> map : data.entrySet()) { for (Map.Entry<String, Object> map : data.entrySet()) {
// 设置列表后续还有数据 // 设置列表后续还有数据
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build(); FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
@ -379,15 +337,18 @@ public class ExcelUtil {
* @param os 输出流 * @param os 输出流
*/ */
public static void exportTemplateMultiSheet(List<Map<String, Object>> data, String templatePath, OutputStream os) { public static void exportTemplateMultiSheet(List<Map<String, Object>> data, String templatePath, OutputStream os) {
if (CollUtil.isEmpty(data)) {
throw new IllegalArgumentException("数据为空");
}
ClassPathResource templateResource = new ClassPathResource(templatePath); ClassPathResource templateResource = new ClassPathResource(templatePath);
ExcelWriter excelWriter = FastExcel.write(os) ExcelWriter excelWriter = EasyExcel.write(os)
.withTemplate(templateResource.getStream()) .withTemplate(templateResource.getStream())
.autoCloseStream(false) .autoCloseStream(false)
// 大数值自动转换 防止失真 // 大数值自动转换 防止失真
.registerConverter(new ExcelBigNumberConvert()) .registerConverter(new ExcelBigNumberConvert())
.build(); .build();
for (int i = 0; i < data.size(); i++) { for (int i = 0; i < data.size(); i++) {
WriteSheet writeSheet = FastExcel.writerSheet(i).build(); WriteSheet writeSheet = EasyExcel.writerSheet(i).build();
for (Map.Entry<String, Object> map : data.get(i).entrySet()) { for (Map.Entry<String, Object> map : data.get(i).entrySet()) {
// 设置列表后续还有数据 // 设置列表后续还有数据
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build(); FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();

View File

@ -1,127 +0,0 @@
package org.dromara.common.excel.utils;
import cn.idev.excel.ExcelWriter;
import cn.idev.excel.FastExcel;
import cn.idev.excel.context.WriteContext;
import cn.idev.excel.write.builder.ExcelWriterSheetBuilder;
import cn.idev.excel.write.builder.ExcelWriterTableBuilder;
import cn.idev.excel.write.metadata.WriteSheet;
import cn.idev.excel.write.metadata.WriteTable;
import cn.idev.excel.write.metadata.fill.FillConfig;
import java.util.Collection;
import java.util.function.Supplier;
/**
* ExcelWriterWrapper Excel写出包装器
* <br>
* 提供了一组与 ExcelWriter 一一对应的写出方法,避免直接提供 ExcelWriter 而导致的一些不可控问题比如提前关闭了IO流等
*
* @author 秋辞未寒
* @see ExcelWriter
*/
public record ExcelWriterWrapper<T>(ExcelWriter excelWriter) {
public void write(Collection<T> data, WriteSheet writeSheet) {
excelWriter.write(data, writeSheet);
}
public void write(Supplier<Collection<T>> supplier, WriteSheet writeSheet) {
excelWriter.write(supplier.get(), writeSheet);
}
public void write(Collection<T> data, WriteSheet writeSheet, WriteTable writeTable) {
excelWriter.write(data, writeSheet, writeTable);
}
public void write(Supplier<Collection<T>> supplier, WriteSheet writeSheet, WriteTable writeTable) {
excelWriter.write(supplier.get(), writeSheet, writeTable);
}
public void fill(Object data, WriteSheet writeSheet) {
excelWriter.fill(data, writeSheet);
}
public void fill(Object data, FillConfig fillConfig, WriteSheet writeSheet) {
excelWriter.fill(data, fillConfig, writeSheet);
}
public void fill(Supplier<Object> supplier, WriteSheet writeSheet) {
excelWriter.fill(supplier, writeSheet);
}
public void fill(Supplier<Object> supplier, FillConfig fillConfig, WriteSheet writeSheet) {
excelWriter.fill(supplier, fillConfig, writeSheet);
}
public WriteContext writeContext() {
return excelWriter.writeContext();
}
/**
* 创建一个 ExcelWriterWrapper
*
* @param excelWriter ExcelWriter
* @return ExcelWriterWrapper
*/
public static <T> ExcelWriterWrapper<T> of(ExcelWriter excelWriter) {
return new ExcelWriterWrapper<>(excelWriter);
}
// -------------------------------- sheet start
public static WriteSheet buildSheet(Integer sheetNo, String sheetName) {
return sheetBuilder(sheetNo, sheetName).build();
}
public static WriteSheet buildSheet(Integer sheetNo) {
return sheetBuilder(sheetNo).build();
}
public static WriteSheet buildSheet(String sheetName) {
return sheetBuilder(sheetName).build();
}
public static WriteSheet buildSheet() {
return sheetBuilder().build();
}
public static ExcelWriterSheetBuilder sheetBuilder(Integer sheetNo, String sheetName) {
return FastExcel.writerSheet(sheetNo, sheetName);
}
public static ExcelWriterSheetBuilder sheetBuilder(Integer sheetNo) {
return FastExcel.writerSheet(sheetNo);
}
public static ExcelWriterSheetBuilder sheetBuilder(String sheetName) {
return FastExcel.writerSheet(sheetName);
}
public static ExcelWriterSheetBuilder sheetBuilder() {
return FastExcel.writerSheet();
}
// -------------------------------- sheet end
// -------------------------------- table start
public static WriteTable buildTable(Integer tableNo) {
return tableBuilder(tableNo).build();
}
public static WriteTable buildTable() {
return tableBuilder().build();
}
public static ExcelWriterTableBuilder tableBuilder(Integer tableNo) {
return FastExcel.writerTable(tableNo);
}
public static ExcelWriterTableBuilder tableBuilder() {
return FastExcel.writerTable();
}
// -------------------------------- table end
}

View File

@ -1,13 +1,11 @@
package org.dromara.common.json.config; package org.dromara.common.json.config;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.json.handler.BigNumberSerializer; import org.dromara.common.json.handler.BigNumberSerializer;
import org.dromara.common.json.handler.CustomDateDeserializer; import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
@ -17,7 +15,6 @@ import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.TimeZone; import java.util.TimeZone;
/** /**
@ -30,7 +27,8 @@ import java.util.TimeZone;
public class JacksonConfig { public class JacksonConfig {
@Bean @Bean
public Module registerJavaTimeModule() { public Jackson2ObjectMapperBuilderCustomizer customizer() {
return builder -> {
// 全局配置序列化返回 JSON 处理 // 全局配置序列化返回 JSON 处理
JavaTimeModule javaTimeModule = new JavaTimeModule(); JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE); javaTimeModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE);
@ -40,13 +38,7 @@ public class JacksonConfig {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter)); javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter)); javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
javaTimeModule.addDeserializer(Date.class, new CustomDateDeserializer()); builder.modules(javaTimeModule);
return javaTimeModule;
}
@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
return builder -> {
builder.timeZone(TimeZone.getDefault()); builder.timeZone(TimeZone.getDefault());
log.info("初始化 jackson 配置"); log.info("初始化 jackson 配置");
}; };

View File

@ -1,37 +0,0 @@
package org.dromara.common.json.handler;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import org.dromara.common.core.utils.ObjectUtils;
import java.io.IOException;
import java.util.Date;
/**
* 自定义 Date 类型反序列化处理器(支持多种格式)
*
* @author AprilWind
*/
public class CustomDateDeserializer extends JsonDeserializer<Date> {
/**
* 反序列化逻辑:将字符串转换为 Date 对象
*
* @param p JSON 解析器,用于获取字符串值
* @param ctxt 上下文环境(可用于获取更多配置)
* @return 转换后的 Date 对象,若为空字符串返回 null
* @throws IOException 当字符串格式非法或转换失败时抛出
*/
@Override
public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
DateTime parse = DateUtil.parse(p.getText());
if (ObjectUtils.isNull(parse)) {
return null;
}
return parse.toJdkDate();
}
}

View File

@ -27,7 +27,9 @@ import org.springframework.http.HttpMethod;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.util.*; import java.util.Collection;
import java.util.Map;
import java.util.StringJoiner;
/** /**
* 操作日志记录处理 * 操作日志记录处理
@ -174,29 +176,15 @@ public class LogAspect {
if (ArrayUtil.isEmpty(paramsArray)) { if (ArrayUtil.isEmpty(paramsArray)) {
return params.toString(); return params.toString();
} }
String[] exclude = ArrayUtil.addAll(excludeParamNames, EXCLUDE_PROPERTIES);
for (Object o : paramsArray) { for (Object o : paramsArray) {
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) { if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
String str = ""; String str = JsonUtils.toJsonString(o);
if (o instanceof List<?> list) {
List<Dict> list1 = new ArrayList<>();
for (Object obj : list) {
String str1 = JsonUtils.toJsonString(obj);
Dict dict = JsonUtils.parseMap(str1);
if (MapUtil.isNotEmpty(dict)) {
MapUtil.removeAny(dict, exclude);
list1.add(dict);
}
}
str = JsonUtils.toJsonString(list1);
} else {
str = JsonUtils.toJsonString(o);
Dict dict = JsonUtils.parseMap(str); Dict dict = JsonUtils.parseMap(str);
if (MapUtil.isNotEmpty(dict)) { if (MapUtil.isNotEmpty(dict)) {
MapUtil.removeAny(dict, exclude); MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);
MapUtil.removeAny(dict, excludeParamNames);
str = JsonUtils.toJsonString(dict); str = JsonUtils.toJsonString(dict);
} }
}
params.add(str); params.add(str);
} }
} }

View File

@ -1,54 +0,0 @@
package org.dromara.common.mybatis.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.dromara.common.mybatis.annotation.DataPermission;
import org.dromara.common.mybatis.helper.DataPermissionHelper;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 数据权限注解Advice
*
* @author 秋辞未寒
*/
@Slf4j
public class DataPermissionAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Object target = invocation.getThis();
Method method = invocation.getMethod();
Object[] args = invocation.getArguments();
// 设置权限注解
DataPermissionHelper.setPermission(getDataPermissionAnnotation(target, method, args));
try {
// 执行代理方法
return invocation.proceed();
} finally {
// 清除权限注解
DataPermissionHelper.removePermission();
}
}
/**
* 获取数据权限注解
*/
private DataPermission getDataPermissionAnnotation(Object target, Method method,Object[] args){
DataPermission dataPermission = method.getAnnotation(DataPermission.class);
// 优先获取方法上的注解
if (dataPermission != null) {
return dataPermission;
}
// 方法上没有注解,则获取类上的注解
Class<?> targetClass = target.getClass();
// 如果是 JDK 动态代理则获取真实的Class实例
if (Proxy.isProxyClass(targetClass)) {
targetClass = targetClass.getInterfaces()[0];
}
dataPermission = targetClass.getAnnotation(DataPermission.class);
return dataPermission;
}
}

View File

@ -0,0 +1,50 @@
package org.dromara.common.mybatis.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.dromara.common.mybatis.annotation.DataPermission;
import org.dromara.common.mybatis.helper.DataPermissionHelper;
/**
* 数据权限处理
*
* @author Lion Li
*/
@Slf4j
@Aspect
public class DataPermissionAspect {
/**
* 处理请求前执行
*/
@Before(value = "@annotation(dataPermission)")
public void doBefore(JoinPoint joinPoint, DataPermission dataPermission) {
DataPermissionHelper.setPermission(dataPermission);
}
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "@annotation(dataPermission)")
public void doAfterReturning(JoinPoint joinPoint, DataPermission dataPermission) {
DataPermissionHelper.removePermission();
}
/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "@annotation(dataPermission)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, DataPermission dataPermission, Exception e) {
DataPermissionHelper.removePermission();
}
}

View File

@ -1,39 +0,0 @@
package org.dromara.common.mybatis.aspect;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.mybatis.annotation.DataPermission;
import org.springframework.aop.support.StaticMethodMatcherPointcut;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 数据权限匹配切点
*
* @author 秋辞未寒
*/
@Slf4j
@SuppressWarnings("all")
public class DataPermissionPointcut extends StaticMethodMatcherPointcut {
@Override
public boolean matches(Method method, Class<?> targetClass) {
// 优先匹配方法
// 数据权限注解不对继承生效,所以检查当前方法是否有注解即可,不再往上匹配父类或接口
if (method.isAnnotationPresent(DataPermission.class)) {
return true;
}
// MyBatis 的 Mapper 就是通过 JDK 动态代理实现的,所以这里需要检查是否匹配 JDK 的动态代理
Class<?> targetClassRef = targetClass;
if (Proxy.isProxyClass(targetClassRef)) {
// 数据权限注解不对继承生效,但由于 SpringIOC 容器拿到的实际上是 MyBatis 代理过后的 Mapper而 targetClass.isAnnotationPresent 实际匹配的是 Proxy 类的注解,不会查找代理类。
// 所以这里不能用 targetClass.isAnnotationPresent只能用 AnnotatedElementUtils.hasAnnotation 或 targetClass.getInterfaces()[0].isAnnotationPresent 去做匹配,以检查被代理的 MapperClass 是否具有注解
// 原理JDK 动态代理本质上就是对接口进行实现然后对具体的接口实现做代理,所以直接通过接口可以拿到实际的 MapperClass
targetClassRef = targetClass.getInterfaces()[0];
}
return targetClassRef.isAnnotationPresent(DataPermission.class);
}
}

View File

@ -1,33 +0,0 @@
package org.dromara.common.mybatis.aspect;
import org.aopalliance.aop.Advice;
import org.springframework.aop.Pointcut;
import org.springframework.aop.support.AbstractPointcutAdvisor;
/**
* 数据权限注解切面定义
*
* @author 秋辞未寒
*/
@SuppressWarnings("all")
public class DataPermissionPointcutAdvisor extends AbstractPointcutAdvisor {
private final Advice advice;
private final Pointcut pointcut;
public DataPermissionPointcutAdvisor() {
this.advice = new DataPermissionAdvice();
this.pointcut = new DataPermissionPointcut();
}
@Override
public Pointcut getPointcut() {
return this.pointcut;
}
@Override
public Advice getAdvice() {
return this.advice;
}
}

View File

@ -11,17 +11,15 @@ import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerIntercept
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import org.dromara.common.core.factory.YmlPropertySourceFactory; import org.dromara.common.core.factory.YmlPropertySourceFactory;
import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.mybatis.aspect.DataPermissionPointcutAdvisor; import org.dromara.common.mybatis.aspect.DataPermissionAspect;
import org.dromara.common.mybatis.handler.InjectionMetaObjectHandler; import org.dromara.common.mybatis.handler.InjectionMetaObjectHandler;
import org.dromara.common.mybatis.handler.MybatisExceptionHandler; import org.dromara.common.mybatis.handler.MybatisExceptionHandler;
import org.dromara.common.mybatis.handler.PlusPostInitTableInfoHandler; import org.dromara.common.mybatis.handler.PlusPostInitTableInfoHandler;
import org.dromara.common.mybatis.interceptor.PlusDataPermissionInterceptor; import org.dromara.common.mybatis.interceptor.PlusDataPermissionInterceptor;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource; import org.springframework.context.annotation.PropertySource;
import org.springframework.context.annotation.Role;
import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.EnableTransactionManagement;
/** /**
@ -29,7 +27,6 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
* *
* @author Lion Li * @author Lion Li
*/ */
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@EnableTransactionManagement(proxyTargetClass = true) @EnableTransactionManagement(proxyTargetClass = true)
@MapperScan("${mybatis-plus.mapperPackage}") @MapperScan("${mybatis-plus.mapperPackage}")
@PropertySource(value = "classpath:common-mybatis.yml", factory = YmlPropertySourceFactory.class) @PropertySource(value = "classpath:common-mybatis.yml", factory = YmlPropertySourceFactory.class)
@ -57,16 +54,15 @@ public class MybatisPlusConfig {
* 数据权限拦截器 * 数据权限拦截器
*/ */
public PlusDataPermissionInterceptor dataPermissionInterceptor() { public PlusDataPermissionInterceptor dataPermissionInterceptor() {
return new PlusDataPermissionInterceptor(); return new PlusDataPermissionInterceptor(SpringUtils.getProperty("mybatis-plus.mapperPackage"));
} }
/** /**
* 数据权限切面处理器 * 数据权限切面处理器
*/ */
@Bean @Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) public DataPermissionAspect dataPermissionAspect() {
public DataPermissionPointcutAdvisor dataPermissionPointcutAdvisor() { return new DataPermissionAspect();
return new DataPermissionPointcutAdvisor();
} }
/** /**

View File

@ -1,6 +1,5 @@
package org.dromara.common.mybatis.core.page; package org.dromara.common.mybatis.core.page;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.http.HttpStatus; import cn.hutool.http.HttpStatus;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.Data; import lombok.Data;
@ -89,19 +88,4 @@ public class TableDataInfo<T> implements Serializable {
return rspData; return rspData;
} }
/**
* 根据原始数据列表和分页参数,构建表格分页数据对象(用于假分页)
*
* @param list 原始数据列表(全部数据)
* @param page 分页参数对象(包含当前页码、每页大小等)
* @return 构造好的分页结果 TableDataInfo<T>
*/
public static <T> TableDataInfo<T> build(List<T> list, IPage<T> page) {
if (CollUtil.isEmpty(list)) {
return TableDataInfo.build();
}
List<T> pageList = CollUtil.page((int) page.getCurrent() - 1, (int) page.getSize(), list);
return new TableDataInfo<>(pageList, list.size());
}
} }

View File

@ -42,46 +42,17 @@ public enum DataBaseType {
* 根据数据库产品名称查找对应的数据库类型 * 根据数据库产品名称查找对应的数据库类型
* *
* @param databaseProductName 数据库产品名称 * @param databaseProductName 数据库产品名称
* @return 对应的数据库类型枚举值 * @return 对应的数据库类型枚举值,如果未找到则返回 null
*/ */
public static DataBaseType find(String databaseProductName) { public static DataBaseType find(String databaseProductName) {
if (StringUtils.isBlank(databaseProductName)) { if (StringUtils.isBlank(databaseProductName)) {
return MY_SQL; return null;
} }
for (DataBaseType type : values()) { for (DataBaseType type : values()) {
if (type.getType().equals(databaseProductName)) { if (type.getType().equals(databaseProductName)) {
return type; return type;
} }
} }
return MY_SQL; return null;
} }
/**
* 判断是否为 MySQL 类型
*/
public boolean isMySql() {
return this == MY_SQL;
}
/**
* 判断是否为 Oracle 类型
*/
public boolean isOracle() {
return this == ORACLE;
}
/**
* 判断是否为 PostgreSQL 类型
*/
public boolean isPostgreSql() {
return this == POSTGRE_SQL;
}
/**
* 判断是否为 SQL Server 类型
*/
public boolean isSqlServer() {
return this == SQL_SERVER;
}
} }

View File

@ -22,11 +22,6 @@ import java.util.Date;
@Slf4j @Slf4j
public class InjectionMetaObjectHandler implements MetaObjectHandler { public class InjectionMetaObjectHandler implements MetaObjectHandler {
/**
* 如果用户不存在默认注入-1代表无用户
*/
private static final Long DEFAULT_USER_ID = -1L;
/** /**
* 插入填充方法,用于在插入数据时自动填充实体对象中的创建时间、更新时间、创建人、更新人等信息 * 插入填充方法,用于在插入数据时自动填充实体对象中的创建时间、更新时间、创建人、更新人等信息
* *
@ -50,11 +45,6 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
baseEntity.setCreateBy(userId); baseEntity.setCreateBy(userId);
baseEntity.setUpdateBy(userId); baseEntity.setUpdateBy(userId);
baseEntity.setCreateDept(ObjectUtils.notNull(baseEntity.getCreateDept(), loginUser.getDeptId())); baseEntity.setCreateDept(ObjectUtils.notNull(baseEntity.getCreateDept(), loginUser.getDeptId()));
} else {
// 填充创建人、更新人和创建部门信息
baseEntity.setCreateBy(DEFAULT_USER_ID);
baseEntity.setUpdateBy(DEFAULT_USER_ID);
baseEntity.setCreateDept(ObjectUtils.notNull(baseEntity.getCreateDept(), DEFAULT_USER_ID));
} }
} }
} else { } else {
@ -84,8 +74,6 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
Long userId = LoginHelper.getUserId(); Long userId = LoginHelper.getUserId();
if (ObjectUtil.isNotNull(userId)) { if (ObjectUtil.isNotNull(userId)) {
baseEntity.setUpdateBy(userId); baseEntity.setUpdateBy(userId);
} else {
baseEntity.setUpdateBy(DEFAULT_USER_ID);
} }
} else { } else {
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date()); this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
@ -105,6 +93,7 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
try { try {
loginUser = LoginHelper.getLoginUser(); loginUser = LoginHelper.getLoginUser();
} catch (Exception e) { } catch (Exception e) {
log.warn("自动注入警告 => 用户未登录");
return null; return null;
} }
return loginUser; return loginUser;

View File

@ -1,6 +1,5 @@
package org.dromara.common.mybatis.handler; package org.dromara.common.mybatis.handler;
import cn.hutool.http.HttpStatus;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R; import org.dromara.common.core.domain.R;
@ -26,7 +25,7 @@ public class MybatisExceptionHandler {
public R<Void> handleDuplicateKeyException(DuplicateKeyException e, HttpServletRequest request) { public R<Void> handleDuplicateKeyException(DuplicateKeyException e, HttpServletRequest request) {
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
log.error("请求地址'{}',数据库中已存在记录'{}'", requestURI, e.getMessage()); log.error("请求地址'{}',数据库中已存在记录'{}'", requestURI, e.getMessage());
return R.fail(HttpStatus.HTTP_CONFLICT, "数据库中已存在该记录,请联系管理员确认"); return R.fail("数据库中已存在该记录,请联系管理员确认");
} }
/** /**
@ -36,12 +35,12 @@ public class MybatisExceptionHandler {
public R<Void> handleCannotFindDataSourceException(MyBatisSystemException e, HttpServletRequest request) { public R<Void> handleCannotFindDataSourceException(MyBatisSystemException e, HttpServletRequest request) {
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
String message = e.getMessage(); String message = e.getMessage();
if (StringUtils.contains(message, "CannotFindDataSourceException")) { if (StringUtils.contains("CannotFindDataSourceException", message)) {
log.error("请求地址'{}', 未找到数据源", requestURI); log.error("请求地址'{}', 未找到数据源", requestURI);
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, "未找到数据源,请联系管理员确认"); return R.fail("未找到数据源,请联系管理员确认");
} }
log.error("请求地址'{}', Mybatis系统异常", requestURI, e); log.error("请求地址'{}', Mybatis系统异常", requestURI, e);
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, message); return R.fail(message);
} }
} }

View File

@ -1,5 +1,6 @@
package org.dromara.common.mybatis.handler; package org.dromara.common.mybatis.handler;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@ -9,6 +10,7 @@ import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression; import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import org.apache.ibatis.io.Resources;
import org.dromara.common.core.domain.dto.RoleDTO; import org.dromara.common.core.domain.dto.RoleDTO;
import org.dromara.common.core.domain.model.LoginUser; import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.exception.ServiceException;
@ -20,13 +22,22 @@ import org.dromara.common.mybatis.annotation.DataPermission;
import org.dromara.common.mybatis.enums.DataScopeType; import org.dromara.common.mybatis.enums.DataScopeType;
import org.dromara.common.mybatis.helper.DataPermissionHelper; import org.dromara.common.mybatis.helper.DataPermissionHelper;
import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.expression.*; import org.springframework.expression.*;
import org.springframework.expression.common.TemplateParserContext; import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.util.ClassUtils;
import java.lang.reflect.Method;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function; import java.util.function.Function;
/** /**
@ -38,6 +49,11 @@ import java.util.function.Function;
@Slf4j @Slf4j
public class PlusDataPermissionHandler { public class PlusDataPermissionHandler {
/**
* 类名称与注解的映射关系缓存(由于aop无法拦截mybatis接口类上的注解 只能通过启动预扫描的方式进行)
*/
private final Map<String, DataPermission> dataPermissionCacheMap = new ConcurrentHashMap<>();
/** /**
* spel 解析器 * spel 解析器
*/ */
@ -48,17 +64,27 @@ public class PlusDataPermissionHandler {
*/ */
private final BeanResolver beanResolver = new BeanFactoryResolver(SpringUtils.getBeanFactory()); private final BeanResolver beanResolver = new BeanFactoryResolver(SpringUtils.getBeanFactory());
/**
* 构造方法,扫描指定包下的 Mapper 类并初始化缓存
*
* @param mapperPackage Mapper 类所在的包路径
*/
public PlusDataPermissionHandler(String mapperPackage) {
scanMapperClasses(mapperPackage);
}
/** /**
* 获取数据过滤条件的 SQL 片段 * 获取数据过滤条件的 SQL 片段
* *
* @param where 原始的查询条件表达式 * @param where 原始的查询条件表达式
* @param mappedStatementId Mapper 方法的 ID
* @param isSelect 是否为查询语句 * @param isSelect 是否为查询语句
* @return 数据过滤条件的 SQL 片段 * @return 数据过滤条件的 SQL 片段
*/ */
public Expression getSqlSegment(Expression where, boolean isSelect) { public Expression getSqlSegment(Expression where, String mappedStatementId, boolean isSelect) {
try { try {
// 获取数据权限配置 // 获取数据权限配置
DataPermission dataPermission = getDataPermission(); DataPermission dataPermission = getDataPermission(mappedStatementId);
// 获取当前登录用户信息 // 获取当前登录用户信息
LoginUser currentUser = DataPermissionHelper.getVariable("user"); LoginUser currentUser = DataPermissionHelper.getVariable("user");
if (ObjectUtil.isNull(currentUser)) { if (ObjectUtil.isNull(currentUser)) {
@ -180,22 +206,92 @@ public class PlusDataPermissionHandler {
return StringUtils.EMPTY; return StringUtils.EMPTY;
} }
/**
* 扫描指定包下的 Mapper 类,并查找其中带有特定注解的方法或类
*
* @param mapperPackage Mapper 类所在的包路径
*/
private void scanMapperClasses(String mapperPackage) {
// 创建资源解析器和元数据读取工厂
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory();
// 将 Mapper 包路径按分隔符拆分为数组
String[] packagePatternArray = StringUtils.splitPreserveAllTokens(mapperPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
String classpath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX;
try {
for (String packagePattern : packagePatternArray) {
// 将包路径转换为资源路径
String path = ClassUtils.convertClassNameToResourcePath(packagePattern);
// 获取指定路径下的所有 .class 文件资源
Resource[] resources = resolver.getResources(classpath + path + "/*.class");
for (Resource resource : resources) {
// 获取资源的类元数据
ClassMetadata classMetadata = factory.getMetadataReader(resource).getClassMetadata();
// 获取资源对应的类对象
Class<?> clazz = Resources.classForName(classMetadata.getClassName());
// 查找类中的特定注解
findAnnotation(clazz);
}
}
} catch (Exception e) {
log.error("初始化数据安全缓存时出错:{}", e.getMessage());
}
}
/**
* 在指定的类中查找特定的注解 DataPermission并将带有这个注解的方法或类存储到 dataPermissionCacheMap 中
*
* @param clazz 要查找的类
*/
private void findAnnotation(Class<?> clazz) {
DataPermission dataPermission;
for (Method method : clazz.getMethods()) {
if (method.isDefault() || method.isVarArgs()) {
continue;
}
String mappedStatementId = clazz.getName() + "." + method.getName();
if (AnnotationUtil.hasAnnotation(method, DataPermission.class)) {
dataPermission = AnnotationUtil.getAnnotation(method, DataPermission.class);
dataPermissionCacheMap.put(mappedStatementId, dataPermission);
}
}
if (AnnotationUtil.hasAnnotation(clazz, DataPermission.class)) {
dataPermission = AnnotationUtil.getAnnotation(clazz, DataPermission.class);
dataPermissionCacheMap.put(clazz.getName(), dataPermission);
}
}
/** /**
* 根据映射语句 ID 或类名获取对应的 DataPermission 注解对象 * 根据映射语句 ID 或类名获取对应的 DataPermission 注解对象
* *
* @param mapperId 映射语句 ID
* @return DataPermission 注解对象,如果不存在则返回 null * @return DataPermission 注解对象,如果不存在则返回 null
*/ */
public DataPermission getDataPermission() { public DataPermission getDataPermission(String mapperId) {
// 检查上下文中是否包含映射语句 ID 对应的 DataPermission 注解对象
if (DataPermissionHelper.getPermission() != null) {
return DataPermissionHelper.getPermission(); return DataPermissionHelper.getPermission();
} }
// 检查缓存中是否包含映射语句 ID 对应的 DataPermission 注解对象
if (dataPermissionCacheMap.containsKey(mapperId)) {
return dataPermissionCacheMap.get(mapperId);
}
// 如果缓存中不包含映射语句 ID 对应的 DataPermission 注解对象,则尝试使用类名作为键查找
String clazzName = mapperId.substring(0, mapperId.lastIndexOf("."));
if (dataPermissionCacheMap.containsKey(clazzName)) {
return dataPermissionCacheMap.get(clazzName);
}
return null;
}
/** /**
* 检查给定的映射语句 ID 是否有效,即是否能够找到对应的 DataPermission 注解对象 * 检查给定的映射语句 ID 是否有效,即是否能够找到对应的 DataPermission 注解对象
* *
* @param mapperId 映射语句 ID
* @return 如果找到对应的 DataPermission 注解对象,则返回 false否则返回 true * @return 如果找到对应的 DataPermission 注解对象,则返回 false否则返回 true
*/ */
public boolean invalid() { public boolean invalid(String mapperId) {
return getDataPermission() == null; return getDataPermission(mapperId) == null;
} }
/** /**

View File

@ -26,14 +26,7 @@ public class DataBaseHelper {
private static final DynamicRoutingDataSource DS = SpringUtils.getBean(DynamicRoutingDataSource.class); private static final DynamicRoutingDataSource DS = SpringUtils.getBean(DynamicRoutingDataSource.class);
/** /**
* 获取当前数据源对应的数据库类型 * 获取当前数据库类型
* <p>
* 通过 DynamicRoutingDataSource 获取当前线程绑定的数据源,
* 然后从数据源获取数据库连接,利用连接的元数据获取数据库产品名称,
* 最后调用 DataBaseType.find 方法将数据库名称转换为对应的枚举类型
*
* @return 当前数据库对应的 DataBaseType 枚举,找不到时默认返回 MY_SQL
* @throws ServiceException 当获取数据库连接或元数据出现异常时抛出业务异常
*/ */
public static DataBaseType getDataBaseType() { public static DataBaseType getDataBaseType() {
DataSource dataSource = DS.determineDataSource(); DataSource dataSource = DS.determineDataSource();
@ -46,31 +39,37 @@ public class DataBaseHelper {
} }
} }
/** public static boolean isMySql() {
* 根据当前数据库类型,生成兼容的 FIND_IN_SET 语句片段 return DataBaseType.MY_SQL == getDataBaseType();
* <p> }
* 用于判断指定值是否存在于逗号分隔的字符串列中SQL写法根据不同数据库方言自动切换
* - Oracle 使用 instr 函数 public static boolean isOracle() {
* - PostgreSQL 使用 strpos 函数 return DataBaseType.ORACLE == getDataBaseType();
* - SQL Server 使用 charindex 函数 }
* - 其他默认使用 MySQL 的 find_in_set 函数
* public static boolean isPostgerSql() {
* @param var1 要查找的值(支持任意类型,内部会转换成字符串) return DataBaseType.POSTGRE_SQL == getDataBaseType();
* @param var2 存储逗号分隔值的数据库列名 }
* @return 适用于当前数据库的 SQL 条件字符串,通常用于 where 或 apply 中拼接
*/ public static boolean isSqlServer() {
return DataBaseType.SQL_SERVER == getDataBaseType();
}
public static String findInSet(Object var1, String var2) { public static String findInSet(Object var1, String var2) {
DataBaseType dataBasyType = getDataBaseType();
String var = Convert.toStr(var1); String var = Convert.toStr(var1);
return switch (getDataBaseType()) { if (dataBasyType == DataBaseType.SQL_SERVER) {
// instr(',0,100,101,' , ',100,') <> 0
case ORACLE -> "instr(','||%s||',' , ',%s,') <> 0".formatted(var2, var);
// (select strpos(',0,100,101,' , ',100,')) <> 0
case POSTGRE_SQL -> "(select strpos(','||%s||',' , ',%s,')) <> 0".formatted(var2, var);
// charindex(',100,' , ',0,100,101,') <> 0 // charindex(',100,' , ',0,100,101,') <> 0
case SQL_SERVER -> "charindex(',%s,' , ','+%s+',') <> 0".formatted(var, var2); return "charindex(',%s,' , ','+%s+',') <> 0".formatted(var, var2);
} else if (dataBasyType == DataBaseType.POSTGRE_SQL) {
// (select strpos(',0,100,101,' , ',100,')) <> 0
return "(select strpos(','||%s||',' , ',%s,')) <> 0".formatted(var2, var);
} else if (dataBasyType == DataBaseType.ORACLE) {
// instr(',0,100,101,' , ',100,') <> 0
return "instr(','||%s||',' , ',%s,') <> 0".formatted(var2, var);
}
// find_in_set(100 , '0,100,101') // find_in_set(100 , '0,100,101')
default -> "find_in_set('%s' , %s) <> 0".formatted(var, var2); return "find_in_set('%s' , %s) <> 0".formatted(var, var2);
};
} }
/** /**

View File

@ -35,7 +35,16 @@ import java.util.List;
@Slf4j @Slf4j
public class PlusDataPermissionInterceptor extends BaseMultiTableInnerInterceptor implements InnerInterceptor { public class PlusDataPermissionInterceptor extends BaseMultiTableInnerInterceptor implements InnerInterceptor {
private final PlusDataPermissionHandler dataPermissionHandler = new PlusDataPermissionHandler(); private final PlusDataPermissionHandler dataPermissionHandler;
/**
* 构造函数,初始化 PlusDataPermissionHandler 实例
*
* @param mapperPackage 扫描的映射器包
*/
public PlusDataPermissionInterceptor(String mapperPackage) {
this.dataPermissionHandler = new PlusDataPermissionHandler(mapperPackage);
}
/** /**
* 在执行查询之前,检查并处理数据权限相关逻辑 * 在执行查询之前,检查并处理数据权限相关逻辑
@ -55,7 +64,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto
return; return;
} }
// 检查是否缺少有效的数据权限注解 // 检查是否缺少有效的数据权限注解
if (dataPermissionHandler.invalid()) { if (dataPermissionHandler.invalid(ms.getId())) {
return; return;
} }
// 解析 sql 分配对应方法 // 解析 sql 分配对应方法
@ -83,7 +92,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto
return; return;
} }
// 检查是否缺少有效的数据权限注解 // 检查是否缺少有效的数据权限注解
if (dataPermissionHandler.invalid()) { if (dataPermissionHandler.invalid(ms.getId())) {
return; return;
} }
PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql(); PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
@ -119,7 +128,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto
*/ */
@Override @Override
protected void processUpdate(Update update, int index, String sql, Object obj) { protected void processUpdate(Update update, int index, String sql, Object obj) {
Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), false); Expression sqlSegment = dataPermissionHandler.getSqlSegment(update.getWhere(), (String) obj, false);
if (null != sqlSegment) { if (null != sqlSegment) {
update.setWhere(sqlSegment); update.setWhere(sqlSegment);
} }
@ -135,7 +144,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto
*/ */
@Override @Override
protected void processDelete(Delete delete, int index, String sql, Object obj) { protected void processDelete(Delete delete, int index, String sql, Object obj) {
Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), false); Expression sqlSegment = dataPermissionHandler.getSqlSegment(delete.getWhere(), (String) obj, false);
if (null != sqlSegment) { if (null != sqlSegment) {
delete.setWhere(sqlSegment); delete.setWhere(sqlSegment);
} }
@ -148,7 +157,7 @@ public class PlusDataPermissionInterceptor extends BaseMultiTableInnerIntercepto
* @param mappedStatementId 映射语句的 ID * @param mappedStatementId 映射语句的 ID
*/ */
protected void setWhere(PlainSelect plainSelect, String mappedStatementId) { protected void setWhere(PlainSelect plainSelect, String mappedStatementId) {
Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), true); Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), mappedStatementId, true);
if (null != sqlSegment) { if (null != sqlSegment) {
plainSelect.setWhere(sqlSegment); plainSelect.setWhere(sqlSegment);
} }

View File

@ -2,7 +2,6 @@ package org.dromara.common.oss.core;
import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.utils.DateUtils; import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.StringUtils;
@ -14,7 +13,9 @@ import org.dromara.common.oss.exception.OssException;
import org.dromara.common.oss.properties.OssProperties; import org.dromara.common.oss.properties.OssProperties;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.async.*; import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody;
import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient; import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.S3AsyncClient;
@ -28,12 +29,9 @@ import software.amazon.awssdk.transfer.s3.progress.LoggingTransferListener;
import java.io.*; import java.io.*;
import java.net.URI; import java.net.URI;
import java.net.URL; import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Duration; import java.time.Duration;
import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
/** /**
@ -42,7 +40,6 @@ import java.util.function.Consumer;
* *
* @author AprilWind * @author AprilWind
*/ */
@Slf4j
public class OssClient { public class OssClient {
/** /**
@ -180,12 +177,12 @@ public class OssClient {
// 创建异步请求体length如果为空会报错 // 创建异步请求体length如果为空会报错
BlockingInputStreamAsyncRequestBody body = BlockingInputStreamAsyncRequestBody.builder() BlockingInputStreamAsyncRequestBody body = BlockingInputStreamAsyncRequestBody.builder()
.contentLength(length) .contentLength(length)
.subscribeTimeout(Duration.ofSeconds(120)) .subscribeTimeout(Duration.ofSeconds(30))
.build(); .build();
// 使用 transferManager 进行上传 // 使用 transferManager 进行上传
Upload upload = transferManager.upload( Upload upload = transferManager.upload(
x -> x.requestBody(body).addTransferListener(LoggingTransferListener.create()) x -> x.requestBody(body)
.putObjectRequest( .putObjectRequest(
y -> y.bucket(properties.getBucketName()) y -> y.bucket(properties.getBucketName())
.key(key) .key(key)
@ -240,61 +237,30 @@ public class OssClient {
* @param key 文件在 Amazon S3 中的对象键 * @param key 文件在 Amazon S3 中的对象键
* @param out 输出流 * @param out 输出流
* @param consumer 自定义处理逻辑 * @param consumer 自定义处理逻辑
* @return 输出流中写入的字节数(长度)
* @throws OssException 如果下载失败,抛出自定义异常 * @throws OssException 如果下载失败,抛出自定义异常
*/ */
public void download(String key, OutputStream out, Consumer<Long> consumer) { public void download(String key, OutputStream out, Consumer<Long> consumer) {
try {
this.download(key, consumer).writeTo(out);
} catch (Exception e) {
throw new OssException("文件下载失败,错误信息:[" + e.getMessage() + "]");
}
}
/**
* 下载文件从 Amazon S3 到 输出流
*
* @param key 文件在 Amazon S3 中的对象键
* @param contentLengthConsumer 文件大小消费者函数
* @return 写出订阅器
* @throws OssException 如果下载失败,抛出自定义异常
*/
public WriteOutSubscriber<OutputStream> download(String key, Consumer<Long> contentLengthConsumer) {
try { try {
// 构建下载请求 // 构建下载请求
DownloadRequest<ResponsePublisher<GetObjectResponse>> publisherDownloadRequest = DownloadRequest.builder() DownloadRequest<ResponseInputStream<GetObjectResponse>> downloadRequest = DownloadRequest.builder()
// 文件对象 // 文件对象
.getObjectRequest(y -> y.bucket(properties.getBucketName()) .getObjectRequest(y -> y.bucket(properties.getBucketName())
.key(key) .key(key)
.build()) .build())
.addTransferListener(LoggingTransferListener.create()) .addTransferListener(LoggingTransferListener.create())
// 使用发布订阅转换器 // 使用订阅转换器
.responseTransformer(AsyncResponseTransformer.toPublisher()) .responseTransformer(AsyncResponseTransformer.toBlockingInputStream())
.build(); .build();
// 使用 S3TransferManager 下载文件 // 使用 S3TransferManager 下载文件
Download<ResponsePublisher<GetObjectResponse>> publisherDownload = transferManager.download(publisherDownloadRequest); Download<ResponseInputStream<GetObjectResponse>> responseFuture = transferManager.download(downloadRequest);
// 获取下载发布订阅转换器 // 输出到流中
ResponsePublisher<GetObjectResponse> publisher = publisherDownload.completionFuture().join().result(); try (ResponseInputStream<GetObjectResponse> responseStream = responseFuture.completionFuture().join().result()) { // auto-closeable stream
// 执行文件大小消费者函数 if (consumer != null) {
Optional.ofNullable(contentLengthConsumer) consumer.accept(responseStream.response().contentLength());
.ifPresent(lengthConsumer -> lengthConsumer.accept(publisher.response().contentLength()));
// 构建写出订阅器对象
return out -> {
// 创建可写入的字节通道
try(WritableByteChannel channel = Channels.newChannel(out)){
// 订阅数据
publisher.subscribe(byteBuffer -> {
while (byteBuffer.hasRemaining()) {
try {
channel.write(byteBuffer);
} catch (IOException e) {
throw new RuntimeException(e);
} }
responseStream.transferTo(out); // 阻塞调用线程 blocks the calling thread
} }
}).join();
}
};
} catch (Exception e) { } catch (Exception e) {
throw new OssException("文件下载失败,错误信息:[" + e.getMessage() + "]"); throw new OssException("文件下载失败,错误信息:[" + e.getMessage() + "]");
} }

View File

@ -1,15 +0,0 @@
package org.dromara.common.oss.core;
import java.io.IOException;
/**
* 写出订阅器
*
* @author 秋辞未寒
*/
@FunctionalInterface
public interface WriteOutSubscriber<T> {
void writeTo(T out) throws IOException;
}

View File

@ -145,25 +145,18 @@ public class PlusSpringCacheManager implements CacheManager {
if (array.length > 3) { if (array.length > 3) {
config.setMaxSize(Integer.parseInt(array[3])); config.setMaxSize(Integer.parseInt(array[3]));
} }
int local = 1;
if (array.length > 4) {
local = Integer.parseInt(array[4]);
}
if (config.getMaxIdleTime() == 0 && config.getTTL() == 0 && config.getMaxSize() == 0) { if (config.getMaxIdleTime() == 0 && config.getTTL() == 0 && config.getMaxSize() == 0) {
return createMap(name, config, local); return createMap(name, config);
} }
return createMapCache(name, config, local); return createMapCache(name, config);
} }
private Cache createMap(String name, CacheConfig config, int local) { private Cache createMap(String name, CacheConfig config) {
RMap<Object, Object> map = RedisUtils.getClient().getMap(name); RMap<Object, Object> map = RedisUtils.getClient().getMap(name);
Cache cache = new RedissonCache(map, allowNullValues); Cache cache = new CaffeineCacheDecorator(name, new RedissonCache(map, allowNullValues));
if (local == 1) {
cache = new CaffeineCacheDecorator(name, cache);
}
if (transactionAware) { if (transactionAware) {
cache = new TransactionAwareCacheDecorator(cache); cache = new TransactionAwareCacheDecorator(cache);
} }
@ -174,13 +167,10 @@ public class PlusSpringCacheManager implements CacheManager {
return cache; return cache;
} }
private Cache createMapCache(String name, CacheConfig config, int local) { private Cache createMapCache(String name, CacheConfig config) {
RMapCache<Object, Object> map = RedisUtils.getClient().getMapCache(name); RMapCache<Object, Object> map = RedisUtils.getClient().getMapCache(name);
Cache cache = new RedissonCache(map, config, allowNullValues); Cache cache = new CaffeineCacheDecorator(name, new RedissonCache(map, config, allowNullValues));
if (local == 1) {
cache = new CaffeineCacheDecorator(name, cache);
}
if (transactionAware) { if (transactionAware) {
cache = new TransactionAwareCacheDecorator(cache); cache = new TransactionAwareCacheDecorator(cache);
} }

View File

@ -16,9 +16,7 @@ import java.util.function.Function;
* *
* @author Lion Li * @author Lion Li
* @version 3.6.0 新增 * @version 3.6.0 新增
* @deprecated redisson 新版本已经将队列功能标记删除 一些技术问题无法解决 建议搭建MQ使用
*/ */
@Deprecated
@NoArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PRIVATE)
public class QueueUtils { public class QueueUtils {

View File

@ -129,9 +129,9 @@ public class RedisUtils {
} catch (Exception e) { } catch (Exception e) {
long timeToLive = bucket.remainTimeToLive(); long timeToLive = bucket.remainTimeToLive();
if (timeToLive == -1) { if (timeToLive == -1) {
bucket.set(value); setCacheObject(key, value);
} else { } else {
bucket.set(value, Duration.ofMillis(timeToLive)); setCacheObject(key, value, Duration.ofMillis(timeToLive));
} }
} }
} else { } else {
@ -147,8 +147,11 @@ public class RedisUtils {
* @param duration 时间 * @param duration 时间
*/ */
public static <T> void setCacheObject(final String key, final T value, final Duration duration) { public static <T> void setCacheObject(final String key, final T value, final Duration duration) {
RBucket<T> bucket = CLIENT.getBucket(key); RBatch batch = CLIENT.createBatch();
bucket.set(value, duration); RBucketAsync<T> bucket = batch.getBucket(key);
bucket.setAsync(value);
bucket.expireAsync(duration);
batch.execute();
} }
/** /**

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