Compare commits

...

423 Commits
v2.3.0 ... 2.X

Author SHA1 Message Date
疯狂的狮子Li
778ee424e6
!252 fix 修复 查询条件写错问题
Merge pull request !252 from 疯狂的狮子Li/dev
2025-12-24 06:12:07 +00:00
疯狂的狮子Li
3b82076b62 fix 修复 查询条件写错问题 2025-12-24 14:11:34 +08:00
疯狂的狮子Li
43e522e35c
!251 fix 修复 判断条件写反问题
Merge pull request !251 from 疯狂的狮子Li/dev
2025-12-24 05:27:47 +00:00
疯狂的狮子Li
b4e1bf2592 fix 修复 判断条件写反问题 2025-12-24 13:11:11 +08:00
疯狂的狮子Li
577bb456a4 remove 删除无用配置类(升级之后问题解决不需要重写了) 2025-12-24 13:07:31 +08:00
疯狂的狮子Li
df130b0455 update easy-es 3.0.0 => 3.0.1 2025-12-23 16:29:40 +08:00
疯狂的狮子Li
d145e3e432 fix 修复 elasticsearch-client 被spring的依赖覆盖导致出现版本兼容问题 2025-12-23 16:21:14 +08:00
疯狂的狮子Li
8923333d3f update 优化 websocket 多线程下IO阻塞的问题 2025-12-23 15:56:20 +08:00
疯狂的狮子Li
b1a7de3bb1 update 优化 增加 HandlerMethodValidationException 参数校验异常连接 2025-12-23 15:31:06 +08:00
疯狂的狮子Li
a87450ec2c
!248 紧急修复 Ip2Region InputStream读取函数导致的OOM问题
Merge pull request !248 from 疯狂的狮子Li/dev
2025-12-23 06:45:57 +00:00
疯狂的狮子Li
672444d9d9 fix 修复 Ip2Region InputStream读取函数导致的OOM问题 2025-12-23 14:45:23 +08:00
疯狂的狮子Li
c6b4014eab update 优化 优化rpc查询实现 减少rpc调用次数 2025-12-23 10:43:27 +08:00
疯狂的狮子Li
1be37bae6b
!247 发布 2.5.2 版本 2025年最后一版
Merge pull request !247 from 疯狂的狮子Li/dev
2025-12-23 01:40:03 +00:00
疯狂的狮子Li
93f9249410 🧨🧨🧨发布 2.5.2 版本 2025年最后一版 2025-12-23 09:28:36 +08:00
疯狂的狮子Li
3ca290b7d7 update 删除错误的配置 2025-12-23 09:23:52 +08:00
疯狂的狮子Li
6f3bc78ebc update anyline 8.7.2-20250603 => 8.7.3-20251210 2025-12-22 13:12:14 +08:00
疯狂的狮子Li
bc86e7e1f0 update snailjob 1.8.0 => 1.9.0 2025-12-22 09:44:28 +08:00
疯狂的狮子Li
ec7d850445 fix 修复 创建租户同步工作流数据 在没有流程定义的情况下不会复制流程类别的问题 2025-12-19 19:39:31 +08:00
疯狂的狮子Li
c09fa60433 update 优化 增强单元格合并处理逻辑 2025-12-19 13:08:27 +08:00
疯狂的狮子Li
972b1248af fix 修复 listenerVariable.getVariable() 获取null问题 2025-12-19 13:06:46 +08:00
疯狂的狮子Li
7185bb64e4 update 优化 增加高安全脱敏方法 2025-12-19 13:06:16 +08:00
疯狂的狮子Li
16e683283e update springboot 3.5.8 => 3.5.9
update springcloud 2025.0.0 => 2025.0.1
2025-12-19 11:32:16 +08:00
疯狂的狮子Li
5a62c58fe3 remove 删除无用工具 2025-12-19 11:07:33 +08:00
疯狂的狮子Li
a24870cc05 update 优化 构建流程参数 2025-12-18 09:44:32 +08:00
疯狂的狮子Li
d68e9e27de update 代码生成字典类型字段新增更新验证策略 2025-12-18 09:43:05 +08:00
疯狂的狮子Li
e8f4828528 update 优化 测试单表和测试树表增加搜索条件 2025-12-18 09:42:27 +08:00
疯狂的狮子Li
2add71a01c update 优化 提示语与待办时间展示 2025-12-15 15:49:03 +08:00
疯狂的狮子Li
7f009f4b09 fix 修复 form_path 输入空字符串导致的问题 2025-12-15 09:42:18 +08:00
疯狂的狮子Li
2787571820 update Ip2Region version to 3.3.1,使用新的xdb文件加载函数优化xdb数据库的加载流程 2025-12-15 09:41:49 +08:00
疯狂的狮子Li
7361519474 fix 修复 工作流类别 顶节点父级可以被修改导致无法加载的问题 2025-12-12 17:16:06 +08:00
疯狂的狮子Li
30195a02f7 fix 修复 微软三方对接参数缺失 2025-12-12 11:41:50 +08:00
疯狂的狮子Li
f9e8b751d6 update ip2region 2.7.0 => 3.3.0 2025-12-12 09:35:26 +08:00
疯狂的狮子Li
fdbffa2b61 update 添加 ID 生成工具类,支持多种 ID 生成方式 2025-12-11 10:25:35 +08:00
疯狂的狮子Li
2c58beca79 fix 修复 获取可驳回节点重复问题(感谢 搬砖的小庄) 2025-12-11 10:20:33 +08:00
疯狂的狮子Li
f72182e589 update 优化 任务执行监听器 传递任务的相关数据 不传递实例相关数据了(避免并行节点覆盖问题) 2025-12-11 09:44:42 +08:00
疯狂的狮子Li
bab2b19604 update 优化 加签判断逻辑 2025-12-11 09:18:18 +08:00
疯狂的狮子Li
79a8e473ce fix 修复 excel 导出多 sheet 合并单元格失效问题 2025-12-10 17:23:18 +08:00
疯狂的狮子Li
9061bdd4cc update warm-flow 1.8.3 => 1.8.4 2025-12-10 17:02:01 +08:00
疯狂的狮子Li
ad10e576bd update warm-flow 1.8.3 => 1.8.4 2025-12-10 16:32:13 +08:00
疯狂的狮子Li
ee735ed10a update 优化 文件上传增加文件内容长度校验 2025-12-10 09:49:45 +08:00
疯狂的狮子Li
513d6cece3 fix 修复 本地文件上传 无法获取文件长度问题 2025-12-09 17:03:58 +08:00
疯狂的狮子Li
c315c41270 update kafka docker镜像升级 使用apache官方镜像 3.6.2 => 3.9.1 2025-12-09 16:47:22 +08:00
疯狂的狮子Li
9db0e10f3d update 优化 日志脱敏改用JsonNode处理提高效率 2025-12-09 16:17:08 +08:00
疯狂的狮子Li
927658cc40 fix 修复 jsonParam 参数可能为空问题 2025-12-09 15:50:25 +08:00
疯狂的狮子Li
63db3e5468 fix 修复 request.getQueryParams() 返回只读类型不可操作问题 2025-12-09 13:00:28 +08:00
疯狂的狮子Li
9d7f870ef0 add 新增 Topiam 赞助商 2025-12-08 13:04:19 +08:00
疯狂的狮子Li
b08d2d11db update 优化 接口访问日志 排除敏感参数输出 2025-12-08 10:07:25 +08:00
疯狂的狮子Li
d6849ae328 update 优化 修改 ossclient 并发配置 2025-12-08 10:03:49 +08:00
疯狂的狮子Li
85f4478d2f update 优化 任务处理增加Lock4j锁支持 2025-12-08 10:03:12 +08:00
AprilWind
23672c120a update 增加SpEL表达式解析异常处理 2025-12-02 19:03:22 +08:00
AprilWind
3be17bc145 refactor 优化工作流服务中的异常处理 2025-12-02 18:42:18 +08:00
AprilWind
d9e5f86efa update 增加SpEL表达式解析异常处理 2025-12-02 18:20:25 +08:00
AprilWind
4bd3467595 update 优化代码生成中的Lock4j锁 2025-12-02 18:18:00 +08:00
AprilWind
8cf324b936 update 优化我的任务查询条件 2025-12-02 18:16:30 +08:00
AprilWind
0e4a01bdf4 fix 修复排他网关执行后,驳回选到未执行的网关 2025-12-02 18:13:21 +08:00
AprilWind
2ef0ca8d58 update 补充操作日志 2025-12-02 18:12:29 +08:00
AprilWind
49a0d38373 fix 修复指定选人审批后 再次驳回到指定选人环节后 全部人能看到待办问题 2025-12-02 18:08:40 +08:00
疯狂的狮子Li
df372fb659 update spring-cloud-alibaba 2023.0.3.4 => 2025.0.0.0
update dubbo 3.3.5 => 3.3.6
2025-11-27 11:17:38 +08:00
疯狂的狮子Li
604856f7c2 update 优化 脱敏工具类支持灵活配置可见长度和掩码长度 2025-11-24 17:09:51 +08:00
疯狂的狮子Li
ddfa8a2601 update 优化 参数配置服务 增加多种配置获取方法,支持不同类型的配置解析 2025-11-24 17:08:45 +08:00
疯狂的狮子Li
36cc2ea1e2 update 优化 流程定义发布检查 确保流程在执行前已发布 2025-11-24 17:05:48 +08:00
疯狂的狮子Li
2043c1c158 update 增加 fory 开启日志说明 2025-11-24 11:55:13 +08:00
疯狂的狮子Li
478d6ebe33 update springboot 3.5.7 => 3.5.8
update springdoc 2.8.13 => 2.8.14
update redisson 3.51.0 => 3.52.0
update fury 更名为 fory 0.9.0 => 0.13.1
2025-11-24 10:53:59 +08:00
疯狂的狮子Li
829c19e806 reset 回滚 snailjob 1.8.1版本到1.8.0版本 出现严重依赖冲突问题 2025-11-20 17:46:49 +08:00
疯狂的狮子Li
c62425e2ea update 优化消息发送逻辑,增加异常处理并记录未处理的消息类型 2025-11-20 17:07:12 +08:00
疯狂的狮子Li
f5bf38f16f update 优化 pg 字段类型适配 2025-11-19 17:42:57 +08:00
疯狂的狮子Li
f58137c60c update 优化 将特殊方法改为私有禁止不懂的用户乱用 2025-11-19 16:32:48 +08:00
疯狂的狮子Li
4a870fa135 update 优化 删除业务ID的方法,支持字符串类型的业务ID 2025-11-19 16:32:10 +08:00
疯狂的狮子Li
0ef3439750 fix 修复 更新sql书写错误 2025-11-14 13:22:14 +08:00
疯狂的狮子Li
e840387fab update 优化 支持创建上传请求的预签名URL 2025-11-13 16:36:44 +08:00
疯狂的狮子Li
0ddb6dee67 update snailjob 1.8.0 => 1.8.1
update warmflow 1.8.2 => 1.8.3
2025-11-13 16:35:09 +08:00
疯狂的狮子Li
2e3fe5804e update 优化 更正注释描述错误 2025-11-13 16:34:12 +08:00
疯狂的狮子Li
65dad95e3b update 优化 !781Excel 模版动态数据下拉 泛型逻辑 2025-11-11 19:01:48 +08:00
疯狂的狮子Li
965fe349f6 add 增加 Excel模版动态数据下拉 2025-11-11 10:05:14 +08:00
疯狂的狮子Li
411c551f90 fix 修复 申请人提交可直接结束流程 2025-11-11 10:05:14 +08:00
LiYaoheng
f0931258a1 !243 fix 增加remote调用dictTypeVo类转换mapper。完善引用注解声明方便阅读代码
* fix 增加remote调用dictTypeVo类转换mapper。完善引用注解声明方便阅读代码
2025-11-05 06:47:29 +00:00
疯狂的狮子Li
3df354dbd4 fix 修复 变量错误问题 2025-10-28 16:51:55 +08:00
疯狂的狮子Li
7e54246af2 update 客户端管理新增客户端key唯一校验逻辑 2025-10-28 11:24:52 +08:00
疯狂的狮子Li
d568797ba4
!242 发布 2.5.1 正式版 日常依赖升级bug修复
Merge pull request !242 from 疯狂的狮子Li/dev
2025-10-28 03:22:22 +00:00
疯狂的狮子Li
00a1eeb088 fix 修复 版本号提交错误 2025-10-28 11:19:38 +08:00
疯狂的狮子Li
6bfa87a7e6 🐳🐳🐳发布 2.5.1 正式版 日常依赖升级bug修复 2025-10-28 11:16:11 +08:00
疯狂的狮子Li
299b56af4f update springboot 3.5.6 => 3.5.7 2025-10-27 09:41:09 +08:00
疯狂的狮子Li
5e2692bc43 fix 修复 seata 表字段长度可能会不够问题 2025-10-23 14:09:50 +08:00
疯狂的狮子Li
a4ac09efd9 fix 修复 全局处理器不生效问题 根据官方issue改为特殊写法(不理解为什么 https://github.com/apache/fesod/issues/648) 2025-10-22 14:21:29 +08:00
疯狂的狮子Li
17bca63e37 fix 修复 查询任务扩展数据不存在导致的空报错 2025-10-22 11:31:37 +08:00
疯狂的狮子Li
37fe228d5e update springcloud-alibaba 2023.0.3.3 => 2023.0.3.4 2025-10-22 09:51:50 +08:00
AprilWind
7a36e85b8c update 优化 sse 修复相同token历史连接未关闭问题 2025-10-20 16:15:35 +08:00
AprilWind
15d938448b update warm-flow 升级 1.8.2 2025-10-20 16:13:50 +08:00
疯狂的狮子Li
b4e645ef66 fix 修复 查询pg类型问题 2025-10-15 18:03:50 +08:00
wlb
422760dde7 !241 fix 翻译时异常导致json序列化结构体不符合预期
* fix 翻译时异常导致json序列化结构体不符合预期
2025-10-15 05:25:25 +00:00
疯狂的狮子Li
a7207e11f7 fix 修复 orderby属性书写重复问题 2025-10-14 18:54:12 +08:00
疯狂的狮子Li
81255a921e update 添加菜单可见性和状态字段到菜单树 2025-10-14 18:53:43 +08:00
疯狂的狮子Li
4be304217e fix 修复 降级方法缺失问题 2025-10-11 17:06:30 +08:00
疯狂的狮子Li
743467c021 update 优化 nginx 配置,增强性能与安全性 2025-09-30 10:10:02 +08:00
疯狂的狮子Li
0055e01dc1 update 优化 拦截sse超时异常 不需要额外处理 2025-09-29 13:35:47 +08:00
疯狂的狮子Li
362894a7ec update springboot-admin 3.5.3 -> 3.5.5 修复登录白屏问题 2025-09-29 11:57:52 +08:00
疯狂的狮子Li
5927171aea fix 修复 三方授权 钉钉回调地址未进行url编码问题 由全局编码改为单独编码 避免其他三方调用重复编码 2025-09-28 16:19:01 +08:00
疯狂的狮子Li
ae6c0a7e64 update 优化 删除Threads类 已经不需要了 2025-09-26 15:27:41 +08:00
疯狂的狮子Li
9533adea09 add 增加 同步租户参数配置功能 2025-09-26 11:59:58 +08:00
疯狂的狮子Li
bccbed47eb fix 修复 mybatis内报token异常无法正常返回前端信息 2025-09-26 11:28:03 +08:00
疯狂的狮子Li
63f1deddc3 update 优化工作流常量使用 2025-09-26 11:27:19 +08:00
疯狂的狮子Li
c87601ce39 update 添加 JSON 格式校验注解及实现 2025-09-26 11:25:25 +08:00
疯狂的狮子Li
4c71784c0e update 忽略压缩后的日志文件 *.log.gz 2025-09-26 11:21:28 +08:00
疯狂的狮子Li
905cf33897 update 隐藏 nginx 版本号以增强安全性 2025-09-26 11:21:04 +08:00
疯狂的狮子Li
5ecb06f2d9 update 优化 satoken 异常信息 强制返回json格式 2025-09-26 09:54:50 +08:00
疯狂的狮子Li
a5a86a5c15 update 更新流程案例json文件 2025-09-25 11:58:44 +08:00
疯狂的狮子Li
49b1d65af7 update 优化 后端发起流程增加扩展表对象 2025-09-24 16:58:12 +08:00
疯狂的狮子Li
50bcac1c73
!239 发布 2.5.0 喜迎国庆
Merge pull request !239 from 疯狂的狮子Li/dev
2025-09-22 03:18:10 +00:00
疯狂的狮子Li
057e3540a9 发布 2.5.0 喜迎国庆🧨🧨🧨 2025-09-22 11:15:10 +08:00
疯狂的狮子Li
26961919e1 update snailjob 1.7.2 => 1.8.0 2025-09-22 11:14:25 +08:00
疯狂的狮子Li
789273d0d9 Revert "update warm-flow 升级 1.8.2-m2"
This reverts commit 0fff64da20cfa3733ee2b8e2344b1193886ff6a1.
2025-09-22 11:09:00 +08:00
疯狂的狮子Li
35e38d5766 update 历史日志文件增加压缩 2025-09-22 11:06:24 +08:00
疯狂的狮子Li
0fff64da20 update warm-flow 升级 1.8.2-m2 2025-09-19 17:54:30 +08:00
疯狂的狮子Li
0082a60970 update 优化 更新ip2region.xdb文件 2025-09-19 17:46:14 +08:00
疯狂的狮子Li
92c13c23c9 update springboot 3.5.4 => 3.5.6 2025-09-19 14:13:17 +08:00
疯狂的狮子Li
cb83f9d9de update 增加 加密头用来判断数据是否已经被加密了 防止重复加密 2025-09-15 17:15:08 +08:00
疯狂的狮子Li
cce5f53b7a update 增加 加密头用来判断数据是否已经被加密了 防止重复加密 2025-09-15 17:12:27 +08:00
疯狂的狮子Li
8512b66746 update 生成模板前端增加fixed 2025-09-15 15:46:30 +08:00
疯狂的狮子Li
43efa94af9 update springboot-admin 3.5.1 => 3.5.3
update springdoc 2.8.11 => 2.8.13
update mybatis-plus 3.5.12 => 3.5.14
update mapstruct-plus 1.4.8 => 1.5.0
update sms4j 3.3.4 => 3.3.5
update rocketmq-client 2.3.0 => 2.3.4
2025-09-15 12:04:51 +08:00
疯狂的狮子Li
7fe1d90641 update 优化 将dubbo redis集群配置移动到common配置文件里便于修改 2025-09-12 15:15:17 +08:00
疯狂的狮子Li
3b2854adb8 update 优化 增加节点悬浮提示配置开关 2025-09-04 17:41:03 +08:00
疯狂的狮子Li
0d994c97b9 update 优化 SysMenu 的 selectObjs 查询 2025-09-04 17:38:46 +08:00
疯狂的狮子Li
61a4b96831 fix 修复 自定义sql在pg数据库类型异常问题 2025-09-04 15:48:23 +08:00
疯狂的狮子Li
52daba0b36 update 优化 全局日期格式转换逻辑 2025-09-04 15:47:11 +08:00
疯狂的狮子Li
c2c778c0c3 update 优化 岗位页面查询权限问题 2025-09-03 14:16:22 +08:00
疯狂的狮子Li
8c315c0c4d fix 修复 StreamUtils 返回不可变类型报错问题 2025-09-02 16:00:23 +08:00
疯狂的狮子Li
9439ec88ab fix 修复 StreamUtils 返回不可变类型报错问题 2025-09-02 15:59:40 +08:00
疯狂的狮子Li
1dc9bfe304 fix 修复 StreamUtils 返回不可变类型报错问题 2025-09-02 15:52:33 +08:00
疯狂的狮子Li
0122a09c27 fix 修复 工作流通知messageType参数判空逻辑错误的问题 2025-09-02 14:49:02 +08:00
疯狂的狮子Li
6551134460 update 优化 支持子菜单配置默认激活的父菜单activeMenu 2025-09-02 10:52:59 +08:00
疯狂的狮子Li
f6ddae57c4 Revert "update 优化 重写selectOne方法 自动拼接limit 1"
This reverts commit c73d3cdf890461c2db8c69a89608e1724b4dc92d.
2025-09-01 14:27:19 +08:00
疯狂的狮子Li
7316d05874 update 优化 oss远程接口增加扩展字段存储 2025-09-01 14:18:15 +08:00
疯狂的狮子Li
0766ef65c7 fix 修复 json模块配置 默认覆盖了spring module 配置问题 改为让spring自动加载注册 2025-09-01 12:04:19 +08:00
疯狂的狮子Li
66dea77421 add 新增 Excel工具类支持更灵活的自定义导出方式,以便用户分批处理导出数据 2025-09-01 12:03:33 +08:00
疯狂的狮子Li
6dcb3153c2 upadte 优化 Stream流工具类 2025-09-01 12:03:09 +08:00
疯狂的狮子Li
de6fffc6fe update 优化 工作流代码 2025-08-29 10:15:28 +08:00
疯狂的狮子Li
328f5dcdac update 优化 将返回值从bo改为vo 2025-08-29 10:11:16 +08:00
疯狂的狮子Li
4266517ead fix 修复 包名错误 2025-08-29 09:56:55 +08:00
疯狂的狮子Li
2db80ae6da update 优化 支持后端监听器解析节点扩展数据到流程变量(按钮权限 抄送人 扩展变量) 2025-08-28 18:10:39 +08:00
疯狂的狮子Li
971f0070f7 update 优化 支持前端返回节点扩展数据(按钮权限 抄送人 扩展变量) 2025-08-28 18:08:49 +08:00
疯狂的狮子Li
7c341548c4 update 解析扩展属性 JSON,构建 Node 扩展属性对象,增强代码可读性 2025-08-28 18:07:31 +08:00
疯狂的狮子Li
85160be7f7 update 添加抄送设置和变量枚举,优化扩展节点配置逻辑 2025-08-28 18:05:20 +08:00
疯狂的狮子Li
d41e373f8b update 优化 流程实例业务扩展的保存和删除逻辑,增强代码可读性 2025-08-28 18:03:01 +08:00
疯狂的狮子Li
fc6d45d8c9 fix 修复 菜单与部门 未做角色状态判断 2025-08-27 17:59:46 +08:00
疯狂的狮子Li
bf0130dea6 update hutool 5.8.38 => 5.8.40 默认支持了验证码不生成负数 2025-08-27 17:57:56 +08:00
疯狂的狮子Li
fbe9cf506b upadte 优化 Excel单元格合并处理器代码逻辑 2025-08-27 17:56:30 +08:00
疯狂的狮子Li
3151741d87 fix 修复 时间解析类异常问题 2025-08-27 17:55:49 +08:00
疯狂的狮子Li
1ebb552d7f update springdoc 2.8.10 => 2.8.11
update redisson 3.50.0 => 3.51.0
update fastexcel 1.2.0 => 1.3.0
2025-08-25 13:53:54 +08:00
疯狂的狮子Li
426a6c484e update 优化 对三方授权 redirectUri 回调地址进行url编码 2025-08-25 13:52:57 +08:00
疯狂的狮子Li
0c5173c388 update 更新 warm-flow 版本至 1.8.1 2025-08-25 13:52:35 +08:00
疯狂的狮子Li
4ae432713b update 优化 代码生成模板空格对齐 2025-08-25 13:52:05 +08:00
疯狂的狮子Li
d425383d38 update 优化 移除不必要的流程状态颜色配置 2025-08-25 13:51:10 +08:00
疯狂的狮子Li
c710b6365e update springdoc 2.8.9 => 2.8.10 2025-08-21 10:09:34 +08:00
疯狂的狮子Li
415fb76139 reset 回滚错误提交 2025-08-21 09:25:20 +08:00
疯狂的狮子Li
ee68904d59 fix 修复 校验租户账号余额 查询语句错误 2025-08-21 09:21:47 +08:00
疯狂的狮子Li
50fd75bfdd fix 修复 流程重新提交报错问题 2025-08-19 18:02:03 +08:00
疯狂的狮子Li
29af0a1423 add 增加 请假表 申请编号字段sql 2025-08-18 09:44:29 +08:00
疯狂的狮子Li
b3e7bef603 add 增加 流程扩展表sql 2025-08-16 11:48:11 +08:00
疯狂的狮子Li
943d8b0f6f add 增加 流程业务扩展表 flow_instance_biz_ext 2025-08-16 11:41:22 +08:00
疯狂的狮子Li
0ed79627b6 update 优化 验证码校验逻辑 2025-08-15 16:30:33 +08:00
疯狂的狮子Li
643fead8f4 update 优化 验证码校验逻辑 2025-08-15 14:04:48 +08:00
疯狂的狮子Li
e67399c2c7 update 优化 工作流后台发起或审批可以手动设置办理人 2025-08-15 11:29:09 +08:00
Lau
e878adb6e8 !237 update 逻辑删除注释更正改为1
* update 逻辑删除注释更正改为1
2025-08-14 11:12:59 +00:00
花生米
d1d5336762 !236 build(docker): 为 ELK 服务添加时区设置
* build(docker): 为 ELK 服务添加时区设置
2025-08-14 09:35:23 +00:00
疯狂的狮子Li
1b07733a12 update snailjob 1.7.0 => 1.7.2 2025-08-14 09:54:34 +08:00
疯狂的狮子Li
89549569d5 update warm-flow 升级 1.8.0 正式版 2025-08-14 09:54:10 +08:00
疯狂的狮子Li
f0056b0ce1 update seata-server 2.4.0 => 2.5.0 2025-08-13 11:39:17 +08:00
疯狂的狮子Li
c593ed5839 update warm-flow 升级 1.8.0-m3 2025-08-13 10:31:43 +08:00
疯狂的狮子Li
4b51157bc3 update 优化 数据库类型获取和判断逻辑,增强代码可读性 2025-08-13 10:31:06 +08:00
疯狂的狮子Li
d32ba1f92f update 优化 Excel单元格合并代码逻辑,明确处理类职责 2025-08-13 10:29:37 +08:00
疯狂的狮子Li
0daa00e24b update 优化 oss下载接口改用发布订阅的方式替代阻塞流,优化大文件下载时的内存占用 2025-08-12 16:50:26 +08:00
疯狂的狮子Li
14091133c4 update 优化 删除监控无用配置代码(升级之后不需要了) 2025-08-11 14:05:57 +08:00
疯狂的狮子Li
3cc8b1767e update 优化 由spring自己初始化线程池 2025-08-11 10:28:14 +08:00
疯狂的狮子Li
f6500b46f2 fix 修复 未注解字段导致列合并错位的问题 2025-08-07 17:41:00 +08:00
疯狂的狮子Li
8352f25fd9 update 优化 以逗号拼接元素 2025-08-06 18:16:24 +08:00
AprilWind
543be7a809 update 新增支持占位符格式的 ServiceException 构造方法
- 新增 ServiceException(String message, Object... args) 构造器,内部使用 Hutool StrFormatter.format 格式化消息
- 解决日志打印和异常抛出信息格式不统一的问题,统一使用 {} 占位符
- 优化异常消息书写,简化拼接,提升代码可读性和维护性
2025-08-06 11:03:01 +08:00
疯狂的狮子Li
e015970f79 add 增加赞助商 2025-08-06 10:21:27 +08:00
疯狂的狮子Li
b41ed6fd92 add 增加赞助商 2025-08-06 10:08:57 +08:00
疯狂的狮子Li
c8408ae750 fix 修复 撤销终止等操作 都变成退回的问题 2025-08-05 11:56:34 +08:00
疯狂的狮子Li
0f8ae82257 fix 修复 漏改代码 2025-08-04 17:18:02 +08:00
疯狂的狮子Li
a0519521a5 update 优化 调整变量修改 2025-08-04 16:11:57 +08:00
疯狂的狮子Li
986082eef3 fix 临时修复 warmflow的bug 2025-08-04 14:36:42 +08:00
疯狂的狮子Li
633252f730 update snailjob 1.6.0 => 1.7.0(新增的网卡配置有异常等待官方修复 不耽误正常使用) 2025-08-04 14:35:51 +08:00
疯狂的狮子Li
716ea1deff update work-flow 1.6.8 => 1.8.0-m1 2025-08-04 11:17:42 +08:00
疯狂的狮子Li
0eeb2a144b update 优化 办理人权限设置列表 2025-08-04 10:40:10 +08:00
疯狂的狮子Li
a8de8886ec update 全局替换为 Convert.toStr 优化 null 字符串处理 String.valueOf 这个方法有问题会把null变成"null"慎用 2025-08-04 10:33:23 +08:00
疯狂的狮子Li
78c6580e28 update 优化 升级jdk版本 2025-08-01 17:24:35 +08:00
疯狂的狮子Li
087f5d5058 update 优化 setCacheObject 简化写法 2025-08-01 16:33:07 +08:00
疯狂的狮子Li
9dc682ff03 fix 修复 最新版本gateway不传递X-Forwarded-Prefix请求头导致doc文档无法获取 2025-08-01 12:47:49 +08:00
疯狂的狮子Li
4a637756bb update 优化 isLogin 判断逻辑 2025-07-31 10:20:49 +08:00
疯狂的狮子Li
3f1e97da2b update 优化 PlusSaTokenDao 删除key同步删除本地缓存 2025-07-31 10:20:22 +08:00
疯狂的狮子Li
2bb9ec9899 fix 修复 依赖漏删 2025-07-31 09:22:11 +08:00
疯狂的狮子Li
5c6ff3fe54 fix 修复 监控代码报错问题 2025-07-30 16:58:47 +08:00
疯狂的狮子Li
64b7bd5b6c update 优化 去除nacos无用依赖 2025-07-30 16:38:43 +08:00
疯狂的狮子Li
4557bc30b6 update springboot 3.4.7 => 3.5.4
update springcloud 2024.0.0 => 2025.0.0
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 16:16:10 +08:00
疯狂的狮子Li
c73d3cdf89 update 优化 重写selectOne方法 自动拼接limit 1 2025-07-30 15:13:50 +08:00
疯狂的狮子Li
ba780cb444 update 优化 数据权限注解全部交给AOP处理,使用自定义动态方法匹配器匹配注解 2025-07-30 09:16:34 +08:00
疯狂的狮子Li
a2392acad6 fix 修复 snailjob 未判断配置空的情况 2025-07-29 14:28:17 +08:00
疯狂的狮子Li
44a5eb2ec9 update 优化 调整自动审批代码逻辑 2025-07-28 17:40:55 +08:00
疯狂的狮子Li
0f0fb92ff6 update 优化 getBackTaskNode 获取驳回节点接口 如果是委派直接返回当前节点 不允许驳回到其他节点 2025-07-28 17:32:59 +08:00
疯狂的狮子Li
36298c79f0 fix 修复 委托、转办时nextTasks为空导致空指针的问题 2025-07-28 17:31:59 +08:00
花生米
f8152410e3 !233 fix 日志不输出文件,add logback-common.xml
* fix 日志不输出文件,add logback-common.xml
2025-07-28 08:44:09 +00:00
疯狂的狮子Li
d5e4a069d4 update 增加国内文档加速地址 2025-07-28 13:38:42 +08:00
AprilWind
d68ff10e6f fix 修复权限判断逻辑 2025-07-26 16:59:12 +08:00
AprilWind
3cb0567b3b fix 修复权限判断逻辑 2025-07-26 16:42:13 +08:00
疯狂的狮子Li
35937624b2 fix 修复 个人中心数据被脱敏问题 2025-07-25 16:37:55 +08:00
疯狂的狮子Li
1bc23099aa fix 修复 权限为null导致报错问题 2025-07-25 15:48:26 +08:00
疯狂的狮子Li
36e18aa71e fix 修复 监听器变量使用错误 2025-07-24 15:24:10 +08:00
疯狂的狮子Li
30653c6d0f update 优化 调整上传超时时间 2025-07-23 17:47:49 +08:00
疯狂的狮子Li
f5729d040d add 增加 催办接口 调整消息发送 2025-07-22 18:15:26 +08:00
疯狂的狮子Li
7ab62a89be add 增加 修改流程变量接口 2025-07-22 18:11:04 +08:00
疯狂的狮子Li
e3b449e91e update 优化 getInfo 接口忽略数据权限 2025-07-22 18:09:01 +08:00
疯狂的狮子Li
4f42258f99 update 优化 删除无用代码 2025-07-22 10:33:21 +08:00
疯狂的狮子Li
e0f68ef605 remove 重大改动 删除sentinel所有相关功能(为什么删除 查看文档详细说明 https://plus-doc.dromara.org/#/questions/sentinel_404) 2025-07-21 16:40:56 +08:00
疯狂的狮子Li
2c93bdc5dd update 更新 snailjob 版本到1.6.0 2025-07-19 23:53:47 +08:00
疯狂的狮子Li
5f3597bffb update 新增赞助商 2025-07-19 23:48:40 +08:00
疯狂的狮子Li
3b06e02394 fix 修复 oracle数据库无法使用不等于语法问题 2025-07-17 16:19:35 +08:00
疯狂的狮子Li
946fb57116 update 优化 对登录也租户列表接口进行限流 防止盗刷 2025-07-17 15:16:18 +08:00
疯狂的狮子Li
9599f41f7e update 优化 增加请求流程后端发起demo案例 2025-07-17 14:39:03 +08:00
疯狂的狮子Li
d07b047dcc fix 修复 退回后审批记录申请人错误 #ICMEJ1 2025-07-17 14:36:35 +08:00
疯狂的狮子Li
967cc6e4f7 update 优化 支持在监听器设置流程变量 2025-07-15 17:03:57 +08:00
疯狂的狮子Li
bb51b61072 fix 修复 seata-server 缺少依赖问题 2025-07-14 15:56:26 +08:00
疯狂的狮子Li
7373a58dfb update 优化 菜单权限查询 2025-07-14 10:59:38 +08:00
疯狂的狮子Li
a899fefc13 fix 修复 设计器画线驳回到申请人后状态未修改 2025-07-14 09:35:25 +08:00
疯狂的狮子Li
792a4b7e37 update 优化 日志脱敏支持List参数 2025-07-11 18:14:56 +08:00
疯狂的狮子Li
ccacb64c47 update 优化 屏蔽无用警告 2025-07-11 15:55:13 +08:00
疯狂的狮子Li
304fa68276 update 优化 判断流程是否已结束 2025-07-10 17:40:25 +08:00
疯狂的狮子Li
33bd7c11a1 update 优化 任务创建监听器 使用下一个节点的任务数据 2025-07-10 17:07:17 +08:00
疯狂的狮子Li
df8fa77e63 update 优化 增加指定全局使用ip网段配置 并默认关闭sentinel使用 2025-07-09 17:15:26 +08:00
疯狂的狮子Li
5d2156cb5e update 优化 工作流任务创建监听器 传递流程参数 2025-07-09 10:41:08 +08:00
疯狂的狮子Li
4244567d2e update 优化 监控使用springSecurity新语法 2025-07-09 09:57:22 +08:00
疯狂的狮子Li
29a78eba27 fix 修复 代码生成 setIsRequired 标志写反 2025-07-09 09:36:45 +08:00
疯狂的狮子Li
b54bece04d update 优化 流程分类新增修改 2025-07-08 18:16:55 +08:00
疯狂的狮子Li
38feed5469 update 优化 SpEL表达式回显查询条件 2025-07-08 17:50:41 +08:00
疯狂的狮子Li
066d48f7b3 fix 修复 satokenDao 无法更新已存在数据的ttl问题 2025-07-08 14:34:30 +08:00
疯狂的狮子Li
999203665a update 优化 sse 登录校验 避免大量报错 2025-07-07 15:46:07 +08:00
疯狂的狮子Li
28daad748d update 优化 sql版本号更新 2025-07-07 15:29:37 +08:00
疯狂的狮子Li
1a6460bcc3 update 优化 删除无用接口 2025-07-07 15:28:28 +08:00
疯狂的狮子Li
d48bbab086 update 优化 接口防重和加锁 2025-07-07 09:29:30 +08:00
疯狂的狮子Li
beb7c55757 update 优化 租户字典同步逻辑代码并添加注释 2025-07-07 09:20:03 +08:00
疯狂的狮子Li
7141e855bb update 优化 SpEL表达式回显名称 2025-07-07 09:18:42 +08:00
疯狂的狮子Li
b77152357f update 优化 补充工作流动态启用注解 2025-07-07 09:15:58 +08:00
MichelleChung
a4d21e06c0 !229 新增工作流扩展spel表达式
* add: 新增 FlowSpel 相关 sql 脚本 ;
2025-07-06 07:52:47 +00:00
疯狂的狮子Li
77a245c13b update 优化根部门不允许删除以及办理人权限名称回显 2025-07-06 15:38:29 +08:00
疯狂的狮子Li
edac6074fb fix 修复 easy-es 启动报错问题 2025-07-06 15:16:46 +08:00
疯狂的狮子Li
2656fcc956 update 优化 删除无用注释 导致代码报null问题 2025-07-06 10:51:13 +08:00
疯狂的狮子Li
7488b091bc update 优化 校验角色是否有数据权限 2025-07-06 10:50:23 +08:00
疯狂的狮子Li
483107955e update 优化 删除无用注释 2025-07-04 18:20:44 +08:00
疯狂的狮子Li
8dc266055c update 优化 发号器工具类便利性优化 2025-07-04 17:48:07 +08:00
疯狂的狮子Li
09c484f496 fix 修复 错误修改导致页面逻辑错误 2025-07-04 17:43:26 +08:00
疯狂的狮子Li
348e8eb5fe update 优化 代码增加注释与深化权限判断 2025-07-04 17:02:16 +08:00
疯狂的狮子Li
753b456b4e update 优化 增加岗位修改校验 2025-07-04 15:31:04 +08:00
疯狂的狮子Li
f852949c22 update 重构用户 角色 部门 菜单的数据权限设计逻辑更符合实际业务场景与优化查询写法提高效率 2025-07-04 15:01:06 +08:00
疯狂的狮子Li
e668e524b2 update 优化 屏蔽掉无用接口 2025-07-04 09:35:02 +08:00
疯狂的狮子Li
8a2e970c54 update 优化 修改类名 避免无用扫描 2025-07-04 09:20:06 +08:00
疯狂的狮子Li
39079e53aa update 优化 优化工作流代码写法 2025-07-03 13:44:59 +08:00
疯狂的狮子Li
c690a30221 update 优化 增加 SysOssExt 附件扩展字段对象 2025-07-03 10:38:46 +08:00
疯狂的狮子Li
e93a2662a7 update 优化 流程图按审批人分组去重 2025-07-03 10:38:46 +08:00
疯狂的狮子Li
54f04c3cdf update 优化 工作流待办任务查询 2025-07-03 10:38:45 +08:00
AprilWind
0b8b47d857 update 修复分页数据缺少code 2025-07-02 17:27:32 +08:00
疯狂的狮子Li
c846fad872 fix 修复 有某些无聊人士 对一个demo案例提漏洞 CVE-2025-6925 2025-07-02 14:36:29 +08:00
疯狂的狮子Li
df7f282e41 update 优化 工作流代码小改动 2025-07-02 13:47:54 +08:00
疯狂的狮子Li
06b145cb83 update 优化 重构代码逻辑 封装简化方法 2025-07-02 13:45:53 +08:00
疯狂的狮子Li
8ed00aed21
!228 发布 2.4.1 小步迭代修复问题
Merge pull request !228 from 疯狂的狮子Li/dev
2025-07-01 01:13:16 +00:00
疯狂的狮子Li
c519815fd4 🐳🐳🐳发布 2.4.1 小步迭代修复问题 2025-07-01 09:11:21 +08:00
疯狂的狮子Li
cd3a831213 update spring-cloud-alibaba 2023.0.1.2 => 2023.0.3.3
update dubbo 3.3.4 => 3.3.5
2025-06-30 16:30:55 +08:00
疯狂的狮子Li
3b58e0952a fix 修复 修改数据权限漏改语句 2025-06-30 16:30:12 +08:00
疯狂的狮子Li
61ea1b3354 update seata-server 2.3.0 => 2.4.0 (注意此版本改动较多 升级需要严格根据提交记录处理) 2025-06-30 15:46:13 +08:00
疯狂的狮子Li
c39a816689 update 升级 nacos seata sentinel 的tomcat版本到稳定版本 2025-06-30 14:41:32 +08:00
疯狂的狮子Li
4ad0969f0a fix 修复 工作流依赖错误删除问题 2025-06-30 13:17:34 +08:00
疯狂的狮子Li
c64410d0ae update 优化 使用新版数据权限 2025-06-30 10:52:34 +08:00
疯狂的狮子Li
d37a00497d update 优化代码小改动 2025-06-30 10:45:49 +08:00
疯狂的狮子Li
1ab8750cd5 update 优化 Redis缓存监控接口 手动归还连接给连接池 提高效率 2025-06-27 10:56:46 +08:00
疯狂的狮子Li
77dcf4b0d4 update 优化代码 2025-06-27 10:56:09 +08:00
疯狂的狮子Li
af04bc74ed update 优化构建多根节点的树结构(支持多个顶级节点) 2025-06-26 15:17:47 +08:00
疯狂的狮子Li
ee7450c0e8 fix 修复 excel 备注与必填注解指定下标位置问题 去除下标跟随主要注解顺序 2025-06-26 15:15:47 +08:00
疯狂的狮子Li
6e7ecc96cb fix 修复 删除错误的注解导致前端时间不显示问题 2025-06-26 09:54:48 +08:00
疯狂的狮子Li
72001f721e update 优化 删除之前临时修复bug的类 satoken官方已经修复 2025-06-25 17:14:15 +08:00
疯狂的狮子Li
d830a7d5cf update 优化 从工作流api模块去除bus依赖 由使用者主动增加依赖 2025-06-25 17:12:24 +08:00
疯狂的狮子Li
f50d7e85c0 update 优化 全局日期格式转换配置,提升日期参数解析兼容性 2025-06-25 10:16:43 +08:00
疯狂的狮子Li
0aabd18e1c fix 修复 超时时间单位设置错误 应该是毫秒 2025-06-24 11:43:40 +08:00
疯狂的狮子Li
249d7bdcde fix 修复 超时时间单位设置错误 应该是毫秒 2025-06-24 11:42:50 +08:00
疯狂的狮子Li
1c126dc5d3 reset 回滚aws-s3版本 有未知问题 2025-06-24 11:25:15 +08:00
疯狂的狮子Li
84f0bc1832 fix 修复 办理任务时未传参数,导致执行任务无法获取到任务参数的问题 2025-06-23 16:58:50 +08:00
疯狂的狮子Li
d2f43e7f95 fix 修复 升级anyline返回值类型变更导致问题 2025-06-23 14:56:17 +08:00
疯狂的狮子Li
4263ccef62 fix 修复 升级anyline返回值类型变更导致问题 2025-06-23 14:42:47 +08:00
疯狂的狮子Li
cf64f2139f update 优化 sse 超时时间设置为一天 避免连接之后直接关闭浏览器导致连接停滞 2025-06-22 16:41:02 +08:00
疯狂的狮子Li
17c94cdc1a update 更新工作流sql(小改动) 2025-06-20 11:01:28 +08:00
疯狂的狮子Li
679c83e837 update warm-flow1.7.3 => 1.7.4 支持流程图悬浮窗 2025-06-20 10:43:09 +08:00
疯狂的狮子Li
a9f10e4fa4 update spring-boot 3.4.6 => 3.4.7
update satoken 1.42.0 => 1.44.0
update hutool 5.8.35 => 5.8.38
update redisson 3.45.1 => 3.50.0
update aws-s3 2.28.22 => 2.31.67
update anyline 8.7.2-20250101 => 8.7.2-20250603
update maven-compiler-plugin 3.11.0 => 3.14.0
update maven-surefire-plugin 3.1.2 => 3.5.3
2025-06-20 10:31:37 +08:00
疯狂的狮子Li
a6ab750508 update 优化 去除自动注入日志警告改为默认值 避免一大堆人去定时任务搞什么登录 2025-06-20 09:32:10 +08:00
疯狂的狮子Li
1efe1ac78a update 优化 加密模块 解密拦截器 将参数一起解密了 防止参数被多次加密不正常 2025-06-17 11:08:21 +08:00
疯狂的狮子Li
cb545862f4 update 优化 工作流设计器获取任务执行人默认正常状态 2025-06-17 11:05:10 +08:00
疯狂的狮子Li
d26147fb61 update 优化工作流,跳过以 $ 或 # 开头的内置变量表达式解析 2025-06-17 09:32:35 +08:00
疯狂的狮子Li
f26ef3515a fix 修复 snailjob的oracle.sql书写错误 2025-06-16 11:28:46 +08:00
疯狂的狮子Li
ef013bd653 update 删除无用功能 2025-06-13 14:25:16 +08:00
疯狂的狮子Li
751e86298f update 优化 去除snailjob的jvm参数 默认不限制 2025-06-12 17:40:42 +08:00
疯狂的狮子Li
6738d68fa4 update 优化 去除正则校验 无用配置导致问题 2025-06-09 14:54:46 +08:00
疯狂的狮子Li
d97146de98 update 优化 租户套餐菜单查询过滤掉 租户管理相关菜单 2025-06-05 18:29:12 +08:00
疯狂的狮子Li
ca7c0c94f3 update 优化 忽略租户表判断改为精确匹配 2025-06-05 16:48:13 +08:00
疯狂的狮子Li
6d319b13ad update easy-es 2.1.0 => 3.0.0 2025-06-05 15:12:50 +08:00
疯狂的狮子Li
1321a1ec38 update 优化 将debian换为更新更契合的rockylinux(centos作者写的稳定) 升级jdk版本避免漏洞 2025-06-03 17:12:24 +08:00
疯狂的狮子Li
6d7fb33ae0 fix 修复 优化dubbo调用跟satoken无关的场景会报错无上下文问题 2025-06-03 16:11:30 +08:00
疯狂的狮子Li
faa9be7e4e fix 修复 部分数据库转移符解析问题导致路由不生效 统一改为使用单斜杠处理 2025-06-03 11:17:52 +08:00
疯狂的狮子Li
36ab89d582 fix 修复 satoken异步调用需要手动传递上下文 2025-06-03 10:12:28 +08:00
疯狂的狮子Li
66bda00007 fix 修复 justauth 官方代码bug 2025-05-31 00:10:57 +08:00
疯狂的狮子Li
f0ec0811fd fix 修复 demo需要传递 version 字段才能启用乐观锁 2025-05-31 00:09:21 +08:00
疯狂的狮子Li
a28f05c145 fix 修复 地址解析工具类报错#ICBHUQ 2025-05-31 00:08:14 +08:00
疯狂的狮子Li
6d7e8e3822 Merge remote-tracking branch 'origin/dev' into 2.X 2025-05-29 18:17:53 +08:00
疯狂的狮子Li
3f0d0ad82b fix 修复 流程数据重复更新 状态被覆盖 无法完成流程问题 2025-05-29 18:16:57 +08:00
疯狂的狮子Li
b7835ec5bd update 优化 给测试用户增加菜单权限(可不更新) 2025-05-29 17:24:56 +08:00
疯狂的狮子Li
cb4e9a2006 fix 修复 PermissionService 无实现类无法启动问题 2025-05-29 16:26:53 +08:00
疯狂的狮子Li
6fb975fe6b fix 修复 监听器 flowParams 为null报错问题 2025-05-29 16:26:53 +08:00
疯狂的狮子Li
1842103a18 fix 修复 PermissionService 无实现类无法启动问题 2025-05-29 16:19:00 +08:00
疯狂的狮子Li
66e1767979 fix 修复 监听器 flowParams 为null报错问题 2025-05-29 15:42:30 +08:00
疯狂的狮子Li
b87d59aec3
!226 发布 2.4.0 正式版
Merge pull request !226 from 疯狂的狮子Li/dev
2025-05-29 03:29:52 +00:00
疯狂的狮子Li
7cfd8b4d46 🐳🐳🐳发布 2.4.0 正式版 2025-05-29 11:29:13 +08:00
疯狂的狮子Li
16a366b23c update warmflow 升级到正式版 1.7.3 2025-05-29 11:24:41 +08:00
疯狂的狮子Li
81abbb204f update 优化 用户查询添加用户昵称条件 2025-05-29 11:24:25 +08:00
疯狂的狮子Li
4b19b384d3 update 优化 调整任务监听名称 2025-05-29 11:23:40 +08:00
疯狂的狮子Li
a37597ee7d update minio 更新到最新 RELEASE.2025-04-22T22-12-26Z  minio 最后一个未阉割版本 不能再进行升级 在往上的版本功能被阉割 2025-05-29 10:15:42 +08:00
疯狂的狮子Li
44342e32cb update 优化 禁止删除默认流程分类 2025-05-28 16:57:29 +08:00
疯狂的狮子Li
67be46bbb1 update 优化 删除重复执行的代码 2025-05-28 16:56:49 +08:00
疯狂的狮子Li
3bb020c8a9 update 优化 调整菜单顺序 相关菜单放到一起 2025-05-28 11:14:01 +08:00
疯狂的狮子Li
9c9710fa4d update 调整查询流程任务记录 2025-05-28 11:12:24 +08:00
疯狂的狮子Li
bdc5791a21 update 优化 为所有服务增加 实现类模块 2025-05-27 18:18:31 +08:00
疯狂的狮子Li
acb254c867 fix 修复 接口使用错误 2025-05-27 17:56:44 +08:00
疯狂的狮子Li
47a45ab1fc update 优化 工作流代码 2025-05-27 17:28:09 +08:00
疯狂的狮子Li
be0b27a8bc update 优化 删除工作流字体文件(不需要了 改成前端渲染了) 2025-05-27 17:20:09 +08:00
疯狂的狮子Li
92afe12288 update 优化 使用前端显示流程图方式和新增办理人转换接口 2025-05-27 17:19:54 +08:00
疯狂的狮子Li
90b508eaf3 update 优化 删除退回任务bo关于驳回的节点的非空校验 2025-05-27 17:17:03 +08:00
疯狂的狮子Li
f34a92c6a4 fix 修复 dubbo redis 元数据中心 报错找不到元数据问题 官方插件代码实现类有问题 2025-05-27 11:26:20 +08:00
疯狂的狮子Li
7ecb5f3826 update 优化 权限获取 增加用户登录了但是查询的loginId是别人的场景 2025-05-26 16:32:19 +08:00
疯狂的狮子Li
f15e8f1ffa fix 修复 解决通过loginId查询角色和菜单权限 而非当前用户时 报错问题 支持跨服务查询权限信息通过satoken的api 2025-05-26 16:20:48 +08:00
疯狂的狮子Li
01bf26d39f update 重构 ruoyi-common-dict 改为为 ruoyi-common-service-impl 定位变更为通用service实现模块 2025-05-26 16:15:09 +08:00
疯狂的狮子Li
bc99124cbd update minio 更新到最新 RELEASE.2025-05-24T17-08-30Z 2025-05-26 14:44:37 +08:00
疯狂的狮子Li
85567dde0c update mapstruct-plus 1.4.6 => 1.4.8 2025-05-26 14:44:05 +08:00
疯狂的狮子Li
fd285c9c38 update 优化 表格增加border 2025-05-26 14:12:25 +08:00
疯狂的狮子Li
dd77741f75 update springboot 3.4.5 => 3.4.6
update springdoc 2.8.5 => 2.8.8
update mybatis-plus 3.5.11 => 3.5.12
update springboot-admin 3.4.5 => 3.4.7
2025-05-26 14:11:21 +08:00
疯狂的狮子Li
47ed39879d update 优化 调整优化监听 2025-05-26 14:09:08 +08:00
疯狂的狮子Li
3727d04744 add 增加 logicflow流程图预览 2025-05-26 14:03:50 +08:00
疯狂的狮子Li
281d0d336a update 优化 调整流程监听 2025-05-23 18:21:17 +08:00
疯狂的狮子Li
88bec4aaea fix 修复 请假表单菜单sql 展示状态错误问题 2025-05-23 18:18:43 +08:00
疯狂的狮子Li
1917d7234d add 新增 批量级联删除菜单接口 2025-05-23 18:16:09 +08:00
疯狂的狮子Li
845a0e57de update 优化 代码生成ServiceImpl层增加日志注解 2025-05-23 18:13:00 +08:00
疯狂的狮子Li
48fb1d92f3 fix 修复 sql补全分号 2025-05-23 18:12:01 +08:00
疯狂的狮子Li
09c03bfc76 update 优化 新增发号器工具类方法 2025-05-22 18:00:49 +08:00
疯狂的狮子Li
488a5631f4 update 优化 统一请假日期字段格式处理 2025-05-22 17:47:43 +08:00
疯狂的狮子Li
5217bd6a1f update 优化 工作流创建事件 将状态交给业务方处理 2025-05-22 17:46:08 +08:00
疯狂的狮子Li
edd372f4e4 update 优化 动态路由迁移到菜单管理 2025-05-22 17:40:23 +08:00
疯狂的狮子Li
bb11ea218d add 新增 自定义字典值校验器 2025-05-20 14:23:04 +08:00
疯狂的狮子Li
92908f435f update snailjob 1.4.0 => 1.5.0 2025-05-20 13:43:35 +08:00
疯狂的狮子Li
baa93b5337 update 升级 JustAuth的钉钉和微信第三方登录 2025-05-20 13:37:21 +08:00
疯狂的狮子Li
21d00dfcc9 update 升级 warm-flow1.7.0 -> 1.7.2 2025-05-20 13:35:51 +08:00
疯狂的狮子Li
287d5cd5dc add 增加 成员项目地址 2025-05-19 17:09:49 +08:00
疯狂的狮子Li
f77994ba6c add 增加 成员项目地址 2025-05-19 16:06:27 +08:00
疯狂的狮子Li
6c10bc8860 fix 修复 新建租户未复制工作流相关数据问题 2025-05-15 13:22:02 +08:00
疯狂的狮子Li
01b137a08e update 优化 mysql建议版本升级到8.0.42 2025-05-14 18:09:11 +08:00
疯狂的狮子Li
0364cae19a update 优化 redis建议版本升级到7.2.8 2025-05-14 18:02:36 +08:00
疯狂的狮子Li
e83e0548d0 fix 修复 重构导致的问题 2025-05-12 18:27:41 +08:00
疯狂的狮子Li
ec7aa9035a update 优化 假分页方法 2025-05-12 18:24:30 +08:00
疯狂的狮子Li
1edc373b2b add 新增 版本更新sql文件 2025-05-12 13:02:58 +08:00
疯狂的狮子Li
2a413d13df add 新增 对接 gitea 三方单点登录 2025-05-12 12:58:57 +08:00
疯狂的狮子Li
74df9e2bc0 add 新增 自定义 Date 类型反序列化处理器(支持多种格式) 2025-05-12 12:56:45 +08:00
疯狂的狮子Li
c65f3e2f2c add 新增 http请求体读取异常与json解析异常处理 2025-05-12 12:52:54 +08:00
疯狂的狮子Li
8b17bd5d5d update 优化 重构办理人接口 2025-05-12 12:51:53 +08:00
疯狂的狮子Li
21521e4d80 update warm-flow 1.6.10 => 1.7.0 适配代码 2025-05-12 11:50:23 +08:00
疯狂的狮子Li
bf6f18d7b8 update 更新 readme 增加新成员项目 2025-05-12 09:33:49 +08:00
AprilWind
55e97d7edb docs 优化EncryptUtils加解密注释 2025-05-11 13:43:58 +08:00
疯狂的狮子Li
3b5559e562 update 更新 readme 2025-05-09 15:00:52 +08:00
疯狂的狮子Li
9f936349e1 update 优化 compose编排增加snailjob端口防止集群冲突 2025-05-08 23:20:26 +08:00
疯狂的狮子Li
520ac26b88 update 更新 readme 2025-05-08 22:15:21 +08:00
疯狂的狮子Li
dc0b36ac38 update 优化 直接从ClassPath加载ip2region数据库文件 2025-05-06 13:32:07 +08:00
疯狂的狮子Li
ca85b8c223 update 优化 多租户忽略表判断支持忽略大小写 2025-05-06 13:29:13 +08:00
疯狂的狮子Li
e2063ca3c7 update 优化 删除多余的常量 避免用户误解 2025-04-30 15:19:27 +08:00
疯狂的狮子Li
d7c3a7fd67 update 查询系统菜单列表新增菜单类型与父级ID查询条件 2025-04-29 16:31:10 +08:00
疯狂的狮子Li
81817ef631 update 调整 代码顺序 2025-04-28 09:37:46 +08:00
李志家
0e79b6a744 !225 feat(social): 实现 TopIAM 的 code 换取 token功能
* feat(social): 实现 TopIAM 的 code 换取 token功能
2025-04-28 01:36:40 +00:00
疯狂的狮子Li
ccd25a5d46 fix 修复 snailjob http basic验证判断错误 2025-04-27 22:10:21 +08:00
疯狂的狮子Li
2674a0cda2 update 调整sql执行顺序 2025-04-25 18:01:05 +08:00
疯狂的狮子Li
2cc792fe32 update springboot 3.4.4 => 3.4.5 2025-04-25 16:41:40 +08:00
疯狂的狮子Li
c3aa5c3aed update springboot 3.4.4 => 3.4.5 2025-04-25 16:40:31 +08:00
AprilWind
fcf71dee33 update 优化日期与字符串工具类 2025-04-25 13:46:07 +08:00
疯狂的狮子Li
ea429e79a9 update 删除已经不存在的功能 2025-04-25 09:52:12 +08:00
疯狂的狮子Li
72bc3b6227 fix 修复 错误命名 2025-04-25 09:28:58 +08:00
疯狂的狮子Li
baf0d3ed50 update 更新协议地址 2025-04-22 16:08:47 +08:00
疯狂的狮子Li
b8d634a933 update EasyExcel升级原作者FastExcel 2025-04-18 17:46:35 +08:00
疯狂的狮子Li
38fca0c0e5 fix 修复 excel 合并单元格在导出在最后一行无法合并时,之前的数据合并失效问题 2025-04-18 13:31:52 +08:00
AprilWind
98d3f66470 update 优化返回任务指派的列表增加时间查询条件 2025-04-18 12:01:36 +08:00
疯狂的狮子Li
99210f3511 update 优化 getNextNodeList 只获取中间节点用于审批 过滤其他无用节点 2025-04-17 14:01:06 +08:00
疯狂的狮子Li
b7562259a9 fix 修复 nacos 配置文件符号错误 2025-04-16 11:11:43 +08:00
疯狂的狮子Li
6628ead2ea fix 修复 临时处理satoken对dubbo内部调用的情况考虑不周 导致无法获取上下文都bug 2025-04-16 10:25:07 +08:00
疯狂的狮子Li
5f91db4cbe update 优化 缓存注解支持关闭本地缓存 2025-04-14 10:11:10 +08:00
疯狂的狮子Li
52295bdc20 update 优化 实体类统一使用包装类型 2025-04-14 09:37:18 +08:00
疯狂的狮子Li
acb701ad59 update warm-flow 1.6.8 => 1.6.10 2025-04-14 09:34:15 +08:00
疯狂的狮子Li
ad30965ef1 add 新增 一大堆snailjob的demo案例(感谢 老马) 2025-04-13 19:17:49 +08:00
AprilWind
dbc72dea3c update 优化工作流用户查询构建 2025-04-13 15:30:42 +08:00
AprilWind
a8da093a89 update 优化Mybatis异常处理器 2025-04-13 15:27:21 +08:00
AprilWind
d879f1c763 update 优化工作流权限按钮获取,若需要扩展更多按钮权限,只需在 sources 中新增对应的枚举类或字典类型 2025-04-13 15:26:20 +08:00
疯狂的狮子Li
9e1fb0e482 update satoken 1.40.0 => 1.42.0 适配所有升级项(改动较多)
SaLoginModel -> SaLoginParameter
device -> deviceType
satoken BCrypt -> hutool BCrypt(satoken不维护了)
SaTokenDao -> SaTokenDaoBySessionFollowObject(satoken做了重构封装)
sse 适配新satoken版本拦截器变化
2025-04-11 15:37:09 +08:00
疯狂的狮子Li
e7f553fe91 update 优化 统一流程demo 权限人分隔符 2025-04-11 15:08:43 +08:00
鞠聪
fcfa6dc976 !223 更新 RabbitMQ 优化发送和消费手动确认机制
* 更新 RabbitMQ 优化发送和消费手动确认机制
2025-04-10 10:04:53 +00:00
疯狂的狮子Li
465a9a25e9 update bouncycastle 1.76 => 1.80 2025-04-10 17:30:06 +08:00
疯狂的狮子Li
1a5e01cd96 fix 修复 临时解决sa-token使用秒 redis是毫秒导致1秒的精度问题 手动补偿(等satoken官方修复) 2025-04-10 17:23:55 +08:00
AprilWind
b849b00398 update 优化工作流获取流程变量 2025-04-10 17:14:27 +08:00
AprilWind
ac0dbde532 update 统一工作流FlowParams构造方式为建造者模式,提升代码可读性 2025-04-10 17:12:43 +08:00
AprilWind
1102573da3 update 优化工作流流程监听增加节点信息 2025-04-10 17:05:58 +08:00
AprilWind
43f24051fe update 优化工作流办理人权限处理器 2025-04-10 16:56:20 +08:00
疯狂的狮子Li
0264b5f7e9 update 调整注释 删除不存在的错误的东西 2025-04-09 10:25:37 +08:00
疯狂的狮子Li
6012dbdfa8 update 增加赞助商 2025-04-08 10:19:35 +08:00
疯狂的狮子Li
7c056c5090 update 优化 统一校验注解长度 2025-04-08 10:19:24 +08:00
疯狂的狮子Li
eec5940703 update 优化 增加api审批简化方法 2025-04-03 13:35:24 +08:00
疯狂的狮子Li
09fb5bb2da fix 修复选择弹窗会签人员后,会签审批出现每个任务的审批人都是选择的多人 https://gitee.com/dromara/RuoYi-Vue-Plus/issues/IBYCY7 2025-04-03 09:25:34 +08:00
疯狂的狮子Li
33e7faa1d1 fix 修复 在线用户设置过期时间与客户端不同步问题 2025-04-02 14:05:29 +08:00
疯狂的狮子Li
39e6ac9ed4 fix 修复 模板导出多个字段下拉值超过100个异常,采用多个sheet的方案解决。 2025-04-02 13:23:04 +08:00
疯狂的狮子Li
c6b5bb0652 update 优化 Dockerfile 构建文件新增暴露 snailjob 客户端端口 2025-04-01 14:34:26 +08:00
疯狂的狮子Li
32f59ba2ee update 优化 使用 record 简化vo代码 2025-03-31 11:30:45 +08:00
疯狂的狮子Li
3b28b04775 update 优化 seata注解使用新包名 2025-03-31 11:07:41 +08:00
疯狂的狮子Li
850dd7164b update 优化 FlwNodeExtServiceImpl 代码实现 2025-03-31 10:57:43 +08:00
疯狂的狮子Li
00356eaa05 update 优化 sse 删除之后 手动触发完成 防止内存泄漏 2025-03-31 09:42:54 +08:00
疯狂的狮子Li
35b8c6d8e3 update 优化 支持excel方法抛出json异常 2025-03-28 23:13:21 +08:00
疯狂的狮子Li
d45ba32876 Revert "update 优化 支持excel方法抛出json异常"
This reverts commit 6d15b209424e6710bd17591ee8fcf47375e41fc2.
2025-03-28 23:12:57 +08:00
疯狂的狮子Li
6d15b20942 update 优化 支持excel方法抛出json异常 2025-03-28 22:53:54 +08:00
483 changed files with 12262 additions and 5738 deletions

1
.gitignore vendored
View File

@ -37,6 +37,7 @@ nbdist/
######################################################################
# Others
*.log
*.log.gz
*.xml.versionsBackup
*.swp

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="ruoyi-sentinel-dashboard" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
<deployment type="dockerfile">
<settings>
<option name="imageTag" value="ruoyi/ruoyi-sentinel-dashboard:2.3.0" />
<option name="buildOnly" value="true" />
<option name="sourceFilePath" value="ruoyi-visual/ruoyi-sentinel-dashboard/Dockerfile" />
</settings>
</deployment>
<method v="2" />
</configuration>
</component>

View File

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

View File

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

View File

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

View File

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

81
pom.xml
View File

@ -13,55 +13,54 @@
<description>Dromara RuoYi-Cloud-Plus微服务系统</description>
<properties>
<revision>2.3.0</revision>
<revision>2.5.2</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version>
<spring-boot.version>3.4.4</spring-boot.version>
<spring-cloud.version>2024.0.0</spring-cloud.version>
<spring-boot-admin.version>3.4.5</spring-boot-admin.version>
<spring-boot.version>3.5.9</spring-boot.version>
<spring-cloud.version>2025.0.1</spring-cloud.version>
<spring-boot-admin.version>3.5.5</spring-boot-admin.version>
<mybatis.version>3.5.16</mybatis.version>
<mybatis-plus.version>3.5.11</mybatis-plus.version>
<mybatis-plus.version>3.5.14</mybatis-plus.version>
<p6spy.version>3.9.1</p6spy.version>
<dynamic-ds.version>4.3.1</dynamic-ds.version>
<velocity.version>2.3</velocity.version>
<swagger.core.version>2.2.28</swagger.core.version>
<springdoc.version>2.8.5</springdoc.version>
<swagger.core.version>2.2.38</swagger.core.version>
<springdoc.version>2.8.14</springdoc.version>
<therapi-javadoc.version>0.15.0</therapi-javadoc.version>
<easyexcel.version>4.0.3</easyexcel.version>
<hutool.version>5.8.35</hutool.version>
<redisson.version>3.45.1</redisson.version>
<fastexcel.version>1.3.0</fastexcel.version>
<hutool.version>5.8.40</hutool.version>
<redisson.version>3.52.0</redisson.version>
<lock4j.version>2.2.7</lock4j.version>
<snailjob.version>1.4.0</snailjob.version>
<satoken.version>1.40.0</satoken.version>
<lombok.version>1.18.36</lombok.version>
<snailjob.version>1.9.0</snailjob.version>
<satoken.version>1.44.0</satoken.version>
<lombok.version>1.18.40</lombok.version>
<logstash.version>7.4</logstash.version>
<easy-es.version>2.1.0</easy-es.version>
<easy-es.version>3.0.1</easy-es.version>
<elasticsearch-client.version>7.17.28</elasticsearch-client.version>
<skywalking-toolkit.version>9.3.0</skywalking-toolkit.version>
<bouncycastle.version>1.76</bouncycastle.version>
<mapstruct-plus.version>1.4.6</mapstruct-plus.version>
<bouncycastle.version>1.80</bouncycastle.version>
<mapstruct-plus.version>1.5.0</mapstruct-plus.version>
<mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
<justauth.version>1.16.7</justauth.version>
<!-- 离线IP地址定位库 -->
<ip2region.version>2.7.0</ip2region.version>
<ip2region.version>3.3.1</ip2region.version>
<!-- 临时修复 fastjson 漏洞 -->
<fastjson.version>1.2.83</fastjson.version>
<!-- OSS 配置 -->
<aws.sdk.version>2.28.22</aws.sdk.version>
<!-- SMS 配置 -->
<sms4j.version>3.3.4</sms4j.version>
<!-- 面向运行时的D-ORM依赖 -->
<anyline.version>8.7.2-20250101</anyline.version>
<!--工作流配置-->
<warm-flow.version>1.6.8</warm-flow.version>
<anyline.version>8.7.3-20251210</anyline.version>
<!-- 工作流配置 -->
<warm-flow.version>1.8.4</warm-flow.version>
<!-- mq配置 -->
<rocketmq.version>2.3.0</rocketmq.version>
<rocketmq.version>2.3.4</rocketmq.version>
<!-- 插件版本 -->
<maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
<maven-surefire-plugin.version>3.1.2</maven-surefire-plugin.version>
<maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version>
<maven-surefire-plugin.version>3.5.3</maven-surefire-plugin.version>
<flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version>
<!-- 打包默认跳过测试 -->
<skipTests>true</skipTests>
@ -139,13 +138,6 @@
<scope>import</scope>
</dependency>
<!-- JustAuth 的依赖配置-->
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
<version>${justauth.version}</version>
</dependency>
<!-- common 的依赖配置-->
<dependency>
<groupId>org.dromara</groupId>
@ -231,9 +223,9 @@
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>${easyexcel.version}</version>
<groupId>cn.idev.excel</groupId>
<artifactId>fastexcel</artifactId>
<version>${fastexcel.version}</version>
</dependency>
<!-- 代码生成使用模板 -->
@ -288,6 +280,18 @@
<version>${easy-es.version}</version>
</dependency>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>${elasticsearch-client.version}</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>${elasticsearch-client.version}</version>
</dependency>
<!-- skywalking 整合 logback -->
<dependency>
<groupId>org.apache.skywalking</groupId>
@ -326,6 +330,13 @@
<version>${sms4j.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

@ -15,7 +15,7 @@
</description>
<properties>
<revision>2.3.0</revision>
<revision>2.5.2</revision>
</properties>
<dependencyManagement>

View File

@ -41,4 +41,9 @@ public class RemoteFile implements Serializable {
*/
private String fileSuffix;
/**
* 扩展字段
*/
private String ext1;
}

View File

@ -1,5 +1,11 @@
package org.dromara.system.api;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Dict;
import java.math.BigDecimal;
import java.util.List;
/**
* 配置服务
*
@ -14,4 +20,88 @@ public interface RemoteConfigService {
*/
boolean selectRegisterEnabled(String tenantId);
/**
* 根据参数 key 获取参数值
*
* @param configKey 参数 key
* @return 参数值
*/
String getConfigValue(String configKey);
/**
* 根据参数 key 获取布尔值
*
* @param configKey 参数 key
* @return Boolean
*/
default Boolean getConfigBool(String configKey) {
return Convert.toBool(getConfigValue(configKey));
}
/**
* 根据参数 key 获取整数值
*
* @param configKey 参数 key
* @return Integer
*/
default Integer getConfigInt(String configKey) {
return Convert.toInt(getConfigValue(configKey));
}
/**
* 根据参数 key 获取长整型值
*
* @param configKey 参数 key
* @return Long
*/
default Long getConfigLong(String configKey) {
return Convert.toLong(getConfigValue(configKey));
}
/**
* 根据参数 key 获取 BigDecimal
*
* @param configKey 参数 key
* @return BigDecimal
*/
default BigDecimal getConfigDecimal(String configKey) {
return Convert.toBigDecimal(getConfigValue(configKey));
}
/**
* 根据参数 key 获取 Map 类型的配置
*
* @param configKey 参数 key
* @return Dict 对象如果配置为空或无法解析返回空 Dict
*/
Dict getConfigMap(String configKey);
/**
* 根据参数 key 获取 Map 类型的配置列表
*
* @param configKey 参数 key
* @return Dict 列表如果配置为空或无法解析返回空列表
*/
List<Dict> getConfigArrayMap(String configKey);
/**
* 根据参数 key 获取指定类型的配置对象
*
* @param configKey 参数 key
* @param clazz 目标对象类型
* @param <T> 目标对象泛型
* @return 对象实例如果配置为空或无法解析返回 null
*/
<T> T getConfigObject(String configKey, Class<T> clazz);
/**
* 根据参数 key 获取指定类型的配置列表
*
* @param configKey 参数 key
* @param clazz 目标元素类型
* @param <T> 元素类型泛型
* @return 指定类型列表如果配置为空或无法解析返回空列表
*/
<T> List<T> getConfigArray(String configKey, Class<T> clazz);
}

View File

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

View File

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

View File

@ -0,0 +1,21 @@
package org.dromara.system.api;
import java.util.List;
import java.util.Map;
/**
* 岗位服务
*
* @author Lion Li
*/
public interface RemotePostService {
/**
* 根据岗位 ID 列表查询岗位名称映射关系
*
* @param postIds 岗位 ID 列表
* @return Map其中 key 为岗位 IDvalue 为对应的岗位名称
*/
Map<Long, String> selectPostNamesByIds(List<Long> postIds);
}

View File

@ -0,0 +1,21 @@
package org.dromara.system.api;
import java.util.List;
import java.util.Map;
/**
* 角色服务
*
* @author Lion Li
*/
public interface RemoteRoleService {
/**
* 根据角色 ID 列表查询角色名称映射关系
*
* @param roleIds 角色 ID 列表
* @return Map其中 key 为角色 IDvalue 为对应的角色名称
*/
Map<Long, String> selectRoleNamesByIds(List<Long> roleIds);
}

View File

@ -8,6 +8,7 @@ import org.dromara.system.api.model.LoginUser;
import org.dromara.system.api.model.XcxLoginUser;
import java.util.List;
import java.util.Map;
/**
* 用户服务
@ -156,4 +157,12 @@ public interface RemoteUserService {
*/
List<RemoteUserVo> selectUsersByPostIds(List<Long> postIds);
/**
* 根据用户 ID 列表查询用户名称映射关系
*
* @param userIds 用户 ID 列表
* @return Map其中 key 为用户 IDvalue 为对应的用户名称
*/
Map<Long, String> selectUserNamesByIds(List<Long> userIds);
}

View File

@ -52,17 +52,17 @@ public class RemoteTaskAssigneeVo 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

@ -22,9 +22,12 @@
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<!-- 非必需模块 如果需要跟工作流同步数据 则需要在对应服务内引入bus模块 如果只是调用工作流api则不需要 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-bus</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

View File

@ -21,7 +21,7 @@ public interface RemoteWorkflowService {
* @param businessIds 业务id
* @return 结果
*/
boolean deleteInstance(List<Long> businessIds);
boolean deleteInstance(List<String> businessIds);
/**
* 获取当前流程状态
@ -85,4 +85,22 @@ public interface RemoteWorkflowService {
*/
boolean completeTask(RemoteCompleteTask completeTask);
/**
* 办理任务
*
* @param taskId 任务ID
* @param message 办理意见
* @return 结果
*/
boolean completeTask(Long taskId, String message);
/**
* 启动流程并办理第一个任务
*
* @param startProcess 参数
* @return 结果
*/
boolean startCompleteTask(RemoteStartProcess startProcess);
}

View File

@ -0,0 +1,83 @@
package org.dromara.workflow.api;
import lombok.extern.slf4j.Slf4j;
import org.dromara.workflow.api.domain.RemoteCompleteTask;
import org.dromara.workflow.api.domain.RemoteStartProcess;
import org.dromara.workflow.api.domain.RemoteStartProcessReturn;
import java.util.List;
import java.util.Map;
/**
* 工作流服务(降级处理)
*
* @author Lion Li
*/
@Slf4j
public class RemoteWorkflowServiceMock implements RemoteWorkflowService {
@Override
public boolean deleteInstance(List<String> businessIds) {
log.warn("服务调用异常 -> 降级处理");
return false;
}
@Override
public String getBusinessStatusByTaskId(Long taskId) {
log.warn("服务调用异常 -> 降级处理");
return null;
}
@Override
public String getBusinessStatus(String businessId) {
log.warn("服务调用异常 -> 降级处理");
return null;
}
@Override
public void setVariable(Long instanceId, Map<String, Object> variable) {
log.warn("服务调用异常 -> 降级处理");
}
@Override
public Map<String, Object> instanceVariable(Long instanceId) {
log.warn("服务调用异常 -> 降级处理");
return null;
}
@Override
public Long getInstanceIdByBusinessId(String businessId) {
log.warn("服务调用异常 -> 降级处理");
return null;
}
@Override
public void syncDef(String tenantId) {
log.warn("服务调用异常 -> 降级处理");
}
@Override
public RemoteStartProcessReturn startWorkFlow(RemoteStartProcess startProcess) {
log.warn("服务调用异常 -> 降级处理");
return null;
}
@Override
public boolean completeTask(RemoteCompleteTask completeTask) {
log.warn("服务调用异常 -> 降级处理");
return false;
}
@Override
public boolean completeTask(Long taskId, String message) {
log.warn("服务调用异常 -> 降级处理");
return false;
}
@Override
public boolean startCompleteTask(RemoteStartProcess startProcess) {
log.warn("服务调用异常 -> 降级处理");
return false;
}
}

View File

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

View File

@ -0,0 +1,45 @@
package org.dromara.workflow.api.domain;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 流程实例业务扩展对象
*
* @author may
* @date 2025-08-05
*/
@Data
public class RemoteFlowInstanceBizExt implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
private Long id;
/**
* 流程实例ID
*/
private Long instanceId;
/**
* 业务ID
*/
private String businessId;
/**
* 业务编码
*/
private String businessCode;
/**
* 业务标题
*/
private String businessTitle;
}

