Compare commits

110 Commits

Author SHA1 Message Date
疯狂的狮子Li
015b406001 !591 发布 5.2.3 正式版
Merge pull request !591 from 疯狂的狮子Li/dev
2024-10-25 03:09:23 +00:00
疯狂的狮子Li
6c950c9569 🎀发布 5.2.3 正式版 2024-10-25 10:58:01 +08:00
疯狂的狮子Li
3ce3ffca05 update springboot 3.2.10 => 3.2.11
update redisson 3.36.0 => 3.37.0
update mapstruct-plus 1.4.4 => 1.4.5
update anyline 8.7.2-20240930 => 8.7.2-20241022
2024-10-25 10:32:52 +08:00
疯狂的狮子Li
d3ccc43d68 update 优化 postgres适配findInSet写法 提高查询效率 2024-10-24 14:47:58 +08:00
疯狂的狮子Li
4c96440686 fix 修复 设置流程变量 代码使用错误问题 2024-10-23 17:38:21 +08:00
疯狂的狮子Li
8d8d76364b fix 修复 xss过滤器 未过滤url参数问题 2024-10-21 13:59:39 +08:00
疯狂的狮子Li
82af98c205 fix 修复 代码书写错误 2024-10-21 13:38:09 +08:00
疯狂的狮子Li
fdbe8c2395 update 优化 过滤器初始化写法 2024-10-20 11:43:39 +08:00
疯狂的狮子Li
59715b1e02 fix 修复 及其特殊场景下获取 StopWatch 为null问题 2024-10-20 00:23:33 +08:00
疯狂的狮子Li
f2ec530065 fix 修复 字典同步 数据异常问题 2024-10-19 14:50:58 +08:00
秋辞未寒
ac89cb46f5 fix 修复重新生成租户ID未生效的问题 2024-10-17 22:38:12 +08:00
疯狂的狮子Li
abfa995a32 update 优化 监听器兼容所有demo案例 2024-10-17 18:12:26 +08:00
疯狂的狮子Li
51cfbef887 update 优化 操作日志记录DELETE请求参数 2024-10-17 17:34:33 +08:00
疯狂的狮子Li
7171ed1508 update 优化 snailjob客户端ip配置说明 2024-10-17 17:25:02 +08:00
AprilWind
6727f3e6a4 fix 修复oss上传10秒超时,设置默认时间一分钟 2024-10-16 15:28:38 +08:00
AprilWind
81006c758a docs 自行开启云存储访问控制ACl策略注释 2024-10-16 10:47:26 +08:00
AprilWind
01025c4024 update 更新AWS SDK版本 2024-10-16 10:39:58 +08:00
AprilWind
517277132e fix 修复腾讯云oss不支持高危权限设置ACL 2024-10-16 10:36:43 +08:00
AprilWind
dc5c92ee2d fix 修复同步云厂商要求明确配置访问样式(路径样式访问) 2024-10-16 10:33:11 +08:00
疯狂的狮子Li
57e142e160 fix 修复 特性情况下自定义验证异常处理器报null问题 2024-10-15 17:35:51 +08:00
疯狂的狮子Li
c563afed59 fix 修复 EncryptorManager 缓存失效问题导致的内存膨胀 2024-10-14 00:38:07 +08:00
疯狂的狮子Li
556cf87f0e update 优化 补全 pg 数据类型 2024-10-12 16:23:17 +08:00
疯狂的狮子Li
e0a00cfe98 update 优化 补全 @Override 注解 2024-10-08 16:30:04 +08:00
疯狂的狮子Li
d60774a7b8 reset 回滚错误提交 2024-10-08 16:10:27 +08:00
疯狂的狮子Li
e3d40b75cb fix 修复 同一个用户不同token连接不同服务导致发送不到问题(改为全局发送) 2024-10-08 14:43:59 +08:00
疯狂的狮子Li
eb4479e940 fix 修复 同步字典存储是未忽略租户 2024-10-01 10:52:07 +08:00
疯狂的狮子Li
7e6d0a5c64 update anyline 20240808 => 20240930 2024-09-30 17:56:04 +08:00
疯狂的狮子Li
88dd4165cb update sms4j 3.3.2 => 3.3.3 2024-09-29 17:41:36 +08:00
疯狂的狮子Li
e96118c574 fix 修复 部分web异常被CryptoFilter截胡问题 2024-09-29 17:41:16 +08:00
疯狂的狮子Li
39c4e5fe55 !589 fix postgres数据库脚本租户管理名称错误
Merge pull request !589 from LionZzzi/fix
2024-09-25 02:14:52 +00:00
Zyyi
46141dc114 fix(postgres数据库脚本): 变更租户管理目录的主菜单为租户管理 2024-09-25 10:00:33 +08:00
疯狂的狮子Li
e3a25f2425 fix 修复 代码生成器 postgres 数据库主键类型映射错误问题 2024-09-24 18:17:49 +08:00
疯狂的狮子Li
64289c16f3 update 优化 适配mp新版本 方法名改动 2024-09-20 15:43:56 +08:00
疯狂的狮子Li
6b05ddb576 update 优化 redis操作 如果无法忽略租户id则全局处理 2024-09-19 23:13:00 +08:00
疯狂的狮子Li
88ee252fce update springboot 3.2.9 => 3.2.10 2024-09-19 18:09:03 +08:00
疯狂的狮子Li
2add7291ab update 优化 sse 异常单独处理 避免出现异常报错问题 2024-09-19 17:53:41 +08:00
疯狂的狮子Li
501be029c6 update 优化 sse 异常单独处理 避免出现异常报错问题 2024-09-19 17:40:42 +08:00
疯狂的狮子Li
d86652dee1 update easyexcel 4.0.2 => 4.0.3
update redisson 3.34.1 => 3.36.0
2024-09-19 13:55:46 +08:00
疯狂的狮子Li
272ca613ee update mybatis-plus 3.5.7 => 3.5.8 代码适配更改 2024-09-19 13:45:57 +08:00
疯狂的狮子Li
74af811a3b update 优化 删除掉有问题的方法(使用RedisUtils) 2024-09-18 18:07:28 +08:00
疯狂的狮子Li
fc72b67090 update 优化 全局开启xss过滤 提高安全性 与cloud版本保持一致 2024-09-13 18:02:44 +08:00
疯狂的狮子Li
e33f76d710 update 优化 去除返回前端的异常信息里包含html标签问题 2024-09-13 17:42:21 +08:00
AprilWind
1c3d594947 docs 查询表名列表增加注释 2024-09-13 15:00:16 +08:00
疯狂的狮子Li
64d9b27310 update 优化 判断当前会话是否已经登录 2024-09-13 14:41:01 +08:00
疯狂的狮子Li
f3f3593cfe update 优化 删除不应该set的属性 2024-09-13 09:38:56 +08:00
疯狂的狮子Li
e5e8e3ce7c update 优化 租户状态更改接口严谨性 2024-09-12 16:58:47 +08:00
疯狂的狮子Li
a7fd7ba72c fix 修复 临时处理 scala库版本漏洞问题 2024-09-11 16:00:39 +08:00
疯狂的狮子Li
020f090f4a update 优化 删除okhttp无用版本限制(spring已经限制过了) 2024-09-11 15:59:54 +08:00
AprilWind
5e3231d59b !582 优化admin配置文件读取
* update 优化admin监控配置读取名称
* update 优化admin监控配置读取
2024-09-11 04:02:16 +00:00
疯狂的狮子Li
b522bc015d update 优化 操作日志查询代码 2024-09-11 10:23:16 +08:00
疯狂的狮子Li
6e64fd7fd7 !585 修复工作流分页查询不兼容sql server的问题
Merge pull request !585 from 赖小麦/fix_workflow_page_query_error_to_sqlserver
2024-09-11 01:27:20 +00:00
sushuai
8b44f5cdbc 修复:工作流中(我的待办、我的已办、我的抄送默认)的分页查询语句不兼容sqlserver的问题 2024-09-10 12:05:08 +08:00
AprilWind
f9b7d955aa add 新增TreeUtil获取节点列表中所有节点的叶子节点 2024-09-10 10:15:01 +08:00
疯狂的狮子Li
6ea2a2fc51 update 优化 统一sql文件命名方式 2024-09-09 11:37:02 +08:00
疯狂的狮子Li
760c8d7200 update 优化 提供生产环境默认组配置 2024-09-09 11:30:40 +08:00
AprilWind
32ad28c3dc update 优化通过角色ID查询用户逻辑 2024-09-06 14:24:06 +08:00
AprilWind
6886e9fd5b update 优化查询用户时多余重复判断以及去重 2024-09-06 14:14:15 +08:00
疯狂的狮子Li
f20130d3db fix 修复 commons-io 依赖冲突问题 2024-09-05 18:50:28 +08:00
疯狂的狮子Li
df9cc881f1 update 优化 连接SSE token过期导致的 Servlet异常 2024-09-05 18:50:00 +08:00
疯狂的狮子Li
4044988afa update 优化 代码生成菜单id匹配写法 2024-09-04 16:16:53 +08:00
疯狂的狮子Li
d3360e81b9 update sa-token 1.38.0 => 1.39.0 2024-09-03 13:52:50 +08:00
疯狂的狮子Li
df070b7d78 update 调整 xml 格式 2024-09-02 14:16:40 +08:00
疯狂的狮子Li
83dd98faf3 fix 修复 开启子部门 父部门未关联开启问题 2024-09-02 14:12:02 +08:00
疯狂的狮子Li
37f89f560f update 优化 更新sql关键字 2024-09-02 13:06:31 +08:00
疯狂的狮子Li
918ed0d6d0 update 优化 删除多余的引号 2024-08-31 23:30:50 +08:00
疯狂的狮子Li
a3c9edde78 fix 修复 升级依赖导致的依赖冲突 2024-08-29 20:53:39 +08:00
疯狂的狮子Li
cac0a4cd16 update 优化 RegexUtils#extractFromString 方法未匹配返回null不返回默认值问题 2024-08-29 14:12:07 +08:00
疯狂的狮子Li
581b6e03d5 update 优化 oss上传直接从请求头获取文件类型 2024-08-29 10:49:41 +08:00
疯狂的狮子Li
801cc584e5 update 优化 代码生成表名判断 使用开头判断避免误判 2024-08-29 10:41:45 +08:00
疯狂的狮子Li
b82ff8617e add 增加 同步租户字典功能 2024-08-26 17:56:19 +08:00
疯狂的狮子Li
c87016c1af update 优化 excel导入 适配异常结构 2024-08-26 15:38:48 +08:00
疯狂的狮子Li
098d3347a0 !577 发布 5.2.2 正式版 安全性提升
Merge pull request !577 from 疯狂的狮子Li/dev
2024-08-26 03:43:59 +00:00
疯狂的狮子Li
463048e017 😴发布 5.2.2 正式版 安全性提升 2024-08-26 11:39:22 +08:00
gssong
79aee1d312 add 添加按照部门id,角色id查询用户 2024-08-24 11:16:39 +08:00
疯狂的狮子Li
eb038e91dd update springboot 3.2.8 => 3.2.9 2024-08-23 10:52:53 +08:00
疯狂的狮子Li
308c22f922 fix 修复 三方登录构建去除无用代码 2024-08-23 10:30:29 +08:00
疯狂的狮子Li
457e59e61c fix 修复 多线程对同一个session发送ws消息报错问题 2024-08-22 17:53:55 +08:00
疯狂的狮子Li
a964ccbd10 update snailjob 1.1.1 => 1.1.2
update mapstruct-plus 1.4.3 => 1.4.4
2024-08-22 11:19:43 +08:00
疯狂的狮子Li
0af532f4f1 update 优化 去除日志部署环境判断 通过日志级别控制 2024-08-22 11:19:13 +08:00
疯狂的狮子Li
4743eb1d3b update 优化 忽略租户与忽略数据权限支持嵌套使用 2024-08-21 13:54:40 +08:00
疯狂的狮子Li
23aafe1cfe update 优化 忽略租户与忽略数据权限支持嵌套使用 2024-08-21 13:54:01 +08:00
疯狂的狮子Li
3f2499feac update 优化 租户相关controller 增加租户开关配置控制是否注册 2024-08-19 09:14:50 +08:00
疯狂的狮子Li
fb97649151 remove 移除 alibaba ttl 与线程池搭配有问题(可传递但无法清除与更新) 2024-08-17 10:30:03 +08:00
疯狂的狮子Li
b33b645ef0 remove 移除 alibaba ttl 与线程池搭配有问题(可传递但无法清除与更新) 2024-08-17 10:29:33 +08:00
疯狂的狮子Li
9318f182b0 reset 回滚 修改spring源码上下文持有者(存在数据未清理内存泄漏问题) 2024-08-17 10:10:51 +08:00
疯狂的狮子Li
facbb7f28f update 优化 个人中心编辑 忽略数据权限 2024-08-15 20:28:52 +08:00
疯狂的狮子Li
4de45ce170 update 优化 兼容部分用户不想给用户分配角色与部门的场景 2024-08-15 19:52:03 +08:00
疯狂的狮子Li
96d57bd263 update 优化 租户套餐重名校验 2024-08-15 19:34:02 +08:00
疯狂的狮子Li
bb4587fe05 update 优化 部门下存在岗位不允许删除 2024-08-15 19:29:04 +08:00
疯狂的狮子Li
19c83f02aa update 优化 角色编辑状态未校验问题 2024-08-15 19:14:51 +08:00
疯狂的狮子Li
0e1fcbfe9c update 优化 用户脱敏增加编辑权限标识符 2024-08-15 18:52:01 +08:00
疯狂的狮子Li
eda882433a update 优化 删除报警注释 2024-08-15 17:45:06 +08:00
疯狂的狮子Li
e6847605cc update 优化 删除无用注释 2024-08-15 17:43:57 +08:00
疯狂的狮子Li
5bdffdb368 update 优化 代码生成器 自动适配oss翻译 2024-08-15 14:10:14 +08:00
疯狂的狮子Li
0ad52b18b8 update 优化 代码生成器 自动适配oss翻译 2024-08-15 14:09:20 +08:00
疯狂的狮子Li
77f44574c0 update 优化 修改spring源码上下文持有者 支持线程切换传递上下文数据 支持一切异步获取用户信息等操作 2024-08-15 11:53:15 +08:00
疯狂的狮子Li
ca06a2311d update hutool 5.8.29 => 5.8.31 解决hutool不兼容jakarta问题 2024-08-13 13:32:34 +08:00
AprilWind
b8d9af65e2 update 增加sse注释说明 2024-08-09 17:24:44 +08:00
AprilWind
bc2b4876b6 fix 修复代码生成集合类型工具判断 2024-08-09 14:08:19 +08:00
疯狂的狮子Li
23b70ca0be update anyline 8.7.2-20240808 2024-08-09 11:54:47 +08:00
疯狂的狮子Li
44d776a76f update sms4j 3.2.1 => 3.3.2 2024-08-09 10:38:43 +08:00
疯狂的狮子Li
f03c00b2c1 reset 回滚 sms4j 版本升级(有问题) 2024-08-07 22:27:05 +08:00
疯狂的狮子Li
7f60ba9888 update redisson 3.33.0 => 3.34.1
update mapstruct-plus 1.3.6 => 1.4.3
update sms4j 3.2.1 => 3.3.1
update lombok 1.18.32 => 1.18.34
2024-08-07 11:22:14 +08:00
疯狂的狮子Li
31569646b0 fix 修复 依赖漏洞 限制部分依赖版本 2024-08-07 11:20:34 +08:00
疯狂的狮子Li
3fc37d6362 update easyexcel 3.3.4 => 4.0.2 2024-08-07 11:19:16 +08:00
疯狂的狮子Li
08d4493994 update 优化 bug 模板 2024-07-15 15:19:22 +08:00
疯狂的狮子Li
367d739e2d Merge remote-tracking branch 'origin/5.X' into 5.X 2024-07-09 16:38:43 +08:00
疯狂的狮子Li
d6688a367d !562 ♥️发布 5.2.1 正式版本
Merge pull request !562 from 疯狂的狮子Li/dev
2024-07-09 02:42:40 +00:00
疯狂的狮子Li
0b331796e2 !551 ♥️发布 5.2.0 正式版本
Merge pull request !551 from 疯狂的狮子Li/dev
2024-06-20 02:10:15 +00:00
疯狂的狮子Li
456620b638 !549 ♥️发布 5.2.0-BETA2 公测版本
Merge pull request !549 from 疯狂的狮子Li/dev
2024-06-06 03:13:46 +00:00
102 changed files with 881 additions and 1750 deletions

View File

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

View File

@@ -1,8 +1,8 @@
<component name="ProjectRunConfigurationManager">
<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="演示机">
<deployment type="dockerfile">
<settings>
<option name="imageTag" value="ruoyi/ruoyi-server:5.2.1" />
<option name="imageTag" value="ruoyi/ruoyi-server:5.2.3" />
<option name="buildOnly" value="true" />
<option name="sourceFilePath" value="ruoyi-admin/Dockerfile" />
</settings>

View File

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

View File

