mirror of
https://gitee.com/dromara/RuoYi-Cloud-Plus.git
synced 2026-03-22 02:37:18 +08:00
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="ruoyi-auth" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="ruoyi/ruoyi-auth:2.2.0" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-auth:2.2.1" />
|
||||
<option name="buildOnly" value="true" />
|
||||
<option name="sourceFilePath" value="ruoyi-auth/Dockerfile" />
|
||||
</settings>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="ruoyi-gateway" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="ruoyi/ruoyi-gateway:2.2.0" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-gateway:2.2.1" />
|
||||
<option name="buildOnly" value="true" />
|
||||
<option name="sourceFilePath" value="ruoyi-gateway/Dockerfile" />
|
||||
</settings>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="ruoyi-gen" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="ruoyi/ruoyi-gen:2.2.0" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-gen:2.2.1" />
|
||||
<option name="buildOnly" value="true" />
|
||||
<option name="sourceFilePath" value="ruoyi-modules/ruoyi-gen/Dockerfile" />
|
||||
</settings>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="ruoyi-job" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="ruoyi/ruoyi-job:2.2.0" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-job:2.2.1" />
|
||||
<option name="buildOnly" value="true" />
|
||||
<option name="sourceFilePath" value="ruoyi-modules/ruoyi-job/Dockerfile" />
|
||||
</settings>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="ruoyi-monitor" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="ruoyi/ruoyi-monitor:2.2.0" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-monitor:2.2.1" />
|
||||
<option name="buildOnly" value="true" />
|
||||
<option name="sourceFilePath" value="ruoyi-visual/ruoyi-monitor/Dockerfile" />
|
||||
</settings>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="ruoyi-nacos" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="ruoyi/ruoyi-nacos:2.2.0" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-nacos:2.2.1" />
|
||||
<option name="buildOnly" value="true" />
|
||||
<option name="sourceFilePath" value="ruoyi-visual/ruoyi-nacos/Dockerfile" />
|
||||
</settings>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="ruoyi-resource" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="ruoyi/ruoyi-resource:2.2.0" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-resource:2.2.1" />
|
||||
<option name="buildOnly" value="true" />
|
||||
<option name="sourceFilePath" value="ruoyi-modules/ruoyi-resource/Dockerfile" />
|
||||
</settings>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="ruoyi-seata-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="ruoyi/ruoyi-seata-server:2.2.0" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-seata-server:2.2.1" />
|
||||
<option name="buildOnly" value="true" />
|
||||
<option name="sourceFilePath" value="ruoyi-visual/ruoyi-seata-server/Dockerfile" />
|
||||
</settings>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="ruoyi-sentinel-dashboard" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="ruoyi/ruoyi-sentinel-dashboard:2.2.0" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-sentinel-dashboard:2.2.1" />
|
||||
<option name="buildOnly" value="true" />
|
||||
<option name="sourceFilePath" value="ruoyi-visual/ruoyi-sentinel-dashboard/Dockerfile" />
|
||||
</settings>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="ruoyi-snailjob-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="ruoyi/ruoyi-snailjob-server:2.2.0" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-snailjob-server:2.2.1" />
|
||||
<option name="buildOnly" value="true" />
|
||||
<option name="sourceFilePath" value="ruoyi-visual/ruoyi-snailjob-server/Dockerfile" />
|
||||
</settings>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="ruoyi-system" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="ruoyi/ruoyi-system:2.2.0" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-system:2.2.1" />
|
||||
<option name="buildOnly" value="true" />
|
||||
<option name="sourceFilePath" value="ruoyi-modules/ruoyi-system/Dockerfile" />
|
||||
</settings>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="ruoyi-workflow" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="ruoyi/ruoyi-workflow:2.2.0" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-workflow:2.2.1" />
|
||||
<option name="buildOnly" value="true" />
|
||||
<option name="sourceFilePath" value="ruoyi-modules/ruoyi-workflow/Dockerfile" />
|
||||
</settings>
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
[](https://gitee.com/dromara/RuoYi-Cloud-Plus/blob/master/LICENSE)
|
||||
[](https://www.jetbrains.com/?from=RuoYi-Cloud-Plus)
|
||||
<br>
|
||||
[](https://gitee.com/dromara/RuoYi-Cloud-Plus)
|
||||
[](https://gitee.com/dromara/RuoYi-Cloud-Plus)
|
||||
[]()
|
||||
[]()
|
||||
[]()
|
||||
[]()
|
||||
|
||||
> RuoYi-Cloud-Plus `微服务通用权限管理系统` 重写 RuoYi-Cloud 全方位升级(不兼容原框架)
|
||||
|
||||
@@ -66,12 +66,14 @@ 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实现 |
|
||||
| 分布式任务调度 | 采用 SnailJob 天生支持分布式 统一的管理中心 支持多种数据库 支持分片重试DAG任务流等 | 采用 Quartz 基于数据库锁性能差 集群需要做很多配置与改造 |
|
||||
| 分布式日志中心 | 采用 ELK 业界成熟解决方案 实时收集所有服务的运行日志 快速发现定位问题 | 无 |
|
||||
| 分布式搜索引擎 | 采用 ElasticSearch、Easy-Es 以 Mybatis-Plus 方式操作 ElasticSearch | 无 |
|
||||
| 分布式消息队列 | 采用 支持 Kafka、RocketMQ、RabbitMQ 各种 延迟消息 事务消息 流消息 | 无 |
|
||||
| 分布式消息总线 | 采用 SpringCloud Bus 实现事件总线 跨服务通知 支持 Kafka、RocketMQ、RabbitMQ | 无 |
|
||||
| 分库分表功能 | 采用 Apache Sharding-Proxy 代理服务无入侵支持分库分表 只需编写分库分表规则即可 | 无 |
|
||||
| 文件存储 | 采用 Minio 分布式文件存储 天生支持多机、多硬盘、多分片、多副本存储<br/>支持权限管理 安全可靠 文件可加密存储 | 采用 本机文件存储 文件裸漏 易丢失泄漏 不支持集群有单点效应 |
|
||||
| 云存储 | 采用 AWS S3 协议客户端 支持 七牛、阿里、腾讯 等一切支持S3协议的厂家 | 不支持 |
|
||||
@@ -80,6 +82,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/>实时监控服务状态 框架还为其扩展了在线日志查看监控 | 无 |
|
||||
| 全方位监控报警 | 采用 Prometheus、Grafana 多样化采集 多模板大屏展示 实时报警监控 提供详细的搭建文档 | 无 |
|
||||
|
||||
@@ -70,12 +70,16 @@ spring:
|
||||
# 允许对象忽略json中不存在的属性
|
||||
fail_on_unknown_properties: false
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
metadata:
|
||||
# admin 监控账号密码
|
||||
username: ruoyi
|
||||
userpassword: 123456
|
||||
# sentinel 配置
|
||||
sentinel:
|
||||
# sentinel 开关
|
||||
enabled: true
|
||||
# 取消控制台懒加载
|
||||
eager: true
|
||||
transport:
|
||||
# dashboard控制台服务名 用于服务发现
|
||||
# 如无此配置将默认使用下方 dashboard 配置直接注册
|
||||
@@ -103,7 +107,8 @@ spring:
|
||||
# redis 密码必须配置
|
||||
password: ruoyi123
|
||||
database: 0
|
||||
timeout: 10s
|
||||
# 需要使用数字
|
||||
timeout: 10000
|
||||
ssl.enabled: false
|
||||
|
||||
# redisson 配置
|
||||
@@ -154,6 +159,7 @@ logging:
|
||||
org.springframework: warn
|
||||
org.apache.dubbo: warn
|
||||
com.alibaba.nacos: warn
|
||||
com.alibaba.cloud.sentinel: warn
|
||||
org.mybatis.spring.mapper: error
|
||||
org.apache.dubbo.config: error
|
||||
# 临时处理 spring 调整日志级别导致启动警告问题 不影响使用等待 alibaba 适配
|
||||
|
||||
@@ -18,6 +18,7 @@ security:
|
||||
- /auth/register
|
||||
- /auth/tenant/list
|
||||
- /resource/sms/code
|
||||
- /resource/sse/close
|
||||
- /*/v3/api-docs
|
||||
- /*/error
|
||||
- /csrf
|
||||
|
||||
@@ -24,9 +24,14 @@ spring:
|
||||
# username: ${datasource.system-postgres.username}
|
||||
# password: ${datasource.system-postgres.password}
|
||||
|
||||
# 默认/推荐使用sse推送
|
||||
sse:
|
||||
enabled: true
|
||||
path: /sse
|
||||
|
||||
websocket:
|
||||
# 如果关闭 需要和前端开关一起关闭
|
||||
enabled: true
|
||||
enabled: false
|
||||
# 路径
|
||||
path: /websocket
|
||||
# 设置访问源地址
|
||||
|
||||
@@ -13,6 +13,15 @@ spring:
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 900000
|
||||
keepaliveTime: 30000
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
metadata:
|
||||
# 解决 er 服务有 context-path 无法监控问题
|
||||
management.context-path: ${server.servlet.context-path}/actuator
|
||||
# 监控账号密码
|
||||
username: ruoyi
|
||||
userpassword: 123456
|
||||
|
||||
# snail-job 服务端配置
|
||||
snail-job:
|
||||
|
||||
@@ -26,6 +26,10 @@ spring:
|
||||
|
||||
# flowable配置
|
||||
flowable:
|
||||
# 开关 用于启动/停用工作流
|
||||
enabled: true
|
||||
process.enabled: ${flowable.enabled}
|
||||
eventregistry.enabled: ${flowable.enabled}
|
||||
# 关闭定时任务JOB
|
||||
async-executor-activate: false
|
||||
# 将databaseSchemaUpdate设置为true。当Flowable发现库与数据库表结构不一致时,会自动将数据库表结构升级至新版本。
|
||||
|
||||
@@ -2,7 +2,7 @@ version: '3'
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0.31
|
||||
image: mysql:8.0.33
|
||||
container_name: mysql
|
||||
environment:
|
||||
# 时区上海
|
||||
@@ -29,7 +29,7 @@ services:
|
||||
network_mode: "host"
|
||||
|
||||
nacos:
|
||||
image: ruoyi/ruoyi-nacos:2.2.0
|
||||
image: ruoyi/ruoyi-nacos:2.2.1
|
||||
container_name: nacos
|
||||
ports:
|
||||
- "8848:8848"
|
||||
@@ -96,7 +96,7 @@ services:
|
||||
network_mode: "host"
|
||||
|
||||
seata-server:
|
||||
image: ruoyi/ruoyi-seata-server:2.2.0
|
||||
image: ruoyi/ruoyi-seata-server:2.2.1
|
||||
container_name: seata-server
|
||||
ports:
|
||||
- "7091:7091"
|
||||
@@ -135,7 +135,7 @@ services:
|
||||
network_mode: "host"
|
||||
|
||||
sentinel:
|
||||
image: ruoyi/ruoyi-sentinel-dashboard:2.2.0
|
||||
image: ruoyi/ruoyi-sentinel-dashboard:2.2.1
|
||||
container_name: sentinel
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
@@ -150,7 +150,7 @@ services:
|
||||
network_mode: "host"
|
||||
|
||||
ruoyi-monitor:
|
||||
image: ruoyi/ruoyi-monitor:2.2.0
|
||||
image: ruoyi/ruoyi-monitor:2.2.1
|
||||
container_name: ruoyi-monitor
|
||||
environment:
|
||||
# 时区上海
|
||||
@@ -166,7 +166,7 @@ services:
|
||||
network_mode: "host"
|
||||
|
||||
ruoyi-snailjob-server:
|
||||
image: ruoyi/ruoyi-snailjob-server:2.2.0
|
||||
image: ruoyi/ruoyi-snailjob-server:2.2.1
|
||||
container_name: ruoyi-snailjob-server
|
||||
environment:
|
||||
# 时区上海
|
||||
@@ -180,7 +180,7 @@ services:
|
||||
network_mode: "host"
|
||||
|
||||
ruoyi-gateway:
|
||||
image: ruoyi/ruoyi-gateway:2.2.0
|
||||
image: ruoyi/ruoyi-gateway:2.2.1
|
||||
container_name: ruoyi-gateway
|
||||
environment:
|
||||
# 时区上海
|
||||
@@ -196,7 +196,7 @@ services:
|
||||
network_mode: "host"
|
||||
|
||||
ruoyi-auth:
|
||||
image: ruoyi/ruoyi-auth:2.2.0
|
||||
image: ruoyi/ruoyi-auth:2.2.1
|
||||
container_name: ruoyi-auth
|
||||
environment:
|
||||
# 时区上海
|
||||
@@ -212,7 +212,7 @@ services:
|
||||
network_mode: "host"
|
||||
|
||||
ruoyi-system:
|
||||
image: ruoyi/ruoyi-system:2.2.0
|
||||
image: ruoyi/ruoyi-system:2.2.1
|
||||
container_name: ruoyi-system
|
||||
environment:
|
||||
# 时区上海
|
||||
@@ -228,7 +228,7 @@ services:
|
||||
network_mode: "host"
|
||||
|
||||
ruoyi-gen:
|
||||
image: ruoyi/ruoyi-gen:2.2.0
|
||||
image: ruoyi/ruoyi-gen:2.2.1
|
||||
container_name: ruoyi-gen
|
||||
environment:
|
||||
# 时区上海
|
||||
@@ -244,7 +244,7 @@ services:
|
||||
network_mode: "host"
|
||||
|
||||
ruoyi-job:
|
||||
image: ruoyi/ruoyi-job:2.2.0
|
||||
image: ruoyi/ruoyi-job:2.2.1
|
||||
container_name: ruoyi-job
|
||||
environment:
|
||||
# 时区上海
|
||||
@@ -260,7 +260,7 @@ services:
|
||||
network_mode: "host"
|
||||
|
||||
ruoyi-resource:
|
||||
image: ruoyi/ruoyi-resource:2.2.0
|
||||
image: ruoyi/ruoyi-resource:2.2.1
|
||||
container_name: ruoyi-resource
|
||||
environment:
|
||||
# 时区上海
|
||||
@@ -276,7 +276,7 @@ services:
|
||||
network_mode: "host"
|
||||
|
||||
ruoyi-workflow:
|
||||
image: ruoyi/ruoyi-workflow:2.2.0
|
||||
image: ruoyi/ruoyi-workflow:2.2.1
|
||||
container_name: ruoyi-workflow
|
||||
environment:
|
||||
# 时区上海
|
||||
|
||||
@@ -71,10 +71,13 @@ http {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header REMOTE-HOST $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# websocket参数
|
||||
proxy_read_timeout 86400s;
|
||||
# sse 与 websocket参数
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_pass http://server/;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ agent.force_reconnection_period=${SW_AGENT_FORCE_RECONNECTION_PERIOD:1}
|
||||
agent.operation_name_threshold=${SW_AGENT_OPERATION_NAME_THRESHOLD:150}
|
||||
|
||||
# Keep tracing even the backend is not available if this value is true.
|
||||
agent.keep_tracing=${SW_AGENT_KEEP_TRACING:false}
|
||||
agent.keep_tracing=${SW_AGENT_KEEP_TRACING:true}
|
||||
|
||||
# The agent use gRPC plain text in default.
|
||||
# If true, SkyWalking agent uses TLS even no CA file detected.
|
||||
|
||||
Binary file not shown.
75
pom.xml
75
pom.xml
@@ -13,40 +13,39 @@
|
||||
<description>RuoYi-Cloud-Plus微服务系统</description>
|
||||
|
||||
<properties>
|
||||
<revision>2.2.0</revision>
|
||||
<revision>2.2.1</revision>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<java.version>17</java.version>
|
||||
<spring-boot.version>3.2.6</spring-boot.version>
|
||||
<spring-cloud.version>2023.0.2</spring-cloud.version>
|
||||
<spring-boot.version>3.2.9</spring-boot.version>
|
||||
<spring-cloud.version>2023.0.3</spring-cloud.version>
|
||||
<spring-boot-admin.version>3.2.3</spring-boot-admin.version>
|
||||
<mybatis.version>3.5.16</mybatis.version>
|
||||
<mybatis-plus.version>3.5.7</mybatis-plus.version>
|
||||
<p6spy.version>3.9.1</p6spy.version>
|
||||
<dynamic-ds.version>4.3.1</dynamic-ds.version>
|
||||
<velocity.version>2.3</velocity.version>
|
||||
<swagger.core.version>2.2.21</swagger.core.version>
|
||||
<springdoc.version>2.5.0</springdoc.version>
|
||||
<swagger.core.version>2.2.22</swagger.core.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>
|
||||
<hutool.version>5.8.27</hutool.version>
|
||||
<redisson.version>3.31.0</redisson.version>
|
||||
<easyexcel.version>4.0.2</easyexcel.version>
|
||||
<hutool.version>5.8.31</hutool.version>
|
||||
<redisson.version>3.34.1</redisson.version>
|
||||
<lock4j.version>2.2.7</lock4j.version>
|
||||
<snailjob.version>1.0.1</snailjob.version>
|
||||
<snailjob.version>1.1.2</snailjob.version>
|
||||
<satoken.version>1.38.0</satoken.version>
|
||||
<lombok.version>1.18.32</lombok.version>
|
||||
<lombok.version>1.18.34</lombok.version>
|
||||
<logstash.version>7.4</logstash.version>
|
||||
<easy-es.version>2.0.0</easy-es.version>
|
||||
<elasticsearch.version>7.14.0</elasticsearch.version>
|
||||
<skywalking-toolkit.version>9.2.0</skywalking-toolkit.version>
|
||||
<bouncycastle.version>1.76</bouncycastle.version>
|
||||
<alibaba-ttl.version>2.14.4</alibaba-ttl.version>
|
||||
<mapstruct-plus.version>1.3.6</mapstruct-plus.version>
|
||||
<mapstruct-plus.version>1.4.4</mapstruct-plus.version>
|
||||
<mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
|
||||
<justauth.version>1.16.6</justauth.version>
|
||||
<!-- 离线IP地址定位库 -->
|
||||
<ip2region.version>2.7.0</ip2region.version>
|
||||
<undertow.version>2.3.15.Final</undertow.version>
|
||||
<!-- 临时修复 fastjson 漏洞 -->
|
||||
<fastjson.version>1.2.83</fastjson.version>
|
||||
|
||||
@@ -56,9 +55,11 @@
|
||||
<okhttp.version>4.10.0</okhttp.version>
|
||||
|
||||
<!-- SMS 配置 -->
|
||||
<sms4j.version>3.2.1</sms4j.version>
|
||||
<sms4j.version>3.3.2</sms4j.version>
|
||||
<!-- 面向运行时的D-ORM依赖 -->
|
||||
<anyline.version>8.7.2-20240808</anyline.version>
|
||||
<!-- 工作流配置 -->
|
||||
<flowable.version>7.0.0</flowable.version>
|
||||
<flowable.version>7.0.1</flowable.version>
|
||||
<!-- mq配置 -->
|
||||
<rocketmq.version>2.3.0</rocketmq.version>
|
||||
|
||||
@@ -233,26 +234,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>
|
||||
|
||||
<!-- 代码生成使用模板 -->
|
||||
@@ -368,6 +353,28 @@
|
||||
<version>${ip2region.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.undertow</groupId>
|
||||
<artifactId>undertow-core</artifactId>
|
||||
<version>${undertow.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.undertow</groupId>
|
||||
<artifactId>undertow-servlet</artifactId>
|
||||
<version>${undertow.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.undertow</groupId>
|
||||
<artifactId>undertow-websockets-jsr</artifactId>
|
||||
<version>${undertow.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<artifactId>commons-compress</artifactId>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<version>1.26.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
@@ -381,12 +388,6 @@
|
||||
<version>5.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>transmittable-thread-local</artifactId>
|
||||
<version>${alibaba-ttl.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.github.linpeilie</groupId>
|
||||
<artifactId>mapstruct-plus-spring-boot-starter</artifactId>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</description>
|
||||
|
||||
<properties>
|
||||
<revision>2.2.0</revision>
|
||||
<revision>2.2.1</revision>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.dromara.auth.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.hutool.captcha.AbstractCaptcha;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
@@ -32,7 +31,6 @@ import java.time.Duration;
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@SaIgnore
|
||||
@Slf4j
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
|
||||
@@ -3,6 +3,8 @@ package org.dromara.auth.listener;
|
||||
import cn.dev33.satoken.config.SaTokenConfig;
|
||||
import cn.dev33.satoken.listener.SaTokenListener;
|
||||
import cn.dev33.satoken.stp.SaLoginModel;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.http.useragent.UserAgent;
|
||||
import cn.hutool.http.useragent.UserAgentUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -85,7 +87,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, useId:{}, token:{}", loginId, tokenValue);
|
||||
}
|
||||
|
||||
@@ -94,7 +99,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 doLogoutByLoginId, useId:{}, token:{}", loginId, tokenValue);
|
||||
}
|
||||
|
||||
@@ -103,7 +111,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, useId:{}, token:{}", loginId, tokenValue);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.apache.dubbo.config.annotation.DubboReference;
|
||||
import org.dromara.auth.form.RegisterBody;
|
||||
import org.dromara.auth.properties.CaptchaProperties;
|
||||
import org.dromara.auth.properties.UserPasswordProperties;
|
||||
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;
|
||||
@@ -205,7 +206,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;
|
||||
Integer maxRetryCount = userPasswordProperties.getMaxRetryCount();
|
||||
Integer lockTime = userPasswordProperties.getLockTime();
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.dromara.common.core.utils.ValidatorUtils;
|
||||
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.api.RemoteUserService;
|
||||
import org.dromara.system.api.domain.vo.RemoteClientVo;
|
||||
import org.dromara.system.api.model.LoginUser;
|
||||
@@ -46,10 +47,11 @@ public class EmailAuthStrategy implements IAuthStrategy {
|
||||
String tenantId = loginBody.getTenantId();
|
||||
String email = loginBody.getEmail();
|
||||
String emailCode = loginBody.getEmailCode();
|
||||
|
||||
// 通过邮箱查找用户
|
||||
LoginUser loginUser = remoteUserService.getUserInfoByEmail(email, tenantId);
|
||||
loginService.checkLogin(LoginType.EMAIL, tenantId, loginUser.getUsername(), () -> !validateEmailCode(tenantId, email, emailCode));
|
||||
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
|
||||
LoginUser user = remoteUserService.getUserInfoByEmail(email, tenantId);
|
||||
loginService.checkLogin(LoginType.EMAIL, tenantId, user.getUsername(), () -> !validateEmailCode(tenantId, email, emailCode));
|
||||
return user;
|
||||
});
|
||||
loginUser.setClientKey(client.getClientKey());
|
||||
loginUser.setDeviceType(client.getDeviceType());
|
||||
SaLoginModel model = new SaLoginModel();
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.dromara.common.core.utils.ValidatorUtils;
|
||||
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.api.RemoteUserService;
|
||||
import org.dromara.system.api.domain.vo.RemoteClientVo;
|
||||
import org.dromara.system.api.model.LoginUser;
|
||||
@@ -58,9 +59,11 @@ public class PasswordAuthStrategy implements IAuthStrategy {
|
||||
if (captchaProperties.getEnabled()) {
|
||||
validateCaptcha(tenantId, username, code, uuid);
|
||||
}
|
||||
|
||||
LoginUser loginUser = remoteUserService.getUserInfo(username, tenantId);
|
||||
loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, loginUser.getPassword()));
|
||||
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
|
||||
LoginUser user = remoteUserService.getUserInfo(username, tenantId);
|
||||
loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
|
||||
return user;
|
||||
});
|
||||
loginUser.setClientKey(client.getClientKey());
|
||||
loginUser.setDeviceType(client.getDeviceType());
|
||||
SaLoginModel model = new SaLoginModel();
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.dromara.common.core.utils.ValidatorUtils;
|
||||
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.api.RemoteUserService;
|
||||
import org.dromara.system.api.domain.vo.RemoteClientVo;
|
||||
import org.dromara.system.api.model.LoginUser;
|
||||
@@ -46,10 +47,11 @@ public class SmsAuthStrategy implements IAuthStrategy {
|
||||
String tenantId = loginBody.getTenantId();
|
||||
String phonenumber = loginBody.getPhonenumber();
|
||||
String smsCode = loginBody.getSmsCode();
|
||||
|
||||
// 通过手机号查找用户
|
||||
LoginUser loginUser = remoteUserService.getUserInfoByPhonenumber(phonenumber, tenantId);
|
||||
loginService.checkLogin(LoginType.SMS, tenantId, loginUser.getUsername(), () -> !validateSmsCode(tenantId, phonenumber, smsCode));
|
||||
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
|
||||
LoginUser user = remoteUserService.getUserInfoByPhonenumber(phonenumber, tenantId);
|
||||
loginService.checkLogin(LoginType.SMS, tenantId, user.getUsername(), () -> !validateSmsCode(tenantId, phonenumber, smsCode));
|
||||
return user;
|
||||
});
|
||||
loginUser.setClientKey(client.getClientKey());
|
||||
loginUser.setDeviceType(client.getDeviceType());
|
||||
SaLoginModel model = new SaLoginModel();
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
<module>ruoyi-common-social</module>
|
||||
<module>ruoyi-common-nacos</module>
|
||||
<module>ruoyi-common-bus</module>
|
||||
<module>ruoyi-common-sse</module>
|
||||
</modules>
|
||||
|
||||
<artifactId>ruoyi-common</artifactId>
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
</description>
|
||||
|
||||
<properties>
|
||||
<revision>2.2.0</revision>
|
||||
<spring-cloud-alibaba.version>2023.0.1.0</spring-cloud-alibaba.version>
|
||||
<revision>2.2.1</revision>
|
||||
<spring-cloud-alibaba.version>2023.0.1.2</spring-cloud-alibaba.version>
|
||||
<sentinel.version>1.8.8</sentinel.version>
|
||||
<seata.version>1.7.1</seata.version>
|
||||
<nacos.client.version>2.3.3</nacos.client.version>
|
||||
@@ -123,7 +123,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-spring-webmvc-adapter</artifactId>
|
||||
<artifactId>sentinel-spring-webmvc-v6x-adapter</artifactId>
|
||||
<version>${sentinel.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</description>
|
||||
|
||||
<properties>
|
||||
<revision>2.2.0</revision>
|
||||
<revision>2.2.1</revision>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
@@ -250,6 +250,13 @@
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- sse -->
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-sse</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
</project>
|
||||
|
||||
@@ -99,11 +99,6 @@
|
||||
<artifactId>ip2region</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>transmittable-thread-local</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -22,4 +22,9 @@ public interface CacheConstants {
|
||||
*/
|
||||
String SYS_DICT_KEY = "sys_dict:";
|
||||
|
||||
/**
|
||||
* 登录账户密码错误次数 redis key
|
||||
*/
|
||||
String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -67,6 +67,16 @@ public interface UserConstants {
|
||||
*/
|
||||
String DICT_NORMAL = "0";
|
||||
|
||||
/**
|
||||
* 通用存在标志
|
||||
*/
|
||||
String DEL_FLAG_NORMAL = "0";
|
||||
|
||||
/**
|
||||
* 通用删除标志
|
||||
*/
|
||||
String DEL_FLAG_REMOVED = "2";
|
||||
|
||||
/**
|
||||
* 是否为系统默认(是)
|
||||
*/
|
||||
|
||||
@@ -16,52 +16,27 @@
|
||||
*/
|
||||
package org.apache.dubbo.metadata.store.redis;
|
||||
|
||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
||||
import org.apache.dubbo.common.URL;
|
||||
import org.apache.dubbo.common.config.configcenter.ConfigItem;
|
||||
import org.apache.dubbo.common.logger.ErrorTypeAwareLogger;
|
||||
import org.apache.dubbo.common.logger.LoggerFactory;
|
||||
import org.apache.dubbo.common.utils.ConcurrentHashMapUtils;
|
||||
import org.apache.dubbo.common.utils.ConcurrentHashSet;
|
||||
import org.apache.dubbo.common.utils.JsonUtils;
|
||||
import org.apache.dubbo.common.utils.StringUtils;
|
||||
import org.apache.dubbo.common.utils.*;
|
||||
import org.apache.dubbo.metadata.MappingChangedEvent;
|
||||
import org.apache.dubbo.metadata.MappingListener;
|
||||
import org.apache.dubbo.metadata.MetadataInfo;
|
||||
import org.apache.dubbo.metadata.ServiceNameMapping;
|
||||
import org.apache.dubbo.metadata.report.identifier.BaseMetadataIdentifier;
|
||||
import org.apache.dubbo.metadata.report.identifier.KeyTypeEnum;
|
||||
import org.apache.dubbo.metadata.report.identifier.MetadataIdentifier;
|
||||
import org.apache.dubbo.metadata.report.identifier.ServiceMetadataIdentifier;
|
||||
import org.apache.dubbo.metadata.report.identifier.SubscriberMetadataIdentifier;
|
||||
import org.apache.dubbo.metadata.report.identifier.*;
|
||||
import org.apache.dubbo.metadata.report.support.AbstractMetadataReport;
|
||||
import org.apache.dubbo.rpc.RpcException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
||||
import redis.clients.jedis.HostAndPort;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisCluster;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
import redis.clients.jedis.JedisPoolConfig;
|
||||
import redis.clients.jedis.JedisPubSub;
|
||||
import redis.clients.jedis.Transaction;
|
||||
import redis.clients.jedis.*;
|
||||
import redis.clients.jedis.params.SetParams;
|
||||
import redis.clients.jedis.util.JedisClusterCRC16;
|
||||
|
||||
import static org.apache.dubbo.common.constants.CommonConstants.CLUSTER_KEY;
|
||||
import static org.apache.dubbo.common.constants.CommonConstants.CYCLE_REPORT_KEY;
|
||||
import static org.apache.dubbo.common.constants.CommonConstants.DEFAULT_TIMEOUT;
|
||||
import static org.apache.dubbo.common.constants.CommonConstants.GROUP_CHAR_SEPARATOR;
|
||||
import static org.apache.dubbo.common.constants.CommonConstants.QUEUES_KEY;
|
||||
import static org.apache.dubbo.common.constants.CommonConstants.TIMEOUT_KEY;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import static org.apache.dubbo.common.constants.CommonConstants.*;
|
||||
import static org.apache.dubbo.common.constants.LoggerCodeConstants.TRANSPORT_FAILED_RESPONSE;
|
||||
import static org.apache.dubbo.metadata.MetadataConstants.META_DATA_STORE_TAG;
|
||||
import static org.apache.dubbo.metadata.ServiceNameMapping.DEFAULT_MAPPING_GROUP;
|
||||
@@ -479,7 +454,7 @@ public class RedisMetadataReport extends AbstractMetadataReport {
|
||||
logger.info("sub from redis:" + key + " message:" + msg);
|
||||
String applicationNames = getMappingData(buildMappingKey(DEFAULT_MAPPING_GROUP), msg);
|
||||
MappingChangedEvent mappingChangedEvent = new MappingChangedEvent(msg, getAppNames(applicationNames));
|
||||
if (!listeners.get(msg).isEmpty()) {
|
||||
if (!CollectionUtils.isEmpty(listeners.get(msg))) {
|
||||
for (MappingListener mappingListener : listeners.get(msg)) {
|
||||
mappingListener.onEvent(mappingChangedEvent);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.dromara.common.dubbo.config;
|
||||
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
@@ -34,6 +35,10 @@ public class CustomBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
*/
|
||||
@Override
|
||||
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
|
||||
String property = System.getProperty("DUBBO_IP_TO_REGISTRY");
|
||||
if (StringUtils.isNotBlank(property)) {
|
||||
return;
|
||||
}
|
||||
// 获取 InetUtils bean,用于获取 IP 地址
|
||||
InetUtils inetUtils = beanFactory.getBean(InetUtils.class);
|
||||
String ip = "127.0.0.1";
|
||||
|
||||
@@ -67,8 +67,8 @@ public class DubboRequestFilter implements Filter {
|
||||
Result result = invoker.invoke(invocation);
|
||||
// 计算调用耗时
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
// 如果发生异常且调用的是泛化服务,则记录异常日志
|
||||
if (result.hasException() && invoker.getInterface().equals(GenericService.class)) {
|
||||
// 如果发生异常且调用的不是泛化服务,则记录异常日志
|
||||
if (result.hasException() && !invoker.getInterface().equals(GenericService.class)) {
|
||||
log.error("DUBBO - 服务异常: {},Exception={}", baselog, result.getException());
|
||||
} else {
|
||||
// 根据日志级别输出服务响应信息
|
||||
|
||||
@@ -27,6 +27,7 @@ dubbo:
|
||||
parameters:
|
||||
namespace: ${spring.profiles.active}
|
||||
database: ${spring.data.redis.database}
|
||||
timeout: ${spring.data.redis.timeout}
|
||||
# 消费者相关配置
|
||||
consumer:
|
||||
# 结果缓存(LRU算法)
|
||||
|
||||
@@ -25,6 +25,11 @@
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>easyexcel</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<artifactId>commons-compress</artifactId>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<version>1.26.2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.dromara.common.mail.config;
|
||||
|
||||
import org.dromara.common.mail.utils.MailAccount;
|
||||
import cn.hutool.extra.mail.MailAccount;
|
||||
import org.dromara.common.mail.config.properties.MailProperties;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 <user@xxx.xx>
|
||||
* </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 + "]";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,21 +50,26 @@
|
||||
<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>
|
||||
<!-- <!– Oracle –>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.oracle.database.jdbc</groupId>-->
|
||||
<!-- <artifactId>ojdbc8</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
<!-- <!– 兼容oracle低版本 –>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.oracle.database.nls</groupId>-->
|
||||
<!-- <artifactId>orai18n</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
<!-- <!– PostgreSql –>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.postgresql</groupId>-->
|
||||
<!-- <artifactId>postgresql</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
<!-- <!– SqlServer –>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.microsoft.sqlserver</groupId>-->
|
||||
<!-- <artifactId>mssql-jdbc</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -30,4 +30,12 @@ public @interface DataColumn {
|
||||
*/
|
||||
String[] value() default "dept_id";
|
||||
|
||||
/**
|
||||
* 权限标识符 用于通过菜单权限标识符来获取数据权限
|
||||
* 拥有此标识符的角色 将不会拼接此角色的数据过滤sql
|
||||
*
|
||||
* @return 权限标识符
|
||||
*/
|
||||
String permission() default "";
|
||||
|
||||
}
|
||||
|
||||
@@ -20,4 +20,11 @@ public @interface DataPermission {
|
||||
*/
|
||||
DataColumn[] value();
|
||||
|
||||
/**
|
||||
* 权限拼接标识符(用于指定连接语句的sql符号)
|
||||
* 如不填 默认 select 用 OR 其他语句用 AND
|
||||
* 内容 OR 或者 AND
|
||||
*/
|
||||
String joinStr() default "";
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.mybatis.helper.DataPermissionHelper;
|
||||
import org.dromara.common.mybatis.service.SysDataScopeService;
|
||||
import org.dromara.system.api.model.LoginUser;
|
||||
|
||||
/**
|
||||
@@ -14,9 +13,9 @@ import org.dromara.system.api.model.LoginUser;
|
||||
* 内置数据:
|
||||
* - {@code user}: 当前登录用户信息,参考 {@link LoginUser}
|
||||
* 内置服务:
|
||||
* - {@code sdss}: 系统数据权限服务,参考 {@link SysDataScopeService}
|
||||
* - {@code sdss}: 系统数据权限服务,参考 SysDataScopeService
|
||||
* 如需扩展数据,可以通过 {@link DataPermissionHelper} 进行操作
|
||||
* 如需扩展服务,可以通过 {@link SysDataScopeService} 自行编写
|
||||
* 如需扩展服务,可以通过 SysDataScopeService 自行编写
|
||||
* </p>
|
||||
*
|
||||
* @author Lion Li
|
||||
@@ -33,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 ");
|
||||
|
||||
|
||||
@@ -47,6 +47,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);
|
||||
@@ -71,6 +75,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);
|
||||
|
||||
@@ -99,7 +99,7 @@ public class PlusDataPermissionHandler {
|
||||
return where;
|
||||
}
|
||||
// 构造数据过滤条件的 SQL 片段
|
||||
String dataFilterSql = buildDataFilter(dataPermission.value(), isSelect);
|
||||
String dataFilterSql = buildDataFilter(dataPermission, isSelect);
|
||||
if (StringUtils.isBlank(dataFilterSql)) {
|
||||
return where;
|
||||
}
|
||||
@@ -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]);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
public 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 处理执行方法
|
||||
*/
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -27,17 +27,6 @@
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-jwt</artifactId>
|
||||
<version>${satoken.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-jwt</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- RuoYi Api System -->
|
||||
|
||||
@@ -2,10 +2,12 @@ package org.dromara.common.security.config;
|
||||
|
||||
import cn.dev33.satoken.SaManager;
|
||||
import cn.dev33.satoken.filter.SaServletFilter;
|
||||
import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil;
|
||||
import cn.dev33.satoken.interceptor.SaInterceptor;
|
||||
import cn.dev33.satoken.same.SaSameUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
import org.dromara.common.core.constant.HttpStatus;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
@@ -35,7 +37,7 @@ public class SecurityConfiguration implements WebMvcConfigurer {
|
||||
public SaServletFilter getSaServletFilter() {
|
||||
return new SaServletFilter()
|
||||
.addInclude("/**")
|
||||
.addExclude("/actuator/**")
|
||||
.addExclude("/actuator", "/actuator/**")
|
||||
.setAuth(obj -> {
|
||||
if (SaManager.getConfig().getCheckSameToken()) {
|
||||
SaSameUtil.checkCurrentRequestToken();
|
||||
@@ -44,4 +46,19 @@ public class SecurityConfiguration implements WebMvcConfigurer {
|
||||
.setError(e -> SaResult.error("认证失败,无法访问系统资源").setCode(HttpStatus.UNAUTHORIZED));
|
||||
}
|
||||
|
||||
/**
|
||||
* 对 actuator 健康检查接口 做账号密码鉴权
|
||||
*/
|
||||
@Bean
|
||||
public SaServletFilter actuatorFilter() {
|
||||
String username = SpringUtils.getProperty("spring.cloud.nacos.discovery.metadata.username");
|
||||
String password = SpringUtils.getProperty("spring.cloud.nacos.discovery.metadata.userpassword");
|
||||
return new SaServletFilter()
|
||||
.addInclude("/actuator", "/actuator/**")
|
||||
.setAuth(obj -> {
|
||||
SaHttpBasicUtil.check(username + ":" + password);
|
||||
})
|
||||
.setError(e -> SaResult.error(e.getMessage()).setCode(HttpStatus.UNAUTHORIZED));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
//可自行添加其他脱敏策略
|
||||
|
||||
|
||||
@@ -1,265 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.alibaba.cloud.sentinel.custom;
|
||||
|
||||
import com.alibaba.cloud.commons.lang.StringUtils;
|
||||
import com.alibaba.cloud.sentinel.SentinelProperties;
|
||||
import com.alibaba.cloud.sentinel.datasource.converter.JsonConverter;
|
||||
import com.alibaba.cloud.sentinel.datasource.converter.XmlConverter;
|
||||
import com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect;
|
||||
import com.alibaba.csp.sentinel.config.SentinelConfig;
|
||||
import com.alibaba.csp.sentinel.init.InitExecutor;
|
||||
import com.alibaba.csp.sentinel.log.LogBase;
|
||||
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule;
|
||||
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
|
||||
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule;
|
||||
import com.alibaba.csp.sentinel.slots.system.SystemRule;
|
||||
import com.alibaba.csp.sentinel.transport.config.TransportConfig;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.dromara.common.core.utils.StreamUtils;
|
||||
import org.dromara.common.sentinel.config.properties.SentinelCustomProperties;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.alibaba.cloud.sentinel.SentinelConstants.BLOCK_PAGE_URL_CONF_KEY;
|
||||
import static com.alibaba.csp.sentinel.config.SentinelConfig.setConfig;
|
||||
|
||||
/**
|
||||
* 改造sentinel自动配置 支持服务名注册
|
||||
*
|
||||
* @author Lion Li
|
||||
*
|
||||
* @author xiaojing
|
||||
* @author jiashuai.xie
|
||||
* @author <a href="mailto:fangjian0423@gmail.com">Jim</a>
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnProperty(name = "spring.cloud.sentinel.enabled", matchIfMissing = true)
|
||||
@EnableConfigurationProperties({SentinelProperties.class, SentinelCustomProperties.class})
|
||||
public class SentinelAutoConfiguration {
|
||||
|
||||
@Value("${project.name:${spring.application.name:}}")
|
||||
private String projectName;
|
||||
|
||||
@Autowired
|
||||
private SentinelProperties properties;
|
||||
@Autowired
|
||||
private SentinelCustomProperties customProperties;
|
||||
@Autowired
|
||||
private DiscoveryClient discoveryClient;
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
if (StringUtils.isEmpty(System.getProperty(LogBase.LOG_DIR))
|
||||
&& StringUtils.isNotBlank(properties.getLog().getDir())) {
|
||||
System.setProperty(LogBase.LOG_DIR, properties.getLog().getDir());
|
||||
}
|
||||
if (StringUtils.isEmpty(System.getProperty(LogBase.LOG_NAME_USE_PID))
|
||||
&& properties.getLog().isSwitchPid()) {
|
||||
System.setProperty(LogBase.LOG_NAME_USE_PID,
|
||||
String.valueOf(properties.getLog().isSwitchPid()));
|
||||
}
|
||||
if (StringUtils.isEmpty(System.getProperty(SentinelConfig.APP_NAME_PROP_KEY))
|
||||
&& StringUtils.isNotBlank(projectName)) {
|
||||
System.setProperty(SentinelConfig.APP_NAME_PROP_KEY, projectName);
|
||||
}
|
||||
if (StringUtils.isEmpty(System.getProperty(TransportConfig.SERVER_PORT))
|
||||
&& StringUtils.isNotBlank(properties.getTransport().getPort())) {
|
||||
System.setProperty(TransportConfig.SERVER_PORT,
|
||||
properties.getTransport().getPort());
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(customProperties.getServerName())) {
|
||||
List<ServiceInstance> instances = discoveryClient.getInstances(customProperties.getServerName());
|
||||
String serverList = StreamUtils.join(instances, instance ->
|
||||
String.format("http://%s:%s", instance.getHost(), instance.getPort()));
|
||||
System.setProperty(TransportConfig.CONSOLE_SERVER, serverList);
|
||||
} else {
|
||||
if (StringUtils.isEmpty(System.getProperty(TransportConfig.CONSOLE_SERVER))
|
||||
&& StringUtils.isNotBlank(properties.getTransport().getDashboard())) {
|
||||
System.setProperty(TransportConfig.CONSOLE_SERVER,
|
||||
properties.getTransport().getDashboard());
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(System.getProperty(TransportConfig.HEARTBEAT_INTERVAL_MS))
|
||||
&& StringUtils
|
||||
.isNotBlank(properties.getTransport().getHeartbeatIntervalMs())) {
|
||||
System.setProperty(TransportConfig.HEARTBEAT_INTERVAL_MS,
|
||||
properties.getTransport().getHeartbeatIntervalMs());
|
||||
}
|
||||
if (StringUtils.isEmpty(System.getProperty(TransportConfig.HEARTBEAT_CLIENT_IP))
|
||||
&& StringUtils.isNotBlank(properties.getTransport().getClientIp())) {
|
||||
System.setProperty(TransportConfig.HEARTBEAT_CLIENT_IP,
|
||||
properties.getTransport().getClientIp());
|
||||
}
|
||||
if (StringUtils.isEmpty(System.getProperty(SentinelConfig.CHARSET))
|
||||
&& StringUtils.isNotBlank(properties.getMetric().getCharset())) {
|
||||
System.setProperty(SentinelConfig.CHARSET,
|
||||
properties.getMetric().getCharset());
|
||||
}
|
||||
if (StringUtils
|
||||
.isEmpty(System.getProperty(SentinelConfig.SINGLE_METRIC_FILE_SIZE))
|
||||
&& StringUtils.isNotBlank(properties.getMetric().getFileSingleSize())) {
|
||||
System.setProperty(SentinelConfig.SINGLE_METRIC_FILE_SIZE,
|
||||
properties.getMetric().getFileSingleSize());
|
||||
}
|
||||
if (StringUtils
|
||||
.isEmpty(System.getProperty(SentinelConfig.TOTAL_METRIC_FILE_COUNT))
|
||||
&& StringUtils.isNotBlank(properties.getMetric().getFileTotalCount())) {
|
||||
System.setProperty(SentinelConfig.TOTAL_METRIC_FILE_COUNT,
|
||||
properties.getMetric().getFileTotalCount());
|
||||
}
|
||||
if (StringUtils.isEmpty(System.getProperty(SentinelConfig.COLD_FACTOR))
|
||||
&& StringUtils.isNotBlank(properties.getFlow().getColdFactor())) {
|
||||
System.setProperty(SentinelConfig.COLD_FACTOR,
|
||||
properties.getFlow().getColdFactor());
|
||||
}
|
||||
if (StringUtils.isNotBlank(properties.getBlockPage())) {
|
||||
setConfig(BLOCK_PAGE_URL_CONF_KEY, properties.getBlockPage());
|
||||
}
|
||||
|
||||
// earlier initialize
|
||||
if (properties.isEager()) {
|
||||
InitExecutor.doInit();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public SentinelResourceAspect sentinelResourceAspect() {
|
||||
return new SentinelResourceAspect();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnClass(name = "org.springframework.web.client.RestTemplate")
|
||||
@ConditionalOnProperty(name = "resttemplate.sentinel.enabled", havingValue = "true",
|
||||
matchIfMissing = true)
|
||||
public SentinelBeanPostProcessor sentinelBeanPostProcessor(
|
||||
ApplicationContext applicationContext) {
|
||||
return new SentinelBeanPostProcessor(applicationContext);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public SentinelDataSourceHandler sentinelDataSourceHandler(
|
||||
DefaultListableBeanFactory beanFactory, SentinelProperties sentinelProperties,
|
||||
Environment env) {
|
||||
return new SentinelDataSourceHandler(beanFactory, sentinelProperties, env);
|
||||
}
|
||||
|
||||
@ConditionalOnClass(ObjectMapper.class)
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
protected static class SentinelConverterConfiguration {
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
protected static class SentinelJsonConfiguration {
|
||||
|
||||
private ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
public SentinelJsonConfiguration() {
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
|
||||
false);
|
||||
}
|
||||
|
||||
@Bean("sentinel-json-flow-converter")
|
||||
public JsonConverter jsonFlowConverter() {
|
||||
return new JsonConverter(objectMapper, FlowRule.class);
|
||||
}
|
||||
|
||||
@Bean("sentinel-json-degrade-converter")
|
||||
public JsonConverter jsonDegradeConverter() {
|
||||
return new JsonConverter(objectMapper, DegradeRule.class);
|
||||
}
|
||||
|
||||
@Bean("sentinel-json-system-converter")
|
||||
public JsonConverter jsonSystemConverter() {
|
||||
return new JsonConverter(objectMapper, SystemRule.class);
|
||||
}
|
||||
|
||||
@Bean("sentinel-json-authority-converter")
|
||||
public JsonConverter jsonAuthorityConverter() {
|
||||
return new JsonConverter(objectMapper, AuthorityRule.class);
|
||||
}
|
||||
|
||||
@Bean("sentinel-json-param-flow-converter")
|
||||
public JsonConverter jsonParamFlowConverter() {
|
||||
return new JsonConverter(objectMapper, ParamFlowRule.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnClass(XmlMapper.class)
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
protected static class SentinelXmlConfiguration {
|
||||
|
||||
private XmlMapper xmlMapper = new XmlMapper();
|
||||
|
||||
public SentinelXmlConfiguration() {
|
||||
xmlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
|
||||
false);
|
||||
}
|
||||
|
||||
@Bean("sentinel-xml-flow-converter")
|
||||
public XmlConverter xmlFlowConverter() {
|
||||
return new XmlConverter(xmlMapper, FlowRule.class);
|
||||
}
|
||||
|
||||
@Bean("sentinel-xml-degrade-converter")
|
||||
public XmlConverter xmlDegradeConverter() {
|
||||
return new XmlConverter(xmlMapper, DegradeRule.class);
|
||||
}
|
||||
|
||||
@Bean("sentinel-xml-system-converter")
|
||||
public XmlConverter xmlSystemConverter() {
|
||||
return new XmlConverter(xmlMapper, SystemRule.class);
|
||||
}
|
||||
|
||||
@Bean("sentinel-xml-authority-converter")
|
||||
public XmlConverter xmlAuthorityConverter() {
|
||||
return new XmlConverter(xmlMapper, AuthorityRule.class);
|
||||
}
|
||||
|
||||
@Bean("sentinel-xml-param-flow-converter")
|
||||
public XmlConverter xmlParamFlowConverter() {
|
||||
return new XmlConverter(xmlMapper, ParamFlowRule.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.dromara.common.sentinel.config;
|
||||
|
||||
import com.alibaba.cloud.commons.lang.StringUtils;
|
||||
import com.alibaba.cloud.sentinel.SentinelProperties;
|
||||
import com.alibaba.cloud.sentinel.custom.SentinelAutoConfiguration;
|
||||
import com.alibaba.csp.sentinel.init.InitExecutor;
|
||||
import com.alibaba.csp.sentinel.transport.config.TransportConfig;
|
||||
import org.dromara.common.core.utils.StreamUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Lion Li
|
||||
*/
|
||||
@AutoConfiguration(before = SentinelAutoConfiguration.class)
|
||||
@EnableConfigurationProperties({SentinelProperties.class, SentinelCustomProperties.class})
|
||||
public class SentinelCustomAutoConfiguration {
|
||||
|
||||
@Autowired
|
||||
private SentinelProperties properties;
|
||||
@Autowired
|
||||
private SentinelCustomProperties customProperties;
|
||||
@Autowired
|
||||
private DiscoveryClient discoveryClient;
|
||||
|
||||
@Bean
|
||||
public void sentinelInit() {
|
||||
if (StringUtils.isNotBlank(customProperties.getServerName())) {
|
||||
List<ServiceInstance> instances = discoveryClient.getInstances(customProperties.getServerName());
|
||||
String serverList = StreamUtils.join(instances, instance ->
|
||||
String.format("http://%s:%s", instance.getHost(), instance.getPort()));
|
||||
System.setProperty(TransportConfig.CONSOLE_SERVER, serverList);
|
||||
} else {
|
||||
if (StringUtils.isEmpty(System.getProperty(TransportConfig.CONSOLE_SERVER))
|
||||
&& StringUtils.isNotBlank(properties.getTransport().getDashboard())) {
|
||||
System.setProperty(TransportConfig.CONSOLE_SERVER,
|
||||
properties.getTransport().getDashboard());
|
||||
}
|
||||
}
|
||||
// 手动初始化 sentinel
|
||||
InitExecutor.doInit();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.dromara.common.sentinel.config.properties;
|
||||
package org.dromara.common.sentinel.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@@ -1 +1 @@
|
||||
|
||||
org.dromara.common.sentinel.config.SentinelCustomAutoConfiguration
|
||||
|
||||
@@ -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);
|
||||
|
||||
40
ruoyi-common/ruoyi-common-sse/pom.xml
Normal file
40
ruoyi-common/ruoyi-common-sse/pom.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webmvc</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.dromara.common.sse.controller;
|
||||
|
||||
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 连接
|
||||
*/
|
||||
@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 {
|
||||
// 销毁时不需要做什么 此方法避免无用操作报错
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package org.dromara.common.sse.core;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.sse.dto.SseMessageDto;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 管理 Server-Sent Events (SSE) 连接
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Slf4j
|
||||
public class SseEmitterManager {
|
||||
|
||||
/**
|
||||
* 订阅的频道
|
||||
*/
|
||||
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) {
|
||||
List<Long> unsentUserIds = new ArrayList<>();
|
||||
// 当前服务内用户,直接发送消息
|
||||
for (Long userId : sseMessageDto.getUserIds()) {
|
||||
if (USER_TOKEN_EMITTERS.containsKey(userId)) {
|
||||
sendMessage(userId, sseMessageDto.getMessage());
|
||||
continue;
|
||||
}
|
||||
unsentUserIds.add(userId);
|
||||
}
|
||||
// 不在当前服务内用户,发布订阅消息
|
||||
if (CollUtil.isNotEmpty(unsentUserIds)) {
|
||||
SseMessageDto broadcastMessage = new SseMessageDto();
|
||||
broadcastMessage.setMessage(sseMessageDto.getMessage());
|
||||
broadcastMessage.setUserIds(unsentUserIds);
|
||||
RedisUtils.publish(SSE_TOPIC, broadcastMessage, consumer -> {
|
||||
log.info("SSE发送主题订阅消息topic:{} session keys:{} message:{}",
|
||||
SSE_TOPIC, unsentUserIds, sseMessageDto.getMessage());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向所有的用户发布订阅的消息(群发)
|
||||
*
|
||||
* @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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.dromara.common.sse.config.SseAutoConfiguration
|
||||
@@ -1,8 +1,9 @@
|
||||
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 +12,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 +30,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 +41,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -39,6 +39,19 @@
|
||||
<artifactId>spring-boot-starter-undertow</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.undertow</groupId>
|
||||
<artifactId>undertow-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.undertow</groupId>
|
||||
<artifactId>undertow-servlet</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.undertow</groupId>
|
||||
<artifactId>undertow-websockets-jsr</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- SpringBoot Actuator -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@@ -16,10 +16,13 @@ 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;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*
|
||||
@@ -89,6 +92,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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截未知的运行时异常
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -82,17 +82,6 @@
|
||||
<artifactId>ruoyi-common-tenant</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 短信 用哪个导入哪个依赖 -->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.aliyun</groupId>-->
|
||||
<!-- <artifactId>dysmsapi20170525</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.tencentcloudapi</groupId>-->
|
||||
<!-- <artifactId>tencentcloud-sdk-java-sms</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-elasticsearch</artifactId>
|
||||
|
||||
@@ -11,6 +11,7 @@ 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 +45,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> selectBatchIds(@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);
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import cn.hutool.core.bean.BeanUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
@@ -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.selectBatchIds(ids);
|
||||
if (list.size() != ids.size()) {
|
||||
throw new ServiceException("您没有删除权限!");
|
||||
}
|
||||
}
|
||||
return baseMapper.deleteByIds(ids) > 0;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
1. rabbitmq: 普通消息、延迟队列
|
||||
2. rocketmq:普通消息、事务消息、延迟消息
|
||||
3. kafka:普通消息、stream流的使用
|
||||
3. kafka:普通消息
|
||||
|
||||
|
||||
## 使用方式
|
||||
|
||||
@@ -33,10 +33,6 @@
|
||||
<groupId>org.springframework.kafka</groupId>
|
||||
<artifactId>spring-kafka</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.kafka</groupId>
|
||||
<artifactId>kafka-streams</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package org.dromara.stream.config;
|
||||
|
||||
import org.apache.kafka.streams.StreamsBuilder;
|
||||
import org.apache.kafka.streams.kstream.KStream;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.kafka.annotation.EnableKafkaStreams;
|
||||
|
||||
/**
|
||||
* kafka stream 配置
|
||||
*
|
||||
* @author LionLi
|
||||
*/
|
||||
@Configuration
|
||||
@EnableKafkaStreams
|
||||
public class KafkaStreamsConfig {
|
||||
|
||||
@Bean
|
||||
public KStream<String, String> demoStream(StreamsBuilder builder) {
|
||||
// 输入主题
|
||||
KStream<String, String> source = builder.stream("input-topic");
|
||||
// 转换逻辑:这里只是简单地将消息转换为大写
|
||||
KStream<String, String> processed = source.mapValues(value -> value.toUpperCase());
|
||||
// 输出到另一个主题
|
||||
processed.to("output-topic");
|
||||
return source;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,11 +30,12 @@ spring:
|
||||
producer:
|
||||
key-serializer: org.apache.kafka.common.serialization.StringSerializer
|
||||
value-serializer: org.apache.kafka.common.serialization.StringSerializer
|
||||
streams:
|
||||
properties:
|
||||
application.id: kafka-streams-id # 应用ID
|
||||
# 以下配置均应该在 kafka 配置文件内编写 写到此处是为了方便调试
|
||||
properties:
|
||||
auto.create.topics.enable: true # 开启自动创建话题功能,默认是false,根据需要设置
|
||||
# 开启自动创建话题功能,默认是false,根据需要设置
|
||||
auto.create.topics.enable: true
|
||||
# 默认副本数 1 避免单机使用无法创建 topic
|
||||
default.replication.factor: 1
|
||||
|
||||
--- # rocketmq 配置
|
||||
rocketmq:
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package org.dromara.gateway.filter;
|
||||
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil;
|
||||
import cn.dev33.satoken.reactor.context.SaReactorSyncHolder;
|
||||
import cn.dev33.satoken.reactor.filter.SaReactorFilter;
|
||||
import cn.dev33.satoken.router.SaRouter;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
import org.dromara.common.core.constant.HttpStatus;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.gateway.config.properties.IgnoreWhiteProperties;
|
||||
@@ -30,7 +32,7 @@ public class AuthFilter {
|
||||
return new SaReactorFilter()
|
||||
// 拦截地址
|
||||
.addInclude("/**")
|
||||
.addExclude("/favicon.ico", "/actuator/**")
|
||||
.addExclude("/favicon.ico", "/actuator", "/actuator/**")
|
||||
// 鉴权方法:每次访问进入
|
||||
.setAuth(obj -> {
|
||||
// 登录校验 -- 拦截所有路由
|
||||
@@ -65,4 +67,20 @@ public class AuthFilter {
|
||||
return SaResult.error("认证失败,无法访问系统资源").setCode(HttpStatus.UNAUTHORIZED);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 对 actuator 健康检查接口 做账号密码鉴权
|
||||
*/
|
||||
@Bean
|
||||
public SaReactorFilter actuatorFilter() {
|
||||
String username = SpringUtils.getProperty("spring.cloud.nacos.discovery.metadata.username");
|
||||
String password = SpringUtils.getProperty("spring.cloud.nacos.discovery.metadata.userpassword");
|
||||
return new SaReactorFilter()
|
||||
.addInclude("/actuator", "/actuator/**")
|
||||
.setAuth(obj -> {
|
||||
SaHttpBasicUtil.check(username + ":" + password);
|
||||
})
|
||||
.setError(e -> SaResult.error(e.getMessage()).setCode(HttpStatus.UNAUTHORIZED));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ package org.dromara.gateway.filter;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.gateway.config.properties.ApiDecryptProperties;
|
||||
import org.dromara.gateway.config.properties.CustomGatewayProperties;
|
||||
import org.dromara.gateway.utils.WebFluxUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||
@@ -75,6 +75,9 @@ public class GlobalLogFilter implements GlobalFilter, Ordered {
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
// 日志处理器在负载均衡器之后执行 负载均衡器会导致线程切换 无法获取上下文内容
|
||||
// 如需在日志内操作线程上下文 例如获取登录用户数据等 可以打开下方注释代码
|
||||
// return ReactiveLoadBalancerClientFilter.LOAD_BALANCER_CLIENT_FILTER_ORDER - 1;
|
||||
return Ordered.LOWEST_PRECEDENCE;
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,37 @@
|
||||
<artifactId>ruoyi-common-security</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>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.springframework.stereotype.Component;
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "gen")
|
||||
public class GenConfig {
|
||||
|
||||
/**
|
||||
* 作者
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package org.dromara.gen.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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -56,7 +56,7 @@ public class GenController extends BaseController {
|
||||
GenTable table = genTableService.selectGenTableById(tableId);
|
||||
List<GenTable> tables = genTableService.selectGenTableAll();
|
||||
List<GenTableColumn> list = genTableService.selectGenTableColumnListByTableId(tableId);
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
Map<String, Object> map = new HashMap<>(3);
|
||||
map.put("info", table);
|
||||
map.put("rows", list);
|
||||
map.put("tables", tables);
|
||||
|
||||
@@ -16,7 +16,6 @@ import org.dromara.common.mybatis.core.domain.BaseEntity;
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("gen_table_column")
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
package org.dromara.gen.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.gen.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);
|
||||
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package org.dromara.gen.mapper;
|
||||
|
||||
import com.baomidou.dynamic.datasource.annotation.DS;
|
||||
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
import org.dromara.gen.domain.GenTable;
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 查询所有表信息
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
@@ -59,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_"};
|
||||
|
||||
/**
|
||||
* 查询业务字段列表
|
||||
*
|
||||
@@ -95,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,
|
||||
@@ -103,11 +110,67 @@ 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 -> !StringUtils.containsAnyIgnoreCase(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);
|
||||
}
|
||||
|
||||
@@ -121,7 +184,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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,7 +268,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);
|
||||
@@ -199,6 +284,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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览代码
|
||||
*
|
||||
@@ -294,7 +405,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("同步数据失败,原表结构不存在");
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user