View File

@ -1,6 +1,7 @@
package org.dromara.workflow.api.domain;
import cn.hutool.core.util.ObjectUtil;
import lombok.Data;
import java.io.Serial;
@ -30,11 +31,21 @@ public class RemoteStartProcess implements Serializable {
*/
private String flowCode;
/**
* 办理人(可不填 用于覆盖当前节点办理人)
*/
private String handler;
/**
* 流程变量前端会提交一个元素{'entity': {业务详情数据对象}}
*/
private Map<String, Object> variables;
/**
* 流程业务扩展信息
*/
private RemoteFlowInstanceBizExt bizExt;
public Map<String, Object> getVariables() {
if (variables == null) {
return new HashMap<>(16);
@ -42,4 +53,12 @@ public class RemoteStartProcess implements Serializable {
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables;
}
public RemoteFlowInstanceBizExt getBizExt() {
if (ObjectUtil.isNull(bizExt)) {
bizExt = new RemoteFlowInstanceBizExt();
}
return bizExt;
}
}

View File

@ -30,13 +30,33 @@ public class ProcessEvent extends RemoteApplicationEvent {
*/
private String flowCode;
/**
* 实例id
*/
private Long instanceId;
/**
* 业务id
*/
private String businessId;
/**
* 状态
* 节点类型0开始节点 1中间节点 2结束节点 3互斥网关 4并行网关
*/
private Integer nodeType;
/**
* 流程节点编码
*/
private String nodeCode;
/**
* 流程节点名称
*/
private String nodeName;
/**
* 流程状态
*/
private String status;
@ -48,7 +68,7 @@ public class ProcessEvent extends RemoteApplicationEvent {
/**
* 当为true时为申请人节点办理
*/
private boolean submit;
private Boolean submit;
public ProcessEvent() {
super(new Object(), SpringUtils.getApplicationName(), DEFAULT_DESTINATION_FACTORY.getDestination(null));

View File

@ -6,15 +6,16 @@ import org.dromara.common.core.utils.SpringUtils;
import org.springframework.cloud.bus.event.RemoteApplicationEvent;
import java.io.Serial;
import java.util.Map;
/**
* 流程创建任务监听
* 流程任务监听
*
* @author may
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ProcessCreateTaskEvent extends RemoteApplicationEvent {
public class ProcessTaskEvent extends RemoteApplicationEvent {
@Serial
private static final long serialVersionUID = 1L;
@ -30,21 +31,46 @@ public class ProcessCreateTaskEvent extends RemoteApplicationEvent {
private String flowCode;
/**
* 审批节点编码
* 节点类型0开始节点 1中间节点 2结束节点 3互斥网关 4并行网关
*/
private Integer nodeType;
/**
* 流程节点编码
*/
private String nodeCode;
/**
* 流程节点名称
*/
private String nodeName;
/**
* 任务id
*/
private Long taskId;
/**
* 实例id
*/
private Long instanceId;
/**
* 业务id
*/
private String businessId;
public ProcessCreateTaskEvent() {
/**
* 流程状态
*/
private String status;
/**
* 办理参数
*/
private Map<String, Object> params;
public ProcessTaskEvent() {
super(new Object(), SpringUtils.getApplicationName(), DEFAULT_DESTINATION_FACTORY.getDestination(null));
}
}

View File

@ -1,6 +1,6 @@
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
FROM bellsoft/liberica-openjdk-debian:17.0.11-cds
#FROM bellsoft/liberica-openjdk-debian:21.0.5-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

@ -26,11 +26,6 @@
<artifactId>hutool-captcha</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-sentinel</artifactId>
</dependency>
<!-- RuoYi Common Security-->
<dependency>
<groupId>org.dromara</groupId>
@ -81,6 +76,12 @@
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-tenant</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-service-impl</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-api-resource</artifactId>

View File

@ -1,88 +0,0 @@
package org.dromara.auth.captcha;
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

@ -64,15 +64,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

@ -25,6 +25,8 @@ import org.dromara.common.core.domain.model.LoginBody;
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;
@ -41,6 +43,7 @@ import org.springframework.web.bind.annotation.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -102,8 +105,8 @@ public class TokenController {
Long userId = LoginHelper.getUserId();
scheduledExecutorService.schedule(() -> {
remoteMessageService.publishMessage(List.of(userId), "欢迎登录RuoYi-Cloud-Plus微服务管理系统");
}, 3, TimeUnit.SECONDS);
remoteMessageService.publishMessage(List.of(userId), DateUtils.getTodayHour(new Date()) + "好,欢迎登录 RuoYi-Cloud-Plus 后台管理系统");
}, 5, TimeUnit.SECONDS);
return R.ok(loginVo);
}
@ -190,6 +193,7 @@ public class TokenController {
*
* @return 租户列表
*/
@RateLimiter(time = 60, count = 20, limitType = LimitType.IP)
@GetMapping("/tenant/list")
public R<LoginTenantVo> tenantList(HttpServletRequest request) throws Exception {
// 返回对象

View File

@ -1,8 +1,8 @@
package org.dromara.auth.enums;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.captcha.generator.MathGenerator;
import cn.hutool.captcha.generator.RandomGenerator;
import org.dromara.auth.captcha.UnsignedMathGenerator;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -18,7 +18,7 @@ public enum CaptchaType {
/**
* 数字
*/
MATH(UnsignedMathGenerator.class),
MATH(MathGenerator.class),
/**
* 字符

View File

@ -27,6 +27,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

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

View File

@ -1,9 +1,8 @@
package org.dromara.auth.listener;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.listener.SaTokenListener;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import cn.hutool.core.convert.Convert;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
@ -37,7 +36,6 @@ import java.time.Duration;
@Slf4j
public class UserActionListener implements SaTokenListener {
private final SaTokenConfig tokenConfig;
@DubboReference
private RemoteUserService remoteUserService;
@DubboReference
@ -47,7 +45,7 @@ public class UserActionListener implements SaTokenListener {
* 每次登录时触发
*/
@Override
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginParameter loginParameter) {
UserAgent userAgent = UserAgentUtil.parse(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = ServletUtils.getClientIP();
SysUserOnline userOnline = new SysUserOnline();
@ -57,17 +55,17 @@ public class UserActionListener implements SaTokenListener {
userOnline.setOs(userAgent.getOs().getName());
userOnline.setLoginTime(System.currentTimeMillis());
userOnline.setTokenId(tokenValue);
String username = (String) loginModel.getExtra(LoginHelper.USER_NAME_KEY);
String tenantId = (String) loginModel.getExtra(LoginHelper.TENANT_KEY);
String username = (String) loginParameter.getExtra(LoginHelper.USER_NAME_KEY);
String tenantId = (String) loginParameter.getExtra(LoginHelper.TENANT_KEY);
userOnline.setUserName(username);
userOnline.setClientKey((String) loginModel.getExtra(LoginHelper.CLIENT_KEY));
userOnline.setDeviceType(loginModel.getDevice());
userOnline.setDeptName((String) loginModel.getExtra(LoginHelper.DEPT_NAME_KEY));
userOnline.setClientKey((String) loginParameter.getExtra(LoginHelper.CLIENT_KEY));
userOnline.setDeviceType(loginParameter.getDeviceType());
userOnline.setDeptName((String) loginParameter.getExtra(LoginHelper.DEPT_NAME_KEY));
TenantHelper.dynamic(tenantId, () -> {
if (tokenConfig.getTimeout() == -1) {
if (loginParameter.getTimeout() == -1) {
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, userOnline);
} else {
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, userOnline, Duration.ofSeconds(tokenConfig.getTimeout()));
RedisUtils.setCacheObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue, userOnline, Duration.ofSeconds(loginParameter.getTimeout()));
}
});
// 记录登录日志
@ -78,7 +76,7 @@ public class UserActionListener implements SaTokenListener {
logininforEvent.setMessage(MessageUtils.message("user.login.success"));
SpringUtils.context().publishEvent(logininforEvent);
// 更新登录信息
remoteUserService.recordLoginInfo((Long) loginModel.getExtra(LoginHelper.USER_KEY), ip);
remoteUserService.recordLoginInfo((Long) loginParameter.getExtra(LoginHelper.USER_KEY), ip);
log.info("user doLogin, useId:{}, token:{}", loginId, tokenValue);
}
@ -164,7 +162,7 @@ public class UserActionListener implements SaTokenListener {
* 每次Token续期时触发
*/
@Override
public void doRenewTimeout(String tokenValue, Object loginId, long timeout) {
public void doRenewTimeout(String loginType, Object loginId, String tokenValue, long timeout) {
}
}

View File

@ -1,7 +1,7 @@
package org.dromara.auth.service;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.secure.BCrypt;
import cn.hutool.crypto.digest.BCrypt;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
@ -174,7 +174,7 @@ public class SysLoginService {
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

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

View File

@ -1,8 +1,8 @@
package org.dromara.auth.service.impl;
import cn.dev33.satoken.secure.BCrypt;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.hutool.crypto.digest.BCrypt;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.stp.parameter.SaLoginParameter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
@ -66,8 +66,8 @@ public class PasswordAuthStrategy implements IAuthStrategy {
});
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
SaLoginModel model = new SaLoginModel();
model.setDevice(client.getDeviceType());
SaLoginParameter model = new SaLoginParameter();
model.setDeviceType(client.getDeviceType());
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
// 例如: 后台用户30分钟过期 app用户1天过期
model.setTimeout(client.getTimeout());
@ -98,7 +98,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

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@
<module>ruoyi-common-bom</module>
<module>ruoyi-common-alibaba-bom</module>
<module>ruoyi-common-log</module>
<module>ruoyi-common-dict</module>
<module>ruoyi-common-service-impl</module>
<module>ruoyi-common-excel</module>
<module>ruoyi-common-core</module>
<module>ruoyi-common-redis</module>
@ -32,7 +32,6 @@
<module>ruoyi-common-sms</module>
<module>ruoyi-common-logstash</module>
<module>ruoyi-common-elasticsearch</module>
<module>ruoyi-common-sentinel</module>
<module>ruoyi-common-skylog</module>
<module>ruoyi-common-prometheus</module>
<module>ruoyi-common-translation</module>

View File

@ -14,14 +14,12 @@
</description>
<properties>
<revision>2.3.0</revision>
<spring-cloud-alibaba.version>2023.0.1.2</spring-cloud-alibaba.version>
<sentinel.version>1.8.8</sentinel.version>
<seata.version>2.3.0</seata.version>
<revision>2.5.2</revision>
<spring-cloud-alibaba.version>2025.0.0.0</spring-cloud-alibaba.version>
<seata.version>2.5.0</seata.version>
<nacos.client.version>2.5.1</nacos.client.version>
<dubbo.version>3.3.4</dubbo.version>
<dubbo.version>3.3.6</dubbo.version>
<dubbo-extensions.version>3.3.1</dubbo-extensions.version>
<spring.context.support.version>1.0.11</spring.context.support.version>
</properties>
<dependencyManagement>
<dependencies>
@ -36,111 +34,20 @@
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>${nacos.client.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-parameter-flow-control</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-extension</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-apollo</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-zookeeper</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-redis</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-consul</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-web-servlet</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-reactor-adapter</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-cluster-server-default</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-cluster-client-default</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-webflux-adapter</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-api-gateway-adapter-common</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-webmvc-v6x-adapter</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-dubbo-adapter</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-apache-dubbo-adapter</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-apache-dubbo3-adapter</artifactId>
<version>${sentinel.version}</version>
<exclusions>
<exclusion>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-log4j2-adapter</artifactId>
</exclusion>
<exclusion>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-logback-adapter-12</artifactId>
</exclusion>
<exclusion>
<groupId>com.alibaba.nacos</groupId>
<artifactId>logback-adapter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.seata</groupId>
@ -177,12 +84,6 @@
<artifactId>dubbo-metadata-report-redis</artifactId>
<version>${dubbo-extensions.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.spring</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.context.support.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>

View File

@ -14,7 +14,7 @@
</description>
<properties>
<revision>2.3.0</revision>
<revision>2.5.2</revision>
</properties>
<dependencyManagement>
@ -54,10 +54,10 @@
<version>${revision}</version>
</dependency>
<!-- 字典 -->
<!-- 通用service实现模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-dict</artifactId>
<artifactId>ruoyi-common-service-impl</artifactId>
<version>${revision}</version>
</dependency>
@ -166,13 +166,6 @@
<version>${revision}</version>
</dependency>
<!-- 限流模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-sentinel</artifactId>
<version>${revision}</version>
</dependency>
<!-- skywalking日志收集模块 -->
<dependency>
<groupId>org.dromara</groupId>

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

@ -4,14 +4,11 @@ import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.Threads;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.core.task.VirtualThreadTaskExecutor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.*;
/**
* 线程池配置
@ -47,7 +44,7 @@ public class ThreadPoolConfig {
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
Threads.printException(r, t);
printException(r, t);
}
};
this.scheduledExecutorService = scheduledThreadPoolExecutor;
@ -56,15 +53,57 @@ public class ThreadPoolConfig {
/**
* 销毁事件
* 停止线程池
* 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务.
* 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数.
* 如果仍然超時則強制退出.
* 另对在shutdown时线程本身被调用中断做了处理.
*/
@PreDestroy
public void destroy() {
try {
log.info("====关闭后台任务任务线程池====");
Threads.shutdownAndAwaitTermination(scheduledExecutorService);
ScheduledExecutorService pool = scheduledExecutorService;
if (pool != null && !pool.isShutdown()) {
pool.shutdown();
try {
if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
pool.shutdownNow();
if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
log.info("Pool did not terminate");
}
}
} catch (InterruptedException ie) {
pool.shutdownNow();
Thread.currentThread().interrupt();
}
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
/**
* 打印线程异常信息
*/
public static void printException(Runnable r, Throwable t) {
if (t == null && r instanceof Future<?>) {
try {
Future<?> future = (Future<?>) r;
if (future.isDone()) {
future.get();
}
} catch (CancellationException ce) {
t = ce;
} catch (ExecutionException ee) {
t = ee.getCause();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
if (t != null) {
log.error(t.getMessage(), t);
}
}
}

View File

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

View File

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

View File

@ -71,4 +71,10 @@ public interface SystemConstants {
* 根部门祖级列表
*/
String ROOT_DEPT_ANCESTORS = "0";
/**
* 排除敏感属性字段
*/
String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };
}

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package org.dromara.common.core.exception;
import cn.hutool.core.text.StrFormatter;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -8,7 +9,7 @@ import lombok.NoArgsConstructor;
import java.io.Serial;
/**
* 业务异常
* 业务异常支持占位符 {}
*
* @author ruoyi
*/
@ -45,8 +46,8 @@ public final class ServiceException extends RuntimeException {
this.code = code;
}
public String getDetailMessage() {
return detailMessage;
public ServiceException(String message, Object... args) {
this.message = StrFormatter.format(message, args);
}
@Override
@ -54,10 +55,6 @@ public final class ServiceException extends RuntimeException {
return message;
}
public Integer getCode() {
return code;
}
public ServiceException setMessage(String message) {
this.message = message;
return this;

View File

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

View File

@ -1,5 +1,7 @@
package org.dromara.common.core.utils;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.dromara.common.core.enums.FormatsType;
import org.dromara.common.core.exception.ServiceException;
@ -175,14 +177,27 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
}
/**
* 计算两个日期之间的天数差以毫秒为单位
* 计算两个时间之间的时间差并以指定单位返回绝对值
*
* @param date1 第一个日期
* @param date2 第二个日期
* @return 两个日期之间的天数差的绝对值
* @param start 起始时间
* @param end 结束时间
* @param unit 所需返回的时间单位DAYSHOURSMINUTESSECONDSMILLISECONDSMICROSECONDSNANOSECONDS
* @return 时间差的绝对值以指定单位表示
*/
public static int differentDaysByMillisecond(Date date1, Date date2) {
return Math.abs((int) ((date2.getTime() - date1.getTime()) / (1000 * 3600 * 24)));
public static long difference(Date start, Date end, TimeUnit unit) {
// 计算时间差单位为毫秒取绝对值避免负数
long diffInMillis = Math.abs(end.getTime() - start.getTime());
// 根据目标单位转换时间差
return switch (unit) {
case DAYS -> diffInMillis / TimeUnit.DAYS.toMillis(1);
case HOURS -> diffInMillis / TimeUnit.HOURS.toMillis(1);
case MINUTES -> diffInMillis / TimeUnit.MINUTES.toMillis(1);
case SECONDS -> diffInMillis / TimeUnit.SECONDS.toMillis(1);
case MILLISECONDS -> diffInMillis;
case MICROSECONDS -> TimeUnit.MILLISECONDS.toMicros(diffInMillis);
case NANOSECONDS -> TimeUnit.MILLISECONDS.toNanos(diffInMillis);
};
}
/**
@ -280,8 +295,84 @@ 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());
}
}
/**
* 根据指定日期时间获取时间段凌晨 / 上午 / 中午 / 下午 / 晚上
*
* @param date 日期时间
* @return 时间段描述
*/
public static String getTodayHour(Date date) {
int hour = DateUtil.hour(date, true);
if (hour <= 6) {
return "凌晨";
} else if (hour < 12) {
return "上午";
} else if (hour == 12) {
return "中午";
} else if (hour <= 18) {
return "下午";
} else {
return "晚上";
}
}
/**
* 将日期格式化为仿微信的友好时间
* <p>
* 规则说明
* 1. 未来时间yyyy-MM-dd HH:mm
* 2. 今天
* - 1 分钟内刚刚
* - 1 小时内X 分钟前
* - 超过 1 小时凌晨/上午/中午/下午/晚上 HH:mm
* 3. 昨天昨天 HH:mm
* 4. 本周周X HH:mm
* 5. 今年内MM-dd HH:mm
* 6. 非今年yyyy-MM-dd HH:mm
*
* @param date 日期时间
* @return 格式化后的时间描述
*/
public static String formatFriendlyTime(Date date) {
if (date == null) {
return "";
}
Date now = DateUtil.date();
// 未来时间或非今年
if (date.after(now) || DateUtil.year(date) != DateUtil.year(now)) {
return parseDateToStr(FormatsType.YYYY_MM_DD_HH_MM, date);
}
// 今天
if (DateUtil.isSameDay(date, now)) {
long minutes = DateUtil.between(date, now, DateUnit.MINUTE);
if (minutes < 1) {
return "刚刚";
}
if (minutes < 60) {
return minutes + "分钟前";
}
return getTodayHour(date) + " " + DateUtil.format(date, "HH:mm");
}
// 昨天
if (DateUtil.isSameDay(date, DateUtil.yesterday())) {
return "昨天 " + DateUtil.format(date, "HH:mm");
}
// 本周
if (DateUtil.isSameWeek(date, now, true)) {
return DateUtil.dayOfWeekEnum(date).toChinese("")
+ " " + DateUtil.format(date, "HH:mm");
}
// 今年内其它时间
return DateUtil.format(date, "MM-dd HH:mm");
}
}

View File

@ -0,0 +1,87 @@
package org.dromara.common.core.utils;
import cn.hutool.core.util.DesensitizedUtil;
import cn.hutool.core.util.StrUtil;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* 脱敏工具类
*
* @author AprilWind
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class DesensitizedUtils extends DesensitizedUtil {
/**
* 灵活脱敏方法
*
* @param value 原始字符串
* @param prefixVisible 前面可见长度
* @param suffixVisible 后面可见长度
* @param maskLength 中间掩码长度固定显示多少 *如果总长度不足则自动缩减
* @return 脱敏后字符串
*/
public static String mask(String value, int prefixVisible, int suffixVisible, int maskLength) {
if (StrUtil.isBlank(value)) {
return value;
}
int len = value.length();
int prefixMaskLimit = prefixVisible + maskLength;
int fullLimit = prefixMaskLimit + suffixVisible;
// 规则 1长度 <= 中间掩码长度 全掩码
if (len <= maskLength) {
return StrUtil.repeat('*', len);
}
String mask = StrUtil.repeat('*', maskLength);
// 规则 2长度 <= 前缀 + 中间掩码
if (len <= prefixMaskLimit) {
return value.substring(0, len - maskLength) + mask;
}
String prefix = value.substring(0, prefixVisible);
// 规则 3长度 <= 前缀 + 中间掩码 + 后缀
if (len <= fullLimit) {
int suffixLen = len - prefixMaskLimit;
return prefix + mask + value.substring(len - suffixLen);
}
// 规则 4标准形态
return prefix + mask + value.substring(len - suffixVisible);
}
/**
* 高安全级别脱敏方法Token / 私钥
*
* @param value 原始字符串
* @param prefixVisible 前面可见长度推荐0~4
* @param suffixVisible 后面可见长度推荐0~4
* @return 脱敏后字符串
*/
public static String maskHighSecurity(String value, int prefixVisible, int suffixVisible) {
if (StrUtil.isBlank(value)) {
return value;
}
int len = value.length();
// 规则1长度 <= 前缀可见长度 全部掩码
if (len <= prefixVisible) {
return StrUtil.repeat('*', len);
}
// 规则2长度 <= 前缀 + 后缀可见长度 优先掩码后面
if (len <= prefixVisible + suffixVisible) {
return value.substring(0, len - prefixVisible) + StrUtil.repeat('*', prefixVisible);
}
// 规则3标准形态 前后可见中间全部掩码
return value.substring(0, prefixVisible)
+ StrUtil.repeat('*', len - prefixVisible - suffixVisible)
+ value.substring(len - suffixVisible);
}
}

View File

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

View File

@ -115,7 +115,7 @@ public class ServletUtils extends JakartaServletUtil {
public static Map<String, String> getParamMap(ServletRequest request) {
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

@ -30,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());
}
/**
@ -39,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);
}
/**
@ -53,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);
}
/**
@ -85,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));
}
/**
@ -99,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());
}
/**
@ -117,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));
}
/**
@ -136,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()));
}
/**
@ -153,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()));
}
@ -174,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())));
}
@ -192,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)));
}
@ -214,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 会导致序列化问题
@ -233,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());
@ -257,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

@ -6,6 +6,7 @@ import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.StrUtil;
import org.springframework.util.AntPathMatcher;
import java.nio.charset.Charset;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -259,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();
}
@ -339,4 +340,46 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils {
return false;
}
/**
* 将字符串从源字符集转换为目标字符集
*
* @param input 原始字符串
* @param fromCharset 源字符集
* @param toCharset 目标字符集
* @return 转换后的字符串
*/
public static String convert(String input, Charset fromCharset, Charset toCharset) {
if (isBlank(input)) {
return input;
}
try {
// 从源字符集获取字节
byte[] bytes = input.getBytes(fromCharset);
// 使用目标字符集解码
return new String(bytes, toCharset);
} catch (Exception e) {
return input;
}
}
/**
* 将可迭代对象中的元素使用逗号拼接成字符串
*
* @param iterable 可迭代对象 ListSet
* @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,64 +0,0 @@
package org.dromara.common.core.utils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.*;
/**
* 线程相关工具类.
*
* @author ruoyi
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Threads {
/**
* 停止线程池
* 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务.
* 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数.
* 如果仍然超時則強制退出.
* 另对在shutdown时线程本身被调用中断做了处理.
*/
public static void shutdownAndAwaitTermination(ExecutorService pool) {
if (pool != null && !pool.isShutdown()) {
pool.shutdown();
try {
if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
pool.shutdownNow();
if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
log.info("Pool did not terminate");
}
}
} catch (InterruptedException ie) {
pool.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
/**
* 打印线程异常信息
*/
public static void printException(Runnable r, Throwable t) {
if (t == null && r instanceof Future<?>) {
try {
Future<?> future = (Future<?>) r;
if (future.isDone()) {
future.get();
}
} catch (CancellationException ce) {
t = ce;
} catch (ExecutionException ee) {
t = ee.getCause();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
if (t != null) {
log.error(t.getMessage(), t);
}
}
}

View File

@ -10,6 +10,9 @@ import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.reflect.ReflectUtils;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -60,6 +63,38 @@ public class TreeBuildUtils extends TreeUtil {
return TreeUtil.build(list, parentId, DEFAULT_CONFIG, nodeParser);
}
/**
* 构建多根节点的树结构支持多个顶级节点
*
* @param list 原始数据列表
* @param getId 获取节点 ID 的方法引用例如node -> node.getId()
* @param getParentId 获取节点父级 ID 的方法引用例如node -> node.getParentId()
* @param parser 树节点属性映射器用于将原始节点 T 转为 Tree 节点
* @param <T> 原始数据类型如实体类DTO
* @param <K> 节点 ID 类型 LongString
* @return 构建完成的树形结构可能包含多个顶级根节点
*/
public static <T, K> List<Tree<K>> buildMultiRoot(List<T> list, Function<T, K> getId, Function<T, K> getParentId, NodeParser<T, K> parser) {
if (CollUtil.isEmpty(list)) {
return CollUtil.newArrayList();
}
// 提取所有节点 ID用于后续判断哪些节点为根节点 parentId 不在其中
Set<K> allIds = StreamUtils.toSet(list, getId);
// 筛选出所有 parentId 不在 allIds 中的节点这些节点的 parentId 可认为是根节点
Set<K> rootParentIds = list.stream()
.map(getParentId)
.filter(Objects::nonNull)
.filter(pid -> !allIds.contains(pid))
.collect(Collectors.toSet());
// 使用流处理遍历每个顶级 parentId构建对应树并合并为一个列表返回
return rootParentIds.stream()
.flatMap(rootParentId -> TreeUtil.build(list, rootParentId, parser).stream())
.collect(Collectors.toList());
}
/**
* 获取节点列表中所有节点的叶子节点
*

View File

@ -1,11 +1,11 @@
package org.dromara.common.core.utils.ip;
import cn.hutool.core.net.NetUtil;
import cn.hutool.http.HtmlUtil;
import org.dromara.common.core.utils.StringUtils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.NetUtils;
import org.dromara.common.core.utils.StringUtils;
/**
* 获取地址类
@ -16,18 +16,28 @@ import lombok.extern.slf4j.Slf4j;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class AddressUtils {
// 未知地址
public static final String UNKNOWN = "XX XX";
// 未知IP
public static final String UNKNOWN_IP = "XX XX";
// 内网地址
public static final String LOCAL_ADDRESS = "内网IP";
public static String getRealAddressByIP(String ip) {
if (StringUtils.isBlank(ip)) {
return UNKNOWN;
// 处理空串并过滤HTML标签
ip = HtmlUtil.cleanHtmlTag(StringUtils.blankToDefault(ip,""));
// 判断是否为IPv4
boolean isIPv4 = NetUtils.isIPv4(ip);
// 判断是否为IPv6
boolean isIPv6 = NetUtils.isIPv6(ip);
// 如果不是IPv4或IPv6则返回未知IP
if (!isIPv4 && !isIPv6) {
return UNKNOWN_IP;
}
// 内网不查询
ip = StringUtils.contains(ip, "0:0:0:0:0:0:0:1") ? "127.0.0.1" : HtmlUtil.cleanHtmlTag(ip);
if (NetUtil.isInnerIP(ip)) {
return "内网IP";
if ((isIPv4 && NetUtils.isInnerIP(ip)) || (isIPv6 && NetUtils.isInnerIPv6(ip))) {
return LOCAL_ADDRESS;
}
return RegionUtils.getCityInfo(ip);
// TipsIp2Region 提供了精简的IPv6地址库精简的IPv6地址库并不能完全支持IPv6地址的查询且准确度上可能会存在问题如需要准确的IPv6地址查询建议自行实现
return RegionUtils.getRegion(ip);
}
}

View File

@ -1,66 +1,158 @@
package org.dromara.common.core.utils.ip;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.util.ObjectUtil;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.file.FileUtils;
import cn.hutool.core.io.resource.ResourceUtil;
import lombok.extern.slf4j.Slf4j;
import org.lionsoul.ip2region.xdb.Searcher;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import org.lionsoul.ip2region.service.Config;
import org.lionsoul.ip2region.service.Ip2Region;
import org.lionsoul.ip2region.xdb.Util;
import java.io.File;
import java.io.InputStream;
import java.time.Duration;
/**
* 根据ip地址定位工具类离线方式
* 参考地址<a href="https://gitee.com/lionsoul/ip2region/tree/master/binding/java">集成 ip2region 实现离线IP地址定位库</a>
* IP地址行政区域工具类
* 参考地址<a href="https://gitee.com/lionsoul/ip2region/tree/master/binding/java">ip2region xdb java 查询客户端实现</a>
* xdb数据库文件下载<a href="https://gitee.com/lionsoul/ip2region/tree/master/data">ip2region data</a>
*
* @author lishuyan
* @author 秋辞未寒
*/
@Slf4j
public class RegionUtils {
private static final Searcher SEARCHER;
// 默认IPv4地址库文件路径
// 下载地址https://gitee.com/lionsoul/ip2region/blob/master/data/ip2region_v4.xdb
public static final String DEFAULT_IPV4_XDB_PATH = "ip2region_v4.xdb";
// 默认IPv6地址库文件路径
// 下载地址https://gitee.com/lionsoul/ip2region/blob/master/data/ip2region_v6.xdb
public static final String DEFAULT_IPV6_XDB_PATH = "ip2region_v6.xdb";
// 未知地址
public static final String UNKNOWN_ADDRESS = "未知";
// Ip2Region服务实例
private static Ip2Region ip2Region;
// 初始化Ip2Region服务实例
static {
String fileName = "/ip2region.xdb";
File existFile = FileUtils.file(FileUtil.getTmpDir() + FileUtil.FILE_SEPARATOR + fileName);
if (!FileUtils.exist(existFile)) {
ClassPathResource fileStream = new ClassPathResource(fileName);
if (ObjectUtil.isEmpty(fileStream.getStream())) {
throw new ServiceException("RegionUtils初始化失败原因IP地址库数据不存在");
try {
// 注意Ip2Region 的xdb文件加载策略 CachePolicy 有三种分别是BufferCache全量读取xdb到内存中VIndexCache默认策略按需读取并缓存NoCache实时读取
// 本项目工具使用的 CachePolicy BufferCacheBufferCache会加载整个xdb文件到内存中setXdbInputStream 仅支持 BufferCache 策略
// 因为加载整个xdb文件会耗费非常大的内存如果你不希望加载整个xdb到内存中更推荐使用 VIndexCache NoCache即实时读取文件策略和 setXdbPath/setXdbFile 加载方法需要注意的一点setXdbPath setXdbFile 不支持读取ClassPath即源码和resource目录中的文件
// 一般而言更建议把xdb数据库放到一个指定的文件目录中即不打包进jar包中然后使用 NoCache + 配合SearcherPool的并发池读取数据更方便随时更新xdb数据库
// TODO 2025年12月23日 Ip2Region封装的 InputStream 读取函数 Searcher.loadContentFromInputStream 在Linux环境下会申请过大的byte[]空间而导致OOM这里先用临时文件的方案解决等后续 Ip2Region 更新解决方案
// 创建临时文件
File v4TempXdb = FileUtil.writeFromStream(ResourceUtil.getStream(DEFAULT_IPV4_XDB_PATH), FileUtil.createTempFile());
// IPv4配置
Config v4Config = Config.custom()
.setCachePolicy(Config.BufferCache)
.setXdbFile(v4TempXdb)
// .setXdbInputStream(ResourceUtil.getStream(DEFAULT_IPV4_XDB_PATH))
.asV4();
// 删除临时文件
v4TempXdb.delete();
// IPv6配置
Config v6Config = null;
InputStream v6XdbInputStream = ResourceUtil.getStreamSafe(DEFAULT_IPV6_XDB_PATH);
if (v6XdbInputStream == null) {
log.warn("未加载 IPv6 地址库:未在类路径下找到文件 {}。当前仅启用 IPv4 查询。如需启用 IPv6请将 ip2region_v6.xdb 放置到 resources 目录", DEFAULT_IPV6_XDB_PATH);
} else {
// 创建临时文件
File v6TempXdb = FileUtil.writeFromStream(ResourceUtil.getStream(DEFAULT_IPV4_XDB_PATH), FileUtil.createTempFile());
v6Config = Config.custom()
.setCachePolicy(Config.BufferCache)
.setXdbFile(v6TempXdb)
// .setXdbInputStream(v6XdbInputStream)
.asV6();
// 删除临时文件
v6TempXdb.delete();
}
FileUtils.writeFromStream(fileStream.getStream(), existFile);
}
String dbPath = existFile.getPath();
// 1 dbPath 加载整个 xdb 到内存
byte[] cBuff;
try {
cBuff = Searcher.loadContentFromFile(dbPath);
// 初始化Ip2Region实例
RegionUtils.ip2Region = Ip2Region.create(v4Config, v6Config);
log.debug("IP工具初始化成功加载IP地址库数据成功");
} catch (Exception e) {
throw new ServiceException("RegionUtils初始化失败原因从ip2region.xdb文件加载内容失败" + e.getMessage());
}
// 2使用上述的 cBuff 创建一个完全基于内存的查询对象
try {
SEARCHER = Searcher.newWithBuffer(cBuff);
} catch (Exception e) {
throw new ServiceException("RegionUtils初始化失败原因" + e.getMessage());
throw new ServiceException("RegionUtils初始化失败原因{}", e.getMessage());
}
}
/**
* 根据IP地址离线获取城市
*
* @param ipString ip地址字符串
*/
public static String getCityInfo(String ip) {
public static String getRegion(String ipString) {
try {
ip = ip.trim();
// 3执行查询
String region = SEARCHER.search(ip);
return region.replace("0|", "").replace("|0", "");
String region = ip2Region.search(ipString);
if (StringUtils.isBlank(region)) {
region = UNKNOWN_ADDRESS;
}
return region;
} catch (Exception e) {
log.error("IP地址离线获取城市异常 {}", ip);
return "未知";
log.error("IP地址离线获取城市异常 {}", ipString);
return UNKNOWN_ADDRESS;
}
}
/**
* 根据IP地址离线获取城市
*
* @param ipBytes ip地址字节数组
*/
public static String getRegion(byte[] ipBytes) {
try {
String region = ip2Region.search(ipBytes);
if (StringUtils.isBlank(region)) {
region = UNKNOWN_ADDRESS;
}
return region;
} catch (Exception e) {
log.error("IP地址离线获取城市异常 {}", Util.ipToString(ipBytes));
return UNKNOWN_ADDRESS;
}
}
/**
* 关闭Ip2Region服务
*/
public static void close() {
if (ip2Region == null) {
return;
}
try {
ip2Region.close(10000);
} catch (Exception e) {
log.error("Ip2Region服务关闭异常", e);
}
}
/**
* 关闭Ip2Region服务
*
* @param timeout 关闭超时时间
*/
public static void close(final Duration timeout) {
if (ip2Region == null) {
return;
}
if (timeout == null) {
close();
return;
}
try {
ip2Region.close(timeout.toMillis());
} catch (Exception e) {
log.error("Ip2Region服务关闭异常", e);
}
}

View File

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

View File

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

View File

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

View File

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

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,51 +0,0 @@
package org.dromara.common.dict.utils;
import org.dromara.common.core.constant.CacheNames;
import org.dromara.common.redis.utils.CacheUtils;
import org.dromara.system.api.domain.vo.RemoteDictDataVo;
import java.util.List;
/**
* 字典工具类
*
* @author ruoyi
*/
public class DictUtils {
/**
* 设置字典缓存
*
* @param key 参数键
* @param dictDatas 字典数据列表
*/
public static void setDictCache(String key, List<RemoteDictDataVo> dictDatas) {
CacheUtils.put(CacheNames.SYS_DICT, key, dictDatas);
}
/**
* 获取字典缓存
*
* @param key 参数键
* @return dictDatas 字典数据列表
*/
public static List<RemoteDictDataVo> getDictCache(String key) {
return CacheUtils.get(CacheNames.SYS_DICT, key);
}
/**
* 删除指定字典缓存
*
* @param key 字典键
*/
public static void removeDictCache(String key) {
CacheUtils.evict(CacheNames.SYS_DICT, key);
}
/**
* 清空字典缓存
*/
public static void clearDictCache() {
CacheUtils.clear(CacheNames.SYS_DICT);
}
}

View File

@ -30,7 +30,7 @@ import org.springframework.context.annotation.Bean;
import java.util.*;
/**
* Swagger 文档配置
* 接口文档配置
*
* @author Lion Li
*/
@ -57,14 +57,15 @@ public class SpringDocAutoConfiguration {
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

@ -48,8 +48,6 @@ import static org.apache.dubbo.metadata.report.support.Constants.DEFAULT_METADAT
*/
public class RedisMetadataReport extends AbstractMetadataReport {
private static final int ONE_DAY_IN_MILLISECONDS = 86400000;
private static final String REDIS_DATABASE_KEY = "database";
private static final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(RedisMetadataReport.class);
@ -85,17 +83,17 @@ public class RedisMetadataReport extends AbstractMetadataReport {
@Override
protected void doStoreProviderMetadata(MetadataIdentifier providerMetadataIdentifier, String serviceDefinitions) {
this.storeMetadata(providerMetadataIdentifier, serviceDefinitions);
this.storeMetadata(providerMetadataIdentifier, serviceDefinitions, true);
}
@Override
protected void doStoreConsumerMetadata(MetadataIdentifier consumerMetadataIdentifier, String value) {
this.storeMetadata(consumerMetadataIdentifier, value);
this.storeMetadata(consumerMetadataIdentifier, value, true);
}
@Override
protected void doSaveMetadata(ServiceMetadataIdentifier serviceMetadataIdentifier, URL url) {
this.storeMetadata(serviceMetadataIdentifier, URL.encode(url.toFullString()));
this.storeMetadata(serviceMetadataIdentifier, URL.encode(url.toFullString()), false);
}
@Override
@ -114,7 +112,7 @@ public class RedisMetadataReport extends AbstractMetadataReport {
@Override
protected void doSaveSubscriberData(SubscriberMetadataIdentifier subscriberMetadataIdentifier, String urlListStr) {
this.storeMetadata(subscriberMetadataIdentifier, urlListStr);
this.storeMetadata(subscriberMetadataIdentifier, urlListStr, false);
}
@Override
@ -127,18 +125,22 @@ public class RedisMetadataReport extends AbstractMetadataReport {
return this.getMetadata(metadataIdentifier);
}
private void storeMetadata(BaseMetadataIdentifier metadataIdentifier, String v) {
private void storeMetadata(BaseMetadataIdentifier metadataIdentifier, String v, boolean ephemeral) {
if (pool != null) {
storeMetadataStandalone(metadataIdentifier, v);
storeMetadataStandalone(metadataIdentifier, v, ephemeral);
} else {
storeMetadataInCluster(metadataIdentifier, v);
storeMetadataInCluster(metadataIdentifier, v, ephemeral);
}
}
private void storeMetadataInCluster(BaseMetadataIdentifier metadataIdentifier, String v) {
private void storeMetadataInCluster(BaseMetadataIdentifier metadataIdentifier, String v, boolean ephemeral) {
try (JedisCluster jedisCluster =
new JedisCluster(jedisClusterNodes, timeout, timeout, 2, password, new GenericObjectPoolConfig<>())) {
jedisCluster.set(metadataIdentifier.getIdentifierKey() + META_DATA_STORE_TAG, v, jedisParams);
if (ephemeral) {
jedisCluster.set(metadataIdentifier.getIdentifierKey() + META_DATA_STORE_TAG, v, jedisParams);
} else {
jedisCluster.set(metadataIdentifier.getIdentifierKey() + META_DATA_STORE_TAG, v);
}
} catch (Throwable e) {
String msg =
"Failed to put " + metadataIdentifier + " to redis cluster " + v + ", cause: " + e.getMessage();
@ -147,9 +149,13 @@ public class RedisMetadataReport extends AbstractMetadataReport {
}
}
private void storeMetadataStandalone(BaseMetadataIdentifier metadataIdentifier, String v) {
private void storeMetadataStandalone(BaseMetadataIdentifier metadataIdentifier, String v, boolean ephemeral) {
try (Jedis jedis = pool.getResource()) {
jedis.set(metadataIdentifier.getUniqueKey(KeyTypeEnum.UNIQUE_KEY), v, jedisParams);
if (ephemeral) {
jedis.set(metadataIdentifier.getUniqueKey(KeyTypeEnum.UNIQUE_KEY), v, jedisParams);
} else {
jedis.set(metadataIdentifier.getUniqueKey(KeyTypeEnum.UNIQUE_KEY), v);
}
} catch (Throwable e) {
String msg = "Failed to put " + metadataIdentifier + " to redis " + v + ", cause: " + e.getMessage();
logger.error(TRANSPORT_FAILED_RESPONSE, "", "", msg, e);
@ -412,7 +418,7 @@ public class RedisMetadataReport extends AbstractMetadataReport {
@Override
public void publishAppMetadata(SubscriberMetadataIdentifier identifier, MetadataInfo metadataInfo) {
this.storeMetadata(identifier, metadataInfo.getContent());
this.storeMetadata(identifier, metadataInfo.getContent(), false);
}
@Override

View File

@ -20,18 +20,14 @@ dubbo:
parameters:
namespace: ${spring.profiles.active}
metadata-report:
address: redis://${spring.data.redis.host}:${spring.data.redis.port}
address: redis://${spring.data.redis.host:localhost}:${spring.data.redis.port:6379}
group: DUBBO_GROUP
username: dubbo
password: ${spring.data.redis.password}
# 集群开关
cluster: false
parameters:
namespace: ${spring.profiles.active}
database: ${spring.data.redis.database}
timeout: ${spring.data.redis.timeout}
# 集群地址 cluster 为 true 生效
backup: 127.0.0.1:6379,127.0.0.1:6381
# 消费者相关配置
consumer:
# 结果缓存(LRU算法)

View File

@ -1,84 +0,0 @@
package org.dromara.easyes.spring.config;
import lombok.Setter;
import org.dromara.easyes.common.property.EasyEsDynamicProperties;
import org.dromara.easyes.common.property.EasyEsProperties;
import org.dromara.easyes.common.strategy.AutoProcessIndexStrategy;
import org.dromara.easyes.common.utils.RestHighLevelClientUtils;
import org.dromara.easyes.core.index.AutoProcessIndexNotSmoothlyStrategy;
import org.dromara.easyes.core.index.AutoProcessIndexSmoothlyStrategy;
import org.dromara.easyes.spring.factory.IndexStrategyFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.Assert;
import java.util.Map;
/**
* @author MoJie
* @since 2.0
*/
@Setter
@Configuration
@ConditionalOnProperty(value = "easy-es.enable", havingValue = "true")
public class EasyEsConfiguration implements InitializingBean {
private EasyEsProperties easyEsProperties;
private EasyEsDynamicProperties easyEsDynamicProperties;
@Autowired
public EasyEsConfiguration(EasyEsProperties easyEsProperties, EasyEsDynamicProperties easyEsDynamicProperties) {
this.easyEsProperties = easyEsProperties;
this.easyEsDynamicProperties = easyEsDynamicProperties;
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.easyEsProperties, "easyEsProperties must is A bean. easy-es配置类必须给配置一个bean");
}
@Bean
public IndexStrategyFactory indexStrategyFactory() {
return new IndexStrategyFactory();
}
@Bean
public RestHighLevelClientUtils restHighLevelClientUtils() {
RestHighLevelClientUtils restHighLevelClientUtils = new RestHighLevelClientUtils();
if (this.easyEsDynamicProperties == null) {
this.easyEsDynamicProperties = new EasyEsDynamicProperties();
}
Map<String, EasyEsProperties> datasourceMap = this.easyEsDynamicProperties.getDatasource();
if (datasourceMap.isEmpty()) {
// 设置默认数据源,兼容不使用多数据源配置场景的老用户使用习惯
datasourceMap.put(RestHighLevelClientUtils.DEFAULT_DS, this.easyEsProperties);
}
for (String key : datasourceMap.keySet()) {
EasyEsProperties easyEsConfigProperties = datasourceMap.get(key);
RestHighLevelClientUtils.registerRestHighLevelClient(key, RestHighLevelClientUtils
.restHighLevelClient(easyEsConfigProperties));
}
return restHighLevelClientUtils;
}
/**
* 索引策略注册
*
* @return {@link AutoProcessIndexStrategy}
* @author MoJie
*/
@Bean
public AutoProcessIndexStrategy autoProcessIndexSmoothlyStrategy() {
return new AutoProcessIndexSmoothlyStrategy();
}
@Bean
public AutoProcessIndexStrategy autoProcessIndexNotSmoothlyStrategy() {
return new AutoProcessIndexNotSmoothlyStrategy();
}
}

View File

@ -1,27 +0,0 @@
package org.dromara.easyes.starter.config;
import org.dromara.easyes.core.config.GeneratorConfig;
import org.dromara.easyes.core.toolkit.Generator;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
/**
* 代码生成注册
* @author MoJie
* @since 2.0
*/
@Component
@ConditionalOnProperty(value = "easy-es.enable", havingValue = "true")
public class GeneratorConfiguration extends Generator {
@Autowired
private RestHighLevelClient client;
@Override
public Boolean generate(GeneratorConfig config) {
super.generateEntity(config, this.client);
return Boolean.TRUE;
}
}

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

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,22 @@
package org.dromara.common.excel.annotation;
import org.dromara.common.excel.core.ExcelOptionsProvider;
import java.lang.annotation.*;
/**
* Excel动态下拉选项注解
*
* @author Angus
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ExcelDynamicOptions {
/**
* 提供者类全限定名
* 实现org.dromara.common.excel.service.ExcelOptionsProvider实现类接口
*/
Class<? extends ExcelOptionsProvider> providerClass();
}

View File

@ -13,10 +13,6 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelNotation {
/**
* col index
*/
int index() default -1;
/**
* 批注内容
*/

View File

@ -15,10 +15,6 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelRequired {
/**
* col index
*/
int index() default -1;
/**
* 字体颜色
*/

View File

@ -2,12 +2,12 @@ package org.dromara.common.excel.convert;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import cn.idev.excel.converters.Converter;
import cn.idev.excel.enums.CellDataTypeEnum;
import cn.idev.excel.metadata.GlobalConfiguration;
import cn.idev.excel.metadata.data.ReadCellData;
import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
@ -28,7 +28,7 @@ public class ExcelBigNumberConvert implements Converter<Long> {
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
return null;
}
@Override

View File

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

View File

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

View File

@ -0,0 +1,220 @@
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;
}
private CellMergeHandler(final boolean hasTitle, final int rowIndex) {
this.hasTitle = hasTitle;
this.rowIndex = hasTitle ? rowIndex : 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) {
//如果是同一行则跳过合并
if (current + rowIndex == lastRow) {
continue;
}
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 是否合并标题
* @param rowIndex 行索引
* @return 单元格合并处理器
*/
public static CellMergeHandler of(final boolean hasTitle, final int rowIndex) {
return new CellMergeHandler(hasTitle, rowIndex);
}
/**
* 创建一个单元格合并处理器实例
*
* @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,25 +1,17 @@
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 com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.handler.WorkbookWriteHandler;
import com.alibaba.excel.write.handler.context.WorkbookWriteHandlerContext;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.SneakyThrows;
import cn.idev.excel.metadata.Head;
import cn.idev.excel.write.handler.SheetWriteHandler;
import cn.idev.excel.write.merge.AbstractMergeStrategy;
import cn.idev.excel.write.metadata.holder.WriteSheetHolder;
import cn.idev.excel.write.metadata.holder.WriteWorkbookHolder;
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.*;
import java.util.List;
/**
* 列值重复合并策略
@ -27,131 +19,47 @@ import java.util.*;
* @author Lion Li
*/
@Slf4j
public class CellMergeStrategy extends AbstractMergeStrategy implements WorkbookWriteHandler {
public class CellMergeStrategy extends AbstractMergeStrategy implements SheetWriteHandler {
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);
}
public CellMergeStrategy(List<?> list, boolean hasTitle, int rowIndex) {
this.cellList = CellMergeHandler.of(hasTitle, rowIndex).handle(list);
}
@Override
protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
//单元格写入了,遍历合并区域,如果该Cell在区域内,但非首行,则清空
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.isNotEmpty(cellList)){
for (CellRangeAddress item : cellList) {
context.getWriteContext().writeSheetHolder().getSheet().addMergedRegion(item);
}
public void afterSheetCreate(final WriteWorkbookHolder writeWorkbookHolder, final WriteSheetHolder writeSheetHolder) {
if (CollUtil.isEmpty(cellList)) {
return;
}
// Sheet 创建时提前写入合并区域后续写入只会影响首格不会移除合并
final Sheet sheet = writeSheetHolder.getSheet();
for (CellRangeAddress item : cellList) {
sheet.addMergedRegion(item);
}
}
@SneakyThrows
private List<CellRangeAddress> handle(List<?> list, boolean hasTitle) {
List<CellRangeAddress> cellList = new ArrayList<>();
if (CollUtil.isEmpty(list)) {
return cellList;
}
Field[] fields = ReflectUtils.getFields(list.get(0).getClass(), field -> !"serialVersionUID".equals(field.getName()));
// 有注解的字段
List<Field> mergeFields = new ArrayList<>();
List<Integer> mergeFieldsIndex = new ArrayList<>();
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
if (field.isAnnotationPresent(CellMerge.class)) {
CellMerge cm = field.getAnnotation(CellMerge.class);
mergeFields.add(field);
mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index());
if (hasTitle) {
ExcelProperty property = field.getAnnotation(ExcelProperty.class);
rowIndex = Math.max(rowIndex, property.value().length);
}
}
}
Map<Field, RepeatCell> map = new HashMap<>();
// 生成两两合并单元格
for (int i = 0; i < list.size(); i++) {
for (int j = 0; j < mergeFields.size(); j++) {
Field field = mergeFields.get(j);
Object val = ReflectUtils.invokeGetter(list.get(i), field.getName());
int colNum = mergeFieldsIndex.get(j);
if (!map.containsKey(field)) {
map.put(field, new RepeatCell(val, i));
} else {
RepeatCell repeatCell = map.get(field);
Object cellValue = repeatCell.getValue();
if (cellValue == null || "".equals(cellValue)) {
// 空值跳过不合并
continue;
}
if (!cellValue.equals(val)) {
if ((i - repeatCell.getCurrent() > 1)) {
cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
}
map.put(field, new RepeatCell(val, i));
} else if (i == list.size() - 1) {
if (i > repeatCell.getCurrent() && isMerge(list, i, field)) {
cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
}
} else if (!isMerge(list, i, field)) {
if ((i - repeatCell.getCurrent() > 1)) {
cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
}
map.put(field, new RepeatCell(val, i));
}
}
}
}
return cellList;
}
private boolean isMerge(List<?> list, int i, Field field) {
boolean isMerge = true;
CellMerge cm = field.getAnnotation(CellMerge.class);
final String[] mergeBy = cm.mergeBy();
if (StrUtil.isAllNotBlank(mergeBy)) {
//比对当前list(i)和list(i - 1)的各个属性值一一比对 如果全为真 则为真
for (String fieldName : mergeBy) {
final Object valCurrent = ReflectUtil.getFieldValue(list.get(i), fieldName);
final Object valPre = ReflectUtil.getFieldValue(list.get(i - 1), fieldName);
if (!Objects.equals(valPre, valCurrent)) {
//依赖字段如有任一不等值,则标记为不可合并
isMerge = false;
}
}
}
return isMerge;
}
@Data
@AllArgsConstructor
static class RepeatCell {
private Object value;
private int current;
}
}

View File

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

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,16 +1,17 @@
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;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.metadata.FieldCache;
import com.alibaba.excel.metadata.FieldWrapper;
import com.alibaba.excel.util.ClassUtils;
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import cn.idev.excel.metadata.FieldCache;
import cn.idev.excel.metadata.FieldWrapper;
import cn.idev.excel.util.ClassUtils;
import cn.idev.excel.write.handler.SheetWriteHandler;
import cn.idev.excel.write.metadata.holder.WriteSheetHolder;
import cn.idev.excel.write.metadata.holder.WriteWorkbookHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;
@ -22,6 +23,7 @@ import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.annotation.ExcelDynamicOptions;
import org.dromara.common.excel.annotation.ExcelEnumFormat;
import java.lang.reflect.Field;
@ -103,7 +105,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 +117,16 @@ 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);
} else if (field.isAnnotationPresent(ExcelDynamicOptions.class)) {
// 处理动态下拉选项
ExcelDynamicOptions dynamicOptions = field.getDeclaredAnnotation(ExcelDynamicOptions.class);
// 获取提供者实例
ExcelOptionsProvider provider = SpringUtils.getBean(dynamicOptions.providerClass());
Set<String> providerOptions = provider.getOptions();
if (CollUtil.isNotEmpty(providerOptions)) {
options = new ArrayList<>(providerOptions);
}
}
if (ObjectUtil.isNotEmpty(options)) {
// 仅当下拉可选项不为空时执行
@ -175,7 +186,7 @@ public class ExcelDownHandler implements SheetWriteHandler {
List<String> firstOptions = options.getOptions();
Map<String, List<String>> secoundOptionsMap = options.getNextOptions();
// 采用按行填充数据的方式避免EasyExcel出现数据无法写入的问题
// 采用按行填充数据的方式避免出现数据无法写入的问题
// Attempting to write a row in the range that is already written to disk
// 使用ArrayList记载数据防止乱序
@ -291,9 +302,11 @@ public class ExcelDownHandler implements SheetWriteHandler {
* @param value 下拉选可选值
*/
private void dropDownWithSheet(DataValidationHelper helper, Workbook workbook, Sheet sheet, Integer celIndex, List<String> value) {
//由于poi的写出相关问题超过100个会被临时写进硬盘导致后续内存合并会出Attempting to write a row[] in the range [] that is already written to disk
String tmpOptionsSheetName = OPTIONS_SHEET_NAME + "_" + currentOptionsColumnIndex;
// 创建下拉数据表
Sheet simpleDataSheet = Optional.ofNullable(workbook.getSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)))
.orElseGet(() -> workbook.createSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)));
Sheet simpleDataSheet = Optional.ofNullable(workbook.getSheet(WorkbookUtil.createSafeSheetName(tmpOptionsSheetName)))
.orElseGet(() -> workbook.createSheet(WorkbookUtil.createSafeSheetName(tmpOptionsSheetName)));
// 将下拉表隐藏
workbook.setSheetHidden(workbook.getSheetIndex(simpleDataSheet), true);
// 完善纵向的一级选项数据表
@ -302,9 +315,9 @@ public class ExcelDownHandler implements SheetWriteHandler {
// 获取每一选项行如果没有则创建
Row row = Optional.ofNullable(simpleDataSheet.getRow(i))
.orElseGet(() -> simpleDataSheet.createRow(finalI));
// 获取本级选项对应的选项列如果没有则创建
Cell cell = Optional.ofNullable(row.getCell(currentOptionsColumnIndex))
.orElseGet(() -> row.createCell(currentOptionsColumnIndex));
// 获取本级选项对应的选项列如果没有则创建上述采用多个sheet,默认索引为1列
Cell cell = Optional.ofNullable(row.getCell(0))
.orElseGet(() -> row.createCell(0));
// 设置值
cell.setCellValue(value.get(i));
}
@ -312,13 +325,13 @@ public class ExcelDownHandler implements SheetWriteHandler {
// 创建名称管理器
Name name = workbook.createName();
// 设置名称管理器的别名
String nameName = String.format("%s_%d", OPTIONS_SHEET_NAME, celIndex);
String nameName = String.format("%s_%d", tmpOptionsSheetName, celIndex);
name.setNameName(nameName);
// 以纵向第一列创建一级下拉拼接引用位置
String function = String.format("%s!$%s$1:$%s$%d",
OPTIONS_SHEET_NAME,
getExcelColumnName(currentOptionsColumnIndex),
getExcelColumnName(currentOptionsColumnIndex),
tmpOptionsSheetName,
getExcelColumnName(0),
getExcelColumnName(0),
value.size());
// 设置名称管理器的引用位置
name.setRefersToFormula(function);

View File

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

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