180 Commits

Author SHA1 Message Date
015b406001 !591 发布 5.2.3 正式版
Merge pull request !591 from 疯狂的狮子Li/dev
2024-10-25 03:09:23 +00:00
6c950c9569 🎀发布 5.2.3 正式版 2024-10-25 10:58:01 +08:00
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
d3ccc43d68 update 优化 postgres适配findInSet写法 提高查询效率 2024-10-24 14:47:58 +08:00
4c96440686 fix 修复 设置流程变量 代码使用错误问题 2024-10-23 17:38:21 +08:00
8d8d76364b fix 修复 xss过滤器 未过滤url参数问题 2024-10-21 13:59:39 +08:00
82af98c205 fix 修复 代码书写错误 2024-10-21 13:38:09 +08:00
fdbe8c2395 update 优化 过滤器初始化写法 2024-10-20 11:43:39 +08:00
59715b1e02 fix 修复 及其特殊场景下获取 StopWatch 为null问题 2024-10-20 00:23:33 +08:00
f2ec530065 fix 修复 字典同步 数据异常问题 2024-10-19 14:50:58 +08:00
ac89cb46f5 fix 修复重新生成租户ID未生效的问题 2024-10-17 22:38:12 +08:00
abfa995a32 update 优化 监听器兼容所有demo案例 2024-10-17 18:12:26 +08:00
51cfbef887 update 优化 操作日志记录DELETE请求参数 2024-10-17 17:34:33 +08:00
7171ed1508 update 优化 snailjob客户端ip配置说明 2024-10-17 17:25:02 +08:00
6727f3e6a4 fix 修复oss上传10秒超时,设置默认时间一分钟 2024-10-16 15:28:38 +08:00
81006c758a docs 自行开启云存储访问控制ACl策略注释 2024-10-16 10:47:26 +08:00
01025c4024 update 更新AWS SDK版本 2024-10-16 10:39:58 +08:00
517277132e fix 修复腾讯云oss不支持高危权限设置ACL 2024-10-16 10:36:43 +08:00
dc5c92ee2d fix 修复同步云厂商要求明确配置访问样式(路径样式访问) 2024-10-16 10:33:11 +08:00
57e142e160 fix 修复 特性情况下自定义验证异常处理器报null问题 2024-10-15 17:35:51 +08:00
c563afed59 fix 修复 EncryptorManager 缓存失效问题导致的内存膨胀 2024-10-14 00:38:07 +08:00
556cf87f0e update 优化 补全 pg 数据类型 2024-10-12 16:23:17 +08:00
e0a00cfe98 update 优化 补全 @Override 注解 2024-10-08 16:30:04 +08:00
d60774a7b8 reset 回滚错误提交 2024-10-08 16:10:27 +08:00
e3d40b75cb fix 修复 同一个用户不同token连接不同服务导致发送不到问题(改为全局发送) 2024-10-08 14:43:59 +08:00
eb4479e940 fix 修复 同步字典存储是未忽略租户 2024-10-01 10:52:07 +08:00
7e6d0a5c64 update anyline 20240808 => 20240930 2024-09-30 17:56:04 +08:00
88dd4165cb update sms4j 3.3.2 => 3.3.3 2024-09-29 17:41:36 +08:00
e96118c574 fix 修复 部分web异常被CryptoFilter截胡问题 2024-09-29 17:41:16 +08:00
39c4e5fe55 !589 fix postgres数据库脚本租户管理名称错误
Merge pull request !589 from LionZzzi/fix
2024-09-25 02:14:52 +00:00
46141dc114 fix(postgres数据库脚本): 变更租户管理目录的主菜单为租户管理 2024-09-25 10:00:33 +08:00
e3a25f2425 fix 修复 代码生成器 postgres 数据库主键类型映射错误问题 2024-09-24 18:17:49 +08:00
64289c16f3 update 优化 适配mp新版本 方法名改动 2024-09-20 15:43:56 +08:00
6b05ddb576 update 优化 redis操作 如果无法忽略租户id则全局处理 2024-09-19 23:13:00 +08:00
88ee252fce update springboot 3.2.9 => 3.2.10 2024-09-19 18:09:03 +08:00
2add7291ab update 优化 sse 异常单独处理 避免出现异常报错问题 2024-09-19 17:53:41 +08:00
501be029c6 update 优化 sse 异常单独处理 避免出现异常报错问题 2024-09-19 17:40:42 +08:00
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
272ca613ee update mybatis-plus 3.5.7 => 3.5.8 代码适配更改 2024-09-19 13:45:57 +08:00
74af811a3b update 优化 删除掉有问题的方法(使用RedisUtils) 2024-09-18 18:07:28 +08:00
fc72b67090 update 优化 全局开启xss过滤 提高安全性 与cloud版本保持一致 2024-09-13 18:02:44 +08:00
e33f76d710 update 优化 去除返回前端的异常信息里包含html标签问题 2024-09-13 17:42:21 +08:00
1c3d594947 docs 查询表名列表增加注释 2024-09-13 15:00:16 +08:00
64d9b27310 update 优化 判断当前会话是否已经登录 2024-09-13 14:41:01 +08:00
f3f3593cfe update 优化 删除不应该set的属性 2024-09-13 09:38:56 +08:00
e5e8e3ce7c update 优化 租户状态更改接口严谨性 2024-09-12 16:58:47 +08:00
a7fd7ba72c fix 修复 临时处理 scala库版本漏洞问题 2024-09-11 16:00:39 +08:00
020f090f4a update 优化 删除okhttp无用版本限制(spring已经限制过了) 2024-09-11 15:59:54 +08:00
5e3231d59b !582 优化admin配置文件读取
* update 优化admin监控配置读取名称
* update 优化admin监控配置读取
2024-09-11 04:02:16 +00:00
b522bc015d update 优化 操作日志查询代码 2024-09-11 10:23:16 +08:00
6e64fd7fd7 !585 修复工作流分页查询不兼容sql server的问题
Merge pull request !585 from 赖小麦/fix_workflow_page_query_error_to_sqlserver
2024-09-11 01:27:20 +00:00
8b44f5cdbc 修复:工作流中(我的待办、我的已办、我的抄送默认)的分页查询语句不兼容sqlserver的问题 2024-09-10 12:05:08 +08:00
f9b7d955aa add 新增TreeUtil获取节点列表中所有节点的叶子节点 2024-09-10 10:15:01 +08:00
6ea2a2fc51 update 优化 统一sql文件命名方式 2024-09-09 11:37:02 +08:00
760c8d7200 update 优化 提供生产环境默认组配置 2024-09-09 11:30:40 +08:00
32ad28c3dc update 优化通过角色ID查询用户逻辑 2024-09-06 14:24:06 +08:00
6886e9fd5b update 优化查询用户时多余重复判断以及去重 2024-09-06 14:14:15 +08:00
f20130d3db fix 修复 commons-io 依赖冲突问题 2024-09-05 18:50:28 +08:00
df9cc881f1 update 优化 连接SSE token过期导致的 Servlet异常 2024-09-05 18:50:00 +08:00
4044988afa update 优化 代码生成菜单id匹配写法 2024-09-04 16:16:53 +08:00
d3360e81b9 update sa-token 1.38.0 => 1.39.0 2024-09-03 13:52:50 +08:00
df070b7d78 update 调整 xml 格式 2024-09-02 14:16:40 +08:00
83dd98faf3 fix 修复 开启子部门 父部门未关联开启问题 2024-09-02 14:12:02 +08:00
37f89f560f update 优化 更新sql关键字 2024-09-02 13:06:31 +08:00
918ed0d6d0 update 优化 删除多余的引号 2024-08-31 23:30:50 +08:00
a3c9edde78 fix 修复 升级依赖导致的依赖冲突 2024-08-29 20:53:39 +08:00
cac0a4cd16 update 优化 RegexUtils#extractFromString 方法未匹配返回null不返回默认值问题 2024-08-29 14:12:07 +08:00
581b6e03d5 update 优化 oss上传直接从请求头获取文件类型 2024-08-29 10:49:41 +08:00
801cc584e5 update 优化 代码生成表名判断 使用开头判断避免误判 2024-08-29 10:41:45 +08:00
b82ff8617e add 增加 同步租户字典功能 2024-08-26 17:56:19 +08:00
c87016c1af update 优化 excel导入 适配异常结构 2024-08-26 15:38:48 +08:00
098d3347a0 !577 发布 5.2.2 正式版 安全性提升
Merge pull request !577 from 疯狂的狮子Li/dev
2024-08-26 03:43:59 +00:00
463048e017 😴发布 5.2.2 正式版 安全性提升 2024-08-26 11:39:22 +08:00
79aee1d312 add 添加按照部门id,角色id查询用户 2024-08-24 11:16:39 +08:00
eb038e91dd update springboot 3.2.8 => 3.2.9 2024-08-23 10:52:53 +08:00
308c22f922 fix 修复 三方登录构建去除无用代码 2024-08-23 10:30:29 +08:00
457e59e61c fix 修复 多线程对同一个session发送ws消息报错问题 2024-08-22 17:53:55 +08:00
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
0af532f4f1 update 优化 去除日志部署环境判断 通过日志级别控制 2024-08-22 11:19:13 +08:00
4743eb1d3b update 优化 忽略租户与忽略数据权限支持嵌套使用 2024-08-21 13:54:40 +08:00
23aafe1cfe update 优化 忽略租户与忽略数据权限支持嵌套使用 2024-08-21 13:54:01 +08:00
3f2499feac update 优化 租户相关controller 增加租户开关配置控制是否注册 2024-08-19 09:14:50 +08:00
fb97649151 remove 移除 alibaba ttl 与线程池搭配有问题(可传递但无法清除与更新) 2024-08-17 10:30:03 +08:00
b33b645ef0 remove 移除 alibaba ttl 与线程池搭配有问题(可传递但无法清除与更新) 2024-08-17 10:29:33 +08:00
9318f182b0 reset 回滚 修改spring源码上下文持有者(存在数据未清理内存泄漏问题) 2024-08-17 10:10:51 +08:00
facbb7f28f update 优化 个人中心编辑 忽略数据权限 2024-08-15 20:28:52 +08:00
4de45ce170 update 优化 兼容部分用户不想给用户分配角色与部门的场景 2024-08-15 19:52:03 +08:00
96d57bd263 update 优化 租户套餐重名校验 2024-08-15 19:34:02 +08:00
bb4587fe05 update 优化 部门下存在岗位不允许删除 2024-08-15 19:29:04 +08:00
19c83f02aa update 优化 角色编辑状态未校验问题 2024-08-15 19:14:51 +08:00
0e1fcbfe9c update 优化 用户脱敏增加编辑权限标识符 2024-08-15 18:52:01 +08:00
eda882433a update 优化 删除报警注释 2024-08-15 17:45:06 +08:00
e6847605cc update 优化 删除无用注释 2024-08-15 17:43:57 +08:00
5bdffdb368 update 优化 代码生成器 自动适配oss翻译 2024-08-15 14:10:14 +08:00
0ad52b18b8 update 优化 代码生成器 自动适配oss翻译 2024-08-15 14:09:20 +08:00
77f44574c0 update 优化 修改spring源码上下文持有者 支持线程切换传递上下文数据 支持一切异步获取用户信息等操作 2024-08-15 11:53:15 +08:00
ca06a2311d update hutool 5.8.29 => 5.8.31 解决hutool不兼容jakarta问题 2024-08-13 13:32:34 +08:00
b8d9af65e2 update 增加sse注释说明 2024-08-09 17:24:44 +08:00
bc2b4876b6 fix 修复代码生成集合类型工具判断 2024-08-09 14:08:19 +08:00
23b70ca0be update anyline 8.7.2-20240808 2024-08-09 11:54:47 +08:00
44d776a76f update sms4j 3.2.1 => 3.3.2 2024-08-09 10:38:43 +08:00
f03c00b2c1 reset 回滚 sms4j 版本升级(有问题) 2024-08-07 22:27:05 +08:00
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
31569646b0 fix 修复 依赖漏洞 限制部分依赖版本 2024-08-07 11:20:34 +08:00
3fc37d6362 update easyexcel 3.3.4 => 4.0.2 2024-08-07 11:19:16 +08:00
6d28072167 update 优化 临时升级 undertow 版本 解决虚拟线程溢出问题 2024-08-07 09:58:11 +08:00
f124fbd6aa fix 修复 关闭应用sse销毁报错问题 2024-08-07 09:57:20 +08:00
88a4a51956 update 默认开启工作流 2024-08-06 16:10:11 +08:00
4306ea4181 update 优化 支持通过配置文件关闭工作流 2024-08-06 16:07:22 +08:00
e19140462d fix 修复 关闭 sse 报错问题 2024-08-06 15:38:16 +08:00
20cc8a6d6c fix 修复 excel 基于其他字段 合并错误问题 2024-08-06 13:53:40 +08:00
a9d7a42c65 update 优化 增加mp填充器兜底策略 2024-08-04 22:58:17 +08:00
f51e6d81b1 update 优化 TenantSpringCacheManager 处理逻辑 2024-08-04 10:45:25 +08:00
f119d082cf fix 修复 一级缓存key未区分租户问题 2024-08-04 10:40:22 +08:00
ecfaa9ad5c update 优化 角色权限判断 2024-08-02 17:09:46 +08:00
f32d0266ee update 更新 readme 2024-08-02 11:37:21 +08:00
7393a61305 fix 修复 id字符串格式转换错误问题 2024-08-02 10:36:34 +08:00
2b0efd1f93 update 优化 增加删除标志位常量优化查询代码 2024-08-02 10:07:47 +08:00
b615a3b088 fix 修复 id字符串格式转换错误问题 2024-08-02 10:03:17 +08:00
85403e975f fix 修复 登出无法正确删除对应的租户数据问题 2024-08-02 00:55:42 +08:00
615ad918ca update 优化 sse 拦截网络中断io异常 2024-08-02 00:55:11 +08:00
b886f3a04b fix 修复 登录错误锁定不区分租户问题 2024-08-01 23:20:29 +08:00
588a47897a fix 修复 转换模型缺少分类字段 2024-08-01 15:15:54 +08:00
2869d590e6 update 优化 sse 关闭连接各种异常问题 2024-08-01 14:55:43 +08:00
6b14bce25e update 优化 监控使用独立web依赖 2024-07-31 17:09:16 +08:00
5aa346327f fix 修复 代码生成 错误匹配表名问题 2024-07-31 13:02:33 +08:00
fcf8516f0d update 优化 适配 anyline 新改动 2024-07-31 09:48:05 +08:00
2a340d4d83 update anyline 8.7.2-20240728 2024-07-29 17:47:01 +08:00
508d7a37e3 update 脱敏策略优化增加密码 2024-07-29 15:18:23 +08:00
08fece39d8 add 新增 更多脱敏策略 2024-07-29 15:04:54 +08:00
857a0b1006 update 优化oss查询代码 2024-07-29 14:33:54 +08:00
239d59c864 update 优化 sse发送消息 增加token有效期判断 2024-07-29 12:44:45 +08:00
7297053dd6 fix 修复 登出后重新登录 sse推送报错问题 2024-07-29 12:28:01 +08:00
bd872f624a fix 修复 代码生成 数据源切换问题 2024-07-27 23:57:13 +08:00
86acb14f05 update anyline 8.7.2-20240726 2024-07-27 15:09:33 +08:00
9825f349ac fix 修复 代码生成 表结构缓存问题 2024-07-27 14:22:57 +08:00
19fd562c24 update snailjob 1.1.0 => 1.1.1 2024-07-27 14:20:20 +08:00
b6d939a9ff fix 修复 代码生成 表结构缓存问题 2024-07-27 14:00:50 +08:00
1619edb8a1 update 优化 sse 自动装配 2024-07-27 14:00:35 +08:00
782821aeb2 update 优化 设置nginx sse相关代理参数 2024-07-26 18:11:36 +08:00
51498958fa fix 修复 权限标识符处理未设置成功状态问题 2024-07-26 17:17:18 +08:00
51edb74474 fix 修复 后端发消息发送失败无限重试问题 2024-07-26 17:14:23 +08:00
d5ab2a7557 update 优化 调整默认推送使用SSE 2024-07-26 16:24:26 +08:00
ee3525cfb2 add 增加 ruoyi-common-sse 模块 支持SSE推送 比ws更轻量更稳定的推送 2024-07-26 16:05:35 +08:00
e9bd0858e2 update 优化 删除无用配置 2024-07-25 13:21:32 +08:00
f46d881866 add 增加 snailjob 健康检查 actuator 账号密码认证 2024-07-25 13:12:58 +08:00
ee5e718f83 !567 update 优化 Monitor监控服务通知分类打印
Merge pull request !567 from AprilWind/dev
2024-07-25 03:10:35 +00:00
e25083aea4 del 删除无用依赖 2024-07-25 10:55:46 +08:00
e74f0ca6f8 update 优化Monitor监控服务信息事件通知 2024-07-25 10:37:27 +08:00
52b0fa9a54 update 优化Monitor监控服务通知分类(可扩展邮箱发送,短信发送) 2024-07-25 10:00:10 +08:00
105c007f03 add 增加 springboot actuator 账号密码认证 杜绝内外网信息泄漏问题 2024-07-24 18:56:40 +08:00
0a3d5fd5d4 update 优化 代码生成分页实现 避免数据误传等问题 2024-07-23 17:05:46 +08:00
ae3c02d4b2 update 优化 代码生成分页实现 避免数据误传等问题 2024-07-23 17:05:20 +08:00
9e17d07a17 update 优化 限流注解 又写key又不是表达式的情况 2024-07-23 16:34:53 +08:00
dcfab4e011 update 优化 代码生成 相关sql 2024-07-23 13:21:15 +08:00
0b78f9361d fix 修复 已经导入的表 未过滤问题 2024-07-23 11:44:39 +08:00
0c4e9dc813 update anyline 8.7.2-20240722 2024-07-23 10:37:15 +08:00
d894cae073 update 优化 代码生成屏蔽无用表 2024-07-23 10:35:22 +08:00
84f553a911 fix 修复 代码生成 报错与警告 2024-07-23 10:06:15 +08:00
05580deaa9 fix 修复 无法导入 bpmn 类型文件问题 2024-07-23 09:58:18 +08:00
aac83bbb91 update 获取表元数据 字段是否必填 和 是否自增 2024-07-22 17:06:46 +08:00
249f1f48a6 reset 回滚错误提交 2024-07-19 17:55:13 +08:00
bfb92fe667 update 优化 依赖配置 2024-07-19 17:54:44 +08:00
640dc43bbe !565 流程发送消息未查询用户邮箱和手机号
Merge pull request !565 from 愿丶/dev-pr
2024-07-19 09:43:47 +00:00
8859d915b0 update workflowUtils查询用户信息发送消息未查询邮件和手机号 2024-07-19 17:39:19 +08:00
82fdb37c6b fix 修复判断表名为空错误 2024-07-19 16:50:40 +08:00
49c18dab63 update 优化根据表名称查询列信息 2024-07-19 16:44:12 +08:00
f5b8a22bde update 更改D-ORM依赖为全依赖 2024-07-19 15:55:31 +08:00
b6b0f9c47d update 优化代码生成表名查询数据库 2024-07-19 15:54:37 +08:00
a2a2fa2311 update 优化查询数据库列表 2024-07-19 15:29:38 +08:00
34690e3e65 update springboot 3.2.6 => 3.2.8
update springdoc 2.5.0 => 2.6.0
update hutool 5.8.27 => 5.8.29
update redisson 3.31.0 => 3.33.0
update flowable 7.0.0 => 7.0.1
2024-07-19 10:08:28 +08:00
54f58257f9 update 优化 注释掉其他数据库 jdbc 依赖 由用户手动添加 2024-07-16 11:04:48 +08:00
58b6c4668f update 优化 oracle snailjob 兼容低版本oracle索引名称长度限制 2024-07-16 09:42:50 +08:00
d0e7eb8409 !564 升级SnailJob版本到1.1.0
Merge pull request !564 from dhb52/dev
2024-07-15 14:49:55 +00:00
77a7a8f30e chore: 升级SnailJob版本到1.1.0 2024-07-15 18:16:43 +08:00
08d4493994 update 优化 bug 模板 2024-07-15 15:19:22 +08:00
f76738e02b update 优化 bug 模板 2024-07-15 15:18:29 +08:00
ab147df2f1 update 优化 数据权限支持通过菜单标识符获取数据所有权 2024-07-12 13:51:34 +08:00
5444ccc857 update 优化 数据权限支持自定义连接符 2024-07-12 13:15:23 +08:00
fc89d62f1a update 优化 TestDemo 删除前校验数据权限 2024-07-12 12:57:45 +08:00
149 changed files with 2179 additions and 2325 deletions

