Compare commits

104 Commits

Author SHA1 Message Date
miracle-bean
89d6f6f247 !815 fix websocket 多线程下IO阻塞的问题
* fix websocket 多线程下IO阻塞的问题
2025-12-23 07:55:24 +00:00
疯狂的狮子Li
1324a1cb16 update 优化 增加 HandlerMethodValidationException 参数校验异常连接 2025-12-23 15:30:32 +08:00
ColorDreams
961bca462e fix 临时修复Ip2Region InputStream读取函数导致的OOM问题 2025-12-23 14:32:48 +08:00
疯狂的狮子Li
496df8494e update 优化 翻译实现类逻辑 2025-12-23 10:38:18 +08:00
疯狂的狮子Li
2f1f9689e0 🧨🧨🧨发布 5.5.2 版本 2025年最后一版 2025-12-23 09:28:20 +08:00
疯狂的狮子Li
8110413fdf update 删除错误的配置 2025-12-23 09:22:42 +08:00
疯狂的狮子Li
c1f64d3450 update anyline 8.7.2-20250603 => 8.7.3-20251210 2025-12-22 13:11:38 +08:00
疯狂的狮子Li
cb00f4c9c1 update snailjob 1.8.0 => 1.9.0 2025-12-22 09:42:56 +08:00
疯狂的狮子Li
79512c69b2 fix 修复 创建租户同步工作流数据 在没有流程定义的情况下不会复制流程类别的问题 2025-12-19 19:38:45 +08:00
疯狂的狮子Li
a5fb128f11 update springboot 3.5.8 => 3.5.9 2025-12-19 11:31:53 +08:00
dr5hx
8a04e3c88f !811 feat(excel): 增强单元格合并处理逻辑
* feat(excel): 增强单元格合并处理逻辑
2025-12-19 01:48:31 +00:00
疯狂的狮子Li
dac447b76f fix 修复 listenerVariable.getVariable() 获取null问题 2025-12-19 09:36:22 +08:00
AprilWind
35a9e4c8e8 update 增加高安全脱敏方法 2025-12-18 19:19:01 +08:00
AprilWind
0d87c12d3c update 优化灵活脱敏方法 2025-12-18 17:30:32 +08:00
AprilWind
f20a0c4342 update 优化构建流程参数 2025-12-16 18:27:21 +08:00
AprilWind
6c8d637bd2 update 优化报错信息提示 2025-12-16 17:05:18 +08:00
马铃薯头
20e9957db2 !807 update 代码生成字典类型字段新增更新验证策略
* update 代码生成字典类型字段新增更新验证策略
2025-12-16 08:36:03 +00:00
AprilWind
9baded9326 update 测试单表和测试树表增加搜索条件 2025-12-16 14:16:13 +08:00
疯狂的狮子Li
b5902debb6 update 优化 删除无用配置类代码 2025-12-15 15:44:54 +08:00
AprilWind
bcd5bb0f86 !805 update 优化我的待办时间展示
* update 优化我的待办时间展示
2025-12-15 06:19:29 +00:00
AprilWind
1a461f7d3d !804 update 优化登录提示语
* update 优化登录提示语
2025-12-15 03:10:49 +00:00
疯狂的狮子Li
e23d99d85b fix 修复 form_path 输入空字符串导致的问题 2025-12-15 09:36:12 +08:00
秋辞未寒
f07c20afab update Ip2Region version to 3.3.1,使用新的xdb文件加载函数优化xdb数据库的加载流程 2025-12-12 22:50:23 +08:00
疯狂的狮子Li
420553eaa6 fix 修复 工作流类别 顶节点父级可以被修改导致无法加载的问题 2025-12-12 17:15:35 +08:00
疯狂的狮子Li
1d8d93eaa3 fix 修复 微软三方对接参数缺失 2025-12-12 11:40:51 +08:00
疯狂的狮子Li
5f0d09fd45 update 优化 日志输出内容 2025-12-12 09:32:07 +08:00
AprilWind
1c2b7d7017 update 优化Ip2Region初始化打印日志 2025-12-12 09:25:18 +08:00
AprilWind
5fb2890167 update 增加对IPv6地址库的支持,优化Ip2Region初始化逻辑 2025-12-11 19:57:27 +08:00
秋辞未寒
1165c8dc06 !803 feat IP地址行政区域离线查询支持IPv6(已提供相关代码,开发者自行决定是否使用)
* update IP地址行政区域助手类重命名以匹配其工具类的功能定位
* feat IP地址行政区域离线查询支持IPv6(已提供相关代码,开发者自行决定是否使用)
2025-12-11 05:23:09 +00:00
AprilWind
ee09377997 !802 update 添加 ID 生成工具类,支持多种 ID 生成方式
* update 使用 IdGeneratorUtil 替代主键生成
* update 添加 ID 生成工具类,支持多种 ID 生成方式
2025-12-11 02:22:49 +00:00
疯狂的狮子Li
1921b22a57 fix 修复 获取可驳回节点重复问题(感谢 搬砖的小庄) 2025-12-11 10:19:11 +08:00
疯狂的狮子Li
8718989c52 update 优化 任务执行监听器 传递任务的相关数据 不传递实例相关数据了(避免并行节点覆盖问题) 2025-12-11 09:43:29 +08:00
疯狂的狮子Li
36069cd0e4 update 优化 加签判断逻辑 2025-12-11 09:17:58 +08:00
马铃薯头
39b19ac361 !798 fix 修复 excel 导出多 sheet 合并单元格失效问题
* fix 修复 excel 导出多 sheet 合并单元格失效问题
2025-12-10 09:22:14 +00:00
疯狂的狮子Li
279488e7ed update warm-flow 1.8.3 => 1.8.4 2025-12-10 17:01:19 +08:00
疯狂的狮子Li
e28e15d943 update warm-flow 1.8.3 => 1.8.4 2025-12-10 16:31:55 +08:00
疯狂的狮子Li
b44b5551e3 update warm-flow 1.8.3 => 1.8.4 2025-12-10 16:29:59 +08:00
疯狂的狮子Li
0cb4b35f53 update 优化 文件上传增加文件内容长度校验 2025-12-10 09:46:18 +08:00
疯狂的狮子Li
9571e71707 fix 修复 本地文件上传 无法获取文件长度问题 2025-12-09 17:02:43 +08:00
疯狂的狮子Li
dfa7d88255 update 优化 日志脱敏改用JsonNode处理提高效率 2025-12-09 16:14:56 +08:00
疯狂的狮子Li
8d29091afa fix 修复 jsonParam 参数可能为空问题 2025-12-09 15:50:39 +08:00
疯狂的狮子Li
116fa0053d add 新增 Topiam 赞助商 2025-12-08 13:04:12 +08:00
疯狂的狮子Li
0c08455b32 update 优化 接口访问日志 排除敏感参数输出 2025-12-08 10:00:57 +08:00
疯狂的狮子Li
581203ba15 update 优化 修改 ossclient 并发配置 2025-12-08 09:26:12 +08:00
AprilWind
50fa220471 update 任务处理增加Lock4j锁支持 2025-12-04 14:54:01 +08:00
AprilWind
287effdc6d update 增加SpEL表达式解析异常处理 2025-12-02 19:02:21 +08:00
AprilWind
1d4fcf737a refactor 优化工作流服务中的异常处理 2025-12-02 17:28:35 +08:00
AprilWind
e672a3bc6c update 增加SpEL表达式解析异常处理 2025-12-02 16:28:09 +08:00
AprilWind
ec703ceeb8 update 优化代码生成中的Lock4j锁 2025-12-02 10:10:24 +08:00
AprilWind
6aa4e83413 update 优化我的任务查询条件 2025-12-01 17:21:09 +08:00
gssong
65d677ac90 fix 修复排他网关执行后,驳回选到未执行的网关 2025-11-25 18:52:25 +08:00
gssong
aca2b6d498 update 补充操作日志 2025-11-25 11:26:10 +08:00
gssong
dd5f72cc99 update 补充操作日志 2025-11-25 11:25:14 +08:00
gssong
b1d3d87360 fix 修复指定选人审批后 再次驳回到指定选人环节后 全部人能看到待办问题 2025-11-24 18:06:09 +08:00
AprilWind
e67fc5ebd4 !789 update 增加脱敏工具类支持灵活配置可见长度和掩码长度
* update 增加脱敏工具类支持灵活配置可见长度和掩码长度
2025-11-24 06:20:37 +00:00
疯狂的狮子Li
6a2c74537e update 增加 fory 开启日志说明 2025-11-24 11:54:52 +08:00
疯狂的狮子Li
041e226059 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:05:38 +08:00
AprilWind
0418b6c6ff !788 update 参数配置服务 增加多种配置获取方法,支持不同类型的配置解析
* update 参数配置服务 增加多种配置获取方法,支持不同类型的配置解析
2025-11-24 01:18:49 +00:00
AprilWind
c9272acce2 update 增加流程定义发布检查,确保流程在执行前已发布 2025-11-21 09:47:05 +08:00
疯狂的狮子Li
8d51adee10 reset 回滚 snailjob 1.8.1版本到1.8.0版本 出现严重依赖冲突问题 2025-11-20 17:46:24 +08:00
AprilWind
6d4cc28dcd update 优化消息发送逻辑,增加异常处理并记录未处理的消息类型 2025-11-20 16:38:46 +08:00
疯狂的狮子Li
fc35a1469f update 优化 pg 字段类型适配 2025-11-19 17:41:07 +08:00
疯狂的狮子Li
f70a37c050 update 优化 将特殊方法改为私有禁止不懂的用户乱用 2025-11-19 16:23:51 +08:00
疯狂的狮子Li
181f461984 fix 修复 pg更新sql书写错误 2025-11-14 13:21:46 +08:00
AprilWind
75618347fa update 优化删除业务ID的方法,支持字符串类型的业务ID 2025-11-14 09:38:00 +08:00
疯狂的狮子Li
5a57e6b835 update 优化 更正注释描述错误 2025-11-13 16:34:03 +08:00
Jack
d1d47d2599 !786 update 上传请求的预签名URL
* update 上传请求的预签名URL
2025-11-13 08:31:04 +00:00
AprilWind
f35938a068 update 升级 snailjob 和 warm-flow 版本至 1.8.1 和 1.8.3 2025-11-13 09:02:33 +08:00
秋辞未寒
888c14615d update 优化 !781Excel 模版动态数据下拉 泛型逻辑 2025-11-11 17:02:53 +08:00
王志龙
fa6c9696f0 !785 FlwSpelController类注释补全
* FlwSpelController类注释补全
2025-11-11 05:34:06 +00:00
Angus
37038449ab !781 Excel模版动态数据下拉
* Excel模版动态数据下拉
* Excel模版动态数据下拉
2025-11-11 01:58:55 +00:00
gssong
9bff358afd fix 修复申请人提交可直接结束流程 2025-11-09 08:44:25 +08:00
疯狂的狮子Li
d2a45156a2 fix 修复 warmflow的官方sql书写不正确问题 2025-10-29 10:13:46 +08:00
Tyler Ge
9df0a8de1c !780 fix: 修复CompleteTaskDTO中getVariables()中variables == null 时的返回值问题
* fix: 修复CompleteTaskDTO中getVariables()中variables == null 时的返回值问题
2025-10-29 01:26:05 +00:00
疯狂的狮子Li
5ea8d8c950 🐳🐳🐳发布 5.5.1 正式版 日常依赖升级bug修复 2025-10-28 11:14:44 +08:00
疯狂的狮子Li
3318109044 update springboot 3.5.6 => 3.5.7 2025-10-27 09:41:00 +08:00
AprilWind
aa1f89e253 update 优化 SSE 心跳检测逻辑,增强连接管理与异常处理 2025-10-24 10:53:52 +08:00
马铃薯头
35c77403d6 !778 update 客户端管理新增客户端key唯一校验逻辑
* update 客户端管理新增客户端key唯一校验逻辑
2025-10-22 07:29:27 +00:00
疯狂的狮子Li
603fb7b92d fix 修复 全局处理器不生效问题 根据官方issue改为特殊写法(不理解为什么 https://github.com/apache/fesod/issues/648) 2025-10-22 14:20:53 +08:00
疯狂的狮子Li
6cf0c79433 fix 修复 查询任务扩展数据不存在导致的空报错 2025-10-22 11:03:04 +08:00
草編的戒指礻
3934e119d6 !776 update 优化 sse 修复相同token历史连接未关闭问题;新增心跳监测,关闭无效连接
* update 优化 sse 心跳定时器执行方式
* update 优化 sse 心跳检测写法
* update 优化 sse 修复相同token历史连接未关闭问题;新增心跳监测,关闭无效连接
2025-10-20 04:01:21 +00:00
AprilWind
33a6a21fdf update warm-flow同步升级sql 2025-10-16 16:40:02 +08:00
AprilWind
7800b1259f update warm-flow 升级 1.8.2 2025-10-16 16:24:06 +08:00
疯狂的狮子Li
3623fc33d9 fix 修复 修复查询pg类型问题 2025-10-15 18:02:53 +08:00
疯狂的狮子Li
f8612eb52e fix 修复 翻译时异常导致json序列化结构体不符合预期 2025-10-15 13:31:24 +08:00
疯狂的狮子Li
8d32b0311a fix 修复 orderby属性书写重复问题 2025-10-14 18:52:49 +08:00
AprilWind
60bcd2d6e9 update 添加菜单可见性和状态字段到菜单树 2025-10-14 10:24:38 +08:00
AprilWind
5ccb511064 !772 update 优化 nginx 配置,增强性能与安全性
* update 优化 nginx 配置,增强性能与安全性
2025-09-30 02:07:34 +00:00
疯狂的狮子Li
78baf6497a update 优化 拦截sse超时异常 不需要额外处理 2025-09-29 13:35:10 +08:00
疯狂的狮子Li
0719e53f01 update springboot-admin 3.5.3 -> 3.5.5 修复登录白屏问题 2025-09-29 11:57:18 +08:00
疯狂的狮子Li
5f2c4205a5 fix 修复 三方授权 钉钉回调地址未进行url编码问题 由全局编码改为单独编码 避免其他三方调用重复编码 2025-09-28 16:18:02 +08:00
疯狂的狮子Li
2fe4c96706 update 优化 删除Threads类 已经不需要了 2025-09-26 15:24:04 +08:00
AprilWind
5c634940c2 update 增强 Mybatis 异常处理,添加根因查找功能 2025-09-26 14:41:32 +08:00
疯狂的狮子Li
6036f8750b add 增加 同步租户参数配置功能 2025-09-26 11:57:24 +08:00
疯狂的狮子Li
dbcd8f58eb fix 修复 mybatis内报token异常无法正常返回前端信息 2025-09-26 11:19:59 +08:00
疯狂的狮子Li
8905e232e5 fix 修复 mybatis内报token异常无法正常返回前端信息 2025-09-26 11:19:48 +08:00
疯狂的狮子Li
4f15158486 update 优化 satoken 异常信息 强制返回json格式 2025-09-26 09:54:43 +08:00
AprilWind
d2413abd5c update 优化工作流常量使用 2025-09-26 09:41:54 +08:00
友杰
f7ffadeaff !771 bug-修改遗漏的常量替换
* bug-修改遗漏的常量替换
2025-09-26 01:30:47 +00:00
AprilWind
f9eec856e7 !769 update 添加 JSON 格式校验注解及实现
* update 添加 JSON 格式校验注解及实现
2025-09-25 10:50:34 +00:00
疯狂的狮子Li
62562650fe update 更新流程案例json文件 2025-09-25 11:57:55 +08:00
疯狂的狮子Li
df171097c3 update 优化 后端发起流程增加扩展表对象 2025-09-24 16:52:46 +08:00
AprilWind
1977aabc9a update 忽略压缩后的日志文件 *.log.gz 2025-09-23 10:20:51 +08:00
AprilWind
483c4e6d0a update 隐藏 nginx 版本号以增强安全性 2025-09-23 09:25:42 +08:00
131 changed files with 3764 additions and 1507 deletions

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/5.X/LICENSE)
[![使用IntelliJ IDEA开发维护](https://img.shields.io/badge/IntelliJ%20IDEA-提供支持-blue.svg)](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
<br>
[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.5.0-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.5.2-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.4-blue.svg)]()
[![JDK-17](https://img.shields.io/badge/JDK-17-green.svg)]()
[![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)]()
@@ -39,6 +39,7 @@ CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
Mall4J 高质量Java商城系统 - https://www.mall4j.com/cn/?statId=11 <br>
aizuda flowlong 工作流 - https://gitee.com/aizuda/flowlong <br>
Ruoyi-Plus-Uniapp - https://ruoyi.plus <br>
Topiam IAM/IDaaS身份管理平台 - https://www.topiam.cn/ <br>
[如何成为赞助商 加群联系作者详谈 每日PV2500-3000 IP1700-2500](https://plus-doc.dromara.org/#/common/add_group)

18
pom.xml
View File

@@ -13,13 +13,13 @@
<description>Dromara RuoYi-Vue-Plus多租户管理系统</description>
<properties>
<revision>5.5.0</revision>
<spring-boot.version>3.5.6</spring-boot.version>
<revision>5.5.2</revision>
<spring-boot.version>3.5.9</spring-boot.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version>
<mybatis.version>3.5.16</mybatis.version>
<springdoc.version>2.8.13</springdoc.version>
<springdoc.version>2.8.14</springdoc.version>
<therapi-javadoc.version>0.15.0</therapi-javadoc.version>
<fastexcel.version>1.3.0</fastexcel.version>
<velocity.version>2.3</velocity.version>
@@ -27,18 +27,18 @@
<mybatis-plus.version>3.5.14</mybatis-plus.version>
<p6spy.version>3.9.1</p6spy.version>
<hutool.version>5.8.40</hutool.version>
<spring-boot-admin.version>3.5.3</spring-boot-admin.version>
<redisson.version>3.51.0</redisson.version>
<spring-boot-admin.version>3.5.5</spring-boot-admin.version>
<redisson.version>3.52.0</redisson.version>
<lock4j.version>2.2.7</lock4j.version>
<dynamic-ds.version>4.3.1</dynamic-ds.version>
<snailjob.version>1.8.0</snailjob.version>
<snailjob.version>1.9.0</snailjob.version>
<mapstruct-plus.version>1.5.0</mapstruct-plus.version>
<mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
<lombok.version>1.18.40</lombok.version>
<bouncycastle.version>1.80</bouncycastle.version>
<justauth.version>1.16.7</justauth.version>
<!-- 离线IP地址定位库 -->
<ip2region.version>2.7.0</ip2region.version>
<ip2region.version>3.3.1</ip2region.version>
<!-- OSS 配置 -->
<aws.sdk.version>2.28.22</aws.sdk.version>
<!-- SMS 配置 -->
@@ -46,9 +46,9 @@
<!-- 限制框架中的fastjson版本 -->
<fastjson.version>1.2.83</fastjson.version>
<!-- 面向运行时的D-ORM依赖 -->
<anyline.version>8.7.2-20250603</anyline.version>
<anyline.version>8.7.3-20251210</anyline.version>
<!-- 工作流配置 -->
<warm-flow.version>1.8.1</warm-flow.version>
<warm-flow.version>1.8.4</warm-flow.version>
<!-- 插件版本 -->
<maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>

View File

@@ -48,6 +48,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;
@@ -106,7 +107,7 @@ public class AuthController {
Long userId = LoginHelper.getUserId();
scheduledExecutorService.schedule(() -> {
SseMessageDto dto = new SseMessageDto();
dto.setMessage("欢迎登录RuoYi-Vue-Plus后台管理系统");
dto.setMessage(DateUtils.getTodayHour(new Date()) + "好,欢迎登录 RuoYi-Vue-Plus 后台管理系统");
dto.setUserIds(List.of(userId));
SseMessageUtils.publishMessage(dto);
}, 5, TimeUnit.SECONDS);
@@ -147,8 +148,8 @@ public class AuthController {
StpUtil.checkLogin();
// 获取第三方登录信息
AuthResponse<AuthUser> response = SocialUtils.loginAuth(
loginBody.getSource(), loginBody.getSocialCode(),
loginBody.getSocialState(), socialProperties);
loginBody.getSource(), loginBody.getSocialCode(),
loginBody.getSocialState(), socialProperties);
AuthUser authUserData = response.getData();
// 判断授权响应是否成功
if (!response.ok()) {

View File

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

View File

@@ -3,17 +3,12 @@ package org.dromara.common.core.config;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.dromara.common.core.config.properties.ThreadPoolProperties;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.Threads;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
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.*;
/**
* 线程池配置
@@ -22,7 +17,6 @@ import java.util.concurrent.ThreadPoolExecutor;
**/
@Slf4j
@AutoConfiguration
@EnableConfigurationProperties(ThreadPoolProperties.class)
public class ThreadPoolConfig {
/**
@@ -50,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;
@@ -59,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

@@ -1,30 +0,0 @@
package org.dromara.common.core.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 线程池 配置属性
*
* @author Lion Li
*/
@Data
@ConfigurationProperties(prefix = "thread-pool")
public class ThreadPoolProperties {
/**
* 是否开启线程池
*/
private boolean enabled;
/**
* 队列最大长度
*/
private int queueCapacity;
/**
* 线程池维护线程所允许的空闲时间
*/
private int keepAliveSeconds;
}

View File

@@ -82,4 +82,10 @@ public interface SystemConstants {
*/
Long DEFAULT_DEPT_ID = 100L;
/**
* 排除敏感属性字段
*/
String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };
}

View File

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

View File

@@ -0,0 +1,45 @@
package org.dromara.common.core.domain.dto;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 流程实例业务扩展对象
*
* @author may
* @date 2025-08-05
*/
@Data
public class FlowInstanceBizExtDTO 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.common.core.domain.dto;
import cn.hutool.core.util.ObjectUtil;
import lombok.Data;
import java.io.Serial;
@@ -40,6 +41,11 @@ public class StartProcessDTO implements Serializable {
*/
private Map<String, Object> variables;
/**
* 流程业务扩展信息
*/
private FlowInstanceBizExtDTO bizExt;
public Map<String, Object> getVariables() {
if (variables == null) {
return new HashMap<>(16);
@@ -47,4 +53,11 @@ public class StartProcessDTO implements Serializable {
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables;
}
public FlowInstanceBizExtDTO getBizExt() {
if (ObjectUtil.isNull(bizExt)) {
bizExt = new FlowInstanceBizExtDTO();
}
return bizExt;
}
}

View File

@@ -1,5 +1,11 @@
package org.dromara.common.core.service;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Dict;
import java.math.BigDecimal;
import java.util.List;
/**
* 通用 参数配置服务
*
@@ -15,4 +21,80 @@ public interface ConfigService {
*/
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

@@ -20,7 +20,7 @@ public interface WorkflowService {
* @param businessIds 业务id
* @return 结果
*/
boolean deleteInstance(List<Long> businessIds);
boolean deleteInstance(List<String> businessIds);
/**
* 获取当前流程状态

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;
@@ -297,4 +299,80 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
}
}
/**
* 根据指定日期时间获取时间段(凌晨 / 上午 / 中午 / 下午 / 晚上)
*
* @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

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

@@ -20,51 +20,24 @@ public class AddressUtils {
public static final String UNKNOWN_IP = "XX XX";
// 内网地址
public static final String LOCAL_ADDRESS = "内网IP";
// 未知地址
public static final String UNKNOWN_ADDRESS = "未知";
public static String getRealAddressByIP(String ip) {
// 处理空串并过滤HTML标签
ip = HtmlUtil.cleanHtmlTag(StringUtils.blankToDefault(ip,""));
// 判断是否为IPv4
if (NetUtils.isIPv4(ip)) {
return resolverIPv4Region(ip);
}
boolean isIPv4 = NetUtils.isIPv4(ip);
// 判断是否为IPv6
if (NetUtils.isIPv6(ip)) {
return resolverIPv6Region(ip);
}
boolean isIPv6 = NetUtils.isIPv6(ip);
// 如果不是IPv4或IPv6则返回未知IP
return UNKNOWN_IP;
}
/**
* 根据IPv4地址查询IP归属行政区域
* @param ip ipv4地址
* @return 归属行政区域
*/
private static String resolverIPv4Region(String ip){
if (!isIPv4 && !isIPv6) {
return UNKNOWN_IP;
}
// 内网不查询
if (NetUtils.isInnerIP(ip)) {
if ((isIPv4 && NetUtils.isInnerIP(ip)) || (isIPv6 && NetUtils.isInnerIPv6(ip))) {
return LOCAL_ADDRESS;
}
return RegionUtils.getCityInfo(ip);
}
/**
* 根据IPv6地址查询IP归属行政区域
* @param ip ipv6地址
* @return 归属行政区域
*/
private static String resolverIPv6Region(String ip){
// 内网不查询
if (NetUtils.isInnerIPv6(ip)) {
return LOCAL_ADDRESS;
}
log.warn("ip2region不支持IPV6地址解析{}", ip);
// 不支持IPv6不再进行没有必要的IP地址信息的解析直接返回
// 如有需要可自行实现IPv6地址信息解析逻辑并在这里返回
return UNKNOWN_ADDRESS;
// TipsIp2Region 提供了精简的IPv6地址库精简的IPv6地址库并不能完全支持IPv6地址的查询且准确度上可能会存在问题如需要准确的IPv6地址查询建议自行实现
return RegionUtils.getRegion(ip);
}
}

View File

@@ -1,50 +1,158 @@
package org.dromara.common.core.utils.ip;
import cn.hutool.core.io.resource.NoResourceException;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import org.lionsoul.ip2region.xdb.Searcher;
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 {
// IP地址库文件名称
public static final String IP_XDB_FILENAME = "ip2region.xdb";
// 默认IPv4地址库文件路径
// 下载地址https://gitee.com/lionsoul/ip2region/blob/master/data/ip2region_v4.xdb
public static final String DEFAULT_IPV4_XDB_PATH = "ip2region_v4.xdb";
private static final Searcher SEARCHER;
// 默认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 {
try {
// 1、将 ip2region 数据库文件 xdb 从 ClassPath 加载到内存。
// 2、基于加载到内存的 xdb 数据创建一个 Searcher 查询对象
SEARCHER = Searcher.newWithBuffer(ResourceUtil.readBytes(IP_XDB_FILENAME));
log.info("RegionUtils初始化成功加载IP地址库数据成功");
} catch (NoResourceException e) {
throw new ServiceException("RegionUtils初始化失败原因IP地址库数据不存在");
// 注意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();
}
// 初始化Ip2Region实例
RegionUtils.ip2Region = Ip2Region.create(v4Config, v6Config);
log.debug("IP工具初始化成功加载IP地址库数据成功");
} 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 {
// 3、执行查询
String region = SEARCHER.search(StringUtils.trim(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,23 @@
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 {
/**
* 提供者类全限定名
* <p>
* {@link org.dromara.common.excel.core.ExcelOptionsProvider} 接口实现类 class
*/
Class<? extends ExcelOptionsProvider> providerClass();
}

View File

@@ -28,7 +28,7 @@ public class ExcelBigNumberConvert implements Converter<Long> {
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
return null;
}
@Override

View File

@@ -29,7 +29,10 @@ public class CellMergeHandler {
// 行合并开始下标
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) {
// 如果入参为空集合则返回空集
@@ -103,6 +106,10 @@ public class CellMergeHandler {
}
if (isAddResult && i > current) {
//如果是同一行,则跳过合并
if (current + rowIndex == lastRow) {
continue;
}
result.add(new CellRangeAddress(current + rowIndex, lastRow, colNum, colNum));
}
}
@@ -147,12 +154,12 @@ public class CellMergeHandler {
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;
}
}
@@ -177,6 +184,16 @@ public class CellMergeHandler {
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);
}
/**
* 创建一个单元格合并处理器实例

View File

@@ -2,15 +2,16 @@ package org.dromara.common.excel.core;
import cn.hutool.core.collection.CollUtil;
import cn.idev.excel.metadata.Head;
import cn.idev.excel.write.handler.WorkbookWriteHandler;
import cn.idev.excel.write.handler.context.WorkbookWriteHandlerContext;
import cn.idev.excel.write.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 java.util.*;
import java.util.List;
/**
* 列值重复合并策略
@@ -18,7 +19,7 @@ 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;
@@ -30,29 +31,34 @@ public class CellMergeStrategy extends AbstractMergeStrategy implements Workbook
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) {
if (CollUtil.isEmpty(cellList)){
if (CollUtil.isEmpty(cellList)) {
return;
}
//单元格写入了,遍历合并区域,如果该Cell在区域内,但非首行,则清空
// 单元格写入了,遍历合并区域,如果该Cell在区域内,但非首行,则清空
final int rowIndex = cell.getRowIndex();
for (CellRangeAddress cellAddresses : cellList) {
final int firstRow = cellAddresses.getFirstRow();
if (cellAddresses.isInRange(cell) && rowIndex != firstRow){
if (cellAddresses.isInRange(cell) && rowIndex != firstRow) {
cell.setBlank();
}
}
}
@Override
public void afterWorkbookDispose(final WorkbookWriteHandlerContext context) {
if (CollUtil.isEmpty(cellList)){
public void afterSheetCreate(final WriteWorkbookHolder writeWorkbookHolder, final WriteSheetHolder writeSheetHolder) {
if (CollUtil.isEmpty(cellList)) {
return;
}
//当前表格写完后,统一写入
// 在 Sheet 创建时提前写入合并区域;后续写入只会影响首格,不会移除合并
final Sheet sheet = writeSheetHolder.getSheet();
for (CellRangeAddress item : cellList) {
context.getWriteContext().writeSheetHolder().getSheet().addMergedRegion(item);
sheet.addMergedRegion(item);
}
}

View File

@@ -23,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;
@@ -117,6 +118,15 @@ public class ExcelDownHandler implements SheetWriteHandler {
ExcelEnumFormat format = field.getDeclaredAnnotation(ExcelEnumFormat.class);
List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField());
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)) {
// 仅当下拉可选项不为空时执行

View File

@@ -0,0 +1,19 @@
package org.dromara.common.excel.core;
import java.util.Set;
/**
* Excel下拉选项数据提供接口
*
* @author Angus
*/
public interface ExcelOptionsProvider {
/**
* 获取下拉选项数据
*
* @return 下拉选项列表
*/
Set<String> getOptions();
}

View File

@@ -5,6 +5,7 @@ import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import lombok.AccessLevel;
@@ -167,4 +168,58 @@ public class JsonUtils {
}
}
/**
* 判断字符串是否为合法 JSON对象或数组
*
* @param str 待校验字符串
* @return true = 合法 JSONfalse = 非法或空
*/
public static boolean isJson(String str) {
if (StringUtils.isBlank(str)) {
return false;
}
try {
OBJECT_MAPPER.readTree(str);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 判断字符串是否为 JSON 对象({}
*
* @param str 待校验字符串
* @return true = JSON 对象
*/
public static boolean isJsonObject(String str) {
if (StringUtils.isBlank(str)) {
return false;
}
try {
JsonNode node = OBJECT_MAPPER.readTree(str);
return node.isObject();
} catch (Exception e) {
return false;
}
}
/**
* 判断字符串是否为 JSON 数组([]
*
* @param str 待校验字符串
* @return true = JSON 数组
*/
public static boolean isJsonArray(String str) {
if (StringUtils.isBlank(str)) {
return false;
}
try {
JsonNode node = OBJECT_MAPPER.readTree(str);
return node.isArray();
} catch (Exception e) {
return false;
}
}
}

View File

@@ -0,0 +1,33 @@
package org.dromara.common.json.validate;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
/**
* JSON 格式校验注解
*
* @author AprilWind
*/
@Documented
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = JsonPatternValidator.class)
public @interface JsonPattern {
/**
* 限制 JSON 类型,默认为 {@link JsonType#ANY},即对象或数组都允许
*/
JsonType type() default JsonType.ANY;
/**
* 校验失败时的提示消息
*/
String message() default "不是有效的 JSON 格式";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@@ -0,0 +1,51 @@
package org.dromara.common.json.validate;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
/**
* JSON 格式校验器
*
* @author AprilWind
*/
public class JsonPatternValidator implements ConstraintValidator<JsonPattern, String> {
/**
* 注解中指定的 JSON 类型枚举
*/
private JsonType jsonType;
/**
* 初始化校验器,从注解中提取 JSON 类型
*
* @param annotation 注解实例
*/
@Override
public void initialize(JsonPattern annotation) {
this.jsonType = annotation.type();
}
/**
* 校验字符串是否为合法 JSON
*
* @param value 待校验字符串
* @param context 校验上下文,可用于自定义错误信息
* @return true = 合法 JSON 或为空false = 非法 JSON
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (StringUtils.isBlank(value)) {
// 交给 @NotBlank 或 @NotNull 控制是否允许为空
return true;
}
// 根据 JSON 类型进行不同的校验
return switch (jsonType) {
case ANY -> JsonUtils.isJson(value);
case OBJECT -> JsonUtils.isJsonObject(value);
case ARRAY -> JsonUtils.isJsonArray(value);
};
}
}

View File

@@ -0,0 +1,30 @@
package org.dromara.common.json.validate;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* JSON 类型枚举
*
* @author AprilWind
*/
@Getter
@AllArgsConstructor
public enum JsonType {
/**
* JSON 对象,例如 {"a":1}
*/
OBJECT,
/**
* JSON 数组,例如 [1,2,3]
*/
ARRAY,
/**
* 任意 JSON 类型,对象或数组都可以
*/
ANY
}

View File

@@ -13,6 +13,7 @@ import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.SpringUtils;
@@ -39,12 +40,6 @@ import java.util.*;
@AutoConfiguration
public class LogAspect {
/**
* 排除敏感属性字段
*/
public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };
/**
* 计时 key
*/
@@ -160,7 +155,7 @@ public class LogAspect {
String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
operLog.setOperParam(StringUtils.substring(params, 0, 3800));
} else {
MapUtil.removeAny(paramsMap, EXCLUDE_PROPERTIES);
MapUtil.removeAny(paramsMap, SystemConstants.EXCLUDE_PROPERTIES);
MapUtil.removeAny(paramsMap, excludeParamNames);
operLog.setOperParam(StringUtils.substring(JsonUtils.toJsonString(paramsMap), 0, 3800));
}
@@ -174,7 +169,7 @@ public class LogAspect {
if (ArrayUtil.isEmpty(paramsArray)) {
return params.toString();
}
String[] exclude = ArrayUtil.addAll(excludeParamNames, EXCLUDE_PROPERTIES);
String[] exclude = ArrayUtil.addAll(excludeParamNames, SystemConstants.EXCLUDE_PROPERTIES);
for (Object o : paramsArray) {
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
String str = "";

View File

@@ -1,10 +1,11 @@
package org.dromara.common.mybatis.handler;
import cn.dev33.satoken.exception.NotLoginException;
import cn.hutool.http.HttpStatus;
import com.baomidou.dynamic.datasource.exception.CannotFindDataSourceException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.utils.StringUtils;
import org.mybatis.spring.MyBatisSystemException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -35,13 +36,54 @@ public class MybatisExceptionHandler {
@ExceptionHandler(MyBatisSystemException.class)
public R<Void> handleCannotFindDataSourceException(MyBatisSystemException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
String message = e.getMessage();
if (StringUtils.contains(message, "CannotFindDataSourceException")) {
Throwable root = getRootCause(e);
if (root instanceof NotLoginException) {
log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, root.getMessage());
return R.fail(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,无法访问系统资源");
}
if (root instanceof CannotFindDataSourceException) {
log.error("请求地址'{}', 未找到数据源", requestURI);
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, "未找到数据源,请联系管理员确认");
}
log.error("请求地址'{}', Mybatis系统异常", requestURI, e);
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, message);
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, e.getMessage());
}
/**
* 获取异常的根因(递归查找)
*
* @param e 当前异常
* @return 根因异常(最底层的 cause
* <p>
* 逻辑说明:
* 1. 如果 e 没有 cause说明 e 本身就是根因,直接返回
* 2. 如果 e 的 cause 和自身相同(防止循环引用),也返回 e
* 3. 否则递归调用,继续向下寻找最底层的 cause
*/
public static Throwable getRootCause(Throwable e) {
Throwable cause = e.getCause();
if (cause == null || cause == e) {
return e;
}
return getRootCause(cause);
}
/**
* 在异常链中查找指定类型的异常
*
* @param e 当前异常
* @param clazz 目标异常类
* @return 找到的指定类型异常,如果没有找到返回 null
*/
public static Throwable findCause(Throwable e, Class<? extends Throwable> clazz) {
Throwable t = e;
while (t != null && t != t.getCause()) {
if (clazz.isInstance(t)) {
return t;
}
t = t.getCause();
}
return null;
}
}

View File

@@ -42,7 +42,7 @@ public class DataBaseHelper {
String databaseProductName = metaData.getDatabaseProductName();
return DataBaseType.find(databaseProductName);
} catch (SQLException e) {
throw new ServiceException(e.getMessage());
throw new RuntimeException("获取数据库类型失败", e);
}
}

View File

@@ -112,7 +112,7 @@ public class DataPermissionHelper {
/**
* 开启忽略数据权限(开启后需手动调用 {@link #disableIgnore()} 关闭)
*/
public static void enableIgnore() {
private static void enableIgnore() {
IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
if (ObjectUtil.isNull(ignoreStrategy)) {
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().dataPermission(true).build());
@@ -126,7 +126,7 @@ public class DataPermissionHelper {
/**
* 关闭忽略数据权限
*/
public static void disableIgnore() {
private static void disableIgnore() {
IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
if (ObjectUtil.isNotNull(ignoreStrategy)) {
boolean noOtherIgnoreStrategy = !Boolean.TRUE.equals(ignoreStrategy.getDynamicTableName())

View File

@@ -0,0 +1,129 @@
package org.dromara.common.mybatis.utils;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.SpringUtils;
/**
* ID 生成工具类
*
* @author AprilWind
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class IdGeneratorUtil {
private static final IdentifierGenerator GENERATOR = SpringUtils.getBean(IdentifierGenerator.class);
/**
* 生成字符串类型主键 ID
* <p>
* 调用 {@link IdentifierGenerator#nextId(Object)},返回 String 格式 ID。
* </p>
*
* @return 字符串格式主键 ID
*/
public static String nextId() {
return GENERATOR.nextId(null).toString();
}
/**
* 生成 Long 类型主键 ID
* <p>
* 自动将生成的数字型主键转换为 Long 类型
* </p>
*
* @return Long 类型主键 ID
*/
public static Long nextLongId() {
return GENERATOR.nextId(null).longValue();
}
/**
* 生成 Number 类型主键 ID
* <p>
* 推荐在需要保留原始 Number 类型时使用
* </p>
*
* @return Number 类型主键 ID
*/
public static Number nextNumberId() {
return GENERATOR.nextId(null);
}
/**
* 根据实体生成数字型主键 ID
* <p>
* 若自定义的 {@link IdentifierGenerator} 根据实体内容生成 ID则可以使用本方法
* </p>
*
* @param entity 实体对象
* @return Number 类型主键 ID
*/
public static Number nextId(Object entity) {
return GENERATOR.nextId(entity);
}
/**
* 根据实体生成字符串主键 ID
* <p>
* 与 {@link #nextId(Object)} 类似,但返回 String 类型
* </p>
*
* @param entity 实体对象
* @return 字符串格式主键 ID
*/
public static String nextStringId(Object entity) {
return GENERATOR.nextId(entity).toString();
}
/**
* 生成 32 位 UUID
* <p>
* 底层使用 {@link IdWorker#get32UUID()}
* </p>
*
* @return 32 位 UUID 字符串
*/
public static String nextUUID() {
return IdWorker.get32UUID();
}
/**
* 根据实体生成 32 位 UUID
* <p>
* 默认 {@link IdentifierGenerator#nextUUID(Object)} 实现忽略实体,但保留该方法便于扩展。
* </p>
*
* @param entity 实体对象
* @return 32 位 UUID 字符串
*/
public static String nextUUID(Object entity) {
return GENERATOR.nextUUID(entity);
}
/**
* 生成带指定前缀的字符串主键 ID
* <p>
* 示例prefix = "ORD",生成结果形如:{@code ORD20251211000123}
* </p>
*
* @param prefix 自定义前缀
* @return 带前缀的字符串主键 ID
*/
public static String nextIdWithPrefix(String prefix) {
return prefix + nextId();
}
/**
* 生成带前缀的 UUID
*
* @param prefix 前缀
* @return prefix + UUID
*/
public static String nextUUIDWithPrefix(String prefix) {
return prefix + nextUUID();
}
}

View File

@@ -14,7 +14,9 @@ import org.dromara.common.oss.exception.OssException;
import org.dromara.common.oss.properties.OssProperties;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.async.*;
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody;
import software.amazon.awssdk.core.async.ResponsePublisher;
import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3AsyncClient;
@@ -33,6 +35,7 @@ import java.nio.channels.WritableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
@@ -94,7 +97,11 @@ public class OssClient {
.region(of())
.forcePathStyle(isStyle)
.httpClient(NettyNioAsyncHttpClient.builder()
.connectionTimeout(Duration.ofSeconds(60)).build())
.connectionTimeout(Duration.ofSeconds(60))
.connectionAcquisitionTimeout(Duration.ofSeconds(30))
.maxConcurrency(100)
.maxPendingConnectionAcquires(1000)
.build())
.build();
//AWS基于 CRT 的 S3 AsyncClient 实例用作 S3 传输管理器的底层客户端
@@ -317,13 +324,13 @@ public class OssClient {
}
/**
* 获取私有URL链接
* 创建下载请求的预签名URL
*
* @param objectKey 对象KEY
* @param expiredTime 链接授权到期时间
*/
public String getPrivateUrl(String objectKey, Duration expiredTime) {
// 使用 AWS S3 预签名 URL 的生成器 获取对象的预签名 URL
public String createPresignedGetUrl(String objectKey, Duration expiredTime) {
// 使用 AWS S3 预签名 URL 的生成器 获取下载对象的预签名 URL
URL url = presigner.presignGetObject(
x -> x.signatureDuration(expiredTime)
.getObjectRequest(
@@ -332,7 +339,28 @@ public class OssClient {
.build())
.build())
.url();
return url.toString();
return url.toExternalForm();
}
/**
* 创建上传请求的预签名URL
*
* @param objectKey 对象KEY
* @param expiredTime 链接授权到期时间
* @param metadata 元数据
*/
public String createPresignedPutUrl(String objectKey, Duration expiredTime, Map<String, String> metadata) {
// 使用 AWS S3 预签名 URL 的生成器 获取上传文件对象的预签名 URL
URL url = presigner.presignPutObject(
x -> x.signatureDuration(expiredTime)
.putObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(objectKey)
.metadata(metadata)
.build())
.build())
.url();
return url.toExternalForm();
}
/**

View File

@@ -43,16 +43,12 @@
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<!-- &lt;!&ndash; redis序列化替代方案 比json快无数的跨语言二进制序列化 &ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>org.apache.fury</groupId>-->
<!-- <artifactId>fury-core</artifactId>-->
<!-- <version>0.9.0</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.slf4j</groupId>-->
<!-- <artifactId>slf4j-api</artifactId>-->
<!-- </dependency>-->
<!-- redis序列化替代方案 比json快无数的跨语言二进制序列化 -->
<dependency>
<groupId>org.apache.fory</groupId>
<artifactId>fory-core</artifactId>
<version>0.13.1</version>
</dependency>
</dependencies>

View File

@@ -53,9 +53,10 @@ public class RedisConfig {
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型类必须是非final修饰的。序列化时将对象全类名一起保存下来
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
// LoggerFactory.useSlf4jLogging(true);
// FuryCodec furyCodec = new FuryCodec();
// CompositeCodec codec = new CompositeCodec(StringCodec.INSTANCE, furyCodec, furyCodec);
// org.apache.fory.logging.LoggerFactory 包别引入错了
// LoggerFactory.useSlf4jLogging(true);
// ForyCodec foryCodec = new ForyCodec();
// CompositeCodec codec = new CompositeCodec(StringCodec.INSTANCE, foryCodec, foryCodec);
TypedJsonJacksonCodec jsonCodec = new TypedJsonJacksonCodec(Object.class, om);
// 组合序列化 key 使用 String 内容使用通用 json 格式
CompositeCodec codec = new CompositeCodec(StringCodec.INSTANCE, jsonCodec, jsonCodec);

View File

@@ -7,7 +7,9 @@ import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import cn.dev33.satoken.util.SaTokenConsts;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.HttpStatus;
@@ -55,6 +57,8 @@ public class SecurityConfig implements WebMvcConfigurer {
// 对未排除的路径进行检查
.check(() -> {
HttpServletRequest request = ServletUtils.getRequest();
HttpServletResponse response = ServletUtils.getResponse();
response.setContentType(SaTokenConsts.CONTENT_TYPE_APPLICATION_JSON);
// 检查是否登录 是否有token
StpUtil.checkLogin();
@@ -94,7 +98,11 @@ public class SecurityConfig implements WebMvcConfigurer {
.setAuth(obj -> {
SaHttpBasicUtil.check(username + ":" + password);
})
.setError(e -> SaResult.error(e.getMessage()).setCode(HttpStatus.UNAUTHORIZED));
.setError(e -> {
HttpServletResponse response = ServletUtils.getResponse();
response.setContentType(SaTokenConsts.CONTENT_TYPE_APPLICATION_JSON);
return SaResult.error(e.getMessage()).setCode(HttpStatus.UNAUTHORIZED);
});
}
}

View File

@@ -3,6 +3,7 @@ package org.dromara.common.sensitive.core;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.DesensitizedUtil;
import lombok.AllArgsConstructor;
import org.dromara.common.core.utils.DesensitizedUtils;
import java.util.function.Function;
@@ -80,6 +81,18 @@ public enum SensitiveStrategy {
*/
FIRST_MASK(DesensitizedUtil::firstMask),
/**
* 通用字符串脱敏
* 可配置前后可见长度和中间掩码长度
* 默认示例前4位可见后4位可见中间固定4个*
*/
STRING_MASK(s -> DesensitizedUtils.mask(s, 4, 4, 4)),
/**
* 高安全级别脱敏Token / 私钥前2位可见后2位可见中间全部掩码
*/
MASK_HIGH_SECURITY(s -> DesensitizedUtils.maskHighSecurity(s, 2, 2)),
/**
* 清空为""
*/

View File

@@ -0,0 +1,109 @@
package me.zhyd.oauth.request;
import com.alibaba.fastjson.JSONObject;
import com.xkcoding.http.support.HttpHeader;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.config.AuthDefaultSource;
import me.zhyd.oauth.enums.scope.AuthDingTalkScope;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.utils.AuthScopeUtils;
import me.zhyd.oauth.utils.GlobalAuthUtils;
import me.zhyd.oauth.utils.HttpUtils;
import me.zhyd.oauth.utils.UrlBuilder;
import java.util.HashMap;
import java.util.Map;
/**
* 新版钉钉二维码登录
*
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @since 1.16.7
*/
public class AuthDingTalkV2Request extends AuthDefaultRequest {
public AuthDingTalkV2Request(AuthConfig config) {
super(config, AuthDefaultSource.DINGTALK_V2);
}
public AuthDingTalkV2Request(AuthConfig config, AuthStateCache authStateCache) {
super(config, AuthDefaultSource.DINGTALK_V2, authStateCache);
}
@Override
public String authorize(String state) {
return UrlBuilder.fromBaseUrl(source.authorize())
.queryParam("response_type", "code")
.queryParam("client_id", config.getClientId())
.queryParam("scope", this.getScopes(",", true, AuthScopeUtils.getDefaultScopes(AuthDingTalkScope.values())))
.queryParam("redirect_uri", GlobalAuthUtils.urlEncode(config.getRedirectUri()))
.queryParam("prompt", "consent")
.queryParam("org_type", config.getDingTalkOrgType())
.queryParam("corpId", config.getDingTalkCorpId())
.queryParam("exclusiveLogin", config.isDingTalkExclusiveLogin())
.queryParam("exclusiveCorpId", config.getDingTalkExclusiveCorpId())
.queryParam("state", getRealState(state))
.build();
}
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
Map<String, String> params = new HashMap<>();
params.put("grantType", "authorization_code");
params.put("clientId", config.getClientId());
params.put("clientSecret", config.getClientSecret());
params.put("code", authCallback.getCode());
String response = new HttpUtils(config.getHttpConfig()).post(this.source.accessToken(), JSONObject.toJSONString(params)).getBody();
JSONObject accessTokenObject = JSONObject.parseObject(response);
if (!accessTokenObject.containsKey("accessToken")) {
throw new AuthException(JSONObject.toJSONString(response), source);
}
return AuthToken.builder()
.accessToken(accessTokenObject.getString("accessToken"))
.refreshToken(accessTokenObject.getString("refreshToken"))
.expireIn(accessTokenObject.getIntValue("expireIn"))
.corpId(accessTokenObject.getString("corpId"))
.build();
}
@Override
public AuthUser getUserInfo(AuthToken authToken) {
HttpHeader header = new HttpHeader();
header.add("x-acs-dingtalk-access-token", authToken.getAccessToken());
String response = new HttpUtils(config.getHttpConfig()).get(this.source.userInfo(), null, header, false).getBody();
JSONObject object = JSONObject.parseObject(response);
authToken.setOpenId(object.getString("openId"));
authToken.setUnionId(object.getString("unionId"));
return AuthUser.builder()
.rawUserInfo(object)
.uuid(object.getString("unionId"))
.username(object.getString("nick"))
.nickname(object.getString("nick"))
.avatar(object.getString("avatarUrl"))
.snapshotUser(object.getBooleanValue("visitor"))
.token(authToken)
.source(source.toString())
.build();
}
/**
* 返回获取accessToken的url
*
* @param code 授权码
* @return 返回获取accessToken的url
*/
protected String accessTokenUrl(String code) {
return UrlBuilder.fromBaseUrl(source.accessToken())
.queryParam("code", code)
.queryParam("clientId", config.getClientId())
.queryParam("clientSecret", config.getClientSecret())
.queryParam("grantType", "authorization_code")
.build();
}
}

View File

@@ -28,9 +28,14 @@ public class SocialLoginConfigProperties {
private String redirectUri;
/**
* 是否获取unionId
* 是否需要申请unionid目前只针对qq登录
*/
private boolean unionId;
private Boolean unionId;
/**
* Microsoft Entra ID原微软 AAD中的租户 ID
*/
private String tenantId;
/**
* Coding 企业名称

View File

@@ -14,9 +14,6 @@ import org.dromara.common.social.gitea.AuthGiteaRequest;
import org.dromara.common.social.maxkey.AuthMaxKeyRequest;
import org.dromara.common.social.topiam.AuthTopIamRequest;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* 认证授权工具类
*
@@ -43,7 +40,7 @@ public class SocialUtils {
AuthConfig.AuthConfigBuilder builder = AuthConfig.builder()
.clientId(obj.getClientId())
.clientSecret(obj.getClientSecret())
.redirectUri(URLEncoder.encode(obj.getRedirectUri(), StandardCharsets.UTF_8))
.redirectUri(obj.getRedirectUri())
.scopes(obj.getScopes());
return switch (source.toLowerCase()) {
case "dingtalk" -> new AuthDingTalkV2Request(builder.build(), STATE_CACHE);
@@ -60,7 +57,7 @@ public class SocialUtils {
case "taobao" -> new AuthTaobaoRequest(builder.build(), STATE_CACHE);
case "douyin" -> new AuthDouyinRequest(builder.build(), STATE_CACHE);
case "linkedin" -> new AuthLinkedinRequest(builder.build(), STATE_CACHE);
case "microsoft" -> new AuthMicrosoftRequest(builder.build(), STATE_CACHE);
case "microsoft" -> new AuthMicrosoftRequest(builder.tenantId(obj.getTenantId()).build(), STATE_CACHE);
case "renren" -> new AuthRenrenRequest(builder.build(), STATE_CACHE);
case "stack_overflow" -> new AuthStackOverflowRequest(builder.stackOverflowKey(obj.getStackOverflowKey()).build(), STATE_CACHE);
case "huawei" -> new AuthHuaweiV3Request(builder.build(), STATE_CACHE);

View File

@@ -1,14 +1,21 @@
package org.dromara.common.sse.core;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.sse.dto.SseMessageDto;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
@@ -26,6 +33,12 @@ public class SseEmitterManager {
private final static Map<Long, Map<String, SseEmitter>> USER_TOKEN_EMITTERS = new ConcurrentHashMap<>();
public SseEmitterManager() {
// 定时执行 SSE 心跳检测
SpringUtils.getBean(ScheduledExecutorService.class)
.scheduleWithFixedDelay(this::sseMonitor, 60L, 60L, TimeUnit.SECONDS);
}
/**
* 建立与指定用户的 SSE 连接
*
@@ -38,6 +51,12 @@ public class SseEmitterManager {
// 每个用户可以有多个 SSE 连接,通过 token 进行区分
Map<String, SseEmitter> emitters = USER_TOKEN_EMITTERS.computeIfAbsent(userId, k -> new ConcurrentHashMap<>());
// 关闭已存在的SseEmitter防止超过最大连接数
SseEmitter oldEmitter = emitters.remove(token);
if (oldEmitter != null) {
oldEmitter.complete();
}
// 创建一个新的 SseEmitter 实例,超时时间设置为一天 避免连接之后直接关闭浏览器导致连接停滞
SseEmitter emitter = new SseEmitter(86400000L);
@@ -97,6 +116,44 @@ public class SseEmitterManager {
}
}
/**
* SSE 心跳检测,关闭无效连接
*/
public void sseMonitor() {
final SseEmitter.SseEventBuilder heartbeat = SseEmitter.event().comment("heartbeat");
// 记录需要移除的用户ID
List<Long> toRemoveUsers = new ArrayList<>();
USER_TOKEN_EMITTERS.forEach((userId, emitterMap) -> {
if (CollUtil.isEmpty(emitterMap)) {
toRemoveUsers.add(userId);
return;
}
emitterMap.entrySet().removeIf(entry -> {
try {
entry.getValue().send(heartbeat);
return false;
} catch (Exception ex) {
try {
entry.getValue().complete();
} catch (Exception ignore) {
// 忽略重复关闭异常
}
return true; // 发送失败 → 移除该连接
}
});
// 移除空连接用户
if (emitterMap.isEmpty()) {
toRemoveUsers.add(userId);
}
});
// 循环结束后统一清理空用户,避免并发修改异常
toRemoveUsers.forEach(USER_TOKEN_EMITTERS::remove);
}
/**
* 订阅SSE消息主题并提供一个消费者函数来处理接收到的消息
*

View File

@@ -55,7 +55,7 @@ public class TenantHelper {
/**
* 开启忽略租户(开启后需手动调用 {@link #disableIgnore()} 关闭)
*/
public static void enableIgnore() {
private static void enableIgnore() {
IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
if (ObjectUtil.isNull(ignoreStrategy)) {
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().tenantLine(true).build());
@@ -69,7 +69,7 @@ public class TenantHelper {
/**
* 关闭忽略租户
*/
public static void disableIgnore() {
private static void disableIgnore() {
IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
if (ObjectUtil.isNotNull(ignoreStrategy)) {
boolean noOtherIgnoreStrategy = !Boolean.TRUE.equals(ignoreStrategy.getDynamicTableName())

View File

@@ -46,8 +46,14 @@ public class TranslationHandler extends JsonSerializer<Object> implements Contex
gen.writeNull();
return;
}
Object result = trans.translation(value, translation.other());
gen.writeObject(result);
try {
Object result = trans.translation(value, translation.other());
gen.writeObject(result);
} catch (Exception e) {
log.error("翻译处理异常type: {}, value: {}", translation.type(), value, e);
// 出现异常时输出原始值而不是中断序列化
gen.writeObject(value);
}
} else {
gen.writeObject(value);
}

View File

@@ -20,7 +20,7 @@ public class NicknameTranslationImpl implements TranslationInterface<String> {
@Override
public String translation(Object key, String other) {
if (key instanceof Long id) {
return userService.selectNicknameByIds(id.toString());
return userService.selectNicknameById(id);
} else if (key instanceof String ids) {
return userService.selectNicknameByIds(ids);
}

View File

@@ -1,5 +1,6 @@
package org.dromara.common.translation.core.impl;
import cn.hutool.core.convert.Convert;
import org.dromara.common.core.service.UserService;
import org.dromara.common.translation.annotation.TranslationType;
import org.dromara.common.translation.constant.TransConstant;
@@ -19,9 +20,6 @@ public class UserNameTranslationImpl implements TranslationInterface<String> {
@Override
public String translation(Object key, String other) {
if (key instanceof Long id) {
return userService.selectUserNameById(id);
}
return null;
return userService.selectUserNameById(Convert.toLong(key));
}
}

View File

@@ -14,7 +14,9 @@ import org.dromara.common.core.exception.SseException;
import org.dromara.common.core.exception.base.BaseException;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.expression.ExpressionException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
@@ -23,6 +25,8 @@ import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
@@ -42,7 +46,7 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public R<Void> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
HttpServletRequest request) {
HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod());
return R.fail(HttpStatus.HTTP_BAD_METHOD, e.getMessage());
@@ -123,7 +127,7 @@ public class GlobalExceptionHandler {
*/
@ResponseStatus(org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(IOException.class)
public void handleRuntimeException(IOException e, HttpServletRequest request) {
public void handleIoException(IOException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
if (requestURI.contains("sse")) {
// sse 经常性连接中断 例如关闭浏览器 直接屏蔽
@@ -132,6 +136,13 @@ public class GlobalExceptionHandler {
log.error("请求地址'{}',连接中断", requestURI, e);
}
/**
* sse 连接超时异常 不需要处理
*/
@ExceptionHandler(AsyncRequestTimeoutException.class)
public void handleRuntimeException(AsyncRequestTimeoutException e) {
}
/**
* 拦截未知的运行时异常
*/
@@ -182,6 +193,16 @@ public class GlobalExceptionHandler {
return R.fail(message);
}
/**
* 方法参数校验异常 用于处理 @Validated 注解
*/
@ExceptionHandler(HandlerMethodValidationException.class)
public R<Void> handlerMethodValidationException(HandlerMethodValidationException e) {
log.error(e.getMessage());
String message = StreamUtils.join(e.getAllErrors(), MessageSourceResolvable::getDefaultMessage, ", ");
return R.fail(message);
}
/**
* JSON 解析异常Jackson 在处理 JSON 格式出错时抛出)
* 可能是请求体格式非法,也可能是服务端反序列化失败
@@ -202,4 +223,13 @@ public class GlobalExceptionHandler {
return R.fail(HttpStatus.HTTP_BAD_REQUEST, "请求参数格式错误:" + e.getMostSpecificCause().getMessage());
}
/**
* SpEL 表达式相关异常
*/
@ExceptionHandler(ExpressionException.class)
public R<Void> handleSpelException(ExpressionException e, HttpServletRequest request) {
log.error("请求地址'{}'SpEL解析异常: {}", request.getRequestURI(), e.getMessage());
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, "SpEL解析失败" + e.getMessage());
}
}

View File

@@ -2,11 +2,17 @@ package org.dromara.common.web.interceptor;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.StopWatch;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.web.filter.RepeatedlyRequestWrapper;
@@ -14,8 +20,10 @@ import org.springframework.http.MediaType;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import java.io.BufferedReader;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* web的调用时间统计拦截器
@@ -31,19 +39,25 @@ public class PlusWebInvokeTimeInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String url = request.getMethod() + " " + request.getRequestURI();
// 打印请求参数
if (isJsonRequest(request)) {
String jsonParam = "";
if (request instanceof RepeatedlyRequestWrapper) {
BufferedReader reader = request.getReader();
jsonParam = IoUtil.read(reader);
jsonParam = IoUtil.read(request.getReader());
if (StringUtils.isNotBlank(jsonParam)) {
ObjectMapper objectMapper = JsonUtils.getObjectMapper();
JsonNode rootNode = objectMapper.readTree(jsonParam);
removeSensitiveFields(rootNode, SystemConstants.EXCLUDE_PROPERTIES);
jsonParam = rootNode.toString();
}
}
log.info("[PLUS]开始请求 => URL[{}],参数类型[json],参数:[{}]", url, jsonParam);
} else {
Map<String, String[]> parameterMap = request.getParameterMap();
if (MapUtil.isNotEmpty(parameterMap)) {
String parameters = JsonUtils.toJsonString(parameterMap);
Map<String, String[]> map = new LinkedHashMap<>(parameterMap);
MapUtil.removeAny(map, SystemConstants.EXCLUDE_PROPERTIES);
String parameters = JsonUtils.toJsonString(map);
log.info("[PLUS]开始请求 => URL[{}],参数类型[param],参数:[{}]", url, parameters);
} else {
log.info("[PLUS]开始请求 => URL[{}],无参数", url);
@@ -57,6 +71,30 @@ public class PlusWebInvokeTimeInterceptor implements HandlerInterceptor {
return true;
}
private void removeSensitiveFields(JsonNode node, String[] excludeProperties) {
if (node == null) {
return;
}
if (node.isObject()) {
ObjectNode objectNode = (ObjectNode) node;
// 收集要删除的字段名(避免 ConcurrentModification
Set<String> fieldsToRemove = new HashSet<>();
objectNode.fieldNames().forEachRemaining(fieldName -> {
if (ArrayUtil.contains(excludeProperties, fieldName)) {
fieldsToRemove.add(fieldName);
}
});
fieldsToRemove.forEach(objectNode::remove);
// 递归处理子节点
objectNode.elements().forEachRemaining(child -> removeSensitiveFields(child, excludeProperties));
} else if (node.isArray()) {
ArrayNode arrayNode = (ArrayNode) node;
for (JsonNode child : arrayNode) {
removeSensitiveFields(child, excludeProperties);
}
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

View File

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

View File

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

View File

@@ -62,6 +62,8 @@ public class TestDemoServiceImpl implements ITestDemoService {
private LambdaQueryWrapper<TestDemo> buildQueryWrapper(TestDemoBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<TestDemo> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getDeptId() != null, TestDemo::getDeptId, bo.getDeptId());
lqw.eq(bo.getUserId() != null, TestDemo::getUserId, bo.getUserId());
lqw.like(StringUtils.isNotBlank(bo.getTestKey()), TestDemo::getTestKey, bo.getTestKey());
lqw.eq(StringUtils.isNotBlank(bo.getValue()), TestDemo::getValue, bo.getValue());
lqw.between(params.get("beginCreateTime") != null && params.get("endCreateTime") != null,

View File

@@ -2,6 +2,7 @@ package org.dromara.demo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.demo.domain.TestTree;
@@ -9,7 +10,6 @@ import org.dromara.demo.domain.bo.TestTreeBo;
import org.dromara.demo.domain.vo.TestTreeVo;
import org.dromara.demo.mapper.TestTreeMapper;
import org.dromara.demo.service.ITestTreeService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Collection;
@@ -44,6 +44,8 @@ public class TestTreeServiceImpl implements ITestTreeService {
private LambdaQueryWrapper<TestTree> buildQueryWrapper(TestTreeBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<TestTree> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getDeptId() != null, TestTree::getDeptId, bo.getDeptId());
lqw.eq(bo.getUserId() != null, TestTree::getUserId, bo.getUserId());
lqw.like(StringUtils.isNotBlank(bo.getTreeName()), TestTree::getTreeName, bo.getTreeName());
lqw.between(params.get("beginCreateTime") != null && params.get("endCreateTime") != null,
TestTree::getCreateTime, params.get("beginCreateTime"), params.get("endCreateTime"));

View File

@@ -90,10 +90,12 @@ public class GenController extends BaseController {
/**
* 导入表结构(保存)
*
* @param tables 表名串
* @param tables 表名串
* @param dataName 数据源名称
*/
@SaCheckPermission("tool:gen:import")
@Log(title = "代码生成", businessType = BusinessType.IMPORT)
@Lock4j(keys = {"#dataName"}, acquireTimeout = 10000)
@RepeatSubmit()
@PostMapping("/importTable")
public R<Void> importTableSave(String tables, String dataName) {
@@ -175,7 +177,7 @@ public class GenController extends BaseController {
*/
@SaCheckPermission("tool:gen:edit")
@Log(title = "代码生成", businessType = BusinessType.UPDATE)
@Lock4j
@Lock4j(keys = {"#tableId"}, acquireTimeout = 5000)
@GetMapping("/synchDb/{tableId}")
public R<Void> synchDb(@PathVariable("tableId") Long tableId) {
genTableService.synchDb(tableId);
@@ -214,7 +216,7 @@ public class GenController extends BaseController {
*/
@SaCheckPermission("tool:gen:list")
@GetMapping(value = "/getDataNames")
public R<Object> getCurrentDataSourceNameList(){
public R<Object> getCurrentDataSourceNameList() {
return R.ok(DataBaseHelper.getDataSourceNameList());
}
}

View File

@@ -4,13 +4,12 @@ import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.ibatis.type.JdbcType;
import jakarta.validation.constraints.NotBlank;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.domain.BaseEntity;
/**
* 代码生成业务字段表 gen_table_column
@@ -115,6 +114,7 @@ public class GenTableColumn extends BaseEntity {
/**
* 字典类型
*/
@TableField(updateStrategy = FieldStrategy.ALWAYS, jdbcType = JdbcType.VARCHAR)
private String dictType;
/**

View File

@@ -8,7 +8,6 @@ import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
@@ -28,6 +27,7 @@ import org.dromara.common.core.utils.file.FileUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.utils.IdGeneratorUtil;
import org.dromara.generator.constant.GenConstants;
import org.dromara.generator.domain.GenTable;
import org.dromara.generator.domain.GenTableColumn;
@@ -60,7 +60,6 @@ public class GenTableServiceImpl implements IGenTableService {
private final GenTableMapper baseMapper;
private final GenTableColumnMapper genTableColumnMapper;
private final IdentifierGenerator identifierGenerator;
private static final String[] TABLE_IGNORE = new String[]{"sj_", "flow_", "gen_"};
@@ -322,7 +321,7 @@ public class GenTableServiceImpl implements IGenTableService {
GenTable table = baseMapper.selectGenTableById(tableId);
List<Long> menuIds = new ArrayList<>();
for (int i = 0; i < 6; i++) {
menuIds.add(identifierGenerator.nextId(null).longValue());
menuIds.add(IdGeneratorUtil.nextLongId());
}
table.setMenuIds(menuIds);
// 设置主键列信息
@@ -468,7 +467,7 @@ public class GenTableServiceImpl implements IGenTableService {
GenTable table = baseMapper.selectGenTableById(tableId);
List<Long> menuIds = new ArrayList<>();
for (int i = 0; i < 6; i++) {
menuIds.add(identifierGenerator.nextId(null).longValue());
menuIds.add(IdGeneratorUtil.nextLongId());
}
table.setMenuIds(menuIds);
// 设置主键列信息

View File

@@ -1,26 +1,27 @@
package org.dromara.system.controller.system;
import java.util.List;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.mybatis.core.page.PageQuery;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.bo.SysClientBo;
import org.dromara.system.service.ISysClientService;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.system.domain.bo.SysClientBo;
import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.service.ISysClientService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 客户端管理
@@ -76,6 +77,9 @@ public class SysClientController extends BaseController {
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody SysClientBo bo) {
if (!sysClientService.checkClickKeyUnique(bo)) {
return R.fail("新增客户端'" + bo.getClientKey() + "'失败客户端key已存在");
}
return toAjax(sysClientService.insertByBo(bo));
}
@@ -87,6 +91,9 @@ public class SysClientController extends BaseController {
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody SysClientBo bo) {
if (!sysClientService.checkClickKeyUnique(bo)) {
return R.fail("修改客户端'" + bo.getClientKey() + "'失败客户端key已存在");
}
return toAjax(sysClientService.updateByBo(bo));
}

View File

@@ -2,21 +2,20 @@ package org.dromara.system.controller.system;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.ObjectUtil;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotEmpty;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.QueryGroup;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.system.domain.bo.SysOssBo;
import org.dromara.system.domain.vo.SysOssUploadVo;
import org.dromara.system.domain.vo.SysOssVo;
import org.dromara.system.service.ISysOssService;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotEmpty;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -70,9 +69,6 @@ public class SysOssController extends BaseController {
@Log(title = "OSS对象存储", businessType = BusinessType.INSERT)
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<SysOssUploadVo> upload(@RequestPart("file") MultipartFile file) {
if (ObjectUtil.isNull(file)) {
return R.fail("上传文件不能为空");
}
SysOssVo oss = ossService.upload(file);
SysOssUploadVo uploadVo = new SysOssUploadVo();
uploadVo.setUrl(oss.getUrl());

View File

@@ -2,6 +2,7 @@ package org.dromara.system.controller.system;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.digest.BCrypt;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
@@ -114,7 +115,7 @@ public class SysProfileController extends BaseController {
@Log(title = "用户头像", businessType = BusinessType.UPDATE)
@PostMapping(value = "/avatar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<AvatarVo> avatar(@RequestPart("avatarfile") MultipartFile avatarfile) {
if (!avatarfile.isEmpty()) {
if (ObjectUtil.isNotNull(avatarfile) && !avatarfile.isEmpty()) {
String extension = FileUtil.extName(avatarfile.getOriginalFilename());
if (!StringUtils.equalsAnyIgnoreCase(extension, MimeTypeUtils.IMAGE_EXTENSION)) {
return R.fail("文件格式不正确,请上传" + Arrays.toString(MimeTypeUtils.IMAGE_EXTENSION) + "格式");

View File

@@ -193,4 +193,19 @@ public class SysTenantController extends BaseController {
return R.ok("同步租户字典成功");
}
/**
* 同步租户参数配置
*/
@SaCheckRole(TenantConstants.SUPER_ADMIN_ROLE_KEY)
@Log(title = "租户管理", businessType = BusinessType.INSERT)
@Lock4j
@GetMapping("/syncTenantConfig")
public R<Void> syncTenantConfig() {
if (!TenantHelper.isEnable()) {
return R.fail("当前未开启租户模式");
}
tenantService.syncTenantConfig();
return R.ok("同步租户参数配置成功");
}
}

View File

@@ -9,6 +9,8 @@ import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.core.constant.RegexConstants;
import org.dromara.common.json.validate.JsonPattern;
import org.dromara.common.json.validate.JsonType;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.system.domain.SysMenu;
@@ -61,6 +63,7 @@ public class SysMenuBo extends BaseEntity {
/**
* 路由参数
*/
@JsonPattern(type = JsonType.OBJECT, message = "路由参数必须符合JSON格式")
private String queryParam;
/**

View File

@@ -1,10 +1,9 @@
package org.dromara.system.service;
import org.dromara.system.domain.SysClient;
import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.bo.SysClientBo;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.system.domain.bo.SysClientBo;
import org.dromara.system.domain.vo.SysClientVo;
import java.util.Collection;
import java.util.List;
@@ -57,4 +56,11 @@ public interface ISysClientService {
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 校验客户端key是否唯一
*
* @param client 客户端信息
* @return 结果
*/
boolean checkClickKeyUnique(SysClientBo client);
}

View File

@@ -1,9 +1,9 @@
package org.dromara.system.service;
import org.dromara.system.domain.vo.SysTenantVo;
import org.dromara.system.domain.bo.SysTenantBo;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.system.domain.bo.SysTenantBo;
import org.dromara.system.domain.vo.SysTenantVo;
import java.util.Collection;
import java.util.List;
@@ -84,4 +84,9 @@ public interface ISysTenantService {
* 同步租户字典
*/
void syncTenantDict();
/**
* 同步租户参数配置
*/
void syncTenantConfig();
}

View File

@@ -1,6 +1,7 @@
package org.dromara.system.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -136,4 +137,19 @@ public class SysClientServiceImpl implements ISysClientService {
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
return baseMapper.deleteByIds(ids) > 0;
}
/**
* 校验客户端key是否唯一
*
* @param client 客户端信息
* @return 结果
*/
@Override
public boolean checkClickKeyUnique(SysClientBo client) {
boolean exist = baseMapper.exists(new LambdaQueryWrapper<SysClient>()
.eq(SysClient::getClientKey, client.getClientKey())
.ne(ObjectUtil.isNotNull(client.getId()), SysClient::getId, client.getId()));
return !exist;
}
}

View File

@@ -1,6 +1,7 @@
package org.dromara.system.service.impl;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -14,6 +15,7 @@ import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.ObjectUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.redis.utils.CacheUtils;
@@ -82,6 +84,7 @@ public class SysConfigServiceImpl implements ISysConfigService, ConfigService {
/**
* 获取注册开关
*
* @param tenantId 租户id
* @return true开启false关闭
*/
@@ -212,4 +215,54 @@ public class SysConfigServiceImpl implements ISysConfigService, ConfigService {
return SpringUtils.getAopProxy(this).selectConfigByKey(configKey);
}
/**
* 根据参数 key 获取 Map 类型的配置
*
* @param configKey 参数 key
* @return Dict 对象,如果配置为空或无法解析,返回空 Dict
*/
@Override
public Dict getConfigMap(String configKey) {
String configValue = getConfigValue(configKey);
return JsonUtils.parseMap(configValue);
}
/**
* 根据参数 key 获取 Map 类型的配置列表
*
* @param configKey 参数 key
* @return Dict 列表,如果配置为空或无法解析,返回空列表
*/
@Override
public List<Dict> getConfigArrayMap(String configKey) {
String configValue = getConfigValue(configKey);
return JsonUtils.parseArrayMap(configValue);
}
/**
* 根据参数 key 获取指定类型的配置对象
*
* @param configKey 参数 key
* @param clazz 目标对象类型
* @return 对象实例,如果配置为空或无法解析,返回 null
*/
@Override
public <T> T getConfigObject(String configKey, Class<T> clazz) {
String configValue = getConfigValue(configKey);
return JsonUtils.parseObject(configValue, clazz);
}
/**
* 根据参数 key 获取指定类型的配置列表=
*
* @param configKey 参数 key
* @param clazz 目标元素类型
* @return 指定类型列表,如果配置为空或无法解析,返回空列表
*/
@Override
public <T> List<T> getConfigArray(String configKey, Class<T> clazz) {
String configValue = getConfigValue(configKey);
return JsonUtils.parseArray(configValue, clazz);
}
}

View File

@@ -241,6 +241,8 @@ public class SysMenuServiceImpl implements ISysMenuService {
.setWeight(menu.getOrderNum());
menuTree.put("menuType", menu.getMenuType());
menuTree.put("icon", menu.getIcon());
menuTree.put("visible", menu.getVisible());
menuTree.put("status", menu.getStatus());
});
}

View File

@@ -192,6 +192,9 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
*/
@Override
public SysOssVo upload(MultipartFile file) {
if (ObjectUtil.isNull(file) || file.isEmpty()) {
throw new ServiceException("上传文件不能为空");
}
String originalfileName = file.getOriginalFilename();
String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
OssClient storage = OssFactory.instance();
@@ -216,12 +219,16 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
*/
@Override
public SysOssVo upload(File file) {
if (ObjectUtil.isNull(file) || !file.isFile() || file.length() <= 0) {
throw new ServiceException("上传文件不能为空");
}
String originalfileName = file.getName();
String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
OssClient storage = OssFactory.instance();
long length = file.length();
UploadResult uploadResult = storage.uploadSuffix(file, suffix);
SysOssExt ext1 = new SysOssExt();
ext1.setFileSize(file.length());
ext1.setFileSize(length);
// 保存文件信息
return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult, ext1);
}
@@ -270,7 +277,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
OssClient storage = OssFactory.instance(oss.getService());
// 仅修改桶类型为 private 的URL临时URL时长为120s
if (AccessPolicyType.PRIVATE == storage.getAccessPolicy()) {
oss.setUrl(storage.getPrivateUrl(oss.getFileName(), Duration.ofSeconds(120)));
oss.setUrl(storage.createPresignedGetUrl(oss.getFileName(), Duration.ofSeconds(120)));
}
return oss;
}

View File

@@ -508,4 +508,60 @@ public class SysTenantServiceImpl implements ISysTenantService {
}
}
/**
* 同步租户参数配置
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void syncTenantConfig() {
// 查询超管 所有参数配置
List<SysConfig> configList = TenantHelper.ignore(() -> configMapper.selectList());
// 所有租户参数配置
Map<String, List<SysConfig>> configMap = StreamUtils.groupByKey(configList, TenantEntity::getTenantId);
// 默认租户字典类型列表
List<SysConfig> defaultConfigList = configMap.get(TenantConstants.DEFAULT_TENANT_ID);
// 获取所有租户编号
List<String> tenantIds = baseMapper.selectObjs(
new LambdaQueryWrapper<SysTenant>().select(SysTenant::getTenantId)
.eq(SysTenant::getStatus, SystemConstants.NORMAL), x -> {
return Convert.toStr(x);
});
// 待入库的字典类型和字典数据
List<SysConfig> saveConfigList = new ArrayList<>();
// 待同步的租户编号(用于清除对于租户的字典缓存)
Set<String> syncTenantIds = new HashSet<>();
// 循环所有租户,处理需要同步的数据
for (String tenantId : tenantIds) {
// 排除默认租户
if (TenantConstants.DEFAULT_TENANT_ID.equals(tenantId)) {
continue;
}
// 根据默认租户的字典类型进行数据同步
for (SysConfig config : defaultConfigList) {
// 获取当前租户的字典类型列表
List<String> typeList = StreamUtils.toList(configMap.get(tenantId), SysConfig::getConfigKey);
if (!typeList.contains(config.getConfigKey())) {
SysConfig type = BeanUtil.toBean(config, SysConfig.class);
type.setConfigId(null);
type.setTenantId(tenantId);
type.setCreateTime(null);
type.setUpdateTime(null);
syncTenantIds.add(tenantId);
saveConfigList.add(type);
}
}
}
TenantHelper.ignore(() -> {
if (CollUtil.isNotEmpty(saveConfigList)) {
configMapper.insertBatch(saveConfigList);
}
});
for (String tenantId : syncTenantIds) {
TenantHelper.dynamic(tenantId, () -> CacheUtils.clear(CacheNames.SYS_CONFIG));
}
}
}

View File

@@ -103,7 +103,7 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
w.in(SysUser::getDeptId, ids);
}).orderByAsc(SysUser::getUserId);
if (StringUtils.isNotBlank(user.getExcludeUserIds())) {
wrapper.notIn(SysUser::getUserId, StringUtils.splitList(user.getExcludeUserIds()));
wrapper.notIn(SysUser::getUserId, StringUtils.splitTo(user.getExcludeUserIds(), Convert::toLong));
}
return wrapper;
}

View File

@@ -92,4 +92,20 @@ public interface FlowConstant {
* 业务编码
*/
String BUSINESS_CODE = "businessCode";
/**
* 忽略-办理权限校验true忽略false不忽略
*/
String VAR_IGNORE = "ignore";
/**
* 忽略-委派处理true忽略false不忽略
*/
String VAR_IGNORE_DEPUTE = "ignoreDepute";
/**
* 忽略-会签票签处理true忽略false不忽略
*/
String VAR_IGNORE_COOPERATE = "ignoreCooperate";
}

View File

@@ -30,7 +30,7 @@ public enum ButtonPermissionEnum implements NodeExtEnum {
/**
* 是否能抄送
*/
COPY("是否能抄送", "copy", false),
COPY("是否能抄送", "copy", true),
/**
* 是否显示退回

View File

@@ -187,6 +187,7 @@ public class FlwDefinitionController extends BaseController {
@RepeatSubmit()
@PutMapping("/active/{id}")
@Transactional(rollbackFor = Exception.class)
@Log(title = "流程定义", businessType = BusinessType.UPDATE)
public R<Boolean> active(@PathVariable Long id, @RequestParam boolean active) {
return R.ok(active ? defService.active(id) : defService.unActive(id));
}

View File

@@ -1,7 +1,9 @@
package org.dromara.workflow.controller;
import cn.hutool.core.convert.Convert;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
@@ -75,8 +77,9 @@ public class FlwInstanceController extends BaseController {
* @param businessIds 业务id
*/
@DeleteMapping("/deleteByBusinessIds/{businessIds}")
@Log(title = "流程实例管理", businessType = BusinessType.DELETE)
public R<Void> deleteByBusinessIds(@PathVariable List<Long> businessIds) {
return toAjax(flwInstanceService.deleteByBusinessIds(businessIds));
return toAjax(flwInstanceService.deleteByBusinessIds(StreamUtils.toList(businessIds, Convert::toStr)));
}
/**
@@ -85,6 +88,7 @@ public class FlwInstanceController extends BaseController {
* @param instanceIds 实例id
*/
@DeleteMapping("/deleteByInstanceIds/{instanceIds}")
@Log(title = "流程实例管理", businessType = BusinessType.DELETE)
public R<Void> deleteByInstanceIds(@PathVariable List<Long> instanceIds) {
return toAjax(flwInstanceService.deleteByInstanceIds(instanceIds));
}
@@ -95,6 +99,7 @@ public class FlwInstanceController extends BaseController {
* @param instanceIds 实例id
*/
@DeleteMapping("/deleteHisByInstanceIds/{instanceIds}")
@Log(title = "流程实例管理", businessType = BusinessType.DELETE)
public R<Void> deleteHisByInstanceIds(@PathVariable List<Long> instanceIds) {
return toAjax(flwInstanceService.deleteHisByInstanceIds(instanceIds));
}
@@ -106,6 +111,7 @@ public class FlwInstanceController extends BaseController {
*/
@RepeatSubmit()
@PutMapping("/cancelProcessApply")
@Log(title = "流程实例管理", businessType = BusinessType.UPDATE)
public R<Void> cancelProcessApply(@RequestBody FlowCancelBo bo) {
return toAjax(flwInstanceService.cancelProcessApply(bo));
}
@@ -118,6 +124,7 @@ public class FlwInstanceController extends BaseController {
*/
@RepeatSubmit()
@PutMapping("/active/{id}")
@Log(title = "流程实例管理", businessType = BusinessType.UPDATE)
public R<Boolean> active(@PathVariable Long id, @RequestParam boolean active) {
return R.ok(active ? insService.active(id) : insService.unActive(id));
}
@@ -160,6 +167,7 @@ public class FlwInstanceController extends BaseController {
*/
@RepeatSubmit()
@PutMapping("/updateVariable")
@Log(title = "流程实例管理", businessType = BusinessType.UPDATE)
public R<Void> updateVariable(@Validated @RequestBody FlowVariableBo bo) {
return toAjax(flwInstanceService.updateVariable(bo));
}

View File

@@ -23,7 +23,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 流程spel达式定义
* 流程spel达式定义
*
* @author Michelle.Chung
* @date 2025-07-04
@@ -38,7 +38,7 @@ public class FlwSpelController extends BaseController {
private final IFlwSpelService flwSpelService;
/**
* 查询流程spel达式定义列表
* 查询流程spel达式定义列表
*/
@SaCheckPermission("workflow:spel:list")
@GetMapping("/list")
@@ -47,7 +47,7 @@ public class FlwSpelController extends BaseController {
}
/**
* 获取流程spel达式定义详细信息
* 获取流程spel达式定义详细信息
*
* @param id 主键
*/
@@ -58,10 +58,10 @@ public class FlwSpelController extends BaseController {
}
/**
* 新增流程spel达式定义
* 新增流程spel达式定义
*/
@SaCheckPermission("workflow:spel:add")
@Log(title = "流程spel达式定义", businessType = BusinessType.INSERT)
@Log(title = "流程spel达式定义", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody FlowSpelBo bo) {
@@ -69,10 +69,10 @@ public class FlwSpelController extends BaseController {
}
/**
* 修改流程spel达式定义
* 修改流程spel达式定义
*/
@SaCheckPermission("workflow:spel:edit")
@Log(title = "流程spel达式定义", businessType = BusinessType.UPDATE)
@Log(title = "流程spel达式定义", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody FlowSpelBo bo) {
@@ -80,12 +80,12 @@ public class FlwSpelController extends BaseController {
}
/**
* 删除流程spel达式定义
* 删除流程spel达式定义
*
* @param ids 主键串
*/
@SaCheckPermission("workflow:spel:remove")
@Log(title = "流程spel达式定义", businessType = BusinessType.DELETE)
@Log(title = "流程spel达式定义", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
return toAjax(flwSpelService.deleteWithValidByIds(List.of(ids), true));

View File

@@ -216,6 +216,7 @@ public class FlwTaskController extends BaseController {
* @return 结果
*/
@PostMapping("/urgeTask")
@Log(title = "任务管理", businessType = BusinessType.INSERT)
public R<Void> urgeTask(@RequestBody FlowUrgeTaskBo bo) {
return toAjax(flwTaskService.urgeTask(bo));
}

View File

@@ -10,7 +10,7 @@ import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.io.Serial;
/**
* 流程spel达式定义对象 flow_spel
* 流程spel达式定义对象 flow_spel
*
* @author Michelle.Chung
* @date 2025-07-04

View File

@@ -1,6 +1,5 @@
package org.dromara.workflow.domain.bo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.dromara.common.core.validate.AddGroup;

View File

@@ -50,6 +50,6 @@ public class FlowInstanceBo implements Serializable {
/**
* 申请人Ids
*/
private List<Long> createByIds;
private List<String> createByIds;
}

View File

@@ -10,7 +10,7 @@ import jakarta.validation.constraints.*;
import org.dromara.workflow.domain.FlowSpel;
/**
* 流程spel达式定义业务对象 flow_spel
* 流程spel达式定义业务对象 flow_spel
*
* @author Michelle.Chung
* @date 2025-07-04

View File

@@ -1,10 +1,13 @@
package org.dromara.workflow.domain.bo;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 任务请求对象
@@ -42,6 +45,11 @@ public class FlowTaskBo implements Serializable {
*/
private Long instanceId;
/**
* 流程状态
*/
private String flowStatus;
/**
* 权限列表
*/
@@ -52,4 +60,10 @@ public class FlowTaskBo implements Serializable {
*/
private List<Long> createByIds;
/**
* 请求参数
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private Map<String, Object> params = new HashMap<>();
}

View File

@@ -253,4 +253,8 @@ public class FlowHisTaskVo implements Serializable {
this.cooperateTypeName = CooperateType.getValueByKey(cooperateType);
}
public String getCreateTime() {
return DateUtils.formatFriendlyTime(createTime);
}
}

View File

@@ -78,7 +78,7 @@ public class FlowInstanceVo {
private String variable;
/**
* 流程状态0待提交 1审批中 2 审批通过 3自动通过 8已完成 9已退回 10失效
* 流程状态
*/
private String flowStatus;

View File

@@ -14,7 +14,7 @@ import java.util.Date;
/**
* 流程spel达式定义视图对象 flow_spel
* 流程spel达式定义视图对象 flow_spel
*
* @author Michelle.Chung
* @date 2025-07-04

View File

@@ -1,6 +1,7 @@
package org.dromara.workflow.domain.vo;
import lombok.Data;
import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.translation.annotation.Translation;
import org.dromara.common.translation.constant.TransConstant;
import org.dromara.warm.flow.core.entity.User;
@@ -8,7 +9,6 @@ import org.dromara.workflow.common.constant.FlowConstant;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -163,7 +163,7 @@ public class FlowTaskVo implements Serializable {
/**
* 流程签署比例值 大于0为票签会签
*/
private BigDecimal nodeRatio;
private String nodeRatio;
/**
* 申请人id
@@ -212,4 +212,8 @@ public class FlowTaskVo implements Serializable {
private String businessTitle;
//业务扩展信息结束
public String getCreateTime() {
return DateUtils.formatFriendlyTime(createTime);
}
}

View File

@@ -7,6 +7,7 @@ import org.dromara.common.core.domain.event.ProcessEvent;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.warm.flow.core.entity.Instance;
import org.dromara.warm.flow.core.entity.Task;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.springframework.stereotype.Component;
@@ -55,22 +56,22 @@ public class FlowProcessEventHandler {
*
* @param flowCode 流程定义编码
* @param instance 实例数据
* @param taskId 任务id
* @param nextTask 任务
* @param params 上一个任务的办理参数
*/
public void processTaskHandler(String flowCode, Instance instance, Long taskId, Map<String, Object> params) {
public void processTaskHandler(String flowCode, Instance instance, Task nextTask, Map<String, Object> params) {
String tenantId = TenantHelper.getTenantId();
log.info("【流程任务事件发布】租户ID: {}, 流程编码: {}, 业务ID: {}, 节点类型: {}, 节点编码: {}, 节点名称: {}, 任务ID: {}",
tenantId, flowCode, instance.getBusinessId(), instance.getNodeType(), instance.getNodeCode(), instance.getNodeName(), taskId);
tenantId, flowCode, instance.getBusinessId(), nextTask.getNodeType(), nextTask.getNodeCode(), nextTask.getNodeName(), nextTask.getId());
ProcessTaskEvent processTaskEvent = new ProcessTaskEvent();
processTaskEvent.setTenantId(tenantId);
processTaskEvent.setFlowCode(flowCode);
processTaskEvent.setInstanceId(instance.getId());
processTaskEvent.setBusinessId(instance.getBusinessId());
processTaskEvent.setNodeType(instance.getNodeType());
processTaskEvent.setNodeCode(instance.getNodeCode());
processTaskEvent.setNodeName(instance.getNodeName());
processTaskEvent.setTaskId(taskId);
processTaskEvent.setNodeType(nextTask.getNodeType());
processTaskEvent.setNodeCode(nextTask.getNodeCode());
processTaskEvent.setNodeName(nextTask.getNodeName());
processTaskEvent.setTaskId(nextTask.getId());
processTaskEvent.setStatus(instance.getFlowStatus());
processTaskEvent.setParams(params);
SpringUtils.context().publishEvent(processTaskEvent);

View File

@@ -5,6 +5,7 @@ import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.enums.BusinessStatusEnum;
@@ -72,8 +73,11 @@ public class WorkflowGlobalListener implements GlobalListener {
public void start(ListenerVariable listenerVariable) {
String ext = listenerVariable.getNode().getExt();
if (StringUtils.isNotBlank(ext)) {
NodeExtVo nodeExt = nodeExtService.parseNodeExt(ext);
Map<String, Object> variable = listenerVariable.getVariable();
if (CollUtil.isNotEmpty(variable)) {
variable = new HashMap<>();
}
NodeExtVo nodeExt = nodeExtService.parseNodeExt(ext, variable);
Set<String> copyList = nodeExt.getCopySettings();
if (CollUtil.isNotEmpty(copyList)) {
List<FlowCopyBo> list = StreamUtils.toList(copyList, x -> {
@@ -104,20 +108,61 @@ public class WorkflowGlobalListener implements GlobalListener {
Definition definition = listenerVariable.getDefinition();
Instance instance = listenerVariable.getInstance();
String applyNodeCode = flwCommonService.applyNodeCode(definition.getId());
String hisStatus = flowParams != null ? flowParams.getHisStatus() : null;
for (Task flowTask : nextTasks) {
// 如果办理或者退回并行存在需要指定办理人,则直接覆盖办理人
if (variable.containsKey(flowTask.getNodeCode()) && TaskStatusEnum.isPassOrBack(flowParams.getHisStatus())) {
String userIds = variable.get(flowTask.getNodeCode()).toString();
flowTask.setPermissionList(List.of(userIds.split(StringUtils.SEPARATOR)));
variable.remove(flowTask.getNodeCode());
String nodeCode = flowTask.getNodeCode();
// 处理办理或退回时指定办理人的情况
if (TaskStatusEnum.PASS.getStatus().equals(hisStatus)) {
processTaskPermission(variable, flowTask, hisStatus);
} else if (TaskStatusEnum.BACK.getStatus().equals(hisStatus)) {
processTaskPermission(variable, flowTask, hisStatus);
}
// 如果是申请节点,则把启动人添加到办理人
if (flowTask.getNodeCode().equals(applyNodeCode)) {
if (nodeCode.equals(applyNodeCode) && StringUtils.isNotBlank(instance.getCreateBy())) {
flowTask.setPermissionList(List.of(instance.getCreateBy()));
}
}
}
/**
* 处理任务权限设置
*
* @param variable 变量集合
* @param flowTask 流程任务
* @param taskStatus 任务状态
*/
private void processTaskPermission(Map<String, Object> variable, Task flowTask, String taskStatus) {
String nodeKey = taskStatus + StrUtil.COLON + flowTask.getNodeCode();
// 检查是否存在状态相关的变量
if (!variable.containsKey(nodeKey)) {
return;
}
// 获取用户ID字符串
Object userIdsObj = variable.get(nodeKey);
if (userIdsObj == null) {
return;
}
String userIds = userIdsObj.toString();
if (StringUtils.isBlank(userIds)) {
return;
}
// 分割用户ID并设置权限列表
String[] userIdArray = userIds.split(StringUtils.SEPARATOR);
if (userIdArray.length > 0) {
flowTask.setPermissionList(List.of(userIdArray));
// 移除已处理的状态变量
variable.remove(nodeKey);
FlowEngine.insService().removeVariables(flowTask.getInstanceId(),nodeKey);
}
}
/**
* 完成监听器,当前任务完成后执行
*
@@ -144,7 +189,8 @@ public class WorkflowGlobalListener implements GlobalListener {
//申请人提交事件
Boolean submit = MapUtil.getBool(variable, FlowConstant.SUBMIT);
if (submit != null && submit) {
flowProcessEventHandler.processHandler(definition.getFlowCode(), instance, instance.getFlowStatus(), variable, true);
String status = determineFlowStatus(instance);
flowProcessEventHandler.processHandler(definition.getFlowCode(), instance, status, variable, true);
} else {
// 判断流程状态(发布:撤销,退回,作废,终止,已完成事件)
String status = determineFlowStatus(instance);
@@ -165,7 +211,7 @@ public class WorkflowGlobalListener implements GlobalListener {
//发布任务事件
if (CollUtil.isNotEmpty(nextTasks)) {
for (Task nextTask : nextTasks) {
flowProcessEventHandler.processTaskHandler(definition.getFlowCode(), instance, nextTask.getId(), params);
flowProcessEventHandler.processTaskHandler(definition.getFlowCode(), instance, nextTask, params);
}
}
if (ObjectUtil.isNull(flowParams)) {
@@ -180,12 +226,14 @@ public class WorkflowGlobalListener implements GlobalListener {
}
if (variable.containsKey(FlowConstant.FLOW_COPY_LIST)) {
List<FlowCopyBo> flowCopyList = MapUtil.get(variable, FlowConstant.FLOW_COPY_LIST, new TypeReference<>() {});
List<FlowCopyBo> flowCopyList = MapUtil.get(variable, FlowConstant.FLOW_COPY_LIST, new TypeReference<>() {
});
// 添加抄送人
flwTaskService.setCopy(task, flowCopyList);
}
if (variable.containsKey(FlowConstant.MESSAGE_TYPE)) {
List<String> messageType = MapUtil.get(variable, FlowConstant.MESSAGE_TYPE, new TypeReference<>() {});
List<String> messageType = MapUtil.get(variable, FlowConstant.MESSAGE_TYPE, new TypeReference<>() {
});
String notice = MapUtil.getStr(variable, FlowConstant.MESSAGE_NOTICE);
flwCommonService.sendMessage(definition.getFlowName(), instance.getId(), messageType, notice);
}

View File

@@ -5,7 +5,7 @@ import org.dromara.workflow.domain.vo.FlowSpelVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
/**
* 流程spel达式定义Mapper接口
* 流程spel达式定义Mapper接口
*
* @author Michelle.Chung
* @date 2025-07-04

View File

@@ -75,7 +75,7 @@ public interface IFlwInstanceService {
* @param businessIds 业务id
* @return 结果
*/
boolean deleteByBusinessIds(List<Long> businessIds);
boolean deleteByBusinessIds(List<String> businessIds);
/**
* 按照实例id删除流程实例

View File

@@ -2,6 +2,8 @@ package org.dromara.workflow.service;
import org.dromara.workflow.domain.vo.NodeExtVo;
import java.util.Map;
/**
* 流程节点扩展属性 服务层
*
@@ -24,9 +26,10 @@ public interface IFlwNodeExtService {
* {"code": "VariablesEnum", "value": "key1=value1,key2=value2"}
* ]
*
* @param ext 扩展属性 JSON 字符串
* @param ext 扩展属性 JSON 字符串
* @param variable 流程变量
* @return NodeExtVo 对象,封装按钮权限列表、抄送对象集合和自定义参数 Map
*/
NodeExtVo parseNodeExt(String ext);
NodeExtVo parseNodeExt(String ext, Map<String, Object> variable);
}

View File

@@ -12,7 +12,7 @@ import java.util.List;
import java.util.Map;
/**
* 流程spel达式定义Service接口
* 流程spel达式定义Service接口
*
* @author Michelle.Chung
* @date 2025-07-04
@@ -20,48 +20,48 @@ import java.util.Map;
public interface IFlwSpelService {
/**
* 查询流程spel达式定义
* 查询流程spel达式定义
*
* @param id 主键
* @return 流程spel达式定义
* @return 流程spel达式定义
*/
FlowSpelVo queryById(Long id);
/**
* 分页查询流程spel达式定义列表
* 分页查询流程spel达式定义列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 流程spel达式定义分页列表
* @return 流程spel达式定义分页列表
*/
TableDataInfo<FlowSpelVo> queryPageList(FlowSpelBo bo, PageQuery pageQuery);
/**
* 查询符合条件的流程spel达式定义列表
* 查询符合条件的流程spel达式定义列表
*
* @param bo 查询条件
* @return 流程spel达式定义列表
* @return 流程spel达式定义列表
*/
List<FlowSpelVo> queryList(FlowSpelBo bo);
/**
* 新增流程spel达式定义
* 新增流程spel达式定义
*
* @param bo 流程spel达式定义
* @param bo 流程spel达式定义
* @return 是否新增成功
*/
Boolean insertByBo(FlowSpelBo bo);
/**
* 修改流程spel达式定义
* 修改流程spel达式定义
*
* @param bo 流程spel达式定义
* @param bo 流程spel达式定义
* @return 是否修改成功
*/
Boolean updateByBo(FlowSpelBo bo);
/**
* 校验并批量删除流程spel达式定义信息
* 校验并批量删除流程spel达式定义信息
*
* @param ids 待删除的主键集合
* @param isValid 是否进行有效性校验

View File

@@ -206,6 +206,9 @@ public class FlwCategoryServiceImpl implements IFlwCategoryService, CategoryServ
if (ObjectUtil.isNull(oldCategory)) {
throw new ServiceException("流程分类不存在,无法修改");
}
if (oldCategory.getParentId() == 0L && category.getParentId() != 0L) {
throw new ServiceException("不允许修改顶级分类的父级节点");
}
if (!oldCategory.getParentId().equals(category.getParentId())) {
FlowCategory newParentCategory = baseMapper.selectById(category.getParentId());
if (ObjectUtil.isNotNull(newParentCategory)) {

View File

@@ -88,23 +88,28 @@ public class FlwCommonServiceImpl implements IFlwCommonService {
if (ObjectUtil.isEmpty(messageTypeEnum)) {
continue;
}
switch (messageTypeEnum) {
case SYSTEM_MESSAGE -> {
SseMessageDto dto = new SseMessageDto();
dto.setUserIds(userIds);
dto.setMessage(message);
SseMessageUtils.publishMessage(dto);
try {
switch (messageTypeEnum) {
case SYSTEM_MESSAGE -> {
SseMessageDto dto = new SseMessageDto();
dto.setUserIds(userIds);
dto.setMessage(message);
SseMessageUtils.publishMessage(dto);
}
case EMAIL_MESSAGE -> MailUtils.sendText(emails, subject, message);
case SMS_MESSAGE -> {
// TODO: 补充短信发送逻辑
log.info("【短信发送 - TODO】用户数量={} 内容={}", userList.size(), message);
}
default -> log.warn("【消息发送】未处理的消息类型:{}", messageTypeEnum);
}
case EMAIL_MESSAGE -> MailUtils.sendText(emails, subject, message);
case SMS_MESSAGE -> {
//todo 短信发送
}
default -> throw new IllegalStateException("Unexpected value: " + messageTypeEnum);
} catch (Exception ex) {
// 记录错误但不抛出,确保主逻辑不受影响
log.error("【消息发送失败】类型={},原因={}", messageTypeEnum, ex.getMessage(), ex);
}
}
}
/**
* 申请人节点编码
*

View File

@@ -208,10 +208,6 @@ public class FlwDefinitionServiceImpl implements IFlwDefinitionService {
@Override
@Transactional(rollbackFor = Exception.class)
public void syncDef(String tenantId) {
List<FlowDefinition> flowDefinitions = flowDefinitionMapper.selectList(new LambdaQueryWrapper<FlowDefinition>().eq(FlowDefinition::getTenantId, DEFAULT_TENANT_ID));
if (CollUtil.isEmpty(flowDefinitions)) {
return;
}
FlowCategory flowCategory = flwCategoryMapper.selectOne(new LambdaQueryWrapper<FlowCategory>()
.eq(FlowCategory::getTenantId, DEFAULT_TENANT_ID)
.eq(FlowCategory::getCategoryId, FlowConstant.FLOW_CATEGORY_ID));
@@ -223,6 +219,11 @@ public class FlwDefinitionServiceImpl implements IFlwDefinitionService {
flowCategory.setUpdateBy(null);
flowCategory.setUpdateTime(null);
flwCategoryMapper.insert(flowCategory);
List<FlowDefinition> flowDefinitions = flowDefinitionMapper.selectList(new LambdaQueryWrapper<FlowDefinition>().eq(FlowDefinition::getTenantId, DEFAULT_TENANT_ID));
if (CollUtil.isEmpty(flowDefinitions)) {
return;
}
List<Long> defIds = StreamUtils.toList(flowDefinitions, FlowDefinition::getId);
List<FlowNode> flowNodes = flowNodeMapper.selectList(new LambdaQueryWrapper<FlowNode>().in(FlowNode::getDefinitionId, defIds));
List<FlowSkip> flowSkips = flowSkipMapper.selectList(new LambdaQueryWrapper<FlowSkip>().in(FlowSkip::getDefinitionId, defIds));

View File

@@ -181,8 +181,8 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteByBusinessIds(List<Long> businessIds) {
List<FlowInstance> flowInstances = flowInstanceMapper.selectList(new LambdaQueryWrapper<FlowInstance>().in(FlowInstance::getBusinessId, StreamUtils.toList(businessIds, Convert::toStr)));
public boolean deleteByBusinessIds(List<String> businessIds) {
List<FlowInstance> flowInstances = flowInstanceMapper.selectList(new LambdaQueryWrapper<FlowInstance>().in(FlowInstance::getBusinessId, businessIds));
if (CollUtil.isEmpty(flowInstances)) {
log.warn("未找到对应的流程实例信息,无法执行删除操作。");
return false;
@@ -211,27 +211,17 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
Function.identity()
);
try {
// 逐一触发删除事件
instances.forEach(instance -> {
Definition definition = definitionMap.get(instance.getDefinitionId());
if (ObjectUtil.isNull(definition)) {
log.warn("实例 ID: {} 对应的流程定义信息未找到,跳过删除事件触发。", instance.getId());
return;
}
flowProcessEventHandler.processDeleteHandler(definition.getFlowCode(), instance.getBusinessId());
});
// 删除实例
boolean remove = insService.remove(instanceIds);
if (!remove) {
log.warn("删除流程实例失败!");
throw new ServiceException("删除流程实例失败");
// 逐一触发删除事件
instances.forEach(instance -> {
Definition definition = definitionMap.get(instance.getDefinitionId());
if (ObjectUtil.isNull(definition)) {
log.warn("实例 ID: {} 对应的流程定义信息未找到,跳过删除事件触发。", instance.getId());
return;
}
} catch (Exception e) {
log.warn("操作失败!{}", e.getMessage());
throw new ServiceException(e.getMessage());
}
return true;
flowProcessEventHandler.processDeleteHandler(definition.getFlowCode(), instance.getBusinessId());
});
// 删除实例
return insService.remove(instanceIds);
}
/**
@@ -254,27 +244,22 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
Definition::getId,
Function.identity()
);
try {
// 逐一触发删除事件
instances.forEach(instance -> {
Definition definition = definitionMap.get(instance.getDefinitionId());
if (ObjectUtil.isNull(definition)) {
log.warn("实例 ID: {} 对应的流程定义信息未找到,跳过删除事件触发。", instance.getId());
return;
}
flowProcessEventHandler.processDeleteHandler(definition.getFlowCode(), instance.getBusinessId());
});
List<FlowTask> flowTaskList = flwTaskService.selectByInstIds(instanceIds);
if (CollUtil.isNotEmpty(flowTaskList)) {
FlowEngine.userService().deleteByTaskIds(StreamUtils.toList(flowTaskList, FlowTask::getId));
// 逐一触发删除事件
instances.forEach(instance -> {
Definition definition = definitionMap.get(instance.getDefinitionId());
if (ObjectUtil.isNull(definition)) {
log.warn("实例 ID: {} 对应的流程定义信息未找到,跳过删除事件触发。", instance.getId());
return;
}
FlowEngine.taskService().deleteByInsIds(instanceIds);
FlowEngine.hisTaskService().deleteByInsIds(instanceIds);
FlowEngine.insService().removeByIds(instanceIds);
} catch (Exception e) {
log.warn("操作失败!{}", e.getMessage());
throw new ServiceException(e.getMessage());
flowProcessEventHandler.processDeleteHandler(definition.getFlowCode(), instance.getBusinessId());
});
List<FlowTask> flowTaskList = flwTaskService.selectByInstIds(instanceIds);
if (CollUtil.isNotEmpty(flowTaskList)) {
FlowEngine.userService().deleteByTaskIds(StreamUtils.toList(flowTaskList, FlowTask::getId));
}
FlowEngine.taskService().deleteByInsIds(instanceIds);
FlowEngine.hisTaskService().deleteByInsIds(instanceIds);
FlowEngine.insService().removeByIds(instanceIds);
return true;
}
@@ -286,29 +271,24 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean cancelProcessApply(FlowCancelBo bo) {
try {
Instance instance = selectInstByBusinessId(bo.getBusinessId());
if (instance == null) {
throw new ServiceException(ExceptionCons.NOT_FOUNT_INSTANCE);
}
Definition definition = defService.getById(instance.getDefinitionId());
if (definition == null) {
throw new ServiceException(ExceptionCons.NOT_FOUNT_DEF);
}
String message = bo.getMessage();
String userIdStr = LoginHelper.getUserIdStr();
BusinessStatusEnum.checkCancelStatus(instance.getFlowStatus());
FlowParams flowParams = FlowParams.build()
.message(message)
.flowStatus(BusinessStatusEnum.CANCEL.getStatus())
.hisStatus(BusinessStatusEnum.CANCEL.getStatus())
.handler(userIdStr)
.ignore(true);
taskService.revoke(instance.getId(), flowParams);
} catch (Exception e) {
log.error("撤销失败: {}", e.getMessage(), e);
throw new ServiceException(e.getMessage());
Instance instance = selectInstByBusinessId(bo.getBusinessId());
if (instance == null) {
throw new ServiceException(ExceptionCons.NOT_FOUNT_INSTANCE);
}
Definition definition = defService.getById(instance.getDefinitionId());
if (definition == null) {
throw new ServiceException(ExceptionCons.NOT_FOUNT_DEF);
}
String message = bo.getMessage();
String userIdStr = LoginHelper.getUserIdStr();
BusinessStatusEnum.checkCancelStatus(instance.getFlowStatus());
FlowParams flowParams = FlowParams.build()
.message(message)
.flowStatus(BusinessStatusEnum.CANCEL.getStatus())
.hisStatus(BusinessStatusEnum.CANCEL.getStatus())
.handler(userIdStr)
.ignore(true);
taskService.revoke(instance.getId(), flowParams);
return true;
}
@@ -422,20 +402,14 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
if (flowInstance == null) {
throw new ServiceException(ExceptionCons.NOT_FOUNT_INSTANCE);
}
try {
Map<String, Object> variableMap = new HashMap<>(Optional.ofNullable(flowInstance.getVariableMap()).orElse(Collections.emptyMap()));
if (!variableMap.containsKey(bo.getKey())) {
log.error("变量不存在: {}", bo.getKey());
return false;
}
variableMap.put(bo.getKey(), bo.getValue());
flowInstance.setVariable(FlowEngine.jsonConvert.objToStr(variableMap));
flowInstanceMapper.updateById(flowInstance);
} catch (Exception e) {
log.error("设置流程变量失败: {}", e.getMessage(), e);
throw new ServiceException(e.getMessage());
Map<String, Object> variableMap = new HashMap<>(Optional.ofNullable(flowInstance.getVariableMap()).orElse(Collections.emptyMap()));
if (!variableMap.containsKey(bo.getKey())) {
log.error("变量不存在: {}", bo.getKey());
return false;
}
return true;
variableMap.put(bo.getKey(), bo.getValue());
flowInstance.setVariable(FlowEngine.jsonConvert.objToStr(variableMap));
return flowInstanceMapper.updateById(flowInstance) > 0;
}
/**
@@ -480,21 +454,16 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean processInvalid(FlowInvalidBo bo) {
try {
Instance instance = insService.getById(bo.getId());
if (instance != null) {
BusinessStatusEnum.checkInvalidStatus(instance.getFlowStatus());
}
FlowParams flowParams = FlowParams.build()
.message(bo.getComment())
.flowStatus(BusinessStatusEnum.INVALID.getStatus())
.hisStatus(TaskStatusEnum.INVALID.getStatus())
.ignore(true);
taskService.terminationByInsId(bo.getId(), flowParams);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new ServiceException(e.getMessage());
Instance instance = insService.getById(bo.getId());
if (instance != null) {
BusinessStatusEnum.checkInvalidStatus(instance.getFlowStatus());
}
FlowParams flowParams = FlowParams.build()
.message(bo.getComment())
.flowStatus(BusinessStatusEnum.INVALID.getStatus())
.hisStatus(TaskStatusEnum.INVALID.getStatus())
.ignore(true);
taskService.terminationByInsId(bo.getId(), flowParams);
return true;
}
}

View File

@@ -9,6 +9,9 @@ import org.dromara.common.core.domain.dto.DictTypeDTO;
import org.dromara.common.core.service.DictService;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.warm.flow.core.FlowEngine;
import org.dromara.warm.flow.core.utils.CollUtil;
import org.dromara.warm.flow.core.utils.ExpressionUtil;
import org.dromara.warm.flow.ui.service.NodeExtService;
import org.dromara.warm.flow.ui.vo.NodeExt;
import org.dromara.workflow.common.ConditionalOnEnable;
@@ -45,7 +48,7 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService
CopySettingEnum.class.getSimpleName(),
Map.of(
"label", "抄送对象",
"type", 2,
"type", 5,
"must", false,
"multiple", false,
"desc", "设置该节点的抄送办理人"
@@ -56,7 +59,7 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService
"type", 2,
"must", false,
"multiple", false,
"desc", "节点执行时可以使用的自定义参数"
"desc", "节点执行时可设置自定义参数多个参数以逗号分隔key1=value1,key2=value2"
),
ButtonPermissionEnum.class.getSimpleName(),
Map.of(
@@ -137,7 +140,7 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService
childNode.setCode(simpleName);
// label名称
childNode.setLabel(Convert.toStr(map.get("label")));
// 1输入框 2文本域 3下拉框 4选择框
// 1输入框 2文本域 3下拉框 4选择框 5用户选择器
childNode.setType(Convert.toInt(map.get("type"), 1));
// 是否必填
childNode.setMust(Convert.toBool(map.get("must"), false));
@@ -170,7 +173,7 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService
childNode.setCode(dictType);
// label名称
childNode.setLabel(dictTypeDTO.getDictName());
// 1输入框 2文本域 3下拉框 4选择框
// 1输入框 2文本域 3下拉框 4选择框 5用户选择器
childNode.setType(3);
// 是否必填
childNode.setMust(false);
@@ -197,19 +200,23 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService
* <p>示例 JSON
* [
* {"code": "ButtonPermissionEnum", "value": "back,termination"},
* {"code": "CopySettingEnum", "value": "1"},
* {"code": "CopySettingEnum", "value": "1,3,4,#{@spelRuleComponent.selectDeptLeaderById(#deptId", "#roleId)}"},
* {"code": "VariablesEnum", "value": "key1=value1,key2=value2"}
* ]
*
* @param ext 扩展属性 JSON 字符串
* @param ext 扩展属性 JSON 字符串
* @param variable 流程变量
* @return NodeExtVo 对象,封装按钮权限列表、抄送对象集合和自定义参数 Map
*/
@Override
public NodeExtVo parseNodeExt(String ext) {
public NodeExtVo parseNodeExt(String ext, Map<String, Object> variable) {
NodeExtVo nodeExtVo = new NodeExtVo();
// 解析 JSON 为 Dict 列表
List<Dict> nodeExtMap = JsonUtils.parseArrayMap(ext);
if (ObjectUtil.isEmpty(nodeExtMap)) {
return nodeExtVo;
}
for (Dict nodeExt : nodeExtMap) {
String code = nodeExt.getStr("code");
@@ -234,8 +241,20 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService
nodeExtVo.setButtonPermissions(buttonList);
} else if (CopySettingEnum.class.getSimpleName().equals(code)) {
List<String> permissions = spelSmartSplit(value).stream()
.map(s -> {
List<String> result = ExpressionUtil.evalVariable(s, variable);
if (CollUtil.isNotEmpty(result)) {
return result;
}
return Collections.singletonList(s);
}).filter(Objects::nonNull)
.flatMap(List::stream)
.distinct()
.collect(Collectors.toList());
List<String> copySettings = FlowEngine.permissionHandler().convertPermissions(permissions);
// 解析抄送对象 ID 集合
nodeExtVo.setCopySettings(StringUtils.str2Set(value, StringUtils.SEPARATOR));
nodeExtVo.setCopySettings(new HashSet<>(copySettings));
} else if (VariablesEnum.class.getSimpleName().equals(code)) {
// 解析自定义参数
@@ -254,4 +273,82 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService
return nodeExtVo;
}
/**
* 按逗号分割字符串,但保留 #{...} 表达式和字符串常量中的逗号
*/
private static List<String> spelSmartSplit(String str) {
List<String> result = new ArrayList<>();
if (str == null || str.trim().isEmpty()) {
return result;
}
StringBuilder token = new StringBuilder();
// #{...} 的嵌套深度
int depth = 0;
// 是否在字符串常量中(" 或 '
boolean inString = false;
// 当前字符串引号类型
char stringQuote = 0;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
// 检测进入 SpEL 表达式 #{...}
if (!inString && c == '#' && depth == 0 && checkNext(str, i, '{')) {
depth++;
token.append("#{");
// 跳过 {
i++;
continue;
}
// 在表达式中遇到 { 或 } 改变嵌套深度
if (!inString && depth > 0) {
if (c == '{') {
depth++;
} else if (c == '}') {
depth--;
}
token.append(c);
continue;
}
// 检测字符串开始/结束
if (depth > 0 && (c == '"' || c == '\'')) {
if (!inString) {
inString = true;
stringQuote = c;
} else if (stringQuote == c) {
inString = false;
}
token.append(c);
continue;
}
// 外层逗号才分割
if (c == ',' && depth == 0 && !inString) {
String part = token.toString().trim();
if (!part.isEmpty()) {
result.add(part);
}
token.setLength(0);
continue;
}
token.append(c);
}
// 添加最后一个
String part = token.toString().trim();
if (!part.isEmpty()) {
result.add(part);
}
return result;
}
private static boolean checkNext(String str, int index, char expected) {
return index + 1 < str.length() && str.charAt(index + 1) == expected;
}
}

View File

@@ -28,7 +28,7 @@ import java.util.List;
import java.util.Map;
/**
* 流程spel达式定义Service业务层处理
* 流程spel达式定义Service业务层处理
*
* @author Michelle.Chung
* @date 2025-07-04
@@ -42,10 +42,10 @@ public class FlwSpelServiceImpl implements IFlwSpelService {
private final FlwSpelMapper baseMapper;
/**
* 查询流程spel达式定义
* 查询流程spel达式定义
*
* @param id 主键
* @return 流程spel达式定义
* @return 流程spel达式定义
*/
@Override
public FlowSpelVo queryById(Long id){
@@ -53,11 +53,11 @@ public class FlwSpelServiceImpl implements IFlwSpelService {
}
/**
* 分页查询流程spel达式定义列表
* 分页查询流程spel达式定义列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 流程spel达式定义分页列表
* @return 流程spel达式定义分页列表
*/
@Override
public TableDataInfo<FlowSpelVo> queryPageList(FlowSpelBo bo, PageQuery pageQuery) {
@@ -67,10 +67,10 @@ public class FlwSpelServiceImpl implements IFlwSpelService {
}
/**
* 查询符合条件的流程spel达式定义列表
* 查询符合条件的流程spel达式定义列表
*
* @param bo 查询条件
* @return 流程spel达式定义列表
* @return 流程spel达式定义列表
*/
@Override
public List<FlowSpelVo> queryList(FlowSpelBo bo) {
@@ -92,9 +92,9 @@ public class FlwSpelServiceImpl implements IFlwSpelService {
}
/**
* 新增流程spel达式定义
* 新增流程spel达式定义
*
* @param bo 流程spel达式定义
* @param bo 流程spel达式定义
* @return 是否新增成功
*/
@Override
@@ -109,9 +109,9 @@ public class FlwSpelServiceImpl implements IFlwSpelService {
}
/**
* 修改流程spel达式定义
* 修改流程spel达式定义
*
* @param bo 流程spel达式定义
* @param bo 流程spel达式定义
* @return 是否修改成功
*/
@Override
@@ -129,7 +129,7 @@ public class FlwSpelServiceImpl implements IFlwSpelService {
}
/**
* 校验并批量删除流程spel达式定义信息
* 校验并批量删除流程spel达式定义信息
*
* @param ids 待删除的主键集合
* @param isValid 是否进行有效性校验

View File

@@ -5,9 +5,10 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.lock.annotation.Lock4j;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
@@ -25,10 +26,12 @@ import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.utils.IdGeneratorUtil;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.warm.flow.core.FlowEngine;
import org.dromara.warm.flow.core.dto.FlowParams;
import org.dromara.warm.flow.core.entity.*;
import org.dromara.warm.flow.core.enums.CooperateType;
import org.dromara.warm.flow.core.enums.NodeType;
import org.dromara.warm.flow.core.enums.SkipType;
import org.dromara.warm.flow.core.enums.UserType;
@@ -60,7 +63,6 @@ import org.dromara.workflow.service.IFlwTaskService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.*;
import static org.dromara.workflow.common.constant.FlowConstant.*;
@@ -84,7 +86,6 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
private final FlowInstanceMapper flowInstanceMapper;
private final FlowTaskMapper flowTaskMapper;
private final FlowHisTaskMapper flowHisTaskMapper;
private final IdentifierGenerator identifierGenerator;
private final UserService userService;
private final FlwTaskMapper flwTaskMapper;
private final FlwCategoryMapper flwCategoryMapper;
@@ -101,6 +102,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
*/
@Override
@Transactional(rollbackFor = Exception.class)
@Lock4j(keys = {"#startProcessBo.flowCode + #startProcessBo.businessId"})
public StartProcessReturnDTO startWorkFlow(StartProcessBo startProcessBo) {
String businessId = startProcessBo.getBusinessId();
if (StringUtils.isBlank(businessId)) {
@@ -137,6 +139,9 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
// 将流程定义内的扩展参数设置到变量中
Definition definition = FlowEngine.defService().getPublishByFlowCode(startProcessBo.getFlowCode());
if (ObjectUtil.isNull(definition)) {
throw new ServiceException("流程【" + startProcessBo.getFlowCode() + "】未发布,请先在流程设计器中发布流程定义");
}
Dict dict = JsonUtils.parseMap(definition.getExt());
boolean autoPass = !ObjectUtil.isNull(dict) && dict.getBool(FlowConstant.AUTO_PASS);
variables.put(FlowConstant.AUTO_PASS, autoPass);
@@ -146,12 +151,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
.flowCode(startProcessBo.getFlowCode())
.variable(startProcessBo.getVariables())
.flowStatus(BusinessStatusEnum.DRAFT.getStatus());
Instance instance;
try {
instance = insService.start(businessId, flowParams);
} catch (Exception e) {
throw new ServiceException(e.getMessage());
}
Instance instance = insService.start(businessId, flowParams);
// 保存流程实例业务信息
this.buildFlowInstanceBizExt(instance, bizExt);
// 申请人执行流程
@@ -197,53 +197,52 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
*/
@Override
@Transactional(rollbackFor = Exception.class)
@Lock4j(keys = {"#completeTaskBo.taskId"})
public boolean completeTask(CompleteTaskBo completeTaskBo) {
try {
// 获取任务ID并查询对应的流程任务和实例信息
Long taskId = completeTaskBo.getTaskId();
List<String> messageType = completeTaskBo.getMessageType();
String notice = completeTaskBo.getNotice();
// 获取抄送人
List<FlowCopyBo> flowCopyList = completeTaskBo.getFlowCopyList();
// 设置抄送人
Map<String, Object> variables = completeTaskBo.getVariables();
variables.put(FlowConstant.FLOW_COPY_LIST, flowCopyList);
// 消息类型
variables.put(FlowConstant.MESSAGE_TYPE, messageType);
// 消息通知
variables.put(FlowConstant.MESSAGE_NOTICE, notice);
// 获取任务ID并查询对应的流程任务和实例信息
Long taskId = completeTaskBo.getTaskId();
List<String> messageType = completeTaskBo.getMessageType();
String notice = completeTaskBo.getNotice();
// 获取抄送人
List<FlowCopyBo> flowCopyList = completeTaskBo.getFlowCopyList();
// 设置抄送人
Map<String, Object> variables = completeTaskBo.getVariables();
variables.put(FlowConstant.FLOW_COPY_LIST, flowCopyList);
// 消息类型
variables.put(FlowConstant.MESSAGE_TYPE, messageType);
// 消息通知
variables.put(FlowConstant.MESSAGE_NOTICE, notice);
FlowTask flowTask = flowTaskMapper.selectById(taskId);
if (ObjectUtil.isNull(flowTask)) {
throw new ServiceException("流程任务不存在或任务已审批!");
}
Instance ins = insService.getById(flowTask.getInstanceId());
// 检查流程状态是否为草稿、已撤销或已退回状态,若是则执行流程提交监听
if (BusinessStatusEnum.isDraftOrCancelOrBack(ins.getFlowStatus())) {
variables.put(FlowConstant.SUBMIT, true);
}
// 设置弹窗处理人
Map<String, Object> assigneeMap = setPopAssigneeMap(completeTaskBo.getAssigneeMap(), ins.getVariableMap());
if (CollUtil.isNotEmpty(assigneeMap)) {
variables.putAll(assigneeMap);
}
// 构建流程参数,包括变量、跳转类型、消息、处理人、权限等信息
FlowParams flowParams = FlowParams.build()
.handler(completeTaskBo.getHandler())
.variable(variables)
.skipType(SkipType.PASS.getKey())
.message(completeTaskBo.getMessage())
.flowStatus(BusinessStatusEnum.WAITING.getStatus())
.hisStatus(TaskStatusEnum.PASS.getStatus())
.hisTaskExt(completeTaskBo.getFileId());
Boolean autoPass = Convert.toBool(variables.getOrDefault(AUTO_PASS, false));
skipTask(taskId, flowParams, flowTask.getInstanceId(), autoPass);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new ServiceException(e.getMessage());
FlowTask flowTask = flowTaskMapper.selectById(taskId);
if (ObjectUtil.isNull(flowTask)) {
throw new ServiceException("流程任务不存在或任务已审批!");
}
Instance ins = insService.getById(flowTask.getInstanceId());
// 检查流程状态是否为草稿、已撤销或已退回状态,若是则执行流程提交监听
if (BusinessStatusEnum.isDraftOrCancelOrBack(ins.getFlowStatus())) {
variables.put(FlowConstant.SUBMIT, true);
}
// 设置弹窗处理人
Map<String, Object> assigneeMap = setPopAssigneeMap(completeTaskBo.getAssigneeMap(), ins.getVariableMap());
if (CollUtil.isNotEmpty(assigneeMap)) {
variables.putAll(assigneeMap);
}
// 构建流程参数,包括变量、跳转类型、消息、处理人、权限等信息
FlowParams flowParams = FlowParams.build()
.handler(completeTaskBo.getHandler())
.variable(variables)
.ignore(Convert.toBool(variables.getOrDefault(VAR_IGNORE, false)))
.ignoreDepute(Convert.toBool(variables.getOrDefault(VAR_IGNORE_DEPUTE, false)))
.ignoreCooperate(Convert.toBool(variables.getOrDefault(VAR_IGNORE_COOPERATE, false)))
.skipType(SkipType.PASS.getKey())
.message(completeTaskBo.getMessage())
.flowStatus(BusinessStatusEnum.WAITING.getStatus())
.hisStatus(TaskStatusEnum.PASS.getStatus())
.hisTaskExt(completeTaskBo.getFileId());
Boolean autoPass = Convert.toBool(variables.getOrDefault(AUTO_PASS, false));
skipTask(taskId, flowParams, flowTask.getInstanceId(), autoPass);
return true;
}
/**
@@ -305,10 +304,12 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
List<String> variableUserIds = Arrays.asList(userIds.split(StringUtils.SEPARATOR));
hashSet.addAll(popUserIds);
hashSet.addAll(variableUserIds);
map.put(entry.getKey(), StringUtils.joinComma(hashSet));
map.put(TaskStatusEnum.PASS.getStatus() + StrUtil.COLON + entry.getKey(), StringUtils.joinComma(hashSet));
map.put(TaskStatusEnum.BACK.getStatus() + StrUtil.COLON + entry.getKey(), StringUtils.joinComma(hashSet));
}
} else {
map.put(entry.getKey(), entry.getValue());
map.put(TaskStatusEnum.PASS.getStatus() + StrUtil.COLON + entry.getKey(), entry.getValue());
map.put(TaskStatusEnum.BACK.getStatus() + StrUtil.COLON + entry.getKey(), entry.getValue());
}
}
return map;
@@ -333,7 +334,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
flowNode.setNodeCode(flowHisTask.getTargetNodeCode());
flowNode.setNodeName(flowHisTask.getTargetNodeName());
//生成新的任务id
long taskId = identifierGenerator.nextId(null).longValue();
long taskId = IdGeneratorUtil.nextLongId();
task.setId(taskId);
task.setNodeName("【抄送】" + task.getNodeName());
Date updateTime = new Date(flowHisTask.getUpdateTime().getTime() - 1000);
@@ -382,7 +383,6 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
QueryWrapper<FlowTaskBo> queryWrapper = buildQueryWrapper(flowTaskBo);
queryWrapper.eq("t.node_type", NodeType.BETWEEN.getKey());
queryWrapper.in("t.approver", LoginHelper.getUserIdStr());
queryWrapper.orderByDesc("t.create_time").orderByDesc("t.update_time");
Page<FlowHisTaskVo> page = flwTaskMapper.getListFinishTask(pageQuery.build(), queryWrapper);
return TableDataInfo.build(page);
}
@@ -448,16 +448,20 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
}
private QueryWrapper<FlowTaskBo> buildQueryWrapper(FlowTaskBo flowTaskBo) {
Map<String, Object> params = flowTaskBo.getParams();
QueryWrapper<FlowTaskBo> wrapper = Wrappers.query();
wrapper.like(StringUtils.isNotBlank(flowTaskBo.getNodeName()), "t.node_name", flowTaskBo.getNodeName());
wrapper.like(StringUtils.isNotBlank(flowTaskBo.getFlowName()), "t.flow_name", flowTaskBo.getFlowName());
wrapper.like(StringUtils.isNotBlank(flowTaskBo.getFlowCode()), "t.flow_code", flowTaskBo.getFlowCode());
wrapper.like(StringUtils.isNotBlank(flowTaskBo.getFlowStatus()), "t.flow_status", flowTaskBo.getFlowStatus());
wrapper.in(CollUtil.isNotEmpty(flowTaskBo.getCreateByIds()), "t.create_by", flowTaskBo.getCreateByIds());
if (StringUtils.isNotBlank(flowTaskBo.getCategory())) {
List<Long> categoryIds = flwCategoryMapper.selectCategoryIdsByParentId(Convert.toLong(flowTaskBo.getCategory()));
wrapper.in("t.category", StreamUtils.toList(categoryIds, Convert::toStr));
}
wrapper.orderByDesc("t.create_time");
wrapper.between(params.get("beginTime") != null && params.get("endTime") != null,
"t.create_time", params.get("beginTime"), params.get("endTime"));
wrapper.orderByDesc("t.create_time").orderByDesc("t.update_time");
return wrapper;
}
@@ -469,40 +473,35 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean backProcess(BackProcessBo bo) {
try {
Long taskId = bo.getTaskId();
String notice = bo.getNotice();
List<String> messageType = bo.getMessageType();
String message = bo.getMessage();
FlowTask task = flowTaskMapper.selectById(taskId);
if (ObjectUtil.isNull(task)) {
throw new ServiceException("任务不存在!");
}
Instance inst = insService.getById(task.getInstanceId());
BusinessStatusEnum.checkBackStatus(inst.getFlowStatus());
Long definitionId = task.getDefinitionId();
String applyNodeCode = flwCommonService.applyNodeCode(definitionId);
Map<String, Object> variable = new HashMap<>();
// 消息类型
variable.put("messageType", messageType);
// 消息通知
variable.put("notice", notice);
FlowParams flowParams = FlowParams.build()
.nodeCode(bo.getNodeCode())
.variable(variable)
.message(message)
.skipType(SkipType.REJECT.getKey())
.flowStatus(applyNodeCode.equals(bo.getNodeCode()) ? TaskStatusEnum.BACK.getStatus() : TaskStatusEnum.WAITING.getStatus())
.hisStatus(TaskStatusEnum.BACK.getStatus())
.hisTaskExt(bo.getFileId());
taskService.skip(task.getId(), flowParams);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new ServiceException(e.getMessage());
Long taskId = bo.getTaskId();
String notice = bo.getNotice();
List<String> messageType = bo.getMessageType();
String message = bo.getMessage();
FlowTask task = flowTaskMapper.selectById(taskId);
if (ObjectUtil.isNull(task)) {
throw new ServiceException("任务不存在!");
}
Instance inst = insService.getById(task.getInstanceId());
BusinessStatusEnum.checkBackStatus(inst.getFlowStatus());
Long definitionId = task.getDefinitionId();
String applyNodeCode = flwCommonService.applyNodeCode(definitionId);
Map<String, Object> variable = new HashMap<>();
// 消息类型
variable.put(FlowConstant.MESSAGE_TYPE, messageType);
// 消息通知
variable.put(FlowConstant.MESSAGE_NOTICE, notice);
FlowParams flowParams = FlowParams.build()
.nodeCode(bo.getNodeCode())
.variable(variable)
.message(message)
.skipType(SkipType.REJECT.getKey())
.flowStatus(applyNodeCode.equals(bo.getNodeCode()) ? TaskStatusEnum.BACK.getStatus() : TaskStatusEnum.WAITING.getStatus())
.hisStatus(TaskStatusEnum.BACK.getStatus())
.hisTaskExt(bo.getFileId());
taskService.skip(task.getId(), flowParams);
return true;
}
/**
@@ -530,8 +529,22 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
}
//获取可驳回的前置节点
List<Node> nodes = nodeService.previousNodeList(task.getDefinitionId(), nowNodeCode);
if (CollUtil.isNotEmpty(nodes)) {
return StreamUtils.filter(nodes, e -> NodeType.BETWEEN.getKey().equals(e.getNodeType()));
List<HisTask> hisTaskList = hisTaskService.getByInsId(task.getInstanceId());
Map<String, Node> nodeMap = StreamUtils.toIdentityMap(nodes, Node::getNodeCode);
Set<String> added = new HashSet<>();
List<Node> backNodeList = new ArrayList<>();
for (HisTask hisTask : hisTaskList) {
Node nodeValue = nodeMap.get(hisTask.getNodeCode());
if (nodeValue != null
&& NodeType.BETWEEN.getKey().equals(nodeValue.getNodeType())
&& added.add(nodeValue.getNodeCode())) {
backNodeList.add(nodeValue);
}
}
if (CollUtil.isNotEmpty(backNodeList)) {
Collections.reverse(backNodeList);
return backNodeList;
}
return nodes;
}
@@ -544,26 +557,21 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean terminationTask(FlowTerminationBo bo) {
try {
Long taskId = bo.getTaskId();
Task task = taskService.getById(taskId);
if (task == null) {
throw new ServiceException("任务不存在!");
}
Instance instance = insService.getById(task.getInstanceId());
if (ObjectUtil.isNotNull(instance)) {
BusinessStatusEnum.checkInvalidStatus(instance.getFlowStatus());
}
FlowParams flowParams = FlowParams.build()
.message(bo.getComment())
.flowStatus(BusinessStatusEnum.TERMINATION.getStatus())
.hisStatus(TaskStatusEnum.TERMINATION.getStatus());
taskService.termination(taskId, flowParams);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new ServiceException(e.getMessage());
Long taskId = bo.getTaskId();
Task task = taskService.getById(taskId);
if (task == null) {
throw new ServiceException("任务不存在!");
}
Instance instance = insService.getById(task.getInstanceId());
if (ObjectUtil.isNotNull(instance)) {
BusinessStatusEnum.checkInvalidStatus(instance.getFlowStatus());
}
FlowParams flowParams = FlowParams.build()
.message(bo.getComment())
.flowStatus(BusinessStatusEnum.TERMINATION.getStatus())
.hisStatus(TaskStatusEnum.TERMINATION.getStatus());
taskService.termination(taskId, flowParams);
return true;
}
/**
@@ -599,16 +607,24 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
if (ObjectUtil.isNull(flowNode)) {
throw new NullPointerException("当前【" + flowTaskVo.getNodeCode() + "】节点编码不存在");
}
NodeExtVo nodeExtVo = flwNodeExtService.parseNodeExt(flowNode.getExt());
NodeExtVo nodeExtVo = flwNodeExtService.parseNodeExt(flowNode.getExt(), instance.getVariableMap());
//设置按钮权限
flowTaskVo.setButtonList(nodeExtVo.getButtonPermissions());
if (CollUtil.isNotEmpty(nodeExtVo.getButtonPermissions())) {
flowTaskVo.setButtonList(nodeExtVo.getButtonPermissions());
} else {
flowTaskVo.setButtonList(new ArrayList<>());
}
if (CollUtil.isNotEmpty(nodeExtVo.getCopySettings())) {
List<FlowCopyVo> list = StreamUtils.toList(nodeExtVo.getCopySettings(), x -> new FlowCopyVo(Convert.toLong(x)));
flowTaskVo.setCopyList(list);
} else {
flowTaskVo.setCopyList(new ArrayList<>());
}
flowTaskVo.setVarList(nodeExtVo.getVariables());
if (CollUtil.isNotEmpty(nodeExtVo.getVariables())) {
flowTaskVo.setVarList(nodeExtVo.getVariables());
} else {
flowTaskVo.setVarList(new HashMap<>());
}
flowTaskVo.setNodeRatio(flowNode.getNodeRatio());
flowTaskVo.setApplyNode(flowNode.getNodeCode().equals(flwCommonService.applyNodeCode(task.getDefinitionId())));
return flowTaskVo;
@@ -731,9 +747,9 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
Long taskId = bo.getTaskId();
Task task = taskService.getById(taskId);
FlowNode flowNode = getByNodeCode(task.getNodeCode(), task.getDefinitionId());
if ("addSignature".equals(taskOperation) || "reductionSignature".equals(taskOperation)) {
if (flowNode.getNodeRatio().compareTo(BigDecimal.ZERO) == 0) {
throw new ServiceException(task.getNodeName() + "不是会签节点!");
if (ADD_SIGNATURE.equals(taskOperation) || REDUCTION_SIGNATURE.equals(taskOperation)) {
if (CooperateType.isOrSign(flowNode.getNodeRatio())) {
throw new ServiceException(task.getNodeName() + "不是会签或票签节点!");
}
}
// 设置任务状态并执行对应的任务操作
@@ -777,23 +793,18 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
if (CollUtil.isEmpty(taskIdList)) {
return false;
}
try {
List<FlowTask> flowTasks = this.selectByIdList(taskIdList);
// 批量删除现有任务的办理人记录
if (CollUtil.isNotEmpty(flowTasks)) {
FlowEngine.userService().deleteByTaskIds(StreamUtils.toList(flowTasks, FlowTask::getId));
List<User> userList = StreamUtils.toList(flowTasks, flowTask ->
new FlowUser()
.setType(TaskAssigneeType.APPROVER.getCode())
.setProcessedBy(userId)
.setAssociated(flowTask.getId()));
if (CollUtil.isNotEmpty(userList)) {
FlowEngine.userService().saveBatch(userList);
}
List<FlowTask> flowTasks = this.selectByIdList(taskIdList);
// 批量删除现有任务的办理人记录
if (CollUtil.isNotEmpty(flowTasks)) {
FlowEngine.userService().deleteByTaskIds(StreamUtils.toList(flowTasks, FlowTask::getId));
List<User> userList = StreamUtils.toList(flowTasks, flowTask ->
new FlowUser()
.setType(TaskAssigneeType.APPROVER.getCode())
.setProcessedBy(userId)
.setAssociated(flowTask.getId()));
if (CollUtil.isNotEmpty(userList)) {
FlowEngine.userService().saveBatch(userList);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new ServiceException(e.getMessage());
}
return true;
}
@@ -833,21 +844,16 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
*/
@Override
public boolean urgeTask(FlowUrgeTaskBo bo) {
try {
if (CollUtil.isEmpty(bo.getTaskIdList())) {
return false;
}
List<UserDTO> userList = this.currentTaskAllUser(bo.getTaskIdList());
if (CollUtil.isEmpty(userList)) {
return false;
}
List<String> messageType = bo.getMessageType();
String message = bo.getMessage();
flwCommonService.sendMessage(messageType, message, "单据审批提醒", userList);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new ServiceException(e.getMessage());
if (CollUtil.isEmpty(bo.getTaskIdList())) {
return false;
}
List<UserDTO> userList = this.currentTaskAllUser(bo.getTaskIdList());
if (CollUtil.isEmpty(userList)) {
return false;
}
List<String> messageType = bo.getMessageType();
String message = bo.getMessage();
flwCommonService.sendMessage(messageType, message, "单据审批提醒", userList);
return true;
}
}

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