93 Commits

Author SHA1 Message Date
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
疯狂的狮子Li
f616c6931c 发布 5.5.0 喜迎国庆🧨🧨🧨 2025-09-22 11:13:32 +08:00
疯狂的狮子Li
26e10293f5 update snailjob 1.7.2 => 1.8.0 2025-09-22 11:11:06 +08:00
疯狂的狮子Li
60e578f763 Revert "update 更新工作流sql"
This reverts commit 8909b8a7d4.
2025-09-22 11:08:23 +08:00
疯狂的狮子Li
5cd4d8ca11 Revert "update warm-flow 升级 1.8.2-m2"
This reverts commit 8ae9bde731.
2025-09-22 11:08:18 +08:00
疯狂的狮子Li
41a6230b6e update 优化 去除不应该加压缩的日志文件 2025-09-22 11:05:13 +08:00
Lau
effda4f6e8 !765 update 历史日志文件增加压缩
* update 历史日志文件增加压缩
2025-09-22 03:00:08 +00:00
疯狂的狮子Li
af4c38e439 update 优化 更新ip2region.xdb文件 2025-09-19 17:44:32 +08:00
疯狂的狮子Li
fafa8cd573 update springboot 3.5.4 => 3.5.6 2025-09-19 14:12:47 +08:00
疯狂的狮子Li
8909b8a7d4 update 更新工作流sql 2025-09-18 18:17:33 +08:00
AprilWind
8ae9bde731 update warm-flow 升级 1.8.2-m2 2025-09-16 21:21:08 +08:00
疯狂的狮子Li
a703cb2ad1 update 增加 加密头用来判断数据是否已经被加密了 防止重复加密 2025-09-15 17:14:47 +08:00
疯狂的狮子Li
a918b880d6 update 增加 加密头用来判断数据是否已经被加密了 防止重复加密 2025-09-15 17:10:31 +08:00
疯狂的狮子Li
e795e315eb update 生成模板前端增加fixed 2025-09-15 15:46:01 +08:00
Lau
81869cfeb3 !764 update 生成模板前端增加fixed
* update 生成模板前端增加fixed
2025-09-15 07:39:34 +00:00
疯狂的狮子Li
fc6f61bc95 update springboot-admin 3.5.1 => 3.5.3
update springdoc 2.8.11 => 2.8.13
update mybatis-plus 3.5.12 => 3.5.14
update mapstruct-plus 1.4.8 => 1.5.0
update sms4j 3.3.4 => 3.3.5
2025-09-15 11:52:05 +08:00
AprilWind
d44e45ad3b update 添加节点悬浮提示配置开关 2025-09-04 17:30:15 +08:00
AprilWind
00ed9ddd10 update 优化 SysMenu 的 selectObjs 查询 2025-09-04 16:04:36 +08:00
疯狂的狮子Li
341fc144a1 fix 修复 自定义sql在pg数据库类型异常问题 2025-09-04 15:42:30 +08:00
AprilWind
b6b1b2de18 update 优化全局日期格式转换逻辑 2025-09-04 15:30:09 +08:00
疯狂的狮子Li
c19f2b9e4e update 优化 岗位页面查询权限问题 2025-09-03 14:14:35 +08:00
秋辞未寒
3a11f18656 fix 修复 StreamUtils 返回不可变类型报错问题 2025-09-02 15:58:11 +08:00
疯狂的狮子Li
5a43212ccc fix 修复 StreamUtils 返回不可变类型报错问题 2025-09-02 15:51:42 +08:00
Rogue杨
f4cfd1c913 !759 [fix] 解决工作流通知messageType参数判空逻辑错误的问题
* [fix] 解决工作流通知messageType参数判空逻辑错误的问题
2025-09-02 04:57:56 +00:00
疯狂的狮子Li
26ce8f30c9 update 优化 支持子菜单配置默认激活的父菜单activeMenu 2025-09-02 10:45:54 +08:00
疯狂的狮子Li
2258962770 Revert "!734 update 重写selectOne方法"
This reverts commit f2e0361fb6.
2025-09-01 14:25:47 +08:00
疯狂的狮子Li
655e84012c Revert "update 优化 增加selectOne使用注意事项"
This reverts commit bf10a13088.
2025-09-01 14:25:40 +08:00
疯狂的狮子Li
f683ef00b8 fix 修复 json模块配置 默认覆盖了spring module 配置问题 改为让spring自动加载注册 2025-09-01 11:46:57 +08:00
秋辞未寒
424b2ea164 update Excel写出包装器添加泛型用于限定write入参类型 2025-08-31 13:24:12 +08:00
秋辞未寒
7bb4838132 feat add Excel工具类支持更灵活的自定义导出方式,以便用户分批处理导出数据 2025-08-30 18:09:01 +08:00
秋辞未寒
20516758ea upadte 优化Stream流工具类 2025-08-30 16:53:13 +08:00
秋辞未寒
2d5f84ebc2 upadte 优化Stream流工具类 2025-08-30 16:46:04 +08:00
疯狂的狮子Li
6bc28e41de update 优化 工作流代码 2025-08-29 10:06:22 +08:00
疯狂的狮子Li
a4fb3fadaf update 优化 将返回值从bo改为vo 2025-08-29 09:52:21 +08:00
疯狂的狮子Li
cfa67fcd8c Revert "update 添加 FlowCopyVo 类,优化抄送对象处理逻辑"
This reverts commit e5e8d305d2.
2025-08-29 01:35:27 +00:00
AprilWind
e5e8d305d2 update 添加 FlowCopyVo 类,优化抄送对象处理逻辑 2025-08-29 09:29:12 +08:00
疯狂的狮子Li
9d0084409e update 优化 支持后端监听器解析节点扩展数据到流程变量(按钮权限 抄送人 扩展变量) 2025-08-28 17:56:10 +08:00
疯狂的狮子Li
ee02f46dfd update 优化 支持前端返回节点扩展数据(按钮权限 抄送人 扩展变量) 2025-08-28 17:55:04 +08:00
AprilWind
25de0b3530 update 解析扩展属性 JSON,构建 Node 扩展属性对象,增强代码可读性 2025-08-28 16:25:33 +08:00
AprilWind
aa76859a05 update 添加抄送设置和变量枚举,优化扩展节点配置逻辑 2025-08-28 15:00:32 +08:00
疯狂的狮子Li
71b70a59fe fix 修复 判断错误导致新增报错问题 2025-08-28 10:59:00 +08:00
AprilWind
05c9528549 !752 update 优化流程实例业务扩展的保存和删除逻辑,增强代码可读性
* update 优化流程实例业务扩展的保存和删除逻辑,增强代码可读性
2025-08-27 11:10:03 +00:00
疯狂的狮子Li
1feb2a3861 fix 修复 菜单与部门 未做角色状态判断 2025-08-27 17:54:05 +08:00
疯狂的狮子Li
237e78e80c update hutool 5.8.38 => 5.8.40 默认支持了验证码不生成负数 2025-08-27 11:58:07 +08:00
秋辞未寒
ffc3dcaec9 upadte 优化Excel单元格合并处理器代码逻辑分支 2025-08-26 17:13:35 +08:00
疯狂的狮子Li
a94e474069 fix 修复 时间解析类异常问题 2025-08-26 16:10:17 +08:00
疯狂的狮子Li
40a0e57870 fix 修复 时间解析类异常问题 2025-08-26 16:02:11 +08:00
127 changed files with 3857 additions and 1681 deletions

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

