Compare commits

..

88 Commits

Author SHA1 Message Date
疯狂的狮子Li
6bfdcae06e !847 发布 5.6.1 版本 依赖升级漏洞修复
Merge pull request !847 from 疯狂的狮子Li/dev
2026-04-24 01:47:21 +00:00
疯狂的狮子Li
ba70f9bb2d 🦁🦁🦁发布 5.6.1 版本 依赖升级漏洞修复 2026-04-24 09:44:18 +08:00
疯狂的狮子Li
b48fc54151 fix 修复 springboot 升级后 依赖缺失 2026-04-24 09:39:53 +08:00
疯狂的狮子Li
2779c02953 update springboot 3.5.12 => 3.5.14
update springdoc 2.8.15 => 2.8.17
update satoken 1.44.0 => 1.45.0
update springboot-admin 3.5.6 => 3.5.8
update lombok 1.18.42 => 1.18.44
update warmflow 1.8.4 => 1.8.5
update anyline 20251210 => 20260319
2026-04-24 09:21:25 +08:00
疯狂的狮子Li
3965e47d8a update ip2region 3.3.4 => 3.3.7 2026-04-24 09:19:44 +08:00
疯狂的狮子Li
91f7fab613 update snailjob 1.9.0 => 1.10.0 2026-04-24 09:19:20 +08:00
疯狂的狮子Li
d259cac6a0 fix 修复 netty 对虚拟线程适配有问题 导致长时间使用 redisson 卡死问题 2026-04-21 10:59:46 +08:00
疯狂的狮子Li
cf006a47da fix 修复 AUTO_PASS 变量取值错误 2026-04-10 17:49:59 +08:00
疯狂的狮子Li
14df8e0d9a fix 修复 代码生成 主库生成其他不同数据源sql模板错误问题 2026-04-10 14:50:31 +08:00
疯狂的狮子Li
ddff4ae38d update 优化 全局异常拦截器 不返回具体的异常内容到前端页面 避免信息泄漏问题 2026-04-09 14:21:46 +08:00
疯狂的狮子Li
a2be9bd3dc update 优化 截断token 避免日志输出具体token内容 防止盗用隐患 2026-04-09 10:58:25 +08:00
疯狂的狮子Li
3904d80329 update 优化 RepeatSubmitAspect.KEY_CACHE 清理不彻底 2026-04-09 10:55:54 +08:00
疯狂的狮子Li
acab0637bf fix 修复 ExcelBigNumberConvert 导入没法转换数据问题 2026-04-08 16:27:38 +08:00
疯狂的狮子Li
ac374b36b2 fix 修复 部门vo子部门属性使用错误 2026-04-07 09:16:44 +08:00
疯狂的狮子Li
631b6be33c fix 修复 下拉数据字符大于255报错问题 2026-04-03 11:29:50 +08:00
疯狂的狮子Li
fe637c4479 update 优化 已办任务列表去除抄送任务 2026-04-02 11:29:16 +08:00
RealXin
cb301e214b !840 update 更新gitignore文件
* update 更新gitignore文件
2026-03-30 12:07:31 +00:00
疯狂的狮子Li
6afb100ba3 !842 fix 修复 更新sql字段内容错误
Merge pull request !842 from 疯狂的狮子Li/dev
2026-03-27 09:35:36 +00:00
疯狂的狮子Li
5d1aaa62d6 fix 修复 更新sql字段内容错误 2026-03-24 13:45:58 +08:00
疯狂的狮子Li
b629c82d0f !839 发布 5.6.0 版本 新年第一版
Merge pull request !839 from 疯狂的狮子Li/dev
2026-03-24 03:48:49 +00:00
疯狂的狮子Li
c601164ba3 🦁🦁🦁发布 5.6.0 版本 新年第一版 2026-03-24 11:38:49 +08:00
疯狂的狮子Li
867a5d084d update bcpkix-jdk18on 1.80 => 1.83 2026-03-24 11:17:54 +08:00
疯狂的狮子Li
78aab8f8cd update springboot 3.5.11 => 3.5.12 2026-03-24 10:20:49 +08:00
疯狂的狮子Li
c919572eb2 update 增加更新sql 2026-03-24 10:15:48 +08:00
gssong
12bcc68b45 update 调整流程撤销如果非发起人或管理员不可以撤销 2026-03-19 10:51:05 +08:00
gssong
f551cd938c add 增加流程实例权限脚本 2026-03-19 10:34:26 +08:00
gssong
e1a8eea3a1 add 增加流程实例权限 2026-03-19 10:28:43 +08:00
gssong
269de03d0b add 增加流程实例权限 2026-03-19 10:26:28 +08:00
gssong
0bc4961694 add 增加流程定义权限 2026-03-18 17:25:55 +08:00
gssong
a71541a227 fix 修复 CVE-2026-2819 工作流接口通过业务id可以越级删除问题 2026-03-18 11:40:35 +08:00
疯狂的狮子Li
334c85ed61 fix 修复 CVE-2026-2819 工作流接口通过业务id可以越级删除问题 2026-03-18 11:35:51 +08:00
疯狂的狮子Li
0dd3b8dc51 fix 修复 excel结尾出现空白格导致合并策略异常问题 2026-03-16 18:00:04 +08:00
Lau
0530a9d307 update 修改vben5前端仓库地址 2026-03-13 14:20:35 +00:00
疯狂的狮子Li
2d7195c61d update 优化 代码增加空判断与其他性能优化 2026-03-10 17:20:37 +08:00
AprilWind
aaede419bc update 优化统一用户昵称 2026-03-10 14:25:29 +08:00
AprilWind
75d8d374bc update 优化统一用户昵称 2026-03-10 14:08:44 +08:00
gssong
07b29e06cf update 调整转办等消息提示 2026-03-09 10:12:02 +08:00
gssong
3cbbd0698d add 增加调整转办等消息提示 2026-03-06 18:35:47 +08:00
疯狂的狮子Li
5312131635 update 优化代码 2026-03-06 17:57:05 +08:00
秋辞未寒
91f505539e Merge remote-tracking branch 'plus/dev' into dev 2026-03-06 17:32:35 +08:00
秋辞未寒
c9774e78c4 update 通过类加载的方式优化 Javadoc 解析器,支持执行多个 Javadoc 解析器 2026-03-06 17:32:28 +08:00
疯狂的狮子Li
337c2f7170 fix 修复 jackson createContextual 用法不标准导致可能出现的并发问题(https://gitee.com/dromara/RuoYi-Cloud-Plus/issues/IFAM5Z) 2026-03-06 16:54:50 +08:00
疯狂的狮子Li
f773818642 fix 修复 jackson createContextual 用法不标准导致可能出现的并发问题(https://gitee.com/dromara/RuoYi-Cloud-Plus/issues/IFAM5Z) 2026-03-06 16:49:36 +08:00
疯狂的狮子Li
9bf8ae5583 fix 修复 移除超级管理员角色后新增角色分配校验,避免无角色分配时报错 2026-03-06 13:08:05 +08:00
疯狂的狮子Li
d190b89681 update README.md 2026-03-06 11:28:27 +08:00
疯狂的狮子Li
d89e09b94e update 优化 !pr835 相关代码 2026-03-05 17:32:51 +08:00
AprilWind
1452ae9685 !835 update Sa-Token 权限码展示,接口详情显示权限码,感谢nextdoc4j
* update 增强接口描述,合并JavaDoc权限信息到操作描述中
* update 优化satoken依赖引用,减少耦合性
* update 优化接口描述文本展示
* update Sa-Token 权限码展示,接口详情显示权限码,感谢nextdoc4j
2026-03-05 09:06:58 +00:00
疯狂的狮子Li
28772b8b30 fix 修正 SysDictDataController 接口注释及日志注解中的错误描述 2026-03-05 17:05:58 +08:00
AprilWind
ab201f9d9e Revert "update 优化redis 工具类使用方法"
This reverts commit 1f5f969d20.
2026-03-04 16:34:46 +08:00
AprilWind
1f5f969d20 update 优化redis 工具类使用方法 2026-03-03 17:53:38 +08:00
疯狂的狮子Li
a89a124a0e update readme github stars label 2026-03-03 17:01:08 +08:00
疯狂的狮子Li
536b6db527 update minio 升级到开源社区维护的最新版本 RELEASE.2026-02-14T12-00-00Z 2026-03-02 16:19:55 +08:00
疯狂的狮子Li
f8e3cff83d update 优化 区分系统监控与流程监控菜单地址 避免冲突 2026-03-02 14:16:23 +08:00
疯狂的狮子Li
6b3ed18c33 update 优化 消除编译相关提醒 2026-03-02 13:27:53 +08:00
疯狂的狮子Li
e669fa3210 fix 修复 undertow 新版本无法上传大文件问题 2026-02-26 13:42:56 +08:00
疯狂的狮子Li
2d116146ce fix 修复 undertow 新版本无法上传大文件问题 2026-02-26 13:41:10 +08:00
疯狂的狮子Li
ed9de38c6f fix 修复 TestDemoImportVo 包放置错误 2026-01-29 15:21:53 +08:00
清酒
28d4c88387 !828 fix: 修复演示案例导入 VO 缺失 AutoMapper 注解导致导入数据功能转换失败的问题
* fix: 修复演示案例导入 VO 缺失 AutoMapper 注解导致导入数据功能转换失败的问题
2026-01-29 07:20:57 +00:00
疯狂的狮子Li
c656f3340d update 优化 增加工作流短信发送案例 2026-01-28 15:29:07 +08:00
ColorDreams
cd0ee3f016 update 更新ip2region版本,优化IP未知地区占位符为0的情况 2026-01-28 13:14:35 +08:00
ColorDreams
48ea66cb1a update 使用release指令代替source和target指令进行编译构建 2026-01-28 13:12:56 +08:00
疯狂的狮子Li
ab6409ea28 !827 fix 修复 springboot升级到3.5.10之后大文件上传请求无响应问题(不清楚原因等spring修复)
Merge pull request !827 from 疯狂的狮子Li/dev
2026-01-27 08:13:42 +00:00
疯狂的狮子Li
34c3b81190 fix 修复 springboot升级到3.5.10之后大文件上传请求无响应问题(不清楚原因等spring修复) 2026-01-27 16:10:02 +08:00
疯狂的狮子Li
91ba3869e7 !826 发布 5.5.3 版本 提前祝大家新年快乐
Merge pull request !826 from 疯狂的狮子Li/dev
2026-01-23 06:04:22 +00:00
疯狂的狮子Li
1bb597b855 🧨🧨🧨发布 5.5.3 版本 提前祝大家新年快乐 2026-01-23 14:00:24 +08:00
疯狂的狮子Li
348d7fc534 update springboot 3.5.9 => 3.5.10
update springdoc 2.8.14 => 2.8.15
update mybatis-plus 3.5.14 => 3.5.16
update hutool 5.8.40 => 5.8.43
update spring-boot-admin 3.5.5 => 3.5.6
2026-01-23 13:48:15 +08:00
Coast
76218091ad !824 update 优化 oss 依赖注释说明
* update 优化 oss 依赖注释说明
2026-01-21 06:41:34 +00:00
疯狂的狮子Li
95c9e37797 update 优化 oss 依赖注释说明 2026-01-21 11:51:43 +08:00
疯狂的狮子Li
aa277b373b fix 修复 不同类别菜单的判断逻辑有误问题 2026-01-21 11:46:33 +08:00
疯狂的狮子Li
79d9f47053 update README.md 2026-01-19 15:09:07 +08:00
疯狂的狮子Li
f984f08a14 fix 修复 按钮菜单 不应该校验路由的问题 2026-01-16 16:47:12 +08:00
疯狂的狮子Li
6f94095bb0 update 优化 自行实现更漂亮的验证码图案 2026-01-14 18:28:37 +08:00
疯狂的狮子Li
2d4685ac5f update 修改验证码默认样式 2026-01-14 16:38:17 +08:00
疯狂的狮子Li
7f9e4e14f0 fix 修复 顶节点判断条件缺失 2026-01-14 09:19:46 +08:00
羡民Coding
c5777c01c1 !822 fix: 文案错误
* fix: 文案错误
2026-01-12 06:01:54 +00:00
疯狂的狮子Li
459e9caf14 update 优化 兼容path大写开头搜索 2026-01-12 09:21:01 +08:00
疯狂的狮子Li
0940ba6762 update 优化 大家都认可用"账"统一改为账 2026-01-12 09:17:39 +08:00
疯狂的狮子Li
d8ed23f227 update 优化 添加菜单路由地址和名称的校验规则 2026-01-09 17:10:51 +08:00
疯狂的狮子Li
948eba6566 update 优化 添加菜单路由地址和名称的校验规则 2026-01-09 13:19:21 +08:00
疯狂的狮子Li
1a14bdf256 update 优化 统一用词 2026-01-09 11:50:28 +08:00
疯狂的狮子Li
bbc684b335 update 删除已经过期的配置类 2026-01-06 17:26:45 +08:00
疯狂的狮子Li
2b8f4e1d2c update 下架过期的赞助商 2026-01-06 17:22:44 +08:00
AprilWind
d634c2a292 update 优化oss日志侦听器打印级别 2026-01-05 14:39:40 +08:00
ColorDreams
8b97e7bc53 update ip2region version to 3.3.2 2025-12-24 19:09:36 +08:00
疯狂的狮子Li
88f871002c !816 fix 修复 判断条件写反问题
Merge pull request !816 from 疯狂的狮子Li/dev
2025-12-24 05:27:11 +00:00
疯狂的狮子Li
874ad7c9b7 fix 修复 判断条件写反问题 2025-12-24 13:10:47 +08:00
miracle-bean
89d6f6f247 !815 fix websocket 多线程下IO阻塞的问题
* fix websocket 多线程下IO阻塞的问题
2025-12-23 07:55:24 +00:00
疯狂的狮子Li
1324a1cb16 update 优化 增加 HandlerMethodValidationException 参数校验异常连接 2025-12-23 15:30:32 +08:00
113 changed files with 1921 additions and 652 deletions

3
.gitignore vendored
View File

@@ -25,6 +25,9 @@ target/
*.iml
*.ipr
### VS CODE ###
.vscode
### JRebel ###
rebel.xml

View File

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

View File

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

View File

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

View File

@@ -5,13 +5,12 @@
## 平台简介
[![码云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?label=Github%20Stars)](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)
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/5.X/LICENSE)
[![使用IntelliJ IDEA开发维护](https://img.shields.io/badge/IntelliJ%20IDEA-提供支持-blue.svg)](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
<br>
[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.5.2-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.4-blue.svg)]()
[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.6.1-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.5-blue.svg)]()
[![JDK-17](https://img.shields.io/badge/JDK-17-green.svg)]()
[![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)]()
@@ -23,7 +22,7 @@
> 系统演示: [传送门](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>
> 成员前端项目地址: 基于vben5 [ruoyi-plus-vben5](https://gitee.com/dapppp/ruoyi-plus-vben5)<br>
> 成员前端项目地址: 基于vben5 [ruoyi-plus-vben5](https://github.com/imdap/ruoyi-plus-vben5)<br>
> 成员前端项目地址: 基于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>
@@ -33,9 +32,8 @@
MaxKey 业界领先单点登录产品 - https://gitee.com/dromara/MaxKey <br>
CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
数舵科技 软件定制开发APP小程序等 - http://www.shuduokeji.com/ <br>
数舵科技 软件定制开发APP小程序等 - https://www.shuduokeji.com/ <br>
引迈信息 软件开发平台 - https://www.jnpfsoft.com/index.html?from=plus-doc <br>
<font color="red">**启山商城系统 多租户商城源码可免费商用可二次开发 - https://www.73app.cn/** </font><br>
Mall4J 高质量Java商城系统 - https://www.mall4j.com/cn/?statId=11 <br>
aizuda flowlong 工作流 - https://gitee.com/aizuda/flowlong <br>
Ruoyi-Plus-Uniapp - https://ruoyi.plus <br>
@@ -55,7 +53,7 @@ Topiam IAM/IDaaS身份管理平台 - https://www.topiam.cn/ <br>
| 权限注解 | 采用 Sa-Token 支持注解 登录校验、角色校验、权限校验、二级认证校验、HttpBasic校验、忽略校验<br/>角色与权限校验支持多种条件 如 `AND` `OR``权限 OR 角色` 等复杂表达式 | 只支持是否存在匹配 |
| 三方鉴权 | 采用 JustAuth 第三方登录组件 支持微信、钉钉等数十种三方认证 | 无 |
| 关系数据库支持 | 原生支持 MySQL、Oracle、PostgreSQL、SQLServer<br/>可同时使用异构切换(支持其他 mybatis-plus 支持的所有数据库 只需要增加jdbc依赖即可使用 达梦金仓等均有成功案例) | 支持 Mysql、Oracle 不支持同时使用、不支持异构切换 |
| 缓存数据库 | 支持 Redis 5-7 支持大部分新功能特性 如 分布式限流、分布式队列 | Redis 简单 get set 支持 |
| 缓存数据库 | 支持 Redis >= 6 支持大部分新功能特性 如 分布式限流、分布式队列 | Redis 简单 get set 支持 |
| Redis客户端 | 采用 Redisson Redis官方推荐 基于Netty的客户端工具<br/>支持Redis 90%以上的命令 底层优化规避很多不正确的用法 例如: keys被转换为scan<br/>支持单机、哨兵、单主集群、多主集群等模式 | Lettuce + RedisTemplate 支持模式少 工具使用繁琐<br/>连接池采用 common-pool Bug多经常性出问题 |
| 缓存注解 | 采用 Spring-Cache 注解 对其扩展了实现支持了更多功能<br/>例如 过期时间 最大空闲时间 组最大长度等 只需一个注解即可完成数据自动缓存 | 需手动编写Redis代码逻辑 |
| ORM框架 | 采用 Mybatis-Plus 基于对象几乎不用写SQL全java操作 功能强大插件众多<br/>例如多租户插件 分页插件 乐观锁插件等等 | 采用 Mybatis 基于XML需要手写SQL |

37
pom.xml
View File

@@ -13,32 +13,32 @@
<description>Dromara RuoYi-Vue-Plus多租户管理系统</description>
<properties>
<revision>5.5.2</revision>
<spring-boot.version>3.5.9</spring-boot.version>
<revision>5.6.1</revision>
<spring-boot.version>3.5.14</spring-boot.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version>
<mybatis.version>3.5.16</mybatis.version>
<springdoc.version>2.8.14</springdoc.version>
<mybatis.version>3.5.19</mybatis.version>
<springdoc.version>2.8.17</springdoc.version>
<therapi-javadoc.version>0.15.0</therapi-javadoc.version>
<fastexcel.version>1.3.0</fastexcel.version>
<velocity.version>2.3</velocity.version>
<satoken.version>1.44.0</satoken.version>
<mybatis-plus.version>3.5.14</mybatis-plus.version>
<satoken.version>1.45.0</satoken.version>
<mybatis-plus.version>3.5.16</mybatis-plus.version>
<p6spy.version>3.9.1</p6spy.version>
<hutool.version>5.8.40</hutool.version>
<spring-boot-admin.version>3.5.5</spring-boot-admin.version>
<hutool.version>5.8.43</hutool.version>
<spring-boot-admin.version>3.5.8</spring-boot-admin.version>
<redisson.version>3.52.0</redisson.version>
<lock4j.version>2.2.7</lock4j.version>
<dynamic-ds.version>4.3.1</dynamic-ds.version>
<snailjob.version>1.9.0</snailjob.version>
<snailjob.version>1.10.0</snailjob.version>
<mapstruct-plus.version>1.5.0</mapstruct-plus.version>
<mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
<lombok.version>1.18.40</lombok.version>
<bouncycastle.version>1.80</bouncycastle.version>
<lombok.version>1.18.44</lombok.version>
<bouncycastle.version>1.83</bouncycastle.version>
<justauth.version>1.16.7</justauth.version>
<!-- 离线IP地址定位库 -->
<ip2region.version>3.3.1</ip2region.version>
<ip2region.version>3.3.7</ip2region.version>
<!-- OSS 配置 -->
<aws.sdk.version>2.28.22</aws.sdk.version>
<!-- SMS 配置 -->
@@ -46,9 +46,9 @@
<!-- 限制框架中的fastjson版本 -->
<fastjson.version>1.2.83</fastjson.version>
<!-- 面向运行时的D-ORM依赖 -->
<anyline.version>8.7.3-20251210</anyline.version>
<anyline.version>8.7.3-20260319</anyline.version>
<!-- 工作流配置 -->
<warm-flow.version>1.8.4</warm-flow.version>
<warm-flow.version>1.8.5</warm-flow.version>
<!-- 插件版本 -->
<maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>
@@ -226,13 +226,13 @@
<artifactId>s3</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
<!-- 基于 AWS CRT 的 S3 客户端的性能增强的 S3 传输管理器 -->
<!-- 客户端的性能增强传输管理器 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3-transfer-manager</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
<!-- 将基于 Netty 的 HTTP 客户端从类路径中移除 -->
<!-- 适用于 Netty 的客户端 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>netty-nio-client</artifactId>
@@ -284,7 +284,7 @@
<!-- 加密包引入 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
@@ -375,8 +375,7 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<release>${java.version}</release>
<encoding>${project.build.sourceEncoding}</encoding>
<annotationProcessorPaths>
<path>

View File

@@ -1,8 +1,9 @@
package org.dromara.web.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.captcha.generator.MathGenerator;
import cn.hutool.captcha.generator.RandomGenerator;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import jakarta.validation.constraints.NotBlank;
@@ -14,14 +15,13 @@ import org.dromara.common.core.domain.R;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.reflect.ReflectUtils;
import org.dromara.common.mail.config.properties.MailProperties;
import org.dromara.common.mail.utils.MailUtils;
import org.dromara.common.ratelimiter.annotation.RateLimiter;
import org.dromara.common.ratelimiter.enums.LimitType;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.web.core.WaveAndCircleCaptcha;
import org.dromara.common.web.config.properties.CaptchaProperties;
import org.dromara.common.web.enums.CaptchaType;
import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.api.entity.SmsResponse;
import org.dromara.sms4j.core.factory.SmsFactory;
@@ -33,6 +33,7 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.awt.*;
import java.time.Duration;
import java.util.LinkedHashMap;
@@ -130,19 +131,21 @@ public class CaptchaController {
String uuid = IdUtil.simpleUUID();
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
// 生成验证码
CaptchaType captchaType = captchaProperties.getType();
String captchaType = captchaProperties.getType();
CodeGenerator codeGenerator;
if (CaptchaType.MATH == captchaType) {
codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getNumberLength(), false);
if ("math".equals(captchaType)) {
codeGenerator = new MathGenerator(captchaProperties.getNumberLength(), false);
} else {
codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getCharLength());
codeGenerator = new RandomGenerator(captchaProperties.getCharLength());
}
AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz());
WaveAndCircleCaptcha captcha = new WaveAndCircleCaptcha(160, 60);
// captcha.setBackground(Color.WHITE); // 不设置就是透明底
captcha.setFont(new Font("Arial", Font.BOLD, 45));
captcha.setGenerator(codeGenerator);
captcha.createCode();
// 如果是数学验证码使用SpEL表达式处理验证码结果
String code = captcha.getCode();
if (CaptchaType.MATH == captchaType) {
if ("math".equals(captchaType)) {
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(StringUtils.remove(code, "="));
code = exp.getValue(String.class);

View File

@@ -14,6 +14,7 @@ import org.dromara.common.core.domain.dto.UserOnlineDTO;
import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.ip.AddressUtils;
import org.dromara.common.log.event.LogininforEvent;
import org.dromara.common.redis.utils.RedisUtils;
@@ -73,7 +74,7 @@ public class UserActionListener implements SaTokenListener {
SpringUtils.context().publishEvent(logininforEvent);
// 更新登录信息
loginService.recordLoginInfo((Long) loginParameter.getExtra(LoginHelper.USER_KEY), ip);
log.info("user doLogin, userId:{}, token:{}", loginId, tokenValue);
log.info("user doLogin, userId:{}, token:***{}", loginId, StringUtils.right(tokenValue, 8));
}
/**
@@ -85,7 +86,7 @@ public class UserActionListener implements SaTokenListener {
TenantHelper.dynamic(tenantId, () -> {
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
});
log.info("user doLogout, userId:{}, token:{}", loginId, tokenValue);
log.info("user doLogout, userId:{}, token:***{}", loginId, StringUtils.right(tokenValue, 8));
}
/**
@@ -97,7 +98,7 @@ public class UserActionListener implements SaTokenListener {
TenantHelper.dynamic(tenantId, () -> {
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
});
log.info("user doKickout, userId:{}, token:{}", loginId, tokenValue);
log.info("user doKickout, userId:{}, token:***{}", loginId, StringUtils.right(tokenValue, 8));
}
/**
@@ -109,7 +110,7 @@ public class UserActionListener implements SaTokenListener {
TenantHelper.dynamic(tenantId, () -> {
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
});
log.info("user doReplaced, userId:{}, token:{}", loginId, tokenValue);
log.info("user doReplaced, userId:{}, token:***{}", loginId, StringUtils.right(tokenValue, 8));
}
/**

View File

@@ -8,7 +8,7 @@ server:
# undertow 配置
undertow:
# HTTP post内容的最大大小。当值为-1时默认值为大小是无限的
max-http-post-size: -1
max-http-post-size: 1GB
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
# 每块buffer的空间大小,越小的空间被利用越充分
buffer-size: 512
@@ -24,9 +24,7 @@ captcha:
# 是否启用验证码校验
enable: true
# 验证码类型 math 数组计算 char 字符验证
type: MATH
# line 线段干扰 circle 圆圈干扰 shear 扭曲干扰
category: CIRCLE
type: math
# 数字验证码位数
numberLength: 1
# 字符验证码长度

View File

@@ -5,7 +5,7 @@ user.jcaptcha.expire=验证码已失效
user.not.exists=对不起, 您的账号:{0} 不存在.
user.password.not.match=用户不存在/密码错误
user.password.retry.limit.count=密码输入错误{0}次
user.password.retry.limit.exceed=密码输入错误{0}次,户锁定{1}分钟
user.password.retry.limit.exceed=密码输入错误{0}次,户锁定{1}分钟
user.password.delete=对不起,您的账号:{0} 已被删除
user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员
role.blocked=角色已封禁,请联系管理员
@@ -47,10 +47,10 @@ repeat.submit.message=不允许重复提交,请稍候再试
rate.limiter.message=访问过于频繁,请稍候再试
sms.code.not.blank=短信验证码不能为空
sms.code.retry.limit.count=短信验证码输入错误{0}次
sms.code.retry.limit.exceed=短信验证码输入错误{0}次,户锁定{1}分钟
sms.code.retry.limit.exceed=短信验证码输入错误{0}次,户锁定{1}分钟
email.code.not.blank=邮箱验证码不能为空
email.code.retry.limit.count=邮箱验证码输入错误{0}次
email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,户锁定{1}分钟
email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,户锁定{1}分钟
xcx.code.not.blank=小程序[code]不能为空
social.source.not.blank=第三方登录平台[source]不能为空
social.code.not.blank=第三方登录平台[code]不能为空

View File

@@ -5,7 +5,7 @@ user.jcaptcha.expire=验证码已失效
user.not.exists=对不起, 您的账号:{0} 不存在.
user.password.not.match=用户不存在/密码错误
user.password.retry.limit.count=密码输入错误{0}次
user.password.retry.limit.exceed=密码输入错误{0}次,户锁定{1}分钟
user.password.retry.limit.exceed=密码输入错误{0}次,户锁定{1}分钟
user.password.delete=对不起,您的账号:{0} 已被删除
user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员
role.blocked=角色已封禁,请联系管理员
@@ -47,10 +47,10 @@ repeat.submit.message=不允许重复提交,请稍候再试
rate.limiter.message=访问过于频繁,请稍候再试
sms.code.not.blank=短信验证码不能为空
sms.code.retry.limit.count=短信验证码输入错误{0}次
sms.code.retry.limit.exceed=短信验证码输入错误{0}次,户锁定{1}分钟
sms.code.retry.limit.exceed=短信验证码输入错误{0}次,户锁定{1}分钟
email.code.not.blank=邮箱验证码不能为空
email.code.retry.limit.count=邮箱验证码输入错误{0}次
email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,户锁定{1}分钟
email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,户锁定{1}分钟
xcx.code.not.blank=小程序[code]不能为空
social.source.not.blank=第三方登录平台[source]不能为空
social.code.not.blank=第三方登录平台[code]不能为空

View File

@@ -14,7 +14,7 @@
</description>
<properties>
<revision>5.5.2</revision>
<revision>5.6.1</revision>
</properties>
<dependencyManagement>

View File

@@ -52,7 +52,7 @@ public interface CacheNames {
String SYS_USER_NAME = "sys_user_name#30d";
/**
* 用户
* 用户
*/
String SYS_NICKNAME = "sys_nickname#30d";

View File

@@ -23,8 +23,8 @@ public class FlowCopyDTO implements Serializable {
private Long userId;
/**
* 用户
* 用户
*/
private String userName;
private String nickName;
}

View File

@@ -48,7 +48,8 @@ public class StartProcessDTO implements Serializable {
public Map<String, Object> getVariables() {
if (variables == null) {
return new HashMap<>(16);
variables = new HashMap<>(16);
return variables;
}
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables;

View File

@@ -61,7 +61,7 @@ public class UserDTO implements Serializable {
private String sex;
/**
* 号状态0正常 1停用
* 号状态0正常 1停用
*/
private String status;

View File

@@ -30,7 +30,7 @@ public class UserOnlineDTO implements Serializable {
private String deptName;
/**
* 用户名称
* 用户账号
*/
private String userName;

View File

@@ -21,18 +21,18 @@ public interface UserService {
String selectUserNameById(Long userId);
/**
* 通过用户ID查询用户账户
* 通过用户ID查询用户昵称
*
* @param userId 用户ID
* @return 用户
* @return 用户
*/
String selectNicknameById(Long userId);
/**
* 通过用户ID查询用户账户
* 通过用户ID查询用户昵称
*
* @param userIds 用户ID 多个用逗号隔开
* @return 用户
* @return 用户
*/
String selectNicknameByIds(String userIds);
@@ -93,11 +93,11 @@ public interface UserService {
List<UserDTO> selectUsersByPostIds(List<Long> postIds);
/**
* 根据用户 ID 列表查询用户称映射关系
* 根据用户 ID 列表查询用户称映射关系
*
* @param userIds 用户 ID 列表
* @return Map其中 key 为用户 IDvalue 为对应的用户
* @return Map其中 key 为用户 IDvalue 为对应的用户
*/
Map<Long, String> selectUserNamesByIds(List<Long> userIds);
Map<Long, String> selectUserNicksByIds(List<Long> userIds);
}

View File

@@ -1,6 +1,5 @@
package org.dromara.common.core.utils.ip;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
@@ -9,7 +8,6 @@ import org.lionsoul.ip2region.service.Config;
import org.lionsoul.ip2region.service.Ip2Region;
import org.lionsoul.ip2region.xdb.Util;
import java.io.File;
import java.io.InputStream;
import java.time.Duration;
@@ -31,6 +29,11 @@ public class RegionUtils {
// 下载地址https://gitee.com/lionsoul/ip2region/blob/master/data/ip2region_v6.xdb
public static final String DEFAULT_IPV6_XDB_PATH = "ip2region_v6.xdb";
// 默认缓存切片大小为15MB仅针对BufferCache全量读取有效如果你的xdb数据库很大合理设置该值可以有效提升BufferCache模式下的查询效率具体可以查看Ip2Region的README
// 注意设置过大的值可能会申请内存时因内存不足而导致OOM请合理设置该值。
// READMEhttps://gitee.com/lionsoul/ip2region/tree/master/binding/java
public static final int DEFAULT_CACHE_SLICE_BYTES = 1024 * 1024 * 15;
// 未知地址
public static final String UNKNOWN_ADDRESS = "未知";
@@ -43,20 +46,18 @@ public class RegionUtils {
// 注意Ip2Region 的xdb文件加载策略 CachePolicy 有三种分别是BufferCache全量读取xdb到内存中、VIndexCache默认策略按需读取并缓存、NoCache实时读取
// 本项目工具使用的 CachePolicy 为 BufferCacheBufferCache会加载整个xdb文件到内存中setXdbInputStream 仅支持 BufferCache 策略。
// 因为加载整个xdb文件会耗费非常大的内存如果你不希望加载整个xdb到内存中更推荐使用 VIndexCache 或 NoCache即实时读取文件策略和 setXdbPath/setXdbFile 加载方法需要注意的一点setXdbPath 和 setXdbFile 不支持读取ClassPath即源码和resource目录中的文件
// 一般而言更建议把xdb数据库放到一个指定的文件目录中即不打包进jar包中然后使用 NoCache + 配合SearcherPool的并发池读取数据更方便随时更新xdb数据库
// 一般而言更建议把xdb数据库放到一个指定的文件目录中即不打包进jar包中然后使用 VIndexCache + 配合SearcherPool的并发池读取数据更方便随时更新xdb数据库
// TODO 2025年12月23日 Ip2Region封装的 InputStream 读取函数 Searcher.loadContentFromInputStream 在Linux环境下会申请过大的byte[]空间而导致OOM这里先用临时文件的方案解决等后续 Ip2Region 更新解决方案
// 创建临时文件
File v4TempXdb = FileUtil.writeFromStream(ResourceUtil.getStream(DEFAULT_IPV4_XDB_PATH), FileUtil.createTempFile());
InputStream v4InputStream = ResourceUtil.getStream(DEFAULT_IPV4_XDB_PATH);
// IPv4配置
Config v4Config = Config.custom()
.setCachePolicy(Config.BufferCache)
.setXdbFile(v4TempXdb)
// .setXdbInputStream(ResourceUtil.getStream(DEFAULT_IPV4_XDB_PATH))
//.setXdbFile(v4TempXdb)
.setXdbInputStream(v4InputStream)
//
.setCacheSliceBytes(DEFAULT_CACHE_SLICE_BYTES)
.asV4();
// 删除临时文件
v4TempXdb.delete();
// IPv6配置
Config v6Config = null;
@@ -64,17 +65,12 @@ public class RegionUtils {
if (v6XdbInputStream == null) {
log.warn("未加载 IPv6 地址库:未在类路径下找到文件 {}。当前仅启用 IPv4 查询。如需启用 IPv6请将 ip2region_v6.xdb 放置到 resources 目录", DEFAULT_IPV6_XDB_PATH);
} else {
// 创建临时文件
File v6TempXdb = FileUtil.writeFromStream(ResourceUtil.getStream(DEFAULT_IPV4_XDB_PATH), FileUtil.createTempFile());
v6Config = Config.custom()
.setCachePolicy(Config.BufferCache)
.setXdbFile(v6TempXdb)
// .setXdbInputStream(v6XdbInputStream)
//.setXdbFile(v6TempXdb)
.setXdbInputStream(v6XdbInputStream)
.setCacheSliceBytes(DEFAULT_CACHE_SLICE_BYTES)
.asV6();
// 删除临时文件
v6TempXdb.delete();
}
// 初始化Ip2Region实例
@@ -94,9 +90,9 @@ public class RegionUtils {
try {
String region = ip2Region.search(ipString);
if (StringUtils.isBlank(region)) {
region = UNKNOWN_ADDRESS;
return UNKNOWN_ADDRESS;
}
return region;
return StringUtils.replace(region, "0", UNKNOWN_ADDRESS);
} catch (Exception e) {
log.error("IP地址离线获取城市异常 {}", ipString);
return UNKNOWN_ADDRESS;
@@ -113,9 +109,9 @@ public class RegionUtils {
try {
String region = ip2Region.search(ipBytes);
if (StringUtils.isBlank(region)) {
region = UNKNOWN_ADDRESS;
return UNKNOWN_ADDRESS;
}
return region;
return StringUtils.replace(region, "0", UNKNOWN_ADDRESS);
} catch (Exception e) {
log.error("IP地址离线获取城市异常 {}", Util.ipToString(ipBytes));
return UNKNOWN_ADDRESS;

View File

@@ -7,6 +7,8 @@ import io.swagger.v3.oas.models.security.SecurityRequirement;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.doc.config.properties.SpringDocProperties;
import org.dromara.common.doc.core.resolver.JavadocResolver;
import org.dromara.common.doc.core.resolver.SaTokenAnnotationMetadataJavadocResolver;
import org.dromara.common.doc.handler.OpenApiHandler;
import org.springdoc.core.configuration.SpringDocConfiguration;
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
@@ -84,8 +86,9 @@ public class SpringDocConfig {
SecurityService securityParser,
SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils,
Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomisers,
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomisers, Optional<JavadocProvider> javadocProvider) {
return new OpenApiHandler(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers, javadocProvider);
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomisers, Optional<JavadocProvider> javadocProvider,
List<JavadocResolver> javadocResolvers) {
return new OpenApiHandler(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers, javadocProvider, javadocResolvers);
}
/**
@@ -112,6 +115,14 @@ public class SpringDocConfig {
};
}
/**
* 注册SaToken JavaDoc权限注解解析器
*/
@Bean
public JavadocResolver saTokenAnnotationJavadocResolver() {
return new SaTokenAnnotationMetadataJavadocResolver();
}
/**
* 单独使用一个类便于判断 解决springdoc路径拼接重复问题
*

View File

@@ -0,0 +1,175 @@
package org.dromara.common.doc.core.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import lombok.Data;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 存储权限框架注解解析后的权限和角色信息
*
* @author AprilWind
*/
@Data
@JsonInclude(Include.NON_EMPTY)
public class SaTokenSecurityMetadata {
/**
* 权限校验信息列表(对应 @SaCheckPermission 注解)
*/
private List<AuthInfo> permissions = new ArrayList<>();
/**
* 角色校验信息列表(对应 @SaCheckRole 注解)
*/
private List<AuthInfo> roles = new ArrayList<>();
/**
* 是否忽略校验(对应 @SaIgnore 注解)
*/
private boolean ignore = false;
/**
* 添加权限信息
*
* @param values 权限值数组
* @param mode 校验模式AND/OR
* @param type 权限类型
* @param orRoles 或角色数组
*/
public void addPermission(String[] values, String mode, String type, String[] orRoles) {
if (values != null && values.length > 0) {
AuthInfo authInfo = new AuthInfo();
authInfo.setValues(values);
authInfo.setMode(mode);
authInfo.setType(type);
if (orRoles != null && orRoles.length > 0) {
authInfo.setOrValues(orRoles);
authInfo.setOrType("role");
}
this.permissions.add(authInfo);
}
}
/**
* 添加角色信息
*
* @param values 角色值数组
* @param mode 校验模式AND/OR
* @param type 角色类型
*/
public void addRole(String[] values, String mode, String type) {
if (values != null && values.length > 0) {
AuthInfo authInfo = new AuthInfo();
authInfo.setValues(values);
authInfo.setMode(mode);
authInfo.setType(type);
this.roles.add(authInfo);
}
}
/**
* 生成 Markdown 结构的权限说明
*
* @return Markdown 文本
*/
public String toMarkdownString() {
StringBuilder sb = new StringBuilder();
sb.append("<br><h3>访问权限</h3><br>");
if (ignore) {
sb.append("> **权限策略**:忽略权限检查<br>");
return sb.toString();
}
if (!ignore && permissions.isEmpty() && roles.isEmpty()){
sb.append("> **权限策略**:需要登录<br><br>");
return sb.toString();
}
if (!permissions.isEmpty()) {
sb.append("**权限校验:**<br><br>");
permissions.forEach(p -> {
String permTags = Arrays.stream(p.getValues())
.map(v -> "`" + v + "`")
.collect(Collectors.joining(p.getModeSymbol()));
sb.append("- ").append(permTags).append("<br>");
if (p.getOrValues() != null && p.getOrValues().length > 0) {
String orTags = Arrays.stream(p.getOrValues())
.map(v -> "`" + v + "`")
.collect(Collectors.joining(p.getModeSymbol()));
sb.append(" - 或角色:").append(orTags).append("<br>");
}
});
sb.append("<br>");
}
if (!roles.isEmpty()) {
sb.append("**角色校验:**<br><br>");
roles.forEach(r -> {
String roleTags = Arrays.stream(r.getValues())
.map(v -> "`" + v + "`")
.collect(Collectors.joining(r.getModeSymbol()));
sb.append("- ").append(roleTags).append("<br>");
});
}
return sb.toString().trim();
}
/**
* 认证信息
*/
@Data
@JsonInclude(Include.NON_EMPTY)
public static class AuthInfo {
/**
* 权限或角色值数组
*/
private String[] values;
/**
* 校验模式AND/OR
*/
private String mode;
/**
* 类型说明
*/
private String type;
/**
* 或权限/角色值数组(用于权限校验时的或角色校验)
*/
private String[] orValues;
/**
* 或值的类型role/permission
*/
private String orType;
/**
* 重写mode的获取方法返回符号而非文字
* @return AND→&OR→|,默认→&
*/
public String getModeSymbol() {
if (mode == null) {
return " & "; // 默认AND返回&
}
return "AND".equalsIgnoreCase(mode) ? " & " : " | ";
}
}
}

View File

@@ -0,0 +1,163 @@
package org.dromara.common.doc.core.resolver;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.util.ClassLoaderUtil;
import io.swagger.v3.oas.models.Operation;
import org.springframework.web.method.HandlerMethod;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.Map;
import java.util.function.Supplier;
/**
* 抽象元数据 Javadoc 解析器
*
* @param <M> 元数据类型
* @author 秋辞未寒
*/
public abstract class AbstractMetadataJavadocResolver<M> implements JavadocResolver {
public static final int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
public static final int LOWEST_PRECEDENCE = Integer.MAX_VALUE;
private final Supplier<M> metadataProvider;
private final int order;
public AbstractMetadataJavadocResolver(Supplier<M> metadataProvider) {
this(metadataProvider, LOWEST_PRECEDENCE);
}
public AbstractMetadataJavadocResolver(Supplier<M> metadataProvider, int order) {
this.metadataProvider = metadataProvider;
this.order = order;
}
@Override
public int getOrder() {
return order;
}
@Override
public String resolve(HandlerMethod handlerMethod, Operation operation) {
return resolve(handlerMethod, operation, metadataProvider.get());
}
/**
* 执行解析并返回解析到的 Javadoc 内容
* @param handlerMethod 处理器方法
* @param operation Swagger Operation实例
* @param metadata 元信息
* @return 解析到的 Javadoc 内容
*/
public abstract String resolve(HandlerMethod handlerMethod, Operation operation, M metadata);
/**
* 检查处理器方法所属的类上是否存在注解
* @param handlerMethod 处理器方法
* @param annotationClass 注解类
* @return 是否存在注解
*/
public boolean hasClassAnnotation(HandlerMethod handlerMethod,Class<? extends Annotation> annotationClass){
return AnnotationUtil.hasAnnotation(handlerMethod.getBeanType(), annotationClass);
}
/**
* 检查处理器方法所属的类上是否存在注解
* @param handlerMethod 处理器方法
* @param annotationTypeName 注解类名称
* @return 是否存在注解
*/
public boolean hasClassAnnotation(HandlerMethod handlerMethod, String annotationTypeName){
return AnnotationUtil.hasAnnotation(handlerMethod.getBeanType(), annotationTypeName);
}
/**
* 检查处理器方法上是否存在注解
* @param handlerMethod 处理器方法
* @param annotationClass 注解类
* @return 是否存在注解
*/
public boolean hasMethodAnnotation(HandlerMethod handlerMethod,Class<? extends Annotation> annotationClass){
return AnnotationUtil.hasAnnotation(handlerMethod.getMethod(), annotationClass);
}
/**
* 检查处理器方法上是否存在注解
* @param handlerMethod 处理器方法
* @param annotationTypeName 注解类名称
* @return 是否存在注解
*/
public boolean hasMethodAnnotation(HandlerMethod handlerMethod, String annotationTypeName){
return AnnotationUtil.hasAnnotation(handlerMethod.getMethod(), annotationTypeName);
}
/**
* 检查处理器方法上是否存在注解
* @param handlerMethod 处理器方法
* @param annotationClass 注解类
* @return 是否存在注解
*/
public boolean hasAnnotation(HandlerMethod handlerMethod,Class<? extends Annotation> annotationClass){
return this.hasClassAnnotation(handlerMethod, annotationClass) || this.hasMethodAnnotation(handlerMethod, annotationClass);
}
/**
* 检查处理器方法上是否存在注解
* @param handlerMethod 处理器方法
* @param annotationTypeName 注解类名称
* @return 是否存在注解
*/
public boolean hasAnnotation(HandlerMethod handlerMethod, String annotationTypeName){
return this.hasClassAnnotation(handlerMethod, annotationTypeName) || this.hasMethodAnnotation(handlerMethod, annotationTypeName);
}
/**
* 获取处理器方法所属类上的注解的值
* @param handlerMethod 处理器方法
* @param annotationClass 注解类
* @return 注解的值
*/
public Map<String, Object> getClassAnnotationValueMap(HandlerMethod handlerMethod, Class<? extends Annotation> annotationClass) {
return AnnotationUtil.getAnnotationValueMap(handlerMethod.getBeanType(), annotationClass);
}
/**
* 获取处理器方法所属类上的注解的值
* @param handlerMethod 处理器方法
* @param annotationClassName 注解类名称
* @return 注解的值
*/
@SuppressWarnings("unchecked")
public Map<String, Object> getClassAnnotationValueMap(HandlerMethod handlerMethod, String annotationClassName) {
Class<? extends Annotation> annotationClass = (Class<? extends Annotation>) ClassLoaderUtil.loadClass(annotationClassName, false);
return AnnotationUtil.getAnnotationValueMap(handlerMethod.getBeanType(), annotationClass);
}
/**
* 获取处理器方法上的注解的值
* @param handlerMethod 处理器方法
* @param annotationClass 注解类
* @return 注解的值
*/
public Map<String, Object> getMethodAnnotationValueMap(HandlerMethod handlerMethod, Class<? extends Annotation> annotationClass) {
return AnnotationUtil.getAnnotationValueMap(handlerMethod.getMethod(), annotationClass);
}
/**
* 获取处理器方法所属类上的注解的值
* @param handlerMethod 处理器方法
* @param annotationClassName 注解类名称
* @return 注解的值
*/
@SuppressWarnings("unchecked")
public Map<String, Object> getMethodAnnotationValueMap(HandlerMethod handlerMethod, String annotationClassName) {
Class<? extends Annotation> annotationClass = (Class<? extends Annotation>) ClassLoaderUtil.loadClass(annotationClassName, false);
return AnnotationUtil.getAnnotationValueMap(handlerMethod.getMethod(), annotationClass);
}
private Map<String, Object> getAnnotationValueMap(AnnotatedElement annotatedElement, Class<? extends Annotation> annotationClass) {
return AnnotationUtil.getAnnotationValueMap(annotatedElement, annotationClass);
}
}

View File

@@ -0,0 +1,51 @@
package org.dromara.common.doc.core.resolver;
import io.swagger.v3.oas.models.Operation;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.Ordered;
import org.springframework.web.method.HandlerMethod;
/**
* Javadoc解析器接口
*
* @author echo
* @author 秋辞未寒
*/
public interface JavadocResolver extends Comparable<JavadocResolver>, Ordered {
/**
* 检查解析器是否支持解析 HandlerMethod
* @param handlerMethod 处理器方法
* @return 是否支持解析
*/
boolean supports(HandlerMethod handlerMethod);
/**
* 执行解析并返回解析到的 Javadoc 内容
* @param handlerMethod 处理器方法
* @param operation Swagger Operation实例
* @return 解析到的 Javadoc 内容
*/
String resolve(HandlerMethod handlerMethod, Operation operation);
/**
* 获取解析器优先级
*/
default int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
/**
* 获取解析器的名称
*
* @return 解析器名称
*/
default String getName() {
return this.getClass().getSimpleName();
}
@Override
default int compareTo(@NotNull JavadocResolver o) {
return Integer.compare(getOrder(), o.getOrder());
}
}

View File

@@ -0,0 +1,164 @@
package org.dromara.common.doc.core.resolver;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ClassLoaderUtil;
import io.swagger.v3.oas.models.Operation;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.doc.core.model.SaTokenSecurityMetadata;
import org.springframework.web.method.HandlerMethod;
import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
/**
* 基于JavaDoc的SaToken权限解析器
*
* @author echo
* @author 秋辞未寒
*/
@SuppressWarnings("unchecked")
@Slf4j
public class SaTokenAnnotationMetadataJavadocResolver extends AbstractMetadataJavadocResolver<SaTokenSecurityMetadata> {
/**
* 默认元数据提供者,每次解析都会创建一个新的元数据对象
*/
public static final Supplier<SaTokenSecurityMetadata> DEFAULT_METADATA_PROVIDER = SaTokenSecurityMetadata::new;
private static final String BASE_CLASS_NAME = "cn.dev33.satoken.annotation";
private static final String SA_CHECK_ROLE_CLASS_NAME = BASE_CLASS_NAME + ".SaCheckRole";
private static final String SA_CHECK_PERMISSION_CLASS_NAME = BASE_CLASS_NAME + ".SaCheckPermission";
private static final String SA_IGNORE_CLASS_NAME = BASE_CLASS_NAME + ".SaIgnore";
private static final String SA_CHECK_LOGIN_NAME = BASE_CLASS_NAME + ".SaCheckLogin";
private static final Class<? extends Annotation> SA_CHECK_ROLE_CLASS;
private static final Class<? extends Annotation> SA_CHECK_PERMISSION_CLASS;
private static final Class<? extends Annotation> SA_IGNORE_CLASS;
private static final Class<? extends Annotation> SA_CHECK_LOGIN_CLASS;
static {
// 通过类加载器去加载注解类Class实例
SA_CHECK_ROLE_CLASS = (Class<? extends Annotation>) ClassLoaderUtil.loadClass(SA_CHECK_ROLE_CLASS_NAME, false);
SA_CHECK_PERMISSION_CLASS = (Class<? extends Annotation>) ClassLoaderUtil.loadClass(SA_CHECK_PERMISSION_CLASS_NAME, false);
SA_IGNORE_CLASS = (Class<? extends Annotation>) ClassLoaderUtil.loadClass(SA_IGNORE_CLASS_NAME, false);
SA_CHECK_LOGIN_CLASS = (Class<? extends Annotation>) ClassLoaderUtil.loadClass(SA_CHECK_LOGIN_NAME, false);
if (log.isDebugEnabled()) {
log.debug("SaTokenAnnotationJavadocResolver init success, load annotation class: {}", List.of(SA_CHECK_ROLE_CLASS, SA_CHECK_PERMISSION_CLASS, SA_IGNORE_CLASS, SA_CHECK_LOGIN_CLASS));
}
}
public SaTokenAnnotationMetadataJavadocResolver() {
this(DEFAULT_METADATA_PROVIDER);
}
public SaTokenAnnotationMetadataJavadocResolver(Supplier<SaTokenSecurityMetadata> metadataProvider) {
super(metadataProvider);
}
public SaTokenAnnotationMetadataJavadocResolver(int order) {
this(DEFAULT_METADATA_PROVIDER,order);
}
public SaTokenAnnotationMetadataJavadocResolver(Supplier<SaTokenSecurityMetadata> metadataProvider, int order) {
super(metadataProvider,order);
}
@Override
public boolean supports(HandlerMethod handlerMethod) {
return hasAnnotation(handlerMethod, SA_CHECK_ROLE_CLASS) || hasAnnotation(handlerMethod, SA_CHECK_PERMISSION_CLASS) || hasAnnotation(handlerMethod, SA_IGNORE_CLASS);
}
@Override
public String resolve(HandlerMethod handlerMethod, Operation operation, SaTokenSecurityMetadata metadata) {
// 检查是否忽略校验
if(hasAnnotation(handlerMethod, SA_IGNORE_CLASS_NAME)){
metadata.setIgnore(true);
return metadata.toMarkdownString();
}
// 解析权限校验
resolvePermissionCheck(handlerMethod, metadata);
// 解析角色校验
resolveRoleCheck(handlerMethod, metadata);
return metadata.toMarkdownString();
}
/**
* 解析权限校验
*/
private void resolvePermissionCheck(HandlerMethod handlerMethod, SaTokenSecurityMetadata metadata) {
// 解析获取方法上的注解角色信息
if (hasMethodAnnotation(handlerMethod, SA_CHECK_PERMISSION_CLASS_NAME)) {
Map<String, Object> annotationValueMap = getMethodAnnotationValueMap(handlerMethod, SA_CHECK_PERMISSION_CLASS);
resolvePermissionAnnotation(metadata, annotationValueMap);
}
// 解析获取类上的注解角色信息
if (hasClassAnnotation(handlerMethod, SA_CHECK_PERMISSION_CLASS_NAME)) {
Map<String, Object> annotationValueMap = getClassAnnotationValueMap(handlerMethod, SA_CHECK_PERMISSION_CLASS);
resolvePermissionAnnotation(metadata, annotationValueMap);
}
}
/**
* 解析权限注解
*/
private void resolvePermissionAnnotation(SaTokenSecurityMetadata metadata, Map<String, Object> annotationValueMap) {
try {
// 反射获取注解属性
Object value = annotationValueMap.get( "value");
Object mode = annotationValueMap.get( "mode");
Object type = annotationValueMap.get( "type");
Object orRole = annotationValueMap.get( "orRole");
String[] values = Convert.toStrArray(value);
String modeStr = mode != null ? mode.toString() : "AND";
String typeStr = type != null ? type.toString() : "";
String[] orRoles = Convert.toStrArray(orRole);
metadata.addPermission(values, modeStr, typeStr, orRoles);
} catch (Exception ignore) {
// 忽略解析错误
}
}
/**
* 解析角色校验
*/
private void resolveRoleCheck(HandlerMethod handlerMethod, SaTokenSecurityMetadata metadata) {
// 解析获取方法上的注解角色信息
if (hasMethodAnnotation(handlerMethod, SA_CHECK_ROLE_CLASS_NAME)) {
Map<String, Object> annotationValueMap = getMethodAnnotationValueMap(handlerMethod, SA_CHECK_ROLE_CLASS);
resolveRoleAnnotation(metadata, annotationValueMap);
}
// 解析获取类上的注解角色信息
if (hasClassAnnotation(handlerMethod, SA_CHECK_ROLE_CLASS_NAME)) {
Map<String, Object> annotationValueMap = getClassAnnotationValueMap(handlerMethod, SA_CHECK_ROLE_CLASS);
resolveRoleAnnotation(metadata, annotationValueMap);
}
}
/**
* 解析角色注解
*/
private void resolveRoleAnnotation(SaTokenSecurityMetadata metadata, Map<String, Object> annotationValueMap) {
try {
// 反射获取注解属性
Object value = annotationValueMap.get("value");
Object mode = annotationValueMap.get("mode");
Object type = annotationValueMap.get("type");
String[] values = Convert.toStrArray(value);
String modeStr = mode != null ? mode.toString() : "AND";
String typeStr = type != null ? type.toString() : "";
metadata.addRole(values, modeStr, typeStr);
} catch (Exception ignore) {
// 忽略解析错误
}
}
}

View File

@@ -12,6 +12,7 @@ import io.swagger.v3.oas.models.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.doc.core.resolver.JavadocResolver;
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
import org.springdoc.core.properties.SpringDocConfigProperties;
@@ -83,6 +84,11 @@ public class OpenApiHandler extends OpenAPIService {
*/
private final PropertyResolverUtils propertyResolverUtils;
/**
* Javadoc解析器接口
*/
private final List<JavadocResolver> javadocResolvers;
/**
* The javadoc provider.
*/
@@ -123,7 +129,8 @@ public class OpenApiHandler extends OpenAPIService {
SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils,
Optional<List<OpenApiBuilderCustomizer>> openApiBuilderCustomizers,
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers,
Optional<JavadocProvider> javadocProvider) {
Optional<JavadocProvider> javadocProvider,
List<JavadocResolver> javadocResolvers) {
super(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider);
if (openAPI.isPresent()) {
this.openAPI = openAPI.get();
@@ -140,6 +147,7 @@ public class OpenApiHandler extends OpenAPIService {
this.openApiBuilderCustomisers = openApiBuilderCustomizers;
this.serverBaseUrlCustomizers = serverBaseUrlCustomizers;
this.javadocProvider = javadocProvider;
this.javadocResolvers = javadocResolvers == null ? new ArrayList<>() : javadocResolvers;
if (springDocConfigProperties.isUseFqn())
TypeNameResolver.std.setUseFqn(true);
}
@@ -220,6 +228,22 @@ public class OpenApiHandler extends OpenAPIService {
securityParser.buildSecurityRequirement(securityRequirements, operation);
}
if (javadocProvider.isPresent()) {
String description = javadocProvider.get().getMethodJavadocDescription(handlerMethod.getMethod());
String summary = javadocProvider.get().getFirstSentence(description);
if (StringUtils.isNotBlank(description)){
operation.setSummary(summary);
}
// 调用解析器提取JavaDoc中的权限信息
if (javadocResolvers != null && !javadocResolvers.isEmpty()) {
for (JavadocResolver resolver : javadocResolvers) {
String desc = resolver.resolve(handlerMethod, operation);
description = description + desc;
}
operation.setDescription(description);
}
}
return operation;
}

View File

@@ -24,7 +24,7 @@
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<artifactId>bcpkix-jdk18on</artifactId>
</dependency>
<dependency>

View File

@@ -6,10 +6,7 @@ import cn.hutool.core.util.ObjectUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.plugin.*;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.encrypt.annotation.EncryptField;
import org.dromara.common.encrypt.core.EncryptContext;
@@ -42,19 +39,19 @@ public class MybatisEncryptInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
return invocation;
}
@Override
public Object plugin(Object target) {
Object target = invocation.getTarget();
if (target instanceof ParameterHandler parameterHandler) {
// 进行加密操作
Object parameterObject = parameterHandler.getParameterObject();
if (ObjectUtil.isNotNull(parameterObject) && !(parameterObject instanceof String)) {
this.encryptHandler(parameterObject);
}
}
return target;
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**

View File

@@ -33,7 +33,7 @@ public class ExcelBigNumberConvert implements Converter<Long> {
@Override
public Long convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
return Convert.toLong(cellData.getData());
return Convert.toLong(cellData.getStringValue());
}
@Override

View File

@@ -64,8 +64,10 @@ public class CellMergeHandler {
// 当前行数据字段值
Object currentRowObjFieldVal = ReflectUtils.invokeGetter(currentRowObj, field.getName());
// 空值跳过不处理
if (currentRowObjFieldVal == null || "".equals(currentRowObjFieldVal)) {
// 空值视为合并中断,需要先收口上一段合并区间
if (isBlankCell(currentRowObjFieldVal)) {
appendMergeResult(result, rowRepeatCellMap.get(field), i - 1, colNum);
rowRepeatCellMap.remove(field);
continue;
}
@@ -78,7 +80,6 @@ public class CellMergeHandler {
// 获取 单元格合并Map 中字段值
RepeatCell repeatCell = rowRepeatCellMap.get(field);
Object cellValue = repeatCell.value();
int current = repeatCell.current();
// 检查是否满足合并条件
// currentRowObj 当前行数据
@@ -86,33 +87,14 @@ public class CellMergeHandler {
// cellMerge 当前行字段合并注解
boolean merge = isMerge(currentRowObj, rows.get(i - 1), cellMerge);
// 是否添加到结果集
boolean isAddResult = false;
// 最新行
int lastRow = i + rowIndex - 1;
// 如果当前行字段值和缓存中的字段值不相等,或不满足合并条件,则替换
if (!currentRowObjFieldVal.equals(cellValue) || !merge) {
appendMergeResult(result, repeatCell, i - 1, colNum);
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) {
//如果是同一行,则跳过合并
if (current + rowIndex == lastRow) {
continue;
}
result.add(new CellRangeAddress(current + rowIndex, lastRow, colNum, colNum));
}
}
appendMergeResult(result, rowRepeatCellMap.get(field), rows.size() - 1, colNum);
rowRepeatCellMap.remove(field);
}
return result;
}
@@ -167,6 +149,17 @@ public class CellMergeHandler {
return true;
}
private boolean isBlankCell(Object value) {
return value == null || StrUtil.isBlankIfStr(value);
}
private void appendMergeResult(List<CellRangeAddress> result, RepeatCell repeatCell, int endIndex, int colNum) {
if (repeatCell == null || endIndex <= repeatCell.current()) {
return;
}
result.add(new CellRangeAddress(repeatCell.current() + rowIndex, endIndex + rowIndex, colNum, colNum));
}
/**
* 单元格合并
*/

View File

@@ -130,8 +130,9 @@ public class ExcelDownHandler implements SheetWriteHandler {
}
if (ObjectUtil.isNotEmpty(options)) {
// 仅当下拉可选项不为空时执行
if (options.size() > 20) {
// 这里限制如果可选项大于20则使用额外表形式
int totalCharacter = options.stream().mapToInt(String::length).sum() + options.size();
if (options.size() > 20 || totalCharacter > 255) {
// 这里限制如果可选项大于20 或 总字符数超过255则使用额外表形式
dropDownWithSheet(helper, workbook, sheet, index, options);
} else {
// 否则使用固定值形式

View File

@@ -12,6 +12,7 @@ import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.constant.HttpStatus;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MessageUtils;
@@ -76,16 +77,16 @@ public class RepeatSubmitAspect {
*/
@AfterReturning(pointcut = "@annotation(repeatSubmit)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Object jsonResult) {
if (jsonResult instanceof R<?> r) {
try {
try {
if (jsonResult instanceof R<?> r) {
// 成功则不删除redis数据 保证在有效时间内无法重复提交
if (r.getCode() == R.SUCCESS) {
if (r.getCode() == HttpStatus.SUCCESS) {
return;
}
RedisUtils.deleteObject(KEY_CACHE.get());
} finally {
KEY_CACHE.remove();
}
} finally {
KEY_CACHE.remove();
}
}

View File

@@ -192,7 +192,7 @@ public class MailUtils {
/**
* 发送邮件给多人
*
* @param mailAccount 邮件户信息
* @param mailAccount 邮件户信息
* @param tos 收件人列表
* @param subject 标题
* @param content 正文
@@ -207,7 +207,7 @@ public class MailUtils {
/**
* 发送邮件给多人
*
* @param mailAccount 邮件户信息
* @param mailAccount 邮件户信息
* @param tos 收件人列表
* @param ccs 抄送人列表可以为null或空
* @param bccs 密送人列表可以为null或空
@@ -343,7 +343,7 @@ public class MailUtils {
/**
* 发送邮件给多人
*
* @param mailAccount 邮件户信息
* @param mailAccount 邮件户信息
* @param tos 收件人列表
* @param subject 标题
* @param content 正文
@@ -360,7 +360,7 @@ public class MailUtils {
/**
* 发送邮件给多人
*
* @param mailAccount 邮件户信息
* @param mailAccount 邮件户信息
* @param tos 收件人列表
* @param ccs 抄送人列表可以为null或空
* @param bccs 密送人列表可以为null或空
@@ -400,7 +400,7 @@ public class MailUtils {
/**
* 发送邮件给多人
*
* @param mailAccount 邮件户信息
* @param mailAccount 邮件户信息
* @param useGlobalSession 是否全局共享Session
* @param tos 收件人列表
* @param ccs 抄送人列表可以为null或空

View File

@@ -46,6 +46,24 @@ public class DataBaseHelper {
}
}
/**
* 获取指定数据源对应的数据库类型
*
* @param dsName 数据源名称
* @return 指定数据库对应的 DataBaseType 枚举,找不到时默认返回 MY_SQL
* @throws ServiceException 当获取数据库连接或元数据出现异常时抛出业务异常
*/
public static DataBaseType getDataBaseType(String dsName) {
DataSource dataSource = DS.getDataSource(dsName);
try (Connection conn = dataSource.getConnection()) {
DatabaseMetaData metaData = conn.getMetaData();
String databaseProductName = metaData.getDatabaseProductName();
return DataBaseType.find(databaseProductName);
} catch (SQLException e) {
throw new RuntimeException("获取数据库类型失败", e);
}
}
/**
* 根据当前数据库类型,生成兼容的 FIND_IN_SET 语句片段
* <p>

View File

@@ -23,7 +23,7 @@ import java.util.function.Supplier;
* @version 3.5.0
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings("unchecked cast")
@SuppressWarnings("unchecked")
public class DataPermissionHelper {
private static final String DATA_PERMISSION_KEY = "data:permission";

View File

@@ -31,7 +31,7 @@
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<exclusions>
<!-- 将基于 CRT 的 HTTP 客户端从类路径中移除 -->
<!-- 东西 30M 特别大的 jar 包 性能跟 Netty 差不多 有需要可以自行替换使用 -->
<exclusion>
<groupId>software.amazon.awssdk</groupId>
<artifactId>aws-crt-client</artifactId>
@@ -49,13 +49,13 @@
</exclusions>
</dependency>
<!-- 将基于 Netty 的 HTTP 客户端从类路径中移除 -->
<!-- 适用于 Netty 的客户端 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>netty-nio-client</artifactId>
</dependency>
<!-- 基于 AWS CRT 的 S3 客户端的性能增强的 S3 传输管理器 -->
<!-- 客户端的性能增强传输管理器 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3-transfer-manager</artifactId>

View File

@@ -141,7 +141,8 @@ public class OssClient {
try {
// 构建上传请求对象
FileUpload fileUpload = transferManager.uploadFile(
x -> x.putObjectRequest(
x -> {
x.source(filePath).putObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(key)
.contentMD5(StringUtils.isNotEmpty(md5Digest) ? md5Digest : null)
@@ -149,10 +150,13 @@ public class OssClient {
// 用于设置对象的访问控制列表ACL。不同云厂商对ACL的支持和实现方式有所不同
// 因此根据具体的云服务提供商你可能需要进行不同的配置自行开启阿里云有acl权限配置腾讯云没有acl权限配置
//.acl(getAccessPolicy().getObjectCannedACL())
.build())
.addTransferListener(LoggingTransferListener.create())
.source(filePath).build());
.build()
);
if (log.isDebugEnabled()) {
x.addTransferListener(LoggingTransferListener.create());
}
}
);
// 等待上传完成并获取上传结果
CompletedFileUpload uploadResult = fileUpload.completionFuture().join();
String eTag = uploadResult.response().eTag();
@@ -192,16 +196,21 @@ public class OssClient {
// 使用 transferManager 进行上传
Upload upload = transferManager.upload(
x -> x.requestBody(body).addTransferListener(LoggingTransferListener.create())
.putObjectRequest(
x -> {
x.requestBody(body).putObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(key)
.contentType(contentType)
// 用于设置对象的访问控制列表ACL。不同云厂商对ACL的支持和实现方式有所不同
// 因此根据具体的云服务提供商你可能需要进行不同的配置自行开启阿里云有acl权限配置腾讯云没有acl权限配置
//.acl(getAccessPolicy().getObjectCannedACL())
.build())
.build());
.build()
);
if (log.isDebugEnabled()) {
x.addTransferListener(LoggingTransferListener.create());
}
}
);
// 将输入流写入请求体
body.writeInputStream(inputStream);
@@ -229,13 +238,17 @@ public class OssClient {
Path tempFilePath = FileUtils.createTempFile().toPath();
// 使用 S3TransferManager 下载文件
FileDownload downloadFile = transferManager.downloadFile(
x -> x.getObjectRequest(
x -> {
x.destination(tempFilePath).getObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(removeBaseUrl(path))
.build())
.addTransferListener(LoggingTransferListener.create())
.destination(tempFilePath)
.build());
.build()
);
if (log.isDebugEnabled()) {
x.addTransferListener(LoggingTransferListener.create());
}
}
);
// 等待文件下载操作完成
downloadFile.completionFuture().join();
return tempFilePath;
@@ -244,8 +257,8 @@ public class OssClient {
/**
* 下载文件从 Amazon S3 到 输出流
*
* @param key 文件在 Amazon S3 中的对象键
* @param out 输出流
* @param key 文件在 Amazon S3 中的对象键
* @param out 输出流
* @param consumer 自定义处理逻辑
* @throws OssException 如果下载失败,抛出自定义异常
*/
@@ -260,26 +273,24 @@ public class OssClient {
/**
* 下载文件从 Amazon S3 到 输出流
*
* @param key 文件在 Amazon S3 中的对象键
* @param key 文件在 Amazon S3 中的对象键
* @param contentLengthConsumer 文件大小消费者函数
* @return 写出订阅器
* @throws OssException 如果下载失败,抛出自定义异常
*/
public WriteOutSubscriber<OutputStream> download(String key, Consumer<Long> contentLengthConsumer) {
try {
// 构建下载请求
DownloadRequest<ResponsePublisher<GetObjectResponse>> publisherDownloadRequest = DownloadRequest.builder()
// 文件对象
.getObjectRequest(y -> y.bucket(properties.getBucketName())
.key(key)
.build())
.addTransferListener(LoggingTransferListener.create())
DownloadRequest.TypedBuilder<ResponsePublisher<GetObjectResponse>> typedBuilder = DownloadRequest.builder()
// 使用发布订阅转换器
.responseTransformer(AsyncResponseTransformer.toPublisher())
.build();
// 文件对象
.getObjectRequest(y -> y.bucket(properties.getBucketName()).key(key).build());
if (log.isDebugEnabled()) {
typedBuilder.addTransferListener(LoggingTransferListener.create());
}
// 使用 S3TransferManager 下载文件
Download<ResponsePublisher<GetObjectResponse>> publisherDownload = transferManager.download(publisherDownloadRequest);
Download<ResponsePublisher<GetObjectResponse>> publisherDownload = transferManager.download(typedBuilder.build());
// 获取下载发布订阅转换器
ResponsePublisher<GetObjectResponse> publisher = publisherDownload.completionFuture().join().result();
// 执行文件大小消费者函数
@@ -289,7 +300,7 @@ public class OssClient {
// 构建写出订阅器对象
return out -> {
// 创建可写入的字节通道
try(WritableByteChannel channel = Channels.newChannel(out)){
try (WritableByteChannel channel = Channels.newChannel(out)) {
// 订阅数据
publisher.subscribe(byteBuffer -> {
while (byteBuffer.hasRemaining()) {
@@ -347,7 +358,7 @@ public class OssClient {
*
* @param objectKey 对象KEY
* @param expiredTime 链接授权到期时间
* @param metadata 元数据
* @param metadata 元数据
*/
public String createPresignedPutUrl(String objectKey, Duration expiredTime, Map<String, String> metadata) {
// 使用 AWS S3 预签名 URL 的生成器 获取上传文件对象的预签名 URL

View File

@@ -9,7 +9,6 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.redis.config.properties.RedissonProperties;
import org.dromara.common.redis.handler.KeyPrefixHandler;
import org.dromara.common.redis.handler.RedisExceptionHandler;
@@ -21,7 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.core.task.VirtualThreadTaskExecutor;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@@ -65,9 +63,10 @@ public class RedisConfig {
// 缓存 Lua 脚本 减少网络传输(redisson 大部分的功能都是基于 Lua 脚本实现)
.setUseScriptCache(true)
.setCodec(codec);
if (SpringUtils.isVirtual()) {
config.setNettyExecutor(new VirtualThreadTaskExecutor("redisson-"));
}
// netty 对虚拟线程适配有问题 暂时禁止使用
//if (SpringUtils.isVirtual()) {
// config.setNettyExecutor(new VirtualThreadTaskExecutor("redisson-"));
//}
RedissonProperties.SingleServerConfig singleServerConfig = redissonProperties.getSingleServerConfig();
if (ObjectUtil.isNotNull(singleServerConfig)) {
// 使用单机模式

View File

@@ -41,6 +41,11 @@
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -113,7 +113,7 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
* @param key 键名称
* @return object
*/
@SuppressWarnings("unchecked cast")
@SuppressWarnings("unchecked")
@Override
public <T> T getObject(String key, Class<T> classType) {
Object o = CAFFEINE.get(key, k -> RedisUtils.getCacheObject(key));

View File

@@ -63,7 +63,7 @@ public class LoginHelper {
/**
* 获取用户(多级缓存)
*/
@SuppressWarnings("unchecked cast")
@SuppressWarnings("unchecked")
public static <T extends LoginUser> T getLoginUser() {
SaSession session = StpUtil.getTokenSession();
if (ObjectUtil.isNull(session)) {
@@ -75,7 +75,7 @@ public class LoginHelper {
/**
* 获取用户基于token
*/
@SuppressWarnings("unchecked cast")
@SuppressWarnings("unchecked")
public static <T extends LoginUser> T getLoginUser(String token) {
SaSession session = StpUtil.getTokenSessionByToken(token);
if (ObjectUtil.isNull(session)) {

View File

@@ -25,9 +25,24 @@ import java.util.Objects;
@Slf4j
public class SensitiveHandler extends JsonSerializer<String> implements ContextualSerializer {
private SensitiveStrategy strategy;
private String[] roleKey;
private String[] perms;
private final SensitiveStrategy strategy;
private final String[] roleKey;
private final String[] perms;
/**
* 提供给 jackson 创建上下文序列化器时使用 不然会报错
*/
public SensitiveHandler() {
this.strategy = null;
this.roleKey = null;
this.perms = null;
}
public SensitiveHandler(SensitiveStrategy strategy, String[] strings, String[] perms) {
this.strategy = strategy;
this.roleKey = strings;
this.perms = perms;
}
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
@@ -48,10 +63,7 @@ public class SensitiveHandler extends JsonSerializer<String> implements Contextu
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
Sensitive annotation = property.getAnnotation(Sensitive.class);
if (Objects.nonNull(annotation) && Objects.equals(String.class, property.getType().getRawClass())) {
this.strategy = annotation.strategy();
this.roleKey = annotation.roleKey();
this.perms = annotation.perms();
return this;
return new SensitiveHandler(annotation.strategy(), annotation.roleKey(), annotation.perms());
}
return prov.findValueSerializer(property.getType(), property);
}

View File

@@ -13,7 +13,7 @@ public interface TransConstant {
String USER_ID_TO_NAME = "user_id_to_name";
/**
* 用户id转用户
* 用户id转用户
*/
String USER_ID_TO_NICKNAME = "user_id_to_nickname";

View File

@@ -31,7 +31,18 @@ public class TranslationHandler extends JsonSerializer<Object> implements Contex
*/
public static final Map<String, TranslationInterface<?>> TRANSLATION_MAPPER = new ConcurrentHashMap<>();
private Translation translation;
private final Translation translation;
/**
* 提供给 jackson 创建上下文序列化器时使用 不然会报错
*/
public TranslationHandler() {
this.translation = null;
}
public TranslationHandler(Translation translation) {
this.translation = translation;
}
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
@@ -63,8 +74,7 @@ public class TranslationHandler extends JsonSerializer<Object> implements Contex
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
Translation translation = property.getAnnotation(Translation.class);
if (Objects.nonNull(translation)) {
this.translation = translation;
return this;
return new TranslationHandler(translation);
}
return prov.findValueSerializer(property.getType(), property);
}

View File

@@ -7,7 +7,7 @@ import org.dromara.common.translation.constant.TransConstant;
import org.dromara.common.translation.core.TranslationInterface;
/**
* 用户称翻译实现
* 用户称翻译实现
*
* @author may
*/

View File

@@ -1,16 +1,8 @@
package org.dromara.common.web.config;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
import cn.hutool.captcha.LineCaptcha;
import cn.hutool.captcha.ShearCaptcha;
import org.dromara.common.web.config.properties.CaptchaProperties;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Lazy;
import java.awt.*;
/**
* 验证码配置
@@ -21,45 +13,4 @@ import java.awt.*;
@EnableConfigurationProperties(CaptchaProperties.class)
public class CaptchaConfig {
private static final int WIDTH = 160;
private static final int HEIGHT = 60;
private static final Color BACKGROUND = Color.LIGHT_GRAY;
private static final Font FONT = new Font("Arial", Font.BOLD, 48);
/**
* 圆圈干扰验证码
*/
@Lazy
@Bean
public CircleCaptcha circleCaptcha() {
CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(WIDTH, HEIGHT);
captcha.setBackground(BACKGROUND);
captcha.setFont(FONT);
return captcha;
}
/**
* 线段干扰的验证码
*/
@Lazy
@Bean
public LineCaptcha lineCaptcha() {
LineCaptcha captcha = CaptchaUtil.createLineCaptcha(WIDTH, HEIGHT);
captcha.setBackground(BACKGROUND);
captcha.setFont(FONT);
return captcha;
}
/**
* 扭曲干扰验证码
*/
@Lazy
@Bean
public ShearCaptcha shearCaptcha() {
ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(WIDTH, HEIGHT);
captcha.setBackground(BACKGROUND);
captcha.setFont(FONT);
return captcha;
}
}

View File

@@ -1,11 +1,14 @@
package org.dromara.common.web.config;
import io.undertow.UndertowOptions;
import io.undertow.server.DefaultByteBufferPool;
import io.undertow.server.handlers.DisallowedMethodsHandler;
import io.undertow.util.HttpString;
import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
import org.dromara.common.core.utils.SpringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.core.task.VirtualThreadTaskExecutor;
@@ -18,6 +21,9 @@ import org.springframework.core.task.VirtualThreadTaskExecutor;
@AutoConfiguration
public class UndertowConfig implements WebServerFactoryCustomizer<UndertowServletWebServerFactory> {
@Autowired
private ServerProperties serverProperties;
/**
* 自定义 Undertow 配置
* <p>
@@ -31,6 +37,11 @@ public class UndertowConfig implements WebServerFactoryCustomizer<UndertowServle
*/
@Override
public void customize(UndertowServletWebServerFactory factory) {
long bytes = serverProperties.getUndertow().getMaxHttpPostSize().toBytes();
factory.addBuilderCustomizers(builder -> {
builder.setServerOption(UndertowOptions.MULTIPART_MAX_ENTITY_SIZE, bytes);
});
factory.addDeploymentInfoCustomizers(deploymentInfo -> {
// 配置 WebSocket 部署信息,设置 WebSocket 使用的缓冲区池
WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo();

View File

@@ -1,7 +1,5 @@
package org.dromara.common.web.config.properties;
import org.dromara.common.web.enums.CaptchaCategory;
import org.dromara.common.web.enums.CaptchaType;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -19,12 +17,7 @@ public class CaptchaProperties {
/**
* 验证码类型
*/
private CaptchaType type;
/**
* 验证码类别
*/
private CaptchaCategory category;
private String type;
/**
* 数字验证码位数

View File

@@ -0,0 +1,197 @@
package org.dromara.common.web.core;
import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.captcha.generator.RandomGenerator;
import cn.hutool.core.img.GraphicsUtil;
import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.Serial;
import java.util.concurrent.ThreadLocalRandom;
/**
* 带干扰线、波浪、圆的验证码
*
* @author Lion Li
*/
public class WaveAndCircleCaptcha extends AbstractCaptcha {
@Serial
private static final long serialVersionUID = 1L;
// 构造方法(略,与之前一致)
public WaveAndCircleCaptcha(int width, int height) {
this(width, height, 4);
}
public WaveAndCircleCaptcha(int width, int height, int codeCount) {
this(width, height, codeCount, 6);
}
public WaveAndCircleCaptcha(int width, int height, int codeCount, int interfereCount) {
this(width, height, new RandomGenerator(codeCount), interfereCount);
}
public WaveAndCircleCaptcha(int width, int height, CodeGenerator generator, int interfereCount) {
super(width, height, generator, interfereCount);
}
public WaveAndCircleCaptcha(int width, int height, int codeCount, int interfereCount, float size) {
super(width, height, new RandomGenerator(codeCount), interfereCount, size);
}
@Override
public Image createImage(String code) {
final BufferedImage image = new BufferedImage(
width,
height,
(null == this.background) ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_INT_RGB
);
final Graphics2D g = ImgUtil.createGraphics(image, this.background);
try {
drawString(g, code);
// 扭曲
shear(g, this.width, this.height, ObjectUtil.defaultIfNull(this.background, Color.WHITE));
drawInterfere(g);
} finally {
g.dispose();
}
return image;
}
private void drawString(Graphics2D g, String code) {
// 设置抗锯齿(让字体渲染更清晰)
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
if (this.textAlpha != null) {
g.setComposite(this.textAlpha);
}
GraphicsUtil.drawStringColourful(g, code, this.font, this.width, this.height);
}
protected void drawInterfere(Graphics2D g) {
ThreadLocalRandom random = RandomUtil.getRandom();
int circleCount = Math.max(0, this.interfereCount - 1);
// 圈圈
for (int i = 0; i < circleCount; i++) {
g.setColor(ImgUtil.randomColor(random));
int x = random.nextInt(width);
int y = random.nextInt(height);
int w = random.nextInt(height >> 1);
int h = random.nextInt(height >> 1);
g.drawOval(x, y, w, h);
}
// 仅 1 条平滑波浪线
if (this.interfereCount >= 1) {
g.setColor(getRandomColor(120, 230, random));
drawSmoothWave(g, random);
}
}
private void drawSmoothWave(Graphics2D g, ThreadLocalRandom random) {
int amplitude = random.nextInt(8) + 5; // 波动幅度
int wavelength = random.nextInt(40) + 30; // 波长
double phase = random.nextDouble() * Math.PI * 2;
// ✅ 关键:限制 baseY 在中间区域
int centerY = height / 2;
int verticalJitter = Math.max(5, height / 6); // 至少偏移5像素
int baseY = centerY - verticalJitter + random.nextInt(verticalJitter * 2);
g.setStroke(new BasicStroke(2.5f)); // 线宽
int[] xPoints = new int[width];
int[] yPoints = new int[width];
for (int x = 0; x < width; x++) {
int y = baseY + (int) (amplitude * Math.sin((double) x / wavelength * 2 * Math.PI + phase));
// 限制 y 不要超出图像边界(可选)
y = Math.max(amplitude, Math.min(y, height - amplitude));
xPoints[x] = x;
yPoints[x] = y;
}
g.drawPolyline(xPoints, yPoints, width);
}
private Color getRandomColor(int min, int max, ThreadLocalRandom random) {
int range = max - min;
return new Color(
min + random.nextInt(range),
min + random.nextInt(range),
min + random.nextInt(range)
);
}
/**
* 扭曲
*
* @param g {@link Graphics}
* @param w1 w1
* @param h1 h1
* @param color 颜色
*/
private void shear(Graphics g, int w1, int h1, Color color) {
shearX(g, w1, h1, color);
shearY(g, w1, h1, color);
}
/**
* X坐标扭曲
*
* @param g {@link Graphics}
* @param w1 宽
* @param h1 高
* @param color 颜色
*/
private void shearX(Graphics g, int w1, int h1, Color color) {
int period = RandomUtil.randomInt(this.width);
int frames = 1;
int phase = RandomUtil.randomInt(2);
for (int i = 0; i < h1; i++) {
double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);
g.copyArea(0, i, w1, 1, (int) d, 0);
g.setColor(color);
g.drawLine((int) d, i, 0, i);
g.drawLine((int) d + w1, i, w1, i);
}
}
/**
* Y坐标扭曲
*
* @param g {@link Graphics}
* @param w1 宽
* @param h1 高
* @param color 颜色
*/
private void shearY(Graphics g, int w1, int h1, Color color) {
int period = RandomUtil.randomInt(this.height >> 1);
int frames = 20;
int phase = 7;
for (int i = 0; i < w1; i++) {
double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);
g.copyArea(i, 0, 1, h1, 0, (int) d);
g.setColor(color);
// 擦除原位置的痕迹
g.drawLine(i, (int) d, i, 0);
g.drawLine(i, (int) d + h1, i, h1);
}
}
}

View File

@@ -1,35 +0,0 @@
package org.dromara.common.web.enums;
import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.captcha.CircleCaptcha;
import cn.hutool.captcha.LineCaptcha;
import cn.hutool.captcha.ShearCaptcha;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 验证码类别
*
* @author Lion Li
*/
@Getter
@AllArgsConstructor
public enum CaptchaCategory {
/**
* 线段干扰
*/
LINE(LineCaptcha.class),
/**
* 圆圈干扰
*/
CIRCLE(CircleCaptcha.class),
/**
* 扭曲干扰
*/
SHEAR(ShearCaptcha.class);
private final Class<? extends AbstractCaptcha> clazz;
}

View File

@@ -1,29 +0,0 @@
package org.dromara.common.web.enums;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.captcha.generator.MathGenerator;
import cn.hutool.captcha.generator.RandomGenerator;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 验证码类型
*
* @author Lion Li
*/
@Getter
@AllArgsConstructor
public enum CaptchaType {
/**
* 数字
*/
MATH(MathGenerator.class),
/**
* 字符
*/
CHAR(RandomGenerator.class);
private final Class<? extends CodeGenerator> clazz;
}

View File

@@ -2,7 +2,6 @@ package org.dromara.common.web.handler;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.HttpStatus;
import com.fasterxml.jackson.core.JsonParseException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
@@ -14,6 +13,8 @@ import org.dromara.common.core.exception.SseException;
import org.dromara.common.core.exception.base.BaseException;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.springframework.boot.json.JsonParseException;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.expression.ExpressionException;
import org.springframework.http.converter.HttpMessageNotReadableException;
@@ -25,6 +26,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
@@ -78,7 +80,7 @@ public class GlobalExceptionHandler {
public R<Void> handleServletException(ServletException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生未知异常.", requestURI, e);
return R.fail(e.getMessage());
return R.fail("发生未知异常,请联系管理员");
}
/**
@@ -117,7 +119,7 @@ public class GlobalExceptionHandler {
public R<Void> handleNoHandlerFoundException(NoHandlerFoundException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}'不存在.", requestURI);
return R.fail(HttpStatus.HTTP_NOT_FOUND, e.getMessage());
return R.fail(HttpStatus.HTTP_NOT_FOUND, "请求地址不存在");
}
/**
@@ -148,7 +150,7 @@ public class GlobalExceptionHandler {
public R<Void> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生未知异常.", requestURI, e);
return R.fail(e.getMessage());
return R.fail("发生未知异常,请联系管理员");
}
/**
@@ -158,7 +160,7 @@ public class GlobalExceptionHandler {
public R<Void> handleException(Exception e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生系统异常.", requestURI, e);
return R.fail(e.getMessage());
return R.fail("发生系统异常,请联系管理员");
}
/**
@@ -191,6 +193,16 @@ public class GlobalExceptionHandler {
return R.fail(message);
}
/**
* 方法参数校验异常 用于处理 @Validated 注解
*/
@ExceptionHandler(HandlerMethodValidationException.class)
public R<Void> handlerMethodValidationException(HandlerMethodValidationException e) {
log.error(e.getMessage());
String message = StreamUtils.join(e.getAllErrors(), MessageSourceResolvable::getDefaultMessage, ", ");
return R.fail(message);
}
/**
* JSON 解析异常Jackson 在处理 JSON 格式出错时抛出)
* 可能是请求体格式非法,也可能是服务端反序列化失败

View File

@@ -8,6 +8,7 @@ import org.dromara.common.websocket.holder.WebSocketSessionHolder;
import org.dromara.common.websocket.utils.WebSocketUtils;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator;
import java.io.IOException;
import java.util.List;
@@ -33,7 +34,7 @@ public class PlusWebSocketHandler extends AbstractWebSocketHandler {
log.info("[connect] invalid token received. sessionId: {}", session.getId());
return;
}
WebSocketSessionHolder.addSession(loginUser.getUserId(), session);
WebSocketSessionHolder.addSession(loginUser.getUserId(), new ConcurrentWebSocketSessionDecorator(session, 10 * 1000, 64000));
log.info("[connect] sessionId: {},userId:{},userType:{}", session.getId(), loginUser.getUserId(), loginUser.getUserType());
}

View File

@@ -113,7 +113,7 @@ public class WebSocketUtils {
* @param session WebSocket会话
* @param message 要发送的WebSocket消息对象
*/
private synchronized static void sendMessage(WebSocketSession session, WebSocketMessage<?> message) {
private static void sendMessage(WebSocketSession session, WebSocketMessage<?> message) {
if (session == null || !session.isOpen()) {
log.warn("[send] session会话已经关闭");
} else {

View File

@@ -1,146 +0,0 @@
package com.aizuda.snailjob.server.common.register;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.aizuda.snailjob.common.core.enums.NodeTypeEnum;
import com.aizuda.snailjob.common.core.util.JsonUtil;
import com.aizuda.snailjob.common.core.util.NetUtil;
import com.aizuda.snailjob.common.core.util.SnailJobVersion;
import com.aizuda.snailjob.common.core.util.StreamUtils;
import com.aizuda.snailjob.common.log.SnailJobLog;
import com.aizuda.snailjob.server.common.cache.CacheConsumerGroup;
import com.aizuda.snailjob.server.common.config.SystemProperties;
import com.aizuda.snailjob.server.common.convert.RegisterNodeInfoConverter;
import com.aizuda.snailjob.server.common.dto.ServerNodeExtAttrs;
import com.aizuda.snailjob.server.common.handler.InstanceManager;
import com.aizuda.snailjob.template.datasource.persistence.po.ServerNode;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.common.collect.Lists;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 服务端注册
*
* @author opensnail
* @date 2023-06-07
* @since 1.6.0
*/
@Component(ServerRegister.BEAN_NAME)
@RequiredArgsConstructor
public class ServerRegister extends AbstractRegister {
public static final String BEAN_NAME = "serverRegister";
private final ScheduledExecutorService serverRegisterNode = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "server-register-node"));
public static final int DELAY_TIME = 30;
public static final String CURRENT_CID;
public static final String GROUP_NAME = "DEFAULT_SERVER";
public static final String NAMESPACE_ID = "DEFAULT_SERVER_NAMESPACE_ID";
private final InstanceManager instanceManager;
private final SystemProperties systemProperties;
private final ServerProperties serverProperties;
static {
CURRENT_CID = IdUtil.getSnowflakeNextIdStr();
}
@Override
public boolean supports(int type) {
return getNodeType().equals(type);
}
@Override
protected void beforeProcessor(RegisterContext context) {
// 新增扩展参数
ServerNodeExtAttrs serverNodeExtAttrs = new ServerNodeExtAttrs();
serverNodeExtAttrs.setWebPort(serverProperties.getPort());
serverNodeExtAttrs.setSystemVersion(SnailJobVersion.getVersion());
context.setGroupName(GROUP_NAME);
context.setHostId(CURRENT_CID);
String serverHost = systemProperties.getServerHost();
if (StrUtil.isEmptyIfStr(serverHost)) {
serverHost = NetUtil.getLocalIpStr();
}
context.setHostIp(serverHost);
context.setHostPort(systemProperties.getServerPort());
context.setContextPath(Optional.ofNullable(serverProperties.getServlet().getContextPath()).orElse(StrUtil.EMPTY));
context.setNamespaceId(NAMESPACE_ID);
context.setExtAttrs(JsonUtil.toJsonString(serverNodeExtAttrs));
}
@Override
protected LocalDateTime getExpireAt() {
return LocalDateTime.now().plusSeconds(DELAY_TIME);
}
@Override
protected boolean doRegister(RegisterContext context, ServerNode serverNode) {
refreshExpireAt(Lists.newArrayList(serverNode));
return Boolean.TRUE;
}
@Override
protected void afterProcessor(final ServerNode serverNode) {
try {
// 同步当前POD消费的组的节点信息
// netty的client只会注册到一个服务端若组分配的和client连接的不是一个POD则会导致当前POD没有其他客户端的注册信息
ConcurrentMap<String /*groupName*/, Set<String>/*namespaceId*/> allConsumerGroupName = CacheConsumerGroup.getAllConsumerGroupName();
if (CollUtil.isNotEmpty(allConsumerGroupName)) {
Set<String> namespaceIdSets = StreamUtils.toSetByFlatMap(allConsumerGroupName.values(), Set::stream);
if (CollUtil.isEmpty(namespaceIdSets)) {
return;
}
List<ServerNode> serverNodes = serverNodeMapper.selectList(
new LambdaQueryWrapper<ServerNode>()
.eq(ServerNode::getNodeType, NodeTypeEnum.CLIENT.getType())
.in(ServerNode::getNamespaceId, namespaceIdSets)
.in(ServerNode::getGroupName, allConsumerGroupName.keySet()));
for (final ServerNode node : serverNodes) {
// 刷新全量本地缓存
instanceManager.registerOrUpdate(RegisterNodeInfoConverter.INSTANCE.toRegisterNodeInfo(node));
// 刷新过期时间
CacheConsumerGroup.addOrUpdate(node.getGroupName(), node.getNamespaceId());
}
}
} catch (Exception e) {
SnailJobLog.LOCAL.error("Client refresh failed", e);
}
}
@Override
protected Integer getNodeType() {
return NodeTypeEnum.SERVER.getType();
}
@Override
public void start() {
SnailJobLog.LOCAL.info("ServerRegister start");
serverRegisterNode.scheduleAtFixedRate(() -> {
try {
this.register(new RegisterContext());
} catch (Exception e) {
SnailJobLog.LOCAL.error("Server-side registration failed", e);
}
}, 0, DELAY_TIME * 2 / 3, TimeUnit.SECONDS);
}
@Override
public void close() {
SnailJobLog.LOCAL.info("ServerRegister close");
}
}

View File

@@ -0,0 +1,198 @@
package org.dromara.demo.controller;
import cn.dev33.satoken.annotation.*;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* SaToken 权限测试 接口文档输出测试
*
* @author AprilWind
*/
@Slf4j
@RestController
@RequestMapping("/demo/saTokenDoc")
public class SaTokenTestController {
// ====================== 基础场景:单一校验规则 ======================
/**
* 场景1仅登录校验无角色/权限限制,只需登录态)
*/
@SaCheckLogin
@GetMapping("/basic/loginOnly")
public R<Void> loginOnly() {
log.info("【场景1】仅登录校验通过");
return R.ok("仅登录校验通过,无需角色/权限");
}
/**
* 场景2单一角色校验AND模式默认
*/
@SaCheckRole("admin")
@GetMapping("/basic/singleRole")
public R<Void> singleRole() {
log.info("【场景2】单一角色(admin)校验通过");
return R.ok("拥有admin角色校验通过");
}
/**
* 场景3单一权限校验AND模式默认
*/
@SaCheckPermission("system:user:view")
@GetMapping("/basic/singlePermission")
public R<Void> singlePermission() {
log.info("【场景3】单一权限(system:user:view)校验通过");
return R.ok("拥有system:user:view权限校验通过");
}
/**
* 场景4忽略所有权限校验SaIgnore优先级最高
*/
@SaIgnore
@SaCheckRole("none_exist") // 该注解会被忽略
@GetMapping("/basic/ignoreAll")
public R<Void> ignoreAll() {
log.info("【场景4】SaIgnore忽略所有权限校验");
return R.ok("SaIgnore生效所有权限校验被忽略");
}
// ====================== 进阶场景多条件组合AND/OR ======================
/**
* 场景5多角色AND模式必须同时拥有所有角色
*/
@SaCheckRole(value = {"admin", "operator"}, mode = SaMode.AND)
@GetMapping("/advance/multiRoleAnd")
public R<Void> multiRoleAnd() {
log.info("【场景5】多角色AND模式(admin+operator)校验通过");
return R.ok("同时拥有admin和operator角色校验通过");
}
/**
* 场景6多角色OR模式拥有任一角色即可
*/
@SaCheckRole(value = {"admin", "test"}, mode = SaMode.OR)
@GetMapping("/advance/multiRoleOr")
public R<Void> multiRoleOr() {
log.info("【场景6】多角色OR模式(admin|test)校验通过");
return R.ok("拥有admin或test角色校验通过");
}
/**
* 场景7多权限AND模式必须同时拥有所有权限
*/
@SaCheckPermission(value = {"system:user:edit", "system:log:view"}, mode = SaMode.AND)
@GetMapping("/advance/multiPermAnd")
public R<Void> multiPermAnd() {
log.info("【场景7】多权限AND模式(system:user:edit+system:log:view)校验通过");
return R.ok("同时拥有system:user:edit和system:log:view权限校验通过");
}
/**
* 场景8多权限OR模式拥有任一权限即可
*/
@SaCheckPermission(value = {"system:user:add", "system:user:delete"}, mode = SaMode.OR)
@GetMapping("/advance/multiPermOr")
public R<Void> multiPermOr() {
log.info("【场景8】多权限OR模式(system:user:add|system:user:delete)校验通过");
return R.ok("拥有system:user:add或system:user:delete权限校验通过");
}
// ====================== 高级场景:通配符/混合组合 ======================
/**
* 场景9权限通配符匹配前缀匹配
* 拥有system:user:* 即可匹配所有用户模块权限
*/
@SaCheckPermission("system:user:*")
@GetMapping("/advanced/permWildcardPrefix")
public R<Void> permWildcardPrefix() {
log.info("【场景9】权限通配符(system:user:*)校验通过");
return R.ok("拥有system:user:*前缀权限,校验通过");
}
/**
* 场景10角色通配符匹配前缀匹配
* 拥有admin_* 即可匹配所有admin开头的角色
*/
@SaCheckRole("admin_*")
@GetMapping("/advanced/roleWildcardPrefix")
public R<Void> roleWildcardPrefix() {
log.info("【场景10】角色通配符(admin_*)校验通过");
return R.ok("拥有admin_*前缀角色,校验通过");
}
/**
* 场景11权限+角色混合AND模式所有条件必须满足
* 需同时满足拥有admin角色 + 拥有system:user:all权限
*/
@SaCheckRole("admin")
@SaCheckPermission("system:user:all")
@GetMapping("/advanced/mixRolePermAnd")
public R<Void> mixRolePermAnd() {
log.info("【场景11】角色+权限混合AND(admin+system:user:all)校验通过");
return R.ok("拥有admin角色且拥有system:user:all权限校验通过");
}
/**
* 场景12权限+角色混合OR模式任一条件满足即可
* 满足任一拥有super_admin角色 | 拥有system:manage权限
*/
@SaCheckRole(value = {"super_admin"}, mode = SaMode.OR)
@SaCheckPermission(value = {"system:manage"}, mode = SaMode.OR)
@GetMapping("/advanced/mixRolePermOr")
public R<Void> mixRolePermOr() {
log.info("【场景12】角色+权限混合OR(super_admin|system:manage)校验通过");
return R.ok("拥有super_admin角色或system:manage权限校验通过");
}
/**
* 场景13orRole参数权限校验失败时兜底角色校验
* 核心逻辑无system:user:export权限时检查是否有admin/operator角色
*/
@SaCheckPermission(value = "system:user:export", orRole = {"admin", "operator"})
@GetMapping("/advanced/permWithOrRole")
public R<Void> permWithOrRole() {
log.info("【场景13】权限+orRole兜底校验通过");
return R.ok("拥有system:user:export权限或拥有admin/operator角色校验通过");
}
// ====================== 特殊场景:临时权限/注解覆盖 ======================
/**
* 场景14SaIgnore局部覆盖方法注解覆盖类注解若有
* 假设类上有@SaCheckLogin方法上@SaIgnore会覆盖
*/
@SaIgnore
@GetMapping("/special/ignoreOverride")
public R<Void> ignoreOverride() {
log.info("【场景14】SaIgnore覆盖类级别权限注解");
return R.ok("方法级SaIgnore覆盖类级别权限校验");
}
/**
* 场景15临时权限校验SaCheckPermission逻辑临时权限>永久权限)
* 注临时权限需通过SaToken API手动设置如 SaHolder.getStpLogic().setTempPermission("system:temp:test")
*/
@SaCheckPermission("system:temp:test")
@GetMapping("/special/tempPermission")
public R<Void> tempPermission() {
log.info("【场景15】临时权限(system:temp:test)校验通过");
return R.ok("临时权限校验通过需先通过API设置临时权限");
}
/**
* 场景16登录类型指定多端登录场景如PC/APP/小程序)
* 注需配合SaToken多账号体系配置
*/
@SaCheckLogin(type = "PC") // 仅校验PC端的登录态
@GetMapping("/special/loginTypeSpecify")
public R<Void> loginTypeSpecify() {
log.info("【场景16】指定登录类型(PC)校验通过");
return R.ok("仅PC端登录态校验通过");
}
}

View File

@@ -17,7 +17,7 @@ import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.demo.domain.TestDemo;
import org.dromara.demo.domain.bo.TestDemoBo;
import org.dromara.demo.domain.bo.TestDemoImportVo;
import org.dromara.demo.domain.vo.TestDemoImportVo;
import org.dromara.demo.domain.vo.TestDemoVo;
import org.dromara.demo.service.ITestDemoService;
import lombok.RequiredArgsConstructor;

View File

@@ -35,8 +35,8 @@ public class ExportDemoVo implements Serializable {
/**
* 用户昵称
*/
@ExcelProperty(value = "用户", index = 0)
@NotEmpty(message = "用户不能为空", groups = AddGroup.class)
@ExcelProperty(value = "用户昵称", index = 0)
@NotEmpty(message = "用户昵称不能为空", groups = AddGroup.class)
private String nickName;
/**

View File

@@ -1,10 +1,12 @@
package org.dromara.demo.domain.bo;
package org.dromara.demo.domain.vo;
import cn.idev.excel.annotation.ExcelProperty;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.dromara.demo.domain.TestDemo;
/**
* 测试单表业务对象 test_demo
@@ -13,6 +15,7 @@ import jakarta.validation.constraints.NotNull;
* @date 2021-07-26
*/
@Data
@AutoMapper(target = TestDemo.class)
public class TestDemoImportVo {
/**

View File

@@ -331,7 +331,7 @@ public class GenTableServiceImpl implements IGenTableService {
VelocityContext context = VelocityUtils.prepareContext(table);
// 获取模板列表
List<String> templates = VelocityUtils.getTemplateList(table.getTplCategory());
List<String> templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getDataName());
for (String template : templates) {
// 渲染模板
StringWriter sw = new StringWriter();
@@ -374,7 +374,7 @@ public class GenTableServiceImpl implements IGenTableService {
VelocityContext context = VelocityUtils.prepareContext(table);
// 获取模板列表
List<String> templates = VelocityUtils.getTemplateList(table.getTplCategory());
List<String> templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getDataName());
for (String template : templates) {
if (!StringUtils.containsAny(template, "sql.vm", "api.ts.vm", "types.ts.vm", "index.vue.vm", "index-tree.vue.vm")) {
// 渲染模板
@@ -478,7 +478,7 @@ public class GenTableServiceImpl implements IGenTableService {
VelocityContext context = VelocityUtils.prepareContext(table);
// 获取模板列表
List<String> templates = VelocityUtils.getTemplateList(table.getTplCategory());
List<String> templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getDataName());
for (String template : templates) {
// 渲染模板
StringWriter sw = new StringWriter();
@@ -523,6 +523,9 @@ public class GenTableServiceImpl implements IGenTableService {
* @param table 业务表信息
*/
public void setPkColumn(GenTable table) {
if (CollUtil.isEmpty(table.getColumns())) {
throw new ServiceException("表【" + table.getTableName() + "】字段为空,请检查表结构");
}
for (GenTableColumn column : table.getColumns()) {
if (column.isPk()) {
table.setPkColumn(column);

View File

@@ -109,7 +109,7 @@ public class VelocityUtils {
*
* @return 模板列表
*/
public static List<String> getTemplateList(String tplCategory) {
public static List<String> getTemplateList(String tplCategory, String dsName) {
List<String> templates = new ArrayList<>();
templates.add("vm/java/domain.java.vm");
templates.add("vm/java/vo.java.vm");
@@ -119,7 +119,7 @@ public class VelocityUtils {
templates.add("vm/java/serviceImpl.java.vm");
templates.add("vm/java/controller.java.vm");
templates.add("vm/xml/mapper.xml.vm");
DataBaseType dataBaseType = DataBaseHelper.getDataBaseType();
DataBaseType dataBaseType = DataBaseHelper.getDataBaseType(dsName);
if (dataBaseType.isOracle()) {
templates.add("vm/sql/oracle/sql.vm");
} else if (dataBaseType.isPostgreSql()) {

View File

@@ -21,6 +21,7 @@ import java.util.stream.IntStream;
*
* @author 老马
*/
@SuppressWarnings({"unchecked", "rawtypes"})
@Component
@JobExecutor(name = "testMapJobAnnotation")
public class TestMapJobAnnotation {

View File

@@ -23,6 +23,7 @@ import java.util.stream.IntStream;
*
* @author 老马
*/
@SuppressWarnings({"unchecked", "rawtypes"})
@Component
@JobExecutor(name = "testMapReduceAnnotation1")
public class TestMapReduceAnnotation1 {

View File

@@ -83,7 +83,7 @@ public class SysDictDataController extends BaseController {
}
/**
* 新增字典类型
* 新增字典数据
*/
@SaCheckPermission("system:dict:add")
@Log(title = "字典数据", businessType = BusinessType.INSERT)
@@ -98,7 +98,7 @@ public class SysDictDataController extends BaseController {
}
/**
* 修改保存字典类型
* 修改保存字典数据
*/
@SaCheckPermission("system:dict:edit")
@Log(title = "字典数据", businessType = BusinessType.UPDATE)
@@ -113,12 +113,12 @@ public class SysDictDataController extends BaseController {
}
/**
* 删除字典类型
* 删除字典数据
*
* @param dictCodes 字典code串
*/
@SaCheckPermission("system:dict:remove")
@Log(title = "字典类型", businessType = BusinessType.DELETE)
@Log(title = "字典数据", businessType = BusinessType.DELETE)
@DeleteMapping("/{dictCodes}")
public R<Void> remove(@PathVariable Long[] dictCodes) {
dictDataService.deleteDictDataByIds(Arrays.asList(dictCodes));

View File

@@ -137,6 +137,8 @@ public class SysMenuController extends BaseController {
return R.fail("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
} else if (SystemConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) {
return R.fail("新增菜单'" + menu.getMenuName() + "'失败地址必须以http(s)://开头");
} else if (!menuService.checkRouteConfigUnique(menu)) {
return R.fail("新增菜单'" + menu.getMenuName() + "'失败,路由名称或地址已存在");
}
return toAjax(menuService.insertMenu(menu));
}
@@ -156,6 +158,8 @@ public class SysMenuController extends BaseController {
return R.fail("修改菜单'" + menu.getMenuName() + "'失败地址必须以http(s)://开头");
} else if (menu.getMenuId().equals(menu.getParentId())) {
return R.fail("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己");
} else if (!menuService.checkRouteConfigUnique(menu)) {
return R.fail("修改菜单'" + menu.getMenuName() + "'失败,路由名称或地址已存在");
}
return toAjax(menuService.updateMenu(menu));
}

View File

@@ -130,11 +130,11 @@ public class SysMenu extends BaseEntity {
public String getRouterPath() {
String routerPath = this.path;
// 内链打开外网方式
if (getParentId() != 0L && isInnerLink()) {
if (!Constants.TOP_PARENT_ID.equals(getParentId()) && isInnerLink()) {
routerPath = innerLinkReplaceEach(routerPath);
}
// 非外链并且是一级目录(类型为目录)
if (0L == getParentId() && SystemConstants.TYPE_DIR.equals(getMenuType())
if (Constants.TOP_PARENT_ID.equals(getParentId()) && SystemConstants.TYPE_DIR.equals(getMenuType())
&& SystemConstants.NO_FRAME.equals(getIsFrame())) {
routerPath = "/" + this.path;
}
@@ -152,7 +152,7 @@ public class SysMenu extends BaseEntity {
String component = SystemConstants.LAYOUT;
if (StringUtils.isNotEmpty(this.component) && !isMenuFrame()) {
component = this.component;
} else if (StringUtils.isEmpty(this.component) && getParentId() != 0L && isInnerLink()) {
} else if (StringUtils.isEmpty(this.component) && !Constants.TOP_PARENT_ID.equals(getParentId()) && isInnerLink()) {
component = SystemConstants.INNER_LINK;
} else if (StringUtils.isEmpty(this.component) && isParentView()) {
component = SystemConstants.PARENT_VIEW;
@@ -164,7 +164,7 @@ public class SysMenu extends BaseEntity {
* 是否为菜单内部跳转
*/
public boolean isMenuFrame() {
return getParentId() == 0L && SystemConstants.TYPE_MENU.equals(menuType) && isFrame.equals(SystemConstants.NO_FRAME);
return Constants.TOP_PARENT_ID.equals(getParentId()) && SystemConstants.TYPE_MENU.equals(menuType) && isFrame.equals(SystemConstants.NO_FRAME);
}
/**
@@ -178,7 +178,7 @@ public class SysMenu extends BaseEntity {
* 是否为parent_view组件
*/
public boolean isParentView() {
return getParentId() != 0L && SystemConstants.TYPE_DIR.equals(menuType);
return !Constants.TOP_PARENT_ID.equals(getParentId()) && SystemConstants.TYPE_DIR.equals(menuType);
}
/**

View File

@@ -78,7 +78,7 @@ public class SysUser extends TenantEntity {
private String password;
/**
* 号状态0正常 1停用
* 号状态0正常 1停用
*/
private String status;

View File

@@ -21,7 +21,7 @@ public class SysUserOnline {
private String deptName;
/**
* 用户名称
* 用户账号
*/
private String userName;

View File

@@ -78,7 +78,7 @@ public class SysUserBo extends BaseEntity {
private String password;
/**
* 号状态0正常 1停用
* 号状态0正常 1停用
*/
private String status;

View File

@@ -104,6 +104,6 @@ public class SysDeptVo implements Serializable {
/**
* 子部门
*/
private List<SysDept> children = new ArrayList<>();
private List<SysDeptVo> children = new ArrayList<>();
}

View File

@@ -34,13 +34,13 @@ public class SysUserExportVo implements Serializable {
/**
* 用户账号
*/
@ExcelProperty(value = "登录名称")
@ExcelProperty(value = "用户账号")
private String userName;
/**
* 用户昵称
*/
@ExcelProperty(value = "用户")
@ExcelProperty(value = "用户")
private String nickName;
/**
@@ -63,9 +63,9 @@ public class SysUserExportVo implements Serializable {
private String sex;
/**
* 号状态0正常 1停用
* 号状态0正常 1停用
*/
@ExcelProperty(value = "号状态", converter = ExcelDictConvert.class)
@ExcelProperty(value = "号状态", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "sys_normal_disable")
private String status;

View File

@@ -38,13 +38,13 @@ public class SysUserImportVo implements Serializable {
/**
* 用户账号
*/
@ExcelProperty(value = "登录名称")
@ExcelProperty(value = "用户账号")
private String userName;
/**
* 用户昵称
*/
@ExcelProperty(value = "用户")
@ExcelProperty(value = "用户")
private String nickName;
/**
@@ -67,9 +67,9 @@ public class SysUserImportVo implements Serializable {
private String sex;
/**
* 号状态0正常 1停用
* 号状态0正常 1停用
*/
@ExcelProperty(value = "号状态", converter = ExcelDictConvert.class)
@ExcelProperty(value = "号状态", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "sys_normal_disable")
private String status;

View File

@@ -89,7 +89,7 @@ public class SysUserVo implements Serializable {
private String password;
/**
* 号状态0正常 1停用
* 号状态0正常 1停用
*/
private String status;

View File

@@ -160,4 +160,13 @@ public interface ISysMenuService {
* @return 结果
*/
boolean checkMenuNameUnique(SysMenuBo menu);
/**
* 校验路由组合是否唯一
*
* @param menu 菜单信息
* @return 结果
*/
boolean checkRouteConfigUnique(SysMenuBo menu);
}

View File

@@ -101,7 +101,7 @@ public interface ISysUserService {
String selectUserPostGroup(Long userId);
/**
* 校验用户名称是否唯一
* 校验用户账号是否唯一
*
* @param user 用户信息
* @return 结果
@@ -174,7 +174,7 @@ public interface ISysUserService {
* 修改用户状态
*
* @param userId 用户ID
* @param status 号状态
* @param status 号状态
* @return 结果
*/
int updateUserStatus(Long userId, String status);

View File

@@ -229,6 +229,9 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
@Override
public String getDictLabel(String dictType, String dictValue, String separator) {
List<SysDictDataVo> datas = SpringUtils.getAopProxy(this).selectDictDataByType(dictType);
if (CollUtil.isEmpty(datas)) {
return StringUtils.EMPTY;
}
Map<String, String> map = StreamUtils.toMap(datas, SysDictDataVo::getDictValue, SysDictDataVo::getDictLabel);
if (StringUtils.containsAny(dictValue, separator)) {
return Arrays.stream(dictValue.split(separator))
@@ -250,6 +253,9 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
@Override
public String getDictValue(String dictType, String dictLabel, String separator) {
List<SysDictDataVo> datas = SpringUtils.getAopProxy(this).selectDictDataByType(dictType);
if (CollUtil.isEmpty(datas)) {
return StringUtils.EMPTY;
}
Map<String, String> map = StreamUtils.toMap(datas, SysDictDataVo::getDictLabel, SysDictDataVo::getDictValue);
if (StringUtils.containsAny(dictLabel, separator)) {
return Arrays.stream(dictLabel.split(separator))
@@ -269,6 +275,9 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
@Override
public Map<String, String> getAllDictByDictType(String dictType) {
List<SysDictDataVo> list = SpringUtils.getAopProxy(this).selectDictDataByType(dictType);
if (CollUtil.isEmpty(list)) {
return new HashMap<>();
}
// 保证顺序
LinkedHashMap<String, String> map = new LinkedHashMap<>();
for (SysDictDataVo vo : list) {
@@ -286,6 +295,9 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
@Override
public DictTypeDTO getDictType(String dictType) {
SysDictTypeVo vo = SpringUtils.getAopProxy(this).selectDictTypeByType(dictType);
if (ObjectUtil.isNull(vo)) {
return null;
}
return BeanUtil.toBean(vo, DictTypeDTO.class);
}
@@ -298,6 +310,9 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
@Override
public List<DictDataDTO> getDictData(String dictType) {
List<SysDictDataVo> list = SpringUtils.getAopProxy(this).selectDictDataByType(dictType);
if (CollUtil.isEmpty(list)) {
return new ArrayList<>();
}
return BeanUtil.copyToList(list, DictDataDTO.class);
}

View File

@@ -6,6 +6,7 @@ import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.utils.MapstructUtils;
@@ -29,13 +30,17 @@ import org.dromara.system.service.ISysMenuService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* 菜单 业务层处理
*
* @author Lion Li
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class SysMenuServiceImpl implements ISysMenuService {
@@ -107,7 +112,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
/**
* 根据用户ID查询菜单
*
* @param userId 用户名称
* @param userId 用户ID
* @return 菜单列表
*/
@Override
@@ -353,6 +358,51 @@ public class SysMenuServiceImpl implements ISysMenuService {
return !exist;
}
/**
* 校验路由名称是否唯一
*
* @param menuBo 菜单信息
* @return 结果
*/
@Override
public boolean checkRouteConfigUnique(SysMenuBo menuBo) {
SysMenu menu = MapstructUtils.convert(menuBo, SysMenu.class);
if (SystemConstants.TYPE_BUTTON.equals(menu.getMenuType())) {
return true;
}
long menuId = ObjectUtil.isNull(menu.getMenuId()) ? -1L : menu.getMenuId();
Long parentId = menu.getParentId();
String path = menu.getPath();
String routeName = StringUtils.isEmpty(menu.getRouteName()) ? path : menu.getRouteName();
List<SysMenu> sysMenuList = baseMapper.selectList(
new LambdaQueryWrapper<SysMenu>()
.in(SysMenu::getMenuType, SystemConstants.TYPE_DIR, SystemConstants.TYPE_MENU)
.and(w ->
w.eq(SysMenu::getPath, path).or().eq(SysMenu::getPath, routeName)
));
for (SysMenu sysMenu : sysMenuList) {
if (!sysMenu.getMenuId().equals(menuId)) {
Long dbParentId = sysMenu.getParentId();
String dbPath = sysMenu.getPath();
String dbRouteName = StringUtils.isEmpty(sysMenu.getRouteName()) ? dbPath : sysMenu.getRouteName();
if (StringUtils.equalsAnyIgnoreCase(path, dbPath) && parentId.equals(dbParentId)) {
log.warn("[同级路由冲突] 同级下已存在相同路由路径 '{}',冲突菜单:{}", dbPath, sysMenu.getMenuName());
return false;
} else if (StringUtils.equalsAnyIgnoreCase(path, dbPath)
&& Constants.TOP_PARENT_ID.equals(parentId)
&& Constants.TOP_PARENT_ID.equals(dbParentId)) {
log.warn("[根目录路由冲突] 根目录下路由 '{}' 必须唯一,已被菜单 '{}' 占用", path, sysMenu.getMenuName());
return false;
} else if (StringUtils.equalsAnyIgnoreCase(routeName, dbRouteName)
&& sysMenu.getMenuType().equals(menu.getMenuType())) {
log.warn("[路由名称冲突] 路由名称 '{}' 需全局唯一,已被菜单 '{}' 使用", routeName, sysMenu.getMenuName());
return false;
}
}
}
return true;
}
/**
* 根据父节点的ID获取所有子节点
*

View File

@@ -232,7 +232,7 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
}
/**
* 校验用户名称是否唯一
* 校验用户账号是否唯一
*
* @param user 用户信息
* @return 结果
@@ -375,7 +375,7 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
* 修改用户状态
*
* @param userId 用户ID
* @param status 号状态
* @param status 号状态
* @return 结果
*/
@Override
@@ -497,6 +497,11 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
roleList.remove(SystemConstants.SUPER_ADMIN_ID);
}
// 移除超管角色后若无剩余角色,说明仅选了超管角色且不允许分配,显式报错
if (roleList.isEmpty()) {
throw new ServiceException("不允许为普通用户分配超级管理员角色,请至少选择一个其他角色");
}
// 校验是否有权限访问这些角色(含数据权限控制)
if (roleMapper.selectRoleCount(roleList) != roleList.size()) {
throw new ServiceException("没有权限访问角色的数据");
@@ -594,10 +599,10 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
}
/**
* 通过用户ID查询用户账户
* 通过用户ID查询用户昵称
*
* @param userId 用户ID
* @return 用户账户
* @return 用户昵称
*/
@Override
@Cacheable(cacheNames = CacheNames.SYS_NICKNAME, key = "#userId")
@@ -608,10 +613,10 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
}
/**
* 通过用户ID查询用户账户
* 通过用户ID查询用户昵称
*
* @param userIds 用户ID 多个用逗号隔开
* @return 用户账户
* @return 用户昵称
*/
@Override
public String selectNicknameByIds(String userIds) {
@@ -751,13 +756,13 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
}
/**
* 根据用户 ID 列表查询用户称映射关系
* 根据用户 ID 列表查询用户称映射关系
*
* @param userIds 用户 ID 列表
* @return Map其中 key 为用户 IDvalue 为对应的用户
* @return Map其中 key 为用户 IDvalue 为对应的用户
*/
@Override
public Map<Long, String> selectUserNamesByIds(List<Long> userIds) {
public Map<Long, String> selectUserNicksByIds(List<Long> userIds) {
if (CollUtil.isEmpty(userIds)) {
return Collections.emptyMap();
}

View File

@@ -23,26 +23,6 @@ public interface FlowConstant {
*/
String INITIATOR_DEPT_ID = "initiatorDeptId";
/**
* 委托
*/
String DELEGATE_TASK = "delegateTask";
/**
* 转办
*/
String TRANSFER_TASK = "transferTask";
/**
* 加签
*/
String ADD_SIGNATURE = "addSignature";
/**
* 减签
*/
String REDUCTION_SIGNATURE = "reductionSignature";
/**
* 流程分类Id转名称
*/

View File

@@ -0,0 +1,53 @@
package org.dromara.workflow.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 任务操作类型枚举
*
* @author may
*/
@Getter
@AllArgsConstructor
public enum TaskOperationEnum {
/**
* 委派
*/
DELEGATE_TASK("delegateTask", "委派"),
/**
* 转办
*/
TRANSFER_TASK("transferTask", "转办"),
/**
* 加签
*/
ADD_SIGNATURE("addSignature", "加签"),
/**
* 减签
*/
REDUCTION_SIGNATURE("reductionSignature", "减签");
private final String code;
private final String desc;
private static final Map<String, TaskOperationEnum> CODE_MAP = Arrays.stream(values())
.collect(Collectors.toConcurrentMap(TaskOperationEnum::getCode, Function.identity()));
/**
* 根据 code 获取枚举
*/
public static TaskOperationEnum getByCode(String code) {
return CODE_MAP.get(code);
}
}

View File

@@ -1,5 +1,6 @@
package org.dromara.workflow.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
@@ -45,6 +46,7 @@ public class FlwDefinitionController extends BaseController {
* @param pageQuery 分页
*/
@GetMapping("/list")
@SaCheckPermission("workflow:definition:list")
public TableDataInfo<FlowDefinitionVo> list(FlowDefinition flowDefinition, PageQuery pageQuery) {
return flwDefinitionService.queryList(flowDefinition, pageQuery);
}
@@ -56,6 +58,7 @@ public class FlwDefinitionController extends BaseController {
* @param pageQuery 分页
*/
@GetMapping("/unPublishList")
@SaCheckPermission("workflow:definition:list")
public TableDataInfo<FlowDefinitionVo> unPublishList(FlowDefinition flowDefinition, PageQuery pageQuery) {
return flwDefinitionService.unPublishList(flowDefinition, pageQuery);
}
@@ -66,6 +69,7 @@ public class FlwDefinitionController extends BaseController {
* @param id 流程定义id
*/
@GetMapping(value = "/{id}")
@SaCheckPermission("workflow:definition:query")
public R<Definition> getInfo(@PathVariable Long id) {
return R.ok(defService.getById(id));
}
@@ -79,6 +83,7 @@ public class FlwDefinitionController extends BaseController {
@PostMapping
@RepeatSubmit()
@Transactional(rollbackFor = Exception.class)
@SaCheckPermission("workflow:definition:add")
public R<Boolean> add(@RequestBody FlowDefinition flowDefinition) {
return R.ok(defService.checkAndSave(flowDefinition));
}
@@ -92,6 +97,7 @@ public class FlwDefinitionController extends BaseController {
@PutMapping
@RepeatSubmit()
@Transactional(rollbackFor = Exception.class)
@SaCheckPermission("workflow:definition:edit")
public R<Boolean> edit(@RequestBody FlowDefinition flowDefinition) {
return R.ok(defService.updateById(flowDefinition));
}
@@ -104,6 +110,7 @@ public class FlwDefinitionController extends BaseController {
@Log(title = "流程定义", businessType = BusinessType.INSERT)
@PutMapping("/publish/{id}")
@RepeatSubmit()
@SaCheckPermission("workflow:definition:publish")
public R<Boolean> publish(@PathVariable Long id) {
return R.ok(flwDefinitionService.publish(id));
}
@@ -117,6 +124,7 @@ public class FlwDefinitionController extends BaseController {
@PutMapping("/unPublish/{id}")
@RepeatSubmit()
@Transactional(rollbackFor = Exception.class)
@SaCheckPermission("workflow:definition:publish")
public R<Boolean> unPublish(@PathVariable Long id) {
return R.ok(defService.unPublish(id));
}
@@ -126,6 +134,7 @@ public class FlwDefinitionController extends BaseController {
*/
@Log(title = "流程定义", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
@SaCheckPermission("workflow:definition:remove")
public R<Void> remove(@PathVariable List<Long> ids) {
return toAjax(flwDefinitionService.removeDef(ids));
}
@@ -139,6 +148,7 @@ public class FlwDefinitionController extends BaseController {
@PostMapping("/copy/{id}")
@RepeatSubmit()
@Transactional(rollbackFor = Exception.class)
@SaCheckPermission("workflow:definition:copy")
public R<Boolean> copy(@PathVariable Long id) {
return R.ok(defService.copyDef(id));
}
@@ -151,6 +161,7 @@ public class FlwDefinitionController extends BaseController {
*/
@Log(title = "流程定义", businessType = BusinessType.IMPORT)
@PostMapping("/importDef")
@SaCheckPermission("workflow:definition:import")
public R<Boolean> importDef(MultipartFile file, String category) {
return R.ok(flwDefinitionService.importJson(file, category));
}
@@ -164,6 +175,7 @@ public class FlwDefinitionController extends BaseController {
*/
@Log(title = "流程定义", businessType = BusinessType.EXPORT)
@PostMapping("/exportDef/{id}")
@SaCheckPermission("workflow:definition:export")
public void exportDef(@PathVariable Long id, HttpServletResponse response) throws IOException {
flwDefinitionService.exportDef(id, response);
}
@@ -174,6 +186,7 @@ public class FlwDefinitionController extends BaseController {
* @param id 流程定义id
*/
@GetMapping("/xmlString/{id}")
@SaCheckPermission("workflow:definition:query")
public R<String> xmlString(@PathVariable Long id) {
return R.ok("操作成功", defService.exportJson(id));
}
@@ -188,6 +201,7 @@ public class FlwDefinitionController extends BaseController {
@PutMapping("/active/{id}")
@Transactional(rollbackFor = Exception.class)
@Log(title = "流程定义", businessType = BusinessType.UPDATE)
@SaCheckPermission("workflow:definition:active")
public R<Boolean> active(@PathVariable Long id, @RequestParam boolean active) {
return R.ok(active ? defService.active(id) : defService.unActive(id));
}

View File

@@ -1,5 +1,6 @@
package org.dromara.workflow.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.convert.Convert;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
@@ -46,6 +47,7 @@ public class FlwInstanceController extends BaseController {
* @param pageQuery 分页
*/
@GetMapping("/pageByRunning")
@SaCheckPermission("workflow:instance:list")
public TableDataInfo<FlowInstanceVo> selectRunningInstanceList(FlowInstanceBo flowInstanceBo, PageQuery pageQuery) {
return flwInstanceService.selectRunningInstanceList(flowInstanceBo, pageQuery);
}
@@ -57,6 +59,7 @@ public class FlwInstanceController extends BaseController {
* @param pageQuery 分页
*/
@GetMapping("/pageByFinish")
@SaCheckPermission("workflow:instance:list")
public TableDataInfo<FlowInstanceVo> selectFinishInstanceList(FlowInstanceBo flowInstanceBo, PageQuery pageQuery) {
return flwInstanceService.selectFinishInstanceList(flowInstanceBo, pageQuery);
}
@@ -67,6 +70,7 @@ public class FlwInstanceController extends BaseController {
* @param businessId 业务id
*/
@GetMapping("/getInfo/{businessId}")
@SaCheckPermission("workflow:instance:query")
public R<FlowInstanceVo> getInfo(@PathVariable Long businessId) {
return R.ok(flwInstanceService.queryByBusinessId(businessId));
}
@@ -78,6 +82,7 @@ public class FlwInstanceController extends BaseController {
*/
@DeleteMapping("/deleteByBusinessIds/{businessIds}")
@Log(title = "流程实例管理", businessType = BusinessType.DELETE)
@SaCheckPermission("workflow:instance:remove")
public R<Void> deleteByBusinessIds(@PathVariable List<Long> businessIds) {
return toAjax(flwInstanceService.deleteByBusinessIds(StreamUtils.toList(businessIds, Convert::toStr)));
}
@@ -89,6 +94,7 @@ public class FlwInstanceController extends BaseController {
*/
@DeleteMapping("/deleteByInstanceIds/{instanceIds}")
@Log(title = "流程实例管理", businessType = BusinessType.DELETE)
@SaCheckPermission("workflow:instance:remove")
public R<Void> deleteByInstanceIds(@PathVariable List<Long> instanceIds) {
return toAjax(flwInstanceService.deleteByInstanceIds(instanceIds));
}
@@ -100,6 +106,7 @@ public class FlwInstanceController extends BaseController {
*/
@DeleteMapping("/deleteHisByInstanceIds/{instanceIds}")
@Log(title = "流程实例管理", businessType = BusinessType.DELETE)
@SaCheckPermission("workflow:instance:remove")
public R<Void> deleteHisByInstanceIds(@PathVariable List<Long> instanceIds) {
return toAjax(flwInstanceService.deleteHisByInstanceIds(instanceIds));
}
@@ -112,6 +119,7 @@ public class FlwInstanceController extends BaseController {
@RepeatSubmit()
@PutMapping("/cancelProcessApply")
@Log(title = "流程实例管理", businessType = BusinessType.UPDATE)
@SaCheckPermission("workflow:instance:cancel")
public R<Void> cancelProcessApply(@RequestBody FlowCancelBo bo) {
return toAjax(flwInstanceService.cancelProcessApply(bo));
}
@@ -125,6 +133,7 @@ public class FlwInstanceController extends BaseController {
@RepeatSubmit()
@PutMapping("/active/{id}")
@Log(title = "流程实例管理", businessType = BusinessType.UPDATE)
@SaCheckPermission("workflow:instance:active")
public R<Boolean> active(@PathVariable Long id, @RequestParam boolean active) {
return R.ok(active ? insService.active(id) : insService.unActive(id));
}
@@ -136,6 +145,7 @@ public class FlwInstanceController extends BaseController {
* @param pageQuery 分页
*/
@GetMapping("/pageByCurrent")
@SaCheckPermission("workflow:instance:currentList")
public TableDataInfo<FlowInstanceVo> selectCurrentInstanceList(FlowInstanceBo flowInstanceBo, PageQuery pageQuery) {
return flwInstanceService.selectCurrentInstanceList(flowInstanceBo, pageQuery);
}
@@ -146,6 +156,7 @@ public class FlwInstanceController extends BaseController {
* @param businessId 业务id
*/
@GetMapping("/flowHisTaskList/{businessId}")
@SaCheckPermission("workflow:instance:query")
public R<Map<String, Object>> flowHisTaskList(@PathVariable String businessId) {
return R.ok(flwInstanceService.flowHisTaskList(businessId));
}
@@ -156,6 +167,7 @@ public class FlwInstanceController extends BaseController {
* @param instanceId 流程实例id
*/
@GetMapping("/instanceVariable/{instanceId}")
@SaCheckPermission("workflow:instance:variableQuery")
public R<Map<String, Object>> instanceVariable(@PathVariable Long instanceId) {
return R.ok(flwInstanceService.instanceVariable(instanceId));
}
@@ -168,6 +180,7 @@ public class FlwInstanceController extends BaseController {
@RepeatSubmit()
@PutMapping("/updateVariable")
@Log(title = "流程实例管理", businessType = BusinessType.UPDATE)
@SaCheckPermission("workflow:instance:variable")
public R<Void> updateVariable(@Validated @RequestBody FlowVariableBo bo) {
return toAjax(flwInstanceService.updateVariable(bo));
}
@@ -180,6 +193,7 @@ public class FlwInstanceController extends BaseController {
@Log(title = "流程实例管理", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping("/invalid")
@SaCheckPermission("workflow:instance:invalid")
public R<Boolean> invalid(@Validated @RequestBody FlowInvalidBo bo) {
return R.ok(flwInstanceService.processInvalid(bo));
}

View File

@@ -61,7 +61,8 @@ public class BackProcessBo implements Serializable {
public Map<String, Object> getVariables() {
if (variables == null) {
return new HashMap<>(16);
variables = new HashMap<>(16);
return variables;
}
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables;

View File

@@ -23,8 +23,8 @@ public class FlowCopyBo implements Serializable {
private Long userId;
/**
* 用户
* 用户
*/
private String userName;
private String nickName;
}

View File

@@ -30,7 +30,8 @@ public class FlowNextNodeBo implements Serializable {
public Map<String, Object> getVariables() {
if (variables == null) {
return new HashMap<>(16);
variables = new HashMap<>(16);
return variables;
}
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables;

View File

@@ -53,7 +53,8 @@ public class StartProcessBo implements Serializable {
public Map<String, Object> getVariables() {
if (variables == null) {
return new HashMap<>(16);
variables = new HashMap<>(16);
return variables;
}
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables;

View File

@@ -40,6 +40,11 @@ public class TaskOperationBo implements Serializable {
@NotNull(message = "任务id不能为空")
private Long taskId;
/**
* 消息类型
*/
private List<String> messageType;
/**
* 意见或备注信息(可选)
*/

View File

@@ -24,10 +24,10 @@ public class FlowCopyVo implements Serializable {
private Long userId;
/**
* 用户
* 用户
*/
@Translation(type = TransConstant.USER_ID_TO_NICKNAME, mapper = "userId")
private String userName;
private String nickName;
public FlowCopyVo(Long userId) {
this.userId = userId;

View File

@@ -74,17 +74,19 @@ public class WorkflowGlobalListener implements GlobalListener {
String ext = listenerVariable.getNode().getExt();
if (StringUtils.isNotBlank(ext)) {
Map<String, Object> variable = listenerVariable.getVariable();
if (CollUtil.isNotEmpty(variable)) {
if (CollUtil.isEmpty(variable)) {
variable = new HashMap<>();
}
NodeExtVo nodeExt = nodeExtService.parseNodeExt(ext, variable);
Set<String> copyList = nodeExt.getCopySettings();
if (CollUtil.isNotEmpty(copyList)) {
List<Long> userIds = StreamUtils.toList(copyList, Convert::toLong);
Map<Long, String> nickNameMap = userService.selectUserNicksByIds(userIds);
List<FlowCopyBo> list = StreamUtils.toList(copyList, x -> {
FlowCopyBo bo = new FlowCopyBo();
Long id = Convert.toLong(x);
bo.setUserId(id);
bo.setUserName(userService.selectUserNameById(id));
bo.setNickName(nickNameMap.getOrDefault(id, StringUtils.EMPTY));
return bo;
});
variable.put(FlowConstant.FLOW_COPY_LIST, list);
@@ -159,7 +161,7 @@ public class WorkflowGlobalListener implements GlobalListener {
flowTask.setPermissionList(List.of(userIdArray));
// 移除已处理的状态变量
variable.remove(nodeKey);
FlowEngine.insService().removeVariables(flowTask.getInstanceId(),nodeKey);
FlowEngine.insService().removeVariables(flowTask.getInstanceId(), nodeKey);
}
}

View File

@@ -5,6 +5,7 @@ import cn.hutool.core.util.ObjectUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.dto.UserDTO;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
@@ -98,7 +99,14 @@ public class FlwCommonServiceImpl implements IFlwCommonService {
}
case EMAIL_MESSAGE -> MailUtils.sendText(emails, subject, message);
case SMS_MESSAGE -> {
// TODO: 补充短信发送逻辑
// LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
// // 根据具体短信服务商参数用法传参
// map.put("code", "1234");
// // 自动获取一个短信服务商
// SmsBlend smsBlend = SmsFactory.getSmsBlend();
// // 指定获取一个短信服务商 configKey
// SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
// SmsResponse smsResponse = smsBlend.sendMessage(phones, templateId, map);
log.info("【短信发送 - TODO】用户数量={} 内容={}", userList.size(), message);
}
default -> log.warn("【消息发送】未处理的消息类型:{}", messageTypeEnum);
@@ -119,6 +127,9 @@ public class FlwCommonServiceImpl implements IFlwCommonService {
@Override
public String applyNodeCode(Long definitionId) {
List<Node> firstBetweenNode = FlowEngine.nodeService().getFirstBetweenNode(definitionId, new HashMap<>());
if (CollUtil.isEmpty(firstBetweenNode)) {
throw new ServiceException("流程定义缺少申请人节点,请检查流程定义配置");
}
return firstBetweenNode.get(0).getNodeCode();
}
}

View File

@@ -111,8 +111,14 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
@Override
public FlowInstanceVo queryByBusinessId(Long businessId) {
FlowInstance instance = this.selectInstByBusinessId(Convert.toStr(businessId));
if (ObjectUtil.isNull(instance)) {
throw new ServiceException(ExceptionCons.NOT_FOUNT_INSTANCE);
}
FlowInstanceVo instanceVo = BeanUtil.toBean(instance, FlowInstanceVo.class);
Definition definition = defService.getById(instanceVo.getDefinitionId());
if (ObjectUtil.isNull(definition)) {
throw new ServiceException(ExceptionCons.NOT_FOUNT_DEF);
}
instanceVo.setFlowName(definition.getFlowName());
instanceVo.setFlowCode(definition.getFlowCode());
instanceVo.setVersion(definition.getVersion());
@@ -187,6 +193,8 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
log.warn("未找到对应的流程实例信息,无法执行删除操作。");
return false;
}
// 发送事件
processDeleteHandler(flowInstances);
return insService.remove(StreamUtils.toList(flowInstances, FlowInstance::getId));
}
@@ -199,27 +207,13 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
@Transactional(rollbackFor = Exception.class)
public boolean deleteByInstanceIds(List<Long> instanceIds) {
// 获取实例信息
List<Instance> instances = insService.getByIds(instanceIds);
if (CollUtil.isEmpty(instances)) {
List<FlowInstance> flowInstances = flowInstanceMapper.selectByIds(instanceIds);
if (CollUtil.isEmpty(flowInstances)) {
log.warn("未找到对应的流程实例信息,无法执行删除操作。");
return false;
}
// 获取定义信息
Map<Long, Definition> definitionMap = StreamUtils.toMap(
defService.getByIds(StreamUtils.toList(instances, Instance::getDefinitionId)),
Definition::getId,
Function.identity()
);
// 逐一触发删除事件
instances.forEach(instance -> {
Definition definition = definitionMap.get(instance.getDefinitionId());
if (ObjectUtil.isNull(definition)) {
log.warn("实例 ID: {} 对应的流程定义信息未找到,跳过删除事件触发。", instance.getId());
return;
}
flowProcessEventHandler.processDeleteHandler(definition.getFlowCode(), instance.getBusinessId());
});
// 发送事件
processDeleteHandler(flowInstances);
// 删除实例
return insService.remove(instanceIds);
}
@@ -233,26 +227,13 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
@Transactional(rollbackFor = Exception.class)
public boolean deleteHisByInstanceIds(List<Long> instanceIds) {
// 获取实例信息
List<Instance> instances = insService.getByIds(instanceIds);
if (CollUtil.isEmpty(instances)) {
List<FlowInstance> flowInstances = flowInstanceMapper.selectByIds(instanceIds);
if (CollUtil.isEmpty(flowInstances)) {
log.warn("未找到对应的流程实例信息,无法执行删除操作。");
return false;
}
// 获取定义信息
Map<Long, Definition> definitionMap = StreamUtils.toMap(
defService.getByIds(StreamUtils.toList(instances, Instance::getDefinitionId)),
Definition::getId,
Function.identity()
);
// 逐一触发删除事件
instances.forEach(instance -> {
Definition definition = definitionMap.get(instance.getDefinitionId());
if (ObjectUtil.isNull(definition)) {
log.warn("实例 ID: {} 对应的流程定义信息未找到,跳过删除事件触发。", instance.getId());
return;
}
flowProcessEventHandler.processDeleteHandler(definition.getFlowCode(), instance.getBusinessId());
});
// 发送事件
processDeleteHandler(flowInstances);
List<FlowTask> flowTaskList = flwTaskService.selectByInstIds(instanceIds);
if (CollUtil.isNotEmpty(flowTaskList)) {
FlowEngine.userService().deleteByTaskIds(StreamUtils.toList(flowTaskList, FlowTask::getId));
@@ -263,6 +244,35 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
return true;
}
private void processDeleteHandler(List<FlowInstance> flowInstances) {
String userId = LoginHelper.getUserIdStr();
for (FlowInstance flowInstance : flowInstances) {
//如果创建人与当前登陆人一致或者当前登陆人为管理员才能删除
if (LoginHelper.isSuperAdmin() || flowInstance.getCreateBy().equals(userId)) {
continue;
}
throw new ServiceException("权限不足,无法删除流程实例信息!");
}
// 获取定义信息
Map<Long, Definition> definitionMap = StreamUtils.toMap(
defService.getByIds(StreamUtils.toList(flowInstances, Instance::getDefinitionId)),
Definition::getId,
Function.identity()
);
// 逐一触发删除事件
flowInstances.forEach(instance -> {
Definition definition = definitionMap.get(instance.getDefinitionId());
if (ObjectUtil.isNull(definition)) {
log.warn("实例 ID: {} 对应的流程定义信息未找到,跳过删除事件触发。", instance.getId());
return;
}
flowProcessEventHandler.processDeleteHandler(definition.getFlowCode(), instance.getBusinessId());
});
}
/**
* 撤销流程
*
@@ -279,8 +289,11 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
if (definition == null) {
throw new ServiceException(ExceptionCons.NOT_FOUNT_DEF);
}
String message = bo.getMessage();
String userIdStr = LoginHelper.getUserIdStr();
if (!LoginHelper.isSuperAdmin() && !instance.getCreateBy().equals(userIdStr)) {
throw new ServiceException("权限不足,无法撤销流程!");
}
String message = bo.getMessage();
BusinessStatusEnum.checkCancelStatus(instance.getFlowStatus());
FlowParams flowParams = FlowParams.build()
.message(message)
@@ -383,6 +396,9 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
@Override
public Map<String, Object> instanceVariable(Long instanceId) {
FlowInstance flowInstance = flowInstanceMapper.selectById(instanceId);
if (ObjectUtil.isNull(flowInstance)) {
throw new ServiceException(ExceptionCons.NOT_FOUNT_INSTANCE);
}
Map<String, Object> variableMap = Optional.ofNullable(flowInstance.getVariableMap()).orElse(Collections.emptyMap());
List<Map<String, Object>> variableList = variableMap.entrySet().stream()
.map(entry -> Map.of("key", entry.getKey(), "value", entry.getValue()))

View File

@@ -102,7 +102,7 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService
* @param sources 数据来源(枚举类或字典类型)
* @return 构建的 `NodeExt` 对象
*/
@SuppressWarnings("unchecked cast")
@SuppressWarnings("unchecked")
private NodeExt buildNodeExt(String code, String name, int type, List<Object> sources) {
NodeExt nodeExt = new NodeExt();
nodeExt.setCode(code);

View File

@@ -1,6 +1,7 @@
package org.dromara.workflow.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -9,6 +10,7 @@ import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.dto.TaskAssigneeDTO;
import org.dromara.common.core.domain.model.TaskAssigneeBody;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
@@ -125,7 +127,14 @@ public class FlwSpelServiceImpl implements IFlwSpelService {
* 保存前的数据校验
*/
private void validEntityBeforeSave(FlowSpel entity){
//TODO 做一些数据校验,如唯一约束
if (StringUtils.isNotBlank(entity.getViewSpel())) {
boolean exists = baseMapper.exists(new LambdaQueryWrapper<FlowSpel>()
.eq(FlowSpel::getViewSpel, entity.getViewSpel())
.ne(ObjectUtil.isNotNull(entity.getId()), FlowSpel::getId, entity.getId()));
if (exists) {
throw new ServiceException("SpEL表达式已存在请勿重复添加");
}
}
}
/**
@@ -137,7 +146,7 @@ public class FlwSpelServiceImpl implements IFlwSpelService {
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if(isValid){
if (isValid){
//TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteByIds(ids) > 0;

View File

@@ -244,7 +244,7 @@ public class FlwTaskAssigneeServiceImpl implements IFlwTaskAssigneeService, Hand
List<Long> longIds = StreamUtils.toList(ids, Convert::toLong);
Map<Long, String> rawMap = switch (type) {
case USER -> userService.selectUserNamesByIds(longIds);
case USER -> userService.selectUserNicksByIds(longIds);
case ROLE -> roleService.selectRoleNamesByIds(longIds);
case DEPT -> deptService.selectDeptNamesByIds(longIds);
case POST -> postService.selectPostNamesByIds(longIds);

View File

@@ -44,8 +44,8 @@ import org.dromara.warm.flow.orm.mapper.FlowInstanceMapper;
import org.dromara.warm.flow.orm.mapper.FlowNodeMapper;
import org.dromara.warm.flow.orm.mapper.FlowTaskMapper;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.constant.FlowConstant;
import org.dromara.workflow.common.enums.TaskAssigneeType;
import org.dromara.workflow.common.enums.TaskOperationEnum;
import org.dromara.workflow.common.enums.TaskStatusEnum;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import org.dromara.workflow.domain.bo.*;
@@ -127,6 +127,9 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
// 已存在流程
BusinessStatusEnum.checkStartStatus(flowInstance.getFlowStatus());
List<Task> taskList = taskService.list(new FlowTask().setInstanceId(flowInstance.getId()));
if (CollUtil.isEmpty(taskList)) {
throw new ServiceException("流程实例缺少任务,请检查流程定义配置");
}
taskService.mergeVariable(flowInstance, variables);
insService.updateById(flowInstance);
StartProcessReturnDTO dto = new StartProcessReturnDTO();
@@ -143,9 +146,9 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
throw new ServiceException("流程【" + startProcessBo.getFlowCode() + "】未发布,请先在流程设计器中发布流程定义");
}
Dict dict = JsonUtils.parseMap(definition.getExt());
boolean autoPass = !ObjectUtil.isNull(dict) && dict.getBool(FlowConstant.AUTO_PASS);
variables.put(FlowConstant.AUTO_PASS, autoPass);
variables.put(FlowConstant.BUSINESS_CODE, this.generateBusinessCode(bizExt));
boolean autoPass = !ObjectUtil.isNull(dict) && dict.getBool(AUTO_PASS);
variables.put(AUTO_PASS, autoPass);
variables.put(BUSINESS_CODE, this.generateBusinessCode(bizExt));
FlowParams flowParams = FlowParams.build()
.handler(startProcessBo.getHandler())
.flowCode(startProcessBo.getFlowCode())
@@ -156,6 +159,9 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
this.buildFlowInstanceBizExt(instance, bizExt);
// 申请人执行流程
List<Task> taskList = taskService.list(new FlowTask().setInstanceId(instance.getId()));
if (CollUtil.isEmpty(taskList)) {
throw new ServiceException("流程启动失败,未生成任务");
}
if (taskList.size() > 1) {
throw new ServiceException("请检查流程第一个环节是否为申请人!");
}
@@ -207,11 +213,11 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
List<FlowCopyBo> flowCopyList = completeTaskBo.getFlowCopyList();
// 设置抄送人
Map<String, Object> variables = completeTaskBo.getVariables();
variables.put(FlowConstant.FLOW_COPY_LIST, flowCopyList);
variables.put(FLOW_COPY_LIST, flowCopyList);
// 消息类型
variables.put(FlowConstant.MESSAGE_TYPE, messageType);
variables.put(MESSAGE_TYPE, messageType);
// 消息通知
variables.put(FlowConstant.MESSAGE_NOTICE, notice);
variables.put(MESSAGE_NOTICE, notice);
FlowTask flowTask = flowTaskMapper.selectById(taskId);
@@ -219,12 +225,16 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
throw new ServiceException("流程任务不存在或任务已审批!");
}
Instance ins = insService.getById(flowTask.getInstanceId());
if (ObjectUtil.isNull(ins)) {
throw new ServiceException("流程实例不存在");
}
// 检查流程状态是否为草稿、已撤销或已退回状态,若是则执行流程提交监听
if (BusinessStatusEnum.isDraftOrCancelOrBack(ins.getFlowStatus())) {
variables.put(FlowConstant.SUBMIT, true);
variables.put(SUBMIT, true);
}
Map<String, Object> insVariableMap = ins.getVariableMap();
// 设置弹窗处理人
Map<String, Object> assigneeMap = setPopAssigneeMap(completeTaskBo.getAssigneeMap(), ins.getVariableMap());
Map<String, Object> assigneeMap = setPopAssigneeMap(completeTaskBo.getAssigneeMap(), insVariableMap);
if (CollUtil.isNotEmpty(assigneeMap)) {
variables.putAll(assigneeMap);
}
@@ -240,7 +250,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
.flowStatus(BusinessStatusEnum.WAITING.getStatus())
.hisStatus(TaskStatusEnum.PASS.getStatus())
.hisTaskExt(completeTaskBo.getFileId());
Boolean autoPass = Convert.toBool(variables.getOrDefault(AUTO_PASS, false));
Boolean autoPass = Convert.toBool(insVariableMap.getOrDefault(AUTO_PASS, false));
skipTask(taskId, flowParams, flowTask.getInstanceId(), autoPass);
return true;
}
@@ -274,9 +284,9 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
flowParams.
message("流程引擎自动审批!").
variable(Map.of(
FlowConstant.SUBMIT, false,
FlowConstant.FLOW_COPY_LIST, Collections.emptyList(),
FlowConstant.MESSAGE_NOTICE, StringUtils.EMPTY));
SUBMIT, false,
FLOW_COPY_LIST, Collections.emptyList(),
MESSAGE_NOTICE, StringUtils.EMPTY));
skipTask(task.getId(), flowParams, instanceId, true);
}
}
@@ -341,7 +351,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
FlowParams flowParams = FlowParams.build()
.skipType(SkipType.NONE.getKey())
.hisStatus(TaskStatusEnum.COPY.getStatus())
.message("【抄送给】" + StreamUtils.join(flowCopyList, FlowCopyBo::getUserName));
.message("【抄送给】" + StreamUtils.join(flowCopyList, FlowCopyBo::getNickName));
HisTask hisTask = hisTaskService.setSkipHisTask(task, flowNode, flowParams);
hisTask.setCreateTime(updateTime);
hisTask.setUpdateTime(updateTime);
@@ -482,15 +492,18 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
throw new ServiceException("任务不存在!");
}
Instance inst = insService.getById(task.getInstanceId());
if (ObjectUtil.isNull(inst)) {
throw new ServiceException("流程实例不存在");
}
BusinessStatusEnum.checkBackStatus(inst.getFlowStatus());
Long definitionId = task.getDefinitionId();
String applyNodeCode = flwCommonService.applyNodeCode(definitionId);
Map<String, Object> variable = new HashMap<>();
// 消息类型
variable.put(FlowConstant.MESSAGE_TYPE, messageType);
variable.put(MESSAGE_TYPE, messageType);
// 消息通知
variable.put(FlowConstant.MESSAGE_NOTICE, notice);
variable.put(MESSAGE_NOTICE, notice);
FlowParams flowParams = FlowParams.build()
.nodeCode(bo.getNodeCode())
@@ -513,6 +526,9 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
@Override
public List<Node> getBackTaskNode(Long taskId, String nowNodeCode) {
FlowTask task = flowTaskMapper.selectById(taskId);
if (ObjectUtil.isNull(task)) {
throw new ServiceException("任务不存在!");
}
List<Node> nodeCodes = nodeService.getByNodeCodes(Collections.singletonList(nowNodeCode), task.getDefinitionId());
if (!CollUtil.isNotEmpty(nodeCodes)) {
return nodeCodes;
@@ -597,7 +613,13 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
}
FlowTaskVo flowTaskVo = BeanUtil.toBean(task, FlowTaskVo.class);
Instance instance = insService.getById(task.getInstanceId());
if (ObjectUtil.isNull(instance)) {
throw new ServiceException("流程实例不存在");
}
Definition definition = defService.getById(task.getDefinitionId());
if (ObjectUtil.isNull(definition)) {
throw new ServiceException("流程定义不存在");
}
flowTaskVo.setFlowStatus(instance.getFlowStatus());
flowTaskVo.setVersion(definition.getVersion());
flowTaskVo.setFlowCode(definition.getFlowCode());
@@ -640,11 +662,23 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
Long taskId = bo.getTaskId();
Map<String, Object> variables = bo.getVariables();
Task task = taskService.getById(taskId);
if (ObjectUtil.isNull(task)) {
throw new ServiceException("任务不存在!");
}
Instance instance = insService.getById(task.getInstanceId());
if (ObjectUtil.isNull(instance)) {
throw new ServiceException("流程实例不存在");
}
Definition definition = defService.getById(task.getDefinitionId());
if (ObjectUtil.isNull(definition)) {
throw new ServiceException("流程定义不存在");
}
Map<String, Object> mergeVariable = MapUtil.mergeAll(instance.getVariableMap(), variables);
// 获取下一节点列表
List<Node> nextNodeList = nodeService.getNextNodeList(task.getDefinitionId(), task.getNodeCode(), null, SkipType.PASS.getKey(), mergeVariable);
if (CollUtil.isEmpty(nextNodeList)) {
return new ArrayList<>();
}
List<FlowNode> nextFlowNodes = BeanUtil.copyToList(nextNodeList, FlowNode.class);
// 只获取中间节点
nextFlowNodes = StreamUtils.filter(nextFlowNodes, node -> NodeType.BETWEEN.getKey().equals(node.getNodeType()));
@@ -719,13 +753,19 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean taskOperation(TaskOperationBo bo, String taskOperation) {
TaskOperationEnum op = TaskOperationEnum.getByCode(taskOperation);
if (op == null) {
log.error("Invalid operation type:{} ", taskOperation);
throw new ServiceException("Invalid operation type " + taskOperation);
}
FlowParams flowParams = FlowParams.build().message(bo.getMessage());
if (LoginHelper.isSuperAdmin() || LoginHelper.isTenantAdmin()) {
flowParams.ignore(true);
}
// 根据操作类型构建 FlowParams
switch (taskOperation) {
switch (op) {
case DELEGATE_TASK, TRANSFER_TASK -> {
ValidatorUtils.validate(bo, AddGroup.class);
flowParams.addHandlers(Collections.singletonList(bo.getUserId()));
@@ -738,47 +778,63 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
ValidatorUtils.validate(bo, EditGroup.class);
flowParams.reductionHandlers(bo.getUserIds());
}
default -> {
log.error("Invalid operation type:{} ", taskOperation);
throw new ServiceException("Invalid operation type " + taskOperation);
}
}
Long taskId = bo.getTaskId();
Task task = taskService.getById(taskId);
if (ObjectUtil.isNull(task)) {
throw new ServiceException("任务不存在!");
}
FlowNode flowNode = getByNodeCode(task.getNodeCode(), task.getDefinitionId());
if (ADD_SIGNATURE.equals(taskOperation) || REDUCTION_SIGNATURE.equals(taskOperation)) {
if (ObjectUtil.isNull(flowNode)) {
throw new ServiceException("流程节点不存在");
}
if (op == TaskOperationEnum.ADD_SIGNATURE || op == TaskOperationEnum.REDUCTION_SIGNATURE) {
if (CooperateType.isOrSign(flowNode.getNodeRatio())) {
throw new ServiceException(task.getNodeName() + "不是会签或票签节点!");
}
}
// 设置任务状态并执行对应的任务操作
switch (taskOperation) {
//委派任务
boolean result = false;
switch (op) {
case DELEGATE_TASK -> {
flowParams.hisStatus(TaskStatusEnum.DEPUTE.getStatus());
return taskService.depute(taskId, flowParams);
result = taskService.depute(taskId, flowParams);
}
//转办任务
case TRANSFER_TASK -> {
flowParams.hisStatus(TaskStatusEnum.TRANSFER.getStatus());
return taskService.transfer(taskId, flowParams);
result = taskService.transfer(taskId, flowParams);
}
//加签,增加办理人
case ADD_SIGNATURE -> {
flowParams.hisStatus(TaskStatusEnum.SIGN.getStatus());
return taskService.addSignature(taskId, flowParams);
result = taskService.addSignature(taskId, flowParams);
}
//减签,减少办理人
case REDUCTION_SIGNATURE -> {
flowParams.hisStatus(TaskStatusEnum.SIGN_OFF.getStatus());
return taskService.reductionSignature(taskId, flowParams);
}
default -> {
log.error("Invalid operation type:{} ", taskOperation);
throw new ServiceException("Invalid operation type " + taskOperation);
result = taskService.reductionSignature(taskId, flowParams);
}
}
// 操作执行成功后再发送消息
if (result && CollUtil.isNotEmpty(bo.getMessageType())) {
List<Long> userIdList = new ArrayList<>();
if (StrUtil.isNotBlank(bo.getUserId())) {
userIdList.add(Convert.toLong(bo.getUserId()));
}
if (CollUtil.isNotEmpty(bo.getUserIds())) {
userIdList.addAll(StreamUtils.toList(bo.getUserIds(), Convert::toLong));
}
if (CollUtil.isNotEmpty(userIdList)) {
flwCommonService.sendMessage(
bo.getMessageType(),
StringUtils.isNotBlank(bo.getMessage()) ? bo.getMessage() : "单据「" + op.getDesc() + "」通知",
"单据「" + op.getDesc() + "」提醒",
userService.selectListByIds(userIdList)
);
}
}
return result;
}
/**

View File

@@ -88,6 +88,7 @@
and b.del_flag = '0'
and c.del_flag = '0'
and a.node_type in ('1','3','4')
and a.flow_status <![CDATA[ <> ]]> 'copy'
) t
${ew.getCustomSqlSegment}
</select>

View File

@@ -65,8 +65,8 @@ services:
network_mode: "host"
minio:
# minio 最后一个未阉割版本 不能再进行升级 在往上的版本功能被阉割
image: minio/minio:RELEASE.2025-04-22T22-12-26Z
# pgsty 开源社区 fork 重新维护的最新版 minio
image: pgsty/minio:RELEASE.2026-02-14T12-00-00Z
container_name: minio
ports:
# api 端口
@@ -99,7 +99,7 @@ services:
network_mode: "host"
ruoyi-server1:
image: ruoyi/ruoyi-server:5.5.2
image: ruoyi/ruoyi-server:5.6.1
container_name: ruoyi-server1
environment:
# 时区上海
@@ -115,7 +115,7 @@ services:
network_mode: "host"
ruoyi-server2:
image: ruoyi/ruoyi-server:5.5.2
image: ruoyi/ruoyi-server:5.6.1
container_name: ruoyi-server2
environment:
# 时区上海
@@ -131,7 +131,7 @@ services:
network_mode: "host"
ruoyi-monitor-admin:
image: ruoyi/ruoyi-monitor-admin:5.5.2
image: ruoyi/ruoyi-monitor-admin:5.6.1
container_name: ruoyi-monitor-admin
environment:
# 时区上海
@@ -143,7 +143,7 @@ services:
network_mode: "host"
ruoyi-snailjob-server:
image: ruoyi/ruoyi-snailjob-server:5.5.2
image: ruoyi/ruoyi-snailjob-server:5.6.1
container_name: ruoyi-snailjob-server
environment:
# 时区上海

View File

@@ -484,6 +484,7 @@ CREATE TABLE sj_job
(
id number GENERATED ALWAYS AS IDENTITY,
namespace_id varchar2(64) DEFAULT '764d604ec6fc45f68cd92514c40e9e1a' NULL,
biz_id varchar2(64) NOT NULL,
group_name varchar2(64) NULL,
job_name varchar2(64) NULL,
args_str clob DEFAULT NULL NULL,
@@ -519,9 +520,11 @@ ALTER TABLE sj_job
CREATE INDEX idx_sj_job_01 ON sj_job (namespace_id, group_name);
CREATE INDEX idx_sj_job_02 ON sj_job (job_status, bucket_index);
CREATE INDEX idx_sj_job_03 ON sj_job (create_dt);
CREATE UNIQUE INDEX uk_sj_job_01 ON sj_job (namespace_id, biz_id);
COMMENT ON COLUMN sj_job.id IS '主键';
COMMENT ON COLUMN sj_job.namespace_id IS '命名空间id';
COMMENT ON COLUMN sj_job.biz_id IS '业务ID';
COMMENT ON COLUMN sj_job.group_name IS '组名称';
COMMENT ON COLUMN sj_job.job_name IS '名称';
COMMENT ON COLUMN sj_job.args_str IS '执行方法参数';
@@ -551,7 +554,7 @@ COMMENT ON COLUMN sj_job.create_dt IS '创建时间';
COMMENT ON COLUMN sj_job.update_dt IS '修改时间';
COMMENT ON TABLE sj_job IS '任务信息';
INSERT INTO sj_job(namespace_id, group_name, job_name, args_str, args_type, next_trigger_at, job_status, task_type, route_key, executor_type, executor_info, trigger_type, trigger_interval, block_strategy,executor_timeout, max_retry_times, parallel_num, retry_interval, bucket_index, resident, notify_ids, owner_id, labels, description, ext_attrs, deleted, create_dt, update_dt) VALUES ('dev', 'ruoyi_group', 'demo-job', NULL, 1, 1710344035622, 1, 1, 4, 1, 'testJobExecutor', 2, '60', 1, 60, 3, 1, 1, 116, 0, '', 1, '','', '', 0, sysdate, sysdate);
INSERT INTO sj_job(namespace_id, biz_id, group_name, job_name, args_str, args_type, next_trigger_at, job_status, task_type, route_key, executor_type, executor_info, trigger_type, trigger_interval, block_strategy,executor_timeout, max_retry_times, parallel_num, retry_interval, bucket_index, resident, notify_ids, owner_id, labels, description, ext_attrs, deleted, create_dt, update_dt) VALUES ('dev', 'demo-job', 'ruoyi_group', 'demo-job', NULL, 1, 1710344035622, 1, 1, 4, 1, 'testJobExecutor', 2, '60', 1, 60, 3, 1, 1, 116, 0, '', 1, '','', '', 0, sysdate, sysdate);
-- sj_job_log_message
CREATE TABLE sj_job_log_message
@@ -738,7 +741,7 @@ CREATE TABLE sj_retry_summary
id number GENERATED ALWAYS AS IDENTITY,
namespace_id varchar2(64) DEFAULT '764d604ec6fc45f68cd92514c40e9e1a' NULL,
group_name varchar2(64) DEFAULT '' NULL,
scene_name varchar2(64) DEFAULT '' NULL,
scene_name varchar2(50) DEFAULT '' NULL,
trigger_at date DEFAULT CURRENT_TIMESTAMP NOT NULL,
running_num number DEFAULT 0 NOT NULL,
finish_num number DEFAULT 0 NOT NULL,
@@ -774,6 +777,7 @@ CREATE TABLE sj_workflow
id number GENERATED ALWAYS AS IDENTITY,
workflow_name varchar2(64) NULL,
namespace_id varchar2(64) DEFAULT '764d604ec6fc45f68cd92514c40e9e1a' NULL,
biz_id varchar2(64) NOT NULL,
group_name varchar2(64) NULL,
workflow_status smallint DEFAULT 1 NOT NULL,
trigger_type smallint NOT NULL,
@@ -799,10 +803,12 @@ ALTER TABLE sj_workflow
CREATE INDEX idx_sj_workflow_01 ON sj_workflow (create_dt);
CREATE INDEX idx_sj_workflow_02 ON sj_workflow (namespace_id, group_name);
CREATE UNIQUE INDEX uk_sj_workflow_01 ON sj_workflow (namespace_id, biz_id);
COMMENT ON COLUMN sj_workflow.id IS '主键';
COMMENT ON COLUMN sj_workflow.workflow_name IS '工作流名称';
COMMENT ON COLUMN sj_workflow.namespace_id IS '命名空间id';
COMMENT ON COLUMN sj_workflow.biz_id IS '业务ID';
COMMENT ON COLUMN sj_workflow.group_name IS '组名称';
COMMENT ON COLUMN sj_workflow.workflow_status IS '工作流状态 0、关闭、1、开启';
COMMENT ON COLUMN sj_workflow.trigger_type IS '触发类型 1.CRON 表达式 2. 固定时间';

View File

@@ -257,7 +257,7 @@ comment on column sys_user.phonenumber is '手机号码';
comment on column sys_user.sex is '用户性别0男 1女 2未知';
comment on column sys_user.avatar is '头像路径';
comment on column sys_user.password is '密码';
comment on column sys_user.status is '号状态0正常 1停用';
comment on column sys_user.status is '号状态0正常 1停用';
comment on column sys_user.del_flag is '删除标志0代表存在 1代表删除';
comment on column sys_user.login_ip is '最后登录IP';
comment on column sys_user.login_date is '最后登录时间';

View File

@@ -473,11 +473,11 @@ INSERT INTO sys_menu VALUES ('11618', '我的任务', '0', '7', 'task', '', '',
INSERT INTO sys_menu VALUES ('11619', '我的待办', '11618', '2', 'taskWaiting', 'workflow/task/taskWaiting', '', '1', '1', 'C', '0', '0', '', 'waiting', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11632', '我的已办', '11618', '3', 'taskFinish', 'workflow/task/taskFinish', '', '1', '1', 'C', '0', '0', '', 'finish', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11633', '我的抄送', '11618', '4', 'taskCopyList', 'workflow/task/taskCopyList', '', '1', '1', 'C', '0', '0', '', 'my-copy', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11620', '流程定义', '11616', '3', 'processDefinition', 'workflow/processDefinition/index', '', '1', '1', 'C', '0', '0', '', 'process-definition', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11621', '流程实例', '11630', '1', 'processInstance', 'workflow/processInstance/index', '', '1', '1', 'C', '0', '0', '', 'tree-table', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11620', '流程定义', '11616', '3', 'processDefinition', 'workflow/processDefinition/index', '', '1', '1', 'C', '0', '0', 'workflow:definition:list', 'process-definition', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11621', '流程实例', '11630', '1', 'processInstance', 'workflow/processInstance/index', '', '1', '1', 'C', '0', '0', 'workflow:instance:list', 'tree-table', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11622', '流程分类', '11616', '1', 'category', 'workflow/category/index', '', '1', '0', 'C', '0', '0', 'workflow:category:list', 'category', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11629', '我发起的', '11618', '1', 'myDocument', 'workflow/task/myDocument', '', '1', '1', 'C', '0', '0', '', 'guide', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11630', '流程监控', '11616', '4', 'monitor', '', '', '1', '0', 'M', '0', '0', '', 'monitor', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11629', '我发起的', '11618', '1', 'myDocument', 'workflow/task/myDocument', '', '1', '1', 'C', '0', '0', 'workflow:instance:currentList', 'guide', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11630', '流程监控', '11616', '4', 'processMonitor', '', '', '1', '0', 'M', '0', '0', '', 'monitor', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11631', '待办任务', '11630', '2', 'allTaskWaiting', 'workflow/task/allTaskWaiting', '', '1', '1', 'C', '0', '0', '', 'waiting', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11700', '流程设计', '11616', '5', 'design/index', 'workflow/processDefinition/design', '', '1', '1', 'C', '1', '0', 'workflow:leave:edit', '#', 103, 1, SYSDATE, NULL, NULL, '/workflow/processDefinition');
INSERT INTO sys_menu VALUES ('11701', '请假申请', '11616', '6', 'leaveEdit/index', 'workflow/leave/leaveEdit', '', '1', '1', 'C', '1', '0', 'workflow:leave:edit', '#', 103, 1, SYSDATE, NULL, NULL, '');
@@ -488,6 +488,26 @@ INSERT INTO sys_menu VALUES ('11625', '流程分类修改', '11622', '3', '#', '
INSERT INTO sys_menu VALUES ('11626', '流程分类删除', '11622', '4', '#', '', '', '1', '0', 'F', '0', '0', 'workflow:category:remove', '#', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11627', '流程分类导出', '11622', '5', '#', '', '', '1', '0', 'F', '0', '0', 'workflow:category:export', '#', 103, 1, SYSDATE, NULL, NULL, '');
-- 流程实例管理相关按钮
INSERT INTO sys_menu VALUES ('11653', '流程实例查询', '11621', '1', '#', '', '', '1', '0', 'F', '0', '0', 'workflow:instance:query', '#', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11654', '流程变量查询', '11621', '2', '#', '', '', '1', '0', 'F', '0', '0', 'workflow:instance:variableQuery', '#', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11655', '流程变量修改', '11621', '3', '#', '', '', '1', '0', 'F', '0', '0', 'workflow:instance:variable', '#', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11656', '流程实例激活/挂起', '11621', '4', '#', '', '', '1', '0', 'F', '0', '0', 'workflow:instance:active', '#', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11657', '流程实例删除', '11621', '5', '#', '', '', '1', '0', 'F', '0', '0', 'workflow:instance:remove', '#', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11658', '流程实例作废', '11621', '6', '#', '', '', '1', '0', 'F', '0', '0', 'workflow:instance:invalid', '#', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11659', '流程实例撤销', '11621', '7', '#', '', '', '1', '0', 'F', '0', '0', 'workflow:instance:cancel', '#', 103, 1, SYSDATE, NULL, NULL, '');
-- 流程定义管理相关按钮
INSERT INTO sys_menu VALUES ('11644', '流程定义查询', '11620', '1', '#', '', '', '1', '0', 'F', '0', '0', 'workflow:definition:query', '#', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11645', '流程定义新增', '11620', '2', '#', '', '', '1', '0', 'F', '0', '0', 'workflow:definition:add', '#', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11646', '流程定义修改', '11620', '3', '#', '', '', '1', '0', 'F', '0', '0', 'workflow:definition:edit', '#', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11647', '流程定义删除', '11620', '4', '#', '', '', '1', '0', 'F', '0', '0', 'workflow:definition:remove', '#', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11648', '流程定义导出', '11620', '5', '#', '', '', '1', '0', 'F', '0', '0', 'workflow:definition:export', '#', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11649', '流程定义导入', '11620', '6', '#', '', '', '1', '0', 'F', '0', '0', 'workflow:definition:import', '#', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11650', '流程定义发布/取消发布', '11620', '7', '#', '', '', '1', '0', 'F', '0', '0', 'workflow:definition:publish', '#', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11651', '流程定义复制', '11620', '8', '#', '', '', '1', '0', 'F', '0', '0', 'workflow:definition:copy', '#', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11652', '流程定义激活/挂起', '11620', '9', '#', '', '', '1', '0', 'F', '0', '0', 'workflow:definition:active', '#', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11801', '流程表达式', '11616', 2, 'spel', 'workflow/spel/index', '', 1, 0, 'C', '0', '0', 'workflow:spel:list', 'input', 103, 1, SYSDATE, 1, SYSDATE, '流程达式定义菜单');
INSERT INTO sys_menu VALUES ('11802', '流程spel表达式定义查询', '11801', 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'workflow:spel:query', '#', 103, 1, SYSDATE, NULL, NULL, '');
INSERT INTO sys_menu VALUES ('11803', '流程spel表达式定义新增', '11801', 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'workflow:spel:add', '#', 103, 1, SYSDATE, NULL, NULL, '');

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