@@ -9,7 +9,7 @@
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/master/LICENSE)
[![使用IntelliJ IDEA开发维护](https://img.shields.io/badge/IntelliJ%20IDEA-提供支持-blue.svg)](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
<br>
[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.2.1-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
[![RuoYi-Vue-Plus](https://img.shields.io/badge/RuoYi_Vue_Plus-5.2.3-success.svg)](https://gitee.com/dromara/RuoYi-Vue-Plus)
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.2-blue.svg)]()
[![JDK-17](https://img.shields.io/badge/JDK-17-green.svg)]()
[![JDK-21](https://img.shields.io/badge/JDK-21-green.svg)]()

72
pom.xml
View File

@@ -13,46 +13,42 @@
<description>RuoYi-Vue-Plus多租户管理系统</description>
<properties>
<revision>5.2.1</revision>
<spring-boot.version>3.2.8</spring-boot.version>
<revision>5.2.3</revision>
<spring-boot.version>3.2.11</spring-boot.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version>
<mybatis.version>3.5.16</mybatis.version>
<springdoc.version>2.6.0</springdoc.version>
<therapi-javadoc.version>0.15.0</therapi-javadoc.version>
<poi.version>5.2.3</poi.version>
<easyexcel.version>3.3.4</easyexcel.version>
<easyexcel.version>4.0.3</easyexcel.version>
<velocity.version>2.3</velocity.version>
<satoken.version>1.38.0</satoken.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<satoken.version>1.39.0</satoken.version>
<mybatis-plus.version>3.5.8</mybatis-plus.version>
<p6spy.version>3.9.1</p6spy.version>
<hutool.version>5.8.29</hutool.version>
<okhttp.version>4.10.0</okhttp.version>
<hutool.version>5.8.31</hutool.version>
<spring-boot-admin.version>3.2.3</spring-boot-admin.version>
<redisson.version>3.33.0</redisson.version>
<redisson.version>3.37.0</redisson.version>
<lock4j.version>2.2.7</lock4j.version>
<dynamic-ds.version>4.3.1</dynamic-ds.version>
<alibaba-ttl.version>2.14.4</alibaba-ttl.version>
<snailjob.version>1.1.1</snailjob.version>
<mapstruct-plus.version>1.3.6</mapstruct-plus.version>
<snailjob.version>1.1.2</snailjob.version>
<mapstruct-plus.version>1.4.5</mapstruct-plus.version>
<mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
<lombok.version>1.18.32</lombok.version>
<lombok.version>1.18.34</lombok.version>
<bouncycastle.version>1.76</bouncycastle.version>
<justauth.version>1.16.6</justauth.version>
<!-- 离线IP地址定位库 -->
<ip2region.version>2.7.0</ip2region.version>
<undertow.version>2.3.15.Final</undertow.version>
<!-- OSS 配置 -->
<aws.sdk.version>2.25.15</aws.sdk.version>
<aws.crt.version>0.29.13</aws.crt.version>
<aws.sdk.version>2.28.22</aws.sdk.version>
<aws.crt.version>0.31.3</aws.crt.version>
<!-- SMS 配置 -->
<sms4j.version>3.2.1</sms4j.version>
<sms4j.version>3.3.3</sms4j.version>
<!-- 限制框架中的fastjson版本 -->
<fastjson.version>1.2.83</fastjson.version>
<!-- 面向运行时的D-ORM依赖 -->
<anyline.version>8.7.2-20240728</anyline.version>
<anyline.version>8.7.2-20241022</anyline.version>
<!--工作流配置-->
<flowable.version>7.0.1</flowable.version>
@@ -71,6 +67,8 @@
<!-- 环境标识,需要与配置文件的名称相对应 -->
<profiles.active>local</profiles.active>
<logging.level>info</logging.level>
<monitor.username>ruoyi</monitor.username>
<monitor.password>123456</monitor.password>
</properties>
</profile>
<profile>
@@ -79,6 +77,8 @@
<!-- 环境标识,需要与配置文件的名称相对应 -->
<profiles.active>dev</profiles.active>
<logging.level>info</logging.level>
<monitor.username>ruoyi</monitor.username>
<monitor.password>123456</monitor.password>
</properties>
<activation>
<!-- 默认环境 -->
@@ -90,6 +90,8 @@
<properties>
<profiles.active>prod</profiles.active>
<logging.level>warn</logging.level>
<monitor.username>ruoyi</monitor.username>
<monitor.password>123456</monitor.password>
</properties>
</profile>
</profiles>
@@ -158,26 +160,10 @@
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>${easyexcel.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- velocity代码生成使用模板 -->
@@ -243,12 +229,6 @@
<version>${p6spy.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<!-- AWS SDK for Java 2.x -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
@@ -310,12 +290,6 @@
<version>${snailjob.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>${alibaba-ttl.version}</version>
</dependency>
<!-- 加密包引入 -->
<dependency>
<groupId>org.bouncycastle</groupId>
@@ -336,6 +310,12 @@
<version>${ip2region.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.15.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>

View File

@@ -156,9 +156,11 @@ public class SysLoginService {
loginUser.setUserType(user.getUserType());
loginUser.setMenuPermission(permissionService.getMenuPermission(user.getUserId()));
loginUser.setRolePermission(permissionService.getRolePermission(user.getUserId()));
Opt<SysDeptVo> deptOpt = Opt.of(user.getDeptId()).map(deptService::selectDeptById);
loginUser.setDeptName(deptOpt.map(SysDeptVo::getDeptName).orElse(StringUtils.EMPTY));
loginUser.setDeptCategory(deptOpt.map(SysDeptVo::getDeptCategory).orElse(StringUtils.EMPTY));
if (ObjectUtil.isNotNull(user.getDeptId())) {
Opt<SysDeptVo> deptOpt = Opt.of(user.getDeptId()).map(deptService::selectDeptById);
loginUser.setDeptName(deptOpt.map(SysDeptVo::getDeptName).orElse(StringUtils.EMPTY));
loginUser.setDeptCategory(deptOpt.map(SysDeptVo::getDeptCategory).orElse(StringUtils.EMPTY));
}
List<SysRoleVo> roles = roleService.selectRolesByUserId(user.getUserId());
loginUser.setRoles(BeanUtil.copyToList(roles, RoleDTO.class));
return loginUser;

View File

@@ -8,8 +8,8 @@ spring.boot.admin.client:
metadata:
username: ${spring.boot.admin.client.username}
userpassword: ${spring.boot.admin.client.password}
username: ruoyi
password: 123456
username: @monitor.username@
password: @monitor.password@
--- # snail-job 配置
snail-job:
@@ -25,6 +25,8 @@ snail-job:
namespace: ${spring.profiles.active}
# 随主应用端口飘逸
port: 2${server.port}
# 客户端ip指定
host:
--- # 数据源配置
spring:

View File

@@ -11,8 +11,8 @@ spring.boot.admin.client:
metadata:
username: ${spring.boot.admin.client.username}
userpassword: ${spring.boot.admin.client.password}
username: ruoyi
password: 123456
username: @monitor.username@
password: @monitor.password@
--- # snail-job 配置
snail-job:
@@ -28,6 +28,8 @@ snail-job:
namespace: ${spring.profiles.active}
# 随主应用端口飘逸
port: 2${server.port}
# 客户端ip指定
host:
--- # 数据源配置
spring:

View File

@@ -223,9 +223,10 @@ xss:
# 过滤开关
enabled: true
# 排除链接(多个用逗号分隔)
excludes: /system/notice
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*
excludeUrls:
- /system/notice
- /workflow/model/save
- /workflow/model/editModelXml
# 全局线程池相关配置
# 如使用JDK21请直接使用虚拟线程 不要开启此配置

View File

@@ -14,7 +14,7 @@
</description>
<properties>
<revision>5.2.1</revision>
<revision>5.2.3</revision>
</properties>
<dependencyManagement>

View File

@@ -94,11 +94,6 @@
<artifactId>ip2region</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,62 @@
package org.dromara.common.core.exception;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.io.Serial;
/**
* sse 特制异常
*
* @author LionLi
*/
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public final class SseException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
/**
* 错误码
*/
private Integer code;
/**
* 错误提示
*/
private String message;
/**
* 错误明细,内部调试错误
*/
private String detailMessage;
public SseException(String message) {
this.message = message;
}
public SseException(String message, Integer code) {
this.message = message;
this.code = code;
}
@Override
public String getMessage() {
return message;
}
public SseException setMessage(String message) {
this.message = message;
return this;
}
public SseException setDetailMessage(String detailMessage) {
this.detailMessage = detailMessage;
return this;
}
}

View File

@@ -66,4 +66,20 @@ public interface UserService {
* @return 用户ids
*/
List<Long> selectUserIdsByRoleIds(List<Long> roleIds);
/**
* 通过角色ID查询用户
*
* @param roleIds 角色ids
* @return 用户
*/
List<UserDTO> selectUsersByRoleIds(List<Long> roleIds);
/**
* 通过部门ID查询用户
*
* @param deptIds 部门ids
* @return 用户
*/
List<UserDTO> selectUsersByDeptIds(List<Long> deptIds);
}

View File

@@ -5,11 +5,13 @@ import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.lang.tree.TreeUtil;
import cn.hutool.core.lang.tree.parser.NodeParser;
import org.dromara.common.core.utils.reflect.ReflectUtils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.reflect.ReflectUtils;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 扩展 hutool TreeUtil 封装系统树构建
@@ -24,12 +26,54 @@ public class TreeBuildUtils extends TreeUtil {
*/
public static final TreeNodeConfig DEFAULT_CONFIG = TreeNodeConfig.DEFAULT_CONFIG.setNameKey("label");
/**
* 构建树形结构
*
* @param <T> 输入节点的类型
* @param <K> 节点ID的类型
* @param list 节点列表,其中包含了要构建树形结构的所有节点
* @param nodeParser 解析器,用于将输入节点转换为树节点
* @return 构建好的树形结构列表
*/
public static <T, K> List<Tree<K>> build(List<T> list, NodeParser<T, K> nodeParser) {
if (CollUtil.isEmpty(list)) {
return null;
return CollUtil.newArrayList();
}
K k = ReflectUtils.invokeGetter(list.get(0), "parentId");
return TreeUtil.build(list, k, DEFAULT_CONFIG, nodeParser);
}
/**
* 获取节点列表中所有节点的叶子节点
*
* @param <K> 节点ID的类型
* @param nodes 节点列表
* @return 包含所有叶子节点的列表
*/
public static <K> List<Tree<K>> getLeafNodes(List<Tree<K>> nodes) {
if (CollUtil.isEmpty(nodes)) {
return CollUtil.newArrayList();
}
return nodes.stream()
.flatMap(TreeBuildUtils::extractLeafNodes)
.collect(Collectors.toList());
}
/**
* 获取指定节点下的所有叶子节点
*
* @param <K> 节点ID的类型
* @param node 要查找叶子节点的根节点
* @return 包含所有叶子节点的列表
*/
private static <K> Stream<Tree<K>> extractLeafNodes(Tree<K> node) {
if (!node.hasChild()) {
return Stream.of(node);
} else {
// 递归调用,获取所有子节点的叶子节点
return node.getChildren().stream()
.flatMap(TreeBuildUtils::extractLeafNodes);
}
}
}

View File

@@ -21,7 +21,8 @@ public final class RegexUtils extends ReUtil {
*/
public static String extractFromString(String input, String regex, String defaultInput) {
try {
return ReUtil.get(regex, input, 1);
String str = ReUtil.get(regex, input, 1);
return str == null ? defaultInput : str;
} catch (Exception e) {
return defaultInput;
}

View File

@@ -15,7 +15,7 @@ public class SqlUtil {
/**
* 定义常用的 sql关键字
*/
public static final String SQL_REGEX = "select |insert |delete |update |drop |count |exec |chr |mid |master |truncate |char |and |declare ";
public static String SQL_REGEX = "and |extractvalue|updatexml|sleep|exec |insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |or |union |like |+|/*|user()";
/**
* 仅支持字母、数字、下划线、空格、逗号、小数点(支持多个字段排序)

View File

@@ -17,7 +17,10 @@ import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.util.ClassUtils;
import java.lang.reflect.Field;
import java.util.*;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@@ -34,7 +37,7 @@ public class EncryptorManager {
/**
* 缓存加密器
*/
Map<EncryptContext, IEncryptor> encryptorMap = new ConcurrentHashMap<>();
Map<Integer, IEncryptor> encryptorMap = new ConcurrentHashMap<>();
/**
* 类加密字段缓存
@@ -67,11 +70,12 @@ public class EncryptorManager {
* @param encryptContext 加密执行者需要的相关配置参数
*/
public IEncryptor registAndGetEncryptor(EncryptContext encryptContext) {
if (encryptorMap.containsKey(encryptContext)) {
return encryptorMap.get(encryptContext);
int key = encryptContext.hashCode();
if (encryptorMap.containsKey(key)) {
return encryptorMap.get(key);
}
IEncryptor encryptor = ReflectUtil.newInstance(encryptContext.getAlgorithm().getClazz(), encryptContext);
encryptorMap.put(encryptContext, encryptor);
encryptorMap.put(key, encryptor);
return encryptor;
}
@@ -81,7 +85,7 @@ public class EncryptorManager {
* @param encryptContext 加密执行者需要的相关配置参数
*/
public void removeEncryptor(EncryptContext encryptContext) {
this.encryptorMap.remove(encryptContext);
this.encryptorMap.remove(encryptContext.hashCode());
}
/**

View File

@@ -99,7 +99,7 @@ public class CryptoFilter implements Filter {
}
}
} catch (Exception e) {
throw new RuntimeException(e);
return null;
}
return null;
}

View File

@@ -159,8 +159,7 @@ public class LogAspect {
private void setRequestValue(JoinPoint joinPoint, OperLogEvent operLog, String[] excludeParamNames) throws Exception {
Map<String, String> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
String requestMethod = operLog.getRequestMethod();
if (MapUtil.isEmpty(paramsMap)
&& HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
if (MapUtil.isEmpty(paramsMap) && StringUtils.equalsAny(requestMethod, HttpMethod.PUT.name(), HttpMethod.POST.name(), HttpMethod.DELETE.name())) {
String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
operLog.setOperParam(StringUtils.substring(params, 0, 2000));
} else {

View File

@@ -1,7 +1,7 @@
package org.dromara.common.mail.config;
import cn.hutool.extra.mail.MailAccount;
import org.dromara.common.mail.config.properties.MailProperties;
import org.dromara.common.mail.utils.MailAccount;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

View File

@@ -1,46 +0,0 @@
package org.dromara.common.mail.utils;
import cn.hutool.core.io.IORuntimeException;
/**
* 全局邮件帐户,依赖于邮件配置文件{@link MailAccount#MAIL_SETTING_PATHS}
*
* @author looly
*/
public enum GlobalMailAccount {
INSTANCE;
private final MailAccount mailAccount;
/**
* 构造
*/
GlobalMailAccount() {
mailAccount = createDefaultAccount();
}
/**
* 获得邮件帐户
*
* @return 邮件帐户
*/
public MailAccount getAccount() {
return this.mailAccount;
}
/**
* 创建默认帐户
*
* @return MailAccount
*/
private MailAccount createDefaultAccount() {
for (String mailSettingPath : MailAccount.MAIL_SETTING_PATHS) {
try {
return new MailAccount(mailSettingPath);
} catch (IORuntimeException ignore) {
//ignore
}
}
return null;
}
}

View File

@@ -1,108 +0,0 @@
package org.dromara.common.mail.utils;
import cn.hutool.core.util.ArrayUtil;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeUtility;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 邮件内部工具类
*
* @author looly
* @since 3.2.3
*/
public class InternalMailUtil {
/**
* 将多个字符串邮件地址转为{@link InternetAddress}列表<br>
* 单个字符串地址可以是多个地址合并的字符串
*
* @param addrStrs 地址数组
* @param charset 编码(主要用于中文用户名的编码)
* @return 地址数组
* @since 4.0.3
*/
public static InternetAddress[] parseAddressFromStrs(String[] addrStrs, Charset charset) {
final List<InternetAddress> resultList = new ArrayList<>(addrStrs.length);
InternetAddress[] addrs;
for (String addrStr : addrStrs) {
addrs = parseAddress(addrStr, charset);
if (ArrayUtil.isNotEmpty(addrs)) {
Collections.addAll(resultList, addrs);
}
}
return resultList.toArray(new InternetAddress[0]);
}
/**
* 解析第一个地址
*
* @param address 地址字符串
* @param charset 编码,{@code null}表示使用系统属性定义的编码或系统编码
* @return 地址列表
*/
public static InternetAddress parseFirstAddress(String address, Charset charset) {
final InternetAddress[] internetAddresses = parseAddress(address, charset);
if (ArrayUtil.isEmpty(internetAddresses)) {
try {
return new InternetAddress(address);
} catch (AddressException e) {
throw new MailException(e);
}
}
return internetAddresses[0];
}
/**
* 将一个地址字符串解析为多个地址<br>
* 地址间使用" "、","、";"分隔
*
* @param address 地址字符串
* @param charset 编码,{@code null}表示使用系统属性定义的编码或系统编码
* @return 地址列表
*/
public static InternetAddress[] parseAddress(String address, Charset charset) {
InternetAddress[] addresses;
try {
addresses = InternetAddress.parse(address);
} catch (AddressException e) {
throw new MailException(e);
}
//编码用户名
if (ArrayUtil.isNotEmpty(addresses)) {
final String charsetStr = null == charset ? null : charset.name();
for (InternetAddress internetAddress : addresses) {
try {
internetAddress.setPersonal(internetAddress.getPersonal(), charsetStr);
} catch (UnsupportedEncodingException e) {
throw new MailException(e);
}
}
}
return addresses;
}
/**
* 编码中文字符<br>
* 编码失败返回原字符串
*
* @param text 被编码的文本
* @param charset 编码
* @return 编码后的结果
*/
public static String encodeText(String text, Charset charset) {
try {
return MimeUtility.encodeText(text, charset.name(), null);
} catch (UnsupportedEncodingException e) {
// ignore
}
return text;
}
}

View File

@@ -1,483 +0,0 @@
package org.dromara.common.mail.utils;
import cn.hutool.core.builder.Builder;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import jakarta.activation.DataHandler;
import jakarta.activation.DataSource;
import jakarta.activation.FileDataSource;
import jakarta.activation.FileTypeMap;
import jakarta.mail.*;
import jakarta.mail.internet.MimeBodyPart;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;
import jakarta.mail.internet.MimeUtility;
import jakarta.mail.util.ByteArrayDataSource;
import java.io.*;
import java.nio.charset.Charset;
import java.util.Date;
/**
* 邮件发送客户端
*
* @author looly
* @since 3.2.0
*/
public class Mail implements Builder<MimeMessage> {
@Serial
private static final long serialVersionUID = 1L;
/**
* 邮箱帐户信息以及一些客户端配置信息
*/
private final MailAccount mailAccount;
/**
* 收件人列表
*/
private String[] tos;
/**
* 抄送人列表carbon copy
*/
private String[] ccs;
/**
* 密送人列表blind carbon copy
*/
private String[] bccs;
/**
* 回复地址(reply-to)
*/
private String[] reply;
/**
* 标题
*/
private String title;
/**
* 内容
*/
private String content;
/**
* 是否为HTML
*/
private boolean isHtml;
/**
* 正文、附件和图片的混合部分
*/
private final Multipart multipart = new MimeMultipart();
/**
* 是否使用全局会话默认为false
*/
private boolean useGlobalSession = false;
/**
* debug输出位置可以自定义debug日志
*/
private PrintStream debugOutput;
/**
* 创建邮件客户端
*
* @param mailAccount 邮件帐号
* @return Mail
*/
public static Mail create(MailAccount mailAccount) {
return new Mail(mailAccount);
}
/**
* 创建邮件客户端,使用全局邮件帐户
*
* @return Mail
*/
public static Mail create() {
return new Mail();
}
// --------------------------------------------------------------- Constructor start
/**
* 构造,使用全局邮件帐户
*/
public Mail() {
this(GlobalMailAccount.INSTANCE.getAccount());
}
/**
* 构造
*
* @param mailAccount 邮件帐户如果为null使用默认配置文件的全局邮件配置
*/
public Mail(MailAccount mailAccount) {
mailAccount = (null != mailAccount) ? mailAccount : GlobalMailAccount.INSTANCE.getAccount();
this.mailAccount = mailAccount.defaultIfEmpty();
}
// --------------------------------------------------------------- Constructor end
// --------------------------------------------------------------- Getters and Setters start
/**
* 设置收件人
*
* @param tos 收件人列表
* @return this
* @see #setTos(String...)
*/
public Mail to(String... tos) {
return setTos(tos);
}
/**
* 设置多个收件人
*
* @param tos 收件人列表
* @return this
*/
public Mail setTos(String... tos) {
this.tos = tos;
return this;
}
/**
* 设置多个抄送人carbon copy
*
* @param ccs 抄送人列表
* @return this
* @since 4.0.3
*/
public Mail setCcs(String... ccs) {
this.ccs = ccs;
return this;
}
/**
* 设置多个密送人blind carbon copy
*
* @param bccs 密送人列表
* @return this
* @since 4.0.3
*/
public Mail setBccs(String... bccs) {
this.bccs = bccs;
return this;
}
/**
* 设置多个回复地址(reply-to)
*
* @param reply 回复地址(reply-to)列表
* @return this
* @since 4.6.0
*/
public Mail setReply(String... reply) {
this.reply = reply;
return this;
}
/**
* 设置标题
*
* @param title 标题
* @return this
*/
public Mail setTitle(String title) {
this.title = title;
return this;
}
/**
* 设置正文<br>
* 正文可以是普通文本也可以是HTML默认普通文本可以通过调用{@link #setHtml(boolean)} 设置是否为HTML
*
* @param content 正文
* @return this
*/
public Mail setContent(String content) {
this.content = content;
return this;
}
/**
* 设置是否是HTML
*
* @param isHtml 是否为HTML
* @return this
*/
public Mail setHtml(boolean isHtml) {
this.isHtml = isHtml;
return this;
}
/**
* 设置正文
*
* @param content 正文内容
* @param isHtml 是否为HTML
* @return this
*/
public Mail setContent(String content, boolean isHtml) {
setContent(content);
return setHtml(isHtml);
}
/**
* 设置文件类型附件文件可以是图片文件此时自动设置cid正文中引用图片默认cid为文件名
*
* @param files 附件文件列表
* @return this
*/
public Mail setFiles(File... files) {
if (ArrayUtil.isEmpty(files)) {
return this;
}
final DataSource[] attachments = new DataSource[files.length];
for (int i = 0; i < files.length; i++) {
attachments[i] = new FileDataSource(files[i]);
}
return setAttachments(attachments);
}
/**
* 增加附件或图片,附件使用{@link DataSource} 形式表示,可以使用{@link FileDataSource}包装文件表示文件附件
*
* @param attachments 附件列表
* @return this
* @since 4.0.9
*/
public Mail setAttachments(DataSource... attachments) {
if (ArrayUtil.isNotEmpty(attachments)) {
final Charset charset = this.mailAccount.getCharset();
MimeBodyPart bodyPart;
String nameEncoded;
try {
for (DataSource attachment : attachments) {
bodyPart = new MimeBodyPart();
bodyPart.setDataHandler(new DataHandler(attachment));
nameEncoded = attachment.getName();
if (this.mailAccount.isEncodefilename()) {
nameEncoded = InternalMailUtil.encodeText(nameEncoded, charset);
}
// 普通附件文件名
bodyPart.setFileName(nameEncoded);
if (StrUtil.startWith(attachment.getContentType(), "image/")) {
// 图片附件,用于正文中引用图片
bodyPart.setContentID(nameEncoded);
}
this.multipart.addBodyPart(bodyPart);
}
} catch (MessagingException e) {
throw new MailException(e);
}
}
return this;
}
/**
* 增加图片,图片的键对应到邮件模板中的占位字符串,图片类型默认为"image/jpeg"
*
* @param cid 图片与占位符占位符格式为cid:${cid}
* @param imageStream 图片文件
* @return this
* @since 4.6.3
*/
public Mail addImage(String cid, InputStream imageStream) {
return addImage(cid, imageStream, null);
}
/**
* 增加图片,图片的键对应到邮件模板中的占位字符串
*
* @param cid 图片与占位符占位符格式为cid:${cid}
* @param imageStream 图片流,不关闭
* @param contentType 图片类型null赋值默认的"image/jpeg"
* @return this
* @since 4.6.3
*/
public Mail addImage(String cid, InputStream imageStream, String contentType) {
ByteArrayDataSource imgSource;
try {
imgSource = new ByteArrayDataSource(imageStream, ObjectUtil.defaultIfNull(contentType, "image/jpeg"));
} catch (IOException e) {
throw new IORuntimeException(e);
}
imgSource.setName(cid);
return setAttachments(imgSource);
}
/**
* 增加图片,图片的键对应到邮件模板中的占位字符串
*
* @param cid 图片与占位符占位符格式为cid:${cid}
* @param imageFile 图片文件
* @return this
* @since 4.6.3
*/
public Mail addImage(String cid, File imageFile) {
InputStream in = null;
try {
in = FileUtil.getInputStream(imageFile);
return addImage(cid, in, FileTypeMap.getDefaultFileTypeMap().getContentType(imageFile));
} finally {
IoUtil.close(in);
}
}
/**
* 设置字符集编码
*
* @param charset 字符集编码
* @return this
* @see MailAccount#setCharset(Charset)
*/
public Mail setCharset(Charset charset) {
this.mailAccount.setCharset(charset);
return this;
}
/**
* 设置是否使用全局会话默认为true
*
* @param isUseGlobalSession 是否使用全局会话默认为true
* @return this
* @since 4.0.2
*/
public Mail setUseGlobalSession(boolean isUseGlobalSession) {
this.useGlobalSession = isUseGlobalSession;
return this;
}
/**
* 设置debug输出位置可以自定义debug日志
*
* @param debugOutput debug输出位置
* @return this
* @since 5.5.6
*/
public Mail setDebugOutput(PrintStream debugOutput) {
this.debugOutput = debugOutput;
return this;
}
// --------------------------------------------------------------- Getters and Setters end
@Override
public MimeMessage build() {
try {
return buildMsg();
} catch (MessagingException e) {
throw new MailException(e);
}
}
/**
* 发送
*
* @return message-id
* @throws MailException 邮件发送异常
*/
public String send() throws MailException {
try {
return doSend();
} catch (MessagingException e) {
if (e instanceof SendFailedException) {
// 当地址无效时,显示更加详细的无效地址信息
final Address[] invalidAddresses = ((SendFailedException) e).getInvalidAddresses();
final String msg = StrUtil.format("Invalid Addresses: {}", ArrayUtil.toString(invalidAddresses));
throw new MailException(msg, e);
}
throw new MailException(e);
}
}
// --------------------------------------------------------------- Private method start
/**
* 执行发送
*
* @return message-id
* @throws MessagingException 发送异常
*/
private String doSend() throws MessagingException {
final MimeMessage mimeMessage = buildMsg();
Transport.send(mimeMessage);
return mimeMessage.getMessageID();
}
/**
* 构建消息
*
* @return {@link MimeMessage}消息
* @throws MessagingException 消息异常
*/
private MimeMessage buildMsg() throws MessagingException {
final Charset charset = this.mailAccount.getCharset();
final MimeMessage msg = new MimeMessage(getSession());
// 发件人
final String from = this.mailAccount.getFrom();
if (StrUtil.isEmpty(from)) {
// 用户未提供发送方则从Session中自动获取
msg.setFrom();
} else {
msg.setFrom(InternalMailUtil.parseFirstAddress(from, charset));
}
// 标题
msg.setSubject(this.title, (null == charset) ? null : charset.name());
// 发送时间
msg.setSentDate(new Date());
// 内容和附件
msg.setContent(buildContent(charset));
// 收件人
msg.setRecipients(MimeMessage.RecipientType.TO, InternalMailUtil.parseAddressFromStrs(this.tos, charset));
// 抄送人
if (ArrayUtil.isNotEmpty(this.ccs)) {
msg.setRecipients(MimeMessage.RecipientType.CC, InternalMailUtil.parseAddressFromStrs(this.ccs, charset));
}
// 密送人
if (ArrayUtil.isNotEmpty(this.bccs)) {
msg.setRecipients(MimeMessage.RecipientType.BCC, InternalMailUtil.parseAddressFromStrs(this.bccs, charset));
}
// 回复地址(reply-to)
if (ArrayUtil.isNotEmpty(this.reply)) {
msg.setReplyTo(InternalMailUtil.parseAddressFromStrs(this.reply, charset));
}
return msg;
}
/**
* 构建邮件信息主体
*
* @param charset 编码,{@code null}则使用{@link MimeUtility#getDefaultJavaCharset()}
* @return 邮件信息主体
* @throws MessagingException 消息异常
*/
private Multipart buildContent(Charset charset) throws MessagingException {
final String charsetStr = null != charset ? charset.name() : MimeUtility.getDefaultJavaCharset();
// 正文
final MimeBodyPart body = new MimeBodyPart();
body.setContent(content, StrUtil.format("text/{}; charset={}", isHtml ? "html" : "plain", charsetStr));
this.multipart.addBodyPart(body);
return this.multipart;
}
/**
* 获取默认邮件会话<br>
* 如果为全局单例的会话,则全局只允许一个邮件帐号,否则每次发送邮件会新建一个新的会话
*
* @return 邮件会话 {@link Session}
*/
private Session getSession() {
final Session session = MailUtils.getSession(this.mailAccount, this.useGlobalSession);
if (null != this.debugOutput) {
session.setDebugOut(debugOutput);
}
return session;
}
// --------------------------------------------------------------- Private method end
}

View File

@@ -1,659 +0,0 @@
package org.dromara.common.mail.utils;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.setting.Setting;
import java.io.Serial;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* 邮件账户对象
*
* @author Luxiaolei
*/
public class MailAccount implements Serializable {
@Serial
private static final long serialVersionUID = -6937313421815719204L;
private static final String MAIL_PROTOCOL = "mail.transport.protocol";
private static final String SMTP_HOST = "mail.smtp.host";
private static final String SMTP_PORT = "mail.smtp.port";
private static final String SMTP_AUTH = "mail.smtp.auth";
private static final String SMTP_TIMEOUT = "mail.smtp.timeout";
private static final String SMTP_CONNECTION_TIMEOUT = "mail.smtp.connectiontimeout";
private static final String SMTP_WRITE_TIMEOUT = "mail.smtp.writetimeout";
// SSL
private static final String STARTTLS_ENABLE = "mail.smtp.starttls.enable";
private static final String SSL_ENABLE = "mail.smtp.ssl.enable";
private static final String SSL_PROTOCOLS = "mail.smtp.ssl.protocols";
private static final String SOCKET_FACTORY = "mail.smtp.socketFactory.class";
private static final String SOCKET_FACTORY_FALLBACK = "mail.smtp.socketFactory.fallback";
private static final String SOCKET_FACTORY_PORT = "smtp.socketFactory.port";
// System Properties
private static final String SPLIT_LONG_PARAMS = "mail.mime.splitlongparameters";
//private static final String ENCODE_FILE_NAME = "mail.mime.encodefilename";
//private static final String CHARSET = "mail.mime.charset";
// 其他
private static final String MAIL_DEBUG = "mail.debug";
public static final String[] MAIL_SETTING_PATHS = new String[]{"config/mail.setting", "config/mailAccount.setting", "mail.setting"};
/**
* SMTP服务器域名
*/
private String host;
/**
* SMTP服务端口
*/
private Integer port;
/**
* 是否需要用户名密码验证
*/
private Boolean auth;
/**
* 用户名
*/
private String user;
/**
* 密码
*/
private String pass;
/**
* 发送方遵循RFC-822标准
*/
private String from;
/**
* 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启
*/
private boolean debug;
/**
* 编码用于编码邮件正文和发送人、收件人等中文
*/
private Charset charset = CharsetUtil.CHARSET_UTF_8;
/**
* 对于超长参数是否切分为多份默认为false国内邮箱附件不支持切分的附件名
*/
private boolean splitlongparameters = false;
/**
* 对于文件名是否使用{@link #charset}编码,默认为 {@code true}
*/
private boolean encodefilename = true;
/**
* 使用 STARTTLS安全连接STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接TLS或SSL 而不是使用一个单独的加密通信端口。
*/
private boolean starttlsEnable = false;
/**
* 使用 SSL安全连接
*/
private Boolean sslEnable;
/**
* SSL协议多个协议用空格分隔
*/
private String sslProtocols;
/**
* 指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字
*/
private String socketFactoryClass = "javax.net.ssl.SSLSocketFactory";
/**
* 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
*/
private boolean socketFactoryFallback;
/**
* 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口
*/
private int socketFactoryPort = 465;
/**
* SMTP超时时长单位毫秒缺省值不超时
*/
private long timeout;
/**
* Socket连接超时值单位毫秒缺省值不超时
*/
private long connectionTimeout;
/**
* Socket写出超时值单位毫秒缺省值不超时
*/
private long writeTimeout;
/**
* 自定义的其他属性,此自定义属性会覆盖默认属性
*/
private final Map<String, Object> customProperty = new HashMap<>();
// -------------------------------------------------------------- Constructor start
/**
* 构造,所有参数需自行定义或保持默认值
*/
public MailAccount() {
}
/**
* 构造
*
* @param settingPath 配置文件路径
*/
public MailAccount(String settingPath) {
this(new Setting(settingPath));
}
/**
* 构造
*
* @param setting 配置文件
*/
public MailAccount(Setting setting) {
setting.toBean(this);
}
// -------------------------------------------------------------- Constructor end
/**
* 获得SMTP服务器域名
*
* @return SMTP服务器域名
*/
public String getHost() {
return host;
}
/**
* 设置SMTP服务器域名
*
* @param host SMTP服务器域名
* @return this
*/
public MailAccount setHost(String host) {
this.host = host;
return this;
}
/**
* 获得SMTP服务端口
*
* @return SMTP服务端口
*/
public Integer getPort() {
return port;
}
/**
* 设置SMTP服务端口
*
* @param port SMTP服务端口
* @return this
*/
public MailAccount setPort(Integer port) {
this.port = port;
return this;
}
/**
* 是否需要用户名密码验证
*
* @return 是否需要用户名密码验证
*/
public Boolean isAuth() {
return auth;
}
/**
* 设置是否需要用户名密码验证
*
* @param isAuth 是否需要用户名密码验证
* @return this
*/
public MailAccount setAuth(boolean isAuth) {
this.auth = isAuth;
return this;
}
/**
* 获取用户名
*
* @return 用户名
*/
public String getUser() {
return user;
}
/**
* 设置用户名
*
* @param user 用户名
* @return this
*/
public MailAccount setUser(String user) {
this.user = user;
return this;
}
/**
* 获取密码
*
* @return 密码
*/
public String getPass() {
return pass;
}
/**
* 设置密码
*
* @param pass 密码
* @return this
*/
public MailAccount setPass(String pass) {
this.pass = pass;
return this;
}
/**
* 获取发送方遵循RFC-822标准
*
* @return 发送方遵循RFC-822标准
*/
public String getFrom() {
return from;
}
/**
* 设置发送方遵循RFC-822标准<br>
* 发件人可以是以下形式:
*
* <pre>
* 1. user@xxx.xx
* 2. name &lt;user@xxx.xx&gt;
* </pre>
*
* @param from 发送方遵循RFC-822标准
* @return this
*/
public MailAccount setFrom(String from) {
this.from = from;
return this;
}
/**
* 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启
*
* @return 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启
* @since 4.0.2
*/
public boolean isDebug() {
return debug;
}
/**
* 设置是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启
*
* @param debug 是否打开调试模式,调试模式会显示与邮件服务器通信过程,默认不开启
* @return this
* @since 4.0.2
*/
public MailAccount setDebug(boolean debug) {
this.debug = debug;
return this;
}
/**
* 获取字符集编码
*
* @return 编码,可能为{@code null}
*/
public Charset getCharset() {
return charset;
}
/**
* 设置字符集编码,此选项不会修改全局配置,若修改全局配置,请设置此项为{@code null}并设置:
* <pre>
* System.setProperty("mail.mime.charset", charset);
* </pre>
*
* @param charset 字符集编码,{@code null} 则表示使用全局设置的默认编码全局编码为mail.mime.charset系统属性
* @return this
*/
public MailAccount setCharset(Charset charset) {
this.charset = charset;
return this;
}
/**
* 对于超长参数是否切分为多份默认为false国内邮箱附件不支持切分的附件名
*
* @return 对于超长参数是否切分为多份
*/
public boolean isSplitlongparameters() {
return splitlongparameters;
}
/**
* 设置对于超长参数是否切分为多份默认为false国内邮箱附件不支持切分的附件名<br>
* 注意此项为全局设置,此项会调用
* <pre>
* System.setProperty("mail.mime.splitlongparameters", true)
* </pre>
*
* @param splitlongparameters 对于超长参数是否切分为多份
*/
public void setSplitlongparameters(boolean splitlongparameters) {
this.splitlongparameters = splitlongparameters;
}
/**
* 对于文件名是否使用{@link #charset}编码,默认为 {@code true}
*
* @return 对于文件名是否使用{@link #charset}编码,默认为 {@code true}
* @since 5.7.16
*/
public boolean isEncodefilename() {
return encodefilename;
}
/**
* 设置对于文件名是否使用{@link #charset}编码,此选项不会修改全局配置<br>
* 如果此选项设置为{@code false},则是否编码取决于两个系统属性:
* <ul>
* <li>mail.mime.encodefilename 是否编码附件文件名</li>
* <li>mail.mime.charset 编码文件名的编码</li>
* </ul>
*
* @param encodefilename 对于文件名是否使用{@link #charset}编码
* @since 5.7.16
*/
public void setEncodefilename(boolean encodefilename) {
this.encodefilename = encodefilename;
}
/**
* 是否使用 STARTTLS安全连接STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接TLS或SSL 而不是使用一个单独的加密通信端口。
*
* @return 是否使用 STARTTLS安全连接
*/
public boolean isStarttlsEnable() {
return this.starttlsEnable;
}
/**
* 设置是否使用STARTTLS安全连接STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接TLS或SSL 而不是使用一个单独的加密通信端口。
*
* @param startttlsEnable 是否使用STARTTLS安全连接
* @return this
*/
public MailAccount setStarttlsEnable(boolean startttlsEnable) {
this.starttlsEnable = startttlsEnable;
return this;
}
/**
* 是否使用 SSL安全连接
*
* @return 是否使用 SSL安全连接
*/
public Boolean isSslEnable() {
return this.sslEnable;
}
/**
* 设置是否使用SSL安全连接
*
* @param sslEnable 是否使用SSL安全连接
* @return this
*/
public MailAccount setSslEnable(Boolean sslEnable) {
this.sslEnable = sslEnable;
return this;
}
/**
* 获取SSL协议多个协议用空格分隔
*
* @return SSL协议多个协议用空格分隔
* @since 5.5.7
*/
public String getSslProtocols() {
return sslProtocols;
}
/**
* 设置SSL协议多个协议用空格分隔
*
* @param sslProtocols SSL协议多个协议用空格分隔
* @since 5.5.7
*/
public void setSslProtocols(String sslProtocols) {
this.sslProtocols = sslProtocols;
}
/**
* 获取指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字
*
* @return 指定实现javax.net.SocketFactory接口的类的名称, 这个类将被用于创建SMTP的套接字
*/
public String getSocketFactoryClass() {
return socketFactoryClass;
}
/**
* 设置指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字
*
* @param socketFactoryClass 指定实现javax.net.SocketFactory接口的类的名称,这个类将被用于创建SMTP的套接字
* @return this
*/
public MailAccount setSocketFactoryClass(String socketFactoryClass) {
this.socketFactoryClass = socketFactoryClass;
return this;
}
/**
* 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
*
* @return 如果设置为true, 未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
*/
public boolean isSocketFactoryFallback() {
return socketFactoryFallback;
}
/**
* 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
*
* @param socketFactoryFallback 如果设置为true,未能创建一个套接字使用指定的套接字工厂类将导致使用java.net.Socket创建的套接字类, 默认值为true
* @return this
*/
public MailAccount setSocketFactoryFallback(boolean socketFactoryFallback) {
this.socketFactoryFallback = socketFactoryFallback;
return this;
}
/**
* 获取指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口
*
* @return 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口
*/
public int getSocketFactoryPort() {
return socketFactoryPort;
}
/**
* 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口
*
* @param socketFactoryPort 指定的端口连接到在使用指定的套接字工厂。如果没有设置,将使用默认端口
* @return this
*/
public MailAccount setSocketFactoryPort(int socketFactoryPort) {
this.socketFactoryPort = socketFactoryPort;
return this;
}
/**
* 设置SMTP超时时长单位毫秒缺省值不超时
*
* @param timeout SMTP超时时长单位毫秒缺省值不超时
* @return this
* @since 4.1.17
*/
public MailAccount setTimeout(long timeout) {
this.timeout = timeout;
return this;
}
/**
* 设置Socket连接超时值单位毫秒缺省值不超时
*
* @param connectionTimeout Socket连接超时值单位毫秒缺省值不超时
* @return this
* @since 4.1.17
*/
public MailAccount setConnectionTimeout(long connectionTimeout) {
this.connectionTimeout = connectionTimeout;
return this;
}
/**
* 设置Socket写出超时值单位毫秒缺省值不超时
*
* @param writeTimeout Socket写出超时值单位毫秒缺省值不超时
* @return this
* @since 5.8.3
*/
public MailAccount setWriteTimeout(long writeTimeout) {
this.writeTimeout = writeTimeout;
return this;
}
/**
* 获取自定义属性列表
*
* @return 自定义参数列表
* @since 5.6.4
*/
public Map<String, Object> getCustomProperty() {
return customProperty;
}
/**
* 设置自定义属性如mail.smtp.ssl.socketFactory
*
* @param key 属性名,空白被忽略
* @param value 属性值, null被忽略
* @return this
* @since 5.6.4
*/
public MailAccount setCustomProperty(String key, Object value) {
if (StrUtil.isNotBlank(key) && ObjectUtil.isNotNull(value)) {
this.customProperty.put(key, value);
}
return this;
}
/**
* 获得SMTP相关信息
*
* @return {@link Properties}
*/
public Properties getSmtpProps() {
//全局系统参数
System.setProperty(SPLIT_LONG_PARAMS, String.valueOf(this.splitlongparameters));
final Properties p = new Properties();
p.put(MAIL_PROTOCOL, "smtp");
p.put(SMTP_HOST, this.host);
p.put(SMTP_PORT, String.valueOf(this.port));
p.put(SMTP_AUTH, String.valueOf(this.auth));
if (this.timeout > 0) {
p.put(SMTP_TIMEOUT, String.valueOf(this.timeout));
}
if (this.connectionTimeout > 0) {
p.put(SMTP_CONNECTION_TIMEOUT, String.valueOf(this.connectionTimeout));
}
// issue#2355
if (this.writeTimeout > 0) {
p.put(SMTP_WRITE_TIMEOUT, String.valueOf(this.writeTimeout));
}
p.put(MAIL_DEBUG, String.valueOf(this.debug));
if (this.starttlsEnable) {
//STARTTLS是对纯文本通信协议的扩展。它将纯文本连接升级为加密连接TLS或SSL 而不是使用一个单独的加密通信端口。
p.put(STARTTLS_ENABLE, "true");
if (null == this.sslEnable) {
//为了兼容旧版本当用户没有此项配置时按照starttlsEnable开启状态时对待
this.sslEnable = true;
}
}
// SSL
if (null != this.sslEnable && this.sslEnable) {
p.put(SSL_ENABLE, "true");
p.put(SOCKET_FACTORY, socketFactoryClass);
p.put(SOCKET_FACTORY_FALLBACK, String.valueOf(this.socketFactoryFallback));
p.put(SOCKET_FACTORY_PORT, String.valueOf(this.socketFactoryPort));
// issue#IZN95@Gitee在Linux下需自定义SSL协议版本
if (StrUtil.isNotBlank(this.sslProtocols)) {
p.put(SSL_PROTOCOLS, this.sslProtocols);
}
}
// 补充自定义属性,允许自定属性覆盖已经设置的值
p.putAll(this.customProperty);
return p;
}
/**
* 如果某些值为null使用默认值
*
* @return this
*/
public MailAccount defaultIfEmpty() {
// 去掉发件人的姓名部分
final String fromAddress = InternalMailUtil.parseFirstAddress(this.from, this.charset).getAddress();
if (StrUtil.isBlank(this.host)) {
// 如果SMTP地址为空默认使用smtp.<发件人邮箱后缀>
this.host = StrUtil.format("smtp.{}", StrUtil.subSuf(fromAddress, fromAddress.indexOf('@') + 1));
}
if (StrUtil.isBlank(user)) {
// 如果用户名为空默认为发件人issue#I4FYVY@Gitee
//this.user = StrUtil.subPre(fromAddress, fromAddress.indexOf('@'));
this.user = fromAddress;
}
if (null == this.auth) {
// 如果密码非空白,则使用认证模式
this.auth = (false == StrUtil.isBlank(this.pass));
}
if (null == this.port) {
// 端口在SSL状态下默认与socketFactoryPort一致非SSL状态下默认为25
this.port = (null != this.sslEnable && this.sslEnable) ? this.socketFactoryPort : 25;
}
if (null == this.charset) {
// 默认UTF-8编码
this.charset = CharsetUtil.CHARSET_UTF_8;
}
return this;
}
@Override
public String toString() {
return "MailAccount [host=" + host + ", port=" + port + ", auth=" + auth + ", user=" + user + ", pass=" + (StrUtil.isEmpty(this.pass) ? "" : "******") + ", from=" + from + ", startttlsEnable="
+ starttlsEnable + ", socketFactoryClass=" + socketFactoryClass + ", socketFactoryFallback=" + socketFactoryFallback + ", socketFactoryPort=" + socketFactoryPort + "]";
}
}

View File

@@ -1,40 +0,0 @@
package org.dromara.common.mail.utils;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.StrUtil;
import java.io.Serial;
/**
* 邮件异常
*
* @author xiaoleilu
*/
public class MailException extends RuntimeException {
@Serial
private static final long serialVersionUID = 8247610319171014183L;
public MailException(Throwable e) {
super(ExceptionUtil.getMessage(e), e);
}
public MailException(String message) {
super(message);
}
public MailException(String messageTemplate, Object... params) {
super(StrUtil.format(messageTemplate, params));
}
public MailException(String message, Throwable throwable) {
super(message, throwable);
}
public MailException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {
super(message, throwable, enableSuppression, writableStackTrace);
}
public MailException(Throwable throwable, String messageTemplate, Object... params) {
super(StrUtil.format(messageTemplate, params), throwable);
}
}

View File

@@ -5,6 +5,9 @@ import cn.hutool.core.io.IoUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.CharUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.mail.JakartaMail;
import cn.hutool.extra.mail.JakartaUserPassAuthenticator;
import cn.hutool.extra.mail.MailAccount;
import jakarta.mail.Authenticator;
import jakarta.mail.Session;
import lombok.AccessLevel;
@@ -17,7 +20,7 @@ import java.io.InputStream;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* 邮件工具类
@@ -385,7 +388,7 @@ public class MailUtils {
public static Session getSession(MailAccount mailAccount, boolean isSingleton) {
Authenticator authenticator = null;
if (mailAccount.isAuth()) {
authenticator = new UserPassAuthenticator(mailAccount.getUser(), mailAccount.getPass());
authenticator = new JakartaUserPassAuthenticator(mailAccount.getUser(), mailAccount.getPass());
}
return isSingleton ? Session.getDefaultInstance(mailAccount.getSmtpProps(), authenticator) //
@@ -412,7 +415,7 @@ public class MailUtils {
*/
private static String send(MailAccount mailAccount, boolean useGlobalSession, Collection<String> tos, Collection<String> ccs, Collection<String> bccs, String subject, String content,
Map<String, InputStream> imageMap, boolean isHtml, File... files) {
final Mail mail = Mail.create(mailAccount).setUseGlobalSession(useGlobalSession);
final JakartaMail mail = JakartaMail.create(mailAccount).setUseGlobalSession(useGlobalSession);
// 可选抄送人
if (CollUtil.isNotEmpty(ccs)) {
@@ -431,7 +434,7 @@ public class MailUtils {
// 图片
if (MapUtil.isNotEmpty(imageMap)) {
for (Map.Entry<String, InputStream> entry : imageMap.entrySet()) {
for (Entry<String, InputStream> entry : imageMap.entrySet()) {
mail.addImage(entry.getKey(), entry.getValue());
// 关闭流
IoUtil.close(entry.getValue());
@@ -463,5 +466,4 @@ public class MailUtils {
return result;
}
// ------------------------------------------------------------------------------------------------------------------------ Private method end
}

View File

@@ -1,33 +0,0 @@
package org.dromara.common.mail.utils;
import jakarta.mail.Authenticator;
import jakarta.mail.PasswordAuthentication;
/**
* 用户名密码验证器
*
* @author looly
* @since 3.1.2
*/
public class UserPassAuthenticator extends Authenticator {
private final String user;
private final String pass;
/**
* 构造
*
* @param user 用户名
* @param pass 密码
*/
public UserPassAuthenticator(String user, String pass) {
this.user = user;
this.pass = pass;
}
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(this.user, this.pass);
}
}

View File

@@ -18,9 +18,7 @@ import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 自定义 Mapper 接口, 实现 自定义扩展
@@ -69,9 +67,7 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
* @return 插入操作是否成功的布尔值
*/
default boolean insertBatch(Collection<T> entityList) {
Db.saveBatch(entityList);
// 临时解决 新版本 mp 插入状态判断错误问题
return true;
return Db.saveBatch(entityList);
}
/**
@@ -81,9 +77,7 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
* @return 更新操作是否成功的布尔值
*/
default boolean updateBatchById(Collection<T> entityList) {
Db.updateBatchById(entityList);
// 临时解决 新版本 mp 插入状态判断错误问题
return true;
return Db.updateBatchById(entityList);
}
/**
@@ -93,9 +87,7 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
* @return 插入或更新操作是否成功的布尔值
*/
default boolean insertOrUpdateBatch(Collection<T> entityList) {
Db.saveOrUpdateBatch(entityList);
// 临时解决 新版本 mp 插入状态判断错误问题
return true;
return Db.saveOrUpdateBatch(entityList);
}
/**
@@ -106,9 +98,7 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
* @return 插入操作是否成功的布尔值
*/
default boolean insertBatch(Collection<T> entityList, int batchSize) {
Db.saveBatch(entityList, batchSize);
// 临时解决 新版本 mp 插入状态判断错误问题
return true;
return Db.saveBatch(entityList, batchSize);
}
/**
@@ -119,9 +109,7 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
* @return 更新操作是否成功的布尔值
*/
default boolean updateBatchById(Collection<T> entityList, int batchSize) {
Db.updateBatchById(entityList, batchSize);
// 临时解决 新版本 mp 插入状态判断错误问题
return true;
return Db.updateBatchById(entityList, batchSize);
}
/**
@@ -132,9 +120,7 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
* @return 插入或更新操作是否成功的布尔值
*/
default boolean insertOrUpdateBatch(Collection<T> entityList, int batchSize) {
Db.saveOrUpdateBatch(entityList, batchSize);
// 临时解决 新版本 mp 插入状态判断错误问题
return true;
return Db.saveOrUpdateBatch(entityList, batchSize);
}
/**
@@ -169,8 +155,8 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
* @param idList 主键ID集合
* @return 查询到的VO对象列表
*/
default List<V> selectVoBatchIds(Collection<? extends Serializable> idList) {
return selectVoBatchIds(idList, this.currentVoClass());
default List<V> selectVoByIds(Collection<? extends Serializable> idList) {
return selectVoByIds(idList, this.currentVoClass());
}
/**
@@ -181,8 +167,8 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
* @param <C> VO类的类型
* @return 查询到的VO对象列表经过转换为指定的VO类后返回
*/
default <C> List<C> selectVoBatchIds(Collection<? extends Serializable> idList, Class<C> voClass) {
List<T> list = this.selectBatchIds(idList);
default <C> List<C> selectVoByIds(Collection<? extends Serializable> idList, Class<C> voClass) {
List<T> list = this.selectByIds(idList);
if (CollUtil.isEmpty(list)) {
return CollUtil.newArrayList();
}

View File

@@ -13,9 +13,9 @@ import org.dromara.common.mybatis.helper.DataPermissionHelper;
* 内置数据:
* - {@code user}: 当前登录用户信息,参考 {@link LoginUser}
* 内置服务:
* - {@code sdss}: 系统数据权限服务,参考 {@link ISysDataScopeService}
* - {@code sdss}: 系统数据权限服务,参考 ISysDataScopeService
* 如需扩展数据,可以通过 {@link DataPermissionHelper} 进行操作
* 如需扩展服务,可以通过 {@link ISysDataScopeService} 自行编写
* 如需扩展服务,可以通过 ISysDataScopeService 自行编写
* </p>
*
* @author Lion Li
@@ -32,29 +32,21 @@ public enum DataScopeType {
/**
* 自定数据权限
* 使用 SpEL 表达式:`#{#deptName} IN ( #{@sdss.getRoleCustom( #user.roleId )} )`
* 如果不满足条件,则使用默认 SQL 表达式:`1 = 0`
*/
CUSTOM("2", " #{#deptName} IN ( #{@sdss.getRoleCustom( #user.roleId )} ) ", " 1 = 0 "),
/**
* 部门数据权限
* 使用 SpEL 表达式:`#{#deptName} = #{#user.deptId}`
* 如果不满足条件,则使用默认 SQL 表达式:`1 = 0`
*/
DEPT("3", " #{#deptName} = #{#user.deptId} ", " 1 = 0 "),
/**
* 部门及以下数据权限
* 使用 SpEL 表达式:`#{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )}`
* 如果不满足条件,则使用默认 SQL 表达式:`1 = 0`
*/
DEPT_AND_CHILD("4", " #{#deptName} IN ( #{@sdss.getDeptAndChild( #user.deptId )} )", " 1 = 0 "),
/**
* 仅本人数据权限
* 使用 SpEL 表达式:`#{#userName} = #{#user.userId}`
* 如果不满足条件,则使用默认 SQL 表达式:`1 = 0`
*/
SELF("5", " #{#userName} = #{#user.userId} ", " 1 = 0 ");

View File

@@ -6,8 +6,8 @@ import cn.hutool.core.util.ObjectUtil;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.Parenthesis;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import org.apache.ibatis.io.Resources;
import org.dromara.common.core.domain.dto.RoleDTO;
@@ -106,7 +106,7 @@ public class PlusDataPermissionHandler {
try {
Expression expression = CCJSqlParserUtil.parseExpression(dataFilterSql);
// 数据权限使用单独的括号 防止与其他条件冲突
Parenthesis parenthesis = new Parenthesis(expression);
ParenthesedExpressionList<Expression> parenthesis = new ParenthesedExpressionList<>(expression);
if (ObjectUtil.isNotNull(where)) {
return new AndExpression(where, parenthesis);
} else {

View File

@@ -62,8 +62,8 @@ public class DataBaseHelper {
// charindex(',100,' , ',0,100,101,') <> 0
return "charindex(',%s,' , ','+%s+',') <> 0".formatted(var, var2);
} else if (dataBasyType == DataBaseType.POSTGRE_SQL) {
// (select position(',100,' in ',0,100,101,')) <> 0
return "(select position(',%s,' in ','||%s||',')) <> 0".formatted(var, var2);
// (select strpos(',0,100,101,' , ',100,')) <> 0
return "(select strpos(','||%s||',' , ',%s,')) <> 0".formatted(var2, var);
} else if (dataBasyType == DataBaseType.ORACLE) {
// instr(',0,100,101,' , ',100,') <> 0
return "instr(','||%s||',' , ',%s,') <> 0".formatted(var2, var);

View File

@@ -2,14 +2,17 @@ package org.dromara.common.mybatis.helper;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaStorage;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.reflect.ReflectUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import java.util.function.Supplier;
/**
@@ -24,6 +27,8 @@ public class DataPermissionHelper {
private static final String DATA_PERMISSION_KEY = "data:permission";
private static final ThreadLocal<Stack<Integer>> REENTRANT_IGNORE = ThreadLocal.withInitial(Stack::new);
/**
* 从上下文中获取指定键的变量值,并将其转换为指定的类型
*
@@ -66,23 +71,54 @@ public class DataPermissionHelper {
throw new NullPointerException("data permission context type exception");
}
private static IgnoreStrategy getIgnoreStrategy() {
Object ignoreStrategyLocal = ReflectUtils.getStaticFieldValue(ReflectUtils.getField(InterceptorIgnoreHelper.class, "IGNORE_STRATEGY_LOCAL"));
if (ignoreStrategyLocal instanceof ThreadLocal<?> IGNORE_STRATEGY_LOCAL) {
if (IGNORE_STRATEGY_LOCAL.get() instanceof IgnoreStrategy ignoreStrategy) {
return ignoreStrategy;
}
}
return null;
}
/**
* 开启忽略数据权限(开启后需手动调用 {@link #disableIgnore()} 关闭)
*/
public static void enableIgnore() {
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().dataPermission(true).build());
IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
if (ObjectUtil.isNull(ignoreStrategy)) {
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().dataPermission(true).build());
} else {
ignoreStrategy.setDataPermission(true);
}
Stack<Integer> reentrantStack = REENTRANT_IGNORE.get();
reentrantStack.push(reentrantStack.size() + 1);
}
/**
* 关闭忽略数据权限
*/
public static void disableIgnore() {
InterceptorIgnoreHelper.clearIgnoreStrategy();
IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
if (ObjectUtil.isNotNull(ignoreStrategy)) {
boolean noOtherIgnoreStrategy = !Boolean.TRUE.equals(ignoreStrategy.getDynamicTableName())
&& !Boolean.TRUE.equals(ignoreStrategy.getBlockAttack())
&& !Boolean.TRUE.equals(ignoreStrategy.getIllegalSql())
&& !Boolean.TRUE.equals(ignoreStrategy.getTenantLine())
&& CollectionUtil.isEmpty(ignoreStrategy.getOthers());
Stack<Integer> reentrantStack = REENTRANT_IGNORE.get();
boolean empty = reentrantStack.isEmpty() || reentrantStack.pop() == 1;
if (noOtherIgnoreStrategy && empty) {
InterceptorIgnoreHelper.clearIgnoreStrategy();
} else if (empty) {
ignoreStrategy.setDataPermission(false);
}
}
}
/**
* 在忽略数据权限中执行
* <p>禁止在忽略数据权限中执行忽略数据权限</p>
*
* @param handle 处理执行方法
*/
@@ -97,7 +133,6 @@ public class DataPermissionHelper {
/**
* 在忽略数据权限中执行
* <p>禁止在忽略数据权限中执行忽略数据权限</p>
*
* @param handle 处理执行方法
*/

View File

@@ -15,12 +15,12 @@ import org.dromara.common.oss.properties.OssProperties;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.core.async.AsyncRequestBody;
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.S3Configuration;
import software.amazon.awssdk.services.s3.crt.S3CrtHttpConfiguration;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.NoSuchBucketException;
import software.amazon.awssdk.services.s3.model.S3Exception;
@@ -83,8 +83,8 @@ public class OssClient {
StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey()));
//MinIO 使用 HTTPS 限制使用域名访问,站点填域名。需要启用路径样式访问
boolean isStyle = !StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE);
//使用对象存储服务时要求明确配置访问样式(路径样式或虚拟托管样式)。需要启用路径样式访问
boolean isStyle = true;
//创建AWS基于 CRT 的 S3 客户端
this.client = S3AsyncClient.crtBuilder()
@@ -95,6 +95,9 @@ public class OssClient {
.minimumPartSizeInBytes(10 * 1025 * 1024L)
.checksumValidationEnabled(false)
.forcePathStyle(isStyle)
.httpConfiguration(S3CrtHttpConfiguration.builder()
.connectionTimeout(Duration.ofSeconds(60)) // 设置连接超时
.build())
.build();
//AWS基于 CRT 的 S3 AsyncClient 实例用作 S3 传输管理器的底层客户端
@@ -178,7 +181,9 @@ public class OssClient {
.key(key)
.contentMD5(StringUtils.isNotEmpty(md5Digest) ? md5Digest : null)
.contentType(contentType)
.acl(getAccessPolicy().getObjectCannedACL())
// 用于设置对象的访问控制列表ACL。不同云厂商对ACL的支持和实现方式有所不同
// 因此根据具体的云服务提供商你可能需要进行不同的配置自行开启阿里云有acl权限配置腾讯云没有acl权限配置
//.acl(getAccessPolicy().getObjectCannedACL())
.build())
.addTransferListener(LoggingTransferListener.create())
.source(filePath).build());
@@ -215,7 +220,10 @@ public class OssClient {
}
try {
// 创建异步请求体length如果为空会报错
BlockingInputStreamAsyncRequestBody body = AsyncRequestBody.forBlockingInputStream(length);
BlockingInputStreamAsyncRequestBody body = BlockingInputStreamAsyncRequestBody.builder()
.contentLength(length)
.subscribeTimeout(Duration.ofSeconds(30))
.build();
// 使用 transferManager 进行上传
Upload upload = transferManager.upload(
@@ -224,7 +232,9 @@ public class OssClient {
y -> y.bucket(properties.getBucketName())
.key(key)
.contentType(contentType)
.acl(getAccessPolicy().getObjectCannedACL())
// 用于设置对象的访问控制列表ACL。不同云厂商对ACL的支持和实现方式有所不同
// 因此根据具体的云服务提供商你可能需要进行不同的配置自行开启阿里云有acl权限配置腾讯云没有acl权限配置
//.acl(getAccessPolicy().getObjectCannedACL())
.build())
.build());
@@ -340,8 +350,8 @@ public class OssClient {
* @return UploadResult 包含上传后的文件信息
* @throws OssException 如果上传失败,抛出自定义异常
*/
public UploadResult uploadSuffix(byte[] data, String suffix) {
return upload(new ByteArrayInputStream(data), getPath(properties.getPrefix(), suffix), Long.valueOf(data.length), FileUtils.getMimeType(suffix));
public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) {
return upload(new ByteArrayInputStream(data), getPath(properties.getPrefix(), suffix), Long.valueOf(data.length), contentType);
}
/**
@@ -353,8 +363,8 @@ public class OssClient {
* @return UploadResult 包含上传后的文件信息
* @throws OssException 如果上传失败,抛出自定义异常
*/
public UploadResult uploadSuffix(InputStream inputStream, String suffix, Long length) {
return upload(inputStream, getPath(properties.getPrefix(), suffix), length, FileUtils.getMimeType(suffix));
public UploadResult uploadSuffix(InputStream inputStream, String suffix, Long length, String contentType) {
return upload(inputStream, getPath(properties.getPrefix(), suffix), length, contentType);
}
/**

View File

@@ -44,6 +44,7 @@ public class CaffeineCacheDecorator implements Cache {
}
@SuppressWarnings("unchecked")
@Override
public <T> T get(Object key, Class<T> type) {
Object o = CAFFEINE.get(getUniqueKey(key), k -> cache.get(key, type));
return (T) o;
@@ -55,6 +56,7 @@ public class CaffeineCacheDecorator implements Cache {
cache.put(key, value);
}
@Override
public ValueWrapper putIfAbsent(Object key, Object value) {
CAFFEINE.invalidate(getUniqueKey(key));
return cache.putIfAbsent(key, value);
@@ -65,6 +67,7 @@ public class CaffeineCacheDecorator implements Cache {
evictIfPresent(key);
}
@Override
public boolean evictIfPresent(Object key) {
boolean b = cache.evictIfPresent(key);
if (b) {
@@ -78,6 +81,7 @@ public class CaffeineCacheDecorator implements Cache {
cache.clear();
}
@Override
public boolean invalidate() {
return cache.invalidate();
}

View File

@@ -1,19 +1,15 @@
package org.dromara.common.redis.utils;
import org.dromara.common.core.utils.SpringUtils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.redisson.api.RMap;
import org.dromara.common.core.utils.SpringUtils;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import java.util.Set;
/**
* 缓存操作工具类 {@link }
* 缓存操作工具类
*
* @author Michelle.Chung
* @date 2022/8/13
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings(value = {"unchecked"})
@@ -21,16 +17,6 @@ public class CacheUtils {
private static final CacheManager CACHE_MANAGER = SpringUtils.getBean(CacheManager.class);
/**
* 获取缓存组内所有的KEY
*
* @param cacheNames 缓存组名称
*/
public static Set<Object> keys(String cacheNames) {
RMap<Object, Object> rmap = (RMap<Object, Object>) CACHE_MANAGER.getCache(cacheNames).getNativeCache();
return rmap.keySet();
}
/**
* 获取缓存值
*

View File

@@ -517,7 +517,7 @@ public class RedisUtils {
}
/**
* 获得缓存的基本对象列表
* 获得缓存的基本对象列表(全局匹配忽略租户 自行拼接租户id)
*
* @param pattern 字符串前缀
* @return 对象列表
@@ -528,7 +528,7 @@ public class RedisUtils {
}
/**
* 删除缓存的基本对象列表
* 删除缓存的基本对象列表(全局匹配忽略租户 自行拼接租户id)
*
* @param pattern 字符串前缀
*/

View File

@@ -7,9 +7,11 @@ import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.HttpStatus;
import org.dromara.common.core.exception.SseException;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
@@ -50,11 +52,20 @@ public class SecurityConfig implements WebMvcConfigurer {
.match(allUrlHandler.getUrls())
// 对未排除的路径进行检查
.check(() -> {
HttpServletRequest request = ServletUtils.getRequest();
// 检查是否登录 是否有token
StpUtil.checkLogin();
try {
StpUtil.checkLogin();
} catch (NotLoginException e) {
if (request.getRequestURI().contains("sse")) {
throw new SseException(e.getMessage(), e.getCode());
} else {
throw e;
}
}
// 检查 header 与 param 里的 clientid 与 token 里的是否一致
String headerCid = ServletUtils.getRequest().getHeader(LoginHelper.CLIENT_KEY);
String headerCid = request.getHeader(LoginHelper.CLIENT_KEY);
String paramCid = ServletUtils.getParameter(LoginHelper.CLIENT_KEY);
String clientId = StpUtil.getExtra(LoginHelper.CLIENT_KEY).toString();
if (!StringUtils.equalsAny(clientId, headerCid, paramCid)) {

View File

@@ -58,9 +58,9 @@ public class SocialUtils {
case "linkedin" -> new AuthLinkedinRequest(builder.build(), STATE_CACHE);
case "microsoft" -> new AuthMicrosoftRequest(builder.build(), STATE_CACHE);
case "renren" -> new AuthRenrenRequest(builder.build(), STATE_CACHE);
case "stack_overflow" -> new AuthStackOverflowRequest(builder.stackOverflowKey("").build(), STATE_CACHE);
case "stack_overflow" -> new AuthStackOverflowRequest(builder.build(), STATE_CACHE);
case "huawei" -> new AuthHuaweiRequest(builder.build(), STATE_CACHE);
case "wechat_enterprise" -> new AuthWeChatEnterpriseQrcodeRequest(builder.agentId("").build(), STATE_CACHE);
case "wechat_enterprise" -> new AuthWeChatEnterpriseQrcodeRequest(builder.build(), STATE_CACHE);
case "gitlab" -> new AuthGitlabRequest(builder.build(), STATE_CACHE);
case "wechat_mp" -> new AuthWeChatMpRequest(builder.build(), STATE_CACHE);
case "aliyun" -> new AuthAliyunRequest(builder.build(), STATE_CACHE);

View File

@@ -16,6 +16,11 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.List;
/**
* SSE 控制器
*
* @author Lion Li
*/
@RestController
@ConditionalOnProperty(value = "sse.enabled", havingValue = "true")
@RequiredArgsConstructor
@@ -23,6 +28,9 @@ public class SseController implements DisposableBean {
private final SseEmitterManager sseEmitterManager;
/**
* 建立 SSE 连接
*/
@GetMapping(value = "${sse.path}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect() {
String tokenValue = StpUtil.getTokenValue();
@@ -30,6 +38,9 @@ public class SseController implements DisposableBean {
return sseEmitterManager.connect(userId, tokenValue);
}
/**
* 关闭 SSE 连接
*/
@SaIgnore
@GetMapping(value = "${sse.path}/close")
public R<Void> close() {
@@ -39,6 +50,12 @@ public class SseController implements DisposableBean {
return R.ok();
}
/**
* 向特定用户发送消息
*
* @param userId 目标用户的 ID
* @param msg 要发送的消息内容
*/
@GetMapping(value = "${sse.path}/send")
public R<Void> send(Long userId, String msg) {
SseMessageDto dto = new SseMessageDto();
@@ -48,12 +65,20 @@ public class SseController implements DisposableBean {
return R.ok();
}
/**
* 向所有用户发送消息
*
* @param msg 要发送的消息内容
*/
@GetMapping(value = "${sse.path}/sendAll")
public R<Void> send(String msg) {
sseEmitterManager.publishAll(msg);
return R.ok();
}
/**
* 清理资源。此方法目前不执行任何操作,但避免因未实现而导致错误
*/
@Override
public void destroy() throws Exception {
// 销毁时不需要做什么 此方法避免无用操作报错

View File

@@ -1,20 +1,23 @@
package org.dromara.common.sse.core;
import cn.hutool.core.collection.CollUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.sse.dto.SseMessageDto;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
/**
* 管理 Server-Sent Events (SSE) 连接
*
* @author Lion Li
*/
@Slf4j
public class SseEmitterManager {
/**
* 订阅的频道
*/
@@ -22,24 +25,44 @@ public class SseEmitterManager {
private final static Map<Long, Map<String, SseEmitter>> USER_TOKEN_EMITTERS = new ConcurrentHashMap<>();
/**
* 建立与指定用户的 SSE 连接
*
* @param userId 用户的唯一标识符,用于区分不同用户的连接
* @param token 用户的唯一令牌,用于识别具体的连接
* @return 返回一个 SseEmitter 实例,客户端可以通过该实例接收 SSE 事件
*/
public SseEmitter connect(Long userId, String token) {
// 从 USER_TOKEN_EMITTERS 中获取或创建当前用户的 SseEmitter 映射表ConcurrentHashMap
// 每个用户可以有多个 SSE 连接,通过 token 进行区分
Map<String, SseEmitter> emitters = USER_TOKEN_EMITTERS.computeIfAbsent(userId, k -> new ConcurrentHashMap<>());
// 创建一个新的 SseEmitter 实例,超时时间设置为 0 表示无限制
SseEmitter emitter = new SseEmitter(0L);
emitters.put(token, emitter);
// 当 emitter 完成、超时或发生错误时,从映射表中移除对应的 token
emitter.onCompletion(() -> emitters.remove(token));
emitter.onTimeout(() -> emitters.remove(token));
emitter.onError((e) -> emitters.remove(token));
try {
// 向客户端发送一条连接成功的事件
emitter.send(SseEmitter.event().comment("connected"));
} catch (IOException e) {
// 如果发送消息失败,则从映射表中移除 emitter
emitters.remove(token);
}
return emitter;
}
/**
* 断开指定用户的 SSE 连接
*
* @param userId 用户的唯一标识符,用于区分不同用户的连接
* @param token 用户的唯一令牌,用于识别具体的连接
*/
public void disconnect(Long userId, String token) {
Map<String, SseEmitter> emitters = USER_TOKEN_EMITTERS.get(userId);
if (emitters != null) {
@@ -98,25 +121,13 @@ public class SseEmitterManager {
* @param sseMessageDto 要发布的SSE消息对象
*/
public void publishMessage(SseMessageDto sseMessageDto) {
List<Long> unsentUserIds = new ArrayList<>();
// 当前服务内用户,直接发送消息
for (Long userId : sseMessageDto.getUserIds()) {
if (USER_TOKEN_EMITTERS.containsKey(userId)) {
sendMessage(userId, sseMessageDto.getMessage());
continue;
}
unsentUserIds.add(userId);
}
// 不在当前服务内用户,发布订阅消息
if (CollUtil.isNotEmpty(unsentUserIds)) {
SseMessageDto broadcastMessage = new SseMessageDto();
broadcastMessage.setMessage(sseMessageDto.getMessage());
broadcastMessage.setUserIds(unsentUserIds);
RedisUtils.publish(SSE_TOPIC, broadcastMessage, consumer -> {
log.info("SSE发送主题订阅消息topic:{} session keys:{} message:{}",
SSE_TOPIC, unsentUserIds, sseMessageDto.getMessage());
});
}
SseMessageDto broadcastMessage = new SseMessageDto();
broadcastMessage.setMessage(sseMessageDto.getMessage());
broadcastMessage.setUserIds(sseMessageDto.getUserIds());
RedisUtils.publish(SSE_TOPIC, broadcastMessage, consumer -> {
log.info("SSE发送主题订阅消息topic:{} session keys:{} message:{}",
SSE_TOPIC, sseMessageDto.getUserIds(), sseMessageDto.getMessage());
});
}
/**

View File

@@ -8,7 +8,7 @@ import org.dromara.common.sse.core.SseEmitterManager;
import org.dromara.common.sse.dto.SseMessageDto;
/**
* 工具类
* SSE工具类
*
* @author Lion Li
*/

View File

@@ -35,7 +35,8 @@ public class TenantKeyPrefixHandler extends KeyPrefixHandler {
}
String tenantId = TenantHelper.getTenantId();
if (StringUtils.isBlank(tenantId)) {
log.error("无法获取有效的租户id -> Null");
log.debug("无法获取有效的租户id -> Null");
return super.map(name);
}
if (StringUtils.startsWith(name, tenantId + "")) {
// 如果存在则直接返回
@@ -61,7 +62,8 @@ public class TenantKeyPrefixHandler extends KeyPrefixHandler {
}
String tenantId = TenantHelper.getTenantId();
if (StringUtils.isBlank(tenantId)) {
log.error("无法获取有效的租户id -> Null");
log.debug("无法获取有效的租户id -> Null");
return super.unmap(name);
}
if (StringUtils.startsWith(unmap, tenantId + "")) {
// 如果存在则删除

View File

@@ -1,8 +1,8 @@
package org.dromara.common.tenant.helper;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import com.alibaba.ttl.TransmittableThreadLocal;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import lombok.AccessLevel;
@@ -11,9 +11,11 @@ import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.reflect.ReflectUtils;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import java.util.Stack;
import java.util.function.Supplier;
/**
@@ -27,7 +29,9 @@ public class TenantHelper {
private static final String DYNAMIC_TENANT_KEY = GlobalConstants.GLOBAL_REDIS_KEY + "dynamicTenant";
private static final ThreadLocal<String> TEMP_DYNAMIC_TENANT = new TransmittableThreadLocal<>();
private static final ThreadLocal<String> TEMP_DYNAMIC_TENANT = new ThreadLocal<>();
private static final ThreadLocal<Stack<Integer>> REENTRANT_IGNORE = ThreadLocal.withInitial(Stack::new);
/**
* 租户功能是否启用
@@ -36,18 +40,49 @@ public class TenantHelper {
return Convert.toBool(SpringUtils.getProperty("tenant.enable"), false);
}
private static IgnoreStrategy getIgnoreStrategy() {
Object ignoreStrategyLocal = ReflectUtils.getStaticFieldValue(ReflectUtils.getField(InterceptorIgnoreHelper.class, "IGNORE_STRATEGY_LOCAL"));
if (ignoreStrategyLocal instanceof ThreadLocal<?> IGNORE_STRATEGY_LOCAL) {
if (IGNORE_STRATEGY_LOCAL.get() instanceof IgnoreStrategy ignoreStrategy) {
return ignoreStrategy;
}
}
return null;
}
/**
* 开启忽略租户(开启后需手动调用 {@link #disableIgnore()} 关闭)
*/
public static void enableIgnore() {
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().tenantLine(true).build());
IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
if (ObjectUtil.isNull(ignoreStrategy)) {
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().tenantLine(true).build());
} else {
ignoreStrategy.setTenantLine(true);
}
Stack<Integer> reentrantStack = REENTRANT_IGNORE.get();
reentrantStack.push(reentrantStack.size() + 1);
}
/**
* 关闭忽略租户
*/
public static void disableIgnore() {
InterceptorIgnoreHelper.clearIgnoreStrategy();
IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
if (ObjectUtil.isNotNull(ignoreStrategy)) {
boolean noOtherIgnoreStrategy = !Boolean.TRUE.equals(ignoreStrategy.getDynamicTableName())
&& !Boolean.TRUE.equals(ignoreStrategy.getBlockAttack())
&& !Boolean.TRUE.equals(ignoreStrategy.getIllegalSql())
&& !Boolean.TRUE.equals(ignoreStrategy.getDataPermission())
&& CollectionUtil.isEmpty(ignoreStrategy.getOthers());
Stack<Integer> reentrantStack = REENTRANT_IGNORE.get();
boolean empty = reentrantStack.isEmpty() || reentrantStack.pop() == 1;
if (noOtherIgnoreStrategy && empty) {
InterceptorIgnoreHelper.clearIgnoreStrategy();
} else if (empty) {
ignoreStrategy.setTenantLine(false);
}
}
}
/**
@@ -94,7 +129,7 @@ public class TenantHelper {
if (!isEnable()) {
return;
}
if (!isLogin() || !global) {
if (!LoginHelper.isLogin() || !global) {
TEMP_DYNAMIC_TENANT.set(tenantId);
return;
}
@@ -111,7 +146,7 @@ public class TenantHelper {
if (!isEnable()) {
return null;
}
if (!isLogin()) {
if (!LoginHelper.isLogin()) {
return TEMP_DYNAMIC_TENANT.get();
}
// 如果线程内有值 优先返回
@@ -131,7 +166,7 @@ public class TenantHelper {
if (!isEnable()) {
return;
}
if (!isLogin()) {
if (!LoginHelper.isLogin()) {
TEMP_DYNAMIC_TENANT.remove();
return;
}
@@ -182,13 +217,4 @@ public class TenantHelper {
return tenantId;
}
private static boolean isLogin() {
try {
StpUtil.checkLogin();
return true;
} catch (Exception e) {
return false;
}
}
}

View File

@@ -43,22 +43,6 @@
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-core</artifactId>
<version>${undertow.version}</version>
</dependency>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-servlet</artifactId>
<version>${undertow.version}</version>
</dependency>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-websockets-jsr</artifactId>
<version>${undertow.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>

View File

@@ -1,19 +1,15 @@
package org.dromara.common.web.config;
import org.dromara.common.core.utils.StringUtils;
import jakarta.servlet.DispatcherType;
import org.dromara.common.web.config.properties.XssProperties;
import org.dromara.common.web.filter.RepeatableFilter;
import org.dromara.common.web.filter.XssFilter;
import jakarta.servlet.DispatcherType;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import java.util.HashMap;
import java.util.Map;
/**
* Filter配置
*
@@ -23,26 +19,21 @@ import java.util.Map;
@EnableConfigurationProperties(XssProperties.class)
public class FilterConfig {
@SuppressWarnings({"rawtypes", "unchecked"})
@Bean
@ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
public FilterRegistrationBean xssFilterRegistration(XssProperties xssProperties) {
FilterRegistrationBean registration = new FilterRegistrationBean();
public FilterRegistrationBean<XssFilter> xssFilterRegistration() {
FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>();
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new XssFilter());
registration.addUrlPatterns(StringUtils.split(xssProperties.getUrlPatterns(), StringUtils.SEPARATOR));
registration.addUrlPatterns("/*");
registration.setName("xssFilter");
registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
Map<String, String> initParameters = new HashMap<>();
initParameters.put("excludes", xssProperties.getExcludes());
registration.setInitParameters(initParameters);
registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE + 1);
return registration;
}
@SuppressWarnings({"rawtypes", "unchecked"})
@Bean
public FilterRegistrationBean someFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
public FilterRegistrationBean<RepeatableFilter> someFilterRegistration() {
FilterRegistrationBean<RepeatableFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new RepeatableFilter());
registration.addUrlPatterns("/*");
registration.setName("repeatableFilter");

View File

@@ -3,6 +3,9 @@ package org.dromara.common.web.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.ArrayList;
import java.util.List;
/**
* xss过滤 配置属性
*
@@ -13,18 +16,13 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
public class XssProperties {
/**
* 过滤开关
* Xss开关
*/
private String enabled;
private Boolean enabled;
/**
* 排除链接(多个用逗号分隔)
* 排除路径
*/
private String excludes;
/**
* 匹配链接
*/
private String urlPatterns;
private List<String> excludeUrls = new ArrayList<>();
}

View File

@@ -1,6 +1,8 @@
package org.dromara.common.web.filter;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.web.config.properties.XssProperties;
import org.springframework.http.HttpMethod;
import jakarta.servlet.*;
@@ -23,13 +25,8 @@ public class XssFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
String tempExcludes = filterConfig.getInitParameter("excludes");
if (StringUtils.isNotEmpty(tempExcludes)) {
String[] url = tempExcludes.split(StringUtils.SEPARATOR);
for (int i = 0; url != null && i < url.length; i++) {
excludes.add(url[i]);
}
}
XssProperties properties = SpringUtils.getBean(XssProperties.class);
excludes.addAll(properties.getExcludeUrls());
}
@Override

View File

@@ -14,6 +14,7 @@ import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/**
* XSS过滤处理
@@ -28,6 +29,33 @@ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
super(request);
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
if (value != null) {
return HtmlUtil.cleanHtmlTag(value).trim();
}
return value;
}
@Override
public Map<String, String[]> getParameterMap() {
Map<String, String[]> valueMap = super.getParameterMap();
for (Map.Entry<String, String[]> entry : valueMap.entrySet()) {
String[] values = entry.getValue();
if (values != null) {
int length = values.length;
String[] escapseValues = new String[length];
for (int i = 0; i < length; i++) {
// 防xss攻击和过滤前后空格
escapseValues[i] = HtmlUtil.cleanHtmlTag(values[i]).trim();
}
valueMap.put(entry.getKey(), escapseValues);
}
}
return valueMap;
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
@@ -40,7 +68,7 @@ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
}
return escapseValues;
}
return super.getParameterValues(name);
return values;
}
@Override

View File

@@ -2,14 +2,17 @@ package org.dromara.common.web.handler;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.HttpStatus;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.exception.SseException;
import org.dromara.common.core.exception.base.BaseException;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
@@ -53,6 +56,27 @@ public class GlobalExceptionHandler {
return ObjectUtil.isNotNull(code) ? R.fail(code, e.getMessage()) : R.fail(e.getMessage());
}
/**
* 认证失败
*/
@ResponseStatus(org.springframework.http.HttpStatus.UNAUTHORIZED)
@ExceptionHandler(SseException.class)
public String handleNotLoginException(SseException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, e.getMessage());
return JsonUtils.toJsonString(R.fail(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,无法访问系统资源"));
}
/**
* servlet异常
*/
@ExceptionHandler(ServletException.class)
public R<Void> handleServletException(ServletException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}',发生未知异常.", requestURI, e);
return R.fail(e.getMessage());
}
/**
* 业务异常
*/
@@ -152,7 +176,7 @@ public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public R<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error(e.getMessage());
String message = e.getBindingResult().getFieldError().getDefaultMessage();
String message = StreamUtils.join(e.getBindingResult().getAllErrors(), DefaultMessageSourceResolvable::getDefaultMessage, ", ");
return R.fail(message);
}

View File

@@ -2,11 +2,11 @@ package org.dromara.common.web.interceptor;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.StopWatch;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.web.filter.RepeatedlyRequestWrapper;
@@ -19,7 +19,6 @@ import java.util.Map;
/**
* web的调用时间统计拦截器
* dev环境有效
*
* @author Lion Li
* @since 3.3.0
@@ -27,37 +26,34 @@ import java.util.Map;
@Slf4j
public class PlusWebInvokeTimeInterceptor implements HandlerInterceptor {
private final String prodProfile = "prod";
private final static ThreadLocal<StopWatch> KEY_CACHE = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!prodProfile.equals(SpringUtils.getActiveProfile())) {
String url = request.getMethod() + " " + request.getRequestURI();
String url = request.getMethod() + " " + request.getRequestURI();
// 打印请求参数
if (isJsonRequest(request)) {
String jsonParam = "";
if (request instanceof RepeatedlyRequestWrapper) {
BufferedReader reader = request.getReader();
jsonParam = IoUtil.read(reader);
}
log.info("[PLUS]开始请求 => URL[{}],参数类型[json],参数:[{}]", url, jsonParam);
} else {
Map<String, String[]> parameterMap = request.getParameterMap();
if (MapUtil.isNotEmpty(parameterMap)) {
String parameters = JsonUtils.toJsonString(parameterMap);
log.info("[PLUS]开始请求 => URL[{}],参数类型[param],参数:[{}]", url, parameters);
} else {
log.info("[PLUS]开始请求 => URL[{}],无参数", url);
}
// 打印请求参数
if (isJsonRequest(request)) {
String jsonParam = "";
if (request instanceof RepeatedlyRequestWrapper) {
BufferedReader reader = request.getReader();
jsonParam = IoUtil.read(reader);
}
log.info("[PLUS]开始请求 => URL[{}],参数类型[json],参数:[{}]", url, jsonParam);
} else {
Map<String, String[]> parameterMap = request.getParameterMap();
if (MapUtil.isNotEmpty(parameterMap)) {
String parameters = JsonUtils.toJsonString(parameterMap);
log.info("[PLUS]开始请求 => URL[{}],参数类型[param],参数:[{}]", url, parameters);
} else {
log.info("[PLUS]开始请求 => URL[{}],无参数", url);
}
StopWatch stopWatch = new StopWatch();
KEY_CACHE.set(stopWatch);
stopWatch.start();
}
StopWatch stopWatch = new StopWatch();
KEY_CACHE.set(stopWatch);
stopWatch.start();
return true;
}
@@ -68,8 +64,8 @@ public class PlusWebInvokeTimeInterceptor implements HandlerInterceptor {
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
if (!prodProfile.equals(SpringUtils.getActiveProfile())) {
StopWatch stopWatch = KEY_CACHE.get();
StopWatch stopWatch = KEY_CACHE.get();
if (ObjectUtil.isNotNull(stopWatch)) {
stopWatch.stop();
log.info("[PLUS]结束请求 => URL[{}],耗时:[{}]毫秒", request.getMethod() + " " + request.getRequestURI(), stopWatch.getTime());
KEY_CACHE.remove();

View File

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

View File

@@ -13,8 +13,8 @@ logging:
spring:
security:
user:
name: ruoyi
password: 123456
name: @monitor.username@
password: @monitor.password@
boot:
admin:
ui:
@@ -44,5 +44,5 @@ spring.boot.admin.client:
metadata:
username: ${spring.boot.admin.client.username}
userpassword: ${spring.boot.admin.client.password}
username: ruoyi
password: 123456
username: @monitor.username@
password: @monitor.password@

View File

@@ -16,6 +16,18 @@
<groupId>com.aizuda</groupId>
<artifactId>snail-job-server-starter</artifactId>
<version>${snailjob.version}</version>
<exclusions>
<exclusion>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>2.13.9</version>
</dependency>
<dependency>

View File

@@ -46,5 +46,5 @@ spring.boot.admin.client:
metadata:
username: ${spring.boot.admin.client.username}
userpassword: ${spring.boot.admin.client.password}
username: ruoyi
password: 123456
username: @monitor.username@
password: @monitor.password@

View File

@@ -46,5 +46,5 @@ spring.boot.admin.client:
metadata:
username: ${spring.boot.admin.client.username}
userpassword: ${spring.boot.admin.client.password}
username: ruoyi
password: 123456
username: @monitor.username@
password: @monitor.password@

View File

@@ -48,7 +48,7 @@ public interface TestDemoMapper extends BaseMapperPlus<TestDemo, TestDemoVo> {
@DataColumn(key = "deptName", value = "dept_id"),
@DataColumn(key = "userName", value = "user_id")
}, joinStr = "AND")
List<TestDemo> selectBatchIds(@Param(Constants.COLL) Collection<? extends Serializable> idList);
List<TestDemo> selectByIds(@Param(Constants.COLL) Collection<? extends Serializable> idList);
@Override
@DataPermission({

View File

@@ -101,7 +101,7 @@ public class TestDemoServiceImpl implements ITestDemoService {
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
// 做一些业务上的校验,判断是否需要校验
List<TestDemo> list = baseMapper.selectBatchIds(ids);
List<TestDemo> list = baseMapper.selectByIds(ids);
if (list.size() != ids.size()) {
throw new ServiceException("您没有删除权限!");
}

View File

@@ -64,19 +64,19 @@
<!-- <dependency>-->
<!-- <groupId>org.anyline</groupId>-->
<!-- <artifactId>anyline-data-jdbc-oracle</artifactId>-->
<!-- <version>${anyline.version}</version>-->
<!-- <version>${anyline.version}</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.anyline</groupId>-->
<!-- <artifactId>anyline-data-jdbc-postgresql</artifactId>-->
<!-- <version>${anyline.version}</version>-->
<!-- <version>${anyline.version}</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.anyline</groupId>-->
<!-- <artifactId>anyline-data-jdbc-mssql</artifactId>-->
<!-- <version>${anyline.version}</version>-->
<!-- <version>${anyline.version}</version>-->
<!-- </dependency>-->
</dependencies>

View File

@@ -56,13 +56,13 @@ public interface GenConstants {
* 数据库时间类型
*/
String[] COLUMNTYPE_TIME = {"datetime", "time", "date", "timestamp", "year", "interval",
"smalldatetime", "datetime2", "datetimeoffset"};
"smalldatetime", "datetime2", "datetimeoffset", "timestamptz"};
/**
* 数据库数字类型
*/
String[] COLUMNTYPE_NUMBER = {"tinyint", "smallint", "mediumint", "int", "number", "integer",
"bit", "bigint", "float", "double", "decimal", "numeric", "real", "double precision",
String[] COLUMNTYPE_NUMBER = {"tinyint", "smallint", "mediumint", "int", "int2", "int4", "int8", "number", "integer",
"bit", "bigint", "float", "float4", "float8", "double", "decimal", "numeric", "real", "double precision",
"smallserial", "serial", "bigserial", "money", "smallmoney"};
/**

View File

@@ -162,7 +162,7 @@ public class GenTable extends BaseEntity {
* 上级菜单ID字段
*/
@TableField(exist = false)
private String parentMenuId;
private Long parentMenuId;
/**
* 上级菜单名称字段

View File

@@ -2,10 +2,8 @@ package org.dromara.generator.mapper;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.generator.domain.GenTable;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@@ -40,6 +38,14 @@ public interface GenTableMapper extends BaseMapperPlus<GenTable, GenTable> {
*/
GenTable selectGenTableByName(String tableName);
/**
* 查询指定数据源下的所有表名列表
*
* @param dataName 数据源名称,用于选择不同的数据源
* @return 当前数据库中的表名列表
*
* @DS("") 使用默认数据源执行查询操作
*/
@DS("")
List<String> selectTableNameList(String dataName);
}

View File

@@ -3,7 +3,6 @@ package org.dromara.generator.service;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
@@ -138,7 +137,7 @@ public class GenTableServiceImpl implements IGenTableService {
}
// 过滤并转换表格数据
List<GenTable> tables = tablesMap.values().stream()
.filter(x -> !StringUtils.containsAnyIgnoreCase(x.getName(), TABLE_IGNORE))
.filter(x -> !startWithAnyIgnoreCase(x.getName(), TABLE_IGNORE))
.filter(x -> {
if (CollUtil.isEmpty(tableNames)) {
return true;
@@ -175,6 +174,16 @@ public class GenTableServiceImpl implements IGenTableService {
return TableDataInfo.build(page);
}
public static boolean startWithAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences) {
// 判断是否是以指定字符串开头
for (CharSequence searchCharSequence : searchCharSequences) {
if (StringUtils.startsWithIgnoreCase(cs, searchCharSequence)) {
return true;
}
}
return false;
}
/**
* 查询据库列表
*
@@ -196,7 +205,7 @@ public class GenTableServiceImpl implements IGenTableService {
.filter(x -> !StringUtils.containsAnyIgnoreCase(x.getName(), TABLE_IGNORE))
.filter(x -> tableNameSet.contains(x.getName())).toList();
if (ArrayUtil.isEmpty(tableList)) {
if (CollUtil.isEmpty(tableList)) {
return new ArrayList<>();
}
return tableList.stream().map(x -> {
@@ -549,7 +558,7 @@ public class GenTableServiceImpl implements IGenTableService {
String treeCode = paramsObj.getStr(GenConstants.TREE_CODE);
String treeParentCode = paramsObj.getStr(GenConstants.TREE_PARENT_CODE);
String treeName = paramsObj.getStr(GenConstants.TREE_NAME);
String parentMenuId = paramsObj.getStr(GenConstants.PARENT_MENU_ID);
Long parentMenuId = paramsObj.getLong(GenConstants.PARENT_MENU_ID);
String parentMenuName = paramsObj.getStr(GenConstants.PARENT_MENU_NAME);
genTable.setTreeCode(treeCode);

View File

@@ -215,6 +215,9 @@ public class VelocityUtils {
importList.add("com.fasterxml.jackson.annotation.JsonFormat");
} else if (!column.isSuperColumn() && GenConstants.TYPE_BIGDECIMAL.equals(column.getJavaType())) {
importList.add("java.math.BigDecimal");
} else if (!column.isSuperColumn() && "imageUpload".equals(column.getHtmlType())) {
importList.add("org.dromara.common.translation.annotation.Translation");
importList.add("org.dromara.common.translation.constant.TransConstant");
}
}
return importList;

View File

@@ -53,6 +53,13 @@ public class ${ClassName}Vo implements Serializable {
#end
private $column.javaType $column.javaField;
#if($column.htmlType == "imageUpload")
/**
* ${column.columnComment}Url
*/
@Translation(type = TransConstant.OSS_ID_TO_URL, mapper = "${column.javaField}")
private String ${column.javaField}Url;
#end
#end
#end

View File

@@ -9,6 +9,12 @@ export interface ${BusinessName}VO {
#elseif($column.javaType == 'Boolean') boolean;
#else string;
#end
#if($column.htmlType == "imageUpload")
/**
* ${column.columnComment}Url
*/
${column.javaField}Url: string;
#end
#end
#end
#if ($table.tree)

View File

@@ -99,9 +99,9 @@
</template>
</el-table-column>
#elseif($column.list && $column.htmlType == "imageUpload")
<el-table-column label="${comment}" align="center" prop="${javaField}" width="100">
<el-table-column label="${comment}" align="center" prop="${javaField}Url" width="100">
<template #default="scope">
<image-preview :src="scope.row.${javaField}" :width="50" :height="50"/>
<image-preview :src="scope.row.${javaField}Url" :width="50" :height="50"/>
</template>
</el-table-column>
#elseif($column.list && $column.dictType && "" != $column.dictType)

View File

@@ -101,9 +101,9 @@
</template>
</el-table-column>
#elseif($column.list && $column.htmlType == "imageUpload")
<el-table-column label="${comment}" align="center" prop="${javaField}" width="100">
<el-table-column label="${comment}" align="center" prop="${javaField}Url" width="100">
<template #default="scope">
<image-preview :src="scope.row.${javaField}" :width="50" :height="50"/>
<image-preview :src="scope.row.${javaField}Url" :width="50" :height="50"/>
</template>
</el-table-column>
#elseif($column.list && $column.dictType && "" != $column.dictType)

View File

@@ -12,6 +12,7 @@ import org.dromara.common.web.core.BaseController;
import org.dromara.system.domain.bo.SysDeptBo;
import org.dromara.system.domain.vo.SysDeptVo;
import org.dromara.system.service.ISysDeptService;
import org.dromara.system.service.ISysPostService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -29,6 +30,7 @@ import java.util.List;
public class SysDeptController extends BaseController {
private final ISysDeptService deptService;
private final ISysPostService postService;
/**
* 获取部门列表
@@ -117,6 +119,9 @@ public class SysDeptController extends BaseController {
if (deptService.checkDeptExistUser(deptId)) {
return R.warn("部门存在用户,不允许删除");
}
if (postService.countPostByDeptId(deptId) > 0) {
return R.warn("部门存在岗位,不允许删除");
}
deptService.checkDeptDataScope(deptId);
return toAjax(deptService.deleteDeptById(deptId));
}

View File

@@ -11,6 +11,7 @@ import org.dromara.common.encrypt.annotation.ApiEncrypt;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.helper.DataPermissionHelper;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.web.core.BaseController;
import org.dromara.system.domain.bo.SysUserBo;
@@ -72,7 +73,8 @@ public class SysProfileController extends BaseController {
if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) {
return R.fail("修改用户'" + username + "'失败,邮箱账号已存在");
}
if (userService.updateUserProfile(user) > 0) {
int rows = DataPermissionHelper.ignore(() -> userService.updateUserProfile(user));
if (rows > 0) {
return R.ok();
}
return R.fail("修改个人信息异常,请联系管理员");

View File

@@ -24,6 +24,7 @@ import org.dromara.common.web.core.BaseController;
import org.dromara.system.domain.bo.SysTenantBo;
import org.dromara.system.domain.vo.SysTenantVo;
import org.dromara.system.service.ISysTenantService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -38,6 +39,7 @@ import java.util.List;
@RequiredArgsConstructor
@RestController
@RequestMapping("/system/tenant")
@ConditionalOnProperty(value = "tenant.enable", havingValue = "true")
public class SysTenantController extends BaseController {
private final ISysTenantService tenantService;
@@ -174,4 +176,18 @@ public class SysTenantController extends BaseController {
return toAjax(TenantHelper.ignore(() -> tenantService.syncTenantPackage(tenantId, packageId)));
}
/**
* 同步租户字典
*/
@SaCheckRole(TenantConstants.SUPER_ADMIN_ROLE_KEY)
@Log(title = "同步租户字典", businessType = BusinessType.INSERT)
@GetMapping("/syncTenantDict")
public R<Void> syncTenantDict() {
if (!TenantHelper.isEnable()) {
return R.fail("当前未开启租户模式");
}
tenantService.syncTenantDict();
return R.ok("同步租户字典成功");
}
}

View File

@@ -20,6 +20,7 @@ import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -34,6 +35,7 @@ import java.util.List;
@RequiredArgsConstructor
@RestController
@RequestMapping("/system/tenant/package")
@ConditionalOnProperty(value = "tenant.enable", havingValue = "true")
public class SysTenantPackageController extends BaseController {
private final ISysTenantPackageService tenantPackageService;
@@ -92,6 +94,9 @@ public class SysTenantPackageController extends BaseController {
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody SysTenantPackageBo bo) {
if (!tenantPackageService.checkPackageNameUnique(bo)) {
return R.fail("新增套餐'" + bo.getPackageName() + "'失败,套餐名称已存在");
}
return toAjax(tenantPackageService.insertByBo(bo));
}
@@ -104,6 +109,9 @@ public class SysTenantPackageController extends BaseController {
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody SysTenantPackageBo bo) {
if (!tenantPackageService.checkPackageNameUnique(bo)) {
return R.fail("修改套餐'" + bo.getPackageName() + "'失败,套餐名称已存在");
}
return toAjax(tenantPackageService.updateByBo(bo));
}

View File

@@ -61,13 +61,13 @@ public class SysUserVo implements Serializable {
/**
* 用户邮箱
*/
@Sensitive(strategy = SensitiveStrategy.EMAIL)
@Sensitive(strategy = SensitiveStrategy.EMAIL, perms = "system:user:edit")
private String email;
/**
* 手机号码
*/
@Sensitive(strategy = SensitiveStrategy.PHONE)
@Sensitive(strategy = SensitiveStrategy.PHONE, perms = "system:user:edit")
private String phonenumber;
/**

View File

@@ -3,10 +3,14 @@ package org.dromara.system.listener;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.digest.BCrypt;
import cn.hutool.http.HtmlUtil;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.excel.core.ExcelListener;
import org.dromara.common.excel.core.ExcelResult;
@@ -79,8 +83,12 @@ public class SysUserImportListener extends AnalysisEventListener<SysUserImportVo
}
} catch (Exception e) {
failureNum++;
String msg = "<br/>" + failureNum + "、账号 " + userVo.getUserName() + " 导入失败:";
failureMsg.append(msg).append(e.getMessage());
String msg = "<br/>" + failureNum + "、账号 " + HtmlUtil.cleanHtmlTag(userVo.getUserName()) + " 导入失败:";
String message = e.getMessage();
if (e instanceof ConstraintViolationException cvException) {
message = StreamUtils.join(cvException.getConstraintViolations(), ConstraintViolation::getMessage, ", ");
}
failureMsg.append(msg).append(message);
log.error(msg, e);
}
}

View File

@@ -80,6 +80,14 @@ public interface ISysPostService {
*/
long countUserPostById(Long postId);
/**
* 通过部门ID查询岗位使用数量
*
* @param deptId 部门id
* @return 结果
*/
long countPostByDeptId(Long deptId);
/**
* 删除岗位信息
*

View File

@@ -1,9 +1,9 @@
package org.dromara.system.service;
import org.dromara.system.domain.vo.SysTenantPackageVo;
import org.dromara.system.domain.bo.SysTenantPackageBo;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.system.domain.bo.SysTenantPackageBo;
import org.dromara.system.domain.vo.SysTenantPackageVo;
import java.util.Collection;
import java.util.List;
@@ -45,6 +45,11 @@ public interface ISysTenantPackageService {
*/
Boolean updateByBo(SysTenantPackageBo bo);
/**
* 校验套餐名称是否唯一
*/
boolean checkPackageNameUnique(SysTenantPackageBo bo);
/**
* 修改套餐状态
*/

View File

@@ -79,4 +79,9 @@ public interface ISysTenantService {
* 同步租户套餐
*/
Boolean syncTenantPackage(String tenantId, Long packageId);
/**
* 同步租户字典
*/
void syncTenantDict();
}

View File

@@ -2,6 +2,7 @@ package org.dromara.system.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.utils.StreamUtils;
@@ -38,6 +39,9 @@ public class SysDataScopeServiceImpl implements ISysDataScopeService {
*/
@Override
public String getRoleCustom(Long roleId) {
if (ObjectUtil.isNull(roleId)) {
return "-1";
}
List<SysRoleDept> list = roleDeptMapper.selectList(
new LambdaQueryWrapper<SysRoleDept>()
.select(SysRoleDept::getDeptId)
@@ -45,7 +49,7 @@ public class SysDataScopeServiceImpl implements ISysDataScopeService {
if (CollUtil.isNotEmpty(list)) {
return StreamUtils.join(list, rd -> Convert.toStr(rd.getDeptId()));
}
return null;
return "-1";
}
/**
@@ -56,6 +60,9 @@ public class SysDataScopeServiceImpl implements ISysDataScopeService {
*/
@Override
public String getDeptAndChild(Long deptId) {
if (ObjectUtil.isNull(deptId)) {
return "-1";
}
List<SysDept> deptList = deptMapper.selectList(new LambdaQueryWrapper<SysDept>()
.select(SysDept::getDeptId)
.apply(DataBaseHelper.findInSet(deptId, "ancestors")));
@@ -64,7 +71,7 @@ public class SysDataScopeServiceImpl implements ISysDataScopeService {
if (CollUtil.isNotEmpty(ids)) {
return StreamUtils.join(ids, Convert::toStr);
}
return null;
return "-1";
}
}

View File

@@ -275,6 +275,8 @@ public class SysDeptServiceImpl implements ISysDeptService, DeptService {
dept.setAncestors(newAncestors);
updateDeptChildren(dept.getDeptId(), newAncestors, oldAncestors);
}
} else {
dept.setAncestors(oldDept.getAncestors());
}
int result = baseMapper.updateById(dept);
if (UserConstants.DEPT_NORMAL.equals(dept.getStatus()) && StringUtils.isNotEmpty(dept.getAncestors())

View File

@@ -3,18 +3,18 @@ package org.dromara.system.service.impl;
import cn.hutool.core.util.ArrayUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.ip.AddressUtils;
import org.dromara.common.log.event.OperLogEvent;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.system.domain.SysOperLog;
import org.dromara.system.domain.bo.SysOperLogBo;
import org.dromara.system.domain.vo.SysOperLogVo;
import org.dromara.system.mapper.SysOperLogMapper;
import org.dromara.system.service.ISysOperLogService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@@ -51,8 +51,18 @@ public class SysOperLogServiceImpl implements ISysOperLogService {
@Override
public TableDataInfo<SysOperLogVo> selectPageOperLogList(SysOperLogBo operLog, PageQuery pageQuery) {
LambdaQueryWrapper<SysOperLog> lqw = buildQueryWrapper(operLog);
if (StringUtils.isBlank(pageQuery.getOrderByColumn())) {
pageQuery.setOrderByColumn("oper_id");
pageQuery.setIsAsc("desc");
}
Page<SysOperLogVo> page = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(page);
}
private LambdaQueryWrapper<SysOperLog> buildQueryWrapper(SysOperLogBo operLog) {
Map<String, Object> params = operLog.getParams();
LambdaQueryWrapper<SysOperLog> lqw = new LambdaQueryWrapper<SysOperLog>()
return new LambdaQueryWrapper<SysOperLog>()
.like(StringUtils.isNotBlank(operLog.getOperIp()), SysOperLog::getOperIp, operLog.getOperIp())
.like(StringUtils.isNotBlank(operLog.getTitle()), SysOperLog::getTitle, operLog.getTitle())
.eq(operLog.getBusinessType() != null && operLog.getBusinessType() > 0,
@@ -67,12 +77,6 @@ public class SysOperLogServiceImpl implements ISysOperLogService {
.like(StringUtils.isNotBlank(operLog.getOperName()), SysOperLog::getOperName, operLog.getOperName())
.between(params.get("beginTime") != null && params.get("endTime") != null,
SysOperLog::getOperTime, params.get("beginTime"), params.get("endTime"));
if (StringUtils.isBlank(pageQuery.getOrderByColumn())) {
pageQuery.setOrderByColumn("oper_id");
pageQuery.setIsAsc("desc");
}
Page<SysOperLogVo> page = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(page);
}
/**
@@ -95,23 +99,8 @@ public class SysOperLogServiceImpl implements ISysOperLogService {
*/
@Override
public List<SysOperLogVo> selectOperLogList(SysOperLogBo operLog) {
Map<String, Object> params = operLog.getParams();
return baseMapper.selectVoList(new LambdaQueryWrapper<SysOperLog>()
.like(StringUtils.isNotBlank(operLog.getOperIp()), SysOperLog::getOperIp, operLog.getOperIp())
.like(StringUtils.isNotBlank(operLog.getTitle()), SysOperLog::getTitle, operLog.getTitle())
.eq(operLog.getBusinessType() != null && operLog.getBusinessType() > 0,
SysOperLog::getBusinessType, operLog.getBusinessType())
.func(f -> {
if (ArrayUtil.isNotEmpty(operLog.getBusinessTypes())) {
f.in(SysOperLog::getBusinessType, Arrays.asList(operLog.getBusinessTypes()));
}
})
.eq(operLog.getStatus() != null && operLog.getStatus() > 0,
SysOperLog::getStatus, operLog.getStatus())
.like(StringUtils.isNotBlank(operLog.getOperName()), SysOperLog::getOperName, operLog.getOperName())
.between(params.get("beginTime") != null && params.get("endTime") != null,
SysOperLog::getOperTime, params.get("beginTime"), params.get("endTime"))
.orderByDesc(SysOperLog::getOperId));
LambdaQueryWrapper<SysOperLog> lqw = buildQueryWrapper(operLog);
return baseMapper.selectVoList(lqw.orderByDesc(SysOperLog::getOperId));
}
/**

View File

@@ -195,7 +195,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
OssClient storage = OssFactory.instance();
UploadResult uploadResult;
try {
uploadResult = storage.uploadSuffix(file.getBytes(), suffix);
uploadResult = storage.uploadSuffix(file.getBytes(), suffix, file.getContentType());
} catch (IOException e) {
throw new ServiceException(e.getMessage());
}
@@ -244,7 +244,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
if (isValid) {
// 做一些业务上的校验,判断是否需要校验
}
List<SysOss> list = baseMapper.selectBatchIds(ids);
List<SysOss> list = baseMapper.selectByIds(ids);
for (SysOss sysOss : list) {
OssClient storage = OssFactory.instance(sysOss.getService());
storage.delete(sysOss.getUrl());

View File

@@ -177,6 +177,17 @@ public class SysPostServiceImpl implements ISysPostService {
return userPostMapper.selectCount(new LambdaQueryWrapper<SysUserPost>().eq(SysUserPost::getPostId, postId));
}
/**
* 通过部门ID查询岗位使用数量
*
* @param deptId 部门id
* @return 结果
*/
@Override
public long countPostByDeptId(Long deptId) {
return baseMapper.selectCount(new LambdaQueryWrapper<SysPost>().eq(SysPost::getDeptId, deptId));
}
/**
* 删除岗位信息
*

View File

@@ -293,6 +293,10 @@ public class SysRoleServiceImpl implements ISysRoleService {
@Transactional(rollbackFor = Exception.class)
public int updateRole(SysRoleBo bo) {
SysRole role = MapstructUtils.convert(bo, SysRole.class);
if (UserConstants.ROLE_DISABLE.equals(role.getStatus()) && this.countUserRoleByRoleId(role.getRoleId()) > 0) {
throw new ServiceException("角色已分配,不能禁用!");
}
// 修改角色信息
baseMapper.updateById(role);
// 删除角色与菜单关联

View File

@@ -1,6 +1,7 @@
package org.dromara.system.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -116,6 +117,17 @@ public class SysTenantPackageServiceImpl implements ISysTenantPackageService {
return baseMapper.updateById(update) > 0;
}
/**
* 校验套餐名称是否唯一
*/
@Override
public boolean checkPackageNameUnique(SysTenantPackageBo bo) {
boolean exist = baseMapper.exists(new LambdaQueryWrapper<SysTenantPackage>()
.eq(SysTenantPackage::getPackageName, bo.getPackageName())
.ne(ObjectUtil.isNotNull(bo.getPackageId()), SysTenantPackage::getPackageId, bo.getPackageId()));
return !exist;
}
/**
* 修改套餐状态
*

View File

@@ -1,6 +1,8 @@
package org.dromara.system.service.impl;
import cn.dev33.satoken.secure.BCrypt;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
@@ -14,9 +16,13 @@ import org.dromara.common.core.constant.TenantConstants;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.redis.utils.CacheUtils;
import org.dromara.common.tenant.core.TenantEntity;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.system.domain.*;
import org.dromara.system.domain.bo.SysTenantBo;
import org.dromara.system.domain.vo.SysTenantVo;
@@ -27,10 +33,7 @@ import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.*;
/**
* 租户Service业务层处理
@@ -201,7 +204,7 @@ public class SysTenantServiceImpl implements ISysTenantService {
String numbers = RandomUtil.randomNumbers(6);
// 判断是否存在,如果存在则重新生成
if (tenantIds.contains(numbers)) {
generateTenantId(tenantIds);
return generateTenantId(tenantIds);
}
return numbers;
}
@@ -266,7 +269,9 @@ public class SysTenantServiceImpl implements ISysTenantService {
@CacheEvict(cacheNames = CacheNames.SYS_TENANT, key = "#bo.tenantId")
@Override
public int updateTenantStatus(SysTenantBo bo) {
SysTenant tenant = MapstructUtils.convert(bo, SysTenant.class);
SysTenant tenant = new SysTenant();
tenant.setId(bo.getId());
tenant.setStatus(bo.getStatus());
return baseMapper.updateById(tenant);
}
@@ -369,4 +374,91 @@ public class SysTenantServiceImpl implements ISysTenantService {
}
return true;
}
/**
* 同步租户字典
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void syncTenantDict() {
// 查询超管 所有字典数据
List<SysDictType> dictTypeList = new ArrayList<>();
List<SysDictData> dictDataList = new ArrayList<>();
TenantHelper.ignore(() -> {
dictTypeList.addAll(dictTypeMapper.selectList());
dictDataList.addAll(dictDataMapper.selectList());
});
Map<String, List<SysDictType>> typeMap = StreamUtils.groupByKey(dictTypeList, TenantEntity::getTenantId);
Map<String, Map<String, List<SysDictData>>> typeDataMap = StreamUtils.groupBy2Key(
dictDataList, TenantEntity::getTenantId, SysDictData::getDictType);
// 管理租户字典数据
List<SysDictType> defaultTypeMap = typeMap.get(TenantConstants.DEFAULT_TENANT_ID);
Map<String, List<SysDictData>> defaultTypeDataMap = typeDataMap.get(TenantConstants.DEFAULT_TENANT_ID);
// 获取所有租户编号
List<String> tenantIds = baseMapper.selectObjs(
new LambdaQueryWrapper<SysTenant>().select(SysTenant::getTenantId)
.eq(SysTenant::getStatus, TenantConstants.NORMAL), x -> {return Convert.toStr(x);});
List<SysDictType> saveTypeList = new ArrayList<>();
List<SysDictData> saveDataList = new ArrayList<>();
Set<String> set = new HashSet<>();
for (String tenantId : tenantIds) {
if (TenantConstants.DEFAULT_TENANT_ID.equals(tenantId)) {
continue;
}
for (SysDictType dictType : defaultTypeMap) {
List<String> typeList = StreamUtils.toList(typeMap.get(tenantId), SysDictType::getDictType);
List<SysDictData> dataList = defaultTypeDataMap.get(dictType.getDictType());
if (typeList.contains(dictType.getDictType())) {
List<SysDictData> dataListTenant = typeDataMap.get(tenantId).get(dictType.getDictType());
Map<String, SysDictData> map = StreamUtils.toIdentityMap(dataListTenant, SysDictData::getDictValue);
for (SysDictData dictData : dataList) {
if (!map.containsKey(dictData.getDictValue())) {
SysDictData data = BeanUtil.toBean(dictData, SysDictData.class);
// 设置字典编码为 null
data.setDictCode(null);
data.setTenantId(tenantId);
data.setCreateTime(null);
data.setUpdateTime(null);
set.add(tenantId);
saveDataList.add(data);
}
}
} else {
SysDictType type = BeanUtil.toBean(dictType, SysDictType.class);
type.setDictId(null);
type.setTenantId(tenantId);
type.setCreateTime(null);
type.setUpdateTime(null);
set.add(tenantId);
saveTypeList.add(type);
if (CollUtil.isNotEmpty(dataList)) {
// 筛选出 dictType 对应的 data
for (SysDictData dictData : dataList) {
SysDictData data = BeanUtil.toBean(dictData, SysDictData.class);
// 设置字典编码为 null
data.setDictCode(null);
data.setTenantId(tenantId);
data.setCreateTime(null);
data.setUpdateTime(null);
set.add(tenantId);
saveDataList.add(data);
}
}
}
}
}
TenantHelper.ignore(() -> {
if (CollUtil.isNotEmpty(saveTypeList)) {
dictTypeMapper.insertBatch(saveTypeList);
}
if (CollUtil.isNotEmpty(saveDataList)) {
dictDataMapper.insertBatch(saveDataList);
}
});
for (String tenantId : set) {
TenantHelper.dynamic(tenantId, () -> CacheUtils.clear(CacheNames.SYS_DICT));
}
}
}

View File

@@ -42,6 +42,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 用户 业务层处理
@@ -628,6 +629,12 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
return ObjectUtil.isNull(sysUser) ? null : sysUser.getEmail();
}
/**
* 通过用户ID查询用户列表
*
* @param userIds 用户ids
* @return 用户列表
*/
@Override
public List<UserDTO> selectListByIds(List<Long> userIds) {
if (CollUtil.isEmpty(userIds)) {
@@ -636,15 +643,63 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
List<SysUserVo> list = baseMapper.selectVoList(new LambdaQueryWrapper<SysUser>()
.select(SysUser::getUserId, SysUser::getUserName, SysUser::getNickName, SysUser::getEmail, SysUser::getPhonenumber)
.eq(SysUser::getStatus, UserConstants.USER_NORMAL)
.in(CollUtil.isNotEmpty(userIds), SysUser::getUserId, userIds));
.in(SysUser::getUserId, userIds));
return BeanUtil.copyToList(list, UserDTO.class);
}
/**
* 通过角色ID查询用户ID
*
* @param roleIds 角色ids
* @return 用户ids
*/
@Override
public List<Long> selectUserIdsByRoleIds(List<Long> roleIds) {
if (CollUtil.isEmpty(roleIds)) {
return List.of();
}
List<SysUserRole> userRoles = userRoleMapper.selectList(
new LambdaQueryWrapper<SysUserRole>().in(SysUserRole::getRoleId, roleIds));
return StreamUtils.toList(userRoles, SysUserRole::getUserId);
}
/**
* 通过角色ID查询用户
*
* @param roleIds 角色ids
* @return 用户
*/
@Override
public List<UserDTO> selectUsersByRoleIds(List<Long> roleIds) {
if (CollUtil.isEmpty(roleIds)) {
return List.of();
}
// 通过角色ID获取用户角色信息
List<SysUserRole> userRoles = userRoleMapper.selectList(
new LambdaQueryWrapper<SysUserRole>().in(SysUserRole::getRoleId, roleIds));
// 获取用户ID列表
Set<Long> userIds = StreamUtils.toSet(userRoles, SysUserRole::getUserId);
return selectListByIds(new ArrayList<>(userIds));
}
/**
* 通过部门ID查询用户
*
* @param deptIds 部门ids
* @return 用户
*/
@Override
public List<UserDTO> selectUsersByDeptIds(List<Long> deptIds) {
if (CollUtil.isEmpty(deptIds)) {
return List.of();
}
List<SysUserVo> list = baseMapper.selectVoList(new LambdaQueryWrapper<SysUser>()
.select(SysUser::getUserId, SysUser::getUserName, SysUser::getNickName, SysUser::getEmail, SysUser::getPhonenumber)
.eq(SysUser::getStatus, UserConstants.USER_NORMAL)
.in(SysUser::getDeptId, deptIds));
return BeanUtil.copyToList(list, UserDTO.class);
}
}

View File

@@ -57,7 +57,7 @@
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>batik-all</artifactId>
<version>1.10</version>
<version>1.17</version>
<exclusions>
<exclusion>
<groupId>xalan</groupId>

View File

@@ -277,6 +277,7 @@ public class ActTaskServiceImpl implements IActTaskService {
if (StringUtils.isNotBlank(taskBo.getProcessDefinitionKey())) {
queryWrapper.eq("t.processDefinitionKey", taskBo.getProcessDefinitionKey());
}
queryWrapper.orderByDesc("t.CREATE_TIME_");
Page<TaskVo> page = actTaskMapper.getTaskWaitByPage(pageQuery.build(), queryWrapper);
List<TaskVo> taskList = page.getRecords();
@@ -366,6 +367,7 @@ public class ActTaskServiceImpl implements IActTaskService {
queryWrapper.like(StringUtils.isNotBlank(taskBo.getProcessDefinitionName()), "t.processDefinitionName", taskBo.getProcessDefinitionName());
queryWrapper.eq(StringUtils.isNotBlank(taskBo.getProcessDefinitionKey()), "t.processDefinitionKey", taskBo.getProcessDefinitionKey());
queryWrapper.eq("t.assignee_", userId);
queryWrapper.orderByDesc("t.START_TIME_");
Page<TaskVo> page = actTaskMapper.getTaskFinishByPage(pageQuery.build(), queryWrapper);
List<TaskVo> taskList = page.getRecords();
@@ -402,6 +404,7 @@ public class ActTaskServiceImpl implements IActTaskService {
queryWrapper.eq("t.processDefinitionKey", taskBo.getProcessDefinitionKey());
}
queryWrapper.eq("t.assignee_", userId);
queryWrapper.orderByDesc("t.START_TIME_");
Page<TaskVo> page = actTaskMapper.getTaskCopyByPage(pageQuery.build(), queryWrapper);
List<TaskVo> taskList = page.getRecords();

View File

@@ -142,11 +142,16 @@ public class TestLeaveServiceImpl implements ITestLeaveService {
*
* @param processTaskEvent 参数
*/
@EventListener(condition = "#processTaskEvent.key=='leave1' && #processTaskEvent.taskDefinitionKey=='Activity_14633hx'")
@EventListener(condition = "#processTaskEvent.key.startsWith('leave')")
public void processTaskHandler(ProcessTaskEvent processTaskEvent) {
log.info("当前任务执行了{}", processTaskEvent.toString());
TestLeave testLeave = baseMapper.selectById(Long.valueOf(processTaskEvent.getBusinessKey()));
testLeave.setStatus(BusinessStatusEnum.WAITING.getStatus());
baseMapper.updateById(testLeave);
// 所有demo案例的申请人节点id
String[] ids = {"Activity_14633hx", "Activity_19b1i4j", "Activity_0uscrk3",
"Activity_0uscrk3", "Activity_0x6b71j", "Activity_0zy3g6j", "Activity_06a55t0"};
if (StringUtils.equalsAny(processTaskEvent.getTaskDefinitionKey(), ids)) {
log.info("当前任务执行了{}", processTaskEvent.toString());
TestLeave testLeave = baseMapper.selectById(Long.valueOf(processTaskEvent.getBusinessKey()));
testLeave.setStatus(BusinessStatusEnum.WAITING.getStatus());
baseMapper.updateById(testLeave);
}
}
}

View File

@@ -96,7 +96,7 @@ public class WfDefinitionConfigServiceImpl implements IWfDefinitionConfigService
baseMapper.delete(new LambdaQueryWrapper<WfDefinitionConfig>().eq(WfDefinitionConfig::getTableName, bo.getTableName()));
add.setTableName(add.getTableName().toLowerCase());
boolean flag = baseMapper.insertOrUpdate(add);
if (baseMapper.insertOrUpdate(add)) {
if (flag) {
bo.setId(add.getId());
}
return flag;

View File

@@ -41,7 +41,7 @@ public class WfFormManageServiceImpl implements IWfFormManageService {
@Override
public List<WfFormManageVo> queryByIds(List<Long> ids) {
return baseMapper.selectVoBatchIds(ids);
return baseMapper.selectVoByIds(ids);
}
/**

View File

@@ -7,7 +7,7 @@ import org.dromara.workflow.domain.ActHiProcinst;
import org.dromara.workflow.service.IActHiProcinstService;
import org.dromara.workflow.service.IActProcessInstanceService;
import org.dromara.workflow.utils.WorkflowUtils;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -24,9 +24,9 @@ import java.util.Map;
public class WorkflowServiceImpl implements WorkflowService {
@Autowired(required = false)
private RuntimeService runtimeService;
private final IActProcessInstanceService iActProcessInstanceService;
private final IActHiProcinstService iActHiProcinstService;
private TaskService taskService;
private final IActProcessInstanceService actProcessInstanceService;
private final IActHiProcinstService actHiProcinstService;
/**
* 运行中的实例 删除程实例,删除历史记录,删除业务与流程关联信息
*
@@ -35,7 +35,7 @@ public class WorkflowServiceImpl implements WorkflowService {
*/
@Override
public boolean deleteRunAndHisInstance(List<String> businessKeys) {
return iActProcessInstanceService.deleteRunAndHisInstance(businessKeys);
return actProcessInstanceService.deleteRunAndHisInstance(businessKeys);
}
/**
@@ -67,7 +67,7 @@ public class WorkflowServiceImpl implements WorkflowService {
*/
@Override
public void setVariable(String taskId, String variableName, Object value) {
runtimeService.setVariable(taskId, variableName, value);
taskService.setVariable(taskId, variableName, value);
}
/**
@@ -78,7 +78,7 @@ public class WorkflowServiceImpl implements WorkflowService {
*/
@Override
public void setVariables(String taskId, Map<String, Object> variables) {
runtimeService.setVariables(taskId, variables);
taskService.setVariables(taskId, variables);
}
/**
@@ -90,7 +90,7 @@ public class WorkflowServiceImpl implements WorkflowService {
*/
@Override
public void setVariableLocal(String taskId, String variableName, Object value) {
runtimeService.setVariableLocal(taskId, variableName, value);
taskService.setVariableLocal(taskId, variableName, value);
}
/**
@@ -101,7 +101,7 @@ public class WorkflowServiceImpl implements WorkflowService {
*/
@Override
public void setVariablesLocal(String taskId, Map<String, Object> variables) {
runtimeService.setVariablesLocal(taskId, variables);
taskService.setVariablesLocal(taskId, variables);
}
/**
@@ -112,7 +112,7 @@ public class WorkflowServiceImpl implements WorkflowService {
*/
@Override
public String getInstanceIdByBusinessKey(String businessKey) {
ActHiProcinst actHiProcinst = iActHiProcinstService.selectByBusinessKey(businessKey);
ActHiProcinst actHiProcinst = actHiProcinstService.selectByBusinessKey(businessKey);
if (actHiProcinst == null) {
return StrUtil.EMPTY;
}

View File

@@ -41,8 +41,7 @@
FROM ACT_RU_TASK RES
INNER JOIN ACT_HI_PROCINST AHP ON RES.PROC_INST_ID_ = AHP.PROC_INST_ID_
INNER JOIN ACT_RE_PROCDEF ARP ON ARP.ID_ = RES.PROC_DEF_ID_
WHERE RES.PARENT_TASK_ID_ IS NULL
ORDER BY RES.CREATE_TIME_ DESC) t ${ew.getCustomSqlSegment}
WHERE RES.PARENT_TASK_ID_ IS NULL) t ${ew.getCustomSqlSegment}
</select>
<select id="getTaskFinishByPage" resultMap="TaskVoResult">
@@ -57,7 +56,7 @@
INNER JOIN ACT_HI_PROCINST AHP ON HTI.PROC_INST_ID_ = AHP.PROC_INST_ID_
INNER JOIN ACT_RE_PROCDEF ARP ON ARP.ID_ = HTI.PROC_DEF_ID_
WHERE HTI.PARENT_TASK_ID_ IS NULL AND HTI.END_TIME_ IS NOT NULL
ORDER BY HTI.START_TIME_ DESC) t ${ew.getCustomSqlSegment}
) t ${ew.getCustomSqlSegment}
</select>
<select id="getTaskCopyByPage" resultMap="TaskVoResult">
@@ -73,6 +72,6 @@
INNER JOIN ACT_RE_PROCDEF ARP ON ARP.ID_ = AHT.PROC_DEF_ID_
WHERE AHT.PARENT_TASK_ID_ IS NOT NULL
and AHT.scope_type_ = 'copy'
ORDER BY AHT.START_TIME_ DESC) t ${ew.getCustomSqlSegment}
) t ${ew.getCustomSqlSegment}
</select>
</mapper>

View File

@@ -100,7 +100,7 @@ services:
network_mode: "host"
ruoyi-server1:
image: ruoyi/ruoyi-server:5.2.1
image: ruoyi/ruoyi-server:5.2.3
container_name: ruoyi-server1
environment:
# 时区上海
@@ -115,7 +115,7 @@ services:
network_mode: "host"
ruoyi-server2:
image: ruoyi/ruoyi-server:5.2.1
image: ruoyi/ruoyi-server:5.2.3
container_name: ruoyi-server2
environment:
# 时区上海
@@ -130,7 +130,7 @@ services:
network_mode: "host"
ruoyi-monitor-admin:
image: ruoyi/ruoyi-monitor-admin:5.2.1
image: ruoyi/ruoyi-monitor-admin:5.2.3
container_name: ruoyi-monitor-admin
environment:
# 时区上海
@@ -142,7 +142,7 @@ services:
network_mode: "host"
ruoyi-snailjob-server:
image: ruoyi/ruoyi-snailjob-server:5.2.1
image: ruoyi/ruoyi-snailjob-server:5.2.3
container_name: ruoyi-snailjob-server
environment:
# 时区上海

View File

@@ -74,6 +74,7 @@ COMMENT ON COLUMN sj_group_config.update_dt IS '修改时间';
COMMENT ON TABLE sj_group_config IS '组配置';
INSERT INTO sj_group_config (namespace_id, group_name, description, token, group_status, version, group_partition, id_generator_mode, init_scene, bucket_index, create_dt, update_dt) VALUES ('dev', 'ruoyi_group', '', 'SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT', 1, 1, 0, 1, 1, 4, sysdate, sysdate);
INSERT INTO sj_group_config (namespace_id, group_name, description, token, group_status, version, group_partition, id_generator_mode, init_scene, bucket_index, create_dt, update_dt) VALUES ('prod', 'ruoyi_group', '', 'SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT', 1, 1, 0, 1, 1, 4, sysdate, sysdate);
-- sj_notify_config
CREATE TABLE sj_notify_config

View File

@@ -68,6 +68,7 @@ COMMENT ON COLUMN sj_group_config.update_dt IS '修改时间';
COMMENT ON TABLE sj_group_config IS '组配置';
INSERT INTO sj_group_config VALUES (1, 'dev', 'ruoyi_group', '', 'SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT', 1, 1, 0, 1, 1, 4, now(), now());
INSERT INTO sj_group_config VALUES (2, 'prod', 'ruoyi_group', '', 'SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT', 1, 1, 0, 1, 1, 4, now(), now());
-- sj_notify_config
CREATE TABLE sj_notify_config

View File

@@ -427,7 +427,7 @@ comment on column sys_menu.remark is '备注';
-- ----------------------------
-- 一级菜单
insert into sys_menu values('1', '系统管理', '0', '1', 'system', null, '', '1', '0', 'M', '0', '0', '', 'system', 103, 1, now(), null, null, '系统管理目录');
insert into sys_menu values('6', '系统管理', '0', '2', 'tenant', null, '', '1', '0', 'M', '0', '0', '', 'chart', 103, 1, now(), null, null, '租户管理目录');
insert into sys_menu values('6', '租户管理', '0', '2', 'tenant', null, '', '1', '0', 'M', '0', '0', '', 'chart', 103, 1, now(), null, null, '租户管理目录');
insert into sys_menu values('2', '系统监控', '0', '3', 'monitor', null, '', '1', '0', 'M', '0', '0', '', 'monitor', 103, 1, now(), null, null, '系统监控目录');
insert into sys_menu values('3', '系统工具', '0', '4', 'tool', null, '', '1', '0', 'M', '0', '0', '', 'tool', 103, 1, now(), null, null, '系统工具目录');
insert into sys_menu values('4', 'PLUS官网', '0', '5', 'https://gitee.com/dromara/RuoYi-Vue-Plus', null, '', '0', '0', 'M', '0', '0', '', 'guide', 103, 1, now(), null, null, 'RuoYi-Vue-Plus官网地址');

View File

@@ -40,6 +40,7 @@ CREATE TABLE `sj_group_config`
DEFAULT CHARSET = utf8mb4 COMMENT ='组配置';
INSERT INTO `sj_group_config` VALUES (1, 'dev', 'ruoyi_group', '', 'SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT', 1, 1, 0, 1, 1, 4, now(), now());
INSERT INTO `sj_group_config` VALUES (2, 'prod', 'ruoyi_group', '', 'SJ_cKqBTPzCsWA3VyuCfFoccmuIEGXjr5KT', 1, 1, 0, 1, 1, 4, now(), now());
CREATE TABLE `sj_notify_config`
(

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