View File

@ -1,49 +0,0 @@
### 使用版本(未按照模板填写直接删除)
- jdk版本(带上尾号): 例如 1.8.0_202
- 框架版本(项目启动时输出的版本号): 例如 4.4.0
- 其他依赖版本(你觉得有必要的):
### 问题前提
> 功能不好用 不会用 是否已经看过项目文档
> 项目运行报错 是否已经拿着报错信息去百度 常见报错百度百度足以
> 是否搜索过其他issue 一些已经解决的问题 会在issue内留下解决方法
> 无法线上解决或者与框架无关的问题的欢迎加VIP群跟作者一对一谈
### 异常模块
> 此报错都涉及到那些系统模块
例如 ruoyi-system ruoyi-auth 等等
### 问题描述
> 越详细越容易直击问题所在
已知: XXX功能不好用 或 XXX数据不正常 等等
### 希望结果
> 想知道你觉得怎么样是正常或者合理的
希望功能可以有XXX结果 或者 XXX现象
### 重现步骤
> 作者并不知道这个问题是如何出现的
- 1
- 2
- 3
### 相关代码与报错信息(请勿发混乱格式)
> 代码可按照如下形式提供或者截图均可 越详细越好
> 大多数问题都是 代码编写错误问题 逻辑问题 或者用法错误等问题
```java
public class XXX {
}
```

View File

@ -9,8 +9,9 @@ body:
label: 版本
description: 你当前正在使用我们软件的哪个版本(pom文件内的版本号)
value: |
jdk版本(带上尾号): 例如 17.0.8
框架版本(项目启动时输出的版本号): 例如 5.1.1
注意: 未填写版本号不予处理直接关闭或删除
jdk版本(带上尾号):
框架版本(项目启动时输出的版本号):
其他依赖版本(你觉得有必要的):
validations:
required: true

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)]()
@ -61,6 +61,7 @@ CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
| 数据库连接池 | 采用 HikariCP Spring官方内置连接池 配置简单 以性能与稳定性闻名天下 | 采用 druid bug众多 社区维护差 活跃度低 配置众多繁琐性能一般 |
| 数据库主键 | 采用 雪花ID 基于时间戳的 有序增长 唯一ID 再也不用为分库分表 数据合并主键冲突重复而发愁 | 采用 数据库自增ID 支持数据量有限 不支持多数据源主键唯一 |
| WebSocket协议 | 基于 Spring 封装的 WebSocket 协议 扩展了Token鉴权与分布式会话同步 不再只是基于单机的废物 | 无 |
| SSE推送 | 采用 Spring SSE 实现 扩展了Token鉴权与分布式会话同步 | 无 |
| 序列化 | 采用 Jackson Spring官方内置序列化 靠谱!!! | 采用 fastjson bugjson 远近闻名 |
| 分布式幂等 | 参考美团GTIS防重系统简化实现(细节可看文档) | 手动编写注解基于aop实现 |
| 分布式锁 | 采用 Lock4j 底层基于 Redisson | 无 |
@ -72,6 +73,7 @@ CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
| 接口文档 | 采用 SpringDoc、javadoc 无注解零入侵基于java注释<br/>只需把注释写好 无需再写一大堆的文档注解了 | 采用 Springfox 已停止维护 需要编写大量的注解来支持文档生成 |
| 校验框架 | 采用 Validation 支持注解与工具类校验 注解支持国际化 | 仅支持注解 且注解不支持国际化 |
| Excel框架 | 采用 Alibaba EasyExcel 基于插件化<br/>框架对其增加了很多功能 例如 自动合并相同内容 自动排列布局 字典翻译等 | 基于 POI 手写实现 功能有限 复杂 扩展性差 |
| 工作流支持 | 支持各种复杂审批 转办 委派 加减签 会签 或签 票签 等功能 | 无 |
| 工具类框架 | 采用 Hutool、Lombok 上百种工具覆盖90%的使用需求 基于注解自动生成 get set 等简化框架大量代码 | 手写工具稳定性差易出问题 工具数量有限 代码臃肿需自己手写 get set 等 |
| 监控框架 | 采用 SpringBoot-Admin 基于SpringBoot官方 actuator 探针机制<br/>实时监控服务状态 框架还为其扩展了在线日志查看监控 | 无 |
| 链路追踪 | 采用 Apache SkyWalking 还在为请求不知道去哪了 到哪出了问题而烦恼吗<br/>用了它即可实时查看请求经过的每一处每一个节点 | 无 |

