168 Commits

Author SHA1 Message Date
3a11f18656 fix 修复 StreamUtils 返回不可变类型报错问题 2025-09-02 15:58:11 +08:00
5a43212ccc fix 修复 StreamUtils 返回不可变类型报错问题 2025-09-02 15:51:42 +08:00
f4cfd1c913 !759 [fix] 解决工作流通知messageType参数判空逻辑错误的问题
* [fix] 解决工作流通知messageType参数判空逻辑错误的问题
2025-09-02 04:57:56 +00:00
26ce8f30c9 update 优化 支持子菜单配置默认激活的父菜单activeMenu 2025-09-02 10:45:54 +08:00
2258962770 Revert "!734 update 重写selectOne方法"
This reverts commit f2e0361fb6.
2025-09-01 14:25:47 +08:00
655e84012c Revert "update 优化 增加selectOne使用注意事项"
This reverts commit bf10a13088.
2025-09-01 14:25:40 +08:00
f683ef00b8 fix 修复 json模块配置 默认覆盖了spring module 配置问题 改为让spring自动加载注册 2025-09-01 11:46:57 +08:00
424b2ea164 update Excel写出包装器添加泛型用于限定write入参类型 2025-08-31 13:24:12 +08:00
7bb4838132 feat add Excel工具类支持更灵活的自定义导出方式,以便用户分批处理导出数据 2025-08-30 18:09:01 +08:00
20516758ea upadte 优化Stream流工具类 2025-08-30 16:53:13 +08:00
2d5f84ebc2 upadte 优化Stream流工具类 2025-08-30 16:46:04 +08:00
6bc28e41de update 优化 工作流代码 2025-08-29 10:06:22 +08:00
a4fb3fadaf update 优化 将返回值从bo改为vo 2025-08-29 09:52:21 +08:00
cfa67fcd8c Revert "update 添加 FlowCopyVo 类,优化抄送对象处理逻辑"
This reverts commit e5e8d305d2.
2025-08-29 01:35:27 +00:00
e5e8d305d2 update 添加 FlowCopyVo 类,优化抄送对象处理逻辑 2025-08-29 09:29:12 +08:00
9d0084409e update 优化 支持后端监听器解析节点扩展数据到流程变量(按钮权限 抄送人 扩展变量) 2025-08-28 17:56:10 +08:00
ee02f46dfd update 优化 支持前端返回节点扩展数据(按钮权限 抄送人 扩展变量) 2025-08-28 17:55:04 +08:00
25de0b3530 update 解析扩展属性 JSON,构建 Node 扩展属性对象,增强代码可读性 2025-08-28 16:25:33 +08:00
aa76859a05 update 添加抄送设置和变量枚举,优化扩展节点配置逻辑 2025-08-28 15:00:32 +08:00
71b70a59fe fix 修复 判断错误导致新增报错问题 2025-08-28 10:59:00 +08:00
05c9528549 !752 update 优化流程实例业务扩展的保存和删除逻辑,增强代码可读性
* update 优化流程实例业务扩展的保存和删除逻辑,增强代码可读性
2025-08-27 11:10:03 +00:00
1feb2a3861 fix 修复 菜单与部门 未做角色状态判断 2025-08-27 17:54:05 +08:00
237e78e80c update hutool 5.8.38 => 5.8.40 默认支持了验证码不生成负数 2025-08-27 11:58:07 +08:00
ffc3dcaec9 upadte 优化Excel单元格合并处理器代码逻辑分支 2025-08-26 17:13:35 +08:00
a94e474069 fix 修复 时间解析类异常问题 2025-08-26 16:10:17 +08:00
40a0e57870 fix 修复 时间解析类异常问题 2025-08-26 16:02:11 +08:00
c01ed34602 update fastexcel 1.2.0 => 1.3.0 2025-08-25 13:50:09 +08:00
26a99003d2 update springdoc 2.8.10 => 2.8.11
update redisson 3.50.0 => 3.51.0
2025-08-25 09:58:46 +08:00
93c886d3ed update 优化 对三方授权 redirectUri 回调地址进行url编码 2025-08-25 09:58:06 +08:00
9e1027690b update 更新 warm-flow 版本至 1.8.1 2025-08-22 10:23:37 +08:00
cc120c06fd !746 update 优化代码生成模板空格对齐
* update 优化代码生成模板空格对齐
2025-08-22 02:16:51 +00:00
3827da078a update 移除不必要的流程状态颜色配置 2025-08-22 09:54:40 +08:00
70d3505b94 update springdoc 2.8.9 => 2.8.10 2025-08-21 10:07:15 +08:00
a39a69cac5 reset 回滚错误提交 2025-08-21 09:25:56 +08:00
1dbce3ab7c fix 修复 校验租户账号余额 查询语句错误 2025-08-21 09:20:35 +08:00
9742b1b596 fix 修复 流程重新提交报错问题 2025-08-19 17:59:29 +08:00
d98d11ae2d fix 修复 流程重新提交报错问题 2025-08-19 17:48:30 +08:00
6742dcb33e fix 修复 sql书写错误 2025-08-19 17:18:37 +08:00
09a51478a5 add 新增 请假表 申请编号字段sql 2025-08-18 09:44:22 +08:00
f02601ab2c update 优化 表sql书写格式 2025-08-16 11:31:45 +08:00
ac56ca0e81 add 补充流程扩展sql 2025-08-15 21:35:42 +08:00
0fcf77e2ed add 增加流程业务扩展 2025-08-15 21:18:25 +08:00
0f0a3a181e !743 update 注册功能同步优化 验证码校验逻辑
* update 注册功能同步优化 验证码校验逻辑
2025-08-15 08:29:49 +00:00
e24e2c51e4 update 优化 验证码校验逻辑 2025-08-15 14:04:17 +08:00
2b0dd82d3d update 优化 工作流后台发起或审批可以手动设置办理人 2025-08-15 11:18:46 +08:00
b97f711eb4 update snailjob 1.7.0 => 1.7.2 2025-08-14 09:52:46 +08:00
0250ca4eb8 update warm-flow 升级 1.8.0 正式版 2025-08-14 09:51:44 +08:00
23338995d7 update warm-flow 升级 1.8.0-m3 2025-08-13 10:27:52 +08:00
84fd02e7d8 update 优化数据库类型获取和判断逻辑,增强代码可读性 2025-08-12 21:55:24 +08:00
ae5bec994d upadte 优化Excel单元格合并代码逻辑,明确处理类职责 2025-08-12 18:26:18 +08:00
8f3a1b589e upadte 优化OSS文件下载代码 2025-08-12 16:37:06 +08:00
ad6b3d4b3f update 改用发布订阅的方式替代阻塞流,优化大文件下载时的内存占用 2025-08-12 16:17:07 +08:00
e2801037cf update 优化 删除监控无用配置代码(升级之后不需要了) 2025-08-11 14:04:39 +08:00
65061f17fe update 优化 由spring自己初始化线程池 2025-08-11 09:47:08 +08:00
d0f4d93615 !739 支持 @ExcelIgnoreUnannotated 注解,修复未注解字段导致列合并错位的问题
* 优化避免每个字段都进行ExcelIgnoreUnannotated.class判断
* 支持 @ExcelIgnoreUnannotated 注解,修复未注解字段导致列合并错位的问题
2025-08-07 09:11:42 +00:00
5d69832423 update 优化以逗号拼接元素 2025-08-06 11:43:37 +08:00
0c1e39ea14 update 优化以逗号拼接元素 2025-08-06 11:18:06 +08:00
a39bc870d1 update 优化发布流程定义抛出异常
- StringUtils.join 默认会跳过 null,不会抛异常
2025-08-06 10:51:51 +08:00
7357912681 update 新增支持占位符格式的 ServiceException 构造方法
- 新增 ServiceException(String message, Object... args) 构造器,内部使用 Hutool StrFormatter.format 格式化消息
- 解决日志打印和异常抛出信息格式不统一的问题,统一使用 {} 占位符
- 优化异常消息书写,简化拼接,提升代码可读性和维护性
2025-08-06 10:43:41 +08:00
901992674e add 增加赞助商 2025-08-06 10:21:12 +08:00
7ceb85ffa0 add 增加赞助商 2025-08-06 10:08:22 +08:00
4672d7de4d fix 修复 撤销终止等操作 都变成退回的问题 2025-08-05 11:55:15 +08:00
87ab6e1744 remove 删除无用代码 2025-08-04 18:43:39 +08:00
ae0a03728b Merge branch 'dev' of https://gitee.com/dromara/RuoYi-Vue-Plus into dev 2025-08-04 18:41:02 +08:00
6fc82a59f1 add 增加启动流程并办理第一个任务接口 2025-08-04 18:40:14 +08:00
6c33fa48ec update snailjob 1.6.0 => 1.7.0(新增的网卡配置有异常等待官方修复 不耽误正常使用) 2025-08-04 14:32:56 +08:00
343d5d21d8 update 更新工作流sql 2025-08-04 11:15:57 +08:00
0ba909c52e update work-flow 1.6.8 => 1.8.0-m1 2025-08-01 18:34:09 +08:00
808ce9c25a update 优化办理人权限设置列表 2025-08-01 17:57:46 +08:00
4351fc5239 update 优化 升级jdk版本 2025-08-01 17:22:47 +08:00
a545f7fc44 update 优化 setCacheObject 简化写法 2025-08-01 16:30:33 +08:00
acfcdf4d9a update 全局替换为 Convert.toStr,优化 null 字符串处理 2025-08-01 15:29:36 +08:00
9683252783 fix 修复 配置缺失 2025-08-01 09:19:15 +08:00
49c00e162b update 删除无用依赖引入 2025-07-31 11:25:07 +08:00
076a0a44fa update 优化 isLogin 判断逻辑 2025-07-31 10:17:27 +08:00
0d93589d99 update 优化 PlusSaTokenDao 删除key同步删除本地缓存 2025-07-31 10:11:46 +08:00
54a8189e27 fix 修复 依赖漏删 2025-07-31 09:21:18 +08:00
84f17011ad fix 修复 监控代码报错问题 2025-07-30 16:57:57 +08:00
f47bd39644 update 优化 springboot 3.5 新特性与过期代码 2025-07-30 16:10:00 +08:00
89f9617ccb update springboot 3.4.7 => 3.5.4
update springboot-admin 3.4.7 => 3.5.1
update springdoc 2.8.8 => 2.8.9
update lombok 1.18.36 => 1.18.38
2025-07-30 15:09:18 +08:00
bf10a13088 update 优化 增加selectOne使用注意事项 2025-07-30 15:07:38 +08:00
f2e0361fb6 !734 update 重写selectOne方法
* update 重写selectOne方法
2025-07-30 02:46:51 +00:00
554152635d update 优化数据权限注解切点逻辑,使切点逻辑更清晰 2025-07-29 15:52:08 +08:00
b379574637 update 优化 压制配置警告 2025-07-29 14:57:42 +08:00
6a556cc6ff fix 修复 snailjob 未判断配置空的情况 2025-07-29 14:27:18 +08:00
a6950275ad update 优化 调整包名 2025-07-29 11:27:42 +08:00
58b1bf5c33 !731 update 把数据权限注解全部交给AOP处理,使用自定义动态方法匹配器匹配注解
* update 把数据权限注解全部交给AOP处理,使用自定义动态方法匹配器匹配注解
2025-07-29 03:25:59 +00:00
c85f693ca6 update 优化 调整自动审批代码逻辑 2025-07-28 17:09:45 +08:00
5f466fd0c4 update 优化 getBackTaskNode 获取驳回节点接口 如果是委派直接返回当前节点 不允许驳回到其他节点 2025-07-28 16:25:20 +08:00
127eaf936c update 增加国内文档加速地址 2025-07-28 13:38:27 +08:00
fcd8556076 !732 [fix] 解决委托、转办时nextTasks为空导致空指针的问题
* [fix] 解决委托、转办时nextTasks为空导致空指针的问题
2025-07-28 02:24:27 +00:00
0512781513 update 优化代码 2025-07-28 09:58:40 +08:00
may
2472359adb add 增加下一节点执行人是当前任务处理人自动审批
fix 修复已完成的实例删除失败
2025-07-27 10:44:57 +08:00
29d4bb4e59 fix 修复权限判断逻辑 2025-07-26 16:58:26 +08:00
cce95424ce fix 修复权限判断逻辑 2025-07-26 16:40:07 +08:00
may
8d7358e663 update 调整变量修改 2025-07-25 23:18:28 +08:00
may
240f10ab45 update 调整变量修改 2025-07-25 23:16:22 +08:00
48213bc9c9 fix 修复 个人中心数据被脱敏问题 2025-07-25 16:36:24 +08:00
3995d9699d fix 修复 权限为null导致报错问题 2025-07-25 15:47:16 +08:00
ecd4e3eaf0 fix 修复 监听器变量使用错误 2025-07-24 15:23:23 +08:00
2e3a42c669 update 优化 调整上传超时时间 2025-07-23 17:46:30 +08:00
82997fc6cd update 优化 getInfo 接口忽略数据权限 2025-07-22 18:08:44 +08:00
0dce571270 add 增加催办接口,调整消息发送 2025-07-22 11:46:04 +08:00
9375578925 add 增加修改流程变量接口 2025-07-22 10:39:17 +08:00
e19ccf5064 remove 删除无用导入 2025-07-22 09:49:40 +08:00
1cea7b72d7 update 优化 去除无用配置 2025-07-19 23:48:30 +08:00
5da9ddf5e3 update 新增赞助商 2025-07-19 23:47:29 +08:00
93ee01c6b9 update 更新snailjob服务端配置 2025-07-19 18:07:09 +08:00
acd30fda3c update 优化 删除无用sql 2025-07-18 23:52:28 +08:00
3f62a76cc8 update 更新snailjob版本到1.6.0 2025-07-18 18:26:09 +08:00
b0b4e573f6 fix 修复 oracle数据库无法使用不等于语法问题 2025-07-17 16:16:28 +08:00
de61899eed update 优化 对登录也租户列表接口进行限流 防止盗刷 2025-07-17 15:15:28 +08:00
3a9bdb36f1 update 优化 增加请求流程后端发起demo案例 2025-07-17 14:28:37 +08:00
may
b815b8e574 fix 修复退回后审批记录申请人错误 #ICMEJ1 2025-07-15 20:22:07 +08:00
45edee4e63 update 优化 支持在监听器设置流程变量 2025-07-15 16:57:44 +08:00
868bc492a2 update 优化 支持在监听器设置流程变量 2025-07-15 16:56:57 +08:00
90fef1bb17 update 优化 菜单权限查询 2025-07-14 10:58:12 +08:00
may
d79b48ea99 fix 修复设计器画线驳回驳回到申请人后状态未修改 2025-07-12 14:49:32 +08:00
f6993a1491 update 优化 日志脱敏支持List参数 2025-07-11 18:14:14 +08:00
6b0b7382a6 update 优化判断流程是否已结束 2025-07-10 17:36:42 +08:00
c41add355f update 优化 任务创建监听器 使用下一个节点的任务数据 2025-07-10 17:05:33 +08:00
74e3d232f5 update 优化 工作流任务创建监听器 传递流程参数 2025-07-09 10:43:22 +08:00
03fca40c7d update 优化 监控使用springSecurity新语法 2025-07-09 09:56:32 +08:00
b2ad257bd8 fix 修复 代码生成 setIsRequired 标志写反 2025-07-09 09:35:35 +08:00
7e4f0d73f4 update 优化流程分类新增修改 2025-07-08 18:01:38 +08:00
2ec802f17f fix 修复 satokenDao 无法更新已存在数据的ttl问题 2025-07-08 14:33:50 +08:00
e0ce662c28 update 优化SpEL表达式回显查询条件 2025-07-07 16:10:34 +08:00
3d9ed1b92f update 优化 sse 登录校验 避免大量报错 2025-07-07 15:45:34 +08:00
d4e6e70c43 update 优化 sql版本号更新 2025-07-07 15:30:22 +08:00
2095a96e67 update 优化 删除无用接口 2025-07-07 15:27:49 +08:00
328b61b252 !726 update 优化租户字典同步逻辑代码并添加注释
* update 优化字典同步逻辑代码并添加注释
2025-07-07 01:11:31 +00:00
781463417c update 优化SpEL表达式回显名称 2025-07-06 19:24:37 +08:00
446a14b928 update SpEL表达式回显名称 2025-07-06 19:21:23 +08:00
6c2518640b update 补充工作流动态启用注解 2025-07-06 17:50:25 +08:00
d8d138092f update 优化接口防重和加锁 2025-07-06 17:04:46 +08:00
ec31b736c7 !725 更新 pr!723 xml
* update: 更新 mapper.xml ;
* update: 更新 FlowSpelBo 参数空值校验 ;
2025-07-06 06:08:44 +00:00
e7467b2c5c !724 更新 pr!723 参数校验以及sql脚本
* update: 更新 FlowSpelBo 参数空值校验 ;
2025-07-06 06:02:44 +00:00
8050e2f1b1 update 优化 !pr723 排除租户全局使用 优化sql脚本 2025-07-06 11:16:23 +08:00
7de4559b4a update 优化 !pr723 排除租户全局使用 优化sql脚本 2025-07-06 11:10:18 +08:00
fc9c0d7657 update 优化 !pr723 排除租户全局使用 优化sql脚本 2025-07-06 11:05:46 +08:00
ab3037dc4f !723 新增工作流扩展spel表达式
* update: 更新变量名称以及初始化数据 ;
* add: 新增 FlowSpel sql 脚本 ;
2025-07-06 02:54:04 +00:00
8281b838b9 update 优化 删除无用注释 导致代码报null问题 2025-07-06 10:41:18 +08:00
2bc7171abd fix 修复密码校验误删字段 2025-07-04 19:29:30 +08:00
4f99487d24 update 优化密码校验 2025-07-04 19:18:42 +08:00
a7cddc8d40 update 优化校验角色是否有数据权限 2025-07-04 18:52:29 +08:00
f3c4c02d73 update 优化校验角色是否有数据权限 2025-07-04 18:39:54 +08:00
eb631360f4 update 优化 删除无用注释 2025-07-04 18:19:50 +08:00
a62bf04428 !721 update 发号器工具类便利性优化
* update 发号器工具类便利性优化
2025-07-04 09:46:23 +00:00
7147f81b42 fix 修复 错误修改导致页面逻辑错误 2025-07-04 17:42:19 +08:00
c9098563ca fix 修复 数据权限字段编辑错误 2025-07-04 16:55:20 +08:00
d4a8c25eab update 优化数据权限 2025-07-04 16:53:18 +08:00
0ddba506bf update 优化新增角色信息 2025-07-04 16:16:12 +08:00
d02bea85cb update 优化新增用户岗位信息判空逻辑 2025-07-04 15:54:39 +08:00
d27c58bfe8 docs 补充重构数据权限注释 2025-07-04 15:35:04 +08:00
34bb51f5c0 update 优化 增加岗位修改校验 2025-07-04 15:29:51 +08:00
64c37aaec6 update 重构用户 角色 部门 菜单的数据权限设计逻辑更符合实际业务场景与优化查询写法提高效率 2025-07-04 14:50:33 +08:00
3de036adde update 优化 屏蔽掉无用接口 2025-07-04 09:34:15 +08:00
e0df8c15d8 update 优化 修改类名 避免无用扫描 2025-07-04 09:19:29 +08:00
176793e15b update 优化 工作流代码写法 2025-07-03 14:48:53 +08:00
589ec1fdbc update 优化 增加oss扩展contentType存储 2025-07-03 10:39:23 +08:00
b421c8d017 !711 update 优化附件扩展字段对象(存储在 SysOss.ext1 的 JSON 字符串中)
* update 优化文件上传附件扩展字段对象
* update 优化保存文件的大小,方便前端进行分片下载
2025-07-03 02:20:53 +00:00
e2200bac71 update 优化流程图按审批人分组去重 2025-07-02 19:08:52 +08:00
f8950d1e20 update 优化获取流程记录 2025-07-02 18:21:53 +08:00
9dfe9f610d !715 update 优化工作流待办任务查询
* update 优化工作流待办任务查询
2025-07-02 09:15:56 +00:00
17610e8721 remove 删除 无用注解 2025-07-02 14:55:44 +08:00
f29b787767 fix 修复 有某些无聊人士 对一个demo案例提漏洞 CVE-2025-6925 2025-07-02 14:35:25 +08:00
9775283a24 !714 update 优化工作流小改动
* update 优化工作流小改动
2025-07-02 05:17:43 +00:00
debc73d7d4 !713 update 优化StreamUtils使用以及岗位删除优化
* update 优化命名含义
* update 优化StreamUtils使用以及岗位删除优化
2025-07-02 05:17:18 +00:00
226 changed files with 6141 additions and 2295 deletions

