mirror of
https://github.com/dromara/RuoYi-Vue-Plus.git
synced 2025-09-30 15:16:41 +08:00
merge 合并5.x分支代码
This commit is contained in:
commit
dc3c86f0d7
@ -2,7 +2,7 @@
|
|||||||
<configuration default="false" name="ruoyi-monitor-admin" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
<configuration default="false" name="ruoyi-monitor-admin" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||||
<deployment type="dockerfile">
|
<deployment type="dockerfile">
|
||||||
<settings>
|
<settings>
|
||||||
<option name="imageTag" value="ruoyi/ruoyi-monitor-admin:5.0.0" />
|
<option name="imageTag" value="ruoyi/ruoyi-monitor-admin:5.1.0" />
|
||||||
<option name="buildOnly" value="true" />
|
<option name="buildOnly" value="true" />
|
||||||
<option name="sourceFilePath" value="ruoyi-extend/ruoyi-monitor-admin/Dockerfile" />
|
<option name="sourceFilePath" value="ruoyi-extend/ruoyi-monitor-admin/Dockerfile" />
|
||||||
</settings>
|
</settings>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="ruoyi-xxl-job-admin" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
<configuration default="false" name="ruoyi-powerjob-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||||
<deployment type="dockerfile">
|
<deployment type="dockerfile">
|
||||||
<settings>
|
<settings>
|
||||||
<option name="imageTag" value="ruoyi/ruoyi-xxl-job-admin:5.0.0" />
|
<option name="imageTag" value="ruoyi/ruoyi-powerjob-server:5.1.0" />
|
||||||
<option name="buildOnly" value="true" />
|
<option name="buildOnly" value="true" />
|
||||||
<option name="sourceFilePath" value="ruoyi-extend/ruoyi-xxl-job-admin/Dockerfile" />
|
<option name="sourceFilePath" value="ruoyi-extend/ruoyi-powerjob-server/Dockerfile" />
|
||||||
</settings>
|
</settings>
|
||||||
</deployment>
|
</deployment>
|
||||||
<method v="2" />
|
<method v="2" />
|
@ -2,7 +2,7 @@
|
|||||||
<configuration default="false" name="ruoyi-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
<configuration default="false" name="ruoyi-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||||
<deployment type="dockerfile">
|
<deployment type="dockerfile">
|
||||||
<settings>
|
<settings>
|
||||||
<option name="imageTag" value="ruoyi/ruoyi-server:5.0.0" />
|
<option name="imageTag" value="ruoyi/ruoyi-server:5.1.0" />
|
||||||
<option name="buildOnly" value="true" />
|
<option name="buildOnly" value="true" />
|
||||||
<option name="sourceFilePath" value="ruoyi-admin/Dockerfile" />
|
<option name="sourceFilePath" value="ruoyi-admin/Dockerfile" />
|
||||||
</settings>
|
</settings>
|
||||||
|
11
README.md
11
README.md
@ -9,7 +9,7 @@
|
|||||||
[](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/master/LICENSE)
|
[](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/master/LICENSE)
|
||||||
[](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
|
[](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
|
||||||
<br>
|
<br>
|
||||||
[](https://gitee.com/dromara/RuoYi-Vue-Plus)
|
[](https://gitee.com/dromara/RuoYi-Vue-Plus)
|
||||||
[]()
|
[]()
|
||||||
[]()
|
[]()
|
||||||
[]()
|
[]()
|
||||||
@ -35,6 +35,7 @@
|
|||||||
| Web容器 | 采用 Undertow 基于 XNIO 的高性能容器 | 采用 Tomcat |
|
| Web容器 | 采用 Undertow 基于 XNIO 的高性能容器 | 采用 Tomcat |
|
||||||
| 权限认证 | 采用 Sa-Token、Jwt 静态使用功能齐全 低耦合 高扩展 | Spring Security 配置繁琐扩展性极差 |
|
| 权限认证 | 采用 Sa-Token、Jwt 静态使用功能齐全 低耦合 高扩展 | Spring Security 配置繁琐扩展性极差 |
|
||||||
| 权限注解 | 采用 Sa-Token 支持注解 登录校验、角色校验、权限校验、二级认证校验、HttpBasic校验、忽略校验<br/>角色与权限校验支持多种条件 如 `AND` `OR` 或 `权限 OR 角色` 等复杂表达式 | 只支持是否存在匹配 |
|
| 权限注解 | 采用 Sa-Token 支持注解 登录校验、角色校验、权限校验、二级认证校验、HttpBasic校验、忽略校验<br/>角色与权限校验支持多种条件 如 `AND` `OR` 或 `权限 OR 角色` 等复杂表达式 | 只支持是否存在匹配 |
|
||||||
|
| 三方鉴权 | 采用 JustAuth 第三方登录组件 支持微信、钉钉等数十种三方认证 | 无 |
|
||||||
| 关系数据库支持 | 原生支持 MySQL、Oracle、PostgreSQL、SQLServer<br/>可同时使用异构切换 | 支持 Mysql、Oracle 不支持同时使用、不支持异构切换 |
|
| 关系数据库支持 | 原生支持 MySQL、Oracle、PostgreSQL、SQLServer<br/>可同时使用异构切换 | 支持 Mysql、Oracle 不支持同时使用、不支持异构切换 |
|
||||||
| 缓存数据库 | 支持 Redis 5-7 支持大部分新功能特性 如 分布式限流、分布式队列 | Redis 简单 get set 支持 |
|
| 缓存数据库 | 支持 Redis 5-7 支持大部分新功能特性 如 分布式限流、分布式队列 | Redis 简单 get set 支持 |
|
||||||
| Redis客户端 | 采用 Redisson Redis官方推荐 基于Netty的客户端工具<br/>支持Redis 90%以上的命令 底层优化规避很多不正确的用法 例如: keys被转换为scan<br/>支持单机、哨兵、单主集群、多主集群等模式 | Lettuce + RedisTemplate 支持模式少 工具使用繁琐<br/>连接池采用 common-pool Bug多经常性出问题 |
|
| Redis客户端 | 采用 Redisson Redis官方推荐 基于Netty的客户端工具<br/>支持Redis 90%以上的命令 底层优化规避很多不正确的用法 例如: keys被转换为scan<br/>支持单机、哨兵、单主集群、多主集群等模式 | Lettuce + RedisTemplate 支持模式少 工具使用繁琐<br/>连接池采用 common-pool Bug多经常性出问题 |
|
||||||
@ -45,6 +46,7 @@
|
|||||||
| 数据权限 | 采用 Mybatis-Plus 插件 自行分析拼接SQL 无感式过滤<br/>只需为Mapper设置好注解条件 支持多种自定义 不限于部门角色 | 采用 注解+aop 实现 基于部门角色 生成的sql兼容性差 不支持其他业务扩展<br/>生成sql后需手动拼接到具体业务sql上 对于多个Mapper查询不起作用 |
|
| 数据权限 | 采用 Mybatis-Plus 插件 自行分析拼接SQL 无感式过滤<br/>只需为Mapper设置好注解条件 支持多种自定义 不限于部门角色 | 采用 注解+aop 实现 基于部门角色 生成的sql兼容性差 不支持其他业务扩展<br/>生成sql后需手动拼接到具体业务sql上 对于多个Mapper查询不起作用 |
|
||||||
| 数据脱敏 | 采用 注解 + jackson 序列化期间脱敏 支持不同模块不同的脱敏条件<br/>支持多种策略 如身份证、手机号、地址、邮箱、银行卡等 可自行扩展 | 无 |
|
| 数据脱敏 | 采用 注解 + jackson 序列化期间脱敏 支持不同模块不同的脱敏条件<br/>支持多种策略 如身份证、手机号、地址、邮箱、银行卡等 可自行扩展 | 无 |
|
||||||
| 数据加解密 | 采用 注解 + mybatis 拦截器 对存取数据期间自动加解密<br/>支持多种策略 如BASE64、AES、RSA、SM2、SM4等 | 无 |
|
| 数据加解密 | 采用 注解 + mybatis 拦截器 对存取数据期间自动加解密<br/>支持多种策略 如BASE64、AES、RSA、SM2、SM4等 | 无 |
|
||||||
|
| 接口传输加密 | 采用 动态 AES + RSA 加密请求 body 每一次请求秘钥都不同大幅度降低可破解性 | 无 |
|
||||||
| 数据翻译 | 采用 注解 + jackson 序列化期间动态修改数据 数据进行翻译<br/>支持多种模式: `映射翻译` `直接翻译` `其他扩展条件翻译` 接口化两步即可完成自定义扩展 内置多种翻译实现 | 无 |
|
| 数据翻译 | 采用 注解 + jackson 序列化期间动态修改数据 数据进行翻译<br/>支持多种模式: `映射翻译` `直接翻译` `其他扩展条件翻译` 接口化两步即可完成自定义扩展 内置多种翻译实现 | 无 |
|
||||||
| 多数据源框架 | 采用 dynamic-datasource 支持世面大部分数据库<br/>通过yml配置即可动态管理异构不同种类的数据库 也可通过前端页面添加数据源<br/>支持spel表达式从请求头参数等条件切换数据源 | 基于 druid 手动编写代码配置数据源 配置繁琐 支持性差 |
|
| 多数据源框架 | 采用 dynamic-datasource 支持世面大部分数据库<br/>通过yml配置即可动态管理异构不同种类的数据库 也可通过前端页面添加数据源<br/>支持spel表达式从请求头参数等条件切换数据源 | 基于 druid 手动编写代码配置数据源 配置繁琐 支持性差 |
|
||||||
| 多数据源事务 | 采用 dynamic-datasource 支持多数据源不同种类的数据库事务回滚 | 不支持 |
|
| 多数据源事务 | 采用 dynamic-datasource 支持多数据源不同种类的数据库事务回滚 | 不支持 |
|
||||||
@ -54,10 +56,10 @@
|
|||||||
| 序列化 | 采用 Jackson Spring官方内置序列化 靠谱!!! | 采用 fastjson bugjson 远近闻名 |
|
| 序列化 | 采用 Jackson Spring官方内置序列化 靠谱!!! | 采用 fastjson bugjson 远近闻名 |
|
||||||
| 分布式幂等 | 参考美团GTIS防重系统简化实现(细节可看文档) | 手动编写注解基于aop实现 |
|
| 分布式幂等 | 参考美团GTIS防重系统简化实现(细节可看文档) | 手动编写注解基于aop实现 |
|
||||||
| 分布式锁 | 采用 Lock4j 底层基于 Redisson | 无 |
|
| 分布式锁 | 采用 Lock4j 底层基于 Redisson | 无 |
|
||||||
| 分布式任务调度 | 采用 Xxl-Job 天生支持分布式 统一的管理中心 | 采用 Quartz 基于数据库锁性能差 集群需要做很多配置与改造 |
|
| 分布式任务调度 | 采用 PowerJob 天生支持分布式 统一的管理中心 | 采用 Quartz 基于数据库锁性能差 集群需要做很多配置与改造 |
|
||||||
| 文件存储 | 采用 Minio 分布式文件存储 天生支持多机、多硬盘、多分片、多副本存储<br/>支持权限管理 安全可靠 文件可加密存储 | 采用 本机文件存储 文件裸漏 易丢失泄漏 不支持集群有单点效应 |
|
| 文件存储 | 采用 Minio 分布式文件存储 天生支持多机、多硬盘、多分片、多副本存储<br/>支持权限管理 安全可靠 文件可加密存储 | 采用 本机文件存储 文件裸漏 易丢失泄漏 不支持集群有单点效应 |
|
||||||
| 云存储 | 采用 AWS S3 协议客户端 支持 七牛、阿里、腾讯 等一切支持S3协议的厂家 | 不支持 |
|
| 云存储 | 采用 AWS S3 协议客户端 支持 七牛、阿里、腾讯 等一切支持S3协议的厂家 | 不支持 |
|
||||||
| 短信 | 支持 阿里、腾讯 只需在yml配置好厂家密钥即可使用 接口化支持扩展其他厂家 | 不支持 |
|
| 短信 | 采用 sms4j 短信融合包 支持数十种短信厂家 只需在yml配置好厂家密钥即可使用 可多厂家共用 | 不支持 |
|
||||||
| 邮件 | 采用 mail-api 通用协议支持大部分邮件厂商 | 不支持 |
|
| 邮件 | 采用 mail-api 通用协议支持大部分邮件厂商 | 不支持 |
|
||||||
| 接口文档 | 采用 SpringDoc、javadoc 无注解零入侵基于java注释<br/>只需把注释写好 无需再写一大堆的文档注解了 | 采用 Springfox 已停止维护 需要编写大量的注解来支持文档生成 |
|
| 接口文档 | 采用 SpringDoc、javadoc 无注解零入侵基于java注释<br/>只需把注释写好 无需再写一大堆的文档注解了 | 采用 Springfox 已停止维护 需要编写大量的注解来支持文档生成 |
|
||||||
| 校验框架 | 采用 Validation 支持注解与工具类校验 注解支持国际化 | 仅支持注解 且注解不支持国际化 |
|
| 校验框架 | 采用 Validation 支持注解与工具类校验 注解支持国际化 | 仅支持注解 且注解不支持国际化 |
|
||||||
@ -76,9 +78,10 @@
|
|||||||
## 本框架与RuoYi的业务差异
|
## 本框架与RuoYi的业务差异
|
||||||
|
|
||||||
| 业务 | 功能说明 | 本框架 | RuoYi |
|
| 业务 | 功能说明 | 本框架 | RuoYi |
|
||||||
|--------|-----------------------------------------|-----|------------------|
|
|--------|----------------------------------------------------------------------|-----|------------------|
|
||||||
| 租户管理 | 系统内租户的管理 如:租户套餐、过期时间、用户数量、企业信息等 | 支持 | 无 |
|
| 租户管理 | 系统内租户的管理 如:租户套餐、过期时间、用户数量、企业信息等 | 支持 | 无 |
|
||||||
| 租户套餐管理 | 系统内租户所能使用的套餐管理 如:套餐内所包含的菜单等 | 支持 | 无 |
|
| 租户套餐管理 | 系统内租户所能使用的套餐管理 如:套餐内所包含的菜单等 | 支持 | 无 |
|
||||||
|
| 客户端管理 | 系统内对接的所有客户端管理 如: pc端、小程序端等<br>支持动态授权登录方式 如: 短信登录、密码登录等 支持动态控制token时效 | 支持 | 无 |
|
||||||
| 用户管理 | 用户的管理配置 如:新增用户、分配用户所属部门、角色、岗位等 | 支持 | 支持 |
|
| 用户管理 | 用户的管理配置 如:新增用户、分配用户所属部门、角色、岗位等 | 支持 | 支持 |
|
||||||
| 部门管理 | 配置系统组织机构(公司、部门、小组) 树结构展现支持数据权限 | 支持 | 支持 |
|
| 部门管理 | 配置系统组织机构(公司、部门、小组) 树结构展现支持数据权限 | 支持 | 支持 |
|
||||||
| 岗位管理 | 配置系统用户所属担任职务 | 支持 | 支持 |
|
| 岗位管理 | 配置系统用户所属担任职务 | 支持 | 支持 |
|
||||||
|
74
pom.xml
74
pom.xml
@ -13,32 +13,33 @@
|
|||||||
<description>RuoYi-Vue-Plus多租户管理系统</description>
|
<description>RuoYi-Vue-Plus多租户管理系统</description>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>5.0.0</revision>
|
<revision>5.1.0-BETA</revision>
|
||||||
<spring-boot.version>3.0.7</spring-boot.version>
|
<spring-boot.version>3.1.2</spring-boot.version>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
<java.version>17</java.version>
|
<java.version>17</java.version>
|
||||||
<spring-boot.mybatis>3.0.1</spring-boot.mybatis>
|
<spring-boot.mybatis>3.0.2</spring-boot.mybatis>
|
||||||
<springdoc.version>2.1.0</springdoc.version>
|
<springdoc.version>2.1.0</springdoc.version>
|
||||||
<therapi-javadoc.version>0.15.0</therapi-javadoc.version>
|
<therapi-javadoc.version>0.15.0</therapi-javadoc.version>
|
||||||
<poi.version>5.2.3</poi.version>
|
<poi.version>5.2.3</poi.version>
|
||||||
<easyexcel.version>3.2.1</easyexcel.version>
|
<easyexcel.version>3.3.2</easyexcel.version>
|
||||||
<velocity.version>2.3</velocity.version>
|
<velocity.version>2.3</velocity.version>
|
||||||
<satoken.version>1.34.0</satoken.version>
|
<satoken.version>1.35.0.RC</satoken.version>
|
||||||
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
|
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
|
||||||
<p6spy.version>3.9.1</p6spy.version>
|
<p6spy.version>3.9.1</p6spy.version>
|
||||||
<hutool.version>5.8.18</hutool.version>
|
<hutool.version>5.8.20</hutool.version>
|
||||||
<okhttp.version>4.10.0</okhttp.version>
|
<okhttp.version>4.10.0</okhttp.version>
|
||||||
<spring-boot-admin.version>3.0.4</spring-boot-admin.version>
|
<spring-boot-admin.version>3.1.3</spring-boot-admin.version>
|
||||||
<redisson.version>3.20.1</redisson.version>
|
<redisson.version>3.23.1</redisson.version>
|
||||||
<lock4j.version>2.2.4</lock4j.version>
|
<lock4j.version>2.2.4</lock4j.version>
|
||||||
<dynamic-ds.version>3.6.1</dynamic-ds.version>
|
<dynamic-ds.version>4.1.2</dynamic-ds.version>
|
||||||
<alibaba-ttl.version>2.14.2</alibaba-ttl.version>
|
<alibaba-ttl.version>2.14.2</alibaba-ttl.version>
|
||||||
<xxl-job.version>2.4.0</xxl-job.version>
|
<powerjob.version>4.3.3</powerjob.version>
|
||||||
<mapstruct-plus.version>1.2.3</mapstruct-plus.version>
|
<mapstruct-plus.version>1.3.5</mapstruct-plus.version>
|
||||||
<mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
|
<mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
|
||||||
<lombok.version>1.18.26</lombok.version>
|
<lombok.version>1.18.26</lombok.version>
|
||||||
<bouncycastle.version>1.72</bouncycastle.version>
|
<bouncycastle.version>1.72</bouncycastle.version>
|
||||||
|
<justauth.version>1.16.5</justauth.version>
|
||||||
<!-- 离线IP地址定位库 -->
|
<!-- 离线IP地址定位库 -->
|
||||||
<ip2region.version>2.7.0</ip2region.version>
|
<ip2region.version>2.7.0</ip2region.version>
|
||||||
|
|
||||||
@ -46,16 +47,15 @@
|
|||||||
<snakeyaml.version>1.33</snakeyaml.version>
|
<snakeyaml.version>1.33</snakeyaml.version>
|
||||||
|
|
||||||
<!-- OSS 配置 -->
|
<!-- OSS 配置 -->
|
||||||
<aws-java-sdk-s3.version>1.12.400</aws-java-sdk-s3.version>
|
<aws-java-sdk-s3.version>1.12.517</aws-java-sdk-s3.version>
|
||||||
<!-- SMS 配置 -->
|
<!-- SMS 配置 -->
|
||||||
<aliyun.sms.version>2.0.23</aliyun.sms.version>
|
<sms4j.version>2.2.0</sms4j.version>
|
||||||
<tencent.sms.version>3.1.687</tencent.sms.version>
|
|
||||||
|
|
||||||
<!-- 插件版本 -->
|
<!-- 插件版本 -->
|
||||||
<maven-jar-plugin.version>3.2.2</maven-jar-plugin.version>
|
<maven-jar-plugin.version>3.2.2</maven-jar-plugin.version>
|
||||||
<maven-war-plugin.version>3.2.2</maven-war-plugin.version>
|
<maven-war-plugin.version>3.2.2</maven-war-plugin.version>
|
||||||
<maven-compiler-plugin.verison>3.11.0</maven-compiler-plugin.verison>
|
<maven-compiler-plugin.verison>3.11.0</maven-compiler-plugin.verison>
|
||||||
<maven-surefire-plugin.version>3.0.0</maven-surefire-plugin.version>
|
<maven-surefire-plugin.version>3.1.2</maven-surefire-plugin.version>
|
||||||
<flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version>
|
<flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version>
|
||||||
|
|
||||||
<!--工作流配置-->
|
<!--工作流配置-->
|
||||||
@ -114,6 +114,13 @@
|
|||||||
<scope>import</scope>
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JustAuth 的依赖配置-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>me.zhyd.oauth</groupId>
|
||||||
|
<artifactId>JustAuth</artifactId>
|
||||||
|
<version>${justauth.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- common 的依赖配置-->
|
<!-- common 的依赖配置-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.dromara</groupId>
|
<groupId>org.dromara</groupId>
|
||||||
@ -197,7 +204,7 @@
|
|||||||
<!-- dynamic-datasource 多数据源-->
|
<!-- dynamic-datasource 多数据源-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.baomidou</groupId>
|
<groupId>com.baomidou</groupId>
|
||||||
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
|
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
|
||||||
<version>${dynamic-ds.version}</version>
|
<version>${dynamic-ds.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@ -237,17 +244,11 @@
|
|||||||
<artifactId>aws-java-sdk-s3</artifactId>
|
<artifactId>aws-java-sdk-s3</artifactId>
|
||||||
<version>${aws-java-sdk-s3.version}</version>
|
<version>${aws-java-sdk-s3.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!--短信sms4j-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.aliyun</groupId>
|
<groupId>org.dromara.sms4j</groupId>
|
||||||
<artifactId>dysmsapi20170525</artifactId>
|
<artifactId>sms4j-spring-boot-starter</artifactId>
|
||||||
<version>${aliyun.sms.version}</version>
|
<version>${sms4j.version}</version>
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.tencentcloudapi</groupId>
|
|
||||||
<artifactId>tencentcloud-sdk-java-sms</artifactId>
|
|
||||||
<version>${tencent.sms.version}</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -274,11 +275,16 @@
|
|||||||
<version>${lock4j.version}</version>
|
<version>${lock4j.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- xxl-job-core -->
|
<!-- PowerJob -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.xuxueli</groupId>
|
<groupId>tech.powerjob</groupId>
|
||||||
<artifactId>xxl-job-core</artifactId>
|
<artifactId>powerjob-worker-spring-boot-starter</artifactId>
|
||||||
<version>${xxl-job.version}</version>
|
<version>${powerjob.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.powerjob</groupId>
|
||||||
|
<artifactId>powerjob-official-processors</artifactId>
|
||||||
|
<version>${powerjob.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@ -480,8 +486,8 @@
|
|||||||
<repositories>
|
<repositories>
|
||||||
<repository>
|
<repository>
|
||||||
<id>public</id>
|
<id>public</id>
|
||||||
<name>aliyun nexus</name>
|
<name>huawei nexus</name>
|
||||||
<url>https://maven.aliyun.com/repository/public/</url>
|
<url>https://mirrors.huaweicloud.com/repository/maven/</url>
|
||||||
<releases>
|
<releases>
|
||||||
<enabled>true</enabled>
|
<enabled>true</enabled>
|
||||||
</releases>
|
</releases>
|
||||||
@ -491,8 +497,8 @@
|
|||||||
<pluginRepositories>
|
<pluginRepositories>
|
||||||
<pluginRepository>
|
<pluginRepository>
|
||||||
<id>public</id>
|
<id>public</id>
|
||||||
<name>aliyun nexus</name>
|
<name>huawei nexus</name>
|
||||||
<url>https://maven.aliyun.com/repository/public/</url>
|
<url>https://mirrors.huaweicloud.com/repository/maven/</url>
|
||||||
<releases>
|
<releases>
|
||||||
<enabled>true</enabled>
|
<enabled>true</enabled>
|
||||||
</releases>
|
</releases>
|
||||||
|
@ -43,6 +43,12 @@
|
|||||||
<artifactId>ruoyi-common-doc</artifactId>
|
<artifactId>ruoyi-common-doc</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.dromara</groupId>
|
||||||
|
<artifactId>ruoyi-common-social</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.dromara</groupId>
|
<groupId>org.dromara</groupId>
|
||||||
<artifactId>ruoyi-system</artifactId>
|
<artifactId>ruoyi-system</artifactId>
|
||||||
@ -82,6 +88,11 @@
|
|||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>me.zhyd.oauth</groupId>
|
||||||
|
<artifactId>JustAuth</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- skywalking 整合 logback -->
|
<!-- skywalking 整合 logback -->
|
||||||
<!-- <dependency>-->
|
<!-- <dependency>-->
|
||||||
<!-- <groupId>org.apache.skywalking</groupId>-->
|
<!-- <groupId>org.apache.skywalking</groupId>-->
|
||||||
|
@ -2,27 +2,38 @@ package org.dromara.web.controller;
|
|||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaIgnore;
|
import cn.dev33.satoken.annotation.SaIgnore;
|
||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import me.zhyd.oauth.model.AuthResponse;
|
||||||
|
import me.zhyd.oauth.model.AuthUser;
|
||||||
|
import me.zhyd.oauth.request.AuthRequest;
|
||||||
|
import me.zhyd.oauth.utils.AuthStateUtils;
|
||||||
import org.dromara.common.core.domain.R;
|
import org.dromara.common.core.domain.R;
|
||||||
import org.dromara.common.core.domain.model.EmailLoginBody;
|
|
||||||
import org.dromara.common.core.domain.model.LoginBody;
|
import org.dromara.common.core.domain.model.LoginBody;
|
||||||
import org.dromara.common.core.domain.model.RegisterBody;
|
import org.dromara.common.core.domain.model.RegisterBody;
|
||||||
import org.dromara.common.core.domain.model.SmsLoginBody;
|
|
||||||
import org.dromara.common.core.utils.MapstructUtils;
|
import org.dromara.common.core.utils.MapstructUtils;
|
||||||
|
import org.dromara.common.core.utils.MessageUtils;
|
||||||
import org.dromara.common.core.utils.StreamUtils;
|
import org.dromara.common.core.utils.StreamUtils;
|
||||||
import org.dromara.common.core.utils.StringUtils;
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
|
import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
|
||||||
|
import org.dromara.common.social.config.properties.SocialProperties;
|
||||||
|
import org.dromara.common.social.utils.SocialUtils;
|
||||||
import org.dromara.common.tenant.helper.TenantHelper;
|
import org.dromara.common.tenant.helper.TenantHelper;
|
||||||
|
import org.dromara.system.domain.SysClient;
|
||||||
import org.dromara.system.domain.bo.SysTenantBo;
|
import org.dromara.system.domain.bo.SysTenantBo;
|
||||||
import org.dromara.system.domain.vo.SysTenantVo;
|
import org.dromara.system.domain.vo.SysTenantVo;
|
||||||
|
import org.dromara.system.service.ISysClientService;
|
||||||
import org.dromara.system.service.ISysConfigService;
|
import org.dromara.system.service.ISysConfigService;
|
||||||
|
import org.dromara.system.service.ISysSocialService;
|
||||||
import org.dromara.system.service.ISysTenantService;
|
import org.dromara.system.service.ISysTenantService;
|
||||||
import org.dromara.web.domain.vo.LoginTenantVo;
|
import org.dromara.web.domain.vo.LoginTenantVo;
|
||||||
import org.dromara.web.domain.vo.LoginVo;
|
import org.dromara.web.domain.vo.LoginVo;
|
||||||
import org.dromara.web.domain.vo.TenantListVo;
|
import org.dromara.web.domain.vo.TenantListVo;
|
||||||
|
import org.dromara.web.service.IAuthStrategy;
|
||||||
import org.dromara.web.service.SysLoginService;
|
import org.dromara.web.service.SysLoginService;
|
||||||
import org.dromara.web.service.SysRegisterService;
|
import org.dromara.web.service.SysRegisterService;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@ -34,6 +45,7 @@ import java.util.List;
|
|||||||
*
|
*
|
||||||
* @author Lion Li
|
* @author Lion Li
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@SaIgnore
|
@SaIgnore
|
||||||
@Validated
|
@Validated
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@ -41,74 +53,87 @@ import java.util.List;
|
|||||||
@RequestMapping("/auth")
|
@RequestMapping("/auth")
|
||||||
public class AuthController {
|
public class AuthController {
|
||||||
|
|
||||||
|
private final SocialProperties socialProperties;
|
||||||
private final SysLoginService loginService;
|
private final SysLoginService loginService;
|
||||||
private final SysRegisterService registerService;
|
private final SysRegisterService registerService;
|
||||||
private final ISysConfigService configService;
|
private final ISysConfigService configService;
|
||||||
private final ISysTenantService tenantService;
|
private final ISysTenantService tenantService;
|
||||||
|
private final ISysSocialService socialUserService;
|
||||||
|
private final ISysClientService clientService;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录方法
|
* 登录方法
|
||||||
*
|
*
|
||||||
* @param body 登录信息
|
* @param loginBody 登录信息
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public R<LoginVo> login(@Validated @RequestBody LoginBody body) {
|
public R<LoginVo> login(@Validated @RequestBody LoginBody loginBody) {
|
||||||
LoginVo loginVo = new LoginVo();
|
// 授权类型和客户端id
|
||||||
// 生成令牌
|
String clientId = loginBody.getClientId();
|
||||||
String token = loginService.login(
|
String grantType = loginBody.getGrantType();
|
||||||
body.getTenantId(),
|
SysClient client = clientService.queryByClientId(clientId);
|
||||||
body.getUsername(), body.getPassword(),
|
// 查询不到 client 或 client 内不包含 grantType
|
||||||
body.getCode(), body.getUuid());
|
if (ObjectUtil.isNull(client) || !StringUtils.contains(client.getGrantType(), grantType)) {
|
||||||
loginVo.setToken(token);
|
log.info("客户端id: {} 认证类型:{} 异常!.", clientId, grantType);
|
||||||
return R.ok(loginVo);
|
return R.fail(MessageUtils.message("auth.grant.type.error"));
|
||||||
|
}
|
||||||
|
// 校验租户
|
||||||
|
loginService.checkTenant(loginBody.getTenantId());
|
||||||
|
// 登录
|
||||||
|
return R.ok(IAuthStrategy.login(loginBody, client));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 短信登录
|
* 第三方登录请求
|
||||||
*
|
*
|
||||||
* @param body 登录信息
|
* @param source 登录来源
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@PostMapping("/smsLogin")
|
@GetMapping("/binding/{source}")
|
||||||
public R<LoginVo> smsLogin(@Validated @RequestBody SmsLoginBody body) {
|
public R<String> authBinding(@PathVariable("source") String source) {
|
||||||
LoginVo loginVo = new LoginVo();
|
SocialLoginConfigProperties obj = socialProperties.getType().get(source);
|
||||||
// 生成令牌
|
if (ObjectUtil.isNull(obj)) {
|
||||||
String token = loginService.smsLogin(body.getTenantId(), body.getPhonenumber(), body.getSmsCode());
|
return R.fail(source + "平台账号暂不支持");
|
||||||
loginVo.setToken(token);
|
}
|
||||||
return R.ok(loginVo);
|
AuthRequest authRequest = SocialUtils.getAuthRequest(source, socialProperties);
|
||||||
|
String authorizeUrl = authRequest.authorize(AuthStateUtils.createState());
|
||||||
|
return R.ok("操作成功", authorizeUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 邮件登录
|
* 第三方登录回调业务处理 绑定授权
|
||||||
*
|
*
|
||||||
* @param body 登录信息
|
* @param loginBody 请求体
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
@PostMapping("/emailLogin")
|
@PostMapping("/social/callback")
|
||||||
public R<LoginVo> emailLogin(@Validated @RequestBody EmailLoginBody body) {
|
public R<Void> socialCallback(@RequestBody LoginBody loginBody) {
|
||||||
LoginVo loginVo = new LoginVo();
|
// 获取第三方登录信息
|
||||||
// 生成令牌
|
AuthResponse<AuthUser> response = SocialUtils.loginAuth(loginBody, socialProperties);
|
||||||
String token = loginService.emailLogin(body.getTenantId(), body.getEmail(), body.getEmailCode());
|
AuthUser authUserData = response.getData();
|
||||||
loginVo.setToken(token);
|
// 判断授权响应是否成功
|
||||||
return R.ok(loginVo);
|
if (!response.ok()) {
|
||||||
|
return R.fail(response.getMsg());
|
||||||
|
}
|
||||||
|
loginService.socialRegister(authUserData);
|
||||||
|
return R.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 小程序登录(示例)
|
* 取消授权
|
||||||
*
|
*
|
||||||
* @param xcxCode 小程序code
|
* @param socialId socialId
|
||||||
* @return 结果
|
|
||||||
*/
|
*/
|
||||||
@PostMapping("/xcxLogin")
|
@DeleteMapping(value = "/unlock/{socialId}")
|
||||||
public R<LoginVo> xcxLogin(@NotBlank(message = "{xcx.code.not.blank}") String xcxCode) {
|
public R<Void> unlockSocial(@PathVariable Long socialId) {
|
||||||
LoginVo loginVo = new LoginVo();
|
Boolean rows = socialUserService.deleteWithValidById(socialId);
|
||||||
// 生成令牌
|
return rows ? R.ok() : R.fail("取消授权失败");
|
||||||
String token = loginService.xcxLogin(xcxCode);
|
|
||||||
loginVo.setToken(token);
|
|
||||||
return R.ok(loginVo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 退出登录
|
* 退出登录
|
||||||
*/
|
*/
|
||||||
@ -140,9 +165,17 @@ public class AuthController {
|
|||||||
List<SysTenantVo> tenantList = tenantService.queryList(new SysTenantBo());
|
List<SysTenantVo> tenantList = tenantService.queryList(new SysTenantBo());
|
||||||
List<TenantListVo> voList = MapstructUtils.convert(tenantList, TenantListVo.class);
|
List<TenantListVo> voList = MapstructUtils.convert(tenantList, TenantListVo.class);
|
||||||
// 获取域名
|
// 获取域名
|
||||||
String host = new URL(request.getRequestURL().toString()).getHost();
|
String host;
|
||||||
|
String referer = request.getHeader("referer");
|
||||||
|
if (StringUtils.isNotBlank(referer)) {
|
||||||
|
// 这里从referer中取值是为了本地使用hosts添加虚拟域名,方便本地环境调试
|
||||||
|
host = referer.split("//")[1].split("/")[0];
|
||||||
|
} else {
|
||||||
|
host = new URL(request.getRequestURL().toString()).getHost();
|
||||||
|
}
|
||||||
// 根据域名进行筛选
|
// 根据域名进行筛选
|
||||||
List<TenantListVo> list = StreamUtils.filter(voList, vo -> StringUtils.equals(vo.getDomain(), host));
|
List<TenantListVo> list = StreamUtils.filter(voList, vo ->
|
||||||
|
StringUtils.equals(vo.getDomain(), host));
|
||||||
// 返回对象
|
// 返回对象
|
||||||
LoginTenantVo vo = new LoginTenantVo();
|
LoginTenantVo vo = new LoginTenantVo();
|
||||||
vo.setVoList(CollUtil.isNotEmpty(list) ? list : voList);
|
vo.setVoList(CollUtil.isNotEmpty(list) ? list : voList);
|
||||||
|
@ -14,11 +14,12 @@ import org.dromara.common.core.utils.reflect.ReflectUtils;
|
|||||||
import org.dromara.common.mail.config.properties.MailProperties;
|
import org.dromara.common.mail.config.properties.MailProperties;
|
||||||
import org.dromara.common.mail.utils.MailUtils;
|
import org.dromara.common.mail.utils.MailUtils;
|
||||||
import org.dromara.common.redis.utils.RedisUtils;
|
import org.dromara.common.redis.utils.RedisUtils;
|
||||||
import org.dromara.common.sms.config.properties.SmsProperties;
|
|
||||||
import org.dromara.common.sms.core.SmsTemplate;
|
|
||||||
import org.dromara.common.sms.entity.SmsResult;
|
|
||||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
import org.dromara.common.web.config.properties.CaptchaProperties;
|
||||||
import org.dromara.common.web.enums.CaptchaType;
|
import org.dromara.common.web.enums.CaptchaType;
|
||||||
|
import org.dromara.sms4j.api.SmsBlend;
|
||||||
|
import org.dromara.sms4j.api.entity.SmsResponse;
|
||||||
|
import org.dromara.sms4j.core.factory.SmsFactory;
|
||||||
|
import org.dromara.sms4j.provider.enumerate.SupplierType;
|
||||||
import org.dromara.web.domain.vo.CaptchaVo;
|
import org.dromara.web.domain.vo.CaptchaVo;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -31,8 +32,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.HashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证码操作处理
|
* 验证码操作处理
|
||||||
@ -47,7 +47,6 @@ import java.util.Map;
|
|||||||
public class CaptchaController {
|
public class CaptchaController {
|
||||||
|
|
||||||
private final CaptchaProperties captchaProperties;
|
private final CaptchaProperties captchaProperties;
|
||||||
private final SmsProperties smsProperties;
|
|
||||||
private final MailProperties mailProperties;
|
private final MailProperties mailProperties;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,21 +56,18 @@ public class CaptchaController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping("/resource/sms/code")
|
@GetMapping("/resource/sms/code")
|
||||||
public R<Void> smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) {
|
public R<Void> smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) {
|
||||||
if (!smsProperties.getEnabled()) {
|
|
||||||
return R.fail("当前系统没有开启短信功能!");
|
|
||||||
}
|
|
||||||
String key = GlobalConstants.CAPTCHA_CODE_KEY + phonenumber;
|
String key = GlobalConstants.CAPTCHA_CODE_KEY + phonenumber;
|
||||||
String code = RandomUtil.randomNumbers(4);
|
String code = RandomUtil.randomNumbers(4);
|
||||||
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||||
// 验证码模板id 自行处理 (查数据库或写死均可)
|
// 验证码模板id 自行处理 (查数据库或写死均可)
|
||||||
String templateId = "";
|
String templateId = "";
|
||||||
Map<String, String> map = new HashMap<>(1);
|
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
|
||||||
map.put("code", code);
|
map.put("code", code);
|
||||||
SmsTemplate smsTemplate = SpringUtils.getBean(SmsTemplate.class);
|
SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.ALIBABA);
|
||||||
SmsResult result = smsTemplate.send(phonenumber, templateId, map);
|
SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, templateId, map);
|
||||||
if (!result.isSuccess()) {
|
if (!"OK".equals(smsResponse.getCode())) {
|
||||||
log.error("验证码短信发送异常 => {}", result);
|
log.error("验证码短信发送异常 => {}", smsResponse);
|
||||||
return R.fail(result.getMessage());
|
return R.fail(smsResponse.getMessage());
|
||||||
}
|
}
|
||||||
return R.ok();
|
return R.ok();
|
||||||
}
|
}
|
||||||
@ -101,7 +97,7 @@ public class CaptchaController {
|
|||||||
/**
|
/**
|
||||||
* 生成验证码
|
* 生成验证码
|
||||||
*/
|
*/
|
||||||
@GetMapping("/code")
|
@GetMapping("/auth/code")
|
||||||
public R<CaptchaVo> getCode() {
|
public R<CaptchaVo> getCode() {
|
||||||
CaptchaVo captchaVo = new CaptchaVo();
|
CaptchaVo captchaVo = new CaptchaVo();
|
||||||
boolean captchaEnabled = captchaProperties.getEnable();
|
boolean captchaEnabled = captchaProperties.getEnable();
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.dromara.web.domain.vo;
|
package org.dromara.web.domain.vo;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -10,6 +11,44 @@ import lombok.Data;
|
|||||||
@Data
|
@Data
|
||||||
public class LoginVo {
|
public class LoginVo {
|
||||||
|
|
||||||
private String token;
|
/**
|
||||||
|
* 授权令牌
|
||||||
|
*/
|
||||||
|
@JsonProperty("access_token")
|
||||||
|
private String accessToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新令牌
|
||||||
|
*/
|
||||||
|
@JsonProperty("refresh_token")
|
||||||
|
private String refreshToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 授权令牌 access_token 的有效期
|
||||||
|
*/
|
||||||
|
@JsonProperty("expire_in")
|
||||||
|
private Long expireIn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新令牌 refresh_token 的有效期
|
||||||
|
*/
|
||||||
|
@JsonProperty("refresh_expire_in")
|
||||||
|
private Long refreshExpireIn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用id
|
||||||
|
*/
|
||||||
|
@JsonProperty("client_id")
|
||||||
|
private String clientId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 令牌权限
|
||||||
|
*/
|
||||||
|
private String scope;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户 openid
|
||||||
|
*/
|
||||||
|
private String openid;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
package org.dromara.web.service;
|
||||||
|
|
||||||
|
|
||||||
|
import org.dromara.common.core.domain.model.LoginBody;
|
||||||
|
import org.dromara.common.core.exception.ServiceException;
|
||||||
|
import org.dromara.common.core.utils.SpringUtils;
|
||||||
|
import org.dromara.system.domain.SysClient;
|
||||||
|
import org.dromara.web.domain.vo.LoginVo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 授权策略
|
||||||
|
*
|
||||||
|
* @author Michelle.Chung
|
||||||
|
*/
|
||||||
|
public interface IAuthStrategy {
|
||||||
|
|
||||||
|
String BASE_NAME = "AuthStrategy";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录
|
||||||
|
*/
|
||||||
|
static LoginVo login(LoginBody loginBody, SysClient client) {
|
||||||
|
// 授权类型和客户端id
|
||||||
|
String clientId = loginBody.getClientId();
|
||||||
|
String grantType = loginBody.getGrantType();
|
||||||
|
String beanName = grantType + BASE_NAME;
|
||||||
|
if (!SpringUtils.containsBean(beanName)) {
|
||||||
|
throw new ServiceException("授权类型不正确!");
|
||||||
|
}
|
||||||
|
IAuthStrategy instance = SpringUtils.getBean(beanName);
|
||||||
|
instance.validate(loginBody);
|
||||||
|
return instance.login(clientId, loginBody, client);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数校验
|
||||||
|
*/
|
||||||
|
void validate(LoginBody loginBody);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录
|
||||||
|
*/
|
||||||
|
LoginVo login(String clientId, LoginBody loginBody, SysClient client);
|
||||||
|
|
||||||
|
}
|
@ -1,39 +1,40 @@
|
|||||||
package org.dromara.web.service;
|
package org.dromara.web.service;
|
||||||
|
|
||||||
import cn.dev33.satoken.exception.NotLoginException;
|
import cn.dev33.satoken.exception.NotLoginException;
|
||||||
import cn.dev33.satoken.secure.BCrypt;
|
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import cn.hutool.core.bean.BeanUtil;
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import me.zhyd.oauth.model.AuthUser;
|
||||||
import org.dromara.common.core.constant.Constants;
|
import org.dromara.common.core.constant.Constants;
|
||||||
import org.dromara.common.core.constant.GlobalConstants;
|
import org.dromara.common.core.constant.GlobalConstants;
|
||||||
import org.dromara.common.core.constant.TenantConstants;
|
import org.dromara.common.core.constant.TenantConstants;
|
||||||
import org.dromara.common.core.domain.dto.RoleDTO;
|
import org.dromara.common.core.domain.dto.RoleDTO;
|
||||||
import org.dromara.common.core.domain.model.LoginUser;
|
import org.dromara.common.core.domain.model.LoginUser;
|
||||||
import org.dromara.common.core.domain.model.XcxLoginUser;
|
|
||||||
import org.dromara.common.core.enums.DeviceType;
|
|
||||||
import org.dromara.common.core.enums.LoginType;
|
import org.dromara.common.core.enums.LoginType;
|
||||||
import org.dromara.common.core.enums.TenantStatus;
|
import org.dromara.common.core.enums.TenantStatus;
|
||||||
import org.dromara.common.core.enums.UserStatus;
|
import org.dromara.common.core.enums.UserStatus;
|
||||||
import org.dromara.common.core.exception.user.CaptchaException;
|
|
||||||
import org.dromara.common.core.exception.user.CaptchaExpireException;
|
|
||||||
import org.dromara.common.core.exception.user.UserException;
|
import org.dromara.common.core.exception.user.UserException;
|
||||||
import org.dromara.common.core.utils.*;
|
import org.dromara.common.core.utils.DateUtils;
|
||||||
|
import org.dromara.common.core.utils.MessageUtils;
|
||||||
|
import org.dromara.common.core.utils.ServletUtils;
|
||||||
|
import org.dromara.common.core.utils.SpringUtils;
|
||||||
import org.dromara.common.log.event.LogininforEvent;
|
import org.dromara.common.log.event.LogininforEvent;
|
||||||
import org.dromara.common.redis.utils.RedisUtils;
|
import org.dromara.common.redis.utils.RedisUtils;
|
||||||
import org.dromara.common.satoken.utils.LoginHelper;
|
import org.dromara.common.satoken.utils.LoginHelper;
|
||||||
import org.dromara.common.tenant.exception.TenantException;
|
import org.dromara.common.tenant.exception.TenantException;
|
||||||
import org.dromara.common.tenant.helper.TenantHelper;
|
import org.dromara.common.tenant.helper.TenantHelper;
|
||||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
|
||||||
import org.dromara.system.domain.SysUser;
|
import org.dromara.system.domain.SysUser;
|
||||||
|
import org.dromara.system.domain.bo.SysSocialBo;
|
||||||
import org.dromara.system.domain.vo.SysTenantVo;
|
import org.dromara.system.domain.vo.SysTenantVo;
|
||||||
import org.dromara.system.domain.vo.SysUserVo;
|
import org.dromara.system.domain.vo.SysUserVo;
|
||||||
import org.dromara.system.mapper.SysUserMapper;
|
import org.dromara.system.mapper.SysUserMapper;
|
||||||
import org.dromara.system.service.ISysPermissionService;
|
import org.dromara.system.service.ISysPermissionService;
|
||||||
|
import org.dromara.system.service.ISysSocialService;
|
||||||
import org.dromara.system.service.ISysTenantService;
|
import org.dromara.system.service.ISysTenantService;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@ -52,107 +53,35 @@ import java.util.function.Supplier;
|
|||||||
@Service
|
@Service
|
||||||
public class SysLoginService {
|
public class SysLoginService {
|
||||||
|
|
||||||
private final SysUserMapper userMapper;
|
|
||||||
private final CaptchaProperties captchaProperties;
|
|
||||||
private final ISysPermissionService permissionService;
|
|
||||||
private final ISysTenantService tenantService;
|
|
||||||
|
|
||||||
@Value("${user.password.maxRetryCount}")
|
@Value("${user.password.maxRetryCount}")
|
||||||
private Integer maxRetryCount;
|
private Integer maxRetryCount;
|
||||||
|
|
||||||
@Value("${user.password.lockTime}")
|
@Value("${user.password.lockTime}")
|
||||||
private Integer lockTime;
|
private Integer lockTime;
|
||||||
|
|
||||||
|
private final ISysTenantService tenantService;
|
||||||
|
private final ISysPermissionService permissionService;
|
||||||
|
private final ISysSocialService sysSocialService;
|
||||||
|
private final SysUserMapper userMapper;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录验证
|
* 绑定第三方用户
|
||||||
*
|
*
|
||||||
* @param username 用户名
|
* @param authUserData 授权响应实体
|
||||||
* @param password 密码
|
* @return 统一响应实体
|
||||||
* @param code 验证码
|
|
||||||
* @param uuid 唯一标识
|
|
||||||
* @return 结果
|
|
||||||
*/
|
*/
|
||||||
public String login(String tenantId, String username, String password, String code, String uuid) {
|
public void socialRegister(AuthUser authUserData) {
|
||||||
boolean captchaEnabled = captchaProperties.getEnable();
|
SysSocialBo bo = new SysSocialBo();
|
||||||
// 验证码开关
|
bo.setUserId(LoginHelper.getUserId());
|
||||||
if (captchaEnabled) {
|
bo.setAuthId(authUserData.getSource() + authUserData.getUuid());
|
||||||
validateCaptcha(tenantId, username, code, uuid);
|
bo.setOpenId(authUserData.getUuid());
|
||||||
}
|
bo.setUserName(authUserData.getUsername());
|
||||||
// 校验租户
|
BeanUtils.copyProperties(authUserData, bo);
|
||||||
checkTenant(tenantId);
|
BeanUtils.copyProperties(authUserData.getToken(), bo);
|
||||||
|
sysSocialService.insertByBo(bo);
|
||||||
// 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
|
|
||||||
SysUserVo user = loadUserByUsername(tenantId, username);
|
|
||||||
checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
|
|
||||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
|
||||||
LoginUser loginUser = buildLoginUser(user);
|
|
||||||
// 生成token
|
|
||||||
LoginHelper.loginByDevice(loginUser, DeviceType.PC);
|
|
||||||
|
|
||||||
recordLogininfor(loginUser.getTenantId(), username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
|
|
||||||
recordLoginInfo(user.getUserId());
|
|
||||||
return StpUtil.getTokenValue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String smsLogin(String tenantId, String phonenumber, String smsCode) {
|
|
||||||
// 校验租户
|
|
||||||
checkTenant(tenantId);
|
|
||||||
// 通过手机号查找用户
|
|
||||||
SysUserVo user = loadUserByPhonenumber(tenantId, phonenumber);
|
|
||||||
|
|
||||||
checkLogin(LoginType.SMS, tenantId, user.getUserName(), () -> !validateSmsCode(tenantId, phonenumber, smsCode));
|
|
||||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
|
||||||
LoginUser loginUser = buildLoginUser(user);
|
|
||||||
// 生成token
|
|
||||||
LoginHelper.loginByDevice(loginUser, DeviceType.APP);
|
|
||||||
|
|
||||||
recordLogininfor(loginUser.getTenantId(), user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
|
|
||||||
recordLoginInfo(user.getUserId());
|
|
||||||
return StpUtil.getTokenValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String emailLogin(String tenantId, String email, String emailCode) {
|
|
||||||
// 校验租户
|
|
||||||
checkTenant(tenantId);
|
|
||||||
// 通过邮箱查找用户
|
|
||||||
SysUserVo user = loadUserByEmail(tenantId, email);
|
|
||||||
|
|
||||||
checkLogin(LoginType.EMAIL, tenantId, user.getUserName(), () -> !validateEmailCode(tenantId, email, emailCode));
|
|
||||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
|
||||||
LoginUser loginUser = buildLoginUser(user);
|
|
||||||
// 生成token
|
|
||||||
LoginHelper.loginByDevice(loginUser, DeviceType.APP);
|
|
||||||
|
|
||||||
recordLogininfor(loginUser.getTenantId(), user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
|
|
||||||
recordLoginInfo(user.getUserId());
|
|
||||||
return StpUtil.getTokenValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public String xcxLogin(String xcxCode) {
|
|
||||||
// xcxCode 为 小程序调用 wx.login 授权后获取
|
|
||||||
// todo 以下自行实现
|
|
||||||
// 校验 appid + appsrcret + xcxCode 调用登录凭证校验接口 获取 session_key 与 openid
|
|
||||||
String openid = "";
|
|
||||||
// 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
|
|
||||||
SysUserVo user = loadUserByOpenid(openid);
|
|
||||||
// 校验租户
|
|
||||||
checkTenant(user.getTenantId());
|
|
||||||
|
|
||||||
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
|
||||||
XcxLoginUser loginUser = new XcxLoginUser();
|
|
||||||
loginUser.setTenantId(user.getTenantId());
|
|
||||||
loginUser.setUserId(user.getUserId());
|
|
||||||
loginUser.setUsername(user.getUserName());
|
|
||||||
loginUser.setUserType(user.getUserType());
|
|
||||||
loginUser.setOpenid(openid);
|
|
||||||
// 生成token
|
|
||||||
LoginHelper.loginByDevice(loginUser, DeviceType.XCX);
|
|
||||||
|
|
||||||
recordLogininfor(loginUser.getTenantId(), user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
|
|
||||||
recordLoginInfo(user.getUserId());
|
|
||||||
return StpUtil.getTokenValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 退出登录
|
* 退出登录
|
||||||
@ -164,9 +93,13 @@ public class SysLoginService {
|
|||||||
// 超级管理员 登出清除动态租户
|
// 超级管理员 登出清除动态租户
|
||||||
TenantHelper.clearDynamic();
|
TenantHelper.clearDynamic();
|
||||||
}
|
}
|
||||||
StpUtil.logout();
|
|
||||||
recordLogininfor(loginUser.getTenantId(), loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success"));
|
recordLogininfor(loginUser.getTenantId(), loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success"));
|
||||||
} catch (NotLoginException ignored) {
|
} catch (NotLoginException ignored) {
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
StpUtil.logout();
|
||||||
|
} catch (NotLoginException ignored) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,7 +111,7 @@ public class SysLoginService {
|
|||||||
* @param status 状态
|
* @param status 状态
|
||||||
* @param message 消息内容
|
* @param message 消息内容
|
||||||
*/
|
*/
|
||||||
private void recordLogininfor(String tenantId, String username, String status, String message) {
|
public void recordLogininfor(String tenantId, String username, String status, String message) {
|
||||||
LogininforEvent logininforEvent = new LogininforEvent();
|
LogininforEvent logininforEvent = new LogininforEvent();
|
||||||
logininforEvent.setTenantId(tenantId);
|
logininforEvent.setTenantId(tenantId);
|
||||||
logininforEvent.setUsername(username);
|
logininforEvent.setUsername(username);
|
||||||
@ -188,123 +121,11 @@ public class SysLoginService {
|
|||||||
SpringUtils.context().publishEvent(logininforEvent);
|
SpringUtils.context().publishEvent(logininforEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验短信验证码
|
|
||||||
*/
|
|
||||||
private boolean validateSmsCode(String tenantId, String phonenumber, String smsCode) {
|
|
||||||
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + phonenumber);
|
|
||||||
if (StringUtils.isBlank(code)) {
|
|
||||||
recordLogininfor(tenantId, phonenumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
|
||||||
throw new CaptchaExpireException();
|
|
||||||
}
|
|
||||||
return code.equals(smsCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验邮箱验证码
|
|
||||||
*/
|
|
||||||
private boolean validateEmailCode(String tenantId, String email, String emailCode) {
|
|
||||||
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + email);
|
|
||||||
if (StringUtils.isBlank(code)) {
|
|
||||||
recordLogininfor(tenantId, email, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
|
||||||
throw new CaptchaExpireException();
|
|
||||||
}
|
|
||||||
return code.equals(emailCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 校验验证码
|
|
||||||
*
|
|
||||||
* @param username 用户名
|
|
||||||
* @param code 验证码
|
|
||||||
* @param uuid 唯一标识
|
|
||||||
*/
|
|
||||||
public void validateCaptcha(String tenantId, String username, String code, String uuid) {
|
|
||||||
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.defaultString(uuid, "");
|
|
||||||
String captcha = RedisUtils.getCacheObject(verifyKey);
|
|
||||||
RedisUtils.deleteObject(verifyKey);
|
|
||||||
if (captcha == null) {
|
|
||||||
recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
|
||||||
throw new CaptchaExpireException();
|
|
||||||
}
|
|
||||||
if (!code.equalsIgnoreCase(captcha)) {
|
|
||||||
recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
|
|
||||||
throw new CaptchaException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private SysUserVo loadUserByUsername(String tenantId, String username) {
|
|
||||||
SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
|
|
||||||
.select(SysUser::getUserName, SysUser::getStatus)
|
|
||||||
.eq(TenantHelper.isEnable(), SysUser::getTenantId, tenantId)
|
|
||||||
.eq(SysUser::getUserName, username));
|
|
||||||
if (ObjectUtil.isNull(user)) {
|
|
||||||
log.info("登录用户:{} 不存在.", username);
|
|
||||||
throw new UserException("user.not.exists", username);
|
|
||||||
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
|
|
||||||
log.info("登录用户:{} 已被停用.", username);
|
|
||||||
throw new UserException("user.blocked", username);
|
|
||||||
}
|
|
||||||
if (TenantHelper.isEnable()) {
|
|
||||||
return userMapper.selectTenantUserByUserName(username, tenantId);
|
|
||||||
}
|
|
||||||
return userMapper.selectUserByUserName(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
private SysUserVo loadUserByPhonenumber(String tenantId, String phonenumber) {
|
|
||||||
SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
|
|
||||||
.select(SysUser::getPhonenumber, SysUser::getStatus)
|
|
||||||
.eq(TenantHelper.isEnable(), SysUser::getTenantId, tenantId)
|
|
||||||
.eq(SysUser::getPhonenumber, phonenumber));
|
|
||||||
if (ObjectUtil.isNull(user)) {
|
|
||||||
log.info("登录用户:{} 不存在.", phonenumber);
|
|
||||||
throw new UserException("user.not.exists", phonenumber);
|
|
||||||
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
|
|
||||||
log.info("登录用户:{} 已被停用.", phonenumber);
|
|
||||||
throw new UserException("user.blocked", phonenumber);
|
|
||||||
}
|
|
||||||
if (TenantHelper.isEnable()) {
|
|
||||||
return userMapper.selectTenantUserByPhonenumber(phonenumber, tenantId);
|
|
||||||
}
|
|
||||||
return userMapper.selectUserByPhonenumber(phonenumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
private SysUserVo loadUserByEmail(String tenantId, String email) {
|
|
||||||
SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
|
|
||||||
.select(SysUser::getPhonenumber, SysUser::getStatus)
|
|
||||||
.eq(TenantHelper.isEnable(), SysUser::getTenantId, tenantId)
|
|
||||||
.eq(SysUser::getEmail, email));
|
|
||||||
if (ObjectUtil.isNull(user)) {
|
|
||||||
log.info("登录用户:{} 不存在.", email);
|
|
||||||
throw new UserException("user.not.exists", email);
|
|
||||||
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
|
|
||||||
log.info("登录用户:{} 已被停用.", email);
|
|
||||||
throw new UserException("user.blocked", email);
|
|
||||||
}
|
|
||||||
if (TenantHelper.isEnable()) {
|
|
||||||
return userMapper.selectTenantUserByEmail(email, tenantId);
|
|
||||||
}
|
|
||||||
return userMapper.selectUserByEmail(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
private SysUserVo loadUserByOpenid(String openid) {
|
|
||||||
// 使用 openid 查询绑定用户 如未绑定用户 则根据业务自行处理 例如 创建默认用户
|
|
||||||
// todo 自行实现 userService.selectUserByOpenid(openid);
|
|
||||||
SysUserVo user = new SysUserVo();
|
|
||||||
if (ObjectUtil.isNull(user)) {
|
|
||||||
log.info("登录用户:{} 不存在.", openid);
|
|
||||||
// todo 用户不存在 业务逻辑自行实现
|
|
||||||
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
|
|
||||||
log.info("登录用户:{} 已被停用.", openid);
|
|
||||||
// todo 用户已被停用 业务逻辑自行实现
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建登录用户
|
* 构建登录用户
|
||||||
*/
|
*/
|
||||||
private LoginUser buildLoginUser(SysUserVo user) {
|
public LoginUser buildLoginUser(SysUserVo user) {
|
||||||
LoginUser loginUser = new LoginUser();
|
LoginUser loginUser = new LoginUser();
|
||||||
loginUser.setTenantId(user.getTenantId());
|
loginUser.setTenantId(user.getTenantId());
|
||||||
loginUser.setUserId(user.getUserId());
|
loginUser.setUserId(user.getUserId());
|
||||||
@ -336,29 +157,28 @@ public class SysLoginService {
|
|||||||
/**
|
/**
|
||||||
* 登录校验
|
* 登录校验
|
||||||
*/
|
*/
|
||||||
private void checkLogin(LoginType loginType, String tenantId, String username, Supplier<Boolean> supplier) {
|
public void checkLogin(LoginType loginType, String tenantId, String username, Supplier<Boolean> supplier) {
|
||||||
String errorKey = GlobalConstants.PWD_ERR_CNT_KEY + username;
|
String errorKey = GlobalConstants.PWD_ERR_CNT_KEY + username;
|
||||||
String loginFail = Constants.LOGIN_FAIL;
|
String loginFail = Constants.LOGIN_FAIL;
|
||||||
|
|
||||||
// 获取用户登录错误次数(可自定义限制策略 例如: key + username + ip)
|
// 获取用户登录错误次数,默认为0 (可自定义限制策略 例如: key + username + ip)
|
||||||
Integer errorNumber = RedisUtils.getCacheObject(errorKey);
|
int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
|
||||||
// 锁定时间内登录 则踢出
|
// 锁定时间内登录 则踢出
|
||||||
if (ObjectUtil.isNotNull(errorNumber) && errorNumber.equals(maxRetryCount)) {
|
if (errorNumber >= maxRetryCount) {
|
||||||
recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
|
recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
|
||||||
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
|
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (supplier.get()) {
|
if (supplier.get()) {
|
||||||
// 是否第一次
|
// 错误次数递增
|
||||||
errorNumber = ObjectUtil.isNull(errorNumber) ? 1 : errorNumber + 1;
|
errorNumber++;
|
||||||
// 达到规定错误次数 则锁定登录
|
|
||||||
if (errorNumber.equals(maxRetryCount)) {
|
|
||||||
RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
|
RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
|
||||||
|
// 达到规定错误次数 则锁定登录
|
||||||
|
if (errorNumber >= maxRetryCount) {
|
||||||
recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
|
recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
|
||||||
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
|
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
|
||||||
} else {
|
} else {
|
||||||
// 未达到规定错误次数 则递增
|
// 未达到规定错误次数
|
||||||
RedisUtils.setCacheObject(errorKey, errorNumber);
|
|
||||||
recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
|
recordLogininfor(tenantId, username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
|
||||||
throw new UserException(loginType.getRetryLimitCount(), errorNumber);
|
throw new UserException(loginType.getRetryLimitCount(), errorNumber);
|
||||||
}
|
}
|
||||||
@ -368,7 +188,12 @@ public class SysLoginService {
|
|||||||
RedisUtils.deleteObject(errorKey);
|
RedisUtils.deleteObject(errorKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkTenant(String tenantId) {
|
/**
|
||||||
|
* 校验租户
|
||||||
|
*
|
||||||
|
* @param tenantId 租户ID
|
||||||
|
*/
|
||||||
|
public void checkTenant(String tenantId) {
|
||||||
if (!TenantHelper.isEnable()) {
|
if (!TenantHelper.isEnable()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,113 @@
|
|||||||
|
package org.dromara.web.service.impl;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.SaLoginModel;
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.dromara.common.core.constant.Constants;
|
||||||
|
import org.dromara.common.core.constant.GlobalConstants;
|
||||||
|
import org.dromara.common.core.domain.model.LoginBody;
|
||||||
|
import org.dromara.common.core.domain.model.LoginUser;
|
||||||
|
import org.dromara.common.core.enums.LoginType;
|
||||||
|
import org.dromara.common.core.enums.UserStatus;
|
||||||
|
import org.dromara.common.core.exception.user.CaptchaExpireException;
|
||||||
|
import org.dromara.common.core.exception.user.UserException;
|
||||||
|
import org.dromara.common.core.utils.MessageUtils;
|
||||||
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
|
import org.dromara.common.core.utils.ValidatorUtils;
|
||||||
|
import org.dromara.common.core.validate.auth.EmailGroup;
|
||||||
|
import org.dromara.common.redis.utils.RedisUtils;
|
||||||
|
import org.dromara.common.satoken.utils.LoginHelper;
|
||||||
|
import org.dromara.common.tenant.helper.TenantHelper;
|
||||||
|
import org.dromara.system.domain.SysClient;
|
||||||
|
import org.dromara.system.domain.SysUser;
|
||||||
|
import org.dromara.system.domain.vo.SysUserVo;
|
||||||
|
import org.dromara.system.mapper.SysUserMapper;
|
||||||
|
import org.dromara.web.domain.vo.LoginVo;
|
||||||
|
import org.dromara.web.service.IAuthStrategy;
|
||||||
|
import org.dromara.web.service.SysLoginService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮件认证策略
|
||||||
|
*
|
||||||
|
* @author Michelle.Chung
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service("email" + IAuthStrategy.BASE_NAME)
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class EmailAuthStrategy implements IAuthStrategy {
|
||||||
|
|
||||||
|
private final SysLoginService loginService;
|
||||||
|
private final SysUserMapper userMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validate(LoginBody loginBody) {
|
||||||
|
ValidatorUtils.validate(loginBody, EmailGroup.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LoginVo login(String clientId, LoginBody loginBody, SysClient client) {
|
||||||
|
String tenantId = loginBody.getTenantId();
|
||||||
|
String email = loginBody.getEmail();
|
||||||
|
String emailCode = loginBody.getEmailCode();
|
||||||
|
|
||||||
|
// 通过邮箱查找用户
|
||||||
|
SysUserVo user = loadUserByEmail(tenantId, email);
|
||||||
|
|
||||||
|
loginService.checkLogin(LoginType.EMAIL, tenantId, user.getUserName(), () -> !validateEmailCode(tenantId, email, emailCode));
|
||||||
|
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||||
|
LoginUser loginUser = loginService.buildLoginUser(user);
|
||||||
|
SaLoginModel model = new SaLoginModel();
|
||||||
|
model.setDevice(client.getDeviceType());
|
||||||
|
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
|
||||||
|
// 例如: 后台用户30分钟过期 app用户1天过期
|
||||||
|
model.setTimeout(client.getTimeout());
|
||||||
|
model.setActiveTimeout(client.getActiveTimeout());
|
||||||
|
model.setExtra(LoginHelper.CLIENT_KEY, clientId);
|
||||||
|
// 生成token
|
||||||
|
LoginHelper.login(loginUser, model);
|
||||||
|
|
||||||
|
loginService.recordLogininfor(loginUser.getTenantId(), user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
|
||||||
|
loginService.recordLoginInfo(user.getUserId());
|
||||||
|
|
||||||
|
LoginVo loginVo = new LoginVo();
|
||||||
|
loginVo.setAccessToken(StpUtil.getTokenValue());
|
||||||
|
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
||||||
|
loginVo.setClientId(clientId);
|
||||||
|
return loginVo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验邮箱验证码
|
||||||
|
*/
|
||||||
|
private boolean validateEmailCode(String tenantId, String email, String emailCode) {
|
||||||
|
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + email);
|
||||||
|
if (StringUtils.isBlank(code)) {
|
||||||
|
loginService.recordLogininfor(tenantId, email, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||||
|
throw new CaptchaExpireException();
|
||||||
|
}
|
||||||
|
return code.equals(emailCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysUserVo loadUserByEmail(String tenantId, String email) {
|
||||||
|
SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
|
||||||
|
.select(SysUser::getEmail, SysUser::getStatus)
|
||||||
|
.eq(TenantHelper.isEnable(), SysUser::getTenantId, tenantId)
|
||||||
|
.eq(SysUser::getEmail, email));
|
||||||
|
if (ObjectUtil.isNull(user)) {
|
||||||
|
log.info("登录用户:{} 不存在.", email);
|
||||||
|
throw new UserException("user.not.exists", email);
|
||||||
|
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
|
||||||
|
log.info("登录用户:{} 已被停用.", email);
|
||||||
|
throw new UserException("user.blocked", email);
|
||||||
|
}
|
||||||
|
if (TenantHelper.isEnable()) {
|
||||||
|
return userMapper.selectTenantUserByEmail(email, tenantId);
|
||||||
|
}
|
||||||
|
return userMapper.selectUserByEmail(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,132 @@
|
|||||||
|
package org.dromara.web.service.impl;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.secure.BCrypt;
|
||||||
|
import cn.dev33.satoken.stp.SaLoginModel;
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.dromara.common.core.constant.Constants;
|
||||||
|
import org.dromara.common.core.constant.GlobalConstants;
|
||||||
|
import org.dromara.common.core.domain.model.LoginBody;
|
||||||
|
import org.dromara.common.core.domain.model.LoginUser;
|
||||||
|
import org.dromara.common.core.enums.LoginType;
|
||||||
|
import org.dromara.common.core.enums.UserStatus;
|
||||||
|
import org.dromara.common.core.exception.user.CaptchaException;
|
||||||
|
import org.dromara.common.core.exception.user.CaptchaExpireException;
|
||||||
|
import org.dromara.common.core.exception.user.UserException;
|
||||||
|
import org.dromara.common.core.utils.MessageUtils;
|
||||||
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
|
import org.dromara.common.core.utils.ValidatorUtils;
|
||||||
|
import org.dromara.common.core.validate.auth.PasswordGroup;
|
||||||
|
import org.dromara.common.redis.utils.RedisUtils;
|
||||||
|
import org.dromara.common.satoken.utils.LoginHelper;
|
||||||
|
import org.dromara.common.tenant.helper.TenantHelper;
|
||||||
|
import org.dromara.common.web.config.properties.CaptchaProperties;
|
||||||
|
import org.dromara.system.domain.SysClient;
|
||||||
|
import org.dromara.system.domain.SysUser;
|
||||||
|
import org.dromara.system.domain.vo.SysUserVo;
|
||||||
|
import org.dromara.system.mapper.SysUserMapper;
|
||||||
|
import org.dromara.web.domain.vo.LoginVo;
|
||||||
|
import org.dromara.web.service.IAuthStrategy;
|
||||||
|
import org.dromara.web.service.SysLoginService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码认证策略
|
||||||
|
*
|
||||||
|
* @author Michelle.Chung
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service("password" + IAuthStrategy.BASE_NAME)
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PasswordAuthStrategy implements IAuthStrategy {
|
||||||
|
|
||||||
|
private final CaptchaProperties captchaProperties;
|
||||||
|
private final SysLoginService loginService;
|
||||||
|
private final SysUserMapper userMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validate(LoginBody loginBody) {
|
||||||
|
ValidatorUtils.validate(loginBody, PasswordGroup.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LoginVo login(String clientId, LoginBody loginBody, SysClient client) {
|
||||||
|
String tenantId = loginBody.getTenantId();
|
||||||
|
String username = loginBody.getUsername();
|
||||||
|
String password = loginBody.getPassword();
|
||||||
|
String code = loginBody.getCode();
|
||||||
|
String uuid = loginBody.getUuid();
|
||||||
|
|
||||||
|
boolean captchaEnabled = captchaProperties.getEnable();
|
||||||
|
// 验证码开关
|
||||||
|
if (captchaEnabled) {
|
||||||
|
validateCaptcha(tenantId, username, code, uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
SysUserVo user = loadUserByUsername(tenantId, username);
|
||||||
|
loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword()));
|
||||||
|
// 此处可根据登录用户的数据不同 自行创建 loginUser
|
||||||
|
LoginUser loginUser = loginService.buildLoginUser(user);
|
||||||
|
SaLoginModel model = new SaLoginModel();
|
||||||
|
model.setDevice(client.getDeviceType());
|
||||||
|
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
|
||||||
|
// 例如: 后台用户30分钟过期 app用户1天过期
|
||||||
|
model.setTimeout(client.getTimeout());
|
||||||
|
model.setActiveTimeout(client.getActiveTimeout());
|
||||||
|
model.setExtra(LoginHelper.CLIENT_KEY, clientId);
|
||||||
|
// 生成token
|
||||||
|
LoginHelper.login(loginUser, model);
|
||||||
|
|
||||||
|
loginService.recordLogininfor(loginUser.getTenantId(), username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
|
||||||
|
loginService.recordLoginInfo(user.getUserId());
|
||||||
|
|
||||||
|
LoginVo loginVo = new LoginVo();
|
||||||
|
loginVo.setAccessToken(StpUtil.getTokenValue());
|
||||||
|
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
||||||
|
loginVo.setClientId(clientId);
|
||||||
|
return loginVo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验验证码
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @param code 验证码
|
||||||
|
* @param uuid 唯一标识
|
||||||
|
*/
|
||||||
|
private void validateCaptcha(String tenantId, String username, String code, String uuid) {
|
||||||
|
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + StringUtils.defaultString(uuid, "");
|
||||||
|
String captcha = RedisUtils.getCacheObject(verifyKey);
|
||||||
|
RedisUtils.deleteObject(verifyKey);
|
||||||
|
if (captcha == null) {
|
||||||
|
loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||||
|
throw new CaptchaExpireException();
|
||||||
|
}
|
||||||
|
if (!code.equalsIgnoreCase(captcha)) {
|
||||||
|
loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
|
||||||
|
throw new CaptchaException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysUserVo loadUserByUsername(String tenantId, String username) {
|
||||||
|
SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
|
||||||
|
.select(SysUser::getUserName, SysUser::getStatus)
|
||||||
|
.eq(TenantHelper.isEnable(), SysUser::getTenantId, tenantId)
|
||||||
|
.eq(SysUser::getUserName, username));
|
||||||
|
if (ObjectUtil.isNull(user)) {
|
||||||
|
log.info("登录用户:{} 不存在.", username);
|
||||||
|
throw new UserException("user.not.exists", username);
|
||||||
|
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
|
||||||
|
log.info("登录用户:{} 已被停用.", username);
|
||||||
|
throw new UserException("user.blocked", username);
|
||||||
|
}
|
||||||
|
if (TenantHelper.isEnable()) {
|
||||||
|
return userMapper.selectTenantUserByUserName(username, tenantId);
|
||||||
|
}
|
||||||
|
return userMapper.selectUserByUserName(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
package org.dromara.web.service.impl;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.SaLoginModel;
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.dromara.common.core.constant.Constants;
|
||||||
|
import org.dromara.common.core.constant.GlobalConstants;
|
||||||
|
import org.dromara.common.core.domain.model.LoginBody;
|
||||||
|
import org.dromara.common.core.domain.model.LoginUser;
|
||||||
|
import org.dromara.common.core.enums.LoginType;
|
||||||
|
import org.dromara.common.core.enums.UserStatus;
|
||||||
|
import org.dromara.common.core.exception.user.CaptchaExpireException;
|
||||||
|
import org.dromara.common.core.exception.user.UserException;
|
||||||
|
import org.dromara.common.core.utils.MessageUtils;
|
||||||
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
|
import org.dromara.common.core.utils.ValidatorUtils;
|
||||||
|
import org.dromara.common.core.validate.auth.SmsGroup;
|
||||||
|
import org.dromara.common.redis.utils.RedisUtils;
|
||||||
|
import org.dromara.common.satoken.utils.LoginHelper;
|
||||||
|
import org.dromara.common.tenant.helper.TenantHelper;
|
||||||
|
import org.dromara.system.domain.SysClient;
|
||||||
|
import org.dromara.system.domain.SysUser;
|
||||||
|
import org.dromara.system.domain.vo.SysUserVo;
|
||||||
|
import org.dromara.system.mapper.SysUserMapper;
|
||||||
|
import org.dromara.web.domain.vo.LoginVo;
|
||||||
|
import org.dromara.web.service.IAuthStrategy;
|
||||||
|
import org.dromara.web.service.SysLoginService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短信认证策略
|
||||||
|
*
|
||||||
|
* @author Michelle.Chung
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service("sms" + IAuthStrategy.BASE_NAME)
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SmsAuthStrategy implements IAuthStrategy {
|
||||||
|
|
||||||
|
private final SysLoginService loginService;
|
||||||
|
private final SysUserMapper userMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validate(LoginBody loginBody) {
|
||||||
|
ValidatorUtils.validate(loginBody, SmsGroup.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LoginVo login(String clientId, LoginBody loginBody, SysClient client) {
|
||||||
|
String tenantId = loginBody.getTenantId();
|
||||||
|
String phonenumber = loginBody.getPhonenumber();
|
||||||
|
String smsCode = loginBody.getSmsCode();
|
||||||
|
|
||||||
|
// 通过手机号查找用户
|
||||||
|
SysUserVo user = loadUserByPhonenumber(tenantId, phonenumber);
|
||||||
|
|
||||||
|
loginService.checkLogin(LoginType.SMS, tenantId, user.getUserName(), () -> !validateSmsCode(tenantId, phonenumber, smsCode));
|
||||||
|
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||||
|
LoginUser loginUser = loginService.buildLoginUser(user);
|
||||||
|
SaLoginModel model = new SaLoginModel();
|
||||||
|
model.setDevice(client.getDeviceType());
|
||||||
|
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
|
||||||
|
// 例如: 后台用户30分钟过期 app用户1天过期
|
||||||
|
model.setTimeout(client.getTimeout());
|
||||||
|
model.setActiveTimeout(client.getActiveTimeout());
|
||||||
|
model.setExtra(LoginHelper.CLIENT_KEY, clientId);
|
||||||
|
// 生成token
|
||||||
|
LoginHelper.login(loginUser, model);
|
||||||
|
|
||||||
|
loginService.recordLogininfor(loginUser.getTenantId(), user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
|
||||||
|
loginService.recordLoginInfo(user.getUserId());
|
||||||
|
|
||||||
|
LoginVo loginVo = new LoginVo();
|
||||||
|
loginVo.setAccessToken(StpUtil.getTokenValue());
|
||||||
|
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
||||||
|
loginVo.setClientId(clientId);
|
||||||
|
return loginVo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验短信验证码
|
||||||
|
*/
|
||||||
|
private boolean validateSmsCode(String tenantId, String phonenumber, String smsCode) {
|
||||||
|
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + phonenumber);
|
||||||
|
if (StringUtils.isBlank(code)) {
|
||||||
|
loginService.recordLogininfor(tenantId, phonenumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||||
|
throw new CaptchaExpireException();
|
||||||
|
}
|
||||||
|
return code.equals(smsCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysUserVo loadUserByPhonenumber(String tenantId, String phonenumber) {
|
||||||
|
SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
|
||||||
|
.select(SysUser::getPhonenumber, SysUser::getStatus)
|
||||||
|
.eq(TenantHelper.isEnable(), SysUser::getTenantId, tenantId)
|
||||||
|
.eq(SysUser::getPhonenumber, phonenumber));
|
||||||
|
if (ObjectUtil.isNull(user)) {
|
||||||
|
log.info("登录用户:{} 不存在.", phonenumber);
|
||||||
|
throw new UserException("user.not.exists", phonenumber);
|
||||||
|
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
|
||||||
|
log.info("登录用户:{} 已被停用.", phonenumber);
|
||||||
|
throw new UserException("user.blocked", phonenumber);
|
||||||
|
}
|
||||||
|
if (TenantHelper.isEnable()) {
|
||||||
|
return userMapper.selectTenantUserByPhonenumber(phonenumber, tenantId);
|
||||||
|
}
|
||||||
|
return userMapper.selectUserByPhonenumber(phonenumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,138 @@
|
|||||||
|
package org.dromara.web.service.impl;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.SaLoginModel;
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import cn.hutool.core.map.MapUtil;
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.http.HttpUtil;
|
||||||
|
import cn.hutool.http.Method;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import me.zhyd.oauth.model.AuthResponse;
|
||||||
|
import me.zhyd.oauth.model.AuthUser;
|
||||||
|
import org.dromara.common.core.constant.Constants;
|
||||||
|
import org.dromara.common.core.domain.model.LoginBody;
|
||||||
|
import org.dromara.common.core.domain.model.LoginUser;
|
||||||
|
import org.dromara.common.core.enums.UserStatus;
|
||||||
|
import org.dromara.common.core.exception.ServiceException;
|
||||||
|
import org.dromara.common.core.exception.user.UserException;
|
||||||
|
import org.dromara.common.core.utils.MessageUtils;
|
||||||
|
import org.dromara.common.core.utils.ValidatorUtils;
|
||||||
|
import org.dromara.common.core.validate.auth.SocialGroup;
|
||||||
|
import org.dromara.common.satoken.utils.LoginHelper;
|
||||||
|
import org.dromara.common.social.config.properties.SocialProperties;
|
||||||
|
import org.dromara.common.social.utils.SocialUtils;
|
||||||
|
import org.dromara.common.tenant.helper.TenantHelper;
|
||||||
|
import org.dromara.system.domain.SysClient;
|
||||||
|
import org.dromara.system.domain.SysUser;
|
||||||
|
import org.dromara.system.domain.vo.SysSocialVo;
|
||||||
|
import org.dromara.system.domain.vo.SysUserVo;
|
||||||
|
import org.dromara.system.mapper.SysUserMapper;
|
||||||
|
import org.dromara.system.service.ISysSocialService;
|
||||||
|
import org.dromara.web.domain.vo.LoginVo;
|
||||||
|
import org.dromara.web.service.IAuthStrategy;
|
||||||
|
import org.dromara.web.service.SysLoginService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 第三方授权策略
|
||||||
|
*
|
||||||
|
* @author thiszhc is 三三
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service("social" + IAuthStrategy.BASE_NAME)
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SocialAuthStrategy implements IAuthStrategy {
|
||||||
|
|
||||||
|
private final SocialProperties socialProperties;
|
||||||
|
private final ISysSocialService sysSocialService;
|
||||||
|
private final SysUserMapper userMapper;
|
||||||
|
private final SysLoginService loginService;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validate(LoginBody loginBody) {
|
||||||
|
ValidatorUtils.validate(loginBody, SocialGroup.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录-第三方授权登录
|
||||||
|
*
|
||||||
|
* @param clientId 客户端id
|
||||||
|
* @param loginBody 登录信息
|
||||||
|
* @param client 客户端信息
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public LoginVo login(String clientId, LoginBody loginBody, SysClient client) {
|
||||||
|
AuthResponse<AuthUser> response = SocialUtils.loginAuth(loginBody, socialProperties);
|
||||||
|
if (!response.ok()) {
|
||||||
|
throw new ServiceException(response.getMsg());
|
||||||
|
}
|
||||||
|
AuthUser authUserData = response.getData();
|
||||||
|
if ("GITEE".equals(authUserData.getSource())) {
|
||||||
|
// 如用户使用 gitee 登录顺手 star 给作者一点支持 拒绝白嫖
|
||||||
|
HttpUtil.createRequest(Method.PUT, "https://gitee.com/api/v5/user/starred/dromara/RuoYi-Vue-Plus")
|
||||||
|
.formStr(MapUtil.of("access_token", authUserData.getToken().getAccessToken()))
|
||||||
|
.executeAsync();
|
||||||
|
HttpUtil.createRequest(Method.PUT, "https://gitee.com/api/v5/user/starred/dromara/RuoYi-Cloud-Plus")
|
||||||
|
.formStr(MapUtil.of("access_token", authUserData.getToken().getAccessToken()))
|
||||||
|
.executeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
SysSocialVo social = sysSocialService.selectByAuthId(authUserData.getSource() + authUserData.getUuid());
|
||||||
|
if (!ObjectUtil.isNotNull(social)) {
|
||||||
|
throw new ServiceException("你还没有绑定第三方账号,绑定后才可以登录!");
|
||||||
|
}
|
||||||
|
// 验证授权表里面的租户id是否包含当前租户id
|
||||||
|
String tenantId = social.getTenantId();
|
||||||
|
if (ObjectUtil.isNotNull(social) && StrUtil.isNotBlank(tenantId)
|
||||||
|
&& !tenantId.contains(loginBody.getTenantId())) {
|
||||||
|
throw new ServiceException("对不起,你没有权限登录当前租户!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找用户
|
||||||
|
SysUserVo user = loadUser(tenantId, social.getUserId());
|
||||||
|
|
||||||
|
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||||
|
LoginUser loginUser = loginService.buildLoginUser(user);
|
||||||
|
SaLoginModel model = new SaLoginModel();
|
||||||
|
model.setDevice(client.getDeviceType());
|
||||||
|
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
|
||||||
|
// 例如: 后台用户30分钟过期 app用户1天过期
|
||||||
|
model.setTimeout(client.getTimeout());
|
||||||
|
model.setActiveTimeout(client.getActiveTimeout());
|
||||||
|
model.setExtra(LoginHelper.CLIENT_KEY, clientId);
|
||||||
|
// 生成token
|
||||||
|
LoginHelper.login(loginUser, model);
|
||||||
|
|
||||||
|
loginService.recordLogininfor(loginUser.getTenantId(), user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
|
||||||
|
loginService.recordLoginInfo(user.getUserId());
|
||||||
|
|
||||||
|
LoginVo loginVo = new LoginVo();
|
||||||
|
loginVo.setAccessToken(StpUtil.getTokenValue());
|
||||||
|
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
||||||
|
loginVo.setClientId(clientId);
|
||||||
|
return loginVo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysUserVo loadUser(String tenantId, Long userId) {
|
||||||
|
SysUser user = userMapper.selectOne(new LambdaQueryWrapper<SysUser>()
|
||||||
|
.select(SysUser::getUserName, SysUser::getStatus)
|
||||||
|
.eq(TenantHelper.isEnable(), SysUser::getTenantId, tenantId)
|
||||||
|
.eq(SysUser::getUserId, userId));
|
||||||
|
if (ObjectUtil.isNull(user)) {
|
||||||
|
log.info("登录用户:{} 不存在.", "");
|
||||||
|
throw new UserException("user.not.exists", "");
|
||||||
|
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
|
||||||
|
log.info("登录用户:{} 已被停用.", "");
|
||||||
|
throw new UserException("user.blocked", "");
|
||||||
|
}
|
||||||
|
if (TenantHelper.isEnable()) {
|
||||||
|
return userMapper.selectTenantUserByUserName(user.getUserName(), tenantId);
|
||||||
|
}
|
||||||
|
return userMapper.selectUserByUserName(user.getUserName());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
package org.dromara.web.service.impl;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.SaLoginModel;
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.dromara.common.core.constant.Constants;
|
||||||
|
import org.dromara.common.core.domain.model.LoginBody;
|
||||||
|
import org.dromara.common.core.domain.model.XcxLoginUser;
|
||||||
|
import org.dromara.common.core.enums.UserStatus;
|
||||||
|
import org.dromara.common.core.utils.MessageUtils;
|
||||||
|
import org.dromara.common.core.utils.ValidatorUtils;
|
||||||
|
import org.dromara.common.core.validate.auth.WechatGroup;
|
||||||
|
import org.dromara.common.satoken.utils.LoginHelper;
|
||||||
|
import org.dromara.system.domain.SysClient;
|
||||||
|
import org.dromara.system.domain.vo.SysUserVo;
|
||||||
|
import org.dromara.web.domain.vo.LoginVo;
|
||||||
|
import org.dromara.web.service.IAuthStrategy;
|
||||||
|
import org.dromara.web.service.SysLoginService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮件认证策略
|
||||||
|
*
|
||||||
|
* @author Michelle.Chung
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service("xcx" + IAuthStrategy.BASE_NAME)
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class XcxAuthStrategy implements IAuthStrategy {
|
||||||
|
|
||||||
|
private final SysLoginService loginService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validate(LoginBody loginBody) {
|
||||||
|
ValidatorUtils.validate(loginBody, WechatGroup.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LoginVo login(String clientId, LoginBody loginBody, SysClient client) {
|
||||||
|
// xcxCode 为 小程序调用 wx.login 授权后获取
|
||||||
|
String xcxCode = loginBody.getXcxCode();
|
||||||
|
// todo 以下自行实现
|
||||||
|
// 校验 appid + appsrcret + xcxCode 调用登录凭证校验接口 获取 session_key 与 openid
|
||||||
|
String openid = "";
|
||||||
|
// 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
|
||||||
|
SysUserVo user = loadUserByOpenid(openid);
|
||||||
|
|
||||||
|
// 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
|
||||||
|
XcxLoginUser loginUser = new XcxLoginUser();
|
||||||
|
loginUser.setTenantId(user.getTenantId());
|
||||||
|
loginUser.setUserId(user.getUserId());
|
||||||
|
loginUser.setUsername(user.getUserName());
|
||||||
|
loginUser.setUserType(user.getUserType());
|
||||||
|
loginUser.setOpenid(openid);
|
||||||
|
|
||||||
|
SaLoginModel model = new SaLoginModel();
|
||||||
|
model.setDevice(client.getDeviceType());
|
||||||
|
// 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
|
||||||
|
// 例如: 后台用户30分钟过期 app用户1天过期
|
||||||
|
model.setTimeout(client.getTimeout());
|
||||||
|
model.setActiveTimeout(client.getActiveTimeout());
|
||||||
|
model.setExtra(LoginHelper.CLIENT_KEY, clientId);
|
||||||
|
// 生成token
|
||||||
|
LoginHelper.login(loginUser, model);
|
||||||
|
|
||||||
|
loginService.recordLogininfor(loginUser.getTenantId(), user.getUserName(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
|
||||||
|
loginService.recordLoginInfo(user.getUserId());
|
||||||
|
|
||||||
|
LoginVo loginVo = new LoginVo();
|
||||||
|
loginVo.setAccessToken(StpUtil.getTokenValue());
|
||||||
|
loginVo.setExpireIn(StpUtil.getTokenTimeout());
|
||||||
|
loginVo.setClientId(clientId);
|
||||||
|
loginVo.setOpenid(openid);
|
||||||
|
return loginVo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysUserVo loadUserByOpenid(String openid) {
|
||||||
|
// 使用 openid 查询绑定用户 如未绑定用户 则根据业务自行处理 例如 创建默认用户
|
||||||
|
// todo 自行实现 userService.selectUserByOpenid(openid);
|
||||||
|
SysUserVo user = new SysUserVo();
|
||||||
|
if (ObjectUtil.isNull(user)) {
|
||||||
|
log.info("登录用户:{} 不存在.", openid);
|
||||||
|
// todo 用户不存在 业务逻辑自行实现
|
||||||
|
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
|
||||||
|
log.info("登录用户:{} 已被停用.", openid);
|
||||||
|
// todo 用户已被停用 业务逻辑自行实现
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -8,27 +8,21 @@ spring.boot.admin.client:
|
|||||||
username: ruoyi
|
username: ruoyi
|
||||||
password: 123456
|
password: 123456
|
||||||
|
|
||||||
--- # xxl-job 配置
|
--- # powerjob 配置
|
||||||
xxl.job:
|
powerjob:
|
||||||
# 执行器开关
|
worker:
|
||||||
enabled: true
|
# 如何开启调度中心请查看文档教程
|
||||||
# 调度中心地址:如调度中心集群部署存在多个地址则用逗号分隔。
|
enabled: false
|
||||||
admin-addresses: http://localhost:9100/xxl-job-admin
|
# 需要先在 powerjob 登录页执行应用注册后才能使用
|
||||||
# 执行器通讯TOKEN:非空时启用
|
app-name: ruoyi-worker
|
||||||
access-token: xxl-job
|
enable-test-mode: false
|
||||||
executor:
|
max-appended-wf-context-length: 4096
|
||||||
# 执行器AppName:执行器心跳注册分组依据;为空则关闭自动注册
|
max-result-length: 4096
|
||||||
appname: xxl-job-executor
|
# 28080 端口 随着主应用端口飘逸 避免集群冲突
|
||||||
# 执行器端口号 执行器从9101开始往后写
|
port: 2${server.port}
|
||||||
port: 9101
|
protocol: http
|
||||||
# 执行器注册:默认IP:PORT
|
server-address: 127.0.0.1:7700
|
||||||
address:
|
store-strategy: disk
|
||||||
# 执行器IP:默认自动获取IP
|
|
||||||
ip:
|
|
||||||
# 执行器运行日志文件存储磁盘路径
|
|
||||||
logpath: ./logs/xxl-job
|
|
||||||
# 执行器日志文件保存天数:大于3生效
|
|
||||||
logretentiondays: 30
|
|
||||||
|
|
||||||
--- # 数据源配置
|
--- # 数据源配置
|
||||||
spring:
|
spring:
|
||||||
@ -130,7 +124,7 @@ spring.data:
|
|||||||
# 连接超时时间
|
# 连接超时时间
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
# 是否开启ssl
|
# 是否开启ssl
|
||||||
ssl: false
|
ssl.enabled: false
|
||||||
|
|
||||||
redisson:
|
redisson:
|
||||||
# redis key前缀
|
# redis key前缀
|
||||||
@ -176,14 +170,100 @@ mail:
|
|||||||
# Socket连接超时值,单位毫秒,缺省值不超时
|
# Socket连接超时值,单位毫秒,缺省值不超时
|
||||||
connectionTimeout: 0
|
connectionTimeout: 0
|
||||||
|
|
||||||
--- # sms 短信
|
--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
|
||||||
|
# https://wind.kim/doc/start 文档地址 各个厂商可同时使用
|
||||||
sms:
|
sms:
|
||||||
enabled: false
|
|
||||||
# 阿里云 dysmsapi.aliyuncs.com
|
# 阿里云 dysmsapi.aliyuncs.com
|
||||||
# 腾讯云 sms.tencentcloudapi.com
|
alibaba:
|
||||||
endpoint: "dysmsapi.aliyuncs.com"
|
#请求地址 默认为 dysmsapi.aliyuncs.com 如无特殊改变可以不用设置
|
||||||
|
requestUrl: dysmsapi.aliyuncs.com
|
||||||
|
#阿里云的accessKey
|
||||||
accessKeyId: xxxxxxx
|
accessKeyId: xxxxxxx
|
||||||
accessKeySecret: xxxxxx
|
#阿里云的accessKeySecret
|
||||||
signName: 测试
|
accessKeySecret: xxxxxxx
|
||||||
# 腾讯专用
|
#短信签名
|
||||||
sdkAppId:
|
signature: 测试
|
||||||
|
tencent:
|
||||||
|
#请求地址默认为 sms.tencentcloudapi.com 如无特殊改变可不用设置
|
||||||
|
requestUrl: sms.tencentcloudapi.com
|
||||||
|
#腾讯云的accessKey
|
||||||
|
accessKeyId: xxxxxxx
|
||||||
|
#腾讯云的accessKeySecret
|
||||||
|
accessKeySecret: xxxxxxx
|
||||||
|
#短信签名
|
||||||
|
signature: 测试
|
||||||
|
#短信sdkAppId
|
||||||
|
sdkAppId: appid
|
||||||
|
#地域信息默认为 ap-guangzhou 如无特殊改变可不用设置
|
||||||
|
territory: ap-guangzhou
|
||||||
|
|
||||||
|
|
||||||
|
--- # 三方授权
|
||||||
|
justauth:
|
||||||
|
enabled: true
|
||||||
|
# 前端外网访问地址
|
||||||
|
address: http://localhost:80
|
||||||
|
type:
|
||||||
|
maxkey:
|
||||||
|
# maxkey 服务器地址
|
||||||
|
# 注意 如下均配置均不需要修改 maxkey 已经内置好了数据
|
||||||
|
server-url: http://sso.maxkey.top
|
||||||
|
client-id: 876892492581044224
|
||||||
|
client-secret: x1Y5MTMwNzIwMjMxNTM4NDc3Mzche8
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=maxkey
|
||||||
|
qq:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=qq
|
||||||
|
union-id: false
|
||||||
|
weibo:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=weibo
|
||||||
|
gitee:
|
||||||
|
client-id: 91436b7940090d09c72c7daf85b959cfd5f215d67eea73acbf61b6b590751a98
|
||||||
|
client-secret: 02c6fcfd70342980cd8dd2f2c06c1a350645d76c754d7a264c4e125f9ba915ac
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=gitee
|
||||||
|
dingtalk:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=dingtalk
|
||||||
|
baidu:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=baidu
|
||||||
|
csdn:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=csdn
|
||||||
|
coding:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=coding
|
||||||
|
coding-group-name: xx
|
||||||
|
oschina:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=oschina
|
||||||
|
alipay:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=alipay
|
||||||
|
alipay-public-key: MIIB**************DAQAB
|
||||||
|
wechat_open:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=wechat_open
|
||||||
|
wechat_mp:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=wechat_mp
|
||||||
|
wechat_enterprise:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=wechat_enterprise
|
||||||
|
agent-id: 1000002
|
||||||
|
gitlab:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=gitlab
|
||||||
|
@ -11,27 +11,21 @@ spring.boot.admin.client:
|
|||||||
username: ruoyi
|
username: ruoyi
|
||||||
password: 123456
|
password: 123456
|
||||||
|
|
||||||
--- # xxl-job 配置
|
--- # powerjob 配置
|
||||||
xxl.job:
|
powerjob:
|
||||||
# 执行器开关
|
worker:
|
||||||
enabled: true
|
# 如何开启调度中心请查看文档教程
|
||||||
# 调度中心地址:如调度中心集群部署存在多个地址则用逗号分隔。
|
enabled: false
|
||||||
admin-addresses: http://localhost:9100/xxl-job-admin
|
# 需要先在 powerjob 登录页执行应用注册后才能使用
|
||||||
# 执行器通讯TOKEN:非空时启用
|
app-name: ruoyi-worker
|
||||||
access-token: xxl-job
|
enable-test-mode: false
|
||||||
executor:
|
max-appended-wf-context-length: 4096
|
||||||
# 执行器AppName:执行器心跳注册分组依据;为空则关闭自动注册
|
max-result-length: 4096
|
||||||
appname: xxl-job-executor
|
# 28080 端口 随着主应用端口飘逸 避免集群冲突
|
||||||
# 执行器端口号 执行器从9101开始往后写
|
port: 2${server.port}
|
||||||
port: 9101
|
protocol: http
|
||||||
# 执行器注册:默认IP:PORT
|
server-address: 127.0.0.1:7700
|
||||||
address:
|
store-strategy: disk
|
||||||
# 执行器IP:默认自动获取IP
|
|
||||||
ip:
|
|
||||||
# 执行器运行日志文件存储磁盘路径
|
|
||||||
logpath: ./logs/xxl-job
|
|
||||||
# 执行器日志文件保存天数:大于3生效
|
|
||||||
logretentiondays: 30
|
|
||||||
|
|
||||||
--- # 数据源配置
|
--- # 数据源配置
|
||||||
spring:
|
spring:
|
||||||
@ -133,7 +127,7 @@ spring.data:
|
|||||||
# 连接超时时间
|
# 连接超时时间
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
# 是否开启ssl
|
# 是否开启ssl
|
||||||
ssl: false
|
ssl.enabled: false
|
||||||
|
|
||||||
redisson:
|
redisson:
|
||||||
# redis key前缀
|
# redis key前缀
|
||||||
@ -179,14 +173,99 @@ mail:
|
|||||||
# Socket连接超时值,单位毫秒,缺省值不超时
|
# Socket连接超时值,单位毫秒,缺省值不超时
|
||||||
connectionTimeout: 0
|
connectionTimeout: 0
|
||||||
|
|
||||||
--- # sms 短信
|
--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
|
||||||
|
# https://wind.kim/doc/start 文档地址 各个厂商可同时使用
|
||||||
sms:
|
sms:
|
||||||
enabled: false
|
|
||||||
# 阿里云 dysmsapi.aliyuncs.com
|
# 阿里云 dysmsapi.aliyuncs.com
|
||||||
# 腾讯云 sms.tencentcloudapi.com
|
alibaba:
|
||||||
endpoint: "dysmsapi.aliyuncs.com"
|
#请求地址 默认为 dysmsapi.aliyuncs.com 如无特殊改变可以不用设置
|
||||||
|
requestUrl: dysmsapi.aliyuncs.com
|
||||||
|
#阿里云的accessKey
|
||||||
accessKeyId: xxxxxxx
|
accessKeyId: xxxxxxx
|
||||||
accessKeySecret: xxxxxx
|
#阿里云的accessKeySecret
|
||||||
signName: 测试
|
accessKeySecret: xxxxxxx
|
||||||
# 腾讯专用
|
#短信签名
|
||||||
sdkAppId:
|
signature: 测试
|
||||||
|
tencent:
|
||||||
|
#请求地址默认为 sms.tencentcloudapi.com 如无特殊改变可不用设置
|
||||||
|
requestUrl: sms.tencentcloudapi.com
|
||||||
|
#腾讯云的accessKey
|
||||||
|
accessKeyId: xxxxxxx
|
||||||
|
#腾讯云的accessKeySecret
|
||||||
|
accessKeySecret: xxxxxxx
|
||||||
|
#短信签名
|
||||||
|
signature: 测试
|
||||||
|
#短信sdkAppId
|
||||||
|
sdkAppId: appid
|
||||||
|
#地域信息默认为 ap-guangzhou 如无特殊改变可不用设置
|
||||||
|
territory: ap-guangzhou
|
||||||
|
|
||||||
|
--- # 三方授权
|
||||||
|
justauth:
|
||||||
|
enabled: true
|
||||||
|
# 前端外网访问地址
|
||||||
|
address: http://localhost:80
|
||||||
|
type:
|
||||||
|
maxkey:
|
||||||
|
# maxkey 服务器地址
|
||||||
|
# 注意 如下均配置均不需要修改 maxkey 已经内置好了数据
|
||||||
|
server-url: http://sso.maxkey.top
|
||||||
|
client-id: 876892492581044224
|
||||||
|
client-secret: x1Y5MTMwNzIwMjMxNTM4NDc3Mzche8
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=maxkey
|
||||||
|
qq:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=qq
|
||||||
|
union-id: false
|
||||||
|
weibo:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=weibo
|
||||||
|
gitee:
|
||||||
|
client-id: 91436b7940090d09c72c7daf85b959cfd5f215d67eea73acbf61b6b590751a98
|
||||||
|
client-secret: 02c6fcfd70342980cd8dd2f2c06c1a350645d76c754d7a264c4e125f9ba915ac
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=gitee
|
||||||
|
dingtalk:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=dingtalk
|
||||||
|
baidu:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=baidu
|
||||||
|
csdn:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=csdn
|
||||||
|
coding:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=coding
|
||||||
|
coding-group-name: xx
|
||||||
|
oschina:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=oschina
|
||||||
|
alipay:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=alipay
|
||||||
|
alipay-public-key: MIIB**************DAQAB
|
||||||
|
wechat_open:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=wechat_open
|
||||||
|
wechat_mp:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=wechat_mp
|
||||||
|
wechat_enterprise:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=wechat_enterprise
|
||||||
|
agent-id: 1000002
|
||||||
|
gitlab:
|
||||||
|
client-id: 10**********6
|
||||||
|
client-secret: 1f7d08**********5b7**********29e
|
||||||
|
redirect-uri: ${justauth.address}/social-callback?source=gitlab
|
||||||
|
@ -96,20 +96,15 @@ spring:
|
|||||||
sa-token:
|
sa-token:
|
||||||
# token名称 (同时也是cookie名称)
|
# token名称 (同时也是cookie名称)
|
||||||
token-name: Authorization
|
token-name: Authorization
|
||||||
# token有效期 设为一天 (必定过期) 单位: 秒
|
# token固定超时 设为七天 (必定过期) 单位: 秒
|
||||||
timeout: 86400
|
timeout: 604800
|
||||||
# token临时有效期 (指定时间无操作就过期) 单位: 秒
|
# 多端不同 token 有效期 可查看 LoginHelper.loginByDevice 方法自定义
|
||||||
activity-timeout: 1800
|
# token最低活跃时间 (指定时间无操作就过期) 单位: 秒
|
||||||
|
active-timeout: 1800
|
||||||
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
|
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
|
||||||
is-concurrent: true
|
is-concurrent: true
|
||||||
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
|
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
|
||||||
is-share: false
|
is-share: false
|
||||||
# 是否尝试从header里读取token
|
|
||||||
is-read-header: true
|
|
||||||
# 是否尝试从cookie里读取token
|
|
||||||
is-read-cookie: false
|
|
||||||
# token前缀
|
|
||||||
token-prefix: "Bearer"
|
|
||||||
# jwt秘钥
|
# jwt秘钥
|
||||||
jwt-secret-key: abcdefghijklmnopqrstuvwxyz
|
jwt-secret-key: abcdefghijklmnopqrstuvwxyz
|
||||||
|
|
||||||
@ -145,6 +140,7 @@ tenant:
|
|||||||
- sys_role_menu
|
- sys_role_menu
|
||||||
- sys_user_post
|
- sys_user_post
|
||||||
- sys_user_role
|
- sys_user_role
|
||||||
|
- sys_client
|
||||||
|
|
||||||
# MyBatisPlus配置
|
# MyBatisPlus配置
|
||||||
# https://baomidou.com/config/
|
# https://baomidou.com/config/
|
||||||
@ -156,39 +152,12 @@ mybatis-plus:
|
|||||||
mapperLocations: classpath*:mapper/**/*Mapper.xml
|
mapperLocations: classpath*:mapper/**/*Mapper.xml
|
||||||
# 实体扫描,多个package用逗号或者分号分隔
|
# 实体扫描,多个package用逗号或者分号分隔
|
||||||
typeAliasesPackage: org.dromara.**.domain
|
typeAliasesPackage: org.dromara.**.domain
|
||||||
# 启动时是否检查 MyBatis XML 文件的存在,默认不检查
|
|
||||||
checkConfigLocation: false
|
|
||||||
configuration:
|
|
||||||
# 自动驼峰命名规则(camel case)映射
|
|
||||||
mapUnderscoreToCamelCase: true
|
|
||||||
# MyBatis 自动映射策略
|
|
||||||
# NONE:不启用 PARTIAL:只对非嵌套 resultMap 自动映射 FULL:对所有 resultMap 自动映射
|
|
||||||
autoMappingBehavior: FULL
|
|
||||||
# MyBatis 自动映射时未知列或未知属性处理策
|
|
||||||
# NONE:不做处理 WARNING:打印相关警告 FAILING:抛出异常和详细信息
|
|
||||||
autoMappingUnknownColumnBehavior: NONE
|
|
||||||
# 更详细的日志输出 会有性能损耗 org.apache.ibatis.logging.stdout.StdOutImpl
|
|
||||||
# 关闭日志记录 (可单纯使用 p6spy 分析) org.apache.ibatis.logging.nologging.NoLoggingImpl
|
|
||||||
# 默认日志输出 org.apache.ibatis.logging.slf4j.Slf4jImpl
|
|
||||||
logImpl: org.apache.ibatis.logging.nologging.NoLoggingImpl
|
|
||||||
global-config:
|
global-config:
|
||||||
# 是否打印 Logo banner
|
|
||||||
banner: true
|
|
||||||
dbConfig:
|
dbConfig:
|
||||||
# 主键类型
|
# 主键类型
|
||||||
# AUTO 自增 NONE 空 INPUT 用户输入 ASSIGN_ID 雪花 ASSIGN_UUID 唯一 UUID
|
# AUTO 自增 NONE 空 INPUT 用户输入 ASSIGN_ID 雪花 ASSIGN_UUID 唯一 UUID
|
||||||
|
# 如需改为自增 需要将数据库表全部设置为自增
|
||||||
idType: ASSIGN_ID
|
idType: ASSIGN_ID
|
||||||
# 逻辑已删除值
|
|
||||||
logicDeleteValue: 2
|
|
||||||
# 逻辑未删除值
|
|
||||||
logicNotDeleteValue: 0
|
|
||||||
# 字段验证策略之 insert,在 insert 的时候的字段验证策略
|
|
||||||
# IGNORED 忽略 NOT_NULL 非NULL NOT_EMPTY 非空 DEFAULT 默认 NEVER 不加入 SQL
|
|
||||||
insertStrategy: NOT_NULL
|
|
||||||
# 字段验证策略之 update,在 update 的时候的字段验证策略
|
|
||||||
updateStrategy: NOT_NULL
|
|
||||||
# 字段验证策略之 select,在 select 的时候的字段验证策略既 wrapper 根据内部 entity 生成的 where 条件
|
|
||||||
where-strategy: NOT_NULL
|
|
||||||
|
|
||||||
# 数据加密
|
# 数据加密
|
||||||
mybatis-encryptor:
|
mybatis-encryptor:
|
||||||
@ -204,8 +173,23 @@ mybatis-encryptor:
|
|||||||
publicKey:
|
publicKey:
|
||||||
privateKey:
|
privateKey:
|
||||||
|
|
||||||
# Swagger配置
|
# api接口加密
|
||||||
swagger:
|
api-decrypt:
|
||||||
|
# 是否开启全局接口加密
|
||||||
|
enabled: true
|
||||||
|
# AES 加密头标识
|
||||||
|
headerFlag: encrypt-key
|
||||||
|
# 公私钥 非对称算法的公私钥 如:SM2,RSA 使用者请自行更换
|
||||||
|
publicKey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
|
||||||
|
privateKey: MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKNPuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gAkM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWowcSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99EcvDQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthhYhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3UP8iWi1Qw0Y=
|
||||||
|
|
||||||
|
springdoc:
|
||||||
|
api-docs:
|
||||||
|
# 是否开启接口文档
|
||||||
|
enabled: true
|
||||||
|
# swagger-ui:
|
||||||
|
# # 持久化认证数据
|
||||||
|
# persistAuthorization: true
|
||||||
info:
|
info:
|
||||||
# 标题
|
# 标题
|
||||||
title: '标题:${ruoyi.name}多租户管理系统_接口文档'
|
title: '标题:${ruoyi.name}多租户管理系统_接口文档'
|
||||||
@ -225,14 +209,6 @@ swagger:
|
|||||||
type: APIKEY
|
type: APIKEY
|
||||||
in: HEADER
|
in: HEADER
|
||||||
name: ${sa-token.token-name}
|
name: ${sa-token.token-name}
|
||||||
|
|
||||||
springdoc:
|
|
||||||
api-docs:
|
|
||||||
# 是否开启接口文档
|
|
||||||
enabled: true
|
|
||||||
swagger-ui:
|
|
||||||
# 持久化认证数据
|
|
||||||
persistAuthorization: true
|
|
||||||
#这里定义了两个分组,可定义多个,也可以不定义
|
#这里定义了两个分组,可定义多个,也可以不定义
|
||||||
group-configs:
|
group-configs:
|
||||||
- group: 1.演示模块
|
- group: 1.演示模块
|
||||||
@ -285,6 +261,6 @@ management:
|
|||||||
websocket:
|
websocket:
|
||||||
enabled: true
|
enabled: true
|
||||||
# 路径
|
# 路径
|
||||||
path: /websocket
|
path: /resource/websocket
|
||||||
# 设置访问源地址
|
# 设置访问源地址
|
||||||
allowedOrigins: '*'
|
allowedOrigins: '*'
|
||||||
|
@ -28,6 +28,9 @@ user.register.error=注册失败,请联系系统管理人员
|
|||||||
user.notfound=请重新登录
|
user.notfound=请重新登录
|
||||||
user.forcelogout=管理员强制退出,请重新登录
|
user.forcelogout=管理员强制退出,请重新登录
|
||||||
user.unknown.error=未知错误,请重新登录
|
user.unknown.error=未知错误,请重新登录
|
||||||
|
auth.grant.type.error=认证权限类型错误
|
||||||
|
auth.grant.type.not.blank=认证权限类型不能为空
|
||||||
|
auth.clientid.not.blank=认证客户端id不能为空
|
||||||
##文件上传消息
|
##文件上传消息
|
||||||
upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
|
upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
|
||||||
upload.filename.exceed.length=上传的文件名最长{0}个字符
|
upload.filename.exceed.length=上传的文件名最长{0}个字符
|
||||||
|
@ -28,6 +28,9 @@ user.register.error=Register failed, please contact system administrator
|
|||||||
user.notfound=Please login again
|
user.notfound=Please login again
|
||||||
user.forcelogout=The administrator is forced to exit,please login again
|
user.forcelogout=The administrator is forced to exit,please login again
|
||||||
user.unknown.error=Unknown error, please login again
|
user.unknown.error=Unknown error, please login again
|
||||||
|
auth.grant.type.error=Auth grant type error
|
||||||
|
auth.grant.type.not.blank=Auth grant type cannot be blank
|
||||||
|
auth.clientid.not.blank=Auth clientid cannot be blank
|
||||||
##文件上传消息
|
##文件上传消息
|
||||||
upload.exceed.maxSize=The uploaded file size exceeds the limit file size!<br/>the maximum allowed file size is:{0}MB!
|
upload.exceed.maxSize=The uploaded file size exceeds the limit file size!<br/>the maximum allowed file size is:{0}MB!
|
||||||
upload.filename.exceed.length=The maximum length of uploaded file name is {0} characters
|
upload.filename.exceed.length=The maximum length of uploaded file name is {0} characters
|
||||||
|
@ -28,6 +28,9 @@ user.register.error=注册失败,请联系系统管理人员
|
|||||||
user.notfound=请重新登录
|
user.notfound=请重新登录
|
||||||
user.forcelogout=管理员强制退出,请重新登录
|
user.forcelogout=管理员强制退出,请重新登录
|
||||||
user.unknown.error=未知错误,请重新登录
|
user.unknown.error=未知错误,请重新登录
|
||||||
|
auth.grant.type.error=认证权限类型错误
|
||||||
|
auth.grant.type.not.blank=认证权限类型不能为空
|
||||||
|
auth.clientid.not.blank=认证客户端id不能为空
|
||||||
##文件上传消息
|
##文件上传消息
|
||||||
upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
|
upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
|
||||||
upload.filename.exceed.length=上传的文件名最长{0}个字符
|
upload.filename.exceed.length=上传的文件名最长{0}个字符
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>ruoyi-common-bom</module>
|
<module>ruoyi-common-bom</module>
|
||||||
|
<module>ruoyi-common-social</module>
|
||||||
<module>ruoyi-common-core</module>
|
<module>ruoyi-common-core</module>
|
||||||
<module>ruoyi-common-doc</module>
|
<module>ruoyi-common-doc</module>
|
||||||
<module>ruoyi-common-excel</module>
|
<module>ruoyi-common-excel</module>
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
</description>
|
</description>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>5.0.0</revision>
|
<revision>5.1.0-BETA</revision>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
@ -117,6 +117,12 @@
|
|||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.dromara</groupId>
|
||||||
|
<artifactId>ruoyi-common-social</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- web服务 -->
|
<!-- web服务 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.dromara</groupId>
|
<groupId>org.dromara</groupId>
|
||||||
@ -165,6 +171,7 @@
|
|||||||
<artifactId>ruoyi-common-websocket</artifactId>
|
<artifactId>ruoyi-common-websocket</artifactId>
|
||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
@ -34,6 +34,11 @@
|
|||||||
<artifactId>spring-boot-starter-validation</artifactId>
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-aop</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!--常用工具类 -->
|
<!--常用工具类 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
|
@ -2,16 +2,14 @@ package org.dromara.common.core.config;
|
|||||||
|
|
||||||
import cn.hutool.core.util.ArrayUtil;
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
import org.dromara.common.core.exception.ServiceException;
|
import org.dromara.common.core.exception.ServiceException;
|
||||||
|
import org.dromara.common.core.utils.SpringUtils;
|
||||||
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
|
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
import org.springframework.scheduling.annotation.AsyncConfigurer;
|
import org.springframework.scheduling.annotation.AsyncConfigurer;
|
||||||
import org.springframework.scheduling.annotation.EnableAsync;
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异步配置
|
* 异步配置
|
||||||
@ -22,16 +20,12 @@ import java.util.concurrent.ScheduledExecutorService;
|
|||||||
@AutoConfiguration
|
@AutoConfiguration
|
||||||
public class AsyncConfig implements AsyncConfigurer {
|
public class AsyncConfig implements AsyncConfigurer {
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("scheduledExecutorService")
|
|
||||||
private ScheduledExecutorService scheduledExecutorService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自定义 @Async 注解使用系统线程池
|
* 自定义 @Async 注解使用系统线程池
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Executor getAsyncExecutor() {
|
public Executor getAsyncExecutor() {
|
||||||
return scheduledExecutorService;
|
return SpringUtils.getBean("scheduledExecutorService");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,7 +22,7 @@ public class ValidatorConfig {
|
|||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public Validator validator(MessageSource messageSource) {
|
public Validator validator(MessageSource messageSource) {
|
||||||
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
|
try (LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean()) {
|
||||||
// 国际化
|
// 国际化
|
||||||
factoryBean.setValidationMessageSource(messageSource);
|
factoryBean.setValidationMessageSource(messageSource);
|
||||||
// 设置使用 HibernateValidator 校验器
|
// 设置使用 HibernateValidator 校验器
|
||||||
@ -35,5 +35,6 @@ public class ValidatorConfig {
|
|||||||
factoryBean.afterPropertiesSet();
|
factoryBean.afterPropertiesSet();
|
||||||
return factoryBean.getValidator();
|
return factoryBean.getValidator();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -31,4 +31,9 @@ public interface GlobalConstants {
|
|||||||
* 登录账户密码错误次数 redis key
|
* 登录账户密码错误次数 redis key
|
||||||
*/
|
*/
|
||||||
String PWD_ERR_CNT_KEY = GLOBAL_REDIS_KEY + "pwd_err_cnt:";
|
String PWD_ERR_CNT_KEY = GLOBAL_REDIS_KEY + "pwd_err_cnt:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 三方认证 redis key
|
||||||
|
*/
|
||||||
|
String SOCIAL_AUTH_CODE_KEY = GLOBAL_REDIS_KEY + "social_auth_codes:";
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package org.dromara.common.core.domain.model;
|
package org.dromara.common.core.domain.model;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
import org.dromara.common.core.constant.UserConstants;
|
import org.dromara.common.core.constant.UserConstants;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import org.dromara.common.core.validate.auth.*;
|
||||||
import org.hibernate.validator.constraints.Length;
|
import org.hibernate.validator.constraints.Length;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
@ -15,6 +17,28 @@ import jakarta.validation.constraints.NotBlank;
|
|||||||
@Data
|
@Data
|
||||||
public class LoginBody {
|
public class LoginBody {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端id
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "{auth.clientid.not.blank}")
|
||||||
|
private String clientId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端key
|
||||||
|
*/
|
||||||
|
private String clientKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端秘钥
|
||||||
|
*/
|
||||||
|
private String clientSecret;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 授权类型
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "{auth.grant.type.not.blank}")
|
||||||
|
private String grantType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 租户ID
|
* 租户ID
|
||||||
*/
|
*/
|
||||||
@ -24,15 +48,15 @@ public class LoginBody {
|
|||||||
/**
|
/**
|
||||||
* 用户名
|
* 用户名
|
||||||
*/
|
*/
|
||||||
@NotBlank(message = "{user.username.not.blank}")
|
@NotBlank(message = "{user.username.not.blank}", groups = {PasswordGroup.class})
|
||||||
@Length(min = UserConstants.USERNAME_MIN_LENGTH, max = UserConstants.USERNAME_MAX_LENGTH, message = "{user.username.length.valid}")
|
@Length(min = UserConstants.USERNAME_MIN_LENGTH, max = UserConstants.USERNAME_MAX_LENGTH, message = "{user.username.length.valid}", groups = {PasswordGroup.class})
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户密码
|
* 用户密码
|
||||||
*/
|
*/
|
||||||
@NotBlank(message = "{user.password.not.blank}")
|
@NotBlank(message = "{user.password.not.blank}", groups = {PasswordGroup.class})
|
||||||
@Length(min = UserConstants.PASSWORD_MIN_LENGTH, max = UserConstants.PASSWORD_MAX_LENGTH, message = "{user.password.length.valid}")
|
@Length(min = UserConstants.PASSWORD_MIN_LENGTH, max = UserConstants.PASSWORD_MAX_LENGTH, message = "{user.password.length.valid}", groups = {PasswordGroup.class})
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,4 +69,52 @@ public class LoginBody {
|
|||||||
*/
|
*/
|
||||||
private String uuid;
|
private String uuid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "{user.phonenumber.not.blank}", groups = {SmsGroup.class})
|
||||||
|
private String phonenumber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短信code
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "{sms.code.not.blank}", groups = {SmsGroup.class})
|
||||||
|
private String smsCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮箱
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "{user.email.not.blank}", groups = {EmailGroup.class})
|
||||||
|
@Email(message = "{user.email.not.valid}")
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮箱code
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "{email.code.not.blank}", groups = {EmailGroup.class})
|
||||||
|
private String emailCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序code
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "{xcx.code.not.blank}", groups = {WechatGroup.class})
|
||||||
|
private String xcxCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 第三方登录平台
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "{social.source.not.blank}" , groups = {SocialGroup.class})
|
||||||
|
private String source;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 第三方登录code
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "{social.code.not.blank}" , groups = {SocialGroup.class})
|
||||||
|
private String socialCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 第三方登录socialState
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "{social.state.not.blank}" , groups = {SocialGroup.class})
|
||||||
|
private String socialState;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
package org.dromara.common.core.domain.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 第三方登录用户身份权限
|
||||||
|
*
|
||||||
|
* @author thiszhc is 三三
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class SocialLogin extends LoginUser{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* openid
|
||||||
|
*/
|
||||||
|
private String openid;
|
||||||
|
}
|
@ -26,7 +26,12 @@ public enum DeviceType {
|
|||||||
/**
|
/**
|
||||||
* 小程序端
|
* 小程序端
|
||||||
*/
|
*/
|
||||||
XCX("xcx");
|
XCX("xcx"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* social第三方端
|
||||||
|
*/
|
||||||
|
SOCIAL("social");
|
||||||
|
|
||||||
private final String device;
|
private final String device;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
package org.dromara.common.core.exception;
|
package org.dromara.common.core.exception;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -7,6 +12,10 @@ import java.io.Serial;
|
|||||||
*
|
*
|
||||||
* @author ruoyi
|
* @author ruoyi
|
||||||
*/
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public class GlobalException extends RuntimeException {
|
public class GlobalException extends RuntimeException {
|
||||||
|
|
||||||
@Serial
|
@Serial
|
||||||
@ -22,12 +31,6 @@ public class GlobalException extends RuntimeException {
|
|||||||
*/
|
*/
|
||||||
private String detailMessage;
|
private String detailMessage;
|
||||||
|
|
||||||
/**
|
|
||||||
* 空构造方法,避免反序列化问题
|
|
||||||
*/
|
|
||||||
public GlobalException() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public GlobalException(String message) {
|
public GlobalException(String message) {
|
||||||
this.message = message;
|
this.message = message;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
package org.dromara.common.core.exception;
|
package org.dromara.common.core.exception;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.io.Serial;
|
import java.io.Serial;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -7,6 +12,10 @@ import java.io.Serial;
|
|||||||
*
|
*
|
||||||
* @author ruoyi
|
* @author ruoyi
|
||||||
*/
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public final class ServiceException extends RuntimeException {
|
public final class ServiceException extends RuntimeException {
|
||||||
|
|
||||||
@Serial
|
@Serial
|
||||||
@ -27,12 +36,6 @@ public final class ServiceException extends RuntimeException {
|
|||||||
*/
|
*/
|
||||||
private String detailMessage;
|
private String detailMessage;
|
||||||
|
|
||||||
/**
|
|
||||||
* 空构造方法,避免反序列化问题
|
|
||||||
*/
|
|
||||||
public ServiceException() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public ServiceException(String message) {
|
public ServiceException(String message) {
|
||||||
this.message = message;
|
this.message = message;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.dromara.common.core.exception.base;
|
package org.dromara.common.core.exception.base;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import org.dromara.common.core.utils.MessageUtils;
|
import org.dromara.common.core.utils.MessageUtils;
|
||||||
import org.dromara.common.core.utils.StringUtils;
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@ -16,6 +17,7 @@ import java.io.Serial;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public class BaseException extends RuntimeException {
|
public class BaseException extends RuntimeException {
|
||||||
|
|
||||||
@Serial
|
@Serial
|
||||||
@ -41,13 +43,6 @@ public class BaseException extends RuntimeException {
|
|||||||
*/
|
*/
|
||||||
private String defaultMessage;
|
private String defaultMessage;
|
||||||
|
|
||||||
public BaseException(String module, String code, Object[] args, String defaultMessage) {
|
|
||||||
this.module = module;
|
|
||||||
this.code = code;
|
|
||||||
this.args = args;
|
|
||||||
this.defaultMessage = defaultMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BaseException(String module, String code, Object[] args) {
|
public BaseException(String module, String code, Object[] args) {
|
||||||
this(module, code, args, null);
|
this(module, code, args, null);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
package org.dromara.common.core.factory;
|
||||||
|
|
||||||
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
|
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
|
||||||
|
import org.springframework.core.env.PropertiesPropertySource;
|
||||||
|
import org.springframework.core.env.PropertySource;
|
||||||
|
import org.springframework.core.io.support.DefaultPropertySourceFactory;
|
||||||
|
import org.springframework.core.io.support.EncodedResource;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* yml 配置源工厂
|
||||||
|
*
|
||||||
|
* @author Lion Li
|
||||||
|
*/
|
||||||
|
public class YmlPropertySourceFactory extends DefaultPropertySourceFactory {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
|
||||||
|
String sourceName = resource.getResource().getFilename();
|
||||||
|
if (StringUtils.isNotBlank(sourceName) && StringUtils.endsWithAny(sourceName, ".yml", ".yaml")) {
|
||||||
|
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
|
||||||
|
factory.setResources(resource.getResource());
|
||||||
|
factory.afterPropertiesSet();
|
||||||
|
return new PropertiesPropertySource(sourceName, factory.getObject());
|
||||||
|
}
|
||||||
|
return super.createPropertySource(name, resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
package org.dromara.common.core.service;
|
package org.dromara.common.core.service;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通用 字典服务
|
* 通用 字典服务
|
||||||
*
|
*
|
||||||
@ -54,4 +56,12 @@ public interface DictService {
|
|||||||
*/
|
*/
|
||||||
String getDictValue(String dictType, String dictLabel, String separator);
|
String getDictValue(String dictType, String dictLabel, String separator);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取字典下所有的字典值与标签
|
||||||
|
*
|
||||||
|
* @param dictType 字典类型
|
||||||
|
* @return dictValue为key,dictLabel为值组成的Map
|
||||||
|
*/
|
||||||
|
Map<String, String> getAllDictByDictType(String dictType);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package org.dromara.common.core.utils;
|
|||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.context.NoSuchMessageException;
|
||||||
import org.springframework.context.i18n.LocaleContextHolder;
|
import org.springframework.context.i18n.LocaleContextHolder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -23,6 +24,10 @@ public class MessageUtils {
|
|||||||
* @return 获取国际化翻译值
|
* @return 获取国际化翻译值
|
||||||
*/
|
*/
|
||||||
public static String message(String code, Object... args) {
|
public static String message(String code, Object... args) {
|
||||||
|
try {
|
||||||
return MESSAGE_SOURCE.getMessage(code, args, LocaleContextHolder.getLocale());
|
return MESSAGE_SOURCE.getMessage(code, args, LocaleContextHolder.getLocale());
|
||||||
|
} catch (NoSuchMessageException e) {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import jakarta.servlet.http.HttpSession;
|
|||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.util.LinkedCaseInsensitiveMap;
|
||||||
import org.springframework.web.context.request.RequestAttributes;
|
import org.springframework.web.context.request.RequestAttributes;
|
||||||
import org.springframework.web.context.request.RequestContextHolder;
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
@ -19,6 +20,7 @@ import java.net.URLDecoder;
|
|||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Enumeration;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -101,14 +103,22 @@ public class ServletUtils extends JakartaServletUtil {
|
|||||||
* 获取request
|
* 获取request
|
||||||
*/
|
*/
|
||||||
public static HttpServletRequest getRequest() {
|
public static HttpServletRequest getRequest() {
|
||||||
|
try {
|
||||||
return getRequestAttributes().getRequest();
|
return getRequestAttributes().getRequest();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取response
|
* 获取response
|
||||||
*/
|
*/
|
||||||
public static HttpServletResponse getResponse() {
|
public static HttpServletResponse getResponse() {
|
||||||
|
try {
|
||||||
return getRequestAttributes().getResponse();
|
return getRequestAttributes().getResponse();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -119,8 +129,33 @@ public class ServletUtils extends JakartaServletUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static ServletRequestAttributes getRequestAttributes() {
|
public static ServletRequestAttributes getRequestAttributes() {
|
||||||
|
try {
|
||||||
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
|
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
|
||||||
return (ServletRequestAttributes) attributes;
|
return (ServletRequestAttributes) attributes;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getHeader(HttpServletRequest request, String name) {
|
||||||
|
String value = request.getHeader(name);
|
||||||
|
if (StringUtils.isEmpty(value)) {
|
||||||
|
return StringUtils.EMPTY;
|
||||||
|
}
|
||||||
|
return urlDecode(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<String, String> getHeaders(HttpServletRequest request) {
|
||||||
|
Map<String, String> map = new LinkedCaseInsensitiveMap<>();
|
||||||
|
Enumeration<String> enumeration = request.getHeaderNames();
|
||||||
|
if (enumeration != null) {
|
||||||
|
while (enumeration.hasMoreElements()) {
|
||||||
|
String key = enumeration.nextElement();
|
||||||
|
String value = request.getHeader(key);
|
||||||
|
map.put(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -72,7 +72,7 @@ public class StreamUtils {
|
|||||||
return CollUtil.newArrayList();
|
return CollUtil.newArrayList();
|
||||||
}
|
}
|
||||||
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
|
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
|
||||||
return collection.stream().sorted(comparing).collect(Collectors.toList());
|
return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -89,7 +89,7 @@ public class StreamUtils {
|
|||||||
if (CollUtil.isEmpty(collection)) {
|
if (CollUtil.isEmpty(collection)) {
|
||||||
return MapUtil.newHashMap();
|
return MapUtil.newHashMap();
|
||||||
}
|
}
|
||||||
return collection.stream().collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
|
return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -108,7 +108,7 @@ public class StreamUtils {
|
|||||||
if (CollUtil.isEmpty(collection)) {
|
if (CollUtil.isEmpty(collection)) {
|
||||||
return MapUtil.newHashMap();
|
return MapUtil.newHashMap();
|
||||||
}
|
}
|
||||||
return collection.stream().collect(Collectors.toMap(key, value, (l, r) -> l));
|
return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, value, (l, r) -> l));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -126,7 +126,7 @@ public class StreamUtils {
|
|||||||
return MapUtil.newHashMap();
|
return MapUtil.newHashMap();
|
||||||
}
|
}
|
||||||
return collection
|
return collection
|
||||||
.stream()
|
.stream().filter(Objects::nonNull)
|
||||||
.collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList()));
|
.collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ public class StreamUtils {
|
|||||||
return MapUtil.newHashMap();
|
return MapUtil.newHashMap();
|
||||||
}
|
}
|
||||||
return collection
|
return collection
|
||||||
.stream()
|
.stream().filter(Objects::nonNull)
|
||||||
.collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList())));
|
.collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList())));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,7 +168,7 @@ public class StreamUtils {
|
|||||||
return MapUtil.newHashMap();
|
return MapUtil.newHashMap();
|
||||||
}
|
}
|
||||||
return collection
|
return collection
|
||||||
.stream()
|
.stream().filter(Objects::nonNull)
|
||||||
.collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l)));
|
.collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
package org.dromara.common.core.validate.auth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author Michelle.Chung
|
||||||
|
*/
|
||||||
|
public interface EmailGroup {
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package org.dromara.common.core.validate.auth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author Michelle.Chung
|
||||||
|
*/
|
||||||
|
public interface PasswordGroup {
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package org.dromara.common.core.validate.auth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author Michelle.Chung
|
||||||
|
*/
|
||||||
|
public interface SmsGroup {
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package org.dromara.common.core.validate.auth;
|
||||||
|
|
||||||
|
public interface SocialGroup {
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package org.dromara.common.core.validate.auth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author Michelle.Chung
|
||||||
|
*/
|
||||||
|
public interface WechatGroup {
|
||||||
|
}
|
@ -1,13 +1,13 @@
|
|||||||
package org.dromara.common.doc.config;
|
package org.dromara.common.doc.config;
|
||||||
|
|
||||||
import org.dromara.common.core.utils.StringUtils;
|
|
||||||
import org.dromara.common.doc.config.properties.SwaggerProperties;
|
|
||||||
import org.dromara.common.doc.handler.OpenApiHandler;
|
|
||||||
import io.swagger.v3.oas.models.OpenAPI;
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
import io.swagger.v3.oas.models.Paths;
|
import io.swagger.v3.oas.models.Paths;
|
||||||
import io.swagger.v3.oas.models.info.Info;
|
import io.swagger.v3.oas.models.info.Info;
|
||||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
|
import org.dromara.common.doc.config.properties.SpringDocProperties;
|
||||||
|
import org.dromara.common.doc.handler.OpenApiHandler;
|
||||||
import org.springdoc.core.configuration.SpringDocConfiguration;
|
import org.springdoc.core.configuration.SpringDocConfiguration;
|
||||||
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
|
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
|
||||||
import org.springdoc.core.customizers.OpenApiCustomizer;
|
import org.springdoc.core.customizers.OpenApiCustomizer;
|
||||||
@ -36,26 +36,26 @@ import java.util.Set;
|
|||||||
*/
|
*/
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@AutoConfiguration(before = SpringDocConfiguration.class)
|
@AutoConfiguration(before = SpringDocConfiguration.class)
|
||||||
@EnableConfigurationProperties(SwaggerProperties.class)
|
@EnableConfigurationProperties(SpringDocProperties.class)
|
||||||
@ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true", matchIfMissing = true)
|
@ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true", matchIfMissing = true)
|
||||||
public class SwaggerConfig {
|
public class SpringDocConfig {
|
||||||
|
|
||||||
private final ServerProperties serverProperties;
|
private final ServerProperties serverProperties;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean(OpenAPI.class)
|
@ConditionalOnMissingBean(OpenAPI.class)
|
||||||
public OpenAPI openApi(SwaggerProperties swaggerProperties) {
|
public OpenAPI openApi(SpringDocProperties properties) {
|
||||||
OpenAPI openApi = new OpenAPI();
|
OpenAPI openApi = new OpenAPI();
|
||||||
// 文档基本信息
|
// 文档基本信息
|
||||||
SwaggerProperties.InfoProperties infoProperties = swaggerProperties.getInfo();
|
SpringDocProperties.InfoProperties infoProperties = properties.getInfo();
|
||||||
Info info = convertInfo(infoProperties);
|
Info info = convertInfo(infoProperties);
|
||||||
openApi.info(info);
|
openApi.info(info);
|
||||||
// 扩展文档信息
|
// 扩展文档信息
|
||||||
openApi.externalDocs(swaggerProperties.getExternalDocs());
|
openApi.externalDocs(properties.getExternalDocs());
|
||||||
openApi.tags(swaggerProperties.getTags());
|
openApi.tags(properties.getTags());
|
||||||
openApi.paths(swaggerProperties.getPaths());
|
openApi.paths(properties.getPaths());
|
||||||
openApi.components(swaggerProperties.getComponents());
|
openApi.components(properties.getComponents());
|
||||||
Set<String> keySet = swaggerProperties.getComponents().getSecuritySchemes().keySet();
|
Set<String> keySet = properties.getComponents().getSecuritySchemes().keySet();
|
||||||
List<SecurityRequirement> list = new ArrayList<>();
|
List<SecurityRequirement> list = new ArrayList<>();
|
||||||
SecurityRequirement securityRequirement = new SecurityRequirement();
|
SecurityRequirement securityRequirement = new SecurityRequirement();
|
||||||
keySet.forEach(securityRequirement::addList);
|
keySet.forEach(securityRequirement::addList);
|
||||||
@ -65,7 +65,7 @@ public class SwaggerConfig {
|
|||||||
return openApi;
|
return openApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Info convertInfo(SwaggerProperties.InfoProperties infoProperties) {
|
private Info convertInfo(SpringDocProperties.InfoProperties infoProperties) {
|
||||||
Info info = new Info();
|
Info info = new Info();
|
||||||
info.setTitle(infoProperties.getTitle());
|
info.setTitle(infoProperties.getTitle());
|
||||||
info.setDescription(infoProperties.getDescription());
|
info.setDescription(infoProperties.getDescription());
|
@ -18,8 +18,8 @@ import java.util.List;
|
|||||||
* @author Lion Li
|
* @author Lion Li
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@ConfigurationProperties(prefix = "swagger")
|
@ConfigurationProperties(prefix = "springdoc")
|
||||||
public class SwaggerProperties {
|
public class SpringDocProperties {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文档基本信息
|
* 文档基本信息
|
@ -1 +1 @@
|
|||||||
org.dromara.common.doc.config.SwaggerConfig
|
org.dromara.common.doc.config.SpringDocConfig
|
||||||
|
@ -32,7 +32,7 @@ public @interface EncryptField {
|
|||||||
String publicKey() default "";
|
String publicKey() default "";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 公钥。RSA、SM2需要
|
* 私钥。RSA、SM2需要
|
||||||
*/
|
*/
|
||||||
String privateKey() default "";
|
String privateKey() default "";
|
||||||
|
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
package org.dromara.common.encrypt.config;
|
||||||
|
|
||||||
|
import jakarta.servlet.DispatcherType;
|
||||||
|
import org.dromara.common.encrypt.filter.CryptoFilter;
|
||||||
|
import org.dromara.common.encrypt.properties.ApiDecryptProperties;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* api 解密自动配置
|
||||||
|
*
|
||||||
|
* @author wdhcr
|
||||||
|
*/
|
||||||
|
@AutoConfiguration
|
||||||
|
@EnableConfigurationProperties(ApiDecryptProperties.class)
|
||||||
|
@ConditionalOnProperty(value = "api-decrypt.enabled", havingValue = "true")
|
||||||
|
public class ApiDecryptAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public FilterRegistrationBean<CryptoFilter> cryptoFilterRegistration(ApiDecryptProperties properties) {
|
||||||
|
FilterRegistrationBean<CryptoFilter> registration = new FilterRegistrationBean<>();
|
||||||
|
registration.setDispatcherTypes(DispatcherType.REQUEST);
|
||||||
|
registration.setFilter(new CryptoFilter(properties));
|
||||||
|
registration.addUrlPatterns("/*");
|
||||||
|
registration.setName("cryptoFilter");
|
||||||
|
registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
|
||||||
|
return registration;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package org.dromara.common.encrypt.filter;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import jakarta.servlet.*;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
|
import org.dromara.common.encrypt.properties.ApiDecryptProperties;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crypto 过滤器
|
||||||
|
*
|
||||||
|
* @author wdhcr
|
||||||
|
*/
|
||||||
|
public class CryptoFilter implements Filter {
|
||||||
|
private final ApiDecryptProperties properties;
|
||||||
|
|
||||||
|
public CryptoFilter(ApiDecryptProperties properties) {
|
||||||
|
this.properties = properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||||
|
ServletRequest requestWrapper = null;
|
||||||
|
HttpServletRequest servletRequest = (HttpServletRequest) request;
|
||||||
|
// 是否为 json 请求
|
||||||
|
if (StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
|
||||||
|
// 是否为 put 或者 post 请求
|
||||||
|
if (HttpMethod.PUT.matches(servletRequest.getMethod()) || HttpMethod.POST.matches(servletRequest.getMethod())) {
|
||||||
|
// 是否存在加密标头
|
||||||
|
String headerValue = servletRequest.getHeader(properties.getHeaderFlag());
|
||||||
|
if (StringUtils.isNotBlank(headerValue)) {
|
||||||
|
requestWrapper = new DecryptRequestBodyWrapper(servletRequest, properties.getPublicKey(), properties.getPrivateKey(), properties.getHeaderFlag());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.doFilter(ObjectUtil.defaultIfNull(requestWrapper, request), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
package org.dromara.common.encrypt.filter;
|
||||||
|
|
||||||
|
import cn.hutool.core.io.IoUtil;
|
||||||
|
import jakarta.servlet.ReadListener;
|
||||||
|
import jakarta.servlet.ServletInputStream;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||||
|
import org.dromara.common.core.constant.Constants;
|
||||||
|
import org.dromara.common.encrypt.utils.EncryptUtils;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密请求参数工具类
|
||||||
|
*
|
||||||
|
* @author wdhcr
|
||||||
|
*/
|
||||||
|
public class DecryptRequestBodyWrapper extends HttpServletRequestWrapper {
|
||||||
|
|
||||||
|
private final byte[] body;
|
||||||
|
|
||||||
|
public DecryptRequestBodyWrapper(HttpServletRequest request, String publicKey, String privateKey, String headerFlag) throws IOException {
|
||||||
|
super(request);
|
||||||
|
// 获取 AES 密码 采用 RSA 加密
|
||||||
|
String headerRsa = request.getHeader(headerFlag);
|
||||||
|
String decryptAes = EncryptUtils.decryptByRsa(headerRsa, privateKey);
|
||||||
|
// 解密 AES 密码
|
||||||
|
String aesPassword = EncryptUtils.decryptByBase64(decryptAes);
|
||||||
|
request.setCharacterEncoding(Constants.UTF8);
|
||||||
|
byte[] readBytes = IoUtil.readBytes(request.getInputStream(), false);
|
||||||
|
String requestBody = new String(readBytes, StandardCharsets.UTF_8);
|
||||||
|
// 解密 body 采用 AES 加密
|
||||||
|
String decryptBody = EncryptUtils.decryptByAes(requestBody, aesPassword);
|
||||||
|
body = decryptBody.getBytes(StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BufferedReader getReader() {
|
||||||
|
return new BufferedReader(new InputStreamReader(getInputStream()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getContentLength() {
|
||||||
|
return body.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getContentLengthLong() {
|
||||||
|
return body.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getContentType() {
|
||||||
|
return MediaType.APPLICATION_JSON_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServletInputStream getInputStream() {
|
||||||
|
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
|
||||||
|
return new ServletInputStream() {
|
||||||
|
@Override
|
||||||
|
public int read() {
|
||||||
|
return bais.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int available() {
|
||||||
|
return body.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isFinished() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReady() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setReadListener(ReadListener readListener) {
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package org.dromara.common.encrypt.interceptor;
|
package org.dromara.common.encrypt.interceptor;
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.hutool.core.convert.Convert;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -75,7 +76,7 @@ public class MybatisDecryptInterceptor implements Interceptor {
|
|||||||
Set<Field> fields = encryptorManager.getFieldCache(sourceObject.getClass());
|
Set<Field> fields = encryptorManager.getFieldCache(sourceObject.getClass());
|
||||||
try {
|
try {
|
||||||
for (Field field : fields) {
|
for (Field field : fields) {
|
||||||
field.set(sourceObject, this.decryptField(String.valueOf(field.get(sourceObject)), field));
|
field.set(sourceObject, this.decryptField(Convert.toStr(field.get(sourceObject)), field));
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("处理解密字段时出错", e);
|
log.error("处理解密字段时出错", e);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.dromara.common.encrypt.interceptor;
|
package org.dromara.common.encrypt.interceptor;
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.hutool.core.convert.Convert;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -84,7 +85,7 @@ public class MybatisEncryptInterceptor implements Interceptor {
|
|||||||
Set<Field> fields = encryptorManager.getFieldCache(sourceObject.getClass());
|
Set<Field> fields = encryptorManager.getFieldCache(sourceObject.getClass());
|
||||||
try {
|
try {
|
||||||
for (Field field : fields) {
|
for (Field field : fields) {
|
||||||
field.set(sourceObject, this.encryptField(String.valueOf(field.get(sourceObject)), field));
|
field.set(sourceObject, this.encryptField(Convert.toStr(field.get(sourceObject)), field));
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("处理加密字段时出错", e);
|
log.error("处理加密字段时出错", e);
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
package org.dromara.common.encrypt.properties;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* api解密属性配置类
|
||||||
|
* @author wdhcr
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@ConfigurationProperties(prefix = "api-decrypt")
|
||||||
|
public class ApiDecryptProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密开关
|
||||||
|
*/
|
||||||
|
private Boolean enabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 头部标识
|
||||||
|
*/
|
||||||
|
private String headerFlag;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公钥
|
||||||
|
*/
|
||||||
|
private String publicKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 私钥
|
||||||
|
*/
|
||||||
|
private String privateKey;
|
||||||
|
}
|
@ -1 +1,3 @@
|
|||||||
org.dromara.common.encrypt.config.EncryptorAutoConfiguration
|
org.dromara.common.encrypt.config.EncryptorAutoConfiguration
|
||||||
|
org.dromara.common.encrypt.config.ApiDecryptAutoConfiguration
|
||||||
|
|
||||||
|
@ -37,14 +37,26 @@ public class ExcelEnumConvert implements Converter<Object> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
|
public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
|
||||||
Object codeValue = cellData.getData();
|
cellData.checkEmpty();
|
||||||
|
// Excel中填入的是枚举中指定的描述
|
||||||
|
Object textValue = switch (cellData.getType()) {
|
||||||
|
case STRING, DIRECT_STRING, RICH_TEXT_STRING -> cellData.getStringValue();
|
||||||
|
case NUMBER -> cellData.getNumberValue();
|
||||||
|
case BOOLEAN -> cellData.getBooleanValue();
|
||||||
|
default -> throw new IllegalArgumentException("单元格类型异常!");
|
||||||
|
};
|
||||||
// 如果是空值
|
// 如果是空值
|
||||||
if (ObjectUtil.isNull(codeValue)) {
|
if (ObjectUtil.isNull(textValue)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Map<Object, String> enumValueMap = beforeConvert(contentProperty);
|
Map<Object, String> enumCodeToTextMap = beforeConvert(contentProperty);
|
||||||
String textValue = enumValueMap.get(codeValue);
|
// 从Java输出至Excel是code转text
|
||||||
return Convert.convert(contentProperty.getField().getType(), textValue);
|
// 因此从Excel转Java应该将text与code对调
|
||||||
|
Map<Object, Object> enumTextToCodeMap = new HashMap<>();
|
||||||
|
enumCodeToTextMap.forEach((key, value) -> enumTextToCodeMap.put(value, key));
|
||||||
|
// 应该从text -> code中查找
|
||||||
|
Object codeValue = enumTextToCodeMap.get(textValue);
|
||||||
|
return Convert.convert(contentProperty.getField().getType(), codeValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -0,0 +1,149 @@
|
|||||||
|
package org.dromara.common.excel.core;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.dromara.common.core.exception.ServiceException;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>Excel下拉可选项</h1>
|
||||||
|
* 注意:为确保下拉框解析正确,传值务必使用createOptionValue()做为值的拼接
|
||||||
|
*
|
||||||
|
* @author Emil.Zhang
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class DropDownOptions {
|
||||||
|
/**
|
||||||
|
* 一级下拉所在列index,从0开始算
|
||||||
|
*/
|
||||||
|
private int index = 0;
|
||||||
|
/**
|
||||||
|
* 二级下拉所在的index,从0开始算,不能与一级相同
|
||||||
|
*/
|
||||||
|
private int nextIndex = 0;
|
||||||
|
/**
|
||||||
|
* 一级下拉所包含的数据
|
||||||
|
*/
|
||||||
|
private List<String> options = new ArrayList<>();
|
||||||
|
/**
|
||||||
|
* 二级下拉所包含的数据Map
|
||||||
|
* <p>以每一个一级选项值为Key,每个一级选项对应的二级数据为Value</p>
|
||||||
|
*/
|
||||||
|
private Map<String, List<String>> nextOptions = new HashMap<>();
|
||||||
|
/**
|
||||||
|
* 分隔符
|
||||||
|
*/
|
||||||
|
private static final String DELIMITER = "_";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建只有一级的下拉选
|
||||||
|
*/
|
||||||
|
public DropDownOptions(int index, List<String> options) {
|
||||||
|
this.index = index;
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>创建每个选项可选值</h2>
|
||||||
|
* <p>注意:不能以数字,特殊符号开头,选项中不可以包含任何运算符号</p>
|
||||||
|
*
|
||||||
|
* @param vars 可选值内包含的参数
|
||||||
|
* @return 合规的可选值
|
||||||
|
*/
|
||||||
|
public static String createOptionValue(Object... vars) {
|
||||||
|
StringBuilder stringBuffer = new StringBuilder();
|
||||||
|
String regex = "^[\\S\\d\\u4e00-\\u9fa5]+$";
|
||||||
|
for (int i = 0; i < vars.length; i++) {
|
||||||
|
String var = StrUtil.trimToEmpty(String.valueOf(vars[i]));
|
||||||
|
if (!var.matches(regex)) {
|
||||||
|
throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字");
|
||||||
|
}
|
||||||
|
stringBuffer.append(var);
|
||||||
|
if (i < vars.length - 1) {
|
||||||
|
// 直至最后一个前,都以_作为切割线
|
||||||
|
stringBuffer.append(DELIMITER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stringBuffer.toString().matches("^\\d_*$")) {
|
||||||
|
throw new ServiceException("禁止以数字开头");
|
||||||
|
}
|
||||||
|
return stringBuffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将处理后合理的可选值解析为原始的参数
|
||||||
|
*
|
||||||
|
* @param option 经过处理后的合理的可选项
|
||||||
|
* @return 原始的参数
|
||||||
|
*/
|
||||||
|
public static List<String> analyzeOptionValue(String option) {
|
||||||
|
return StrUtil.split(option, DELIMITER, true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建级联下拉选项
|
||||||
|
*
|
||||||
|
* @param parentList 父实体可选项原始数据
|
||||||
|
* @param parentIndex 父下拉选位置
|
||||||
|
* @param sonList 子实体可选项原始数据
|
||||||
|
* @param sonIndex 子下拉选位置
|
||||||
|
* @param parentHowToGetIdFunction 父类如何获取唯一标识
|
||||||
|
* @param sonHowToGetParentIdFunction 子类如何获取父类的唯一标识
|
||||||
|
* @param howToBuildEveryOption 如何生成下拉选内容
|
||||||
|
* @return 级联下拉选项
|
||||||
|
*/
|
||||||
|
public static <T> DropDownOptions buildLinkedOptions(List<T> parentList,
|
||||||
|
int parentIndex,
|
||||||
|
List<T> sonList,
|
||||||
|
int sonIndex,
|
||||||
|
Function<T, Number> parentHowToGetIdFunction,
|
||||||
|
Function<T, Number> sonHowToGetParentIdFunction,
|
||||||
|
Function<T, String> howToBuildEveryOption) {
|
||||||
|
DropDownOptions parentLinkSonOptions = new DropDownOptions();
|
||||||
|
// 先创建父类的下拉
|
||||||
|
parentLinkSonOptions.setIndex(parentIndex);
|
||||||
|
parentLinkSonOptions.setOptions(
|
||||||
|
parentList.stream()
|
||||||
|
.map(howToBuildEveryOption)
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
);
|
||||||
|
// 提取父-子级联下拉
|
||||||
|
Map<String, List<String>> sonOptions = new HashMap<>();
|
||||||
|
// 父级依据自己的ID分组
|
||||||
|
Map<Number, List<T>> parentGroupByIdMap =
|
||||||
|
parentList.stream().collect(Collectors.groupingBy(parentHowToGetIdFunction));
|
||||||
|
// 遍历每个子集,提取到Map中
|
||||||
|
sonList.forEach(everySon -> {
|
||||||
|
if (parentGroupByIdMap.containsKey(sonHowToGetParentIdFunction.apply(everySon))) {
|
||||||
|
// 找到对应的上级
|
||||||
|
T parentObj = parentGroupByIdMap.get(sonHowToGetParentIdFunction.apply(everySon)).get(0);
|
||||||
|
// 提取名称和ID作为Key
|
||||||
|
String key = howToBuildEveryOption.apply(parentObj);
|
||||||
|
// Key对应的Value
|
||||||
|
List<String> thisParentSonOptionList;
|
||||||
|
if (sonOptions.containsKey(key)) {
|
||||||
|
thisParentSonOptionList = sonOptions.get(key);
|
||||||
|
} else {
|
||||||
|
thisParentSonOptionList = new ArrayList<>();
|
||||||
|
sonOptions.put(key, thisParentSonOptionList);
|
||||||
|
}
|
||||||
|
// 往Value中添加当前子集选项
|
||||||
|
thisParentSonOptionList.add(howToBuildEveryOption.apply(everySon));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parentLinkSonOptions.setNextIndex(sonIndex);
|
||||||
|
parentLinkSonOptions.setNextOptions(sonOptions);
|
||||||
|
return parentLinkSonOptions;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,373 @@
|
|||||||
|
package org.dromara.common.excel.core;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
|
import cn.hutool.core.util.EnumUtil;
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.alibaba.excel.annotation.ExcelProperty;
|
||||||
|
import com.alibaba.excel.write.handler.SheetWriteHandler;
|
||||||
|
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
|
||||||
|
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.poi.ss.usermodel.*;
|
||||||
|
import org.apache.poi.ss.util.CellRangeAddressList;
|
||||||
|
import org.apache.poi.ss.util.WorkbookUtil;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFDataValidation;
|
||||||
|
import org.dromara.common.core.exception.ServiceException;
|
||||||
|
import org.dromara.common.core.service.DictService;
|
||||||
|
import org.dromara.common.core.utils.SpringUtils;
|
||||||
|
import org.dromara.common.core.utils.StreamUtils;
|
||||||
|
import org.dromara.common.excel.annotation.ExcelDictFormat;
|
||||||
|
import org.dromara.common.excel.annotation.ExcelEnumFormat;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>Excel表格下拉选操作</h1>
|
||||||
|
* 考虑到下拉选过多可能导致Excel打开缓慢的问题,只校验前1000行
|
||||||
|
* <p>
|
||||||
|
* 即只有前1000行的数据可以用下拉框,超出的自行通过限制数据量的形式,第二次输出
|
||||||
|
*
|
||||||
|
* @author Emil.Zhang
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class ExcelDownHandler implements SheetWriteHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Excel表格中的列名英文
|
||||||
|
* 仅为了解析列英文,禁止修改
|
||||||
|
*/
|
||||||
|
private static final String EXCEL_COLUMN_NAME = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
/**
|
||||||
|
* 单选数据Sheet名
|
||||||
|
*/
|
||||||
|
private static final String OPTIONS_SHEET_NAME = "options";
|
||||||
|
/**
|
||||||
|
* 联动选择数据Sheet名的头
|
||||||
|
*/
|
||||||
|
private static final String LINKED_OPTIONS_SHEET_NAME = "linkedOptions";
|
||||||
|
/**
|
||||||
|
* 下拉可选项
|
||||||
|
*/
|
||||||
|
private final List<DropDownOptions> dropDownOptions;
|
||||||
|
/**
|
||||||
|
* 当前单选进度
|
||||||
|
*/
|
||||||
|
private int currentOptionsColumnIndex;
|
||||||
|
/**
|
||||||
|
* 当前联动选择进度
|
||||||
|
*/
|
||||||
|
private int currentLinkedOptionsSheetIndex;
|
||||||
|
private final DictService dictService;
|
||||||
|
|
||||||
|
public ExcelDownHandler(List<DropDownOptions> options) {
|
||||||
|
this.dropDownOptions = options;
|
||||||
|
this.currentOptionsColumnIndex = 0;
|
||||||
|
this.currentLinkedOptionsSheetIndex = 0;
|
||||||
|
this.dictService = SpringUtils.getBean(DictService.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>开始创建下拉数据</h2>
|
||||||
|
* 1.通过解析传入的@ExcelProperty同级是否标注有@DropDown选项
|
||||||
|
* 如果有且设置了value值,则将其直接置为下拉可选项
|
||||||
|
* <p>
|
||||||
|
* 2.或者在调用ExcelUtil时指定了可选项,将依据传入的可选项做下拉
|
||||||
|
* <p>
|
||||||
|
* 3.二者并存,注意调用方式
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
|
||||||
|
Sheet sheet = writeSheetHolder.getSheet();
|
||||||
|
// 开始设置下拉框 HSSFWorkbook
|
||||||
|
DataValidationHelper helper = sheet.getDataValidationHelper();
|
||||||
|
Field[] fields = writeWorkbookHolder.getClazz().getDeclaredFields();
|
||||||
|
Workbook workbook = writeWorkbookHolder.getWorkbook();
|
||||||
|
int length = fields.length;
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
// 循环实体中的每个属性
|
||||||
|
// 可选的下拉值
|
||||||
|
List<String> options = new ArrayList<>();
|
||||||
|
if (fields[i].isAnnotationPresent(ExcelDictFormat.class)) {
|
||||||
|
// 如果指定了@ExcelDictFormat,则使用字典的逻辑
|
||||||
|
ExcelDictFormat format = fields[i].getDeclaredAnnotation(ExcelDictFormat.class);
|
||||||
|
String dictType = format.dictType();
|
||||||
|
String converterExp = format.readConverterExp();
|
||||||
|
if (StrUtil.isNotBlank(dictType)) {
|
||||||
|
// 如果传递了字典名,则依据字典建立下拉
|
||||||
|
Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType))
|
||||||
|
.orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType)))
|
||||||
|
.values();
|
||||||
|
options = new ArrayList<>(values);
|
||||||
|
} else if (StrUtil.isNotBlank(converterExp)) {
|
||||||
|
// 如果指定了确切的值,则直接解析确切的值
|
||||||
|
options = StrUtil.split(converterExp, format.separator(), true, true);
|
||||||
|
}
|
||||||
|
} else if (fields[i].isAnnotationPresent(ExcelEnumFormat.class)) {
|
||||||
|
// 否则如果指定了@ExcelEnumFormat,则使用枚举的逻辑
|
||||||
|
ExcelEnumFormat format = fields[i].getDeclaredAnnotation(ExcelEnumFormat.class);
|
||||||
|
List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField());
|
||||||
|
options = StreamUtils.toList(values, String::valueOf);
|
||||||
|
}
|
||||||
|
if (ObjectUtil.isNotEmpty(options)) {
|
||||||
|
// 仅当下拉可选项不为空时执行
|
||||||
|
// 获取列下标,默认为当前循环次数
|
||||||
|
int index = i;
|
||||||
|
if (fields[i].isAnnotationPresent(ExcelProperty.class)) {
|
||||||
|
// 如果指定了列下标,以指定的为主
|
||||||
|
index = fields[i].getDeclaredAnnotation(ExcelProperty.class).index();
|
||||||
|
}
|
||||||
|
if (options.size() > 20) {
|
||||||
|
// 这里限制如果可选项大于20,则使用额外表形式
|
||||||
|
dropDownWithSheet(helper, workbook, sheet, index, options);
|
||||||
|
} else {
|
||||||
|
// 否则使用固定值形式
|
||||||
|
dropDownWithSimple(helper, sheet, index, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (CollUtil.isEmpty(dropDownOptions)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dropDownOptions.forEach(everyOptions -> {
|
||||||
|
// 如果传递了下拉框选择器参数
|
||||||
|
if (!everyOptions.getNextOptions().isEmpty()) {
|
||||||
|
// 当二级选项不为空时,使用额外关联表的形式
|
||||||
|
dropDownLinkedOptions(helper, workbook, sheet, everyOptions);
|
||||||
|
} else if (everyOptions.getOptions().size() > 10) {
|
||||||
|
// 当一级选项参数个数大于10,使用额外表的形式
|
||||||
|
dropDownWithSheet(helper, workbook, sheet, everyOptions.getIndex(), everyOptions.getOptions());
|
||||||
|
} else if (everyOptions.getOptions().size() != 0) {
|
||||||
|
// 当一级选项个数不为空,使用默认形式
|
||||||
|
dropDownWithSimple(helper, sheet, everyOptions.getIndex(), everyOptions.getOptions());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>简单下拉框</h2>
|
||||||
|
* 直接将可选项拼接为指定列的数据校验值
|
||||||
|
*
|
||||||
|
* @param celIndex 列index
|
||||||
|
* @param value 下拉选可选值
|
||||||
|
*/
|
||||||
|
private void dropDownWithSimple(DataValidationHelper helper, Sheet sheet, Integer celIndex, List<String> value) {
|
||||||
|
if (ObjectUtil.isEmpty(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.markOptionsToSheet(helper, sheet, celIndex, helper.createExplicitListConstraint(ArrayUtil.toArray(value, String.class)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>额外表格形式的级联下拉框</h2>
|
||||||
|
*
|
||||||
|
* @param options 额外表格形式存储的下拉可选项
|
||||||
|
*/
|
||||||
|
private void dropDownLinkedOptions(DataValidationHelper helper, Workbook workbook, Sheet sheet, DropDownOptions options) {
|
||||||
|
String linkedOptionsSheetName = String.format("%s_%d", LINKED_OPTIONS_SHEET_NAME, currentLinkedOptionsSheetIndex);
|
||||||
|
// 创建联动下拉数据表
|
||||||
|
Sheet linkedOptionsDataSheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(linkedOptionsSheetName));
|
||||||
|
// 将下拉表隐藏
|
||||||
|
workbook.setSheetHidden(workbook.getSheetIndex(linkedOptionsDataSheet), true);
|
||||||
|
// 完善横向的一级选项数据表
|
||||||
|
List<String> firstOptions = options.getOptions();
|
||||||
|
Map<String, List<String>> secoundOptionsMap = options.getNextOptions();
|
||||||
|
|
||||||
|
// 创建名称管理器
|
||||||
|
Name name = workbook.createName();
|
||||||
|
// 设置名称管理器的别名
|
||||||
|
name.setNameName(linkedOptionsSheetName);
|
||||||
|
// 以横向第一行创建一级下拉拼接引用位置
|
||||||
|
String firstOptionsFunction = String.format("%s!$%s$1:$%s$1",
|
||||||
|
linkedOptionsSheetName,
|
||||||
|
getExcelColumnName(0),
|
||||||
|
getExcelColumnName(firstOptions.size())
|
||||||
|
);
|
||||||
|
// 设置名称管理器的引用位置
|
||||||
|
name.setRefersToFormula(firstOptionsFunction);
|
||||||
|
// 设置数据校验为序列模式,引用的是名称管理器中的别名
|
||||||
|
this.markOptionsToSheet(helper, sheet, options.getIndex(), helper.createFormulaListConstraint(linkedOptionsSheetName));
|
||||||
|
|
||||||
|
for (int columIndex = 0; columIndex < firstOptions.size(); columIndex++) {
|
||||||
|
// 先提取主表中一级下拉的列名
|
||||||
|
String firstOptionsColumnName = getExcelColumnName(columIndex);
|
||||||
|
// 一次循环是每一个一级选项
|
||||||
|
int finalI = columIndex;
|
||||||
|
// 本次循环的一级选项值
|
||||||
|
String thisFirstOptionsValue = firstOptions.get(columIndex);
|
||||||
|
// 创建第一行的数据
|
||||||
|
Optional.ofNullable(linkedOptionsDataSheet.getRow(0))
|
||||||
|
// 如果不存在则创建第一行
|
||||||
|
.orElseGet(() -> linkedOptionsDataSheet.createRow(finalI))
|
||||||
|
// 第一行当前列
|
||||||
|
.createCell(columIndex)
|
||||||
|
// 设置值为当前一级选项值
|
||||||
|
.setCellValue(thisFirstOptionsValue);
|
||||||
|
|
||||||
|
// 第二行开始,设置第二级别选项参数
|
||||||
|
List<String> secondOptions = secoundOptionsMap.get(thisFirstOptionsValue);
|
||||||
|
if (CollUtil.isEmpty(secondOptions)) {
|
||||||
|
// 必须保证至少有一个关联选项,否则将导致Excel解析错误
|
||||||
|
secondOptions = Collections.singletonList("暂无_0");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 以该一级选项值创建子名称管理器
|
||||||
|
Name sonName = workbook.createName();
|
||||||
|
// 设置名称管理器的别名
|
||||||
|
sonName.setNameName(thisFirstOptionsValue);
|
||||||
|
// 以第二行该列数据拼接引用位置
|
||||||
|
String sonFunction = String.format("%s!$%s$2:$%s$%d",
|
||||||
|
linkedOptionsSheetName,
|
||||||
|
firstOptionsColumnName,
|
||||||
|
firstOptionsColumnName,
|
||||||
|
secondOptions.size() + 1
|
||||||
|
);
|
||||||
|
// 设置名称管理器的引用位置
|
||||||
|
sonName.setRefersToFormula(sonFunction);
|
||||||
|
// 数据验证为序列模式,引用到每一个主表中的二级选项位置
|
||||||
|
// 创建子项的名称管理器,只是为了使得Excel可以识别到数据
|
||||||
|
String mainSheetFirstOptionsColumnName = getExcelColumnName(options.getIndex());
|
||||||
|
for (int i = 0; i < 100; i++) {
|
||||||
|
// 以一级选项对应的主体所在位置创建二级下拉
|
||||||
|
String secondOptionsFunction = String.format("=INDIRECT(%s%d)", mainSheetFirstOptionsColumnName, i + 1);
|
||||||
|
// 二级只能主表每一行的每一列添加二级校验
|
||||||
|
markLinkedOptionsToSheet(helper, sheet, i, options.getNextIndex(), helper.createFormulaListConstraint(secondOptionsFunction));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int rowIndex = 0; rowIndex < secondOptions.size(); rowIndex++) {
|
||||||
|
// 从第二行开始填充二级选项
|
||||||
|
int finalRowIndex = rowIndex + 1;
|
||||||
|
int finalColumIndex = columIndex;
|
||||||
|
|
||||||
|
Row row = Optional.ofNullable(linkedOptionsDataSheet.getRow(finalRowIndex))
|
||||||
|
// 没有则创建
|
||||||
|
.orElseGet(() -> linkedOptionsDataSheet.createRow(finalRowIndex));
|
||||||
|
Optional
|
||||||
|
// 在本级一级选项所在的列
|
||||||
|
.ofNullable(row.getCell(finalColumIndex))
|
||||||
|
// 不存在则创建
|
||||||
|
.orElseGet(() -> row.createCell(finalColumIndex))
|
||||||
|
// 设置二级选项值
|
||||||
|
.setCellValue(secondOptions.get(rowIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLinkedOptionsSheetIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>额外表格形式的普通下拉框</h2>
|
||||||
|
* 由于下拉框可选值数量过多,为提升Excel打开效率,使用额外表格形式做下拉
|
||||||
|
*
|
||||||
|
* @param celIndex 下拉选
|
||||||
|
* @param value 下拉选可选值
|
||||||
|
*/
|
||||||
|
private void dropDownWithSheet(DataValidationHelper helper, Workbook workbook, Sheet sheet, Integer celIndex, List<String> value) {
|
||||||
|
// 创建下拉数据表
|
||||||
|
Sheet simpleDataSheet = Optional.ofNullable(workbook.getSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)))
|
||||||
|
.orElseGet(() -> workbook.createSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)));
|
||||||
|
// 将下拉表隐藏
|
||||||
|
workbook.setSheetHidden(workbook.getSheetIndex(simpleDataSheet), true);
|
||||||
|
// 完善纵向的一级选项数据表
|
||||||
|
for (int i = 0; i < value.size(); i++) {
|
||||||
|
int finalI = i;
|
||||||
|
// 获取每一选项行,如果没有则创建
|
||||||
|
Row row = Optional.ofNullable(simpleDataSheet.getRow(i))
|
||||||
|
.orElseGet(() -> simpleDataSheet.createRow(finalI));
|
||||||
|
// 获取本级选项对应的选项列,如果没有则创建
|
||||||
|
Cell cell = Optional.ofNullable(row.getCell(currentOptionsColumnIndex))
|
||||||
|
.orElseGet(() -> row.createCell(currentOptionsColumnIndex));
|
||||||
|
// 设置值
|
||||||
|
cell.setCellValue(value.get(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建名称管理器
|
||||||
|
Name name = workbook.createName();
|
||||||
|
// 设置名称管理器的别名
|
||||||
|
String nameName = String.format("%s_%d", OPTIONS_SHEET_NAME, celIndex);
|
||||||
|
name.setNameName(nameName);
|
||||||
|
// 以纵向第一列创建一级下拉拼接引用位置
|
||||||
|
String function = String.format("%s!$%s$1:$%s$%d",
|
||||||
|
OPTIONS_SHEET_NAME,
|
||||||
|
getExcelColumnName(currentOptionsColumnIndex),
|
||||||
|
getExcelColumnName(currentOptionsColumnIndex),
|
||||||
|
value.size());
|
||||||
|
// 设置名称管理器的引用位置
|
||||||
|
name.setRefersToFormula(function);
|
||||||
|
// 设置数据校验为序列模式,引用的是名称管理器中的别名
|
||||||
|
this.markOptionsToSheet(helper, sheet, celIndex, helper.createFormulaListConstraint(nameName));
|
||||||
|
currentOptionsColumnIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 挂载下拉的列,仅限一级选项
|
||||||
|
*/
|
||||||
|
private void markOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer celIndex,
|
||||||
|
DataValidationConstraint constraint) {
|
||||||
|
// 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
|
||||||
|
CellRangeAddressList addressList = new CellRangeAddressList(1, 1000, celIndex, celIndex);
|
||||||
|
markDataValidationToSheet(helper, sheet, constraint, addressList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 挂载下拉的列,仅限二级选项
|
||||||
|
*/
|
||||||
|
private void markLinkedOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer rowIndex,
|
||||||
|
Integer celIndex, DataValidationConstraint constraint) {
|
||||||
|
// 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
|
||||||
|
CellRangeAddressList addressList = new CellRangeAddressList(rowIndex, rowIndex, celIndex, celIndex);
|
||||||
|
markDataValidationToSheet(helper, sheet, constraint, addressList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用数据校验
|
||||||
|
*/
|
||||||
|
private void markDataValidationToSheet(DataValidationHelper helper, Sheet sheet,
|
||||||
|
DataValidationConstraint constraint, CellRangeAddressList addressList) {
|
||||||
|
// 数据有效性对象
|
||||||
|
DataValidation dataValidation = helper.createValidation(constraint, addressList);
|
||||||
|
// 处理Excel兼容性问题
|
||||||
|
if (dataValidation instanceof XSSFDataValidation) {
|
||||||
|
//数据校验
|
||||||
|
dataValidation.setSuppressDropDownArrow(true);
|
||||||
|
//错误提示
|
||||||
|
dataValidation.setErrorStyle(DataValidation.ErrorStyle.STOP);
|
||||||
|
dataValidation.createErrorBox("提示", "此值与单元格定义数据不一致");
|
||||||
|
dataValidation.setShowErrorBox(true);
|
||||||
|
//选定提示
|
||||||
|
dataValidation.createPromptBox("填写说明:", "填写内容只能为下拉中数据,其他数据将导致导入失败");
|
||||||
|
dataValidation.setShowPromptBox(true);
|
||||||
|
sheet.addValidationData(dataValidation);
|
||||||
|
} else {
|
||||||
|
dataValidation.setSuppressDropDownArrow(false);
|
||||||
|
}
|
||||||
|
sheet.addValidationData(dataValidation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>依据列index获取列名英文</h2>
|
||||||
|
* 依据列index转换为Excel中的列名英文
|
||||||
|
* <p>例如第1列,index为0,解析出来为A列</p>
|
||||||
|
* 第27列,index为26,解析为AA列
|
||||||
|
* <p>第28列,index为27,解析为AB列</p>
|
||||||
|
*
|
||||||
|
* @param columnIndex 列index
|
||||||
|
* @return 列index所在得英文名
|
||||||
|
*/
|
||||||
|
private String getExcelColumnName(int columnIndex) {
|
||||||
|
// 26一循环的次数
|
||||||
|
int columnCircleCount = columnIndex / 26;
|
||||||
|
// 26一循环内的位置
|
||||||
|
int thisCircleColumnIndex = columnIndex % 26;
|
||||||
|
// 26一循环的次数大于0,则视为栏名至少两位
|
||||||
|
String columnPrefix = columnCircleCount == 0
|
||||||
|
? StrUtil.EMPTY
|
||||||
|
: StrUtil.subWithLength(EXCEL_COLUMN_NAME, columnCircleCount - 1, 1);
|
||||||
|
// 从26一循环内取对应的栏位名
|
||||||
|
String columnNext = StrUtil.subWithLength(EXCEL_COLUMN_NAME, thisCircleColumnIndex, 1);
|
||||||
|
// 将二者拼接即为最终的栏位名
|
||||||
|
return columnPrefix + columnNext;
|
||||||
|
}
|
||||||
|
}
|
@ -10,21 +10,19 @@ import com.alibaba.excel.write.metadata.WriteSheet;
|
|||||||
import com.alibaba.excel.write.metadata.fill.FillConfig;
|
import com.alibaba.excel.write.metadata.fill.FillConfig;
|
||||||
import com.alibaba.excel.write.metadata.fill.FillWrapper;
|
import com.alibaba.excel.write.metadata.fill.FillWrapper;
|
||||||
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
|
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
|
||||||
import org.dromara.common.core.utils.StringUtils;
|
|
||||||
import org.dromara.common.core.utils.file.FileUtils;
|
|
||||||
import org.dromara.common.excel.convert.ExcelBigNumberConvert;
|
|
||||||
import org.dromara.common.excel.core.CellMergeStrategy;
|
|
||||||
import org.dromara.common.excel.core.DefaultExcelListener;
|
|
||||||
import org.dromara.common.excel.core.ExcelListener;
|
|
||||||
import org.dromara.common.excel.core.ExcelResult;
|
|
||||||
import jakarta.servlet.ServletOutputStream;
|
import jakarta.servlet.ServletOutputStream;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
|
import org.dromara.common.core.utils.file.FileUtils;
|
||||||
|
import org.dromara.common.excel.convert.ExcelBigNumberConvert;
|
||||||
|
import org.dromara.common.excel.core.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -87,7 +85,26 @@ public class ExcelUtil {
|
|||||||
try {
|
try {
|
||||||
resetResponse(sheetName, response);
|
resetResponse(sheetName, response);
|
||||||
ServletOutputStream os = response.getOutputStream();
|
ServletOutputStream os = response.getOutputStream();
|
||||||
exportExcel(list, sheetName, clazz, false, os);
|
exportExcel(list, sheetName, clazz, false, os, null);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("导出Excel异常");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出excel
|
||||||
|
*
|
||||||
|
* @param list 导出数据集合
|
||||||
|
* @param sheetName 工作表的名称
|
||||||
|
* @param clazz 实体类
|
||||||
|
* @param response 响应体
|
||||||
|
* @param options 级联下拉选
|
||||||
|
*/
|
||||||
|
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, HttpServletResponse response, List<DropDownOptions> options) {
|
||||||
|
try {
|
||||||
|
resetResponse(sheetName, response);
|
||||||
|
ServletOutputStream os = response.getOutputStream();
|
||||||
|
exportExcel(list, sheetName, clazz, false, os, options);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException("导出Excel异常");
|
throw new RuntimeException("导出Excel异常");
|
||||||
}
|
}
|
||||||
@ -106,7 +123,27 @@ public class ExcelUtil {
|
|||||||
try {
|
try {
|
||||||
resetResponse(sheetName, response);
|
resetResponse(sheetName, response);
|
||||||
ServletOutputStream os = response.getOutputStream();
|
ServletOutputStream os = response.getOutputStream();
|
||||||
exportExcel(list, sheetName, clazz, merge, os);
|
exportExcel(list, sheetName, clazz, merge, os, null);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("导出Excel异常");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出excel
|
||||||
|
*
|
||||||
|
* @param list 导出数据集合
|
||||||
|
* @param sheetName 工作表的名称
|
||||||
|
* @param clazz 实体类
|
||||||
|
* @param merge 是否合并单元格
|
||||||
|
* @param response 响应体
|
||||||
|
* @param options 级联下拉选
|
||||||
|
*/
|
||||||
|
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, HttpServletResponse response, List<DropDownOptions> options) {
|
||||||
|
try {
|
||||||
|
resetResponse(sheetName, response);
|
||||||
|
ServletOutputStream os = response.getOutputStream();
|
||||||
|
exportExcel(list, sheetName, clazz, merge, os, options);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException("导出Excel异常");
|
throw new RuntimeException("导出Excel异常");
|
||||||
}
|
}
|
||||||
@ -121,7 +158,20 @@ public class ExcelUtil {
|
|||||||
* @param os 输出流
|
* @param os 输出流
|
||||||
*/
|
*/
|
||||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os) {
|
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os) {
|
||||||
exportExcel(list, sheetName, clazz, false, os);
|
exportExcel(list, sheetName, clazz, false, os, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出excel
|
||||||
|
*
|
||||||
|
* @param list 导出数据集合
|
||||||
|
* @param sheetName 工作表的名称
|
||||||
|
* @param clazz 实体类
|
||||||
|
* @param os 输出流
|
||||||
|
* @param options 级联下拉选内容
|
||||||
|
*/
|
||||||
|
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os, List<DropDownOptions> options) {
|
||||||
|
exportExcel(list, sheetName, clazz, false, os, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -133,7 +183,8 @@ public class ExcelUtil {
|
|||||||
* @param merge 是否合并单元格
|
* @param merge 是否合并单元格
|
||||||
* @param os 输出流
|
* @param os 输出流
|
||||||
*/
|
*/
|
||||||
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, OutputStream os) {
|
public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge,
|
||||||
|
OutputStream os, List<DropDownOptions> options) {
|
||||||
ExcelWriterSheetBuilder builder = EasyExcel.write(os, clazz)
|
ExcelWriterSheetBuilder builder = EasyExcel.write(os, clazz)
|
||||||
.autoCloseStream(false)
|
.autoCloseStream(false)
|
||||||
// 自动适配
|
// 自动适配
|
||||||
@ -145,6 +196,8 @@ public class ExcelUtil {
|
|||||||
// 合并处理器
|
// 合并处理器
|
||||||
builder.registerWriteHandler(new CellMergeStrategy(list, true));
|
builder.registerWriteHandler(new CellMergeStrategy(list, true));
|
||||||
}
|
}
|
||||||
|
// 添加下拉框操作
|
||||||
|
builder.registerWriteHandler(new ExcelDownHandler(options));
|
||||||
builder.doWrite(list);
|
builder.doWrite(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,7 +306,7 @@ public class ExcelUtil {
|
|||||||
/**
|
/**
|
||||||
* 重置响应体
|
* 重置响应体
|
||||||
*/
|
*/
|
||||||
private static void resetResponse(String sheetName, HttpServletResponse response) {
|
private static void resetResponse(String sheetName, HttpServletResponse response) throws UnsupportedEncodingException {
|
||||||
String filename = encodingFilename(sheetName);
|
String filename = encodingFilename(sheetName);
|
||||||
FileUtils.setAttachmentResponseHeader(response, filename);
|
FileUtils.setAttachmentResponseHeader(response, filename);
|
||||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
|
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
|
||||||
@ -275,7 +328,7 @@ public class ExcelUtil {
|
|||||||
if (StringUtils.containsAny(propertyValue, separator)) {
|
if (StringUtils.containsAny(propertyValue, separator)) {
|
||||||
for (String value : propertyValue.split(separator)) {
|
for (String value : propertyValue.split(separator)) {
|
||||||
if (itemArray[0].equals(value)) {
|
if (itemArray[0].equals(value)) {
|
||||||
propertyString.append(itemArray[1]).append(separator);
|
propertyString.append(itemArray[1] + separator);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -304,7 +357,7 @@ public class ExcelUtil {
|
|||||||
if (StringUtils.containsAny(propertyValue, separator)) {
|
if (StringUtils.containsAny(propertyValue, separator)) {
|
||||||
for (String value : propertyValue.split(separator)) {
|
for (String value : propertyValue.split(separator)) {
|
||||||
if (itemArray[1].equals(value)) {
|
if (itemArray[1].equals(value)) {
|
||||||
propertyString.append(itemArray[0]).append(separator);
|
propertyString.append(itemArray[0] + separator);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.dromara.common.idempotent.aspectj;
|
package org.dromara.common.idempotent.aspectj;
|
||||||
|
|
||||||
import cn.dev33.satoken.SaManager;
|
import cn.dev33.satoken.SaManager;
|
||||||
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import cn.hutool.crypto.SecureUtil;
|
import cn.hutool.crypto.SecureUtil;
|
||||||
import org.dromara.common.core.constant.GlobalConstants;
|
import org.dromara.common.core.constant.GlobalConstants;
|
||||||
@ -25,6 +26,7 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.StringJoiner;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 防止重复提交(参考美团GTIS防重系统)
|
* 防止重复提交(参考美团GTIS防重系统)
|
||||||
@ -39,10 +41,8 @@ public class RepeatSubmitAspect {
|
|||||||
@Before("@annotation(repeatSubmit)")
|
@Before("@annotation(repeatSubmit)")
|
||||||
public void doBefore(JoinPoint point, RepeatSubmit repeatSubmit) throws Throwable {
|
public void doBefore(JoinPoint point, RepeatSubmit repeatSubmit) throws Throwable {
|
||||||
// 如果注解不为0 则使用注解数值
|
// 如果注解不为0 则使用注解数值
|
||||||
long interval = 0;
|
long interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
|
||||||
if (repeatSubmit.interval() > 0) {
|
|
||||||
interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
|
|
||||||
}
|
|
||||||
if (interval < 1000) {
|
if (interval < 1000) {
|
||||||
throw new ServiceException("重复提交间隔时间不能小于'1'秒");
|
throw new ServiceException("重复提交间隔时间不能小于'1'秒");
|
||||||
}
|
}
|
||||||
@ -58,9 +58,7 @@ public class RepeatSubmitAspect {
|
|||||||
submitKey = SecureUtil.md5(submitKey + ":" + nowParams);
|
submitKey = SecureUtil.md5(submitKey + ":" + nowParams);
|
||||||
// 唯一标识(指定key + url + 消息头)
|
// 唯一标识(指定key + url + 消息头)
|
||||||
String cacheRepeatKey = GlobalConstants.REPEAT_SUBMIT_KEY + url + submitKey;
|
String cacheRepeatKey = GlobalConstants.REPEAT_SUBMIT_KEY + url + submitKey;
|
||||||
String key = RedisUtils.getCacheObject(cacheRepeatKey);
|
if (RedisUtils.setObjectIfAbsent(cacheRepeatKey, "", Duration.ofMillis(interval))) {
|
||||||
if (key == null) {
|
|
||||||
RedisUtils.setCacheObject(cacheRepeatKey, "", Duration.ofMillis(interval));
|
|
||||||
KEY_CACHE.set(cacheRepeatKey);
|
KEY_CACHE.set(cacheRepeatKey);
|
||||||
} else {
|
} else {
|
||||||
String message = repeatSubmit.message();
|
String message = repeatSubmit.message();
|
||||||
@ -78,7 +76,7 @@ public class RepeatSubmitAspect {
|
|||||||
*/
|
*/
|
||||||
@AfterReturning(pointcut = "@annotation(repeatSubmit)", returning = "jsonResult")
|
@AfterReturning(pointcut = "@annotation(repeatSubmit)", returning = "jsonResult")
|
||||||
public void doAfterReturning(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Object jsonResult) {
|
public void doAfterReturning(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Object jsonResult) {
|
||||||
if (jsonResult instanceof R r) {
|
if (jsonResult instanceof R<?> r) {
|
||||||
try {
|
try {
|
||||||
// 成功则不删除redis数据 保证在有效时间内无法重复提交
|
// 成功则不删除redis数据 保证在有效时间内无法重复提交
|
||||||
if (r.getCode() == R.SUCCESS) {
|
if (r.getCode() == R.SUCCESS) {
|
||||||
@ -107,19 +105,16 @@ public class RepeatSubmitAspect {
|
|||||||
* 参数拼装
|
* 参数拼装
|
||||||
*/
|
*/
|
||||||
private String argsArrayToString(Object[] paramsArray) {
|
private String argsArrayToString(Object[] paramsArray) {
|
||||||
StringBuilder params = new StringBuilder();
|
StringJoiner params = new StringJoiner(" ");
|
||||||
if (paramsArray != null && paramsArray.length > 0) {
|
if (ArrayUtil.isEmpty(paramsArray)) {
|
||||||
|
return params.toString();
|
||||||
|
}
|
||||||
for (Object o : paramsArray) {
|
for (Object o : paramsArray) {
|
||||||
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
|
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
|
||||||
try {
|
params.add(JsonUtils.toJsonString(o));
|
||||||
params.append(JsonUtils.toJsonString(o)).append(" ");
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return params.toString();
|
||||||
}
|
|
||||||
return params.toString().trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -140,9 +135,8 @@ public class RepeatSubmitAspect {
|
|||||||
}
|
}
|
||||||
} else if (Map.class.isAssignableFrom(clazz)) {
|
} else if (Map.class.isAssignableFrom(clazz)) {
|
||||||
Map map = (Map) o;
|
Map map = (Map) o;
|
||||||
for (Object value : map.entrySet()) {
|
for (Object value : map.values()) {
|
||||||
Map.Entry entry = (Map.Entry) value;
|
return value instanceof MultipartFile;
|
||||||
return entry.getValue() instanceof MultipartFile;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|
||||||
|
@ -22,10 +22,14 @@
|
|||||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- xxl-job-core -->
|
<!--PowerJob-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.xuxueli</groupId>
|
<groupId>tech.powerjob</groupId>
|
||||||
<artifactId>xxl-job-core</artifactId>
|
<artifactId>powerjob-worker-spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.powerjob</groupId>
|
||||||
|
<artifactId>powerjob-official-processors</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
package com.xxl.job.core.glue.impl;
|
|
||||||
|
|
||||||
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
|
|
||||||
import com.xxl.job.core.glue.GlueFactory;
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.core.annotation.AnnotationUtils;
|
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.lang.reflect.Modifier;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author xuxueli 2018-11-01
|
|
||||||
*/
|
|
||||||
public class SpringGlueFactory extends GlueFactory {
|
|
||||||
private static Logger logger = LoggerFactory.getLogger(SpringGlueFactory.class);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* inject action of spring
|
|
||||||
* @param instance
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void injectService(Object instance){
|
|
||||||
if (instance==null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (XxlJobSpringExecutor.getApplicationContext() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Field[] fields = instance.getClass().getDeclaredFields();
|
|
||||||
for (Field field : fields) {
|
|
||||||
if (Modifier.isStatic(field.getModifiers())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object fieldBean = null;
|
|
||||||
// with bean-id, bean could be found by both @Resource and @Autowired, or bean could only be found by @Autowired
|
|
||||||
|
|
||||||
if (AnnotationUtils.getAnnotation(field, Resource.class) != null) {
|
|
||||||
try {
|
|
||||||
Resource resource = AnnotationUtils.getAnnotation(field, Resource.class);
|
|
||||||
if (resource.name()!=null && resource.name().length()>0){
|
|
||||||
fieldBean = XxlJobSpringExecutor.getApplicationContext().getBean(resource.name());
|
|
||||||
} else {
|
|
||||||
fieldBean = XxlJobSpringExecutor.getApplicationContext().getBean(field.getName());
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
}
|
|
||||||
if (fieldBean==null ) {
|
|
||||||
fieldBean = XxlJobSpringExecutor.getApplicationContext().getBean(field.getType());
|
|
||||||
}
|
|
||||||
} else if (AnnotationUtils.getAnnotation(field, Autowired.class) != null) {
|
|
||||||
Qualifier qualifier = AnnotationUtils.getAnnotation(field, Qualifier.class);
|
|
||||||
if (qualifier!=null && qualifier.value()!=null && qualifier.value().length()>0) {
|
|
||||||
fieldBean = XxlJobSpringExecutor.getApplicationContext().getBean(qualifier.value());
|
|
||||||
} else {
|
|
||||||
fieldBean = XxlJobSpringExecutor.getApplicationContext().getBean(field.getType());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fieldBean!=null) {
|
|
||||||
field.setAccessible(true);
|
|
||||||
try {
|
|
||||||
field.set(instance, fieldBean);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
logger.error(e.getMessage(), e);
|
|
||||||
} catch (IllegalAccessException e) {
|
|
||||||
logger.error(e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,21 @@
|
|||||||
|
package org.dromara.common.job.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
import tech.powerjob.worker.PowerJobWorker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动定时任务
|
||||||
|
* @author yhan219
|
||||||
|
* @since 2023/6/2
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@ConditionalOnBean(PowerJobWorker.class)
|
||||||
|
@ConditionalOnProperty(prefix = "powerjob.worker", name = "enabled", havingValue = "true")
|
||||||
|
@EnableScheduling
|
||||||
|
public class PowerJobConfig {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -1,38 +0,0 @@
|
|||||||
package org.dromara.common.job.config;
|
|
||||||
|
|
||||||
import org.dromara.common.job.config.properties.XxlJobProperties;
|
|
||||||
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* xxl-job config
|
|
||||||
*
|
|
||||||
* @author Lion Li
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@AutoConfiguration
|
|
||||||
@EnableConfigurationProperties(XxlJobProperties.class)
|
|
||||||
@ConditionalOnProperty(prefix = "xxl.job", name = "enabled", havingValue = "true")
|
|
||||||
public class XxlJobConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public XxlJobSpringExecutor xxlJobExecutor(XxlJobProperties xxlJobProperties) {
|
|
||||||
log.info(">>>>>>>>>>> xxl-job config init.");
|
|
||||||
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
|
|
||||||
xxlJobSpringExecutor.setAdminAddresses(xxlJobProperties.getAdminAddresses());
|
|
||||||
xxlJobSpringExecutor.setAccessToken(xxlJobProperties.getAccessToken());
|
|
||||||
XxlJobProperties.Executor executor = xxlJobProperties.getExecutor();
|
|
||||||
xxlJobSpringExecutor.setAppname(executor.getAppname());
|
|
||||||
xxlJobSpringExecutor.setAddress(executor.getAddress());
|
|
||||||
xxlJobSpringExecutor.setIp(executor.getIp());
|
|
||||||
xxlJobSpringExecutor.setPort(executor.getPort());
|
|
||||||
xxlJobSpringExecutor.setLogPath(executor.getLogPath());
|
|
||||||
xxlJobSpringExecutor.setLogRetentionDays(executor.getLogRetentionDays());
|
|
||||||
return xxlJobSpringExecutor;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
package org.dromara.common.job.config.properties;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* xxljob配置类
|
|
||||||
*
|
|
||||||
* @author Lion Li
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@ConfigurationProperties(prefix = "xxl.job")
|
|
||||||
public class XxlJobProperties {
|
|
||||||
|
|
||||||
private Boolean enabled;
|
|
||||||
|
|
||||||
private String adminAddresses;
|
|
||||||
|
|
||||||
private String accessToken;
|
|
||||||
|
|
||||||
private Executor executor;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@NoArgsConstructor
|
|
||||||
public static class Executor {
|
|
||||||
|
|
||||||
private String appname;
|
|
||||||
|
|
||||||
private String address;
|
|
||||||
|
|
||||||
private String ip;
|
|
||||||
|
|
||||||
private int port;
|
|
||||||
|
|
||||||
private String logPath;
|
|
||||||
|
|
||||||
private int logRetentionDays;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
org.dromara.common.job.config.XxlJobConfig
|
|
@ -2,6 +2,7 @@ package org.dromara.common.log.aspect;
|
|||||||
|
|
||||||
import cn.hutool.core.lang.Dict;
|
import cn.hutool.core.lang.Dict;
|
||||||
import cn.hutool.core.map.MapUtil;
|
import cn.hutool.core.map.MapUtil;
|
||||||
|
import cn.hutool.core.util.ArrayUtil;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import com.alibaba.ttl.TransmittableThreadLocal;
|
import com.alibaba.ttl.TransmittableThreadLocal;
|
||||||
import org.dromara.common.core.utils.ServletUtils;
|
import org.dromara.common.core.utils.ServletUtils;
|
||||||
@ -28,6 +29,7 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.StringJoiner;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 操作日志记录处理
|
* 操作日志记录处理
|
||||||
@ -170,11 +172,12 @@ public class LogAspect {
|
|||||||
* 参数拼装
|
* 参数拼装
|
||||||
*/
|
*/
|
||||||
private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
|
private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
|
||||||
StringBuilder params = new StringBuilder();
|
StringJoiner params = new StringJoiner(" ");
|
||||||
if (paramsArray != null && paramsArray.length > 0) {
|
if (ArrayUtil.isEmpty(paramsArray)) {
|
||||||
|
return params.toString();
|
||||||
|
}
|
||||||
for (Object o : paramsArray) {
|
for (Object o : paramsArray) {
|
||||||
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
|
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
|
||||||
try {
|
|
||||||
String str = JsonUtils.toJsonString(o);
|
String str = JsonUtils.toJsonString(o);
|
||||||
Dict dict = JsonUtils.parseMap(str);
|
Dict dict = JsonUtils.parseMap(str);
|
||||||
if (MapUtil.isNotEmpty(dict)) {
|
if (MapUtil.isNotEmpty(dict)) {
|
||||||
@ -182,14 +185,10 @@ public class LogAspect {
|
|||||||
MapUtil.removeAny(dict, excludeParamNames);
|
MapUtil.removeAny(dict, excludeParamNames);
|
||||||
str = JsonUtils.toJsonString(dict);
|
str = JsonUtils.toJsonString(dict);
|
||||||
}
|
}
|
||||||
params.append(str).append(" ");
|
params.add(str);
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return params.toString();
|
||||||
}
|
|
||||||
return params.toString().trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -210,9 +209,8 @@ public class LogAspect {
|
|||||||
}
|
}
|
||||||
} else if (Map.class.isAssignableFrom(clazz)) {
|
} else if (Map.class.isAssignableFrom(clazz)) {
|
||||||
Map map = (Map) o;
|
Map map = (Map) o;
|
||||||
for (Object value : map.entrySet()) {
|
for (Object value : map.values()) {
|
||||||
Map.Entry entry = (Map.Entry) value;
|
return value instanceof MultipartFile;
|
||||||
return entry.getValue() instanceof MultipartFile;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
<!-- dynamic-datasource 多数据源-->
|
<!-- dynamic-datasource 多数据源-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.baomidou</groupId>
|
<groupId>com.baomidou</groupId>
|
||||||
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
|
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2018 organization baomidou
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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.baomidou.dynamic.datasource.processor.jakarta;
|
|
||||||
|
|
||||||
import com.baomidou.dynamic.datasource.processor.DsProcessor;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import org.aopalliance.intercept.MethodInvocation;
|
|
||||||
import org.springframework.web.context.request.RequestContextHolder;
|
|
||||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author TaoYu
|
|
||||||
* @since 3.6.0
|
|
||||||
*/
|
|
||||||
public class DsJakartaHeaderProcessor extends DsProcessor {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* header prefix
|
|
||||||
*/
|
|
||||||
private static final String HEADER_PREFIX = "#header";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean matches(String key) {
|
|
||||||
return key.startsWith(HEADER_PREFIX);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String doDetermineDatasource(MethodInvocation invocation, String key) {
|
|
||||||
HttpServletRequest request = (HttpServletRequest) ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
|
||||||
return request.getHeader(key.substring(8));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright © 2018 organization baomidou
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* http://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.baomidou.dynamic.datasource.processor.jakarta;
|
|
||||||
|
|
||||||
import com.baomidou.dynamic.datasource.processor.DsProcessor;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import org.aopalliance.intercept.MethodInvocation;
|
|
||||||
import org.springframework.web.context.request.RequestContextHolder;
|
|
||||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author TaoYu
|
|
||||||
* @since 3.6.0
|
|
||||||
*/
|
|
||||||
public class DsJakartaSessionProcessor extends DsProcessor {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* session开头
|
|
||||||
*/
|
|
||||||
private static final String SESSION_PREFIX = "#session";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean matches(String key) {
|
|
||||||
return key.startsWith(SESSION_PREFIX);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String doDetermineDatasource(MethodInvocation invocation, String key) {
|
|
||||||
HttpServletRequest request = (HttpServletRequest) ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
|
||||||
return request.getSession().getAttribute(key.substring(9)).toString();
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,11 +7,13 @@ import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
|
|||||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||||
|
import org.dromara.common.core.factory.YmlPropertySourceFactory;
|
||||||
import org.dromara.common.mybatis.handler.InjectionMetaObjectHandler;
|
import org.dromara.common.mybatis.handler.InjectionMetaObjectHandler;
|
||||||
import org.dromara.common.mybatis.interceptor.PlusDataPermissionInterceptor;
|
import org.dromara.common.mybatis.interceptor.PlusDataPermissionInterceptor;
|
||||||
import org.mybatis.spring.annotation.MapperScan;
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.PropertySource;
|
||||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -22,6 +24,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
|
|||||||
@EnableTransactionManagement(proxyTargetClass = true)
|
@EnableTransactionManagement(proxyTargetClass = true)
|
||||||
@AutoConfiguration
|
@AutoConfiguration
|
||||||
@MapperScan("${mybatis-plus.mapperPackage}")
|
@MapperScan("${mybatis-plus.mapperPackage}")
|
||||||
|
@PropertySource(value = "classpath:common-mybatis.yml", factory = YmlPropertySourceFactory.class)
|
||||||
public class MybatisPlusConfig {
|
public class MybatisPlusConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
@ -12,6 +12,9 @@ import javax.sql.DataSource;
|
|||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.DatabaseMetaData;
|
import java.sql.DatabaseMetaData;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据库助手
|
* 数据库助手
|
||||||
@ -69,4 +72,11 @@ public class DataBaseHelper {
|
|||||||
// find_in_set(100 , '0,100,101')
|
// find_in_set(100 , '0,100,101')
|
||||||
return "find_in_set('%s' , %s) <> 0".formatted(var, var2);
|
return "find_in_set('%s' , %s) <> 0".formatted(var, var2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前加载的数据库名
|
||||||
|
*/
|
||||||
|
public static List<String> getDataSourceNameList() {
|
||||||
|
return new ArrayList<>(DS.getDataSources().keySet());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
# 内置配置 不允许修改 如需修改请在 nacos 上写相同配置覆盖
|
||||||
|
# MyBatisPlus配置
|
||||||
|
# https://baomidou.com/config/
|
||||||
|
mybatis-plus:
|
||||||
|
# 启动时是否检查 MyBatis XML 文件的存在,默认不检查
|
||||||
|
checkConfigLocation: false
|
||||||
|
configuration:
|
||||||
|
# 自动驼峰命名规则(camel case)映射
|
||||||
|
mapUnderscoreToCamelCase: true
|
||||||
|
# MyBatis 自动映射策略
|
||||||
|
# NONE:不启用 PARTIAL:只对非嵌套 resultMap 自动映射 FULL:对所有 resultMap 自动映射
|
||||||
|
autoMappingBehavior: FULL
|
||||||
|
# MyBatis 自动映射时未知列或未知属性处理策
|
||||||
|
# NONE:不做处理 WARNING:打印相关警告 FAILING:抛出异常和详细信息
|
||||||
|
autoMappingUnknownColumnBehavior: NONE
|
||||||
|
# 更详细的日志输出 会有性能损耗 org.apache.ibatis.logging.stdout.StdOutImpl
|
||||||
|
# 关闭日志记录 (可单纯使用 p6spy 分析) org.apache.ibatis.logging.nologging.NoLoggingImpl
|
||||||
|
# 默认日志输出 org.apache.ibatis.logging.slf4j.Slf4jImpl
|
||||||
|
logImpl: org.apache.ibatis.logging.nologging.NoLoggingImpl
|
||||||
|
global-config:
|
||||||
|
# 是否打印 Logo banner
|
||||||
|
banner: true
|
||||||
|
dbConfig:
|
||||||
|
# 主键类型
|
||||||
|
# AUTO 自增 NONE 空 INPUT 用户输入 ASSIGN_ID 雪花 ASSIGN_UUID 唯一 UUID
|
||||||
|
idType: ASSIGN_ID
|
||||||
|
# 逻辑已删除值(框架表均使用此值 禁止随意修改)
|
||||||
|
logicDeleteValue: 2
|
||||||
|
# 逻辑未删除值
|
||||||
|
logicNotDeleteValue: 0
|
||||||
|
insertStrategy: NOT_NULL
|
||||||
|
updateStrategy: NOT_NULL
|
||||||
|
whereStrategy: NOT_NULL
|
@ -24,6 +24,7 @@ import org.dromara.common.oss.exception.OssException;
|
|||||||
import org.dromara.common.oss.properties.OssProperties;
|
import org.dromara.common.oss.properties.OssProperties;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.File;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@ -115,6 +116,18 @@ public class OssClient {
|
|||||||
return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
|
return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UploadResult upload(File file, String path) {
|
||||||
|
try {
|
||||||
|
PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, file);
|
||||||
|
// 设置上传对象的 Acl 为公共读
|
||||||
|
putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
|
||||||
|
client.putObject(putObjectRequest);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
|
||||||
|
}
|
||||||
|
return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
|
||||||
|
}
|
||||||
|
|
||||||
public void delete(String path) {
|
public void delete(String path) {
|
||||||
path = path.replace(getUrl() + "/", "");
|
path = path.replace(getUrl() + "/", "");
|
||||||
try {
|
try {
|
||||||
@ -132,6 +145,10 @@ public class OssClient {
|
|||||||
return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
|
return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UploadResult uploadSuffix(File file, String suffix) {
|
||||||
|
return upload(file, getPath(properties.getPrefix(), suffix));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取文件元数据
|
* 获取文件元数据
|
||||||
*
|
*
|
||||||
|
@ -51,13 +51,13 @@ public class OssFactory {
|
|||||||
if (client == null) {
|
if (client == null) {
|
||||||
CLIENT_CACHE.put(key, new OssClient(configKey, properties));
|
CLIENT_CACHE.put(key, new OssClient(configKey, properties));
|
||||||
log.info("创建OSS实例 key => {}", configKey);
|
log.info("创建OSS实例 key => {}", configKey);
|
||||||
return CLIENT_CACHE.get(configKey);
|
return CLIENT_CACHE.get(key);
|
||||||
}
|
}
|
||||||
// 配置不相同则重新构建
|
// 配置不相同则重新构建
|
||||||
if (!client.checkPropertiesSame(properties)) {
|
if (!client.checkPropertiesSame(properties)) {
|
||||||
CLIENT_CACHE.put(key, new OssClient(configKey, properties));
|
CLIENT_CACHE.put(key, new OssClient(configKey, properties));
|
||||||
log.info("重载OSS实例 key => {}", configKey);
|
log.info("重载OSS实例 key => {}", configKey);
|
||||||
return CLIENT_CACHE.get(configKey);
|
return CLIENT_CACHE.get(key);
|
||||||
}
|
}
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
@ -118,6 +118,10 @@ public class PlusSpringCacheManager implements CacheManager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Cache getCache(String name) {
|
public Cache getCache(String name) {
|
||||||
|
// 重写 cacheName 支持多参数
|
||||||
|
String[] array = StringUtils.delimitedListToStringArray(name, "#");
|
||||||
|
name = array[0];
|
||||||
|
|
||||||
Cache cache = instanceMap.get(name);
|
Cache cache = instanceMap.get(name);
|
||||||
if (cache != null) {
|
if (cache != null) {
|
||||||
return cache;
|
return cache;
|
||||||
@ -132,9 +136,6 @@ public class PlusSpringCacheManager implements CacheManager {
|
|||||||
configMap.put(name, config);
|
configMap.put(name, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重写 cacheName 支持多参数
|
|
||||||
String[] array = StringUtils.delimitedListToStringArray(name, "#");
|
|
||||||
name = array[0];
|
|
||||||
if (array.length > 1) {
|
if (array.length > 1) {
|
||||||
config.setTTL(DurationStyle.detectAndParse(array[1]).toMillis());
|
config.setTTL(DurationStyle.detectAndParse(array[1]).toMillis());
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package org.dromara.common.redis.utils;
|
package org.dromara.common.redis.utils;
|
||||||
|
|
||||||
import org.dromara.common.core.utils.SpringUtils;
|
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.dromara.common.core.utils.SpringUtils;
|
||||||
import org.redisson.api.*;
|
import org.redisson.api.*;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
@ -129,6 +129,18 @@ public class RedisUtils {
|
|||||||
batch.execute();
|
batch.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果不存在则设置 并返回 true 如果存在则返回 false
|
||||||
|
*
|
||||||
|
* @param key 缓存的键值
|
||||||
|
* @param value 缓存的值
|
||||||
|
* @return set成功或失败
|
||||||
|
*/
|
||||||
|
public static <T> boolean setObjectIfAbsent(final String key, final T value, final Duration duration) {
|
||||||
|
RBucket<T> bucket = CLIENT.getBucket(key);
|
||||||
|
return bucket.setIfAbsent(value, duration);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册对象监听器
|
* 注册对象监听器
|
||||||
* <p>
|
* <p>
|
||||||
@ -374,6 +386,21 @@ public class RedisUtils {
|
|||||||
return rMap.remove(hKey);
|
return rMap.remove(hKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除Hash中的数据
|
||||||
|
*
|
||||||
|
* @param key Redis键
|
||||||
|
* @param hKeys Hash键
|
||||||
|
*/
|
||||||
|
public static <T> void delMultiCacheMapValue(final String key, final Set<String> hKeys) {
|
||||||
|
RBatch batch = CLIENT.createBatch();
|
||||||
|
RMapAsync<String, T> rMap = batch.getMap(key);
|
||||||
|
for (String hKey : hKeys) {
|
||||||
|
rMap.removeAsync(hKey);
|
||||||
|
}
|
||||||
|
batch.execute();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取多个Hash中的数据
|
* 获取多个Hash中的数据
|
||||||
*
|
*
|
||||||
|
@ -4,10 +4,12 @@ import cn.dev33.satoken.dao.SaTokenDao;
|
|||||||
import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
|
import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
|
||||||
import cn.dev33.satoken.stp.StpInterface;
|
import cn.dev33.satoken.stp.StpInterface;
|
||||||
import cn.dev33.satoken.stp.StpLogic;
|
import cn.dev33.satoken.stp.StpLogic;
|
||||||
|
import org.dromara.common.core.factory.YmlPropertySourceFactory;
|
||||||
import org.dromara.common.satoken.core.dao.PlusSaTokenDao;
|
import org.dromara.common.satoken.core.dao.PlusSaTokenDao;
|
||||||
import org.dromara.common.satoken.core.service.SaPermissionImpl;
|
import org.dromara.common.satoken.core.service.SaPermissionImpl;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.PropertySource;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -16,6 +18,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|||||||
* @author Lion Li
|
* @author Lion Li
|
||||||
*/
|
*/
|
||||||
@AutoConfiguration
|
@AutoConfiguration
|
||||||
|
@PropertySource(value = "classpath:common-satoken.yml", factory = YmlPropertySourceFactory.class)
|
||||||
public class SaTokenConfig implements WebMvcConfigurer {
|
public class SaTokenConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
@ -2,17 +2,17 @@ package org.dromara.common.satoken.utils;
|
|||||||
|
|
||||||
import cn.dev33.satoken.context.SaHolder;
|
import cn.dev33.satoken.context.SaHolder;
|
||||||
import cn.dev33.satoken.context.model.SaStorage;
|
import cn.dev33.satoken.context.model.SaStorage;
|
||||||
|
import cn.dev33.satoken.session.SaSession;
|
||||||
import cn.dev33.satoken.stp.SaLoginModel;
|
import cn.dev33.satoken.stp.SaLoginModel;
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import cn.hutool.core.convert.Convert;
|
import cn.hutool.core.convert.Convert;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
import org.dromara.common.core.constant.TenantConstants;
|
import org.dromara.common.core.constant.TenantConstants;
|
||||||
import org.dromara.common.core.constant.UserConstants;
|
import org.dromara.common.core.constant.UserConstants;
|
||||||
import org.dromara.common.core.domain.model.LoginUser;
|
import org.dromara.common.core.domain.model.LoginUser;
|
||||||
import org.dromara.common.core.enums.DeviceType;
|
|
||||||
import org.dromara.common.core.enums.UserType;
|
import org.dromara.common.core.enums.UserType;
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@ -34,31 +34,21 @@ public class LoginHelper {
|
|||||||
public static final String LOGIN_USER_KEY = "loginUser";
|
public static final String LOGIN_USER_KEY = "loginUser";
|
||||||
public static final String TENANT_KEY = "tenantId";
|
public static final String TENANT_KEY = "tenantId";
|
||||||
public static final String USER_KEY = "userId";
|
public static final String USER_KEY = "userId";
|
||||||
|
public static final String CLIENT_KEY = "clientid";
|
||||||
/**
|
|
||||||
* 登录系统
|
|
||||||
*
|
|
||||||
* @param loginUser 登录用户信息
|
|
||||||
*/
|
|
||||||
public static void login(LoginUser loginUser) {
|
|
||||||
loginByDevice(loginUser, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录系统 基于 设备类型
|
* 登录系统 基于 设备类型
|
||||||
* 针对相同用户体系不同设备
|
* 针对相同用户体系不同设备
|
||||||
*
|
*
|
||||||
* @param loginUser 登录用户信息
|
* @param loginUser 登录用户信息
|
||||||
|
* @param model 配置参数
|
||||||
*/
|
*/
|
||||||
public static void loginByDevice(LoginUser loginUser, DeviceType deviceType) {
|
public static void login(LoginUser loginUser, SaLoginModel model) {
|
||||||
SaStorage storage = SaHolder.getStorage();
|
SaStorage storage = SaHolder.getStorage();
|
||||||
storage.set(LOGIN_USER_KEY, loginUser);
|
storage.set(LOGIN_USER_KEY, loginUser);
|
||||||
storage.set(TENANT_KEY, loginUser.getTenantId());
|
storage.set(TENANT_KEY, loginUser.getTenantId());
|
||||||
storage.set(USER_KEY, loginUser.getUserId());
|
storage.set(USER_KEY, loginUser.getUserId());
|
||||||
SaLoginModel model = new SaLoginModel();
|
model = ObjectUtil.defaultIfNull(model, new SaLoginModel());
|
||||||
if (ObjectUtil.isNotNull(deviceType)) {
|
|
||||||
model.setDevice(deviceType.getDevice());
|
|
||||||
}
|
|
||||||
StpUtil.login(loginUser.getLoginId(),
|
StpUtil.login(loginUser.getLoginId(),
|
||||||
model.setExtra(TENANT_KEY, loginUser.getTenantId())
|
model.setExtra(TENANT_KEY, loginUser.getTenantId())
|
||||||
.setExtra(USER_KEY, loginUser.getUserId()));
|
.setExtra(USER_KEY, loginUser.getUserId()));
|
||||||
@ -73,7 +63,11 @@ public class LoginHelper {
|
|||||||
if (loginUser != null) {
|
if (loginUser != null) {
|
||||||
return loginUser;
|
return loginUser;
|
||||||
}
|
}
|
||||||
loginUser = (LoginUser) StpUtil.getTokenSession().get(LOGIN_USER_KEY);
|
SaSession session = StpUtil.getTokenSession();
|
||||||
|
if (ObjectUtil.isNull(session)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
loginUser = (LoginUser) session.get(LOGIN_USER_KEY);
|
||||||
SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
|
SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
|
||||||
return loginUser;
|
return loginUser;
|
||||||
}
|
}
|
||||||
@ -82,7 +76,11 @@ public class LoginHelper {
|
|||||||
* 获取用户基于token
|
* 获取用户基于token
|
||||||
*/
|
*/
|
||||||
public static LoginUser getLoginUser(String token) {
|
public static LoginUser getLoginUser(String token) {
|
||||||
return (LoginUser) StpUtil.getTokenSessionByToken(token).get(LOGIN_USER_KEY);
|
SaSession session = StpUtil.getTokenSessionByToken(token);
|
||||||
|
if (ObjectUtil.isNull(session)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (LoginUser) session.get(LOGIN_USER_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -137,8 +135,8 @@ public class LoginHelper {
|
|||||||
* 获取用户类型
|
* 获取用户类型
|
||||||
*/
|
*/
|
||||||
public static UserType getUserType() {
|
public static UserType getUserType() {
|
||||||
String loginId = StpUtil.getLoginIdAsString();
|
String loginType = StpUtil.getLoginIdAsString();
|
||||||
return UserType.getUserType(loginId);
|
return UserType.getUserType(loginType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
# 内置配置 不允许修改 如需修改请在 nacos 上写相同配置覆盖
|
||||||
|
# Sa-Token配置
|
||||||
|
sa-token:
|
||||||
|
# 允许动态设置 token 有效期
|
||||||
|
dynamic-active-timeout: true
|
||||||
|
# 允许从 请求参数 读取 token
|
||||||
|
is-read-body: true
|
||||||
|
# 允许从 header 读取 token
|
||||||
|
is-read-header: true
|
||||||
|
# 关闭 cookie 鉴权 从根源杜绝 csrf 漏洞风险
|
||||||
|
is-read-cookie: false
|
||||||
|
# token前缀
|
||||||
|
token-prefix: "Bearer"
|
@ -1,9 +1,13 @@
|
|||||||
package org.dromara.common.security.config;
|
package org.dromara.common.security.config;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.exception.NotLoginException;
|
||||||
import cn.dev33.satoken.interceptor.SaInterceptor;
|
import cn.dev33.satoken.interceptor.SaInterceptor;
|
||||||
import cn.dev33.satoken.router.SaRouter;
|
import cn.dev33.satoken.router.SaRouter;
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import org.dromara.common.core.utils.ServletUtils;
|
||||||
import org.dromara.common.core.utils.SpringUtils;
|
import org.dromara.common.core.utils.SpringUtils;
|
||||||
|
import org.dromara.common.core.utils.StringUtils;
|
||||||
|
import org.dromara.common.satoken.utils.LoginHelper;
|
||||||
import org.dromara.common.security.config.properties.SecurityProperties;
|
import org.dromara.common.security.config.properties.SecurityProperties;
|
||||||
import org.dromara.common.security.handler.AllUrlHandler;
|
import org.dromara.common.security.handler.AllUrlHandler;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -44,6 +48,18 @@ public class SecurityConfig implements WebMvcConfigurer {
|
|||||||
// 检查是否登录 是否有token
|
// 检查是否登录 是否有token
|
||||||
StpUtil.checkLogin();
|
StpUtil.checkLogin();
|
||||||
|
|
||||||
|
// 检查 header 里的 clientId 与 token 里的是否一致
|
||||||
|
String headerCid = ServletUtils.getRequest().getHeader(LoginHelper.CLIENT_KEY);
|
||||||
|
String clientId = StpUtil.getExtra(LoginHelper.CLIENT_KEY).toString();
|
||||||
|
if (!StringUtils.equals(headerCid, clientId)) {
|
||||||
|
// token 无效
|
||||||
|
throw NotLoginException.newInstance(
|
||||||
|
StpUtil.getLoginType(),
|
||||||
|
NotLoginException.INVALID_TOKEN,
|
||||||
|
NotLoginException.NOT_TOKEN_MESSAGE,
|
||||||
|
StpUtil.getTokenValue());
|
||||||
|
}
|
||||||
|
|
||||||
// 有效率影响 用于临时测试
|
// 有效率影响 用于临时测试
|
||||||
// if (log.isDebugEnabled()) {
|
// if (log.isDebugEnabled()) {
|
||||||
// log.debug("剩余有效时间: {}", StpUtil.getTokenTimeout());
|
// log.debug("剩余有效时间: {}", StpUtil.getTokenTimeout());
|
||||||
|
@ -5,21 +5,22 @@ import cn.dev33.satoken.exception.NotPermissionException;
|
|||||||
import cn.dev33.satoken.exception.NotRoleException;
|
import cn.dev33.satoken.exception.NotRoleException;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import cn.hutool.http.HttpStatus;
|
import cn.hutool.http.HttpStatus;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.validation.ConstraintViolation;
|
||||||
|
import jakarta.validation.ConstraintViolationException;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.dromara.common.core.domain.R;
|
import org.dromara.common.core.domain.R;
|
||||||
import org.dromara.common.core.exception.DemoModeException;
|
import org.dromara.common.core.exception.DemoModeException;
|
||||||
import org.dromara.common.core.exception.ServiceException;
|
import org.dromara.common.core.exception.ServiceException;
|
||||||
import org.dromara.common.core.utils.StreamUtils;
|
import org.dromara.common.core.utils.StreamUtils;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
||||||
import org.springframework.validation.BindException;
|
import org.springframework.validation.BindException;
|
||||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
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.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.validation.ConstraintViolation;
|
|
||||||
import jakarta.validation.ConstraintViolationException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全局异常处理器
|
* 全局异常处理器
|
||||||
@ -81,6 +82,26 @@ public class GlobalExceptionHandler {
|
|||||||
return ObjectUtil.isNotNull(code) ? R.fail(code, e.getMessage()) : R.fail(e.getMessage());
|
return ObjectUtil.isNotNull(code) ? R.fail(code, e.getMessage()) : R.fail(e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求路径中缺少必需的路径变量
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(MissingPathVariableException.class)
|
||||||
|
public R<Void> handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request) {
|
||||||
|
String requestURI = request.getRequestURI();
|
||||||
|
log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI, e);
|
||||||
|
return R.fail(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求参数类型不匹配
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||||
|
public R<Void> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
|
||||||
|
String requestURI = request.getRequestURI();
|
||||||
|
log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e);
|
||||||
|
return R.fail(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), e.getValue()));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 拦截未知的运行时异常
|
* 拦截未知的运行时异常
|
||||||
*/
|
*/
|
||||||
|
@ -16,22 +16,19 @@
|
|||||||
</description>
|
</description>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
|
||||||
<groupId>org.dromara</groupId>
|
|
||||||
<artifactId>ruoyi-common-json</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.aliyun</groupId>
|
<groupId>org.dromara.sms4j</groupId>
|
||||||
<artifactId>dysmsapi20170525</artifactId>
|
<artifactId>sms4j-spring-boot-starter</artifactId>
|
||||||
<optional>true</optional>
|
<exclusions>
|
||||||
|
<!-- 排除京东短信内存在的fastjson等待作者后续修复 -->
|
||||||
|
<exclusion>
|
||||||
|
<groupId>com.alibaba</groupId>
|
||||||
|
<artifactId>fastjson</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.tencentcloudapi</groupId>
|
|
||||||
<artifactId>tencentcloud-sdk-java-sms</artifactId>
|
|
||||||
<optional>true</optional>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
@ -1,15 +1,6 @@
|
|||||||
package org.dromara.common.sms.config;
|
package org.dromara.common.sms.config;
|
||||||
|
|
||||||
import org.dromara.common.sms.config.properties.SmsProperties;
|
|
||||||
import org.dromara.common.sms.core.AliyunSmsTemplate;
|
|
||||||
import org.dromara.common.sms.core.SmsTemplate;
|
|
||||||
import org.dromara.common.sms.core.TencentSmsTemplate;
|
|
||||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 短信配置类
|
* 短信配置类
|
||||||
@ -18,31 +9,7 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
* @version 4.2.0
|
* @version 4.2.0
|
||||||
*/
|
*/
|
||||||
@AutoConfiguration
|
@AutoConfiguration
|
||||||
@EnableConfigurationProperties(SmsProperties.class)
|
//@EnableConfigurationProperties(SmsProperties.class)
|
||||||
public class SmsConfig {
|
public class SmsConfig {
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@ConditionalOnProperty(value = "sms.enabled", havingValue = "true")
|
|
||||||
@ConditionalOnClass(com.aliyun.dysmsapi20170525.Client.class)
|
|
||||||
static class AliyunSmsConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SmsTemplate aliyunSmsTemplate(SmsProperties smsProperties) {
|
|
||||||
return new AliyunSmsTemplate(smsProperties);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@ConditionalOnProperty(value = "sms.enabled", havingValue = "true")
|
|
||||||
@ConditionalOnClass(com.tencentcloudapi.sms.v20190711.SmsClient.class)
|
|
||||||
static class TencentSmsConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SmsTemplate tencentSmsTemplate(SmsProperties smsProperties) {
|
|
||||||
return new TencentSmsTemplate(smsProperties);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,45 +1,19 @@
|
|||||||
package org.dromara.common.sms.config.properties;
|
//package org.dromara.common.sms.config.properties;
|
||||||
|
//
|
||||||
import lombok.Data;
|
//import lombok.Data;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
//import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
//
|
||||||
/**
|
///**
|
||||||
* SMS短信 配置属性
|
// * SMS短信 配置属性
|
||||||
*
|
// *
|
||||||
* @author Lion Li
|
// * @author Lion Li
|
||||||
* @version 4.2.0
|
// * @version 4.2.0
|
||||||
*/
|
// */
|
||||||
@Data
|
//@Data
|
||||||
@ConfigurationProperties(prefix = "sms")
|
//@ConfigurationProperties(prefix = "sms")
|
||||||
public class SmsProperties {
|
//public class SmsProperties {
|
||||||
|
//
|
||||||
private Boolean enabled;
|
// private Boolean enabled;
|
||||||
|
//
|
||||||
/**
|
//
|
||||||
* 配置节点
|
//}
|
||||||
* 阿里云 dysmsapi.aliyuncs.com
|
|
||||||
* 腾讯云 sms.tencentcloudapi.com
|
|
||||||
*/
|
|
||||||
private String endpoint;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* key
|
|
||||||
*/
|
|
||||||
private String accessKeyId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 密匙
|
|
||||||
*/
|
|
||||||
private String accessKeySecret;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 短信签名
|
|
||||||
*/
|
|
||||||
private String signName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 短信应用ID (腾讯专属)
|
|
||||||
*/
|
|
||||||
private String sdkAppId;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -1,66 +0,0 @@
|
|||||||
package org.dromara.common.sms.core;
|
|
||||||
|
|
||||||
import com.aliyun.dysmsapi20170525.Client;
|
|
||||||
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
|
|
||||||
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
|
|
||||||
import com.aliyun.teaopenapi.models.Config;
|
|
||||||
import org.dromara.common.core.utils.StringUtils;
|
|
||||||
import org.dromara.common.json.utils.JsonUtils;
|
|
||||||
import org.dromara.common.sms.config.properties.SmsProperties;
|
|
||||||
import org.dromara.common.sms.entity.SmsResult;
|
|
||||||
import org.dromara.common.sms.exception.SmsException;
|
|
||||||
import lombok.SneakyThrows;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Aliyun 短信模板
|
|
||||||
*
|
|
||||||
* @author Lion Li
|
|
||||||
* @version 4.2.0
|
|
||||||
*/
|
|
||||||
public class AliyunSmsTemplate implements SmsTemplate {
|
|
||||||
|
|
||||||
private SmsProperties properties;
|
|
||||||
|
|
||||||
private Client client;
|
|
||||||
|
|
||||||
@SneakyThrows(Exception.class)
|
|
||||||
public AliyunSmsTemplate(SmsProperties smsProperties) {
|
|
||||||
this.properties = smsProperties;
|
|
||||||
Config config = new Config()
|
|
||||||
// 您的AccessKey ID
|
|
||||||
.setAccessKeyId(smsProperties.getAccessKeyId())
|
|
||||||
// 您的AccessKey Secret
|
|
||||||
.setAccessKeySecret(smsProperties.getAccessKeySecret())
|
|
||||||
// 访问的域名
|
|
||||||
.setEndpoint(smsProperties.getEndpoint());
|
|
||||||
this.client = new Client(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SmsResult send(String phones, String templateId, Map<String, String> param) {
|
|
||||||
if (StringUtils.isBlank(phones)) {
|
|
||||||
throw new SmsException("手机号不能为空");
|
|
||||||
}
|
|
||||||
if (StringUtils.isBlank(templateId)) {
|
|
||||||
throw new SmsException("模板ID不能为空");
|
|
||||||
}
|
|
||||||
SendSmsRequest req = new SendSmsRequest()
|
|
||||||
.setPhoneNumbers(phones)
|
|
||||||
.setSignName(properties.getSignName())
|
|
||||||
.setTemplateCode(templateId)
|
|
||||||
.setTemplateParam(JsonUtils.toJsonString(param));
|
|
||||||
try {
|
|
||||||
SendSmsResponse resp = client.sendSms(req);
|
|
||||||
return SmsResult.builder()
|
|
||||||
.isSuccess("OK".equals(resp.getBody().getCode()))
|
|
||||||
.message(resp.getBody().getMessage())
|
|
||||||
.response(JsonUtils.toJsonString(resp))
|
|
||||||
.build();
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new SmsException(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
package org.dromara.common.sms.core;
|
|
||||||
|
|
||||||
import org.dromara.common.sms.entity.SmsResult;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 短信模板
|
|
||||||
*
|
|
||||||
* @author Lion Li
|
|
||||||
* @version 4.2.0
|
|
||||||
*/
|
|
||||||
public interface SmsTemplate {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送短信
|
|
||||||
*
|
|
||||||
* @param phones 电话号(多个逗号分割)
|
|
||||||
* @param templateId 模板id
|
|
||||||
* @param param 模板对应参数
|
|
||||||
* 阿里 需使用 模板变量名称对应内容 例如: code=1234
|
|
||||||
* 腾讯 需使用 模板变量顺序对应内容 例如: 1=1234, 1为模板内第一个参数
|
|
||||||
*/
|
|
||||||
SmsResult send(String phones, String templateId, Map<String, String> param);
|
|
||||||
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
package org.dromara.common.sms.core;
|
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollUtil;
|
|
||||||
import cn.hutool.core.util.ArrayUtil;
|
|
||||||
import org.dromara.common.core.utils.StringUtils;
|
|
||||||
import org.dromara.common.json.utils.JsonUtils;
|
|
||||||
import org.dromara.common.sms.config.properties.SmsProperties;
|
|
||||||
import org.dromara.common.sms.entity.SmsResult;
|
|
||||||
import org.dromara.common.sms.exception.SmsException;
|
|
||||||
import com.tencentcloudapi.common.Credential;
|
|
||||||
import com.tencentcloudapi.common.profile.ClientProfile;
|
|
||||||
import com.tencentcloudapi.common.profile.HttpProfile;
|
|
||||||
import com.tencentcloudapi.sms.v20190711.SmsClient;
|
|
||||||
import com.tencentcloudapi.sms.v20190711.models.SendSmsRequest;
|
|
||||||
import com.tencentcloudapi.sms.v20190711.models.SendSmsResponse;
|
|
||||||
import com.tencentcloudapi.sms.v20190711.models.SendStatus;
|
|
||||||
import lombok.SneakyThrows;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tencent 短信模板
|
|
||||||
*
|
|
||||||
* @author Lion Li
|
|
||||||
* @version 4.2.0
|
|
||||||
*/
|
|
||||||
public class TencentSmsTemplate implements SmsTemplate {
|
|
||||||
|
|
||||||
private SmsProperties properties;
|
|
||||||
|
|
||||||
private SmsClient client;
|
|
||||||
|
|
||||||
@SneakyThrows(Exception.class)
|
|
||||||
public TencentSmsTemplate(SmsProperties smsProperties) {
|
|
||||||
this.properties = smsProperties;
|
|
||||||
Credential credential = new Credential(smsProperties.getAccessKeyId(), smsProperties.getAccessKeySecret());
|
|
||||||
HttpProfile httpProfile = new HttpProfile();
|
|
||||||
httpProfile.setEndpoint(smsProperties.getEndpoint());
|
|
||||||
ClientProfile clientProfile = new ClientProfile();
|
|
||||||
clientProfile.setHttpProfile(httpProfile);
|
|
||||||
this.client = new SmsClient(credential, "", clientProfile);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SmsResult send(String phones, String templateId, Map<String, String> param) {
|
|
||||||
if (StringUtils.isBlank(phones)) {
|
|
||||||
throw new SmsException("手机号不能为空");
|
|
||||||
}
|
|
||||||
if (StringUtils.isBlank(templateId)) {
|
|
||||||
throw new SmsException("模板ID不能为空");
|
|
||||||
}
|
|
||||||
SendSmsRequest req = new SendSmsRequest();
|
|
||||||
Set<String> set = Arrays.stream(phones.split(StringUtils.SEPARATOR)).map(p -> "+86" + p).collect(Collectors.toSet());
|
|
||||||
req.setPhoneNumberSet(ArrayUtil.toArray(set, String.class));
|
|
||||||
if (CollUtil.isNotEmpty(param)) {
|
|
||||||
req.setTemplateParamSet(ArrayUtil.toArray(param.values(), String.class));
|
|
||||||
}
|
|
||||||
req.setTemplateID(templateId);
|
|
||||||
req.setSign(properties.getSignName());
|
|
||||||
req.setSmsSdkAppid(properties.getSdkAppId());
|
|
||||||
try {
|
|
||||||
SendSmsResponse resp = client.SendSms(req);
|
|
||||||
SmsResult.SmsResultBuilder builder = SmsResult.builder()
|
|
||||||
.isSuccess(true)
|
|
||||||
.message("send success")
|
|
||||||
.response(JsonUtils.toJsonString(resp));
|
|
||||||
for (SendStatus sendStatus : resp.getSendStatusSet()) {
|
|
||||||
if (!"Ok".equals(sendStatus.getCode())) {
|
|
||||||
builder.isSuccess(false).message(sendStatus.getMessage());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return builder.build();
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new SmsException(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
package org.dromara.common.sms.entity;
|
|
||||||
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 上传返回体
|
|
||||||
*
|
|
||||||
* @author Lion Li
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
public class SmsResult {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否成功
|
|
||||||
*/
|
|
||||||
private boolean isSuccess;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 响应消息
|
|
||||||
*/
|
|
||||||
private String message;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 实际响应体
|
|
||||||
* <p>
|
|
||||||
* 可自行转换为 SDK 对应的 SendSmsResponse
|
|
||||||
*/
|
|
||||||
private String response;
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package org.dromara.common.sms.exception;
|
|
||||||
|
|
||||||
import java.io.Serial;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sms异常类
|
|
||||||
*
|
|
||||||
* @author Lion Li
|
|
||||||
*/
|
|
||||||
public class SmsException extends RuntimeException {
|
|
||||||
|
|
||||||
@Serial
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
public SmsException(String msg) {
|
|
||||||
super(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
34
ruoyi-common/ruoyi-common-social/pom.xml
Normal file
34
ruoyi-common/ruoyi-common-social/pom.xml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
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-social</artifactId>
|
||||||
|
|
||||||
|
<description>
|
||||||
|
ruoyi-common-social 授权认证
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>me.zhyd.oauth</groupId>
|
||||||
|
<artifactId>JustAuth</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.dromara</groupId>
|
||||||
|
<artifactId>ruoyi-common-json</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.dromara</groupId>
|
||||||
|
<artifactId>ruoyi-common-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
@ -0,0 +1,23 @@
|
|||||||
|
package org.dromara.common.social.config;
|
||||||
|
|
||||||
|
import me.zhyd.oauth.cache.AuthStateCache;
|
||||||
|
import org.dromara.common.social.config.properties.SocialProperties;
|
||||||
|
import org.dromara.common.social.utils.AuthRedisStateCache;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Social 配置属性
|
||||||
|
* @author thiszhc
|
||||||
|
*/
|
||||||
|
@AutoConfiguration
|
||||||
|
@EnableConfigurationProperties(SocialProperties.class)
|
||||||
|
public class SocialAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public AuthStateCache authStateCache() {
|
||||||
|
return new AuthRedisStateCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
package org.dromara.common.social.config.properties;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 社交登录配置
|
||||||
|
*
|
||||||
|
* @author thiszhc
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class SocialLoginConfigProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用 ID
|
||||||
|
*/
|
||||||
|
private String clientId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用密钥
|
||||||
|
*/
|
||||||
|
private String clientSecret;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回调地址
|
||||||
|
*/
|
||||||
|
private String redirectUri;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否获取unionId
|
||||||
|
*/
|
||||||
|
private boolean unionId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coding 企业名称
|
||||||
|
*/
|
||||||
|
private String codingGroupName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付宝公钥
|
||||||
|
*/
|
||||||
|
private String alipayPublicKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 企业微信应用ID
|
||||||
|
*/
|
||||||
|
private String agentId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* stackoverflow api key
|
||||||
|
*/
|
||||||
|
private String stackOverflowKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备ID
|
||||||
|
*/
|
||||||
|
private String deviceId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端系统类型
|
||||||
|
*/
|
||||||
|
private String clientOsType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* maxkey 服务器地址
|
||||||
|
*/
|
||||||
|
private String serverUrl;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package org.dromara.common.social.config.properties;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Social 配置属性
|
||||||
|
*
|
||||||
|
* @author thiszhc
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "justauth")
|
||||||
|
public class SocialProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*/
|
||||||
|
private Boolean enabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 授权类型
|
||||||
|
*/
|
||||||
|
private Map<String, SocialLoginConfigProperties> type;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
package org.dromara.common.social.maxkey;
|
||||||
|
|
||||||
|
import cn.hutool.core.lang.Dict;
|
||||||
|
import me.zhyd.oauth.cache.AuthStateCache;
|
||||||
|
import me.zhyd.oauth.config.AuthConfig;
|
||||||
|
import me.zhyd.oauth.exception.AuthException;
|
||||||
|
import me.zhyd.oauth.model.AuthCallback;
|
||||||
|
import me.zhyd.oauth.model.AuthToken;
|
||||||
|
import me.zhyd.oauth.model.AuthUser;
|
||||||
|
import me.zhyd.oauth.request.AuthDefaultRequest;
|
||||||
|
import org.dromara.common.core.utils.SpringUtils;
|
||||||
|
import org.dromara.common.json.utils.JsonUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author 长春叭哥 2023年03月26日
|
||||||
|
*/
|
||||||
|
public class AuthMaxKeyRequest extends AuthDefaultRequest {
|
||||||
|
|
||||||
|
public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.maxkey.server-url");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设定归属域
|
||||||
|
*/
|
||||||
|
public AuthMaxKeyRequest(AuthConfig config) {
|
||||||
|
super(config, AuthMaxKeySource.MAXKEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthMaxKeyRequest(AuthConfig config, AuthStateCache authStateCache) {
|
||||||
|
super(config, AuthMaxKeySource.MAXKEY, authStateCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AuthToken getAccessToken(AuthCallback authCallback) {
|
||||||
|
String body = doPostAuthorizationCode(authCallback.getCode());
|
||||||
|
Dict object = JsonUtils.parseMap(body);
|
||||||
|
// oauth/token 验证异常
|
||||||
|
if (object.containsKey("error")) {
|
||||||
|
throw new AuthException(object.getStr("error_description"));
|
||||||
|
}
|
||||||
|
// user 验证异常
|
||||||
|
if (object.containsKey("message")) {
|
||||||
|
throw new AuthException(object.getStr("message"));
|
||||||
|
}
|
||||||
|
return AuthToken.builder()
|
||||||
|
.accessToken(object.getStr("access_token"))
|
||||||
|
.refreshToken(object.getStr("refresh_token"))
|
||||||
|
.idToken(object.getStr("id_token"))
|
||||||
|
.tokenType(object.getStr("token_type"))
|
||||||
|
.scope(object.getStr("scope"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AuthUser getUserInfo(AuthToken authToken) {
|
||||||
|
String body = doGetUserInfo(authToken);
|
||||||
|
Dict object = JsonUtils.parseMap(body);
|
||||||
|
// oauth/token 验证异常
|
||||||
|
if (object.containsKey("error")) {
|
||||||
|
throw new AuthException(object.getStr("error_description"));
|
||||||
|
}
|
||||||
|
// user 验证异常
|
||||||
|
if (object.containsKey("message")) {
|
||||||
|
throw new AuthException(object.getStr("message"));
|
||||||
|
}
|
||||||
|
return AuthUser.builder()
|
||||||
|
.uuid(object.getStr("id"))
|
||||||
|
.username(object.getStr("username"))
|
||||||
|
.nickname(object.getStr("name"))
|
||||||
|
.avatar(object.getStr("avatar_url"))
|
||||||
|
.blog(object.getStr("web_url"))
|
||||||
|
.company(object.getStr("organization"))
|
||||||
|
.location(object.getStr("location"))
|
||||||
|
.email(object.getStr("email"))
|
||||||
|
.remark(object.getStr("bio"))
|
||||||
|
.token(authToken)
|
||||||
|
.source(source.toString())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
package org.dromara.common.social.maxkey;
|
||||||
|
|
||||||
|
import me.zhyd.oauth.config.AuthSource;
|
||||||
|
import me.zhyd.oauth.request.AuthDefaultRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Oauth2 默认接口说明
|
||||||
|
*
|
||||||
|
* @author 长春叭哥 2023年03月26日
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public enum AuthMaxKeySource implements AuthSource {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自己搭建的 maxkey 私服
|
||||||
|
*/
|
||||||
|
MAXKEY {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 授权的api
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String authorize() {
|
||||||
|
return AuthMaxKeyRequest.SERVER_URL + "/sign/authz/oauth/v20/authorize";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取accessToken的api
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String accessToken() {
|
||||||
|
return AuthMaxKeyRequest.SERVER_URL + "/sign/authz/oauth/v20/token";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息的api
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String userInfo() {
|
||||||
|
return AuthMaxKeyRequest.SERVER_URL + "/sign/api/oauth/v20/me";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台对应的 AuthRequest 实现类,必须继承自 {@link AuthDefaultRequest}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Class<? extends AuthDefaultRequest> getTargetClass() {
|
||||||
|
return AuthMaxKeyRequest.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package org.dromara.common.social.utils;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import me.zhyd.oauth.cache.AuthStateCache;
|
||||||
|
import org.dromara.common.core.constant.GlobalConstants;
|
||||||
|
import org.dromara.common.redis.utils.RedisUtils;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 授权状态缓存
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class AuthRedisStateCache implements AuthStateCache {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存入缓存
|
||||||
|
*
|
||||||
|
* @param key 缓存key
|
||||||
|
* @param value 缓存内容
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void cache(String key, String value) {
|
||||||
|
// 授权超时时间 默认三分钟
|
||||||
|
RedisUtils.setCacheObject(GlobalConstants.SOCIAL_AUTH_CODE_KEY + key, value, Duration.ofMinutes(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存入缓存
|
||||||
|
*
|
||||||
|
* @param key 缓存key
|
||||||
|
* @param value 缓存内容
|
||||||
|
* @param timeout 指定缓存过期时间(毫秒)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void cache(String key, String value, long timeout) {
|
||||||
|
RedisUtils.setCacheObject(GlobalConstants.SOCIAL_AUTH_CODE_KEY + key, value, Duration.ofMillis(timeout));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存内容
|
||||||
|
*
|
||||||
|
* @param key 缓存key
|
||||||
|
* @return 缓存内容
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String get(String key) {
|
||||||
|
return RedisUtils.getCacheObject(GlobalConstants.SOCIAL_AUTH_CODE_KEY + key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否存在key,如果对应key的value值已过期,也返回false
|
||||||
|
*
|
||||||
|
* @param key 缓存key
|
||||||
|
* @return true:存在key,并且value没过期;false:key不存在或者已过期
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean containsKey(String key) {
|
||||||
|
return RedisUtils.hasKey(GlobalConstants.SOCIAL_AUTH_CODE_KEY + key);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
package org.dromara.common.social.utils;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
|
import me.zhyd.oauth.config.AuthConfig;
|
||||||
|
import me.zhyd.oauth.exception.AuthException;
|
||||||
|
import me.zhyd.oauth.model.AuthCallback;
|
||||||
|
import me.zhyd.oauth.model.AuthResponse;
|
||||||
|
import me.zhyd.oauth.model.AuthUser;
|
||||||
|
import me.zhyd.oauth.request.*;
|
||||||
|
import org.dromara.common.core.domain.model.LoginBody;
|
||||||
|
import org.dromara.common.core.utils.SpringUtils;
|
||||||
|
import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
|
||||||
|
import org.dromara.common.social.config.properties.SocialProperties;
|
||||||
|
import org.dromara.common.social.maxkey.AuthMaxKeyRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证授权工具类
|
||||||
|
*
|
||||||
|
* @author thiszhc
|
||||||
|
*/
|
||||||
|
public class SocialUtils {
|
||||||
|
|
||||||
|
private static final AuthRedisStateCache STATE_CACHE = SpringUtils.getBean(AuthRedisStateCache.class);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static AuthResponse<AuthUser> loginAuth(LoginBody loginBody, SocialProperties socialProperties) throws AuthException {
|
||||||
|
AuthRequest authRequest = getAuthRequest(loginBody.getSource(), socialProperties);
|
||||||
|
AuthCallback callback = new AuthCallback();
|
||||||
|
callback.setCode(loginBody.getSocialCode());
|
||||||
|
callback.setState(loginBody.getSocialState());
|
||||||
|
return authRequest.login(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AuthRequest getAuthRequest(String source, SocialProperties socialProperties) throws AuthException {
|
||||||
|
SocialLoginConfigProperties obj = socialProperties.getType().get(source);
|
||||||
|
if (ObjectUtil.isNull(obj)) {
|
||||||
|
throw new AuthException("不支持的第三方登录类型");
|
||||||
|
}
|
||||||
|
String clientId = obj.getClientId();
|
||||||
|
String clientSecret = obj.getClientSecret();
|
||||||
|
String redirectUri = obj.getRedirectUri();
|
||||||
|
return switch (source.toLowerCase()) {
|
||||||
|
case "dingtalk" -> new AuthDingTalkRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
|
||||||
|
case "baidu" -> new AuthBaiduRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
|
||||||
|
case "github" -> new AuthGithubRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
|
||||||
|
case "gitee" -> new AuthGiteeRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
|
||||||
|
case "weibo" -> new AuthWeiboRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
|
||||||
|
case "coding" -> new AuthCodingRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
|
||||||
|
case "oschina" -> new AuthOschinaRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
|
||||||
|
// 支付宝在创建回调地址时,不允许使用localhost或者127.0.0.1,所以这儿的回调地址使用的局域网内的ip
|
||||||
|
case "alipay" -> new AuthAlipayRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), socialProperties.getType().get("alipay").getAlipayPublicKey(), STATE_CACHE);
|
||||||
|
case "qq" -> new AuthQqRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
|
||||||
|
case "wechat_open" -> new AuthWeChatOpenRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
|
||||||
|
case "taobao" -> new AuthTaobaoRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
|
||||||
|
case "douyin" -> new AuthDouyinRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
|
||||||
|
case "linkedin" -> new AuthLinkedinRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
|
||||||
|
case "microsoft" -> new AuthMicrosoftRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
|
||||||
|
case "renren" -> new AuthRenrenRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
|
||||||
|
case "stack_overflow" -> new AuthStackOverflowRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).stackOverflowKey("").build(), STATE_CACHE);
|
||||||
|
case "huawei" -> new AuthHuaweiRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
|
||||||
|
case "wechat_enterprise" -> new AuthWeChatEnterpriseQrcodeRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).agentId("").build(), STATE_CACHE);
|
||||||
|
case "gitlab" -> new AuthGitlabRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
|
||||||
|
case "wechat_mp" -> new AuthWeChatMpRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
|
||||||
|
case "aliyun" -> new AuthAliyunRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
|
||||||
|
case "maxkey" -> new AuthMaxKeyRequest(AuthConfig.builder().clientId(clientId).clientSecret(clientSecret).redirectUri(redirectUri).build(), STATE_CACHE);
|
||||||
|
default -> throw new AuthException("未获取到有效的Auth配置");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
org.dromara.common.social.config.SocialAutoConfiguration
|
@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>ruoyi-monitor-admin</module>
|
<module>ruoyi-monitor-admin</module>
|
||||||
<module>ruoyi-xxl-job-admin</module>
|
<module>ruoyi-powerjob-server</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user