75
pom.xml
View File

@ -13,45 +13,44 @@
<description>RuoYi-Vue-Plus多租户管理系统</description>
<properties>
<revision>5.2.1</revision>
<spring-boot.version>3.2.6</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.5.0</springdoc.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.27</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.31.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.0.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>
<!-- 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-20241022</anyline.version>
<!--工作流配置-->
<flowable.version>7.0.0</flowable.version>
<flowable.version>7.0.1</flowable.version>
<!-- 插件版本 -->
<maven-jar-plugin.version>3.2.2</maven-jar-plugin.version>
@ -68,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>
@ -76,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>
<!-- 默认环境 -->
@ -87,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>
@ -155,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代码生成使用模板 -->
@ -240,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>
@ -307,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>
@ -333,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

@ -22,21 +22,28 @@
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- Oracle -->
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
</dependency>
<!-- PostgreSql -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<!-- SqlServer -->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
</dependency>
<!-- &lt;!&ndash; mp支持的数据库均支持 只需要增加对应的jdbc依赖即可 &ndash;&gt;-->
<!-- &lt;!&ndash; Oracle &ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>com.oracle.database.jdbc</groupId>-->
<!-- <artifactId>ojdbc8</artifactId>-->
<!-- </dependency>-->
<!-- &lt;!&ndash; 兼容oracle低版本 &ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>com.oracle.database.nls</groupId>-->
<!-- <artifactId>orai18n</artifactId>-->
<!-- </dependency>-->
<!-- &lt;!&ndash; PostgreSql &ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>org.postgresql</groupId>-->
<!-- <artifactId>postgresql</artifactId>-->
<!-- </dependency>-->
<!-- &lt;!&ndash; SqlServer &ndash;&gt;-->
<!-- <dependency>-->
<!-- <groupId>com.microsoft.sqlserver</groupId>-->
<!-- <artifactId>mssql-jdbc</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.dromara</groupId>

View File

@ -24,9 +24,9 @@ import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
import org.dromara.common.social.config.properties.SocialProperties;
import org.dromara.common.social.utils.SocialUtils;
import org.dromara.common.sse.dto.SseMessageDto;
import org.dromara.common.sse.utils.SseMessageUtils;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.common.websocket.dto.WebSocketMessageDto;
import org.dromara.common.websocket.utils.WebSocketUtils;
import org.dromara.system.domain.bo.SysTenantBo;
import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysTenantVo;
@ -102,11 +102,11 @@ public class AuthController {
Long userId = LoginHelper.getUserId();
scheduledExecutorService.schedule(() -> {
WebSocketMessageDto dto = new WebSocketMessageDto();
SseMessageDto dto = new SseMessageDto();
dto.setMessage("欢迎登录RuoYi-Vue-Plus后台管理系统");
dto.setSessionKeys(List.of(userId));
WebSocketUtils.publishMessage(dto);
}, 3, TimeUnit.SECONDS);
dto.setUserIds(List.of(userId));
SseMessageUtils.publishMessage(dto);
}, 5, TimeUnit.SECONDS);
return R.ok(loginVo);
}

View File

@ -3,6 +3,8 @@ package org.dromara.web.listener;
import cn.dev33.satoken.config.SaTokenConfig;
import cn.dev33.satoken.listener.SaTokenListener;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import lombok.RequiredArgsConstructor;
@ -81,7 +83,10 @@ public class UserActionListener implements SaTokenListener {
*/
@Override
public void doLogout(String loginType, Object loginId, String tokenValue) {
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY));
TenantHelper.dynamic(tenantId, () -> {
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
});
log.info("user doLogout, userId:{}, token:{}", loginId, tokenValue);
}
@ -90,7 +95,10 @@ public class UserActionListener implements SaTokenListener {
*/
@Override
public void doKickout(String loginType, Object loginId, String tokenValue) {
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY));
TenantHelper.dynamic(tenantId, () -> {
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
});
log.info("user doKickout, userId:{}, token:{}", loginId, tokenValue);
}
@ -99,7 +107,10 @@ public class UserActionListener implements SaTokenListener {
*/
@Override
public void doReplaced(String loginType, Object loginId, String tokenValue) {
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
String tenantId = Convert.toStr(StpUtil.getExtra(tokenValue, LoginHelper.TENANT_KEY));
TenantHelper.dynamic(tenantId, () -> {
RedisUtils.deleteObject(CacheConstants.ONLINE_TOKEN_KEY + tokenValue);
});
log.info("user doReplaced, userId:{}, token:{}", loginId, tokenValue);
}

View File

@ -4,13 +4,14 @@ import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Opt;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.lock.annotation.Lock4j;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthUser;
import org.dromara.common.core.constant.CacheConstants;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.constant.TenantConstants;
import org.dromara.common.core.domain.dto.RoleDTO;
import org.dromara.common.core.domain.model.LoginUser;
@ -155,16 +156,13 @@ public class SysLoginService {
loginUser.setUserType(user.getUserType());
loginUser.setMenuPermission(permissionService.getMenuPermission(user.getUserId()));
loginUser.setRolePermission(permissionService.getRolePermission(user.getUserId()));
TenantHelper.dynamic(user.getTenantId(), () -> {
SysDeptVo dept = null;
if (ObjectUtil.isNotNull(user.getDeptId())) {
dept = deptService.selectDeptById(user.getDeptId());
}
loginUser.setDeptName(ObjectUtil.isNull(dept) ? "" : dept.getDeptName());
loginUser.setDeptCategory(ObjectUtil.isNull(dept) ? "" : dept.getDeptCategory());
List<SysRoleVo> roles = roleService.selectRolesByUserId(user.getUserId());
loginUser.setRoles(BeanUtil.copyToList(roles, RoleDTO.class));
});
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;
}
@ -186,7 +184,7 @@ public class SysLoginService {
* 登录校验
*/
public void checkLogin(LoginType loginType, String tenantId, String username, Supplier<Boolean> supplier) {
String errorKey = GlobalConstants.PWD_ERR_CNT_KEY + username;
String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
String loginFail = Constants.LOGIN_FAIL;
// 获取用户登录错误次数默认为0 (可自定义限制策略 例如: key + username + ip)

View File

@ -21,7 +21,6 @@ import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.system.domain.SysClient;
import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysUserVo;
@ -51,13 +50,12 @@ public class EmailAuthStrategy implements IAuthStrategy {
String tenantId = loginBody.getTenantId();
String email = loginBody.getEmail();
String emailCode = loginBody.getEmailCode();
// 通过邮箱查找用户
SysUserVo user = loadUserByEmail(tenantId, email);
loginService.checkLogin(LoginType.EMAIL, tenantId, user.getUserName(), () -> !validateEmailCode(tenantId, email, emailCode));
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
LoginUser loginUser = loginService.buildLoginUser(user);
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
SysUserVo user = loadUserByEmail(email);
loginService.checkLogin(LoginType.EMAIL, tenantId, user.getUserName(), () -> !validateEmailCode(tenantId, email, emailCode));
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
return loginService.buildLoginUser(user);
});
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
SaLoginModel model = new SaLoginModel();
@ -89,18 +87,16 @@ public class EmailAuthStrategy implements IAuthStrategy {
return code.equals(emailCode);
}
private SysUserVo loadUserByEmail(String tenantId, String email) {
return TenantHelper.dynamic(tenantId, () -> {
SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getEmail, email));
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", email);
throw new UserException("user.not.exists", email);
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", email);
throw new UserException("user.blocked", email);
}
return user;
});
private SysUserVo loadUserByEmail(String email) {
SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getEmail, email));
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", email);
throw new UserException("user.not.exists", email);
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", email);
throw new UserException("user.blocked", email);
}
return user;
}
}

View File

@ -62,11 +62,12 @@ public class PasswordAuthStrategy implements IAuthStrategy {
if (captchaEnabled) {
validateCaptcha(tenantId, username, code, uuid);
}
SysUserVo user = loadUserByUsername(tenantId, username);
loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
// 此处可根据登录用户的数据不同 自行创建 loginUser
LoginUser loginUser = loginService.buildLoginUser(user);
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
SysUserVo user = loadUserByUsername(username);
loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
// 此处可根据登录用户的数据不同 自行创建 loginUser
return loginService.buildLoginUser(user);
});
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
SaLoginModel model = new SaLoginModel();
@ -107,18 +108,16 @@ public class PasswordAuthStrategy implements IAuthStrategy {
}
}
private SysUserVo loadUserByUsername(String tenantId, String username) {
return TenantHelper.dynamic(tenantId, () -> {
SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUserName, username));
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", username);
throw new UserException("user.not.exists", username);
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", username);
throw new UserException("user.blocked", username);
}
return user;
});
private SysUserVo loadUserByUsername(String username) {
SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUserName, username));
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", username);
throw new UserException("user.not.exists", username);
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", username);
throw new UserException("user.blocked", username);
}
return user;
}
}

View File

@ -21,7 +21,6 @@ import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.system.domain.SysClient;
import org.dromara.system.domain.SysUser;
import org.dromara.system.domain.vo.SysClientVo;
import org.dromara.system.domain.vo.SysUserVo;
@ -51,13 +50,12 @@ public class SmsAuthStrategy implements IAuthStrategy {
String tenantId = loginBody.getTenantId();
String phonenumber = loginBody.getPhonenumber();
String smsCode = loginBody.getSmsCode();
// 通过手机号查找用户
SysUserVo user = loadUserByPhonenumber(tenantId, phonenumber);
loginService.checkLogin(LoginType.SMS, tenantId, user.getUserName(), () -> !validateSmsCode(tenantId, phonenumber, smsCode));
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
LoginUser loginUser = loginService.buildLoginUser(user);
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
SysUserVo user = loadUserByPhonenumber(phonenumber);
loginService.checkLogin(LoginType.SMS, tenantId, user.getUserName(), () -> !validateSmsCode(tenantId, phonenumber, smsCode));
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
return loginService.buildLoginUser(user);
});
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
SaLoginModel model = new SaLoginModel();
@ -89,18 +87,16 @@ public class SmsAuthStrategy implements IAuthStrategy {
return code.equals(smsCode);
}
private SysUserVo loadUserByPhonenumber(String tenantId, String phonenumber) {
return TenantHelper.dynamic(tenantId, () -> {
SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getPhonenumber, phonenumber));
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", phonenumber);
throw new UserException("user.not.exists", phonenumber);
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", phonenumber);
throw new UserException("user.blocked", phonenumber);
}
return user;
});
private SysUserVo loadUserByPhonenumber(String phonenumber) {
SysUserVo user = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getPhonenumber, phonenumber));
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", phonenumber);
throw new UserException("user.not.exists", phonenumber);
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", phonenumber);
throw new UserException("user.blocked", phonenumber);
}
return user;
}
}

View File

@ -92,11 +92,11 @@ public class SocialAuthStrategy implements IAuthStrategy {
} else {
social = list.get(0);
}
// 查找用户
SysUserVo user = loadUser(social.getTenantId(), social.getUserId());
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
LoginUser loginUser = loginService.buildLoginUser(user);
LoginUser loginUser = TenantHelper.dynamic(social.getTenantId(), () -> {
SysUserVo user = loadUser(social.getUserId());
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
return loginService.buildLoginUser(user);
});
loginUser.setClientKey(client.getClientKey());
loginUser.setDeviceType(client.getDeviceType());
SaLoginModel model = new SaLoginModel();
@ -116,18 +116,16 @@ public class SocialAuthStrategy implements IAuthStrategy {
return loginVo;
}
private SysUserVo loadUser(String tenantId, Long userId) {
return TenantHelper.dynamic(tenantId, () -> {
SysUserVo user = userMapper.selectVoById(userId);
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", "");
throw new UserException("user.not.exists", "");
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", "");
throw new UserException("user.blocked", "");
}
return user;
});
private SysUserVo loadUser(Long userId) {
SysUserVo user = userMapper.selectVoById(userId);
if (ObjectUtil.isNull(user)) {
log.info("登录用户:{} 不存在.", "");
throw new UserException("user.not.exists", "");
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", "");
throw new UserException("user.blocked", "");
}
return user;
}
}