View File

@ -27,7 +27,7 @@
> 成员前端项目地址: 基于soybean [ruoyi-plus-soybean](https://gitee.com/xlsea/ruoyi-plus-soybean)<br>
> 成员项目地址: 删除多租户与工作流 [RuoYi-Vue-Plus-Single](https://gitee.com/ColorDreams/RuoYi-Vue-Plus-Single)<br>
> 文档地址: [plus-doc](https://plus-doc.dromara.org) 文档在华为云上如果打不开大概率是DNS问题 可以尝试切换网络等方式(或者科学上网)
> 文档地址: [plus-doc](https://plus-doc.dromara.org) 国内加速: [plus-doc.top](https://plus-doc.top)
## 赞助商
@ -37,7 +37,10 @@ CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <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>
[如何成为赞助商 加群联系作者详谈](https://plus-doc.dromara.org/#/common/add_group)
aizuda flowlong 工作流 - https://gitee.com/aizuda/flowlong <br>
Ruoyi-Plus-Uniapp - https://ruoyi.plus <br>
[如何成为赞助商 加群联系作者详谈 每日PV2500-3000 IP1700-2500](https://plus-doc.dromara.org/#/common/add_group)
# 本框架与RuoYi的功能差异

56
pom.xml
View File

@ -14,27 +14,27 @@
<properties>
<revision>5.4.1</revision>
<spring-boot.version>3.4.7</spring-boot.version>
<spring-boot.version>3.5.4</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.8</springdoc.version>
<springdoc.version>2.8.11</springdoc.version>
<therapi-javadoc.version>0.15.0</therapi-javadoc.version>
<fastexcel.version>1.2.0</fastexcel.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.12</mybatis-plus.version>
<p6spy.version>3.9.1</p6spy.version>
<hutool.version>5.8.38</hutool.version>
<spring-boot-admin.version>3.4.7</spring-boot-admin.version>
<redisson.version>3.50.0</redisson.version>
<hutool.version>5.8.40</hutool.version>
<spring-boot-admin.version>3.5.1</spring-boot-admin.version>
<redisson.version>3.51.0</redisson.version>
<lock4j.version>2.2.7</lock4j.version>
<dynamic-ds.version>4.3.1</dynamic-ds.version>
<snailjob.version>1.5.0</snailjob.version>
<snailjob.version>1.7.2</snailjob.version>
<mapstruct-plus.version>1.4.8</mapstruct-plus.version>
<mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
<lombok.version>1.18.36</lombok.version>
<lombok.version>1.18.38</lombok.version>
<bouncycastle.version>1.80</bouncycastle.version>
<justauth.version>1.16.7</justauth.version>
<!-- 离线IP地址定位库 -->
@ -48,7 +48,7 @@
<!-- 面向运行时的D-ORM依赖 -->
<anyline.version>8.7.2-20250603</anyline.version>
<!-- 工作流配置 -->
<warm-flow.version>1.7.4</warm-flow.version>
<warm-flow.version>1.8.1</warm-flow.version>
<!-- 插件版本 -->
<maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>
@ -118,25 +118,6 @@
<scope>import</scope>
</dependency>
<!-- Warm-Flow国产工作流引擎, 在线文档http://warm-flow.cn/ -->
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-mybatis-plus-sb3-starter</artifactId>
<version>${warm-flow.version}</version>
</dependency>
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-plugin-ui-sb-web</artifactId>
<version>${warm-flow.version}</version>
</dependency>
<!-- JustAuth 的依赖配置-->
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
<version>${justauth.version}</version>
</dependency>
<!-- common 的依赖配置-->
<dependency>
<groupId>org.dromara</groupId>
@ -313,6 +294,25 @@
<version>${mapstruct-plus.version}</version>
</dependency>
<!-- Warm-Flow国产工作流引擎, 在线文档http://warm-flow.cn/ -->
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-mybatis-plus-sb3-starter</artifactId>
<version>${warm-flow.version}</version>
</dependency>
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-plugin-ui-sb-web</artifactId>
<version>${warm-flow.version}</version>
</dependency>
<!-- JustAuth 的依赖配置-->
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
<version>${justauth.version}</version>
</dependency>
<!-- 离线IP地址定位库 ip2region -->
<dependency>
<groupId>org.lionsoul</groupId>

View File

@ -1,6 +1,6 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
FROM bellsoft/liberica-openjdk-rocky:17.0.15-cds
#FROM bellsoft/liberica-openjdk-rocky:21.0.7-cds
FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds
#FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds
#FROM findepi/graalvm:java17-native
LABEL maintainer="Lion Li"

View File

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

View File

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

View File

@ -87,7 +87,7 @@ public class SysRegisterService {
recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
throw new CaptchaExpireException();
}
if (!code.equalsIgnoreCase(captcha)) {
if (!StringUtils.equalsIgnoreCase(code, captcha)) {
recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
throw new CaptchaException();
}

View File

@ -102,7 +102,7 @@ public class PasswordAuthStrategy implements IAuthStrategy {
loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
throw new CaptchaExpireException();
}
if (!code.equalsIgnoreCase(captcha)) {
if (!StringUtils.equalsIgnoreCase(code, captcha)) {
loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
throw new CaptchaException();
}

View File

@ -27,8 +27,6 @@ snail-job:
port: 2${server.port}
# 客户端ip指定
host:
# RPC类型: netty, grpc
rpc-type: grpc
--- # 数据源配置
spring:

View File

@ -30,8 +30,6 @@ snail-job:
port: 2${server.port}
# 客户端ip指定
host:
# RPC类型: netty, grpc
rpc-type: grpc
--- # 数据源配置
spring:

View File

@ -57,6 +57,13 @@ spring:
# 开启虚拟线程 仅jdk21可用
virtual:
enabled: false
task:
execution:
# 从 springboot 3.5 开始 spring自带线程池
# 不再需要 AsyncConfig与ThreadPoolConfig 可直接注入线程池使用
thread-name-prefix: async-
# 由spring自己初始化线程池
mode: force
# 资源信息
messages:
# 国际化资源文件路径
@ -127,6 +134,7 @@ tenant:
- sys_user_role
- sys_client
- sys_oss_config
- flow_spel
# MyBatisPlus配置
# https://baomidou.com/config/
@ -189,13 +197,6 @@ springdoc:
name: Lion Li
email: crazylionli@163.com
url: https://gitee.com/dromara/RuoYi-Vue-Plus
components:
# 鉴权方式配置
security-schemes:
apiKey:
type: APIKEY
in: HEADER
name: ${sa-token.token-name}
#这里定义了两个分组,可定义多个,也可以不定义
group-configs:
- group: 1.演示模块
@ -213,20 +214,10 @@ springdoc:
xss:
# 过滤开关
enabled: true
# 排除链接(多个用逗号分隔)
# 排除链接
excludeUrls:
- /system/notice
# 全局线程池相关配置
# 如使用JDK21请直接使用虚拟线程 不要开启此配置
thread-pool:
# 是否开启线程池
enabled: false
# 队列最大长度
queueCapacity: 128
# 线程池维护线程所允许的空闲时间
keepAliveSeconds: 300
--- # 分布式锁 lock4j 全局配置
lock4j:
# 获取分布式锁超时时间,默认为 3000 毫秒
@ -266,13 +257,7 @@ warm-flow:
enabled: true
# 是否开启设计器ui
ui: true
# 是否显示流程图顶部文字
top-text-show: true
# 默认Authorization如果有多个token用逗号分隔
token-name: ${sa-token.token-name},clientid
# 流程状态对应的三元色
chart-status-color:
## 未办理
- 62,62,62
## 待办理
- 255,205,23
## 已办理
- 157,255,0

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,6 +27,11 @@ public class ProcessEvent implements Serializable {
*/
private String flowCode;
/**
* 实例id
*/
private Long instanceId;
/**
* 业务id
*/

View File

@ -4,6 +4,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Map;
/**
* 流程任务监听
@ -46,6 +47,11 @@ public class ProcessTaskEvent implements Serializable {
*/
private Long taskId;
/**
* 实例id
*/
private Long instanceId;
/**
* 业务id
*/
@ -56,4 +62,9 @@ public class ProcessTaskEvent implements Serializable {
*/
private String status;
/**
* 办理参数
*/
private Map<String, Object> params;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -100,28 +100,4 @@ public interface UserService {
*/
Map<Long, String> selectUserNamesByIds(List<Long> userIds);
/**
* 根据角色 ID 列表查询角色名称映射关系
*
* @param roleIds 角色 ID 列表
* @return Map其中 key 为角色 IDvalue 为对应的角色名称
*/
Map<Long, String> selectRoleNamesByIds(List<Long> roleIds);
/**
* 根据部门 ID 列表查询部门名称映射关系
*
* @param deptIds 部门 ID 列表
* @return Map其中 key 为部门 IDvalue 为对应的部门名称
*/
Map<Long, String> selectDeptNamesByIds(List<Long> deptIds);
/**
* 根据岗位 ID 列表查询岗位名称映射关系
*
* @param postIds 岗位 ID 列表
* @return Map其中 key 为岗位 IDvalue 为对应的岗位名称
*/
Map<Long, String> selectPostNamesByIds(List<Long> postIds);
}

View File

@ -82,6 +82,7 @@ public interface WorkflowService {
* completeTask.getVariables().put("ignore", true);
*
* @param completeTask 参数
* @return 结果
*/
boolean completeTask(CompleteTaskDTO completeTask);
@ -90,6 +91,15 @@ public interface WorkflowService {
*
* @param taskId 任务ID
* @param message 办理意见
* @return 结果
*/
boolean completeTask(Long taskId, String message);
/**
* 启动流程并办理第一个任务
*
* @param startProcess 参数
* @return 结果
*/
boolean startCompleteTask(StartProcessDTO startProcess);
}

View File

@ -293,7 +293,7 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
// 校验时间跨度不超过最大限制
if (diff > maxValue) {
throw new ServiceException("最大时间跨度为 " + maxValue + " " + unit.toString().toLowerCase());
throw new ServiceException("最大时间跨度为 {} {}", maxValue, unit.toString().toLowerCase());
}
}

View File

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

View File

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

View File

@ -260,13 +260,13 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils {
if (s != null) {
final int len = s.length();
if (s.length() <= size) {
sb.append(String.valueOf(c).repeat(size - len));
sb.append(Convert.toStr(c).repeat(size - len));
sb.append(s);
} else {
return s.substring(len - size, len);
}
} else {
sb.append(String.valueOf(c).repeat(Math.max(0, size)));
sb.append(Convert.toStr(c).repeat(Math.max(0, size)));
}
return sb.toString();
}
@ -361,5 +361,24 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils {
return input;
}
}
/**
* 将可迭代对象中的元素使用逗号拼接成字符串
*
* @param iterable 可迭代对象,如 List、Set 等
* @return 拼接后的字符串
*/
public static String joinComma(Iterable<?> iterable) {
return StringUtils.join(iterable, SEPARATOR);
}
/**
* 将数组中的元素使用逗号拼接成字符串
*
* @param array 任意类型的数组
* @return 拼接后的字符串
*/
public static String joinComma(Object[] array) {
return StringUtils.join(array, SEPARATOR);
}
}

View File

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

View File

@ -54,14 +54,15 @@ public class SpringDocConfig {
openApi.externalDocs(properties.getExternalDocs());
openApi.tags(properties.getTags());
openApi.paths(properties.getPaths());
openApi.components(properties.getComponents());
Set<String> keySet = properties.getComponents().getSecuritySchemes().keySet();
List<SecurityRequirement> list = new ArrayList<>();
SecurityRequirement securityRequirement = new SecurityRequirement();
keySet.forEach(securityRequirement::addList);
list.add(securityRequirement);
openApi.security(list);
if (properties.getComponents() != null) {
openApi.components(properties.getComponents());
Set<String> keySet = properties.getComponents().getSecuritySchemes().keySet();
List<SecurityRequirement> list = new ArrayList<>();
SecurityRequirement securityRequirement = new SecurityRequirement();
keySet.forEach(securityRequirement::addList);
list.add(securityRequirement);
openApi.security(list);
}
return openApi;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package org.dromara.common.excel.core;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.ObjectUtil;
@ -103,7 +104,7 @@ public class ExcelDownHandler implements SheetWriteHandler {
if (StringUtils.isNotBlank(dictType)) {
// 如果传递了字典名,则依据字典建立下拉
Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType))
.orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType)))
.orElseThrow(() -> new ServiceException("字典 {} 不存在", dictType))
.values();
options = new ArrayList<>(values);
} else if (StringUtils.isNotBlank(converterExp)) {
@ -115,7 +116,7 @@ public class ExcelDownHandler implements SheetWriteHandler {
// 否则如果指定了@ExcelEnumFormat则使用枚举的逻辑
ExcelEnumFormat format = field.getDeclaredAnnotation(ExcelEnumFormat.class);
List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField());
options = StreamUtils.toList(values, String::valueOf);
options = StreamUtils.toList(values, Convert::toStr);
}
if (ObjectUtil.isNotEmpty(options)) {
// 仅当下拉可选项不为空时执行

View File

@ -27,6 +27,7 @@ import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
/**
* Excel相关处理
@ -203,6 +204,44 @@ public class ExcelUtil {
builder.doWrite(list);
}
/**
* 导出excel
*
* @param headType 带Excel注解的类型
* @param os 输出流
* @param options Excel下拉可选项
* @param consumer 导出助手消费函数
*/
public static <T> void exportExcel(Class<T> headType, OutputStream os, List<DropDownOptions> options, Consumer<ExcelWriterWrapper<T>> consumer) {
try (ExcelWriter writer = FastExcel.write(os, headType)
.autoCloseStream(false)
// 自动适配
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
// 大数值自动转换 防止失真
.registerConverter(new ExcelBigNumberConvert())
// 批注必填项处理
.registerWriteHandler(new DataWriteHandler(headType))
// 添加下拉框操作
.registerWriteHandler(new ExcelDownHandler(options))
.build()) {
// 执行消费函数
consumer.accept(ExcelWriterWrapper.of(writer));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 导出excel
*
* @param headType 带Excel注解的类型
* @param os 输出流
* @param consumer 导出助手消费函数
*/
public static <T> void exportExcel(Class<T> headType, OutputStream os, Consumer<ExcelWriterWrapper<T>> consumer) {
exportExcel(headType, os, null, consumer);
}
/**
* 单表多数据模板导出 模板格式为 {.属性}
*

View File

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

View File

@ -1,5 +1,6 @@
package org.dromara.common.json.config;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
@ -28,20 +29,24 @@ import java.util.TimeZone;
@AutoConfiguration(before = JacksonAutoConfiguration.class)
public class JacksonConfig {
@Bean
public Module registerJavaTimeModule() {
// 全局配置序列化返回 JSON 处理
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE);
javaTimeModule.addSerializer(Long.TYPE, BigNumberSerializer.INSTANCE);
javaTimeModule.addSerializer(BigInteger.class, BigNumberSerializer.INSTANCE);
javaTimeModule.addSerializer(BigDecimal.class, ToStringSerializer.instance);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
javaTimeModule.addDeserializer(Date.class, new CustomDateDeserializer());
return javaTimeModule;
}
@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
return builder -> {
// 全局配置序列化返回 JSON 处理
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE);
javaTimeModule.addSerializer(Long.TYPE, BigNumberSerializer.INSTANCE);
javaTimeModule.addSerializer(BigInteger.class, BigNumberSerializer.INSTANCE);
javaTimeModule.addSerializer(BigDecimal.class, ToStringSerializer.instance);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
javaTimeModule.addDeserializer(Date.class, new CustomDateDeserializer());
builder.modules(javaTimeModule);
builder.timeZone(TimeZone.getDefault());
log.info("初始化 jackson 配置");
};

View File

@ -1,9 +1,11 @@
package org.dromara.common.json.handler;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import org.dromara.common.core.utils.ObjectUtils;
import java.io.IOException;
import java.util.Date;
@ -25,7 +27,11 @@ public class CustomDateDeserializer extends JsonDeserializer<Date> {
*/
@Override
public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return DateUtil.parse(p.getText());
DateTime parse = DateUtil.parse(p.getText());
if (ObjectUtils.isNull(parse)) {
return null;
}
return parse.toJdkDate();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
package org.dromara.common.redis.utils;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.SpringUtils;
@ -10,6 +10,10 @@ import org.redisson.api.RIdGenerator;
import org.redisson.api.RedissonClient;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
/**
* 发号器工具类
@ -23,12 +27,12 @@ public class SequenceUtils {
/**
* 默认初始值
*/
public static final Long DEFAULT_INIT_VALUE = 1L;
public static final long DEFAULT_INIT_VALUE = 1L;
/**
* 默认步长
*/
public static final Long DEFAULT_STEP_VALUE = 1L;
public static final long DEFAULT_STEP_VALUE = 1L;
/**
* 默认过期时间-天
@ -40,6 +44,11 @@ public class SequenceUtils {
*/
public static final Duration DEFAULT_EXPIRE_TIME_MINUTE = Duration.ofMinutes(1);
/**
* 默认最小ID容量位数 - 6位数即至少可以生成的ID为999999个
*/
public static final int DEFAULT_MIN_ID_CAPACITY_BITS = 6;
/**
* 获取Redisson客户端实例
*/
@ -54,14 +63,11 @@ public class SequenceUtils {
* @param stepValue ID步长
* @return ID生成器
*/
private static RIdGenerator getIdGenerator(String key, Duration expireTime, Long initValue, Long stepValue) {
if (initValue == null || initValue <= 0) {
initValue = DEFAULT_INIT_VALUE;
}
if (stepValue == null || stepValue <= 0) {
stepValue = DEFAULT_STEP_VALUE;
}
public static RIdGenerator getIdGenerator(String key, Duration expireTime, long initValue, long stepValue) {
RIdGenerator idGenerator = REDISSON_CLIENT.getIdGenerator(key);
// 初始值和步长不能小于等于0
initValue = initValue <= 0 ? DEFAULT_INIT_VALUE : initValue;
stepValue = stepValue <= 0 ? DEFAULT_STEP_VALUE : stepValue;
// 设置初始值和步长
idGenerator.tryInit(initValue, stepValue);
// 设置过期时间
@ -69,6 +75,17 @@ public class SequenceUtils {
return idGenerator;
}
/**
* 获取ID生成器
*
* @param key 业务key
* @param expireTime 过期时间
* @return ID生成器
*/
public static RIdGenerator getIdGenerator(String key, Duration expireTime) {
return getIdGenerator(key, expireTime, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE);
}
/**
* 获取指定业务key的唯一id
*
@ -78,10 +95,21 @@ public class SequenceUtils {
* @param stepValue ID步长
* @return 唯一id
*/
public static long nextId(String key, Duration expireTime, Long initValue, Long stepValue) {
public static long getNextId(String key, Duration expireTime, long initValue, long stepValue) {
return getIdGenerator(key, expireTime, initValue, stepValue).nextId();
}
/**
* 获取指定业务key的唯一id (ID初始值=1,ID步长=1)
*
* @param key 业务key
* @param expireTime 过期时间
* @return 唯一id
*/
public static long getNextId(String key, Duration expireTime) {
return getIdGenerator(key, expireTime).nextId();
}
/**
* 获取指定业务key的唯一id字符串
*
@ -91,19 +119,8 @@ public class SequenceUtils {
* @param stepValue ID步长
* @return 唯一id
*/
public static String nextIdStr(String key, Duration expireTime, Long initValue, Long stepValue) {
return String.valueOf(nextId(key, expireTime, initValue, stepValue));
}
/**
* 获取指定业务key的唯一id (ID初始值=1,ID步长=1)
*
* @param key 业务key
* @param expireTime 过期时间
* @return 唯一id
*/
public static long nextId(String key, Duration expireTime) {
return getIdGenerator(key, expireTime, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE).nextId();
public static String getNextIdString(String key, Duration expireTime, long initValue, long stepValue) {
return Convert.toStr(getNextId(key, expireTime, initValue, stepValue));
}
/**
@ -113,8 +130,8 @@ public class SequenceUtils {
* @param expireTime 过期时间
* @return 唯一id
*/
public static String nextIdStr(String key, Duration expireTime) {
return String.valueOf(nextId(key, expireTime));
public static String getNextIdString(String key, Duration expireTime) {
return Convert.toStr(getNextId(key, expireTime));
}
/**
@ -125,56 +142,210 @@ public class SequenceUtils {
* @param width 位数不足左补0
* @return 补零后的唯一id字符串
*/
public static String nextPaddedIdStr(String key, Duration expireTime, Integer width) {
return StringUtils.leftPad(nextIdStr(key, expireTime), width, '0');
public static String getPaddedNextIdString(String key, Duration expireTime, Integer width) {
return StringUtils.leftPad(getNextIdString(key, expireTime), width, '0');
}
/**
* 获取 yyyyMMdd 开头的唯一id
* 获取 yyyyMMdd 格式的唯一id
*
* @return 唯一id
* @deprecated 请使用 {@link #getDateId(String)} 或 {@link #getDateId(String, boolean)}、{@link #getDateId(String, boolean, int)}确保不同业务的ID连续性
*/
public static String nextIdDate() {
return nextIdDate("");
@Deprecated
public static String getDateId() {
return getDateId("");
}
/**
* 获取 prefix + yyyyMMdd 开头的唯一id
* 获取 prefix + yyyyMMdd 格式的唯一id
*
* @param prefix 业务前缀
* @return 唯一id
*/
public static String nextIdDate(String prefix) {
// 前缀+日期 构建 prefixKey
String prefixKey = StringUtils.format("{}{}", StringUtils.blankToDefault(prefix, ""), DateUtil.format(DateUtil.date(), DatePattern.PURE_DATE_FORMATTER));
// 获取下一个id
long nextId = getIdGenerator(prefixKey, DEFAULT_EXPIRE_TIME_DAY, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE).nextId();
// 返回完整id
return StringUtils.format("{}{}", prefixKey, nextId);
public static String getDateId(String prefix) {
return getDateId(prefix, true);
}
/**
* 获取 yyyyMMddHHmmss 开头的唯一id
* 获取 prefix + yyyyMMdd 格式的唯一id
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @return 唯一id
*/
public static String nextIdDateTime() {
return nextIdDateTime("");
public static String getDateId(String prefix, boolean isWithPrefix) {
return getDateId(prefix, isWithPrefix, -1);
}
/**
* 获取 prefix + yyyyMMddHHmmss 开头的唯一id
* 获取 prefix + yyyyMMdd 格式的唯一id (启用ID补位补位长度 = {@link #DEFAULT_MIN_ID_CAPACITY_BITS})}
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @return 唯一id
*/
public static String getPaddedDateId(String prefix, boolean isWithPrefix) {
return getDateId(prefix, isWithPrefix, DEFAULT_MIN_ID_CAPACITY_BITS);
}
/**
* 获取 prefix + yyyyMMdd 格式的唯一id
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @param minIdCapacityBits 最小ID容量位数小于该位数的ID左补0小于等于0表示不启用补位
* @return 唯一id
*/
public static String getDateId(String prefix, boolean isWithPrefix, int minIdCapacityBits) {
return getDateId(prefix, isWithPrefix, minIdCapacityBits, LocalDate.now());
}
/**
* 获取 prefix + yyyyMMdd 格式的唯一id
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @param minIdCapacityBits 最小ID容量位数小于该位数的ID左补0小于等于0表示不启用补位
* @param time 时间
* @return 唯一id
*/
public static String getDateId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDate time) {
return getDateId(prefix, isWithPrefix, minIdCapacityBits, time, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE);
}
/**
* 获取 prefix + yyyyMMdd 格式的唯一id
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @param minIdCapacityBits 最小ID容量位数小于该位数的ID左补0小于等于0表示不启用补位
* @param time 时间
* @param initValue ID初始值
* @param stepValue ID步长
* @return 唯一id
*/
public static String getDateId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDate time, long initValue, long stepValue) {
return getDatePatternId(prefix, isWithPrefix, minIdCapacityBits, time, DatePattern.PURE_DATE_FORMATTER, DEFAULT_EXPIRE_TIME_DAY, initValue, stepValue);
}
/**
* 获取 yyyyMMddHHmmss 格式的唯一id
*
* @return 唯一id
* @deprecated 请使用 {@link #getDateTimeId(String)} 或 {@link #getDateTimeId(String, boolean)}、{@link #getDateTimeId(String, boolean, int)}确保不同业务的ID连续性
*/
@Deprecated
public static String getDateTimeId() {
return getDateTimeId("", false);
}
/**
* 获取 prefix + yyyyMMddHHmmss 格式的唯一id
*
* @param prefix 业务前缀
* @return 唯一id
*/
public static String nextIdDateTime(String prefix) {
// 前缀+日期时间 构建 prefixKey
String prefixKey = StringUtils.format("{}{}", StringUtils.blankToDefault(prefix, ""), DateUtil.format(DateUtil.date(), DatePattern.PURE_DATETIME_FORMATTER));
// 获取下一个id
long nextId = getIdGenerator(prefixKey, DEFAULT_EXPIRE_TIME_MINUTE, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE).nextId();
// 返回完整id
return StringUtils.format("{}{}", prefixKey, nextId);
public static String getDateTimeId(String prefix) {
return getDateTimeId(prefix, true);
}
/**
* 获取 prefix + yyyyMMddHHmmss 格式的唯一id
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @return 唯一id
*/
public static String getDateTimeId(String prefix, boolean isWithPrefix) {
return getDateTimeId(prefix, isWithPrefix, -1);
}
/**
* 获取 prefix + yyyyMMddHHmmss 格式的唯一id (启用ID补位补位长度 = {@link #DEFAULT_MIN_ID_CAPACITY_BITS})}
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @return 唯一id
*/
public static String getPaddedDateTimeId(String prefix, boolean isWithPrefix) {
return getDateTimeId(prefix, isWithPrefix, DEFAULT_MIN_ID_CAPACITY_BITS);
}
/**
* 获取 prefix + yyyyMMddHHmmss 格式的唯一id
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @param minIdCapacityBits 最小ID容量位数小于该位数的ID左补0小于等于0表示不启用补位
* @return 唯一id
*/
public static String getDateTimeId(String prefix, boolean isWithPrefix, int minIdCapacityBits) {
return getDateTimeId(prefix, isWithPrefix, minIdCapacityBits, LocalDateTime.now());
}
/**
* 获取 prefix + yyyyMMddHHmmss 格式的唯一id
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @param minIdCapacityBits 最小ID容量位数小于该位数的ID左补0小于等于0表示不启用补位
* @param time 时间
* @return 唯一id
*/
public static String getDateTimeId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDateTime time) {
return getDateTimeId(prefix, isWithPrefix, minIdCapacityBits, time, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE);
}
/**
* 获取 prefix + yyyyMMddHHmmss 格式的唯一id
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @param minIdCapacityBits 最小ID容量位数小于该位数的ID左补0小于等于0表示不启用补位
* @param initValue ID初始值
* @param stepValue ID步长
* @return 唯一id
*/
public static String getDateTimeId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDateTime time, long initValue, long stepValue) {
return getDatePatternId(prefix, isWithPrefix, minIdCapacityBits, time, DatePattern.PURE_DATETIME_FORMATTER, DEFAULT_EXPIRE_TIME_MINUTE, initValue, stepValue);
}
/**
* 获取指定业务key的指定时间格式的ID
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @param minIdCapacityBits 最小ID容量位数小于该位数的ID左补0小于等于0表示不启用补位
* @param temporalAccessor 时间访问器
* @param timeFormatter 时间格式
* @param expireTime 过期时间
* @param initValue ID初始值
* @param stepValue ID步长
* @return 唯一id
*/
private static String getDatePatternId(String prefix, boolean isWithPrefix, int minIdCapacityBits, TemporalAccessor temporalAccessor, DateTimeFormatter timeFormatter, Duration expireTime, long initValue, long stepValue) {
// 时间前缀
String timePrefix = timeFormatter.format(temporalAccessor);
// 业务前缀 + 时间前缀 构建 prefixKey
String prefixKey = StringUtils.format("{}{}", StringUtils.blankToDefault(prefix, ""), timePrefix);
// 获取id例 -> 1
String nextId = getNextIdString(prefixKey, expireTime, initValue, stepValue);
// minIdCapacityBits 大于0且 nextId 的长度小于 minIdCapacityBits则左补0
if (minIdCapacityBits > 0 && nextId.length() < minIdCapacityBits) {
nextId = StringUtils.leftPad(nextId, minIdCapacityBits, '0');
}
// 是否携带业务前缀
if (isWithPrefix) {
// 例 -> P202507031
// 其中 P 为业务前缀202507031 为 yyyyMMdd 格式时间, 1 为nextId
return StringUtils.format("{}{}", prefixKey, nextId);
}
// 例 -> 202507031
// 其中 202507031 为 yyyyMMdd 格式时间, 1 为nextId
return StringUtils.format("{}{}", timePrefix, nextId);
}
}

View File

@ -53,11 +53,7 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
if (timeout == NEVER_EXPIRE) {
RedisUtils.setCacheObject(key, value);
} else {
if (RedisUtils.hasKey(key)) {
RedisUtils.setCacheObject(key, value, true);
} else {
RedisUtils.setCacheObject(key, value, Duration.ofSeconds(timeout));
}
RedisUtils.setCacheObject(key, value, Duration.ofSeconds(timeout));
}
CAFFEINE.invalidate(key);
}
@ -78,7 +74,9 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
*/
@Override
public void delete(String key) {
RedisUtils.deleteObject(key);
if (RedisUtils.deleteObject(key)) {
CAFFEINE.invalidate(key);
}
}
/**
@ -134,11 +132,7 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
if (timeout == NEVER_EXPIRE) {
RedisUtils.setCacheObject(key, object);
} else {
if (RedisUtils.hasKey(key)) {
RedisUtils.setCacheObject(key, object, true);
} else {
RedisUtils.setCacheObject(key, object, Duration.ofSeconds(timeout));
}
RedisUtils.setCacheObject(key, object, Duration.ofSeconds(timeout));
}
CAFFEINE.invalidate(key);
}
@ -159,7 +153,9 @@ public class PlusSaTokenDao implements SaTokenDaoBySessionFollowObject {
*/
@Override
public void deleteObject(String key) {
RedisUtils.deleteObject(key);
if (RedisUtils.deleteObject(key)) {
CAFFEINE.invalidate(key);
}
}
/**

View File

@ -1,6 +1,7 @@
package org.dromara.common.satoken.core.service;
import cn.dev33.satoken.stp.StpInterface;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.enums.UserType;
@ -39,8 +40,12 @@ public class SaPermissionImpl implements StpInterface {
if (userType == UserType.APP_USER) {
// 其他端 自行根据业务编写
}
// SYS_USER 默认返回权限
return new ArrayList<>(loginUser.getMenuPermission());
if (CollUtil.isNotEmpty(loginUser.getMenuPermission())) {
// SYS_USER 默认返回权限
return new ArrayList<>(loginUser.getMenuPermission());
} else {
return new ArrayList<>();
}
}
/**
@ -62,8 +67,12 @@ public class SaPermissionImpl implements StpInterface {
if (userType == UserType.APP_USER) {
// 其他端 自行根据业务编写
}
// SYS_USER 默认返回权限
return new ArrayList<>(loginUser.getRolePermission());
if (CollUtil.isNotEmpty(loginUser.getRolePermission())) {
// SYS_USER 默认返回权限
return new ArrayList<>(loginUser.getRolePermission());
} else {
return new ArrayList<>();
}
}
private PermissionService getPermissionService() {

View File

@ -207,7 +207,8 @@ public class LoginHelper {
*/
public static boolean isLogin() {
try {
return getLoginUser() != null;
StpUtil.checkLogin();
return true;
} catch (Exception e) {
return false;
}

View File

@ -1,5 +1,6 @@
package org.dromara.common.sensitive.core;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.DesensitizedUtil;
import lombok.AllArgsConstructor;
@ -52,7 +53,7 @@ public enum SensitiveStrategy {
/**
* 用户ID
*/
USER_ID(s -> String.valueOf(DesensitizedUtil.userId())),
USER_ID(s -> Convert.toStr(DesensitizedUtil.userId())),
/**
* 密码

View File

@ -14,6 +14,9 @@ import org.dromara.common.social.gitea.AuthGiteaRequest;
import org.dromara.common.social.maxkey.AuthMaxKeyRequest;
import org.dromara.common.social.topiam.AuthTopIamRequest;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* 认证授权工具类
*
@ -40,7 +43,7 @@ public class SocialUtils {
AuthConfig.AuthConfigBuilder builder = AuthConfig.builder()
.clientId(obj.getClientId())
.clientSecret(obj.getClientSecret())
.redirectUri(obj.getRedirectUri())
.redirectUri(URLEncoder.encode(obj.getRedirectUri(), StandardCharsets.UTF_8))
.scopes(obj.getScopes());
return switch (source.toLowerCase()) {
case "dingtalk" -> new AuthDingTalkV2Request(builder.build(), STATE_CACHE);

View File

@ -6,7 +6,6 @@ import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.sse.core.SseEmitterManager;
import org.dromara.common.sse.dto.SseMessageDto;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.MediaType;
@ -14,8 +13,6 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.List;
/**
* SSE 控制器
*
@ -33,7 +30,9 @@ public class SseController implements DisposableBean {
*/
@GetMapping(value = "${sse.path}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect() {
StpUtil.checkLogin();
if (!StpUtil.isLogin()) {
return null;
}
String tokenValue = StpUtil.getTokenValue();
Long userId = LoginHelper.getUserId();
return sseEmitterManager.connect(userId, tokenValue);
@ -51,31 +50,32 @@ public class SseController implements DisposableBean {
return R.ok();
}
/**
* 向特定用户发送消息
*
* @param userId 目标用户的 ID
* @param msg 要发送的消息内容
*/
@GetMapping(value = "${sse.path}/send")
public R<Void> send(Long userId, String msg) {
SseMessageDto dto = new SseMessageDto();
dto.setUserIds(List.of(userId));
dto.setMessage(msg);
sseEmitterManager.publishMessage(dto);
return R.ok();
}
/**
* 向所有用户发送消息
*
* @param msg 要发送的消息内容
*/
@GetMapping(value = "${sse.path}/sendAll")
public R<Void> send(String msg) {
sseEmitterManager.publishAll(msg);
return R.ok();
}
// 以下为demo仅供参考 禁止使用 请在业务逻辑中使用工具发送而不是用接口发送
// /**
// * 向特定用户发送消息
// *
// * @param userId 目标用户的 ID
// * @param msg 要发送的消息内容
// */
// @GetMapping(value = "${sse.path}/send")
// public R<Void> send(Long userId, String msg) {
// SseMessageDto dto = new SseMessageDto();
// dto.setUserIds(List.of(userId));
// dto.setMessage(msg);
// sseEmitterManager.publishMessage(dto);
// return R.ok();
// }
//
// /**
// * 向所有用户发送消息
// *
// * @param msg 要发送的消息内容
// */
// @GetMapping(value = "${sse.path}/sendAll")
// public R<Void> send(String msg) {
// sseEmitterManager.publishAll(msg);
// return R.ok();
// }
/**
* 清理资源。此方法目前不执行任何操作,但避免因未实现而导致错误

View File

@ -21,11 +21,6 @@
<artifactId>ruoyi-common-json</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
<!-- SpringBoot Web容器 -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@ -7,6 +7,7 @@ import org.dromara.common.web.filter.XssFilter;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistration;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
@ -21,24 +22,20 @@ public class FilterConfig {
@Bean
@ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
public FilterRegistrationBean<XssFilter> xssFilterRegistration() {
FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>();
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new XssFilter());
registration.addUrlPatterns("/*");
registration.setName("xssFilter");
registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE + 1);
return registration;
@FilterRegistration(
name = "xssFilter",
urlPatterns = "/*",
order = FilterRegistrationBean.HIGHEST_PRECEDENCE + 1,
dispatcherTypes = DispatcherType.REQUEST
)
public XssFilter xssFilter() {
return new XssFilter();
}
@Bean
public FilterRegistrationBean<RepeatableFilter> someFilterRegistration() {
FilterRegistrationBean<RepeatableFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new RepeatableFilter());
registration.addUrlPatterns("/*");
registration.setName("repeatableFilter");
registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
return registration;
@FilterRegistration(name = "repeatableFilter", urlPatterns = "/*")
public RepeatableFilter repeatableFilter() {
return new RepeatableFilter();
}
}

View File

@ -1,8 +1,8 @@
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 org.dromara.common.web.utils.UnsignedMathGenerator;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -18,7 +18,7 @@ public enum CaptchaType {
/**
* 数字
*/
MATH(UnsignedMathGenerator.class),
MATH(MathGenerator.class),
/**
* 字符

View File

@ -1,88 +0,0 @@
package org.dromara.common.web.utils;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.math.Calculator;
import cn.hutool.core.util.CharUtil;
import cn.hutool.core.util.RandomUtil;
import org.dromara.common.core.utils.StringUtils;
import java.io.Serial;
/**
* 无符号计算生成器
*
* @author Lion Li
*/
public class UnsignedMathGenerator implements CodeGenerator {
@Serial
private static final long serialVersionUID = -5514819971774091076L;
private static final String OPERATORS = "+-*";
/**
* 参与计算数字最大长度
*/
private final int numberLength;
/**
* 构造
*/
public UnsignedMathGenerator() {
this(2);
}
/**
* 构造
*
* @param numberLength 参与计算最大数字位数
*/
public UnsignedMathGenerator(int numberLength) {
this.numberLength = numberLength;
}
@Override
public String generate() {
final int limit = getLimit();
int a = RandomUtil.randomInt(limit);
int b = RandomUtil.randomInt(limit);
String max = Integer.toString(Math.max(a,b));
String min = Integer.toString(Math.min(a,b));
max = StringUtils.rightPad(max, this.numberLength, CharUtil.SPACE);
min = StringUtils.rightPad(min, this.numberLength, CharUtil.SPACE);
return max + RandomUtil.randomChar(OPERATORS) + min + '=';
}
@Override
public boolean verify(String code, String userInputCode) {
int result;
try {
result = Integer.parseInt(userInputCode);
} catch (NumberFormatException e) {
// 用户输入非数字
return false;
}
final int calculateResult = (int) Calculator.conversion(code);
return result == calculateResult;
}
/**
* 获取验证码长度
*
* @return 验证码长度
*/
public int getLength() {
return this.numberLength * 2 + 2;
}
/**
* 根据长度获取参与计算数字最大值
*
* @return 最大值
*/
private int getLimit() {
return Integer.parseInt("1" + StringUtils.repeat('0', this.numberLength));
}
}

View File

@ -1,6 +1,6 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
FROM bellsoft/liberica-openjdk-rocky:17.0.15-cds
#FROM bellsoft/liberica-openjdk-rocky:21.0.7-cds
FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds
#FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds
#FROM findepi/graalvm:java17-native
LABEL maintainer="Lion Li"

View File

@ -1,5 +1,6 @@
package org.dromara.monitor.admin;
import de.codecentric.boot.admin.server.config.EnableAdminServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@ -8,6 +9,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
*
* @author Lion Li
*/
@EnableAdminServer
@SpringBootApplication
public class MonitorAdminApplication {

View File

@ -1,31 +0,0 @@
package org.dromara.monitor.admin.config;
import de.codecentric.boot.admin.server.config.EnableAdminServer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
import org.springframework.boot.task.ThreadPoolTaskExecutorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
/**
* springboot-admin server配置类
*
* @author Lion Li
*/
@Configuration
@EnableAdminServer
public class AdminServerConfig {
@Lazy
@Bean(name = TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
@ConditionalOnMissingBean(Executor.class)
public ThreadPoolTaskExecutor applicationTaskExecutor(ThreadPoolTaskExecutorBuilder builder) {
return builder.build();
}
}

View File

@ -10,7 +10,7 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
/**
* admin 监控 安全配置
@ -32,14 +32,14 @@ public class SecurityConfig {
SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
successHandler.setTargetUrlParameter("redirectTo");
successHandler.setDefaultTargetUrl(adminContextPath + "/");
PathPatternRequestMatcher.Builder mvc = PathPatternRequestMatcher.withDefaults();
return httpSecurity
.headers((header) ->
header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.authorizeHttpRequests((authorize) ->
authorize.requestMatchers(
new AntPathRequestMatcher(adminContextPath + "/assets/**"),
new AntPathRequestMatcher(adminContextPath + "/login")
mvc.matcher(adminContextPath + "/assets/**"),
mvc.matcher(adminContextPath + "/login")
).permitAll()
.anyRequest().authenticated())
.formLogin((formLogin) ->

View File

@ -20,6 +20,9 @@ spring:
ui:
title: RuoYi-Vue-Plus服务监控中心
context-path: /admin
# 忽略无用警告
thymeleaf:
check-template-location: false
--- # Actuator 监控端点的配置项
management:

View File

@ -1,6 +1,6 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
FROM bellsoft/liberica-openjdk-rocky:17.0.15-cds
#FROM bellsoft/liberica-openjdk-rocky:21.0.7-cds
FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds
#FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds
#FROM findepi/graalvm:java17-native
LABEL maintainer="Lion Li"

View File

@ -0,0 +1,146 @@
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

@ -16,15 +16,26 @@ spring:
--- # snail-job 服务端配置
snail-job:
# 拉取重试数据的每批次的大小
retry-pull-page-size: 1000
# 拉取重试数据的每批次的大小
job-pull-page-size: 1000
# 服务器端口
# 服务端节点IP(默认按照`NetUtil.getLocalIpStr()`)
server-host:
# 服务端netty的端口号
server-port: 17888
# 日志保存时间(单位: day)
# 合并日志默认保存天数
merge-Log-days: 1
# 合并日志默认的条数
merge-Log-num: 500
# 配置每批次拉取重试数据的大小
retry-pull-page-size: 100
# 配置日志保存时间(单位:天)
log-storage: 7
rpc-type: grpc
# bucket的总数量
bucket-total: 128
# Dashboard 任务容错天数
summary-day: 7
# 配置负载均衡周期时间
load-balance-cycle-time: 10
# 重试任务拉取的并行度
retry-max-pull-parallel: 2
--- # 监控中心配置
spring.boot.admin.client:

View File

@ -16,15 +16,26 @@ spring:
--- # snail-job 服务端配置
snail-job:
# 拉取重试数据的每批次的大小
retry-pull-page-size: 1000
# 拉取重试数据的每批次的大小
job-pull-page-size: 1000
# 服务器端口
# 服务端节点IP(默认按照`NetUtil.getLocalIpStr()`)
server-host:
# 服务端netty的端口号
server-port: 17888
# 日志保存时间(单位: day)
# 合并日志默认保存天数
merge-Log-days: 1
# 合并日志默认的条数
merge-Log-num: 500
# 配置每批次拉取重试数据的大小
retry-pull-page-size: 100
# 配置日志保存时间(单位:天)
log-storage: 7
rpc-type: grpc
# bucket的总数量
bucket-total: 128
# Dashboard 任务容错天数
summary-day: 7
# 配置负载均衡周期时间
load-balance-cycle-time: 10
# 重试任务拉取的并行度
retry-max-pull-parallel: 2
--- # 监控中心配置
spring.boot.admin.client:

View File

@ -85,6 +85,7 @@
<!-- 控制台输出日志级别 -->
<root level="info">
<appender-ref ref="console" />
<appender-ref ref="file_console" />
<appender-ref ref="async_info" />
<appender-ref ref="async_error" />
<appender-ref ref="snail_log_server_appender" />

View File

@ -1,6 +1,5 @@
package org.dromara.demo.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.mail.utils.MailUtils;
@ -18,12 +17,11 @@ import java.util.Arrays;
*
* @author Michelle.Chung
*/
@SaIgnore
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo/mail")
public class MailController {
public class MailSendController {
/**
* 发送邮件
@ -44,11 +42,11 @@ public class MailController {
* @param to 接收人
* @param subject 标题
* @param text 内容
* @param filePath 附件路径
*/
@GetMapping("/sendMessageWithAttachment")
public R<Void> sendMessageWithAttachment(String to, String subject, String text, String filePath) {
MailUtils.sendText(to, subject, text, new File(filePath));
public R<Void> sendMessageWithAttachment(String to, String subject, String text) {
// 附件路径 禁止前端传递 有任意读取系统文件风险
MailUtils.sendText(to, subject, text, new File("/xxx/xxx"));
return R.ok();
}
@ -58,10 +56,11 @@ public class MailController {
* @param to 接收人
* @param subject 标题
* @param text 内容
* @param paths 附件路径
*/
@GetMapping("/sendMessageWithAttachments")
public R<Void> sendMessageWithAttachments(String to, String subject, String text, String[] paths) {
public R<Void> sendMessageWithAttachments(String to, String subject, String text) {
// 附件路径 禁止前端传递 有任意读取系统文件风险
String[] paths = new String[]{"/xxx/xxx", "/xxx/xxx"};
File[] array = Arrays.stream(paths).map(File::new).toArray(File[]::new);
MailUtils.sendText(to, subject, text, array);
return R.ok();

View File

@ -1,5 +1,6 @@
package org.dromara.demo.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.hutool.core.collection.CollUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
@ -14,6 +15,7 @@ import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -94,6 +96,16 @@ public class TestExcelController {
exportExcelService.exportWithOptions(response);
}
/**
* 自定义导出
*
* @param response /
*/
@GetMapping("/customExport")
public void customExport(HttpServletResponse response) throws IOException {
exportExcelService.customExport(response);
}
/**
* 多个sheet导出
*/

View File

@ -2,6 +2,8 @@ package org.dromara.demo.service;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 导出下拉框Excel示例
*
@ -15,4 +17,11 @@ public interface IExportExcelService {
* @param response /
*/
void exportWithOptions(HttpServletResponse response);
/**
* 自定义导出
*
* @param response /
*/
void customExport(HttpServletResponse response) throws IOException;
}

View File

@ -2,17 +2,21 @@ package org.dromara.demo.service.impl;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.idev.excel.write.metadata.WriteSheet;
import jakarta.servlet.http.HttpServletResponse;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.file.FileUtils;
import org.dromara.common.excel.core.DropDownOptions;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.excel.utils.ExcelWriterWrapper;
import org.dromara.demo.domain.vo.ExportDemoVo;
import org.dromara.demo.service.IExportExcelService;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -233,4 +237,61 @@ public class ExportExcelServiceImpl implements IExportExcelService {
this.name = name;
}
}
@Override
public void customExport(HttpServletResponse response) throws IOException {
String filename = ExcelUtil.encodingFilename("自定义导出");
FileUtils.setAttachmentResponseHeader(response, filename);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
ExcelUtil.exportExcel(ExportDemoVo.class, response.getOutputStream(), wrapper -> {
// 创建表格数据,业务中一般通过数据库查询
List<ExportDemoVo> excelDataList = new ArrayList<>();
for (int i = 0; i < 30; i++) {
// 模拟数据库中的一条数据
ExportDemoVo everyRowData = new ExportDemoVo();
everyRowData.setNickName("用户-" + i);
everyRowData.setUserStatus(SystemConstants.NORMAL);
everyRowData.setGender("1");
everyRowData.setPhoneNumber(String.format("175%08d", i));
everyRowData.setEmail(String.format("175%08d", i) + "@163.com");
everyRowData.setProvinceId(i);
everyRowData.setCityId(i);
everyRowData.setAreaId(i);
excelDataList.add(everyRowData);
}
// 创建表格
WriteSheet sheet = ExcelWriterWrapper.sheetBuilder("自定义导出demo")
// 合并单元格
// .registerWriteHandler(new CellMergeStrategy(excelDataList, true))
.build();
wrapper.write(excelDataList, sheet);
List<ExportDemoVo> excelDataList2 = new ArrayList<>();
for (int i = 0; i < 20; i++) {
int index = 1000 + i;
// 模拟数据库中的一条数据
ExportDemoVo everyRowData = new ExportDemoVo();
everyRowData.setNickName("用户-" + index);
everyRowData.setUserStatus(SystemConstants.NORMAL);
everyRowData.setGender("1");
everyRowData.setPhoneNumber(String.format("175%08d", index));
everyRowData.setEmail(String.format("175%08d", index) + "@163.com");
everyRowData.setProvinceId(index);
everyRowData.setCityId(index);
everyRowData.setAreaId(index);
excelDataList2.add(everyRowData);
}
wrapper.write(excelDataList2, sheet);
// 或者在同一个excel中创建多个表格
// WriteSheet sheet2 = ExcelWriterWrapper.sheetBuilder("自定义导出demo2").build();
// wrapper.write(excelDataList2, sheet2);
});
}
}

View File

@ -42,6 +42,11 @@
<artifactId>ruoyi-common-log</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-idempotent</artifactId>
</dependency>
<!--velocity代码生成使用模板 -->
<dependency>
<groupId>org.apache.velocity</groupId>

View File

@ -3,9 +3,11 @@ package org.dromara.generator.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IoUtil;
import com.baomidou.lock.annotation.Lock4j;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
@ -50,6 +52,7 @@ public class GenController extends BaseController {
*
* @param tableId 表ID
*/
@RepeatSubmit()
@SaCheckPermission("tool:gen:query")
@GetMapping(value = "/{tableId}")
public R<Map<String, Object>> getInfo(@PathVariable Long tableId) {
@ -91,6 +94,7 @@ public class GenController extends BaseController {
*/
@SaCheckPermission("tool:gen:import")
@Log(title = "代码生成", businessType = BusinessType.IMPORT)
@RepeatSubmit()
@PostMapping("/importTable")
public R<Void> importTableSave(String tables, String dataName) {
String[] tableNames = Convert.toStrArray(tables);
@ -105,6 +109,7 @@ public class GenController extends BaseController {
*/
@SaCheckPermission("tool:gen:edit")
@Log(title = "代码生成", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping
public R<Void> editSave(@Validated @RequestBody GenTable genTable) {
genTableService.validateEdit(genTable);
@ -170,6 +175,7 @@ public class GenController extends BaseController {
*/
@SaCheckPermission("tool:gen:edit")
@Log(title = "代码生成", businessType = BusinessType.UPDATE)
@Lock4j
@GetMapping("/synchDb/{tableId}")
public R<Void> synchDb(@PathVariable("tableId") Long tableId) {
genTableService.synchDb(tableId);

View File

@ -302,7 +302,7 @@ public class GenTableServiceImpl implements IGenTableService {
tableColumn.setColumnComment(column.getComment());
tableColumn.setColumnType(column.getOriginType().toLowerCase());
tableColumn.setSort(column.getPosition());
tableColumn.setIsRequired(column.isNullable() ? "1" : "0");
tableColumn.setIsRequired(column.isNullable() ? "0" : "1");
tableColumn.setIsIncrement(column.isAutoIncrement() ? "1" : "0");
tableColumns.add(tableColumn);
});

View File

@ -3,6 +3,7 @@ package org.dromara.generator.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Dict;
import org.dromara.common.mybatis.enums.DataBaseType;
import org.dromara.generator.constant.GenConstants;
import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.core.utils.StringUtils;
@ -118,11 +119,12 @@ public class VelocityUtils {
templates.add("vm/java/serviceImpl.java.vm");
templates.add("vm/java/controller.java.vm");
templates.add("vm/xml/mapper.xml.vm");
if (DataBaseHelper.isOracle()) {
DataBaseType dataBaseType = DataBaseHelper.getDataBaseType();
if (dataBaseType.isOracle()) {
templates.add("vm/sql/oracle/sql.vm");
} else if (DataBaseHelper.isPostgerSql()) {
} else if (dataBaseType.isPostgreSql()) {
templates.add("vm/sql/postgres/sql.vm");
} else if (DataBaseHelper.isSqlServer()) {
} else if (dataBaseType.isSqlServer()) {
templates.add("vm/sql/sqlserver/sql.vm");
} else {
templates.add("vm/sql/sql.vm");

View File

@ -54,11 +54,8 @@ export interface ${BusinessName}Query #if(!${treeCode})extends PageQuery #end{
#end
#end
#end
/**
* 日期范围参数
*/
params?: any;
/**
* 日期范围参数
*/
params?: any;
}

View File

@ -4,8 +4,8 @@ import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.aizuda.snailjob.client.job.core.annotation.JobExecutor;
import com.aizuda.snailjob.client.job.core.dto.JobArgs;
import com.aizuda.snailjob.client.model.ExecuteResult;
import com.aizuda.snailjob.common.log.SnailJobLog;
import com.aizuda.snailjob.model.dto.ExecuteResult;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.job.entity.BillDto;
import org.springframework.stereotype.Component;

View File

@ -3,8 +3,8 @@ package org.dromara.job.snailjob;
import cn.hutool.core.util.StrUtil;
import com.aizuda.snailjob.client.job.core.annotation.JobExecutor;
import com.aizuda.snailjob.client.job.core.dto.JobArgs;
import com.aizuda.snailjob.client.model.ExecuteResult;
import com.aizuda.snailjob.common.log.SnailJobLog;
import com.aizuda.snailjob.model.dto.ExecuteResult;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.job.entity.BillDto;
import org.springframework.stereotype.Component;

View File

@ -2,9 +2,9 @@ package org.dromara.job.snailjob;
import com.aizuda.snailjob.client.job.core.annotation.JobExecutor;
import com.aizuda.snailjob.client.job.core.dto.JobArgs;
import com.aizuda.snailjob.client.model.ExecuteResult;
import com.aizuda.snailjob.common.core.util.JsonUtil;
import com.aizuda.snailjob.common.log.SnailJobLog;
import com.aizuda.snailjob.model.dto.ExecuteResult;
import org.springframework.stereotype.Component;
/**

View File

@ -3,8 +3,8 @@ package org.dromara.job.snailjob;
import cn.hutool.core.util.RandomUtil;
import com.aizuda.snailjob.client.job.core.annotation.JobExecutor;
import com.aizuda.snailjob.client.job.core.dto.JobArgs;
import com.aizuda.snailjob.client.model.ExecuteResult;
import com.aizuda.snailjob.common.log.SnailJobLog;
import com.aizuda.snailjob.model.dto.ExecuteResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

View File

@ -2,7 +2,7 @@ package org.dromara.job.snailjob;
import com.aizuda.snailjob.client.job.core.dto.JobArgs;
import com.aizuda.snailjob.client.job.core.executor.AbstractJobExecutor;
import com.aizuda.snailjob.client.model.ExecuteResult;
import com.aizuda.snailjob.model.dto.ExecuteResult;
import org.springframework.stereotype.Component;
/**

View File

@ -6,8 +6,8 @@ import com.aizuda.snailjob.client.job.core.MapHandler;
import com.aizuda.snailjob.client.job.core.annotation.JobExecutor;
import com.aizuda.snailjob.client.job.core.annotation.MapExecutor;
import com.aizuda.snailjob.client.job.core.dto.MapArgs;
import com.aizuda.snailjob.client.model.ExecuteResult;
import com.aizuda.snailjob.common.log.SnailJobLog;
import com.aizuda.snailjob.model.dto.ExecuteResult;
import org.springframework.stereotype.Component;
import java.util.List;

View File

@ -8,8 +8,8 @@ import com.aizuda.snailjob.client.job.core.annotation.MapExecutor;
import com.aizuda.snailjob.client.job.core.annotation.ReduceExecutor;
import com.aizuda.snailjob.client.job.core.dto.MapArgs;
import com.aizuda.snailjob.client.job.core.dto.ReduceArgs;
import com.aizuda.snailjob.client.model.ExecuteResult;
import com.aizuda.snailjob.common.log.SnailJobLog;
import com.aizuda.snailjob.model.dto.ExecuteResult;
import org.springframework.stereotype.Component;
import java.util.List;

View File

@ -1,9 +1,10 @@
package org.dromara.job.snailjob;
import cn.hutool.core.convert.Convert;
import com.aizuda.snailjob.client.job.core.annotation.JobExecutor;
import com.aizuda.snailjob.client.job.core.dto.JobArgs;
import com.aizuda.snailjob.client.model.ExecuteResult;
import com.aizuda.snailjob.common.log.SnailJobLog;
import com.aizuda.snailjob.model.dto.ExecuteResult;
import org.springframework.stereotype.Component;
/**
@ -17,7 +18,7 @@ import org.springframework.stereotype.Component;
public class TestStaticShardingJob {
public ExecuteResult jobExecute(JobArgs jobArgs) {
String jobParams = String.valueOf(jobArgs.getJobParams());
String jobParams = Convert.toStr(jobArgs.getJobParams());
SnailJobLog.LOCAL.info("开始执行分片任务,参数:{}", jobParams);
// 获得jobArgs 中传入的开始id和结束id
String[] split = jobParams.split(",");

View File

@ -4,8 +4,8 @@ import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.aizuda.snailjob.client.job.core.annotation.JobExecutor;
import com.aizuda.snailjob.client.job.core.dto.JobArgs;
import com.aizuda.snailjob.client.model.ExecuteResult;
import com.aizuda.snailjob.common.log.SnailJobLog;
import com.aizuda.snailjob.model.dto.ExecuteResult;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.job.entity.BillDto;
import org.springframework.stereotype.Component;

View File

@ -53,6 +53,13 @@ public class CacheController {
}
}
/**
* 缓存监控列表信息
*
* @param info 信息
* @param dbSize 数据库
* @param commandStats 命令统计
*/
public record CacheListInfoVo(Properties info, Long dbSize, List<Map<String, String>> commandStats) {}
}

View File

@ -1,11 +1,13 @@
package org.dromara.system.controller.monitor;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.baomidou.lock.annotation.Lock4j;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.constant.CacheConstants;
import org.dromara.common.core.domain.R;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
@ -69,6 +71,7 @@ public class SysLogininforController extends BaseController {
*/
@SaCheckPermission("monitor:logininfor:remove")
@Log(title = "登录日志", businessType = BusinessType.CLEAN)
@Lock4j
@DeleteMapping("/clean")
public R<Void> clean() {
logininforService.cleanLogininfor();
@ -77,6 +80,7 @@ public class SysLogininforController extends BaseController {
@SaCheckPermission("monitor:logininfor:unlock")
@Log(title = "账户解锁", businessType = BusinessType.OTHER)
@RepeatSubmit()
@GetMapping("/unlock/{userName}")
public R<Void> unlock(@PathVariable("userName") String userName) {
String loginName = CacheConstants.PWD_ERR_CNT_KEY + userName;

View File

@ -1,21 +1,22 @@
package org.dromara.system.controller.monitor;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.mybatis.core.page.PageQuery;
import com.baomidou.lock.annotation.Lock4j;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.system.domain.bo.SysOperLogBo;
import org.dromara.system.domain.vo.SysOperLogVo;
import org.dromara.system.service.ISysOperLogService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**
@ -67,6 +68,7 @@ public class SysOperlogController extends BaseController {
*/
@Log(title = "操作日志", businessType = BusinessType.CLEAN)
@SaCheckPermission("monitor:operlog:remove")
@Lock4j
@DeleteMapping("/clean")
public R<Void> clean() {
operLogService.cleanOperLog();

View File

@ -10,6 +10,7 @@ import org.dromara.common.core.domain.R;
import org.dromara.common.core.domain.dto.UserOnlineDTO;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.TableDataInfo;
@ -81,6 +82,7 @@ public class SysUserOnlineController extends BaseController {
*/
@SaCheckPermission("monitor:online:forceLogout")
@Log(title = "在线用户", businessType = BusinessType.FORCE)
@RepeatSubmit()
@DeleteMapping("/{tokenId}")
public R<Void> forceLogout(@PathVariable String tokenId) {
try {
@ -114,6 +116,7 @@ public class SysUserOnlineController extends BaseController {
* @param tokenId token值
*/
@Log(title = "在线设备", businessType = BusinessType.FORCE)
@RepeatSubmit()
@DeleteMapping("/myself/{tokenId}")
public R<Void> remove(@PathVariable("tokenId") String tokenId) {
try {

View File

@ -5,6 +5,7 @@ import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
@ -78,6 +79,7 @@ public class SysConfigController extends BaseController {
*/
@SaCheckPermission("system:config:add")
@Log(title = "参数管理", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping
public R<Void> add(@Validated @RequestBody SysConfigBo config) {
if (!configService.checkConfigKeyUnique(config)) {
@ -92,6 +94,7 @@ public class SysConfigController extends BaseController {
*/
@SaCheckPermission("system:config:edit")
@Log(title = "参数管理", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping
public R<Void> edit(@Validated @RequestBody SysConfigBo config) {
if (!configService.checkConfigKeyUnique(config)) {
@ -106,6 +109,7 @@ public class SysConfigController extends BaseController {
*/
@SaCheckPermission("system:config:edit")
@Log(title = "参数管理", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping("/updateByKey")
public R<Void> updateByKey(@RequestBody SysConfigBo config) {
configService.updateConfig(config);

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