@@ -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) [![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) [![使用IntelliJ IDEA开发维护](https://img.shields.io/badge/IntelliJ%20IDEA-提供支持-blue.svg)](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
<br> <br>
[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.4.1-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus) [![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.5.1-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.4-blue.svg)]() [![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.4-blue.svg)]()
[![JDK-17](https://img.shields.io/badge/JDK-17-green.svg)]() [![JDK-17](https://img.shields.io/badge/JDK-17-green.svg)]()
[![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)]() [![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)]()

22
pom.xml
View File

@@ -13,28 +13,28 @@
<description>Dromara RuoYi-Vue-Plus多租户管理系统</description> <description>Dromara RuoYi-Vue-Plus多租户管理系统</description>
<properties> <properties>
<revision>5.4.1</revision> <revision>5.5.1</revision>
<spring-boot.version>3.5.4</spring-boot.version> <spring-boot.version>3.5.7</spring-boot.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version> <java.version>17</java.version>
<mybatis.version>3.5.16</mybatis.version> <mybatis.version>3.5.16</mybatis.version>
<springdoc.version>2.8.11</springdoc.version> <springdoc.version>2.8.13</springdoc.version>
<therapi-javadoc.version>0.15.0</therapi-javadoc.version> <therapi-javadoc.version>0.15.0</therapi-javadoc.version>
<fastexcel.version>1.3.0</fastexcel.version> <fastexcel.version>1.3.0</fastexcel.version>
<velocity.version>2.3</velocity.version> <velocity.version>2.3</velocity.version>
<satoken.version>1.44.0</satoken.version> <satoken.version>1.44.0</satoken.version>
<mybatis-plus.version>3.5.12</mybatis-plus.version> <mybatis-plus.version>3.5.14</mybatis-plus.version>
<p6spy.version>3.9.1</p6spy.version> <p6spy.version>3.9.1</p6spy.version>
<hutool.version>5.8.38</hutool.version> <hutool.version>5.8.40</hutool.version>
<spring-boot-admin.version>3.5.1</spring-boot-admin.version> <spring-boot-admin.version>3.5.5</spring-boot-admin.version>
<redisson.version>3.51.0</redisson.version> <redisson.version>3.51.0</redisson.version>
<lock4j.version>2.2.7</lock4j.version> <lock4j.version>2.2.7</lock4j.version>
<dynamic-ds.version>4.3.1</dynamic-ds.version> <dynamic-ds.version>4.3.1</dynamic-ds.version>
<snailjob.version>1.7.2</snailjob.version> <snailjob.version>1.8.0</snailjob.version>
<mapstruct-plus.version>1.4.8</mapstruct-plus.version> <mapstruct-plus.version>1.5.0</mapstruct-plus.version>
<mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version> <mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
<lombok.version>1.18.38</lombok.version> <lombok.version>1.18.40</lombok.version>
<bouncycastle.version>1.80</bouncycastle.version> <bouncycastle.version>1.80</bouncycastle.version>
<justauth.version>1.16.7</justauth.version> <justauth.version>1.16.7</justauth.version>
<!-- 离线IP地址定位库 --> <!-- 离线IP地址定位库 -->
@@ -42,13 +42,13 @@
<!-- OSS 配置 --> <!-- OSS 配置 -->
<aws.sdk.version>2.28.22</aws.sdk.version> <aws.sdk.version>2.28.22</aws.sdk.version>
<!-- SMS 配置 --> <!-- SMS 配置 -->
<sms4j.version>3.3.4</sms4j.version> <sms4j.version>3.3.5</sms4j.version>
<!-- 限制框架中的fastjson版本 --> <!-- 限制框架中的fastjson版本 -->
<fastjson.version>1.2.83</fastjson.version> <fastjson.version>1.2.83</fastjson.version>
<!-- 面向运行时的D-ORM依赖 --> <!-- 面向运行时的D-ORM依赖 -->
<anyline.version>8.7.2-20250603</anyline.version> <anyline.version>8.7.2-20250603</anyline.version>
<!-- 工作流配置 --> <!-- 工作流配置 -->
<warm-flow.version>1.8.1</warm-flow.version> <warm-flow.version>1.8.3</warm-flow.version>
<!-- 插件版本 --> <!-- 插件版本 -->
<maven-jar-plugin.version>3.4.2</maven-jar-plugin.version> <maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>

View File

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

View File

@@ -259,5 +259,7 @@ warm-flow:
ui: true ui: true
# 是否显示流程图顶部文字 # 是否显示流程图顶部文字
top-text-show: true top-text-show: true
# 是否渲染节点悬浮提示默认true
node-tooltip: true
# 默认Authorization如果有多个token用逗号分隔 # 默认Authorization如果有多个token用逗号分隔
token-name: ${sa-token.token-name},clientid token-name: ${sa-token.token-name},clientid

View File

@@ -38,7 +38,7 @@
<!-- 循环政策:基于时间创建日志文件 --> <!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 --> <!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern> <fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<!-- 日志最大的历史 60天 --> <!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory> <maxHistory>60</maxHistory>
</rollingPolicy> </rollingPolicy>
@@ -60,7 +60,7 @@
<!-- 循环政策:基于时间创建日志文件 --> <!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 --> <!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern> <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
<!-- 日志最大的历史 60天 --> <!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory> <maxHistory>60</maxHistory>
</rollingPolicy> </rollingPolicy>

View File

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

View File

@@ -5,15 +5,12 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.dromara.common.core.config.properties.ThreadPoolProperties; import org.dromara.common.core.config.properties.ThreadPoolProperties;
import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.Threads;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.core.task.VirtualThreadTaskExecutor; import org.springframework.core.task.VirtualThreadTaskExecutor;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.*;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/** /**
* 线程池配置 * 线程池配置
@@ -50,7 +47,7 @@ public class ThreadPoolConfig {
@Override @Override
protected void afterExecute(Runnable r, Throwable t) { protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t); super.afterExecute(r, t);
Threads.printException(r, t); printException(r, t);
} }
}; };
this.scheduledExecutorService = scheduledThreadPoolExecutor; this.scheduledExecutorService = scheduledThreadPoolExecutor;
@@ -59,15 +56,57 @@ public class ThreadPoolConfig {
/** /**
* 销毁事件 * 销毁事件
* 停止线程池
* 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务.
* 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数.
* 如果仍然超時,則強制退出.
* 另对在shutdown时线程本身被调用中断做了处理.
*/ */
@PreDestroy @PreDestroy
public void destroy() { public void destroy() {
try { try {
log.info("====关闭后台任务任务线程池===="); 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) { } catch (Exception e) {
log.error(e.getMessage(), 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

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

View File

@@ -67,7 +67,8 @@ public class CompleteTaskDTO implements Serializable {
public Map<String, Object> getVariables() { public Map<String, Object> getVariables() {
if (variables == null) { if (variables == null) {
return new HashMap<>(16); variables = new HashMap<>(16);
return variables;
} }
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue())); variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables; 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; package org.dromara.common.core.domain.dto;
import cn.hutool.core.util.ObjectUtil;
import lombok.Data; import lombok.Data;
import java.io.Serial; import java.io.Serial;
@@ -40,6 +41,11 @@ public class StartProcessDTO implements Serializable {
*/ */
private Map<String, Object> variables; private Map<String, Object> variables;
/**
* 流程业务扩展信息
*/
private FlowInstanceBizExtDTO bizExt;
public Map<String, Object> getVariables() { public Map<String, Object> getVariables() {
if (variables == null) { if (variables == null) {
return new HashMap<>(16); return new HashMap<>(16);
@@ -47,4 +53,11 @@ public class StartProcessDTO implements Serializable {
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue())); variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables; 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; 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); 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 * @param businessIds 业务id
* @return 结果 * @return 结果
*/ */
boolean deleteInstance(List<Long> businessIds); boolean deleteInstance(List<String> businessIds);
/** /**
* 获取当前流程状态 * 获取当前流程状态

View File

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

View File

@@ -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

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

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 @Override
public CellDataTypeEnum supportExcelTypeKey() { public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING; return null;
} }
@Override @Override

View File

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

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.StreamUtils;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.excel.annotation.ExcelDictFormat; import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.annotation.ExcelDynamicOptions;
import org.dromara.common.excel.annotation.ExcelEnumFormat; import org.dromara.common.excel.annotation.ExcelEnumFormat;
import java.lang.reflect.Field; import java.lang.reflect.Field;
@@ -117,6 +118,15 @@ public class ExcelDownHandler implements SheetWriteHandler {
ExcelEnumFormat format = field.getDeclaredAnnotation(ExcelEnumFormat.class); ExcelEnumFormat format = field.getDeclaredAnnotation(ExcelEnumFormat.class);
List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField()); List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField());
options = StreamUtils.toList(values, Convert::toStr); options = StreamUtils.toList(values, 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)) { 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

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException; import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import lombok.AccessLevel; 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

@@ -6,11 +6,9 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.toolkit.reflect.GenericTypeUtils; import com.baomidou.mybatisplus.core.toolkit.reflect.GenericTypeUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.toolkit.Db; import com.baomidou.mybatisplus.extension.toolkit.Db;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.logging.Log; import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory; import org.apache.ibatis.logging.LogFactory;
import org.dromara.common.core.utils.MapstructUtils; import org.dromara.common.core.utils.MapstructUtils;
@@ -132,7 +130,7 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
* @return 查询到的单个VO对象 * @return 查询到的单个VO对象
*/ */
default V selectVoById(Serializable id) { default V selectVoById(Serializable id) {
return this.selectVoById(id, this.currentVoClass()); return selectVoById(id, this.currentVoClass());
} }
/** /**
@@ -158,7 +156,7 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
* @return 查询到的VO对象列表 * @return 查询到的VO对象列表
*/ */
default List<V> selectVoByIds(Collection<? extends Serializable> idList) { default List<V> selectVoByIds(Collection<? extends Serializable> idList) {
return this.selectVoByIds(idList, this.currentVoClass()); return selectVoByIds(idList, this.currentVoClass());
} }
/** /**
@@ -184,7 +182,7 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
* @return 查询到的VO对象列表 * @return 查询到的VO对象列表
*/ */
default List<V> selectVoByMap(Map<String, Object> map) { default List<V> selectVoByMap(Map<String, Object> map) {
return this.selectVoByMap(map, this.currentVoClass()); return selectVoByMap(map, this.currentVoClass());
} }
/** /**
@@ -210,7 +208,7 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
* @return 查询到的单个VO对象 * @return 查询到的单个VO对象
*/ */
default V selectVoOne(Wrapper<T> wrapper) { default V selectVoOne(Wrapper<T> wrapper) {
return this.selectVoOne(wrapper, this.currentVoClass()); return selectVoOne(wrapper, this.currentVoClass());
} }
/** /**
@@ -221,12 +219,11 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
* @return 查询到的单个VO对象 * @return 查询到的单个VO对象
*/ */
default V selectVoOne(Wrapper<T> wrapper, boolean throwEx) { default V selectVoOne(Wrapper<T> wrapper, boolean throwEx) {
return this.selectVoOne(wrapper, this.currentVoClass(), throwEx); return selectVoOne(wrapper, this.currentVoClass(), throwEx);
} }
/** /**
* 根据条件查询单个VO对象并指定返回的VO对象的类型(自动拼接 limit 1) * 根据条件查询单个VO对象并指定返回的VO对象的类型
* 注意不要再自己添加 limit 1 做限制了
* *
* @param wrapper 查询条件Wrapper * @param wrapper 查询条件Wrapper
* @param voClass 返回的VO对象的Class对象 * @param voClass 返回的VO对象的Class对象
@@ -234,12 +231,11 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
* @return 查询到的单个VO对象经过类型转换为指定的VO类后返回 * @return 查询到的单个VO对象经过类型转换为指定的VO类后返回
*/ */
default <C> C selectVoOne(Wrapper<T> wrapper, Class<C> voClass) { default <C> C selectVoOne(Wrapper<T> wrapper, Class<C> voClass) {
return this.selectVoOne(wrapper, voClass, true); return selectVoOne(wrapper, voClass, true);
} }
/** /**
* 根据条件查询单个实体对象并将其转换为指定的VO对象(自动拼接 limit 1) * 根据条件查询单个实体对象并将其转换为指定的VO对象
* 注意不要再自己添加 limit 1 做限制了
* *
* @param wrapper 查询条件Wrapper * @param wrapper 查询条件Wrapper
* @param voClass 要转换的VO类的Class对象 * @param voClass 要转换的VO类的Class对象
@@ -255,33 +251,13 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
return MapstructUtils.convert(obj, voClass); return MapstructUtils.convert(obj, voClass);
} }
/**
* 根据条件查询单条记录(自动拼接 limit 1 限制返回 1 条数据,不依赖 {@code throwEx} 参数)
* 注意不要再自己添加 limit 1 做限制了
* <p>
* <strong>注意:</strong>
* 1. 使用 {@code Page<>(1, 1)} 强制分页查询,确保 SQL 自动添加 {@code LIMIT 1},因此 {@code throwEx} 参数不再生效
* 2. 原方法的 {@code throwEx} 逻辑(多条数据抛异常)已被优化掉,因为分页查询不会返回多条记录
* </p>
*
* @param queryWrapper 查询条件(可为 null
* @param throwEx <del>是否抛出异常(已弃用,此参数不再生效)</del>
* @return 单条记录或无数据时返回 null
*/
@Override
default T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper, boolean throwEx) {
// 强制分页查询LIMIT 1确保最多返回 1 条记录
List<T> list = this.selectList(new Page<>(1, 1), queryWrapper);
return CollUtil.isEmpty(list) ? null : list.get(0);
}
/** /**
* 查询所有VO对象列表 * 查询所有VO对象列表
* *
* @return 查询到的VO对象列表 * @return 查询到的VO对象列表
*/ */
default List<V> selectVoList() { default List<V> selectVoList() {
return this.selectVoList(new QueryWrapper<>(), this.currentVoClass()); return selectVoList(new QueryWrapper<>(), this.currentVoClass());
} }
/** /**
@@ -318,7 +294,7 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
* @return 查询到的VO对象分页列表 * @return 查询到的VO对象分页列表
*/ */
default <P extends IPage<V>> P selectVoPage(IPage<T> page, Wrapper<T> wrapper) { default <P extends IPage<V>> P selectVoPage(IPage<T> page, Wrapper<T> wrapper) {
return this.selectVoPage(page, wrapper, this.currentVoClass()); return selectVoPage(page, wrapper, this.currentVoClass());
} }
/** /**

View File

@@ -1,10 +1,11 @@
package org.dromara.common.mybatis.handler; package org.dromara.common.mybatis.handler;
import cn.dev33.satoken.exception.NotLoginException;
import cn.hutool.http.HttpStatus; import cn.hutool.http.HttpStatus;
import com.baomidou.dynamic.datasource.exception.CannotFindDataSourceException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R; import org.dromara.common.core.domain.R;
import org.dromara.common.core.utils.StringUtils;
import org.mybatis.spring.MyBatisSystemException; import org.mybatis.spring.MyBatisSystemException;
import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.DuplicateKeyException;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -35,13 +36,54 @@ public class MybatisExceptionHandler {
@ExceptionHandler(MyBatisSystemException.class) @ExceptionHandler(MyBatisSystemException.class)
public R<Void> handleCannotFindDataSourceException(MyBatisSystemException e, HttpServletRequest request) { public R<Void> handleCannotFindDataSourceException(MyBatisSystemException e, HttpServletRequest request) {
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
String message = e.getMessage(); Throwable root = getRootCause(e);
if (StringUtils.contains(message, "CannotFindDataSourceException")) { if (root instanceof NotLoginException) {
log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, root.getMessage());
return R.fail(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,无法访问系统资源");
}
if (root instanceof CannotFindDataSourceException) {
log.error("请求地址'{}', 未找到数据源", requestURI); log.error("请求地址'{}', 未找到数据源", requestURI);
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, "未找到数据源,请联系管理员确认"); return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, "未找到数据源,请联系管理员确认");
} }
log.error("请求地址'{}', Mybatis系统异常", requestURI, e); 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

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

View File

@@ -33,6 +33,7 @@ import java.nio.channels.WritableByteChannel;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Duration; import java.time.Duration;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -317,13 +318,13 @@ public class OssClient {
} }
/** /**
* 获取私有URL链接 * 创建下载请求的预签名URL
* *
* @param objectKey 对象KEY * @param objectKey 对象KEY
* @param expiredTime 链接授权到期时间 * @param expiredTime 链接授权到期时间
*/ */
public String getPrivateUrl(String objectKey, Duration expiredTime) { public String createPresignedGetUrl(String objectKey, Duration expiredTime) {
// 使用 AWS S3 预签名 URL 的生成器 获取对象的预签名 URL // 使用 AWS S3 预签名 URL 的生成器 获取下载对象的预签名 URL
URL url = presigner.presignGetObject( URL url = presigner.presignGetObject(
x -> x.signatureDuration(expiredTime) x -> x.signatureDuration(expiredTime)
.getObjectRequest( .getObjectRequest(
@@ -332,7 +333,28 @@ public class OssClient {
.build()) .build())
.build()) .build())
.url(); .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

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

@@ -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

@@ -14,9 +14,6 @@ import org.dromara.common.social.gitea.AuthGiteaRequest;
import org.dromara.common.social.maxkey.AuthMaxKeyRequest; import org.dromara.common.social.maxkey.AuthMaxKeyRequest;
import org.dromara.common.social.topiam.AuthTopIamRequest; 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() AuthConfig.AuthConfigBuilder builder = AuthConfig.builder()
.clientId(obj.getClientId()) .clientId(obj.getClientId())
.clientSecret(obj.getClientSecret()) .clientSecret(obj.getClientSecret())
.redirectUri(URLEncoder.encode(obj.getRedirectUri(), StandardCharsets.UTF_8)) .redirectUri(obj.getRedirectUri())
.scopes(obj.getScopes()); .scopes(obj.getScopes());
return switch (source.toLowerCase()) { return switch (source.toLowerCase()) {
case "dingtalk" -> new AuthDingTalkV2Request(builder.build(), STATE_CACHE); case "dingtalk" -> new AuthDingTalkV2Request(builder.build(), STATE_CACHE);

View File

@@ -1,14 +1,21 @@
package org.dromara.common.sse.core; package org.dromara.common.sse.core;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.redis.utils.RedisUtils; import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.sse.dto.SseMessageDto; import org.dromara.common.sse.dto.SseMessageDto;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer; 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<>(); 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 连接 * 建立与指定用户的 SSE 连接
* *
@@ -38,6 +51,12 @@ public class SseEmitterManager {
// 每个用户可以有多个 SSE 连接,通过 token 进行区分 // 每个用户可以有多个 SSE 连接,通过 token 进行区分
Map<String, SseEmitter> emitters = USER_TOKEN_EMITTERS.computeIfAbsent(userId, k -> new ConcurrentHashMap<>()); 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 实例,超时时间设置为一天 避免连接之后直接关闭浏览器导致连接停滞
SseEmitter emitter = new SseEmitter(86400000L); 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消息主题并提供一个消费者函数来处理接收到的消息 * 订阅SSE消息主题并提供一个消费者函数来处理接收到的消息
* *

View File

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

View File

@@ -46,8 +46,14 @@ public class TranslationHandler extends JsonSerializer<Object> implements Contex
gen.writeNull(); gen.writeNull();
return; return;
} }
Object result = trans.translation(value, translation.other()); try {
gen.writeObject(result); 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 { } else {
gen.writeObject(value); gen.writeObject(value);
} }

View File

@@ -1,7 +1,8 @@
package org.dromara.common.web.config; package org.dromara.common.web.config;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.ObjectUtils;
import org.dromara.common.web.handler.GlobalExceptionHandler; import org.dromara.common.web.handler.GlobalExceptionHandler;
import org.dromara.common.web.interceptor.PlusWebInvokeTimeInterceptor; import org.dromara.common.web.interceptor.PlusWebInvokeTimeInterceptor;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
@@ -34,10 +35,11 @@ public class ResourcesConfig implements WebMvcConfigurer {
public void addFormatters(FormatterRegistry registry) { public void addFormatters(FormatterRegistry registry) {
// 全局日期格式转换配置 // 全局日期格式转换配置
registry.addConverter(String.class, Date.class, source -> { registry.addConverter(String.class, Date.class, source -> {
if (StringUtils.isBlank(source)) { DateTime parse = DateUtil.parse(source);
if (ObjectUtils.isNull(parse)) {
return null; return null;
} }
return DateUtil.parse(source); return parse.toJdkDate();
}); });
} }

View File

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

View File

@@ -23,6 +23,7 @@ import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException; import org.springframework.web.servlet.NoHandlerFoundException;
@@ -123,7 +124,7 @@ public class GlobalExceptionHandler {
*/ */
@ResponseStatus(org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR) @ResponseStatus(org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(IOException.class) @ExceptionHandler(IOException.class)
public void handleRuntimeException(IOException e, HttpServletRequest request) { public void handleIoException(IOException e, HttpServletRequest request) {
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
if (requestURI.contains("sse")) { if (requestURI.contains("sse")) {
// sse 经常性连接中断 例如关闭浏览器 直接屏蔽 // sse 经常性连接中断 例如关闭浏览器 直接屏蔽
@@ -132,6 +133,13 @@ public class GlobalExceptionHandler {
log.error("请求地址'{}',连接中断", requestURI, e); log.error("请求地址'{}',连接中断", requestURI, e);
} }
/**
* sse 连接超时异常 不需要处理
*/
@ExceptionHandler(AsyncRequestTimeoutException.class)
public void handleRuntimeException(AsyncRequestTimeoutException e) {
}
/** /**
* 拦截未知的运行时异常 * 拦截未知的运行时异常
*/ */

View File

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

View File

@@ -18,7 +18,7 @@ spring:
snail-job: snail-job:
# 服务端节点IP(默认按照`NetUtil.getLocalIpStr()`) # 服务端节点IP(默认按照`NetUtil.getLocalIpStr()`)
server-host: server-host:
# 服务端netty的端口号 # 服务端端口号
server-port: 17888 server-port: 17888
# 合并日志默认保存天数 # 合并日志默认保存天数
merge-Log-days: 1 merge-Log-days: 1

View File

@@ -18,7 +18,7 @@ spring:
snail-job: snail-job:
# 服务端节点IP(默认按照`NetUtil.getLocalIpStr()`) # 服务端节点IP(默认按照`NetUtil.getLocalIpStr()`)
server-host: server-host:
# 服务端netty的端口号 # 服务端端口号
server-port: 17888 server-port: 17888
# 合并日志默认保存天数 # 合并日志默认保存天数
merge-Log-days: 1 merge-Log-days: 1

View File

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

View File

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

View File

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

View File

@@ -123,7 +123,7 @@
#end #end
#end #end
#end #end
<el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
<template #default="scope"> <template #default="scope">
<el-tooltip content="修改" placement="top"> <el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${moduleName}:${businessName}:edit']" /> <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${moduleName}:${businessName}:edit']" />

View File

@@ -120,7 +120,7 @@
<el-table-column label="${comment}" align="center" prop="${javaField}" /> <el-table-column label="${comment}" align="center" prop="${javaField}" />
#end #end
#end #end
<el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
<template #default="scope"> <template #default="scope">
<el-tooltip content="修改" placement="top"> <el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${moduleName}:${businessName}:edit']"></el-button> <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${moduleName}:${businessName}:edit']"></el-button>

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper <!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="${packageName}.mapper.${ClassName}Mapper"> <mapper namespace="${packageName}.mapper.${ClassName}Mapper">
</mapper> </mapper>

View File

@@ -1,26 +1,27 @@
package org.dromara.system.controller.system; 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 cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.validation.annotation.Validated; import jakarta.validation.constraints.NotEmpty;
import org.dromara.common.idempotent.annotation.RepeatSubmit; import jakarta.validation.constraints.NotNull;
import org.dromara.common.log.annotation.Log; import lombok.RequiredArgsConstructor;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.core.domain.R; import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup; import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup; import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil; import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.system.domain.vo.SysClientVo; import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.system.domain.bo.SysClientBo; import org.dromara.common.log.annotation.Log;
import org.dromara.system.service.ISysClientService; 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.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() @RepeatSubmit()
@PostMapping() @PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody SysClientBo bo) { 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)); return toAjax(sysClientService.insertByBo(bo));
} }
@@ -87,6 +91,9 @@ public class SysClientController extends BaseController {
@RepeatSubmit() @RepeatSubmit()
@PutMapping() @PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody SysClientBo bo) { 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)); return toAjax(sysClientService.updateByBo(bo));
} }

View File

@@ -1,6 +1,7 @@
package org.dromara.system.controller.system; package org.dromara.system.controller.system;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -13,8 +14,10 @@ import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController; import org.dromara.common.web.core.BaseController;
import org.dromara.system.domain.bo.SysDeptBo;
import org.dromara.system.domain.bo.SysPostBo; import org.dromara.system.domain.bo.SysPostBo;
import org.dromara.system.domain.vo.SysPostVo; import org.dromara.system.domain.vo.SysPostVo;
import org.dromara.system.service.ISysDeptService;
import org.dromara.system.service.ISysPostService; import org.dromara.system.service.ISysPostService;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -35,6 +38,7 @@ import java.util.List;
public class SysPostController extends BaseController { public class SysPostController extends BaseController {
private final ISysPostService postService; private final ISysPostService postService;
private final ISysDeptService deptService;
/** /**
* 获取岗位列表 * 获取岗位列表
@@ -134,4 +138,14 @@ public class SysPostController extends BaseController {
return R.ok(list); return R.ok(list);
} }
/**
* 获取部门树列表
*/
@SaCheckPermission("system:post:list")
@GetMapping("/deptTree")
public R<List<Tree<Long>>> deptTree(SysDeptBo dept) {
return R.ok(deptService.selectDeptTreeList(dept));
}
} }

View File

@@ -193,4 +193,19 @@ public class SysTenantController extends BaseController {
return R.ok("同步租户字典成功"); 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.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.dromara.common.core.constant.RegexConstants; 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.common.mybatis.core.domain.BaseEntity;
import org.dromara.system.domain.SysMenu; import org.dromara.system.domain.SysMenu;
@@ -61,6 +63,7 @@ public class SysMenuBo extends BaseEntity {
/** /**
* 路由参数 * 路由参数
*/ */
@JsonPattern(type = JsonType.OBJECT, message = "路由参数必须符合JSON格式")
private String queryParam; private String queryParam;
/** /**

View File

@@ -32,6 +32,11 @@ public class MetaVo {
*/ */
private String link; private String link;
/**
* 激活菜单
*/
private String activeMenu;
public MetaVo(String title, String icon) { public MetaVo(String title, String icon) {
this.title = title; this.title = title;
this.icon = icon; this.icon = icon;
@@ -58,4 +63,16 @@ public class MetaVo {
} }
} }
public MetaVo(String title, String icon, Boolean noCache, String link, String activeMenu) {
this.title = title;
this.icon = icon;
this.noCache = noCache;
if (StringUtils.ishttp(link)) {
this.link = link;
}
if (StringUtils.startWithAnyIgnoreCase(activeMenu, "/")) {
this.activeMenu = activeMenu;
}
}
} }

View File

@@ -30,7 +30,9 @@ public interface SysDeptMapper extends BaseMapperPlus<SysDept, SysDeptVo> {
*/ */
default String buildDeptByRoleSql(Long roleId) { default String buildDeptByRoleSql(Long roleId) {
return """ return """
select dept_id from sys_role_dept where role_id = %d select srd.dept_id from sys_role_dept srd
left join sys_role sr on sr.role_id = srd.role_id
where srd.role_id = %d and sr.status = '0'
""".formatted(roleId); """.formatted(roleId);
} }
@@ -47,7 +49,9 @@ public interface SysDeptMapper extends BaseMapperPlus<SysDept, SysDeptVo> {
default String buildParentDeptByRoleSql(Long roleId) { default String buildParentDeptByRoleSql(Long roleId) {
return """ return """
select parent_id from sys_dept where dept_id in ( select parent_id from sys_dept where dept_id in (
select dept_id from sys_role_dept where role_id = %d select srd.dept_id from sys_role_dept srd
left join sys_role sr on sr.role_id = srd.role_id
where srd.role_id = %d and sr.status = '0'
) )
""".formatted(roleId); """.formatted(roleId);
} }

View File

@@ -32,7 +32,9 @@ public interface SysMenuMapper extends BaseMapperPlus<SysMenu, SysMenuVo> {
default String buildMenuByUserSql(Long userId) { default String buildMenuByUserSql(Long userId) {
return """ return """
select menu_id from sys_role_menu where role_id in ( select menu_id from sys_role_menu where role_id in (
select role_id from sys_user_role where user_id = %d select sur.role_id from sys_user_role sur
left join sys_role sr on sr.role_id = sur.role_id
where sur.user_id = %d and sr.status = '0'
) )
""".formatted(userId); """.formatted(userId);
} }
@@ -50,7 +52,9 @@ public interface SysMenuMapper extends BaseMapperPlus<SysMenu, SysMenuVo> {
*/ */
default String buildMenuByRoleSql(Long roleId) { default String buildMenuByRoleSql(Long roleId) {
return """ return """
select menu_id from sys_role_menu where role_id = %d select srm.menu_id from sys_role_menu srm
left join sys_role sr on sr.role_id = srm.role_id
where srm.role_id = %d and sr.status = '0'
""".formatted(roleId); """.formatted(roleId);
} }
@@ -68,7 +72,9 @@ public interface SysMenuMapper extends BaseMapperPlus<SysMenu, SysMenuVo> {
default String buildParentMenuByRoleSql(Long roleId) { default String buildParentMenuByRoleSql(Long roleId) {
return """ return """
select parent_id from sys_menu where menu_id in ( select parent_id from sys_menu where menu_id in (
select menu_id from sys_role_menu where role_id = %d select srm.menu_id from sys_role_menu srm
left join sys_role sr on sr.role_id = srm.role_id
where srm.role_id = %d and sr.status = '0'
) )
""".formatted(roleId); """.formatted(roleId);
} }

View File

@@ -1,10 +1,9 @@
package org.dromara.system.service; 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.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.Collection;
import java.util.List; import java.util.List;
@@ -57,4 +56,11 @@ public interface ISysClientService {
*/ */
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid); 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; 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.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.Collection;
import java.util.List; import java.util.List;
@@ -84,4 +84,9 @@ public interface ISysTenantService {
* 同步租户字典 * 同步租户字典
*/ */
void syncTenantDict(); void syncTenantDict();
/**
* 同步租户参数配置
*/
void syncTenantConfig();
} }

View File

@@ -1,6 +1,7 @@
package org.dromara.system.service.impl; package org.dromara.system.service.impl;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; 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) { public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
return baseMapper.deleteByIds(ids) > 0; 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; package org.dromara.system.service.impl;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; 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.ObjectUtils;
import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils; 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.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.redis.utils.CacheUtils; import org.dromara.common.redis.utils.CacheUtils;
@@ -82,6 +84,7 @@ public class SysConfigServiceImpl implements ISysConfigService, ConfigService {
/** /**
* 获取注册开关 * 获取注册开关
*
* @param tenantId 租户id * @param tenantId 租户id
* @return true开启false关闭 * @return true开启false关闭
*/ */
@@ -212,4 +215,54 @@ public class SysConfigServiceImpl implements ISysConfigService, ConfigService {
return SpringUtils.getAopProxy(this).selectConfigByKey(configKey); 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

@@ -161,6 +161,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
}); });
} }
return baseMapper.selectObjs(new LambdaQueryWrapper<SysMenu>() return baseMapper.selectObjs(new LambdaQueryWrapper<SysMenu>()
.select(SysMenu::getMenuId)
.in(SysMenu::getMenuId, menuIds) .in(SysMenu::getMenuId, menuIds)
.notIn(CollUtil.isNotEmpty(parentIds), SysMenu::getMenuId, parentIds), x -> { .notIn(CollUtil.isNotEmpty(parentIds), SysMenu::getMenuId, parentIds), x -> {
return Convert.toLong(x); return Convert.toLong(x);
@@ -185,7 +186,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
router.setPath(menu.getRouterPath()); router.setPath(menu.getRouterPath());
router.setComponent(menu.getComponentInfo()); router.setComponent(menu.getComponentInfo());
router.setQuery(menu.getQueryParam()); router.setQuery(menu.getQueryParam());
router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath())); router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath(), menu.getRemark()));
List<SysMenu> cMenus = menu.getChildren(); List<SysMenu> cMenus = menu.getChildren();
if (CollUtil.isNotEmpty(cMenus) && SystemConstants.TYPE_DIR.equals(menu.getMenuType())) { if (CollUtil.isNotEmpty(cMenus) && SystemConstants.TYPE_DIR.equals(menu.getMenuType())) {
router.setAlwaysShow(true); router.setAlwaysShow(true);
@@ -199,7 +200,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
children.setPath(menu.getPath()); children.setPath(menu.getPath());
children.setComponent(menu.getComponent()); children.setComponent(menu.getComponent());
children.setName(frameName); children.setName(frameName);
children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath())); children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath(), menu.getRemark()));
children.setQuery(menu.getQueryParam()); children.setQuery(menu.getQueryParam());
childrenList.add(children); childrenList.add(children);
router.setChildren(childrenList); router.setChildren(childrenList);
@@ -240,6 +241,8 @@ public class SysMenuServiceImpl implements ISysMenuService {
.setWeight(menu.getOrderNum()); .setWeight(menu.getOrderNum());
menuTree.put("menuType", menu.getMenuType()); menuTree.put("menuType", menu.getMenuType());
menuTree.put("icon", menu.getIcon()); menuTree.put("icon", menu.getIcon());
menuTree.put("visible", menu.getVisible());
menuTree.put("status", menu.getStatus());
}); });
} }

View File

@@ -270,7 +270,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
OssClient storage = OssFactory.instance(oss.getService()); OssClient storage = OssFactory.instance(oss.getService());
// 仅修改桶类型为 private 的URL临时URL时长为120s // 仅修改桶类型为 private 的URL临时URL时长为120s
if (AccessPolicyType.PRIVATE == storage.getAccessPolicy()) { 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; 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); w.in(SysUser::getDeptId, ids);
}).orderByAsc(SysUser::getUserId); }).orderByAsc(SysUser::getUserId);
if (StringUtils.isNotBlank(user.getExcludeUserIds())) { 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; return wrapper;
} }

View File

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

View File

@@ -0,0 +1,20 @@
package org.dromara.workflow.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 抄送设置枚举
*
* @author AprilWind
*/
@Getter
@AllArgsConstructor
public enum CopySettingEnum implements NodeExtEnum {
;
private final String label;
private final String value;
private final boolean selected;
}

View File

@@ -0,0 +1,20 @@
package org.dromara.workflow.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 变量枚举
*
* @author AprilWind
*/
@Getter
@AllArgsConstructor
public enum VariablesEnum implements NodeExtEnum {
;
private final String label;
private final String value;
private final boolean selected;
}

View File

@@ -1,7 +1,9 @@
package org.dromara.workflow.controller; package org.dromara.workflow.controller;
import cn.hutool.core.convert.Convert;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R; 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.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log; import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType; import org.dromara.common.log.enums.BusinessType;
@@ -76,7 +78,7 @@ public class FlwInstanceController extends BaseController {
*/ */
@DeleteMapping("/deleteByBusinessIds/{businessIds}") @DeleteMapping("/deleteByBusinessIds/{businessIds}")
public R<Void> deleteByBusinessIds(@PathVariable List<Long> businessIds) { public R<Void> deleteByBusinessIds(@PathVariable List<Long> businessIds) {
return toAjax(flwInstanceService.deleteByBusinessIds(businessIds)); return toAjax(flwInstanceService.deleteByBusinessIds(StreamUtils.toList(businessIds, Convert::toStr)));
} }
/** /**

View File

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

View File

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

View File

@@ -1,46 +0,0 @@
package org.dromara.workflow.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.workflow.domain.FlowInstanceBizExt;
/**
* 流程实例业务扩展业务对象 flow_instance_biz_ext
*
* @author may
* @date 2025-08-05
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = FlowInstanceBizExt.class, reverseConvertGenerate = false)
public class FlowInstanceBizExtBo extends BaseEntity {
/**
* 主键
*/
private Long id;
/**
* 流程实例ID
*/
private Long instanceId;
/**
* 业务ID
*/
private String businessId;
/**
* 业务编码
*/
private String businessCode;
/**
* 业务标题
*/
private String businessTitle;
}

View File

@@ -50,6 +50,6 @@ public class FlowInstanceBo implements Serializable {
/** /**
* 申请人Ids * 申请人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; import org.dromara.workflow.domain.FlowSpel;
/** /**
* 流程spel达式定义业务对象 flow_spel * 流程spel达式定义业务对象 flow_spel
* *
* @author Michelle.Chung * @author Michelle.Chung
* @date 2025-07-04 * @date 2025-07-04

View File

@@ -1,9 +1,11 @@
package org.dromara.workflow.domain.bo; package org.dromara.workflow.domain.bo;
import cn.hutool.core.util.ObjectUtil;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import lombok.Data; import lombok.Data;
import org.dromara.common.core.validate.AddGroup; import org.dromara.common.core.validate.AddGroup;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
@@ -47,7 +49,7 @@ public class StartProcessBo implements Serializable {
/** /**
* 流程业务扩展信息 * 流程业务扩展信息
*/ */
private FlowInstanceBizExtBo flowInstanceBizExtBo; private FlowInstanceBizExt bizExt;
public Map<String, Object> getVariables() { public Map<String, Object> getVariables() {
if (variables == null) { if (variables == null) {
@@ -56,4 +58,11 @@ public class StartProcessBo implements Serializable {
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue())); variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables; return variables;
} }
public FlowInstanceBizExt getBizExt() {
if (ObjectUtil.isNull(bizExt)) {
bizExt = new FlowInstanceBizExt();
}
return bizExt;
}
} }

View File

@@ -0,0 +1,36 @@
package org.dromara.workflow.domain.vo;
import lombok.Data;
import org.dromara.common.translation.annotation.Translation;
import org.dromara.common.translation.constant.TransConstant;
import java.io.Serial;
import java.io.Serializable;
/**
* 抄送对象
*
* @author AprilWind
*/
@Data
public class FlowCopyVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户id
*/
private Long userId;
/**
* 用户名称
*/
@Translation(type = TransConstant.USER_ID_TO_NICKNAME, mapper = "userId")
private String userName;
public FlowCopyVo(Long userId) {
this.userId = userId;
}
}