View File

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

View File

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

View File

@ -121,9 +121,6 @@ security:
# swagger 文档配置
- /*/api-docs
- /*/api-docs/**
# actuator 监控配置
- /actuator
- /actuator/**
# 多租户配置
tenant:
@ -226,9 +223,10 @@ xss:
# 过滤开关
enabled: true
# 排除链接(多个用逗号分隔)
excludes: /system/notice
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*
excludeUrls:
- /system/notice
- /workflow/model/save
- /workflow/model/editModelXml
# 全局线程池相关配置
# 如使用JDK21请直接使用虚拟线程 不要开启此配置
@ -259,10 +257,15 @@ management:
logfile:
external-file: ./logs/sys-console.log
--- # 默认/推荐使用sse推送
sse:
enabled: true
path: /resource/sse
--- # websocket
websocket:
# 如果关闭 需要和前端开关一起关闭
enabled: true
enabled: false
# 路径
path: /resource/websocket
# 设置访问源地址
@ -270,6 +273,10 @@ websocket:
--- #flowable配置
flowable:
# 开关 用于启动/停用工作流
enabled: true
process.enabled: ${flowable.enabled}
eventregistry.enabled: ${flowable.enabled}
async-executor-activate: false #关闭定时任务JOB
# 将databaseSchemaUpdate设置为true。当Flowable发现库与数据库表结构不一致时会自动将数据库表结构升级至新版本。
database-schema-update: true

View File

@ -33,6 +33,7 @@
<module>ruoyi-common-encrypt</module>
<module>ruoyi-common-tenant</module>
<module>ruoyi-common-websocket</module>
<module>ruoyi-common-sse</module>
</modules>
<artifactId>ruoyi-common</artifactId>

View File

@ -14,7 +14,7 @@
</description>
<properties>
<revision>5.2.1</revision>
<revision>5.2.3</revision>
</properties>
<dependencyManagement>
@ -172,6 +172,13 @@
<version>${revision}</version>
</dependency>
<!-- SSE模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-sse</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</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

@ -22,4 +22,9 @@ public interface CacheConstants {
*/
String SYS_DICT_KEY = "sys_dict:";
/**
* 登录账户密码错误次数 redis key
*/
String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
}

View File

@ -27,11 +27,6 @@ public interface GlobalConstants {
*/
String RATE_LIMIT_KEY = GLOBAL_REDIS_KEY + "rate_limit:";
/**
* 登录账户密码错误次数 redis key
*/
String PWD_ERR_CNT_KEY = GLOBAL_REDIS_KEY + "pwd_err_cnt:";
/**
* 三方认证 redis key
*/

View File

@ -67,6 +67,16 @@ public interface UserConstants {
*/
String DICT_NORMAL = "0";
/**
* 通用存在标志
*/
String DEL_FLAG_NORMAL = "0";
/**
* 通用删除标志
*/
String DEL_FLAG_REMOVED = "2";
/**
* 是否为系统默认(是)
*/

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

@ -107,7 +107,7 @@ public class CellMergeStrategy extends AbstractMergeStrategy implements Workbook
}
if (!cellValue.equals(val)) {
if ((i - repeatCell.getCurrent() > 1) && isMerge(list, i, field)) {
if ((i - repeatCell.getCurrent() > 1)) {
cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
}
map.put(field, new RepeatCell(val, i));
@ -115,6 +115,11 @@ public class CellMergeStrategy extends AbstractMergeStrategy implements Workbook
if (i > repeatCell.getCurrent() && isMerge(list, i, field)) {
cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
}
} else if (!isMerge(list, i, field)) {
if ((i - repeatCell.getCurrent() > 1)) {
cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
}
map.put(field, new RepeatCell(val, i));
}
}
}

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

@ -30,4 +30,11 @@ public @interface DataColumn {
*/
String[] value() default "dept_id";
/**
* 权限标识符 用于通过菜单权限标识符来获取数据权限
* 拥有此标识符的角色 将不会拼接此角色的数据过滤sql
*
* @return 权限标识符
*/
String permission() default "";
}

View File

@ -20,4 +20,11 @@ public @interface DataPermission {
*/
DataColumn[] value();
/**
* 权限拼接标识符(用于指定连接语句的sql符号)
* 如不填 默认 select 用 OR 其他语句用 AND
* 内容 OR 或者 AND
*/
String joinStr() default "";
}

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

@ -48,6 +48,10 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
? baseEntity.getCreateDept() : loginUser.getDeptId());
}
}
} else {
Date date = new Date();
this.strictInsertFill(metaObject, "createTime", Date.class, date);
this.strictInsertFill(metaObject, "updateTime", Date.class, date);
}
} catch (Exception e) {
throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);
@ -72,6 +76,8 @@ public class InjectionMetaObjectHandler implements MetaObjectHandler {
if (ObjectUtil.isNotNull(userId)) {
baseEntity.setUpdateBy(userId);
}
} else {
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
} catch (Exception e) {
throw new ServiceException("自动注入异常 => " + e.getMessage(), HttpStatus.HTTP_UNAUTHORIZED);

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;
@ -99,14 +99,14 @@ public class PlusDataPermissionHandler {
return where;
}
// 构造数据过滤条件的 SQL 片段
String dataFilterSql = buildDataFilter(dataPermission.value(), isSelect);
String dataFilterSql = buildDataFilter(dataPermission, isSelect);
if (StringUtils.isBlank(dataFilterSql)) {
return where;
}
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 {
@ -120,14 +120,17 @@ public class PlusDataPermissionHandler {
/**
* 构建数据过滤条件的 SQL 语句
*
* @param dataColumns 数据权限注解中的列信息
* @param isSelect 标志当前操作是否为查询操作,查询操作和更新或删除操作在处理过滤条件时会有不同的处理方式
* @param dataPermission 数据权限注解
* @param isSelect 标志当前操作是否为查询操作,查询操作和更新或删除操作在处理过滤条件时会有不同的处理方式
* @return 构建的数据过滤条件的 SQL 语句
* @throws ServiceException 如果角色的数据范围异常或者 key 与 value 的长度不匹配,则抛出 ServiceException 异常
*/
private String buildDataFilter(DataColumn[] dataColumns, boolean isSelect) {
private String buildDataFilter(DataPermission dataPermission, boolean isSelect) {
// 更新或删除需满足所有条件
String joinStr = isSelect ? " OR " : " AND ";
if (StringUtils.isNotBlank(dataPermission.joinStr())) {
joinStr = " " + dataPermission.joinStr() + " ";
}
LoginUser user = DataPermissionHelper.getVariable("user");
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(beanResolver);
@ -145,7 +148,7 @@ public class PlusDataPermissionHandler {
return "";
}
boolean isSuccess = false;
for (DataColumn dataColumn : dataColumns) {
for (DataColumn dataColumn : dataPermission.value()) {
if (dataColumn.key().length != dataColumn.value().length) {
throw new ServiceException("角色数据范围异常 => key与value长度不匹配");
}
@ -155,6 +158,13 @@ public class PlusDataPermissionHandler {
)) {
continue;
}
// 包含权限标识符 这直接跳过
if (StringUtils.isNotBlank(dataColumn.permission()) &&
CollUtil.contains(user.getMenuPermission(), dataColumn.permission())
) {
isSuccess = true;
continue;
}
// 设置注解变量 key 为表达式变量 value 为变量值
for (int i = 0; i < dataColumn.key().length; i++) {
context.setVariable(dataColumn.key()[i], dataColumn.value()[i]);

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

@ -80,11 +80,11 @@ public class RateLimiterAspect {
private String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
String key = rateLimiter.key();
if (StringUtils.isNotBlank(key)) {
// 判断 key 不为空 和 不是表达式
if (StringUtils.isNotBlank(key) && StringUtils.containsAny(key, "#")) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method targetMethod = signature.getMethod();
Object[] args = point.getArgs();
//noinspection DataFlowIssue
MethodBasedEvaluationContext context =
new MethodBasedEvaluationContext(null, targetMethod, args, pnd);
context.setBeanResolver(new BeanFactoryResolver(SpringUtils.getBeanFactory()));

View File

@ -15,15 +15,17 @@ public class CaffeineCacheDecorator implements Cache {
private static final com.github.benmanes.caffeine.cache.Cache<Object, Object>
CAFFEINE = SpringUtils.getBean("caffeine");
private final String name;
private final Cache cache;
public CaffeineCacheDecorator(Cache cache) {
public CaffeineCacheDecorator(String name, Cache cache) {
this.name = name;
this.cache = cache;
}
@Override
public String getName() {
return cache.getName();
return name;
}
@Override
@ -32,7 +34,7 @@ public class CaffeineCacheDecorator implements Cache {
}
public String getUniqueKey(Object key) {
return cache.getName() + ":" + key;
return name + ":" + key;
}
@Override
@ -42,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;
@ -53,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);
@ -63,6 +67,7 @@ public class CaffeineCacheDecorator implements Cache {
evictIfPresent(key);
}
@Override
public boolean evictIfPresent(Object key) {
boolean b = cache.evictIfPresent(key);
if (b) {
@ -76,6 +81,7 @@ public class CaffeineCacheDecorator implements Cache {
cache.clear();
}
@Override
public boolean invalidate() {
return cache.invalidate();
}

View File

@ -156,7 +156,7 @@ public class PlusSpringCacheManager implements CacheManager {
private Cache createMap(String name, CacheConfig config) {
RMap<Object, Object> map = RedisUtils.getClient().getMap(name);
Cache cache = new CaffeineCacheDecorator(new RedissonCache(map, allowNullValues));
Cache cache = new CaffeineCacheDecorator(name, new RedissonCache(map, allowNullValues));
if (transactionAware) {
cache = new TransactionAwareCacheDecorator(cache);
}
@ -170,7 +170,7 @@ public class PlusSpringCacheManager implements CacheManager {
private Cache createMapCache(String name, CacheConfig config) {
RMapCache<Object, Object> map = RedisUtils.getClient().getMapCache(name);
Cache cache = new CaffeineCacheDecorator(new RedissonCache(map, config, allowNullValues));
Cache cache = new CaffeineCacheDecorator(name, new RedissonCache(map, config, allowNullValues));
if (transactionAware) {
cache = new TransactionAwareCacheDecorator(cache);
}

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

@ -1,11 +1,17 @@
package org.dromara.common.security.config;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil;
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;
@ -14,6 +20,7 @@ import org.dromara.common.security.config.properties.SecurityProperties;
import org.dromara.common.security.handler.AllUrlHandler;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@ -45,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)) {
@ -71,4 +87,19 @@ public class SecurityConfig implements WebMvcConfigurer {
.excludePathPatterns(securityProperties.getExcludes());
}
/**
* 对 actuator 健康检查接口 做账号密码鉴权
*/
@Bean
public SaServletFilter getSaServletFilter() {
String username = SpringUtils.getProperty("spring.boot.admin.client.username");
String password = SpringUtils.getProperty("spring.boot.admin.client.password");
return new SaServletFilter()
.addInclude("/actuator", "/actuator/**")
.setAuth(obj -> {
SaHttpBasicUtil.check(username + ":" + password);
})
.setError(e -> SaResult.error(e.getMessage()).setCode(HttpStatus.UNAUTHORIZED));
}
}

View File

@ -37,7 +37,57 @@ public enum SensitiveStrategy {
/**
* 银行卡
*/
BANK_CARD(DesensitizedUtil::bankCard);
BANK_CARD(DesensitizedUtil::bankCard),
/**
* 中文名
*/
CHINESE_NAME(DesensitizedUtil::chineseName),
/**
* 固定电话
*/
FIXED_PHONE(DesensitizedUtil::fixedPhone),
/**
* 用户ID
*/
USER_ID(s -> String.valueOf(DesensitizedUtil.userId())),
/**
* 密码
*/
PASSWORD(DesensitizedUtil::password),
/**
* ipv4
*/
IPV4(DesensitizedUtil::ipv4),
/**
* ipv6
*/
IPV6(DesensitizedUtil::ipv6),
/**
* 中国大陆车牌,包含普通车辆、新能源车辆
*/
CAR_LICENSE(DesensitizedUtil::carLicense),
/**
* 只显示第一个字符
*/
FIRST_MASK(DesensitizedUtil::firstMask),
/**
* 清空为null
*/
CLEAR(s -> DesensitizedUtil.clear()),
/**
* 清空为""
*/
CLEAR_TO_NULL(s -> DesensitizedUtil.clearToNull());
//可自行添加其他脱敏策略

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

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-sse</artifactId>
<description>
ruoyi-common-sse 模块
</description>
<dependencies>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-satoken</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-json</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,36 @@
package org.dromara.common.sse.config;
import org.dromara.common.sse.controller.SseController;
import org.dromara.common.sse.core.SseEmitterManager;
import org.dromara.common.sse.listener.SseTopicListener;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* SSE 自动装配
*
* @author Lion Li
*/
@AutoConfiguration
@ConditionalOnProperty(value = "sse.enabled", havingValue = "true")
@EnableConfigurationProperties(SseProperties.class)
public class SseAutoConfiguration {
@Bean
public SseEmitterManager sseEmitterManager() {
return new SseEmitterManager();
}
@Bean
public SseTopicListener sseTopicListener() {
return new SseTopicListener();
}
@Bean
public SseController sseController(SseEmitterManager sseEmitterManager) {
return new SseController(sseEmitterManager);
}
}

View File

@ -0,0 +1,21 @@
package org.dromara.common.sse.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* SSE 配置项
*
* @author Lion Li
*/
@Data
@ConfigurationProperties("sse")
public class SseProperties {
private Boolean enabled;
/**
* 路径
*/
private String path;
}

View File

@ -0,0 +1,87 @@
package org.dromara.common.sse.controller;
import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.stp.StpUtil;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.sse.core.SseEmitterManager;
import org.dromara.common.sse.dto.SseMessageDto;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.List;
/**
* SSE 控制器
*
* @author Lion Li
*/
@RestController
@ConditionalOnProperty(value = "sse.enabled", havingValue = "true")
@RequiredArgsConstructor
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();
Long userId = LoginHelper.getUserId();
return sseEmitterManager.connect(userId, tokenValue);
}
/**
* 关闭 SSE 连接
*/
@SaIgnore
@GetMapping(value = "${sse.path}/close")
public R<Void> close() {
String tokenValue = StpUtil.getTokenValue();
Long userId = LoginHelper.getUserId();
sseEmitterManager.disconnect(userId, tokenValue);
return R.ok();
}
/**
* 向特定用户发送消息
*
* @param userId 目标用户的 ID
* @param msg 要发送的消息内容
*/
@GetMapping(value = "${sse.path}/send")
public R<Void> send(Long userId, String msg) {
SseMessageDto dto = new SseMessageDto();
dto.setUserIds(List.of(userId));
dto.setMessage(msg);
sseEmitterManager.publishMessage(dto);
return R.ok();
}
/**
* 向所有用户发送消息
*
* @param msg 要发送的消息内容
*/
@GetMapping(value = "${sse.path}/sendAll")
public R<Void> send(String msg) {
sseEmitterManager.publishAll(msg);
return R.ok();
}
/**
* 清理资源。此方法目前不执行任何操作,但避免因未实现而导致错误
*/
@Override
public void destroy() throws Exception {
// 销毁时不需要做什么 此方法避免无用操作报错
}
}

View File

@ -0,0 +1,145 @@
package org.dromara.common.sse.core;
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.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
/**
* 管理 Server-Sent Events (SSE) 连接
*
* @author Lion Li
*/
@Slf4j
public class SseEmitterManager {
/**
* 订阅的频道
*/
private final static String SSE_TOPIC = "global:sse";
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) {
try {
emitters.get(token).send(SseEmitter.event().comment("disconnected"));
} catch (Exception ignore) {
}
emitters.remove(token);
}
}
/**
* 订阅SSE消息主题并提供一个消费者函数来处理接收到的消息
*
* @param consumer 处理SSE消息的消费者函数
*/
public void subscribeMessage(Consumer<SseMessageDto> consumer) {
RedisUtils.subscribe(SSE_TOPIC, SseMessageDto.class, consumer);
}
/**
* 向指定的用户会话发送消息
*
* @param userId 要发送消息的用户id
* @param message 要发送的消息内容
*/
public void sendMessage(Long userId, String message) {
Map<String, SseEmitter> emitters = USER_TOKEN_EMITTERS.get(userId);
if (emitters != null) {
for (Map.Entry<String, SseEmitter> entry : emitters.entrySet()) {
try {
entry.getValue().send(SseEmitter.event()
.name("message")
.data(message));
} catch (Exception e) {
emitters.remove(entry.getKey());
}
}
}
}
/**
* 本机全用户会话发送消息
*
* @param message 要发送的消息内容
*/
public void sendMessage(String message) {
for (Long userId : USER_TOKEN_EMITTERS.keySet()) {
sendMessage(userId, message);
}
}
/**
* 发布SSE订阅消息
*
* @param sseMessageDto 要发布的SSE消息对象
*/
public void publishMessage(SseMessageDto sseMessageDto) {
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());
});
}
/**
* 向所有的用户发布订阅的消息(群发)
*
* @param message 要发布的消息内容
*/
public void publishAll(String message) {
SseMessageDto broadcastMessage = new SseMessageDto();
broadcastMessage.setMessage(message);
RedisUtils.publish(SSE_TOPIC, broadcastMessage, consumer -> {
log.info("SSE发送主题订阅消息topic:{} message:{}", SSE_TOPIC, message);
});
}
}

View File

@ -0,0 +1,29 @@
package org.dromara.common.sse.dto;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 消息的dto
*
* @author zendwang
*/
@Data
public class SseMessageDto implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 需要推送到的session key 列表
*/
private List<Long> userIds;
/**
* 需要发送的消息
*/
private String message;
}

View File

@ -0,0 +1,48 @@
package org.dromara.common.sse.listener;
import cn.hutool.core.collection.CollUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.sse.core.SseEmitterManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.Ordered;
/**
* SSE 主题订阅监听器
*
* @author Lion Li
*/
@Slf4j
public class SseTopicListener implements ApplicationRunner, Ordered {
@Autowired
private SseEmitterManager sseEmitterManager;
/**
* 在Spring Boot应用程序启动时初始化SSE主题订阅监听器
*
* @param args 应用程序参数
* @throws Exception 初始化过程中可能抛出的异常
*/
@Override
public void run(ApplicationArguments args) throws Exception {
sseEmitterManager.subscribeMessage((message) -> {
log.info("SSE主题订阅收到消息session keys={} message={}", message.getUserIds(), message.getMessage());
// 如果key不为空就按照key发消息 如果为空就群发
if (CollUtil.isNotEmpty(message.getUserIds())) {
message.getUserIds().forEach(key -> {
sseEmitterManager.sendMessage(key, message.getMessage());
});
} else {
sseEmitterManager.sendMessage(message.getMessage());
}
});
log.info("初始化SSE主题订阅监听器成功");
}
@Override
public int getOrder() {
return -1;
}
}

View File

@ -0,0 +1,58 @@
package org.dromara.common.sse.utils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.sse.core.SseEmitterManager;
import org.dromara.common.sse.dto.SseMessageDto;
/**
* SSE工具类
*
* @author Lion Li
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class SseMessageUtils {
private final static SseEmitterManager MANAGER = SpringUtils.getBean(SseEmitterManager.class);
/**
* 向指定的WebSocket会话发送消息
*
* @param userId 要发送消息的用户id
* @param message 要发送的消息内容
*/
public static void sendMessage(Long userId, String message) {
MANAGER.sendMessage(userId, message);
}
/**
* 本机全用户会话发送消息
*
* @param message 要发送的消息内容
*/
public static void sendMessage(String message) {
MANAGER.sendMessage(message);
}
/**
* 发布SSE订阅消息
*
* @param sseMessageDto 要发布的SSE消息对象
*/
public static void publishMessage(SseMessageDto sseMessageDto) {
MANAGER.publishMessage(sseMessageDto);
}
/**
* 向所有的用户发布订阅的消息(群发)
*
* @param message 要发布的消息内容
*/
public static void publishAll(String message) {
MANAGER.publishAll(message);
}
}

View File

@ -0,0 +1 @@
org.dromara.common.sse.config.SseAutoConfiguration

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

@ -1,5 +1,7 @@
package org.dromara.common.tenant.manager;
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.redis.manager.PlusSpringCacheManager;
@ -11,6 +13,7 @@ import org.springframework.cache.Cache;
*
* @author Lion Li
*/
@Slf4j
public class TenantSpringCacheManager extends PlusSpringCacheManager {
public TenantSpringCacheManager() {
@ -18,10 +21,16 @@ public class TenantSpringCacheManager extends PlusSpringCacheManager {
@Override
public Cache getCache(String name) {
if (InterceptorIgnoreHelper.willIgnoreTenantLine("")) {
return super.getCache(name);
}
if (StringUtils.contains(name, GlobalConstants.GLOBAL_REDIS_KEY)) {
return super.getCache(name);
}
String tenantId = TenantHelper.getTenantId();
if (StringUtils.isBlank(tenantId)) {
log.error("无法获取有效的租户id -> Null");
}
if (StringUtils.startsWith(name, tenantId)) {
// 如果存在则直接返回
return super.getCache(name);

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,24 +2,30 @@ 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;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import java.io.IOException;
/**
* 全局异常处理器
*
@ -50,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());
}
/**
* 业务异常
*/
@ -89,6 +116,20 @@ public class GlobalExceptionHandler {
return R.fail(HttpStatus.HTTP_NOT_FOUND, e.getMessage());
}
/**
* 拦截未知的运行时异常
*/
@ResponseStatus(org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(IOException.class)
public void handleRuntimeException(IOException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
if (requestURI.contains("sse")) {
// sse 经常性连接中断 例如关闭浏览器 直接屏蔽
return;
}
log.error("请求地址'{}',连接中断", requestURI, e);
}
/**
* 拦截未知的运行时异常
*/
@ -135,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

@ -12,10 +12,21 @@
<artifactId>ruoyi-monitor-admin</artifactId>
<dependencies>
<!-- SpringWeb模块 -->
<!-- SpringBoot Web容器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- web 容器使用 undertow 性能更强 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<!-- spring security 安全认证 -->

View File

@ -39,9 +39,7 @@ public class SecurityConfig {
.authorizeHttpRequests((authorize) ->
authorize.requestMatchers(
new AntPathRequestMatcher(adminContextPath + "/assets/**"),
new AntPathRequestMatcher(adminContextPath + "/login"),
new AntPathRequestMatcher("/actuator"),
new AntPathRequestMatcher("/actuator/**")
new AntPathRequestMatcher(adminContextPath + "/login")
).permitAll()
.anyRequest().authenticated())
.formLogin((formLogin) ->

View File

@ -9,6 +9,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import static de.codecentric.boot.admin.server.domain.values.StatusInfo.*;
/**
* 自定义事件通知处理
*
@ -28,13 +30,26 @@ public class CustomNotifier extends AbstractEventNotifier {
return Mono.fromRunnable(() -> {
// 实例状态改变事件
if (event instanceof InstanceStatusChangedEvent) {
// 获取实例注册名称
String registName = instance.getRegistration().getName();
// 获取实例ID
String instanceId = event.getInstance().getValue();
// 获取实例状态
String status = ((InstanceStatusChangedEvent) event).getStatusInfo().getStatus();
log.info("Instance Status Change: [{}],[{}],[{}]", registName, instanceId, status);
// 获取服务URL
String serviceUrl = instance.getRegistration().getServiceUrl();
String statusName = switch (status) {
case STATUS_UP -> "服务上线"; // 实例成功启动并可以正常处理请求
case STATUS_OFFLINE -> "服务离线"; //实例被手动或自动地从服务中移除
case STATUS_RESTRICTED -> "服务受限"; //表示实例在某些方面受限,可能无法完全提供所有服务
case STATUS_OUT_OF_SERVICE -> "停止服务状态"; //表示实例已被标记为停止提供服务,可能是计划内维护或测试
case STATUS_DOWN -> "服务下线"; //实例因崩溃、错误或其他原因停止运行
case STATUS_UNKNOWN -> "服务未知异常"; //监控系统无法确定实例的当前状态
default -> "未知状态"; //没有匹配的状态
};
log.info("Instance Status Change: 状态名称【{}】, 注册名称【{}】, 实例ID【{}】, 状态【{}】, 服务URL【{}】",
statusName, registName, instanceId, status, serviceUrl);
}
});
}
}

View File

@ -13,8 +13,8 @@ logging:
spring:
security:
user:
name: ruoyi
password: 123456
name: @monitor.username@
password: @monitor.password@
boot:
admin:
ui:
@ -41,5 +41,8 @@ spring.boot.admin.client:
url: http://localhost:9090/admin
instance:
service-host-type: IP
username: ruoyi
password: 123456
metadata:
username: ${spring.boot.admin.client.username}
userpassword: ${spring.boot.admin.client.password}
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

@ -0,0 +1,64 @@
package com.aizuda.snailjob.server.starter.filter;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class ActuatorAuthFilter implements Filter {
private final String username;
private final String password;
public ActuatorAuthFilter(String username, String password) {
this.username = username;
this.password = password;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 获取 Authorization 头
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Basic ")) {
// 如果没有提供 Authorization 或者格式不对,则返回 401
response.setHeader("WWW-Authenticate", "Basic realm=\"realm\"");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
return;
}
// 解码 Base64 编码的用户名和密码
String base64Credentials = authHeader.substring("Basic ".length());
byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
String credentials = new String(credDecoded, StandardCharsets.UTF_8);
String[] split = credentials.split(":");
if (split.length != 2) {
response.setHeader("WWW-Authenticate", "Basic realm=\"realm\"");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
return;
}
// 验证用户名和密码
if (!username.equals(split[0]) && password.equals(split[1])) {
response.setHeader("WWW-Authenticate", "Basic realm=\"realm\"");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
return;
}
// 如果认证成功,继续处理请求
filterChain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
}

View File

@ -0,0 +1,29 @@
package com.aizuda.snailjob.server.starter.filter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 权限安全配置
*
* @author Lion Li
*/
@Configuration
public class SecurityConfig {
@Value("${spring.boot.admin.client.username}")
private String username;
@Value("${spring.boot.admin.client.password}")
private String password;
@Bean
public FilterRegistrationBean<ActuatorAuthFilter> actuatorFilterRegistrationBean() {
FilterRegistrationBean<ActuatorAuthFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new ActuatorAuthFilter(username, password));
registrationBean.addUrlPatterns("/actuator", "/actuator/**");
return registrationBean;
}
}

View File

@ -43,5 +43,8 @@ spring.boot.admin.client:
url: http://localhost:9090/admin
instance:
service-host-type: IP
username: ruoyi
password: 123456
metadata:
username: ${spring.boot.admin.client.username}
userpassword: ${spring.boot.admin.client.password}
username: @monitor.username@
password: @monitor.password@

View File

@ -43,5 +43,8 @@ spring.boot.admin.client:
url: http://localhost:9090/admin
instance:
service-host-type: IP
username: ruoyi
password: 123456
metadata:
username: ${spring.boot.admin.client.username}
userpassword: ${spring.boot.admin.client.password}
username: @monitor.username@
password: @monitor.password@

View File

@ -4,14 +4,14 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.poi.ss.formula.functions.T;
import org.apache.ibatis.annotations.Param;
import org.dromara.common.mybatis.annotation.DataColumn;
import org.dromara.common.mybatis.annotation.DataPermission;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.demo.domain.TestDemo;
import org.dromara.demo.domain.vo.TestDemoVo;
import org.apache.ibatis.annotations.Param;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
@ -44,16 +44,17 @@ public interface TestDemoMapper extends BaseMapperPlus<TestDemo, TestDemoVo> {
List<TestDemo> selectList(@Param(Constants.WRAPPER) Wrapper<TestDemo> queryWrapper);
@Override
@DataPermission({
@DataPermission(value = {
@DataColumn(key = "deptName", value = "dept_id"),
@DataColumn(key = "userName", value = "user_id")
})
int updateById(@Param(Constants.ENTITY) TestDemo entity);
}, joinStr = "AND")
List<TestDemo> selectByIds(@Param(Constants.COLL) Collection<? extends Serializable> idList);
@Override
@DataPermission({
@DataColumn(key = "deptName", value = "dept_id"),
@DataColumn(key = "userName", value = "user_id")
})
int deleteByIds(@Param(Constants.COLL) Collection<?> idList);
int updateById(@Param(Constants.ENTITY) TestDemo entity);
}

View File

@ -3,6 +3,8 @@ package org.dromara.demo.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
@ -12,7 +14,6 @@ import org.dromara.demo.domain.bo.TestDemoBo;
import org.dromara.demo.domain.vo.TestDemoVo;
import org.dromara.demo.mapper.TestDemoMapper;
import org.dromara.demo.service.ITestDemoService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Collection;
@ -99,7 +100,11 @@ public class TestDemoServiceImpl implements ITestDemoService {
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
//TODO 做一些业务上的校验,判断是否需要校验
// 做一些业务上的校验,判断是否需要校验
List<TestDemo> list = baseMapper.selectByIds(ids);
if (list.size() != ids.size()) {
throw new ServiceException("您没有删除权限!");
}
}
return baseMapper.deleteByIds(ids) > 0;
}

View File

@ -47,6 +47,38 @@
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
</dependency>
<dependency>
<groupId>org.anyline</groupId>
<artifactId>anyline-environment-spring-data-jdbc</artifactId>
<version>${anyline.version}</version>
</dependency>
<dependency>
<groupId>org.anyline</groupId>
<artifactId>anyline-data-jdbc-mysql</artifactId>
<version>${anyline.version}</version>
</dependency>
<!-- anyline支持100+种类型数据库 添加对应的jdbc依赖与anyline对应数据库依赖包即可 -->
<!-- <dependency>-->
<!-- <groupId>org.anyline</groupId>-->
<!-- <artifactId>anyline-data-jdbc-oracle</artifactId>-->
<!-- <version>${anyline.version}</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.anyline</groupId>-->
<!-- <artifactId>anyline-data-jdbc-postgresql</artifactId>-->
<!-- <version>${anyline.version}</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.anyline</groupId>-->
<!-- <artifactId>anyline-data-jdbc-mssql</artifactId>-->
<!-- <version>${anyline.version}</version>-->
<!-- </dependency>-->
</dependencies>
</project>

View File

@ -0,0 +1,105 @@
package org.dromara.generator.config;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.anyline.data.datasource.DataSourceMonitor;
import org.anyline.data.runtime.DataRuntime;
import org.anyline.util.ConfigTable;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.util.HashMap;
import java.util.Map;
/**
* anyline 适配 动态数据源改造
*
* @author Lion Li
*/
@Slf4j
@Component
public class MyBatisDataSourceMonitor implements DataSourceMonitor {
public MyBatisDataSourceMonitor() {
// 调整执行模式为自定义
ConfigTable.KEEP_ADAPTER = 2;
// 禁用缓存
ConfigTable.METADATA_CACHE_SCOPE = 0;
}
private final Map<String, String> features = new HashMap<>();
/**
* 数据源特征 用来定准 adapter 包含数据库或JDBC协议关键字<br/>
* 一般会通过 产品名_url 合成 如果返回null 上层方法会通过driver_产品名_url合成
*
* @param datasource 数据源
* @return String 返回null由上层自动提取
*/
@Override
public String feature(DataRuntime runtime, Object datasource) {
String feature = null;
if (datasource instanceof JdbcTemplate jdbc) {
DataSource ds = jdbc.getDataSource();
if (ds instanceof DynamicRoutingDataSource) {
String key = DynamicDataSourceContextHolder.peek();
feature = features.get(key);
if (null == feature) {
Connection con = null;
try {
con = DataSourceUtils.getConnection(ds);
DatabaseMetaData meta = con.getMetaData();
String url = meta.getURL();
feature = meta.getDatabaseProductName().toLowerCase().replace(" ", "") + "_" + url;
features.put(key, feature);
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
if (null != con && !DataSourceUtils.isConnectionTransactional(con, ds)) {
DataSourceUtils.releaseConnection(con, ds);
}
}
}
}
}
return feature;
}
/**
* 数据源唯一标识 如果不实现则默认feature
* @param datasource 数据源
* @return String 返回null由上层自动提取
*/
@Override
public String key(DataRuntime runtime, Object datasource) {
if(datasource instanceof JdbcTemplate jdbc){
DataSource ds = jdbc.getDataSource();
if(ds instanceof DynamicRoutingDataSource){
return DynamicDataSourceContextHolder.peek();
}
}
return runtime.getKey();
}
/**
* ConfigTable.KEEP_ADAPTER=2 : 根据当前接口判断是否保持同一个数据源绑定同一个adapter<br/>
* DynamicRoutingDataSource类型的返回false,因为同一个DynamicRoutingDataSource可能对应多类数据库, 如果项目中只有一种数据库 应该直接返回true
*
* @param datasource 数据源
* @return boolean
*/
@Override
public boolean keepAdapter(DataRuntime runtime, Object datasource) {
if (datasource instanceof JdbcTemplate jdbc) {
DataSource ds = jdbc.getDataSource();
return !(ds instanceof DynamicRoutingDataSource);
}
return true;
}
}

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

@ -17,7 +17,6 @@ import jakarta.validation.constraints.NotBlank;
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("gen_table_column")

View File

@ -1,13 +1,9 @@
package org.dromara.generator.mapper;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import org.apache.ibatis.annotations.Param;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.generator.domain.GenTableColumn;
import java.util.List;
/**
* 业务字段 数据层
*
@ -15,14 +11,5 @@ import java.util.List;
*/
@InterceptorIgnore(dataPermission = "true", tenantLine = "true")
public interface GenTableColumnMapper extends BaseMapperPlus<GenTableColumn, GenTableColumn> {
/**
* 根据表名称查询列信息
*
* @param tableName 表名称
* @param dataName 数据源名称
* @return 列信息
*/
@DS("#dataName")
List<GenTableColumn> selectDbTableColumnsByName(@Param("tableName") String tableName, String dataName);
}

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;
@ -17,22 +15,6 @@ import java.util.List;
@InterceptorIgnore(dataPermission = "true", tenantLine = "true")
public interface GenTableMapper extends BaseMapperPlus<GenTable, GenTable> {
/**
* 查询据库列表
*
* @param genTable 查询条件
* @return 数据库表集合
*/
Page<GenTable> selectPageDbTableList(@Param("page") Page<GenTable> page, @Param("genTable") GenTable genTable);
/**
* 查询据库列表
*
* @param tableNames 表名称组
* @return 数据库表集合
*/
List<GenTable> selectDbTableListByNames(String[] tableNames);
/**
* 查询所有表信息
*
@ -56,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

@ -9,15 +9,20 @@ import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.anyline.metadata.Column;
import org.anyline.metadata.Table;
import org.anyline.proxy.ServiceProxy;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.dromara.common.core.constant.Constants;
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.StringUtils;
import org.dromara.common.core.utils.file.FileUtils;
@ -41,11 +46,7 @@ import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@ -63,6 +64,8 @@ public class GenTableServiceImpl implements IGenTableService {
private final GenTableColumnMapper genTableColumnMapper;
private final IdentifierGenerator identifierGenerator;
private static final String[] TABLE_IGNORE = new String[]{"sj_", "act_", "flw_", "gen_"};
/**
* 查询业务字段列表
*
@ -99,7 +102,7 @@ public class GenTableServiceImpl implements IGenTableService {
Map<String, Object> params = genTable.getParams();
QueryWrapper<GenTable> wrapper = Wrappers.query();
wrapper
.eq(StringUtils.isNotEmpty(genTable.getDataName()),"data_name", genTable.getDataName())
.eq(StringUtils.isNotEmpty(genTable.getDataName()), "data_name", genTable.getDataName())
.like(StringUtils.isNotBlank(genTable.getTableName()), "lower(table_name)", StringUtils.lowerCase(genTable.getTableName()))
.like(StringUtils.isNotBlank(genTable.getTableComment()), "lower(table_comment)", StringUtils.lowerCase(genTable.getTableComment()))
.between(params.get("beginTime") != null && params.get("endTime") != null,
@ -107,14 +110,80 @@ public class GenTableServiceImpl implements IGenTableService {
return wrapper;
}
/**
* 查询数据库列表
*
* @param genTable 包含查询条件的GenTable对象
* @param pageQuery 包含分页信息的PageQuery对象
* @return 包含分页结果的TableDataInfo对象
*/
@DS("#genTable.dataName")
@Override
public TableDataInfo<GenTable> selectPageDbTableList(GenTable genTable, PageQuery pageQuery) {
genTable.getParams().put("genTableNames",baseMapper.selectTableNameList(genTable.getDataName()));
Page<GenTable> page = baseMapper.selectPageDbTableList(pageQuery.build(), genTable);
// 获取查询条件
String tableName = genTable.getTableName();
String tableComment = genTable.getTableComment();
LinkedHashMap<String, Table<?>> tablesMap = ServiceProxy.metadata().tables();
if (CollUtil.isEmpty(tablesMap)) {
return TableDataInfo.build();
}
List<String> tableNames = baseMapper.selectTableNameList(genTable.getDataName());
String[] tableArrays;
if (CollUtil.isNotEmpty(tableNames)) {
tableArrays = tableNames.toArray(new String[0]);
} else {
tableArrays = new String[0];
}
// 过滤并转换表格数据
List<GenTable> tables = tablesMap.values().stream()
.filter(x -> !startWithAnyIgnoreCase(x.getName(), TABLE_IGNORE))
.filter(x -> {
if (CollUtil.isEmpty(tableNames)) {
return true;
}
return !StringUtils.equalsAnyIgnoreCase(x.getName(), tableArrays);
})
.filter(x -> {
boolean nameMatches = true;
boolean commentMatches = true;
// 进行表名称的模糊查询
if (StringUtils.isNotBlank(tableName)) {
nameMatches = StringUtils.containsIgnoreCase(x.getName(), tableName);
}
// 进行表描述的模糊查询
if (StringUtils.isNotBlank(tableComment)) {
commentMatches = StringUtils.containsIgnoreCase(x.getComment(), tableComment);
}
// 同时匹配名称和描述
return nameMatches && commentMatches;
})
.map(x -> {
GenTable gen = new GenTable();
gen.setTableName(x.getName());
gen.setTableComment(x.getComment());
gen.setCreateTime(x.getCreateTime());
gen.setUpdateTime(x.getUpdateTime());
return gen;
}).toList();
IPage<GenTable> page = pageQuery.build();
page.setTotal(tables.size());
// 手动分页 set数据
page.setRecords(CollUtil.page((int) page.getCurrent() - 1, (int) page.getSize(), tables));
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;
}
/**
* 查询据库列表
*
@ -125,7 +194,29 @@ public class GenTableServiceImpl implements IGenTableService {
@DS("#dataName")
@Override
public List<GenTable> selectDbTableListByNames(String[] tableNames, String dataName) {
return baseMapper.selectDbTableListByNames(tableNames);
Set<String> tableNameSet = new HashSet<>(List.of(tableNames));
LinkedHashMap<String, Table<?>> tablesMap = ServiceProxy.metadata().tables();
if (CollUtil.isEmpty(tablesMap)) {
return new ArrayList<>();
}
List<Table<?>> tableList = tablesMap.values().stream()
.filter(x -> !StringUtils.containsAnyIgnoreCase(x.getName(), TABLE_IGNORE))
.filter(x -> tableNameSet.contains(x.getName())).toList();
if (CollUtil.isEmpty(tableList)) {
return new ArrayList<>();
}
return tableList.stream().map(x -> {
GenTable gen = new GenTable();
gen.setDataName(dataName);
gen.setTableName(x.getName());
gen.setTableComment(x.getComment());
gen.setCreateTime(x.getCreateTime());
gen.setUpdateTime(x.getUpdateTime());
return gen;
}).toList();
}
/**
@ -187,7 +278,7 @@ public class GenTableServiceImpl implements IGenTableService {
int row = baseMapper.insert(table);
if (row > 0) {
// 保存列信息
List<GenTableColumn> genTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName, dataName);
List<GenTableColumn> genTableColumns = SpringUtils.getAopProxy(this).selectDbTableColumnsByName(tableName, dataName);
List<GenTableColumn> saveColumns = new ArrayList<>();
for (GenTableColumn column : genTableColumns) {
GenUtils.initColumnField(column, table);
@ -203,6 +294,32 @@ public class GenTableServiceImpl implements IGenTableService {
}
}
/**
* 根据表名称查询列信息
*
* @param tableName 表名称
* @param dataName 数据源名称
* @return 列信息
*/
@DS("#dataName")
@Override
public List<GenTableColumn> selectDbTableColumnsByName(String tableName, String dataName) {
LinkedHashMap<String, Column> columns = ServiceProxy.metadata().columns(tableName);
List<GenTableColumn> tableColumns = new ArrayList<>();
columns.forEach((columnName, column) -> {
GenTableColumn tableColumn = new GenTableColumn();
tableColumn.setIsPk(String.valueOf(column.isPrimaryKey()));
tableColumn.setColumnName(column.getName());
tableColumn.setColumnComment(column.getComment());
tableColumn.setColumnType(column.getTypeName().toLowerCase());
tableColumn.setSort(column.getPosition());
tableColumn.setIsRequired(column.isNullable() == 0 ? "1" : "0");
tableColumn.setIsIncrement(column.isAutoIncrement() == -1 ? "0" : "1");
tableColumns.add(tableColumn);
});
return tableColumns;
}
/**
* 预览代码
*
@ -298,7 +415,7 @@ public class GenTableServiceImpl implements IGenTableService {
List<GenTableColumn> tableColumns = table.getColumns();
Map<String, GenTableColumn> tableColumnMap = StreamUtils.toIdentityMap(tableColumns, GenTableColumn::getColumnName);
List<GenTableColumn> dbTableColumns = genTableColumnMapper.selectDbTableColumnsByName(table.getTableName(), table.getDataName());
List<GenTableColumn> dbTableColumns = SpringUtils.getAopProxy(this).selectDbTableColumnsByName(table.getTableName(), table.getDataName());
if (CollUtil.isEmpty(dbTableColumns)) {
throw new ServiceException("同步数据失败,原表结构不存在");
}
@ -441,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

@ -85,6 +85,15 @@ public interface IGenTableService {
*/
void importGenTable(List<GenTable> tableList, String dataName);
/**
* 根据表名称查询列信息
*
* @param tableName 表名称
* @param dataName 数据源名称
* @return 列信息
*/
List<GenTableColumn> selectDbTableColumnsByName(String tableName, String dataName);
/**
* 预览代码
*

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

@ -7,87 +7,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<resultMap type="org.dromara.generator.domain.GenTableColumn" id="GenTableColumnResult">
</resultMap>
<select id="selectDbTableColumnsByName" parameterType="String" resultMap="GenTableColumnResult">
<if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isMySql()">
select column_name,
(case when (is_nullable = 'no' <![CDATA[ && ]]> column_key != 'PRI') then '1' else '0' end) as is_required,
(case when column_key = 'PRI' then '1' else '0' end) as is_pk,
ordinal_position as sort,
column_comment,
(case when extra = 'auto_increment' then '1' else '0' end) as is_increment,
column_type
from information_schema.columns where table_schema = (select database()) and table_name = (#{tableName})
order by ordinal_position
</if>
<if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isOracle()">
select lower(temp.column_name) as column_name,
(case when (temp.nullable = 'N' and temp.constraint_type != 'P') then '1' else '0' end) as is_required,
(case when temp.constraint_type = 'P' then '1' else '0' end) as is_pk,
temp.column_id as sort,
temp.comments as column_comment,
(case when temp.constraint_type = 'P' then '1' else '0' end) as is_increment,
lower(temp.data_type) as column_type
from (
select col.column_id, col.column_name,col.nullable, col.data_type, colc.comments, uc.constraint_type, row_number()
over (partition by col.column_name order by uc.constraint_type desc) as row_flg
from user_tab_columns col
left join user_col_comments colc on colc.table_name = col.table_name and colc.column_name = col.column_name
left join user_cons_columns ucc on ucc.table_name = col.table_name and ucc.column_name = col.column_name
left join user_constraints uc on uc.constraint_name = ucc.constraint_name
where col.table_name = upper(#{tableName})
) temp
WHERE temp.row_flg = 1
ORDER BY temp.column_id
</if>
<if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isPostgerSql()">
SELECT column_name, is_required, is_pk, sort, column_comment, is_increment, column_type
FROM (
SELECT c.relname AS table_name,
a.attname AS column_name,
d.description AS column_comment,
CASE WHEN a.attnotnull AND con.conname IS NULL THEN 1 ELSE 0
END AS is_required,
CASE WHEN con.conname IS NOT NULL THEN 1 ELSE 0
END AS is_pk,
a.attnum AS sort,
CASE WHEN "position"(pg_get_expr(ad.adbin, ad.adrelid),
((c.relname::text || '_'::text) || a.attname::text) || '_seq'::text) > 0 THEN 1 ELSE 0
END AS is_increment,
btrim(
CASE WHEN t.typelem <![CDATA[ <> ]]> 0::oid AND t.typlen = '-1'::integer THEN 'ARRAY'::text ELSE
CASE WHEN t.typtype = 'd'::"char" THEN format_type(t.typbasetype, NULL::integer)
ELSE format_type(a.atttypid, NULL::integer) END
END, '"'::text
) AS column_type
FROM pg_attribute a
JOIN (pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid) ON a.attrelid = c.oid
LEFT JOIN pg_description d ON d.objoid = c.oid AND a.attnum = d.objsubid
LEFT JOIN pg_constraint con ON con.conrelid = c.oid AND (a.attnum = ANY (con.conkey))
LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
LEFT JOIN pg_type t ON a.atttypid = t.oid
WHERE (c.relkind = ANY (ARRAY ['r'::"char", 'p'::"char"]))
AND a.attnum > 0
AND n.nspname = 'public'::name
ORDER BY c.relname, a.attnum
) temp
WHERE table_name = (#{tableName})
AND column_type <![CDATA[ <> ]]> '-'
</if>
<if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isSqlServer()">
SELECT
cast(A.NAME as nvarchar) as column_name,
cast(B.NAME as nvarchar) + (case when B.NAME = 'numeric' then '(' + cast(A.prec as nvarchar) + ',' + cast(A.scale as nvarchar) + ')' else '' end) as column_type,
cast(G.[VALUE] as nvarchar) as column_comment,
(SELECT 1 FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE Z WHERE TABLE_NAME = D.NAME and A.NAME = Z.column_name ) as is_pk,
colorder as sort
FROM SYSCOLUMNS A
LEFT JOIN SYSTYPES B ON A.XTYPE = B.XUSERTYPE
INNER JOIN SYSOBJECTS D ON A.ID = D.ID AND D.XTYPE='U' AND D.NAME != 'DTPROPERTIES'
LEFT JOIN SYS.EXTENDED_PROPERTIES G ON A.ID = G.MAJOR_ID AND A.COLID = G.MINOR_ID
LEFT JOIN SYS.EXTENDED_PROPERTIES F ON D.ID = F.MAJOR_ID AND F.MINOR_ID = 0
WHERE D.NAME = #{tableName}
ORDER BY A.COLORDER
</if>
</select>
</mapper>

View File

@ -14,239 +14,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<id property="columnId" column="column_id"/>
</resultMap>
<select id="selectPageDbTableList" resultMap="GenTableResult">
<if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isMySql()">
select table_name, table_comment, create_time, update_time
from information_schema.tables
where table_schema = (select database())
AND table_name NOT LIKE 'sj_%' AND table_name NOT LIKE 'gen_%'
AND table_name NOT LIKE 'act_%' AND table_name NOT LIKE 'flw_%'
<if test="genTable.params.genTableNames != null and genTable.params.genTableNames.size > 0">
AND table_name NOT IN
<foreach collection="genTable.params.genTableNames" open="(" close=")" separator="," item="item">
#{item}
</foreach>
</if>
<if test="genTable.tableName != null and genTable.tableName != ''">
AND lower(table_name) like lower(concat('%', #{genTable.tableName}, '%'))
</if>
<if test="genTable.tableComment != null and genTable.tableComment != ''">
AND lower(table_comment) like lower(concat('%', #{genTable.tableComment}, '%'))
</if>
order by create_time desc
</if>
<if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isOracle()">
select lower(dt.table_name) as table_name, dtc.comments as table_comment, uo.created as create_time, uo.last_ddl_time as update_time
from user_tables dt, user_tab_comments dtc, user_objects uo
where dt.table_name = dtc.table_name
and dt.table_name = uo.object_name
and uo.object_type = 'TABLE'
AND dt.table_name NOT LIKE 'SJ_%' AND dt.table_name NOT LIKE 'GEN_%'
AND dt.table_name NOT LIKE 'ACT_%' AND dt.table_name NOT LIKE 'FLW_%'
<if test="genTable.params.genTableNames != null and genTable.params.genTableNames.size > 0">
AND lower(dt.table_name) NOT IN
<foreach collection="genTable.params.genTableNames" open="(" close=")" separator="," item="item">
#{item}
</foreach>
</if>
<if test="genTable.tableName != null and genTable.tableName != ''">
AND lower(dt.table_name) like lower(concat(concat('%', #{genTable.tableName}), '%'))
</if>
<if test="genTable.tableComment != null and genTable.tableComment != ''">
AND lower(dtc.comments) like lower(concat(concat('%', #{genTable.tableComment}), '%'))
</if>
order by create_time desc
</if>
<if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isPostgerSql()">
select table_name, table_comment, create_time, update_time
from (
SELECT c.relname AS table_name,
obj_description(c.oid) AS table_comment,
CURRENT_TIMESTAMP AS create_time,
CURRENT_TIMESTAMP AS update_time
FROM pg_class c
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE (c.relkind = ANY (ARRAY ['r'::"char", 'p'::"char"]))
AND c.relname != 'spatial_%'::text
AND n.nspname = 'public'::name
AND n.nspname <![CDATA[ <> ]]> ''::name
) list_table
where table_name NOT LIKE 'sj_%' AND table_name NOT LIKE 'gen_%'
AND table_name NOT LIKE 'act_%' AND table_name NOT LIKE 'flw_%'
<if test="genTable.params.genTableNames != null and genTable.params.genTableNames.size > 0">
AND table_name NOT IN
<foreach collection="genTable.params.genTableNames" open="(" close=")" separator="," item="item">
#{item}
</foreach>
</if>
<if test="genTable.tableName != null and genTable.tableName != ''">
AND lower(table_name) like lower(concat('%', #{genTable.tableName}, '%'))
</if>
<if test="genTable.tableComment != null and genTable.tableComment != ''">
AND lower(table_comment) like lower(concat('%', #{genTable.tableComment}, '%'))
</if>
order by create_time desc
</if>
<if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isSqlServer()">
SELECT cast(D.NAME as nvarchar) as table_name,
cast(F.VALUE as nvarchar) as table_comment,
crdate as create_time,
refdate as update_time
FROM SYSOBJECTS D
INNER JOIN SYS.EXTENDED_PROPERTIES F ON D.ID = F.MAJOR_ID
AND F.MINOR_ID = 0 AND D.XTYPE = 'U' AND D.NAME != 'DTPROPERTIES'
AND D.NAME NOT LIKE 'sj_%' AND D.NAME NOT LIKE 'gen_%'
AND D.NAME NOT LIKE 'act_%' AND D.NAME NOT LIKE 'flw_%'
<if test="genTable.params.genTableNames != null and genTable.params.genTableNames.size > 0">
AND D.NAME NOT IN
<foreach collection="genTable.params.genTableNames" open="(" close=")" separator="," item="item">
#{item}
</foreach>
</if>
<if test="genTable.tableName != null and genTable.tableName != ''">
AND lower(D.NAME) like lower(concat(N'%', N'${genTable.tableName}', N'%'))
</if>
<if test="genTable.tableComment != null and genTable.tableComment != ''">
AND lower(CAST(F.VALUE AS nvarchar)) like lower(concat(N'%', N'${genTable.tableComment}', N'%'))
</if>
order by crdate desc
</if>
</select>
<select id="selectDbTableListByNames" resultMap="GenTableResult">
<if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isMySql()">
select table_name, table_comment, create_time, update_time from information_schema.tables
where table_schema = (select database())
and table_name NOT LIKE 'sj_%' and table_name NOT LIKE 'gen_%'
and table_name NOT LIKE 'act_%' AND table_name NOT LIKE 'flw_%'
and table_name in
<foreach collection="array" item="name" open="(" separator="," close=")">
#{name}
</foreach>
</if>
<if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isOracle()">
select lower(dt.table_name) as table_name, dtc.comments as table_comment, uo.created as create_time, uo.last_ddl_time as update_time
from user_tables dt, user_tab_comments dtc, user_objects uo
where dt.table_name = dtc.table_name
and dt.table_name = uo.object_name
and uo.object_type = 'TABLE'
and dt.table_name NOT LIKE 'SJ_%' AND dt.table_name NOT LIKE 'GEN_%'
and dt.table_name NOT LIKE 'ACT_%' AND dt.table_name NOT LIKE 'FLW_%'
and lower(dt.table_name) in
<foreach collection="array" item="name" open="(" separator="," close=")">
#{name}
</foreach>
</if>
<if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isPostgerSql()">
select table_name, table_comment, create_time, update_time
from (
SELECT c.relname AS table_name,
obj_description(c.oid) AS table_comment,
CURRENT_TIMESTAMP AS create_time,
CURRENT_TIMESTAMP AS update_time
FROM pg_class c
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE (c.relkind = ANY (ARRAY ['r'::"char", 'p'::"char"]))
AND c.relname != 'spatial_%'::text
AND n.nspname = 'public'::name
AND n.nspname <![CDATA[ <> ]]> ''::name
) list_table
where table_name NOT LIKE 'sj_%' and table_name NOT LIKE 'gen_%'
and table_name NOT LIKE 'act_%' and table_name NOT LIKE 'flw_%'
and table_name in
<foreach collection="array" item="name" open="(" separator="," close=")">
#{name}
</foreach>
</if>
<if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isSqlServer()">
SELECT cast(D.NAME as nvarchar) as table_name,
cast(F.VALUE as nvarchar) as table_comment,
crdate as create_time,
refdate as update_time
FROM SYSOBJECTS D
INNER JOIN SYS.EXTENDED_PROPERTIES F ON D.ID = F.MAJOR_ID
AND F.MINOR_ID = 0 AND D.XTYPE = 'U' AND D.NAME != 'DTPROPERTIES'
AND D.NAME NOT LIKE 'sj_%' AND D.NAME NOT LIKE 'gen_%'
AND D.NAME NOT LIKE 'act_%' AND D.NAME NOT LIKE 'flw_%'
AND D.NAME in
<foreach collection="array" item="name" open="(" separator="," close=")">
#{name}
</foreach>
</if>
</select>
<select id="selectTableByName" parameterType="String" resultMap="GenTableResult">
<if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isMySql()">
select table_name, table_comment, create_time, update_time from information_schema.tables
where table_schema = (select database())
and table_name NOT LIKE 'sj_%' and table_name NOT LIKE 'gen_%'
and table_name NOT LIKE 'act_%' AND table_name NOT LIKE 'flw_%'
and table_name = #{tableName}
</if>
<if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isOracle()">
select lower(dt.table_name) as table_name, dtc.comments as table_comment, uo.created as create_time, uo.last_ddl_time as update_time
from user_tables dt, user_tab_comments dtc, user_objects uo
where dt.table_name = dtc.table_name
and dt.table_name = uo.object_name
and uo.object_type = 'TABLE'
AND dt.table_name NOT LIKE 'SJ_%' AND dt.table_name NOT LIKE 'GEN_%'
AND dt.table_name NOT LIKE 'ACT_%' AND dt.table_name NOT LIKE 'FLW_%'
AND dt.table_name NOT IN (select table_name from gen_table)
and lower(dt.table_name) = #{tableName}
</if>
<if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isPostgerSql()">
select table_name, table_comment, create_time, update_time
from (
SELECT c.relname AS table_name,
obj_description(c.oid) AS table_comment,
CURRENT_TIMESTAMP AS create_time,
CURRENT_TIMESTAMP AS update_time
FROM pg_class c
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE (c.relkind = ANY (ARRAY ['r'::"char", 'p'::"char"]))
AND c.relname != 'spatial_%'::text
AND n.nspname = 'public'::name
AND n.nspname <![CDATA[ <> ]]> ''::name
) list_table
where table_name NOT LIKE 'sj_%' and table_name NOT LIKE 'gen_%'
and table_name NOT LIKE 'act_%' and table_name NOT LIKE 'flw_%'
and table_name = #{tableName}
</if>
<if test="@org.dromara.common.mybatis.helper.DataBaseHelper@isSqlServer()">
SELECT cast(D.NAME as nvarchar) as table_name,
cast(F.VALUE as nvarchar) as table_comment,
crdate as create_time,
refdate as update_time
FROM SYSOBJECTS D
INNER JOIN SYS.EXTENDED_PROPERTIES F ON D.ID = F.MAJOR_ID
AND F.MINOR_ID = 0 AND D.XTYPE = 'U' AND D.NAME != 'DTPROPERTIES'
AND D.NAME NOT LIKE 'sj_%' AND D.NAME NOT LIKE 'gen_%'
AND D.NAME NOT LIKE 'act_%' AND D.NAME NOT LIKE 'flw_%'
AND D.NAME = #{tableName}
</if>
</select>
<select id="selectGenTableById" parameterType="Long" resultMap="GenTableResult">
<sql id="genSelect">
SELECT t.table_id, t.data_name, t.table_name, t.table_comment, t.sub_table_name, t.sub_table_fk_name, t.class_name, t.tpl_category, t.package_name, t.module_name, t.business_name, t.function_name, t.function_author, t.gen_type, t.gen_path, t.options, t.remark,
c.column_id, c.column_name, c.column_comment, c.column_type, c.java_type, c.java_field, c.is_pk, c.is_increment, c.is_required, c.is_insert, c.is_edit, c.is_list, c.is_query, c.query_type, c.html_type, c.dict_type, c.sort
FROM gen_table t
LEFT JOIN gen_table_column c ON t.table_id = c.table_id
LEFT JOIN gen_table_column c ON t.table_id = c.table_id
</sql>
<select id="selectGenTableById" parameterType="Long" resultMap="GenTableResult">
<include refid="genSelect"/>
where t.table_id = #{tableId} order by c.sort
</select>
<select id="selectGenTableByName" parameterType="String" resultMap="GenTableResult">
SELECT t.table_id, t.data_name, t.table_name, t.table_comment, t.sub_table_name, t.sub_table_fk_name, t.class_name, t.tpl_category, t.package_name, t.module_name, t.business_name, t.function_name, t.function_author, t.gen_type, t.gen_path, t.options, t.remark,
c.column_id, c.column_name, c.column_comment, c.column_type, c.java_type, c.java_field, c.is_pk, c.is_increment, c.is_required, c.is_insert, c.is_edit, c.is_list, c.is_query, c.query_type, c.html_type, c.dict_type, c.sort
FROM gen_table t
LEFT JOIN gen_table_column c ON t.table_id = c.table_id
<include refid="genSelect"/>
where t.table_name = #{tableName} order by c.sort
</select>
<select id="selectGenTableAll" parameterType="String" resultMap="GenTableResult">
SELECT t.table_id, t.data_name, t.table_name, t.table_comment, t.sub_table_name, t.sub_table_fk_name, t.class_name, t.tpl_category, t.package_name, t.module_name, t.business_name, t.function_name, t.function_author, t.options, t.remark,
c.column_id, c.column_name, c.column_comment, c.column_type, c.java_type, c.java_field, c.is_pk, c.is_increment, c.is_required, c.is_insert, c.is_edit, c.is_list, c.is_query, c.query_type, c.html_type, c.dict_type, c.sort
FROM gen_table t
LEFT JOIN gen_table_column c ON t.table_id = c.table_id
<include refid="genSelect"/>
order by c.sort
</select>

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