View File

@@ -1,58 +0,0 @@
package org.dromara.workflow.domain.vo;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import java.io.Serial;
import java.io.Serializable;
/**
* 流程实例业务扩展视图对象 flow_instance_biz_ext
*
* @author may
* @date 2025-08-05
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = FlowInstanceBizExt.class)
public class FlowInstanceBizExtVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@ExcelProperty(value = "主键")
private Long id;
/**
* 流程实例ID
*/
@ExcelProperty(value = "流程实例ID")
private Long instanceId;
/**
* 业务ID
*/
@ExcelProperty(value = "业务ID")
private String businessId;
/**
* 业务编码
*/
@ExcelProperty(value = "业务编码")
private String businessCode;
/**
* 业务标题
*/
@ExcelProperty(value = "业务标题")
private String businessTitle;
}

View File

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

View File

@@ -11,6 +11,7 @@ import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 任务视图 * 任务视图
@@ -185,6 +186,20 @@ public class FlowTaskVo implements Serializable {
*/ */
private List<ButtonPermissionVo> buttonList; private List<ButtonPermissionVo> buttonList;
/**
* 抄送对象 ID 集合
* <p>
* 根据扩展属性中 CopySettingEnum 类型的数据生成,存储需要抄送的对象 ID
*/
private List<FlowCopyVo> copyList;
/**
* 自定义参数 Map
* <p>
* 根据扩展属性中 VariablesEnum 类型的数据生成,存储 key=value 格式的自定义参数
*/
private Map<String, String> varList;
//业务扩展信息开始 //业务扩展信息开始
/** /**
* 业务编码 * 业务编码

View File

@@ -0,0 +1,45 @@
package org.dromara.workflow.domain.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Node 扩展属性解析结果 VO
* <p>
* 用于封装从扩展属性 JSON 中解析出的各类信息,包括按钮权限、抄送对象和自定义参数。
*
* @author AprilWind
*/
@Data
public class NodeExtVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 按钮权限列表
* <p>
* 根据扩展属性中 ButtonPermissionEnum 类型的数据生成,每个元素表示一个按钮及其是否勾选。
*/
private List<ButtonPermissionVo> buttonPermissions;
/**
* 抄送对象 ID 集合
* <p>
* 根据扩展属性中 CopySettingEnum 类型的数据生成,存储需要抄送的对象 ID
*/
private Set<String> copySettings;
/**
* 自定义参数 Map
* <p>
* 根据扩展属性中 VariablesEnum 类型的数据生成,存储 key=value 格式的自定义参数
*/
private Map<String, String> variables;
}

View File

@@ -24,7 +24,7 @@ import java.util.Map;
public class FlowProcessEventHandler { public class FlowProcessEventHandler {
/** /**
* 总体流程监听(例如: 草稿,撤销,退回,作废,终止,已完成,单任务完成等) * 总体流程监听(例如: 草稿,撤销,退回,作废,终止,已完成等)
* *
* @param flowCode 流程定义编码 * @param flowCode 流程定义编码
* @param instance 实例数据 * @param instance 实例数据

View File

@@ -1,33 +1,39 @@
package org.dromara.workflow.listener; package org.dromara.workflow.listener;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.TypeReference; import cn.hutool.core.lang.TypeReference;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.enums.BusinessStatusEnum; import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.common.core.service.UserService;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.StringUtils;
import org.dromara.warm.flow.core.FlowEngine;
import org.dromara.warm.flow.core.dto.FlowParams; import org.dromara.warm.flow.core.dto.FlowParams;
import org.dromara.warm.flow.core.entity.Definition; import org.dromara.warm.flow.core.entity.Definition;
import org.dromara.warm.flow.core.entity.Instance; import org.dromara.warm.flow.core.entity.Instance;
import org.dromara.warm.flow.core.entity.Task; import org.dromara.warm.flow.core.entity.Task;
import org.dromara.warm.flow.core.listener.GlobalListener; import org.dromara.warm.flow.core.listener.GlobalListener;
import org.dromara.warm.flow.core.listener.ListenerVariable; import org.dromara.warm.flow.core.listener.ListenerVariable;
import org.dromara.warm.flow.core.service.InsService;
import org.dromara.workflow.common.ConditionalOnEnable; import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.constant.FlowConstant; import org.dromara.workflow.common.constant.FlowConstant;
import org.dromara.workflow.common.enums.TaskStatusEnum; import org.dromara.workflow.common.enums.TaskStatusEnum;
import org.dromara.workflow.domain.bo.FlowCopyBo; import org.dromara.workflow.domain.bo.FlowCopyBo;
import org.dromara.workflow.domain.vo.NodeExtVo;
import org.dromara.workflow.handler.FlowProcessEventHandler; import org.dromara.workflow.handler.FlowProcessEventHandler;
import org.dromara.workflow.service.IFlwCommonService; import org.dromara.workflow.service.IFlwCommonService;
import org.dromara.workflow.service.IFlwInstanceService; import org.dromara.workflow.service.IFlwInstanceService;
import org.dromara.workflow.service.IFlwNodeExtService;
import org.dromara.workflow.service.IFlwTaskService; import org.dromara.workflow.service.IFlwTaskService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* 全局任务办理监听 * 全局任务办理监听
@@ -41,10 +47,11 @@ import java.util.Map;
public class WorkflowGlobalListener implements GlobalListener { public class WorkflowGlobalListener implements GlobalListener {
private final IFlwTaskService flwTaskService; private final IFlwTaskService flwTaskService;
private final IFlwInstanceService instanceService; private final IFlwInstanceService flwInstanceService;
private final FlowProcessEventHandler flowProcessEventHandler; private final FlowProcessEventHandler flowProcessEventHandler;
private final IFlwCommonService flwCommonService; private final IFlwCommonService flwCommonService;
private final InsService insService; private final IFlwNodeExtService nodeExtService;
private final UserService userService;
/** /**
* 创建监听器,任务创建时执行 * 创建监听器,任务创建时执行
@@ -63,6 +70,25 @@ public class WorkflowGlobalListener implements GlobalListener {
*/ */
@Override @Override
public void start(ListenerVariable listenerVariable) { public void start(ListenerVariable listenerVariable) {
String ext = listenerVariable.getNode().getExt();
if (StringUtils.isNotBlank(ext)) {
Map<String, Object> variable = listenerVariable.getVariable();
NodeExtVo nodeExt = nodeExtService.parseNodeExt(ext, variable);
Set<String> copyList = nodeExt.getCopySettings();
if (CollUtil.isNotEmpty(copyList)) {
List<FlowCopyBo> list = StreamUtils.toList(copyList, x -> {
FlowCopyBo bo = new FlowCopyBo();
Long id = Convert.toLong(x);
bo.setUserId(id);
bo.setUserName(userService.selectUserNameById(id));
return bo;
});
variable.put(FlowConstant.FLOW_COPY_LIST, list);
}
if (CollUtil.isNotEmpty(nodeExt.getVariables())) {
variable.putAll(nodeExt.getVariables());
}
}
} }
/** /**
@@ -118,7 +144,8 @@ public class WorkflowGlobalListener implements GlobalListener {
//申请人提交事件 //申请人提交事件
Boolean submit = MapUtil.getBool(variable, FlowConstant.SUBMIT); Boolean submit = MapUtil.getBool(variable, FlowConstant.SUBMIT);
if (submit != null && 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 { } else {
// 判断流程状态(发布:撤销,退回,作废,终止,已完成事件) // 判断流程状态(发布:撤销,退回,作废,终止,已完成事件)
String status = determineFlowStatus(instance); String status = determineFlowStatus(instance);
@@ -132,7 +159,7 @@ public class WorkflowGlobalListener implements GlobalListener {
flowProcessEventHandler.processHandler(definition.getFlowCode(), instance, BusinessStatusEnum.BACK.getStatus(), params, false); flowProcessEventHandler.processHandler(definition.getFlowCode(), instance, BusinessStatusEnum.BACK.getStatus(), params, false);
// 修改流程实例状态 // 修改流程实例状态
instance.setFlowStatus(BusinessStatusEnum.BACK.getStatus()); instance.setFlowStatus(BusinessStatusEnum.BACK.getStatus());
insService.updateById(instance); FlowEngine.insService().updateById(instance);
} }
} }
} }
@@ -154,19 +181,18 @@ public class WorkflowGlobalListener implements GlobalListener {
} }
if (variable.containsKey(FlowConstant.FLOW_COPY_LIST)) { 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); flwTaskService.setCopy(task, flowCopyList);
} }
if (variable.containsKey(FlowConstant.MESSAGE_TYPE)) { 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); String notice = MapUtil.getStr(variable, FlowConstant.MESSAGE_NOTICE);
// 消息通知 flwCommonService.sendMessage(definition.getFlowName(), instance.getId(), messageType, notice);
if (CollUtil.isNotEmpty(messageType)) {
flwCommonService.sendMessage(definition.getFlowName(), instance.getId(), messageType, notice);
}
} }
insService.removeVariables(instance.getId(), FlowEngine.insService().removeVariables(instance.getId(),
FlowConstant.FLOW_COPY_LIST, FlowConstant.FLOW_COPY_LIST,
FlowConstant.MESSAGE_TYPE, FlowConstant.MESSAGE_TYPE,
FlowConstant.MESSAGE_NOTICE, FlowConstant.MESSAGE_NOTICE,
@@ -190,7 +216,7 @@ public class WorkflowGlobalListener implements GlobalListener {
if (flwTaskService.isTaskEnd(instanceId)) { if (flwTaskService.isTaskEnd(instanceId)) {
String status = BusinessStatusEnum.FINISH.getStatus(); String status = BusinessStatusEnum.FINISH.getStatus();
// 更新流程状态为已完成 // 更新流程状态为已完成
instanceService.updateStatus(instanceId, status); flwInstanceService.updateStatus(instanceId, status);
log.info("流程已结束,状态更新为: {}", status); log.info("流程已结束,状态更新为: {}", status);
return status; return status;
} }

View File

@@ -1,8 +1,11 @@
package org.dromara.workflow.mapper; package org.dromara.workflow.mapper;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.workflow.domain.FlowInstanceBizExt; import org.dromara.workflow.domain.FlowInstanceBizExt;
import org.dromara.workflow.domain.vo.FlowInstanceBizExtVo;
import java.util.List;
/** /**
* 流程实例业务扩展Mapper接口 * 流程实例业务扩展Mapper接口
@@ -10,6 +13,49 @@ import org.dromara.workflow.domain.vo.FlowInstanceBizExtVo;
* @author may * @author may
* @date 2025-08-05 * @date 2025-08-05
*/ */
public interface FlwInstanceBizExtMapper extends BaseMapperPlus<FlowInstanceBizExt, FlowInstanceBizExtVo> { public interface FlwInstanceBizExtMapper extends BaseMapperPlus<FlowInstanceBizExt, FlowInstanceBizExt> {
/**
* 根据 instanceId 保存或更新流程实例业务扩展
*
* @param entity 流程实例业务扩展实体
* @return 操作是否成功
*/
default int saveOrUpdateByInstanceId(FlowInstanceBizExt entity) {
// 查询是否存在
FlowInstanceBizExt exist = this.selectOne(new LambdaQueryWrapper<FlowInstanceBizExt>()
.eq(FlowInstanceBizExt::getInstanceId, entity.getInstanceId()));
if (ObjectUtil.isNotNull(exist)) {
// 存在就带上主键更新
entity.setId(exist.getId());
return updateById(entity);
} else {
// 不存在就插入
return insert(entity);
}
}
/**
* 按照流程实例ID删除单个流程实例业务扩展
*
* @param instanceId 流程实例ID
* @return 删除的行数
*/
default int deleteByInstId(Long instanceId) {
return this.delete(new LambdaQueryWrapper<FlowInstanceBizExt>()
.eq(FlowInstanceBizExt::getInstanceId, instanceId));
}
/**
* 按照流程实例ID批量删除流程实例业务扩展
*
* @param instanceIds 流程实例ID列表
* @return 删除的行数
*/
default int deleteByInstIds(List<Long> instanceIds) {
return this.delete(new LambdaQueryWrapper<FlowInstanceBizExt>()
.in(FlowInstanceBizExt::getInstanceId, instanceIds));
}
} }

View File

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

View File

@@ -1,30 +0,0 @@
package org.dromara.workflow.service;
import org.dromara.workflow.domain.bo.FlowInstanceBizExtBo;
import java.util.List;
/**
* 流程实例业务扩展Service接口
*
* @author may
* @date 2025-08-05
*/
public interface IFlwInstanceBizExtService {
/**
* 新增/修改流程实例业务扩展
*
* @param bo 流程实例业务扩展
* @return 是否新增成功
*/
Boolean saveOrUpdate(FlowInstanceBizExtBo bo);
/**
* 按照流程实例ID批量删除
*
* @param instanceIds 流程实例ID
* @return 是否删除成功
*/
Boolean deleteByInstIds(List<Long> instanceIds);
}

View File

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

View File

@@ -1,8 +1,8 @@
package org.dromara.workflow.service; package org.dromara.workflow.service;
import org.dromara.workflow.domain.vo.ButtonPermissionVo; import org.dromara.workflow.domain.vo.NodeExtVo;
import java.util.List; import java.util.Map;
/** /**
* 流程节点扩展属性 服务层 * 流程节点扩展属性 服务层
@@ -12,11 +12,24 @@ import java.util.List;
public interface IFlwNodeExtService { public interface IFlwNodeExtService {
/** /**
* 扩展属性构建按钮权限列表:根据 ext 中记录的权限值,标记每个按钮是否勾选 * 解析扩展属性 JSON 并构建 Node 扩展属性对象
* <p>
* 根据传入的 JSON 字符串,将扩展属性分为三类:
* 1. ButtonPermissionEnum解析为按钮权限列表标记每个按钮是否勾选
* 2. CopySettingEnum解析为抄送对象 ID 集合
* 3. VariablesEnum解析为自定义参数 Map
* *
* @param ext 扩展属性 JSON 字符串 * <p>示例 JSON
* @return 按钮权限 VO 列表 * [
* {"code": "ButtonPermissionEnum", "value": "back,termination"},
* {"code": "CopySettingEnum", "value": "1"},
* {"code": "VariablesEnum", "value": "key1=value1,key2=value2"}
* ]
*
* @param ext 扩展属性 JSON 字符串
* @param variable 流程变量
* @return NodeExtVo 对象,封装按钮权限列表、抄送对象集合和自定义参数 Map
*/ */
List<ButtonPermissionVo> buildButtonPermissionsFromExt(String ext); NodeExtVo parseNodeExt(String ext, Map<String, Object> variable);
} }

View File

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

View File

@@ -24,6 +24,7 @@ import org.dromara.warm.flow.orm.mapper.FlowHisTaskMapper;
import org.dromara.warm.flow.ui.service.ChartExtService; import org.dromara.warm.flow.ui.service.ChartExtService;
import org.dromara.workflow.common.ConditionalOnEnable; import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.constant.FlowConstant; import org.dromara.workflow.common.constant.FlowConstant;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList; import java.util.ArrayList;
@@ -48,6 +49,8 @@ public class FlwChartExtServiceImpl implements ChartExtService {
private final DeptService deptService; private final DeptService deptService;
private final FlowHisTaskMapper flowHisTaskMapper; private final FlowHisTaskMapper flowHisTaskMapper;
private final DictService dictService; private final DictService dictService;
@Value("${warm-flow.node-tooltip:true}")
private boolean nodeTooltip;
/** /**
* 设置流程图提示信息 * 设置流程图提示信息
@@ -56,6 +59,11 @@ public class FlwChartExtServiceImpl implements ChartExtService {
*/ */
@Override @Override
public void execute(DefJson defJson) { public void execute(DefJson defJson) {
// 配置关闭,直接返回,不渲染悬浮窗
if (!nodeTooltip) {
return;
}
// 根据流程实例ID查询所有相关的历史任务列表 // 根据流程实例ID查询所有相关的历史任务列表
List<FlowHisTask> flowHisTasks = this.getHisTaskGroupedByNode(defJson.getInstance().getId()); List<FlowHisTask> flowHisTasks = this.getHisTaskGroupedByNode(defJson.getInstance().getId());
if (CollUtil.isEmpty(flowHisTasks)) { if (CollUtil.isEmpty(flowHisTasks)) {
@@ -103,6 +111,11 @@ public class FlwChartExtServiceImpl implements ChartExtService {
*/ */
@Override @Override
public void initPromptContent(DefJson defJson) { public void initPromptContent(DefJson defJson) {
// 配置关闭,直接返回,不渲染悬浮窗
if (!nodeTooltip) {
return;
}
defJson.setTopText("流程名称: " + defJson.getFlowName()); defJson.setTopText("流程名称: " + defJson.getFlowName());
defJson.getNodeList().forEach(nodeJson -> { defJson.getNodeList().forEach(nodeJson -> {
nodeJson.setPromptContent( nodeJson.setPromptContent(
@@ -152,8 +165,10 @@ public class FlwChartExtServiceImpl implements ChartExtService {
/** /**
* 处理节点的扩展信息,构建用于流程图悬浮提示的内容 * 处理节点的扩展信息,构建用于流程图悬浮提示的内容
* *
* @param nodeJson 当前节点对象 * @param nodeJson 当前流程节点对象,包含节点基础信息和提示内容容器
* @param taskList 当前节点对应的历史审批任务列表 * @param taskList 当前节点关联的历史审批任务列表,用于生成提示信息
* @param userMap 用户信息映射表key 为用户IDvalue 为用户DTO对象用于获取审批人信息
* @param dictType 数据字典映射表key 为字典项编码value 为对应显示值,用于翻译审批状态等
*/ */
private void processNodeExtInfo(NodeJson nodeJson, List<FlowHisTask> taskList, Map<Long, UserDTO> userMap, Map<String, String> dictType) { private void processNodeExtInfo(NodeJson nodeJson, List<FlowHisTask> taskList, Map<Long, UserDTO> userMap, Map<String, String> dictType) {

View File

@@ -11,9 +11,8 @@ import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mail.utils.MailUtils; import org.dromara.common.mail.utils.MailUtils;
import org.dromara.common.sse.dto.SseMessageDto; import org.dromara.common.sse.dto.SseMessageDto;
import org.dromara.common.sse.utils.SseMessageUtils; import org.dromara.common.sse.utils.SseMessageUtils;
import org.dromara.warm.flow.core.FlowEngine;
import org.dromara.warm.flow.core.entity.Node; import org.dromara.warm.flow.core.entity.Node;
import org.dromara.warm.flow.core.enums.SkipType;
import org.dromara.warm.flow.core.service.NodeService;
import org.dromara.warm.flow.orm.entity.FlowTask; import org.dromara.warm.flow.orm.entity.FlowTask;
import org.dromara.workflow.common.ConditionalOnEnable; import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.MessageTypeEnum; import org.dromara.workflow.common.enums.MessageTypeEnum;
@@ -21,8 +20,10 @@ import org.dromara.workflow.service.IFlwCommonService;
import org.dromara.workflow.service.IFlwTaskService; import org.dromara.workflow.service.IFlwTaskService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.Set;
/** /**
@@ -35,19 +36,27 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor @RequiredArgsConstructor
@Service @Service
public class FlwCommonServiceImpl implements IFlwCommonService { public class FlwCommonServiceImpl implements IFlwCommonService {
private final NodeService nodeService;
private static final String DEFAULT_SUBJECT = "单据审批提醒";
/** /**
* 发送消息 * 根据流程实例发送消息给当前处理人
* *
* @param flowName 流程定义名称 * @param flowName 流程定义名称
* @param messageType 消息类型 * @param instId 流程实例ID
* @param message 消息内容,为空则发送默认配置的消息内容 * @param messageType 消息类型列表
* @param message 消息内容,为空则使用默认消息
*/ */
@Override @Override
public void sendMessage(String flowName, Long instId, List<String> messageType, String message) { public void sendMessage(String flowName, Long instId, List<String> messageType, String message) {
if (CollUtil.isEmpty(messageType)) {
return;
}
IFlwTaskService flwTaskService = SpringUtils.getBean(IFlwTaskService.class); IFlwTaskService flwTaskService = SpringUtils.getBean(IFlwTaskService.class);
List<FlowTask> list = flwTaskService.selectByInstId(instId); List<FlowTask> list = flwTaskService.selectByInstId(instId);
if (CollUtil.isEmpty(list)) {
return;
}
if (StringUtils.isBlank(message)) { if (StringUtils.isBlank(message)) {
message = "有新的【" + flowName + "】单据已经提交至您,请您及时处理。"; message = "有新的【" + flowName + "】单据已经提交至您,请您及时处理。";
} }
@@ -55,43 +64,52 @@ public class FlwCommonServiceImpl implements IFlwCommonService {
if (CollUtil.isEmpty(userList)) { if (CollUtil.isEmpty(userList)) {
return; return;
} }
sendMessage(messageType, message, "单据审批提醒", userList); sendMessage(messageType, message, DEFAULT_SUBJECT, userList);
} }
/** /**
* 发送消息 * 发送消息给指定用户列表
* *
* @param messageType 消息类型 * @param messageType 消息类型列表
* @param message 消息内容 * @param message 消息内容
* @param subject 邮件标题 * @param subject 邮件标题
* @param userList 接收用户 * @param userList 接收用户列表
*/ */
@Override @Override
public void sendMessage(List<String> messageType, String message, String subject, List<UserDTO> userList) { public void sendMessage(List<String> messageType, String message, String subject, List<UserDTO> userList) {
if (CollUtil.isEmpty(messageType) || CollUtil.isEmpty(userList)) {
return;
}
List<Long> userIds = new ArrayList<>(StreamUtils.toSet(userList, UserDTO::getUserId));
Set<String> emails = StreamUtils.toSet(userList, UserDTO::getEmail);
for (String code : messageType) { for (String code : messageType) {
MessageTypeEnum messageTypeEnum = MessageTypeEnum.getByCode(code); MessageTypeEnum messageTypeEnum = MessageTypeEnum.getByCode(code);
if (ObjectUtil.isEmpty(messageTypeEnum)) { if (ObjectUtil.isEmpty(messageTypeEnum)) {
continue; continue;
} }
switch (messageTypeEnum) { try {
case SYSTEM_MESSAGE -> { switch (messageTypeEnum) {
SseMessageDto dto = new SseMessageDto(); case SYSTEM_MESSAGE -> {
dto.setUserIds(StreamUtils.toList(userList, UserDTO::getUserId).stream().distinct().collect(Collectors.toList())); SseMessageDto dto = new SseMessageDto();
dto.setMessage(message); dto.setUserIds(userIds);
SseMessageUtils.publishMessage(dto); 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 -> { } catch (Exception ex) {
MailUtils.sendText(StreamUtils.join(userList, UserDTO::getEmail), subject, message); // 记录错误但不抛出,确保主逻辑不受影响
} log.error("【消息发送失败】类型={},原因={}", messageTypeEnum, ex.getMessage(), ex);
case SMS_MESSAGE -> {
//todo 短信发送
}
default -> throw new IllegalStateException("Unexpected value: " + messageTypeEnum);
} }
} }
} }
/** /**
* 申请人节点编码 * 申请人节点编码
* *
@@ -100,8 +118,7 @@ public class FlwCommonServiceImpl implements IFlwCommonService {
*/ */
@Override @Override
public String applyNodeCode(Long definitionId) { public String applyNodeCode(Long definitionId) {
Node startNode = nodeService.getStartNode(definitionId); List<Node> firstBetweenNode = FlowEngine.nodeService().getFirstBetweenNode(definitionId, new HashMap<>());
Node nextNode = nodeService.getNextNode(definitionId, startNode.getNodeCode(), null, SkipType.PASS.getKey()); return firstBetweenNode.get(0).getNodeCode();
return nextNode.getNodeCode();
} }
} }

View File

@@ -1,57 +0,0 @@
package org.dromara.workflow.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import org.dromara.workflow.domain.bo.FlowInstanceBizExtBo;
import org.dromara.workflow.mapper.FlwInstanceBizExtMapper;
import org.dromara.workflow.service.IFlwInstanceBizExtService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 流程实例业务扩展Service业务层处理
*
* @author may
* @date 2025-08-05
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class FlwInstanceBizExtServiceImpl implements IFlwInstanceBizExtService {
private final FlwInstanceBizExtMapper baseMapper;
/**
* 新增/修改流程实例业务扩展
*
* @param bo 流程实例业务扩展
* @return 是否新增成功
*/
@Override
public Boolean saveOrUpdate(FlowInstanceBizExtBo bo) {
FlowInstanceBizExt convert = MapstructUtils.convert(bo, FlowInstanceBizExt.class);
FlowInstanceBizExt flowInstanceBizExt = baseMapper.selectOne(new LambdaQueryWrapper<FlowInstanceBizExt>()
.eq(FlowInstanceBizExt::getInstanceId, bo.getInstanceId()));
if (flowInstanceBizExt != null) {
flowInstanceBizExt.setBusinessTitle(convert.getBusinessTitle());
return baseMapper.insertOrUpdate(flowInstanceBizExt);
}
return baseMapper.insertOrUpdate(convert);
}
/**
* 按照流程实例ID批量删除
*
* @param instanceIds 流程实例ID
* @return 是否删除成功
*/
@Override
public Boolean deleteByInstIds(List<Long> instanceIds) {
return baseMapper.delete(new LambdaQueryWrapper<FlowInstanceBizExt>().in(FlowInstanceBizExt::getInstanceId, instanceIds)) > 0;
}
}

View File

@@ -181,8 +181,8 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
*/ */
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean deleteByBusinessIds(List<Long> businessIds) { public boolean deleteByBusinessIds(List<String> businessIds) {
List<FlowInstance> flowInstances = flowInstanceMapper.selectList(new LambdaQueryWrapper<FlowInstance>().in(FlowInstance::getBusinessId, StreamUtils.toList(businessIds, Convert::toStr))); List<FlowInstance> flowInstances = flowInstanceMapper.selectList(new LambdaQueryWrapper<FlowInstance>().in(FlowInstance::getBusinessId, businessIds));
if (CollUtil.isEmpty(flowInstances)) { if (CollUtil.isEmpty(flowInstances)) {
log.warn("未找到对应的流程实例信息,无法执行删除操作。"); log.warn("未找到对应的流程实例信息,无法执行删除操作。");
return false; return false;

View File

@@ -1,26 +1,31 @@
package org.dromara.workflow.service.impl; package org.dromara.workflow.service.impl;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.dto.DictTypeDTO; import org.dromara.common.core.domain.dto.DictTypeDTO;
import org.dromara.common.core.service.DictService; import org.dromara.common.core.service.DictService;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils; 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.service.NodeExtService;
import org.dromara.warm.flow.ui.vo.NodeExt; import org.dromara.warm.flow.ui.vo.NodeExt;
import org.dromara.workflow.common.ConditionalOnEnable; import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.ButtonPermissionEnum; import org.dromara.workflow.common.enums.ButtonPermissionEnum;
import org.dromara.workflow.common.enums.CopySettingEnum;
import org.dromara.workflow.common.enums.NodeExtEnum; import org.dromara.workflow.common.enums.NodeExtEnum;
import org.dromara.workflow.common.enums.VariablesEnum;
import org.dromara.workflow.domain.vo.ButtonPermissionVo; import org.dromara.workflow.domain.vo.ButtonPermissionVo;
import org.dromara.workflow.domain.vo.NodeExtVo;
import org.dromara.workflow.service.IFlwNodeExtService; import org.dromara.workflow.service.IFlwNodeExtService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
/** /**
* 流程设计器-节点扩展属性 * 流程设计器-节点扩展属性
@@ -36,14 +41,35 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService
/** /**
* 存储不同 dictType 对应的配置信息 * 存储不同 dictType 对应的配置信息
*/ */
private static final Map<String, ButtonPermission> CHILD_NODE_MAP = new HashMap<>(); private static final Map<String, Map<String, Object>> CHILD_NODE_MAP;
record ButtonPermission(String label, Integer type, Boolean must, Boolean multiple) {
}
static { static {
CHILD_NODE_MAP.put(ButtonPermissionEnum.class.getSimpleName(), CHILD_NODE_MAP = Map.of(
new ButtonPermission("权限按钮", 4, false, true)); CopySettingEnum.class.getSimpleName(),
Map.of(
"label", "抄送对象",
"type", 5,
"must", false,
"multiple", false,
"desc", "设置该节点的抄送办理人"
),
VariablesEnum.class.getSimpleName(),
Map.of(
"label", "自定义参数",
"type", 2,
"must", false,
"multiple", false,
"desc", "节点执行时可设置自定义参数多个参数以逗号分隔key1=value1,key2=value2"
),
ButtonPermissionEnum.class.getSimpleName(),
Map.of(
"label", "权限按钮",
"type", 4,
"must", false,
"multiple", true,
"desc", "控制该节点的按钮权限"
)
);
} }
private final DictService dictService; private final DictService dictService;
@@ -56,6 +82,9 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService
@Override @Override
public List<NodeExt> getNodeExt() { public List<NodeExt> getNodeExt() {
List<NodeExt> nodeExtList = new ArrayList<>(); List<NodeExt> nodeExtList = new ArrayList<>();
// 构建基础设置页面
nodeExtList.add(buildNodeExt("wf_basic_tab", "基础设置", 1,
List.of(CopySettingEnum.class, VariablesEnum.class)));
// 构建按钮权限页面 // 构建按钮权限页面
nodeExtList.add(buildNodeExt("wf_button_tab", "权限", 2, nodeExtList.add(buildNodeExt("wf_button_tab", "权限", 2,
List.of(ButtonPermissionEnum.class))); List.of(ButtonPermissionEnum.class)));
@@ -105,9 +134,20 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService
return null; return null;
} }
String simpleName = enumClass.getSimpleName(); String simpleName = enumClass.getSimpleName();
NodeExt.ChildNode childNode = buildChildNodeMap(simpleName); NodeExt.ChildNode childNode = new NodeExt.ChildNode();
Map<String, Object> map = CHILD_NODE_MAP.get(simpleName);
// 编码此json中唯 // 编码此json中唯
childNode.setCode(simpleName); childNode.setCode(simpleName);
// label名称
childNode.setLabel(Convert.toStr(map.get("label")));
// 1输入框 2文本域 3下拉框 4选择框 5用户选择器
childNode.setType(Convert.toInt(map.get("type"), 1));
// 是否必填
childNode.setMust(Convert.toBool(map.get("must"), false));
// 是否多选
childNode.setMultiple(Convert.toBool(map.get("multiple"), true));
// 描述
childNode.setDesc(Convert.toStr(map.get("desc"), null));
// 字典,下拉框和复选框时用到 // 字典,下拉框和复选框时用到
childNode.setDict(Arrays.stream(enumClass.getEnumConstants()) childNode.setDict(Arrays.stream(enumClass.getEnumConstants())
.map(NodeExtEnum.class::cast) .map(NodeExtEnum.class::cast)
@@ -128,12 +168,18 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService
if (ObjectUtil.isNull(dictTypeDTO)) { if (ObjectUtil.isNull(dictTypeDTO)) {
return null; return null;
} }
NodeExt.ChildNode childNode = buildChildNodeMap(dictType); NodeExt.ChildNode childNode = new NodeExt.ChildNode();
// 编码此json中唯一 // 编码此json中唯一
childNode.setCode(dictType); childNode.setCode(dictType);
// label名称 // label名称
childNode.setLabel(dictTypeDTO.getDictName()); childNode.setLabel(dictTypeDTO.getDictName());
// 描述 // 1输入框 2文本域 3下拉框 4选择框 5用户选择器
childNode.setType(3);
// 是否必填
childNode.setMust(false);
// 是否多选
childNode.setMultiple(true);
// 描述 (可根据描述参数解析更多配置如typemustmultiple等)
childNode.setDesc(dictTypeDTO.getRemark()); childNode.setDesc(dictTypeDTO.getRemark());
// 字典,下拉框和复选框时用到 // 字典,下拉框和复选框时用到
childNode.setDict(dictService.getDictData(dictType) childNode.setDict(dictService.getDictData(dictType)
@@ -144,100 +190,165 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService
} }
/** /**
* 根据 CHILD_NODE_MAP 中的配置信息,构建一个基本的 ChildNode 对象 * 解析扩展属性 JSON 并构建 Node 扩展属性对象
* 该方法用于设置 ChildNode 的常规属性,例如 label、type、是否必填、是否多选等 * <p>
* 根据传入的 JSON 字符串,将扩展属性分为三类:
* 1. ButtonPermissionEnum解析为按钮权限列表标记每个按钮是否勾选
* 2. CopySettingEnum解析为抄送对象 ID 集合
* 3. VariablesEnum解析为自定义参数 Map
* *
* @param key CHILD_NODE_MAP 的 key * <p>示例 JSON
* @return 返回构建好的 ChildNode 对象 * [
*/ * {"code": "ButtonPermissionEnum", "value": "back,termination"},
private NodeExt.ChildNode buildChildNodeMap(String key) { * {"code": "CopySettingEnum", "value": "1,3,4,#{@spelRuleComponent.selectDeptLeaderById(#deptId", "#roleId)}"},
NodeExt.ChildNode childNode = new NodeExt.ChildNode(); * {"code": "VariablesEnum", "value": "key1=value1,key2=value2"}
ButtonPermission bp = CHILD_NODE_MAP.get(key); * ]
if (bp == null) {
childNode.setType(1);
childNode.setMust(false);
childNode.setMultiple(true);
return childNode;
}
// label名称
childNode.setLabel(bp.label());
// 1输入框 2输入框 3下拉框 4选择框
childNode.setType(bp.type());
// 是否必填
childNode.setMust(bp.must());
// 是否多选
childNode.setMultiple(bp.multiple());
return childNode;
}
/**
* 从扩展属性构建按钮权限列表:根据 ext 中记录的权限值,标记每个按钮是否勾选
* *
* @param ext 扩展属性 JSON 字符串 * @param ext 扩展属性 JSON 字符串
* @return 按钮权限 VO 列表 * @param variable 流程变量
* @return NodeExtVo 对象,封装按钮权限列表、抄送对象集合和自定义参数 Map
*/ */
@Override @Override
public List<ButtonPermissionVo> buildButtonPermissionsFromExt(String ext) { public NodeExtVo parseNodeExt(String ext, Map<String, Object> variable) {
// 解析 ext 为 Map<code, Set<value>>,用于标记权限 NodeExtVo nodeExtVo = new NodeExtVo();
Map<String, Set<String>> permissionMap = JsonUtils.parseArray(ext, ButtonPermissionVo.class)
.stream()
.collect(Collectors.toMap(
ButtonPermissionVo::getCode,
item -> StringUtils.splitList(item.getValue()).stream()
.map(String::trim)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet()),
(a, b) -> b,
HashMap::new
));
// 构建按钮权限列表,标记哪些按钮在 permissionMap 中出现(表示已勾选) // 解析 JSON 为 Dict 列表
return buildPermissionsFromSources(permissionMap, List.of(ButtonPermissionEnum.class)); List<Dict> nodeExtMap = JsonUtils.parseArrayMap(ext);
if (ObjectUtil.isEmpty(nodeExtMap)) {
return nodeExtVo;
}
for (Dict nodeExt : nodeExtMap) {
String code = nodeExt.getStr("code");
String value = nodeExt.getStr("value");
if (ButtonPermissionEnum.class.getSimpleName().equals(code)) {
// 解析按钮权限
// 将 value 拆分为 Set<String>,便于精确匹配
Set<String> buttonSet = StringUtils.str2Set(value, StringUtils.SEPARATOR);
// 获取按钮字典配置
NodeExt.ChildNode childNode = buildChildNode(ButtonPermissionEnum.class);
// 构建 ButtonPermissionVo 列表
List<ButtonPermissionVo> buttonList = Optional.ofNullable(childNode)
.map(NodeExt.ChildNode::getDict)
.orElse(List.of())
.stream()
.map(dict -> new ButtonPermissionVo(dict.getValue(), buttonSet.contains(dict.getValue())))
.toList();
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(new HashSet<>(copySettings));
} else if (VariablesEnum.class.getSimpleName().equals(code)) {
// 解析自定义参数
// 将 key=value 字符串拆分为 Map
Map<String, String> variables = Arrays.stream(StringUtils.split(value, StringUtils.SEPARATOR))
.map(s -> StringUtils.split(s, "="))
.filter(arr -> arr.length == 2)
.collect(Collectors.toMap(arr -> arr[0], arr -> arr[1]));
nodeExtVo.setVariables(variables);
} else {
// 未知扩展类型,记录日志
log.warn("未知扩展类型code={}, value={}", code, value);
}
}
return nodeExtVo;
} }
/** /**
* 将权限映射与按钮权限来源(枚举类或字典类型)进行匹配,生成权限视图列表 * 按逗号分割字符串,但保留 #{...} 表达式和字符串常量中的逗号
* <p>
* 使用说明:
* - sources 支持传入多个来源类型,支持 NodeExtEnum 枚举类 或 字典类型字符串dictType
* - 若需要扩展更多按钮权限,只需在 sources 中新增对应的枚举类或字典类型
* <p>
* 示例:
* buildPermissionsFromSources(permissionMap, List.of(ButtonPermissionEnum.class, "custom_button_dict"));
*
* @param permissionMap 权限映射
* @param sources 枚举类或字典类型列表
* @return 按钮权限视图对象列表
*/ */
@SuppressWarnings("unchecked cast") private static List<String> spelSmartSplit(String str) {
private List<ButtonPermissionVo> buildPermissionsFromSources(Map<String, Set<String>> permissionMap, List<Object> sources) { List<String> result = new ArrayList<>();
return sources.stream() if (str == null || str.trim().isEmpty()) {
.flatMap(source -> { return result;
if (source instanceof Class<?> clazz && NodeExtEnum.class.isAssignableFrom(clazz)) { }
Set<String> selectedSet = permissionMap.getOrDefault(clazz.getSimpleName(), Collections.emptySet());
return extractDictItems(this.buildChildNode((Class<? extends NodeExtEnum>) clazz), selectedSet).stream(); StringBuilder token = new StringBuilder();
} else if (source instanceof String dictType) { // #{...} 的嵌套深度
Set<String> selectedSet = permissionMap.getOrDefault(dictType, Collections.emptySet()); int depth = 0;
return extractDictItems(this.buildChildNode(dictType), selectedSet).stream(); // 是否在字符串常量中(" 或 '
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--;
} }
return Stream.empty(); token.append(c);
}).toList(); 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;
*
* @param childNode 子节点
* @param selectedSet 已选中的值集
* @return 按钮权限视图对象列表
*/
private List<ButtonPermissionVo> extractDictItems(NodeExt.ChildNode childNode, Set<String> selectedSet) {
return Optional.ofNullable(childNode)
.map(NodeExt.ChildNode::getDict)
.orElse(List.of())
.stream()
.map(dict -> new ButtonPermissionVo(dict.getValue(), selectedSet.contains(dict.getValue())))
.toList();
} }
} }

View File

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

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