mirror of
				https://gitee.com/hhyykk/ipms-sjy.git
				synced 2025-10-31 02:08:43 +08:00 
			
		
		
		
	Merge branch 'upStreamMaster'
This commit is contained in:
		
							
								
								
									
										2
									
								
								Jenkinsfile
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								Jenkinsfile
									
									
									
									
										vendored
									
									
								
							| @@ -21,7 +21,7 @@ pipeline { | |||||||
|         // GitHub 账号名 |         // GitHub 账号名 | ||||||
|         GITHUB_ACCOUNT = 'https://gitee.com/zhijiantianya/ruoyi-vue-pro' |         GITHUB_ACCOUNT = 'https://gitee.com/zhijiantianya/ruoyi-vue-pro' | ||||||
|         // 应用名称 |         // 应用名称 | ||||||
|         APP_NAME = 'yudao-admin-server' |         APP_NAME = 'yudao-server' | ||||||
|         // 应用部署路径 |         // 应用部署路径 | ||||||
|         APP_DEPLOY_BASE_DIR = '/media/pi/KINGTON/data/work/projects/' |         APP_DEPLOY_BASE_DIR = '/media/pi/KINGTON/data/work/projects/' | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								README.md
									
									
									
									
									
								
							| @@ -7,19 +7,21 @@ | |||||||
|  |  | ||||||
| ## 🐯 平台简介 | ## 🐯 平台简介 | ||||||
|  |  | ||||||
| **芋道**,一套**全部开源**的**企业级**的快速开发平台,毫无保留给个人及企业免费使用。 | **芋道**,以开发者为中心,打造中国第一流的快速开发平台,全部开源,个人与企业可 100% 免费使用。 | ||||||
|  |  | ||||||
| > 有任何问题,或者想要的功能,可以在 _Issues_ 中提给艿艿。 | > 有任何问题,或者想要的功能,可以在 _Issues_ 中提给艿艿。 | ||||||
|  | > | ||||||
|  | > 😜 给项目点点 Star 吧,这对我们真的很重要! | ||||||
|  |  | ||||||
| * 前端采用 [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) ,正在支持 Vue 3 + ElementUI Plus 最新方案。 | * 前端采用 [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) ,正在支持 Vue 3 + ElementUI Plus 最新方案。 | ||||||
| * 后端采用 Spring Boot、MySQL + MyBatis Plus、Redis + Redisson。 | * 后端采用 Spring Boot、MySQL + MyBatis Plus、Redis + Redisson。 | ||||||
| * 权限认证使用 Spring Security & Token & Redis,支持多终端、多种用户的认证系统。 | * 权限认证使用 Spring Security & Token & Redis,支持多终端、多种用户的认证系统。 | ||||||
| * 支持加载动态权限菜单,按钮级别权限控制,本地缓存提升性能。 | * 支持加载动态权限菜单,按钮级别权限控制,本地缓存提升性能。 | ||||||
| * 支持 SaaS 多租户系统,可自定义每个租户的权限,提供透明化的多租户底层封装。 | * 支持 SaaS 多租户系统,可自定义每个租户的权限,提供透明化的多租户底层封装。 | ||||||
| * 工作流使用 Activiti ,支持动态表单、在线设计流程、多种任务分配方式。 | * 工作流使用 Activiti + Flowable,支持动态表单、在线设计流程、多种任务分配方式。 | ||||||
| * 高效率开发,使用代码生成器可以一键生成前后端代码 + 单元测试 + Swagger 接口文档 + Validator 参数校验。 | * 高效率开发,使用代码生成器可以一键生成前后端代码 + 单元测试 + Swagger 接口文档 + Validator 参数校验。 | ||||||
| * 集成微信小程序、微信公众号、企业微信、钉钉等三方登陆,集成支付宝、微信等支付与退款。 | * 集成微信小程序、微信公众号、企业微信、钉钉等三方登陆,集成支付宝、微信等支付与退款。 | ||||||
| * 集成阿里云、腾讯云、云片等短信渠道,集成阿里云、腾讯云、七牛云等云存储服务。 | * 集成阿里云、腾讯云、云片等短信渠道,集成 MinIO、阿里云、腾讯云、七牛云等云存储服务。 | ||||||
|  |  | ||||||
| | 项目名                | 说明                     | 传说门                                                                                                                                 | | | 项目名                | 说明                     | 传说门                                                                                                                                 | | ||||||
| |--------------------|------------------------|-------------------------------------------------------------------------------------------------------------------------------------| | |--------------------|------------------------|-------------------------------------------------------------------------------------------------------------------------------------| | ||||||
| @@ -150,7 +152,7 @@ ps:核心功能已经实现,正在对接微信小程序中... | |||||||
|  |  | ||||||
| | 框架                                                                                          | 说明               | 版本       | 学习指南                                                           | | | 框架                                                                                          | 说明               | 版本       | 学习指南                                                           | | ||||||
| |---------------------------------------------------------------------------------------------|------------------|----------|----------------------------------------------------------------| | |---------------------------------------------------------------------------------------------|------------------|----------|----------------------------------------------------------------| | ||||||
| | [Spring Boot](https://spring.io/projects/spring-boot)                                       | 应用开发框架           | 2.5.10   | [文档](https://github.com/YunaiV/SpringBoot-Labs)                | | | [Spring Boot](https://spring.io/projects/spring-boot)                                       | 应用开发框架           | 2.5.12   | [文档](https://github.com/YunaiV/SpringBoot-Labs)                | | ||||||
| | [MySQL](https://www.mysql.com/cn/)                                                          | 数据库服务器           | 5.7      |                                                                | | | [MySQL](https://www.mysql.com/cn/)                                                          | 数据库服务器           | 5.7      |                                                                | | ||||||
| | [Druid](https://github.com/alibaba/druid)                                                   | JDBC 连接池、监控组件    | 1.2.8    | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) | | | [Druid](https://github.com/alibaba/druid)                                                   | JDBC 连接池、监控组件    | 1.2.8    | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) | | ||||||
| | [MyBatis Plus](https://mp.baomidou.com/)                                                    | MyBatis 增强工具包    | 3.5.1    | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao)         | | | [MyBatis Plus](https://mp.baomidou.com/)                                                    | MyBatis 增强工具包    | 3.5.1    | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao)         | | ||||||
|   | |||||||
| @@ -1,20 +1,15 @@ | |||||||
| #!/bin/bash | #!/bin/bash | ||||||
| set -e | set -e | ||||||
|  |  | ||||||
| # 基础 |  | ||||||
| # export JAVA_HOME=/work/programs/jdk/jdk1.8.0_181 |  | ||||||
| # export PATH=PATH=$PATH:$JAVA_HOME/bin |  | ||||||
| # export CLASSPATH=$JAVA_HOME/jre/lib/rt.jar:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar |  | ||||||
|  |  | ||||||
| DATE=$(date +%Y%m%d%H%M) | DATE=$(date +%Y%m%d%H%M) | ||||||
| # 基础路径 | # 基础路径 | ||||||
| BASE_PATH=/media/pi/KINGTON/data/work/projects/yudao-admin-server | BASE_PATH=/work/projects/yudao-server | ||||||
| # 编译后 jar 的地址。部署时,Jenkins 会上传 jar 包到该目录下 | # 编译后 jar 的地址。部署时,Jenkins 会上传 jar 包到该目录下 | ||||||
| SOURCE_PATH=$BASE_PATH/build | SOURCE_PATH=$BASE_PATH/build | ||||||
| # 服务名称。同时约定部署服务的 jar 包名字也为它。 | # 服务名称。同时约定部署服务的 jar 包名字也为它。 | ||||||
| SERVER_NAME=yudao-admin-server | SERVER_NAME=yudao-server | ||||||
| # 环境 | # 环境 | ||||||
| PROFILES_ACTIVE=dev | PROFILES_ACTIVE=development | ||||||
| # 健康检查 URL | # 健康检查 URL | ||||||
| HEALTH_CHECK_URL=http://127.0.0.1:48080/actuator/health/ | HEALTH_CHECK_URL=http://127.0.0.1:48080/actuator/health/ | ||||||
|  |  | ||||||
| @@ -62,7 +57,7 @@ function transfer() { | |||||||
|     echo "[transfer] 转移 $SERVER_NAME.jar 完成" |     echo "[transfer] 转移 $SERVER_NAME.jar 完成" | ||||||
| } | } | ||||||
|  |  | ||||||
| # 停止 | # 停止:优雅关闭之前已经启动的服务 | ||||||
| function stop() { | function stop() { | ||||||
|     echo "[stop] 开始停止 $BASE_PATH/$SERVER_NAME" |     echo "[stop] 开始停止 $BASE_PATH/$SERVER_NAME" | ||||||
|     PID=$(ps -ef | grep $BASE_PATH/$SERVER_NAME | grep -v "grep" | awk '{print $2}') |     PID=$(ps -ef | grep $BASE_PATH/$SERVER_NAME | grep -v "grep" | awk '{print $2}') | ||||||
| @@ -71,8 +66,8 @@ function stop() { | |||||||
|         # 正常关闭 |         # 正常关闭 | ||||||
|         echo "[stop] $BASE_PATH/$SERVER_NAME 运行中,开始 kill [$PID]" |         echo "[stop] $BASE_PATH/$SERVER_NAME 运行中,开始 kill [$PID]" | ||||||
|         kill -15 $PID |         kill -15 $PID | ||||||
|         # 等待最大 60 秒,直到关闭完成。 |         # 等待最大 120 秒,直到关闭完成。 | ||||||
|         for ((i = 0; i < 60; i++)) |         for ((i = 0; i < 120; i++)) | ||||||
|             do |             do | ||||||
|                 sleep 1 |                 sleep 1 | ||||||
|                 PID=$(ps -ef | grep $BASE_PATH/$SERVER_NAME | grep -v "grep" | awk '{print $2}') |                 PID=$(ps -ef | grep $BASE_PATH/$SERVER_NAME | grep -v "grep" | awk '{print $2}') | ||||||
| @@ -95,7 +90,7 @@ function stop() { | |||||||
|     fi |     fi | ||||||
| } | } | ||||||
|  |  | ||||||
| # 启动 | # 启动:启动后端项目 | ||||||
| function start() { | function start() { | ||||||
|     # 开启启动前,打印启动参数 |     # 开启启动前,打印启动参数 | ||||||
|     echo "[start] 开始启动 $BASE_PATH/$SERVER_NAME" |     echo "[start] 开始启动 $BASE_PATH/$SERVER_NAME" | ||||||
| @@ -108,13 +103,13 @@ function start() { | |||||||
|     echo "[start] 启动 $BASE_PATH/$SERVER_NAME 完成" |     echo "[start] 启动 $BASE_PATH/$SERVER_NAME 完成" | ||||||
| } | } | ||||||
|  |  | ||||||
| # 健康检查 | # 健康检查:自动判断后端项目是否正常启动 | ||||||
| function healthCheck() { | function healthCheck() { | ||||||
|     # 如果配置健康检查,则进行健康检查 |     # 如果配置健康检查,则进行健康检查 | ||||||
|     if [ -n "$HEALTH_CHECK_URL" ]; then |     if [ -n "$HEALTH_CHECK_URL" ]; then | ||||||
|         # 健康检查最大 60 秒,直到健康检查通过 |         # 健康检查最大 120 秒,直到健康检查通过 | ||||||
|         echo "[healthCheck] 开始通过 $HEALTH_CHECK_URL 地址,进行健康检查"; |         echo "[healthCheck] 开始通过 $HEALTH_CHECK_URL 地址,进行健康检查"; | ||||||
|         for ((i = 0; i < 60; i++)) |         for ((i = 0; i < 120; i++)) | ||||||
|             do |             do | ||||||
|                 # 请求健康检查地址,只获取状态码。 |                 # 请求健康检查地址,只获取状态码。 | ||||||
|                 result=`curl -I -m 10 -o /dev/null -s -w %{http_code} $HEALTH_CHECK_URL || echo "000"` |                 result=`curl -I -m 10 -o /dev/null -s -w %{http_code} $HEALTH_CHECK_URL || echo "000"` | ||||||
| @@ -138,11 +133,11 @@ function healthCheck() { | |||||||
|         else |         else | ||||||
|             tail -n 10 nohup.out |             tail -n 10 nohup.out | ||||||
|         fi |         fi | ||||||
|     # 如果未配置健康检查,则 slepp 60 秒,人工看日志是否部署成功。 |     # 如果未配置健康检查,则 sleep 120 秒,人工看日志是否部署成功。 | ||||||
|     else |     else | ||||||
|         echo "[healthCheck] HEALTH_CHECK_URL 未配置,开始 sleep 60 秒"; |         echo "[healthCheck] HEALTH_CHECK_URL 未配置,开始 sleep 120 秒"; | ||||||
|         sleep 60 |         sleep 120 | ||||||
|         echo "[healthCheck] sleep 60 秒完成,查看日志,自行判断是否启动成功"; |         echo "[healthCheck] sleep 120 秒完成,查看日志,自行判断是否启动成功"; | ||||||
|         tail -n 50 nohup.out |         tail -n 50 nohup.out | ||||||
|     fi |     fi | ||||||
| } | } | ||||||
| @@ -159,7 +154,7 @@ function deploy() { | |||||||
|     # 启动 Java 服务 |     # 启动 Java 服务 | ||||||
|     start |     start | ||||||
|     # 健康检查 |     # 健康检查 | ||||||
| #    healthCheck |     healthCheck | ||||||
| } | } | ||||||
|  |  | ||||||
| deploy | deploy | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								pom.xml
									
									
									
									
									
								
							| @@ -25,7 +25,7 @@ | |||||||
|     <url>https://github.com/YunaiV/ruoyi-vue-pro</url> |     <url>https://github.com/YunaiV/ruoyi-vue-pro</url> | ||||||
|  |  | ||||||
|     <properties> |     <properties> | ||||||
|         <revision>1.6.1-snapshot</revision> |         <revision>1.6.2-snapshot</revision> | ||||||
|         <!-- Maven 相关 --> |         <!-- Maven 相关 --> | ||||||
|         <java.version>1.8</java.version> |         <java.version>1.8</java.version> | ||||||
|         <maven.compiler.source>${java.version}</maven.compiler.source> |         <maven.compiler.source>${java.version}</maven.compiler.source> | ||||||
|   | |||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -14,9 +14,9 @@ | |||||||
|     <url>https://github.com/YunaiV/ruoyi-vue-pro</url> |     <url>https://github.com/YunaiV/ruoyi-vue-pro</url> | ||||||
|  |  | ||||||
|     <properties> |     <properties> | ||||||
|         <revision>1.6.1-snapshot</revision> |         <revision>1.6.2-snapshot</revision> | ||||||
|         <!-- 统一依赖管理 --> |         <!-- 统一依赖管理 --> | ||||||
|         <spring.boot.version>2.5.10</spring.boot.version> |         <spring.boot.version>2.5.12</spring.boot.version> | ||||||
|         <!-- Web 相关 --> |         <!-- Web 相关 --> | ||||||
|         <knife4j.version>3.0.2</knife4j.version> |         <knife4j.version>3.0.2</knife4j.version> | ||||||
|         <swagger-annotations.version>1.5.22</swagger-annotations.version> |         <swagger-annotations.version>1.5.22</swagger-annotations.version> | ||||||
| @@ -26,7 +26,7 @@ | |||||||
|         <druid.version>1.2.8</druid.version> |         <druid.version>1.2.8</druid.version> | ||||||
|         <mybatis-plus.version>3.4.3.4</mybatis-plus.version> |         <mybatis-plus.version>3.4.3.4</mybatis-plus.version> | ||||||
|         <dynamic-datasource.version>3.5.0</dynamic-datasource.version> |         <dynamic-datasource.version>3.5.0</dynamic-datasource.version> | ||||||
|         <redisson.version>3.16.6</redisson.version> |         <redisson.version>3.17.0</redisson.version> | ||||||
|         <!-- Config 配置中心相关 --> |         <!-- Config 配置中心相关 --> | ||||||
|         <apollo.version>1.9.2</apollo.version> |         <apollo.version>1.9.2</apollo.version> | ||||||
|         <!-- Job 定时任务相关 --> |         <!-- Job 定时任务相关 --> | ||||||
| @@ -60,6 +60,7 @@ | |||||||
|         <minio.version>8.2.2</minio.version> |         <minio.version>8.2.2</minio.version> | ||||||
|         <aliyun-java-sdk-core.version>4.5.25</aliyun-java-sdk-core.version> |         <aliyun-java-sdk-core.version>4.5.25</aliyun-java-sdk-core.version> | ||||||
|         <aliyun-java-sdk-dysmsapi.version>2.1.0</aliyun-java-sdk-dysmsapi.version> |         <aliyun-java-sdk-dysmsapi.version>2.1.0</aliyun-java-sdk-dysmsapi.version> | ||||||
|  |         <tencentcloud-sdk-java.version>3.1.471</tencentcloud-sdk-java.version> | ||||||
|         <yunpian-java-sdk.version>1.2.7</yunpian-java-sdk.version> |         <yunpian-java-sdk.version>1.2.7</yunpian-java-sdk.version> | ||||||
|         <justauth.version>1.4.0</justauth.version> |         <justauth.version>1.4.0</justauth.version> | ||||||
|     </properties> |     </properties> | ||||||
| @@ -552,6 +553,11 @@ | |||||||
|                 <artifactId>aliyun-java-sdk-dysmsapi</artifactId> |                 <artifactId>aliyun-java-sdk-dysmsapi</artifactId> | ||||||
|                 <version>${aliyun-java-sdk-dysmsapi.version}</version> |                 <version>${aliyun-java-sdk-dysmsapi.version}</version> | ||||||
|             </dependency> |             </dependency> | ||||||
|  |             <dependency> | ||||||
|  |                 <groupId>com.tencentcloudapi</groupId> | ||||||
|  |                 <artifactId>tencentcloud-sdk-java</artifactId> | ||||||
|  |                 <version>${tencentcloud-sdk-java.version}</version> | ||||||
|  |             </dependency> | ||||||
|             <!-- SMS SDK end --> |             <!-- SMS SDK end --> | ||||||
|  |  | ||||||
|             <dependency> |             <dependency> | ||||||
|   | |||||||
| @@ -1,8 +1,19 @@ | |||||||
| package cn.iocoder.yudao.framework.common.util.collection; | package cn.iocoder.yudao.framework.common.util.collection; | ||||||
|  |  | ||||||
|  | import cn.hutool.core.collection.CollectionUtil; | ||||||
| import cn.hutool.core.util.ArrayUtil; | import cn.hutool.core.util.ArrayUtil; | ||||||
|  | import cn.hutool.core.util.TypeUtil; | ||||||
|  | import org.springframework.cglib.core.TypeUtils; | ||||||
|  |  | ||||||
|  | import java.lang.reflect.Array; | ||||||
|  | import java.lang.reflect.ParameterizedType; | ||||||
|  | import java.lang.reflect.Type; | ||||||
|  | import java.util.Collection; | ||||||
|  | import java.util.List; | ||||||
| import java.util.function.Consumer; | import java.util.function.Consumer; | ||||||
|  | import java.util.function.Function; | ||||||
|  |  | ||||||
|  | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Array 工具类 |  * Array 工具类 | ||||||
| @@ -30,4 +41,16 @@ public class ArrayUtils { | |||||||
|         return result; |         return result; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public static <T, V> V[] toArray(Collection<T> from, Function<T, V> mapper) { | ||||||
|  |         return toArray(convertList(from, mapper)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @SuppressWarnings("unchecked") | ||||||
|  |     public static <T> T[] toArray(Collection<T> from) { | ||||||
|  |         if (CollectionUtil.isEmpty(from)) { | ||||||
|  |             return (T[]) (new Object[0]); | ||||||
|  |         } | ||||||
|  |         return ArrayUtil.toArray(from, (Class<T>) CollectionUtil.getElementType(from.iterator())); | ||||||
|  |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -115,7 +115,7 @@ public class CollectionUtils { | |||||||
|             return new HashMap<>(); |             return new HashMap<>(); | ||||||
|         } |         } | ||||||
|         return from.stream() |         return from.stream() | ||||||
|                    .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList()))); |                 .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList()))); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // 暂时没想好名字,先以 2 结尾噶 |     // 暂时没想好名字,先以 2 结尾噶 | ||||||
| @@ -169,4 +169,5 @@ public class CollectionUtils { | |||||||
|     public static <T> Collection<T> singleton(T deptId) { |     public static <T> Collection<T> singleton(T deptId) { | ||||||
|         return deptId == null ? Collections.emptyList() : Collections.singleton(deptId); |         return deptId == null ? Collections.emptyList() : Collections.singleton(deptId); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,9 +1,7 @@ | |||||||
| package cn.iocoder.yudao.framework.common.util.http; | package cn.iocoder.yudao.framework.common.util.http; | ||||||
|  |  | ||||||
| import cn.hutool.core.io.FileUtil; |  | ||||||
| import cn.hutool.core.map.TableMap; | import cn.hutool.core.map.TableMap; | ||||||
| import cn.hutool.core.net.url.UrlBuilder; | import cn.hutool.core.net.url.UrlBuilder; | ||||||
| import cn.hutool.core.util.ReferenceUtil; |  | ||||||
| import cn.hutool.core.util.ReflectUtil; | import cn.hutool.core.util.ReflectUtil; | ||||||
|  |  | ||||||
| import java.nio.charset.Charset; | import java.nio.charset.Charset; | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ package cn.iocoder.yudao.framework.common.util.io; | |||||||
|  |  | ||||||
| import cn.hutool.core.io.IORuntimeException; | import cn.hutool.core.io.IORuntimeException; | ||||||
| import cn.hutool.core.io.IoUtil; | import cn.hutool.core.io.IoUtil; | ||||||
| import cn.hutool.core.util.CharsetUtil; |  | ||||||
| import cn.hutool.core.util.StrUtil; | import cn.hutool.core.util.StrUtil; | ||||||
|  |  | ||||||
| import java.io.InputStream; | import java.io.InputStream; | ||||||
|   | |||||||
| @@ -35,7 +35,7 @@ import java.util.Set; | |||||||
|  * 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。 |  * 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。 | ||||||
|  * |  * | ||||||
|  * 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改? |  * 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改? | ||||||
|  * 1. 一般情况下,dept_id 不进行修改,则会导致用户看到之前的数据。【yudao-admin-server 采用该方案】 |  * 1. 一般情况下,dept_id 不进行修改,则会导致用户看到之前的数据。【yudao-server 采用该方案】 | ||||||
|  * 2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 DeptDataPermissionRule 的实现代码】 |  * 2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 DeptDataPermissionRule 的实现代码】 | ||||||
|  *  1)编写洗数据的脚本,将 dept_id 修改成新部门的编号;【建议】 |  *  1)编写洗数据的脚本,将 dept_id 修改成新部门的编号;【建议】 | ||||||
|  *      最终过滤条件是 WHERE dept_id = ? |  *      最终过滤条件是 WHERE dept_id = ? | ||||||
|   | |||||||
| @@ -82,9 +82,9 @@ public class WXPayClientConfig implements PayClientConfig { | |||||||
|     @NotBlank(message = "apiclient_cert 不能为空", groups = V3.class) |     @NotBlank(message = "apiclient_cert 不能为空", groups = V3.class) | ||||||
|     private String privateCertContent; |     private String privateCertContent; | ||||||
|     /** |     /** | ||||||
|      * apiV3 秘钥值 |      * apiV3 密钥值 | ||||||
|      */ |      */ | ||||||
|     @NotBlank(message = "apiV3 秘钥值 不能为空", groups = V3.class) |     @NotBlank(message = "apiV3 密钥值 不能为空", groups = V3.class) | ||||||
|     private String apiV3Key; |     private String apiV3Key; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ | |||||||
|     <packaging>jar</packaging> |     <packaging>jar</packaging> | ||||||
|  |  | ||||||
|     <name>${project.artifactId}</name> |     <name>${project.artifactId}</name> | ||||||
|     <description>短信拓展,支持阿里云、云片</description> |     <description>短信拓展,支持阿里云、云片、腾讯云</description> | ||||||
|     <url>https://github.com/YunaiV/ruoyi-vue-pro</url> |     <url>https://github.com/YunaiV/ruoyi-vue-pro</url> | ||||||
|  |  | ||||||
|     <dependencies> |     <dependencies> | ||||||
| @@ -77,6 +77,10 @@ | |||||||
|             <groupId>com.aliyun</groupId> |             <groupId>com.aliyun</groupId> | ||||||
|             <artifactId>aliyun-java-sdk-dysmsapi</artifactId> |             <artifactId>aliyun-java-sdk-dysmsapi</artifactId> | ||||||
|         </dependency> |         </dependency> | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>com.tencentcloudapi</groupId> | ||||||
|  |             <artifactId>tencentcloud-sdk-java</artifactId> | ||||||
|  |         </dependency> | ||||||
|         <!-- SMS SDK end --> |         <!-- SMS SDK end --> | ||||||
|     </dependencies> |     </dependencies> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ import java.util.List; | |||||||
|  * 短信客户端,用于对接各短信平台的 SDK,实现短信发送等功能 |  * 短信客户端,用于对接各短信平台的 SDK,实现短信发送等功能 | ||||||
|  * |  * | ||||||
|  * @author zzf |  * @author zzf | ||||||
|  * @date 2021/1/25 14:14 |  * @since 2021/1/25 14:14 | ||||||
|  */ |  */ | ||||||
| public interface SmsClient { | public interface SmsClient { | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; | |||||||
|  * 短信客户端的工厂接口 |  * 短信客户端的工厂接口 | ||||||
|  * |  * | ||||||
|  * @author zzf |  * @author zzf | ||||||
|  * @date 2021/1/28 14:01 |  * @since 2021/1/28 14:01 | ||||||
|  */ |  */ | ||||||
| public interface SmsClientFactory { | public interface SmsClientFactory { | ||||||
|  |  | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ import java.util.List; | |||||||
|  * 短信客户端的抽象类,提供模板方法,减少子类的冗余代码 |  * 短信客户端的抽象类,提供模板方法,减少子类的冗余代码 | ||||||
|  * |  * | ||||||
|  * @author zzf |  * @author zzf | ||||||
|  * @date 2021/2/1 9:28 |  * @since 2021/2/1 9:28 | ||||||
|  */ |  */ | ||||||
| @Slf4j | @Slf4j | ||||||
| public abstract class AbstractSmsClient implements SmsClient { | public abstract class AbstractSmsClient implements SmsClient { | ||||||
| @@ -31,7 +31,7 @@ public abstract class AbstractSmsClient implements SmsClient { | |||||||
|     protected final SmsCodeMapping codeMapping; |     protected final SmsCodeMapping codeMapping; | ||||||
|  |  | ||||||
|     public AbstractSmsClient(SmsChannelProperties properties, SmsCodeMapping codeMapping) { |     public AbstractSmsClient(SmsChannelProperties properties, SmsCodeMapping codeMapping) { | ||||||
|         this.properties = properties; |         this.properties = prepareProperties(properties); | ||||||
|         this.codeMapping = codeMapping; |         this.codeMapping = codeMapping; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -54,11 +54,21 @@ public abstract class AbstractSmsClient implements SmsClient { | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         log.info("[refresh][配置({})发生变化,重新初始化]", properties); |         log.info("[refresh][配置({})发生变化,重新初始化]", properties); | ||||||
|         this.properties = properties; |         this.properties = prepareProperties(properties); | ||||||
|         // 初始化 |         // 初始化 | ||||||
|         this.init(); |         this.init(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 在赋值给{@link this#properties}前,子类可根据需要预处理短信渠道配置 | ||||||
|  |      * | ||||||
|  |      * @param properties 数据库中存储的短信渠道配置 | ||||||
|  |      * @return 满足子类实现的短信渠道配置 | ||||||
|  |      */ | ||||||
|  |     protected SmsChannelProperties prepareProperties(SmsChannelProperties properties) { | ||||||
|  |         return properties; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public Long getId() { |     public Long getId() { | ||||||
|         return properties.getId(); |         return properties.getId(); | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.sms.core.client.SmsClient; | |||||||
| import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory; | import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory; | ||||||
| import cn.iocoder.yudao.framework.sms.core.client.impl.aliyun.AliyunSmsClient; | import cn.iocoder.yudao.framework.sms.core.client.impl.aliyun.AliyunSmsClient; | ||||||
| import cn.iocoder.yudao.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient; | import cn.iocoder.yudao.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient; | ||||||
|  | import cn.iocoder.yudao.framework.sms.core.client.impl.tencent.TencentSmsClient; | ||||||
| import cn.iocoder.yudao.framework.sms.core.client.impl.yunpian.YunpianSmsClient; | import cn.iocoder.yudao.framework.sms.core.client.impl.yunpian.YunpianSmsClient; | ||||||
| import cn.iocoder.yudao.framework.sms.core.enums.SmsChannelEnum; | import cn.iocoder.yudao.framework.sms.core.enums.SmsChannelEnum; | ||||||
| import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; | import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; | ||||||
| @@ -44,7 +45,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory { | |||||||
|         Arrays.stream(SmsChannelEnum.values()).forEach(channel -> { |         Arrays.stream(SmsChannelEnum.values()).forEach(channel -> { | ||||||
|             // 创建一个空的 SmsChannelProperties 对象 |             // 创建一个空的 SmsChannelProperties 对象 | ||||||
|             SmsChannelProperties properties = new SmsChannelProperties().setCode(channel.getCode()) |             SmsChannelProperties properties = new SmsChannelProperties().setCode(channel.getCode()) | ||||||
|                     .setApiKey("default").setApiSecret("default"); |                     .setApiKey("default default").setApiSecret("default"); | ||||||
|             // 创建 Sms 客户端 |             // 创建 Sms 客户端 | ||||||
|             AbstractSmsClient smsClient = createSmsClient(properties); |             AbstractSmsClient smsClient = createSmsClient(properties); | ||||||
|             channelCodeClients.put(channel.getCode(), smsClient); |             channelCodeClients.put(channel.getCode(), smsClient); | ||||||
| @@ -81,6 +82,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory { | |||||||
|             case ALIYUN: return new AliyunSmsClient(properties); |             case ALIYUN: return new AliyunSmsClient(properties); | ||||||
|             case YUN_PIAN: return new YunpianSmsClient(properties); |             case YUN_PIAN: return new YunpianSmsClient(properties); | ||||||
|             case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties); |             case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties); | ||||||
|  |             case TENCENT: return new TencentSmsClient(properties); | ||||||
|         } |         } | ||||||
|         // 创建失败,错误日志 + 抛出异常 |         // 创建失败,错误日志 + 抛出异常 | ||||||
|         log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties); |         log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties); | ||||||
|   | |||||||
| @@ -41,7 +41,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE | |||||||
|  * 阿里短信客户端的实现类 |  * 阿里短信客户端的实现类 | ||||||
|  * |  * | ||||||
|  * @author zzf |  * @author zzf | ||||||
|  * @date 2021/1/25 14:17 |  * @since 2021/1/25 14:17 | ||||||
|  */ |  */ | ||||||
| @Slf4j | @Slf4j | ||||||
| public class AliyunSmsClient extends AbstractSmsClient { | public class AliyunSmsClient extends AbstractSmsClient { | ||||||
|   | |||||||
| @@ -0,0 +1,41 @@ | |||||||
|  | package cn.iocoder.yudao.framework.sms.core.client.impl.tencent; | ||||||
|  |  | ||||||
|  | import cn.hutool.core.bean.BeanUtil; | ||||||
|  | import cn.hutool.core.lang.Assert; | ||||||
|  | import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; | ||||||
|  | import lombok.Data; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 腾讯云短信配置实现类 | ||||||
|  |  * 腾讯云发送短信时,需要额外的参数 sdkAppId, | ||||||
|  |  * | ||||||
|  |  * @author shiwp | ||||||
|  |  */ | ||||||
|  | @Data | ||||||
|  | public class TencentSmsChannelProperties extends SmsChannelProperties { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 应用 id | ||||||
|  |      */ | ||||||
|  |     private String sdkAppId; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 考虑到不破坏原有的 apiKey + apiSecret 的结构, | ||||||
|  |      * 所以腾讯云短信存储时,将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。 | ||||||
|  |      * 因此在使用时,需要将 secretId 和 sdkAppId 解析出来,分别存储到对应字段中。 | ||||||
|  |      */ | ||||||
|  |     public static TencentSmsChannelProperties build(SmsChannelProperties properties) { | ||||||
|  |         if (properties instanceof TencentSmsChannelProperties) { | ||||||
|  |             return (TencentSmsChannelProperties) properties; | ||||||
|  |         } | ||||||
|  |         TencentSmsChannelProperties result = BeanUtil.toBean(properties, TencentSmsChannelProperties.class); | ||||||
|  |         String combineKey = properties.getApiKey(); | ||||||
|  |         Assert.notEmpty(combineKey, "apiKey 不能为空"); | ||||||
|  |         String[] keys = combineKey.trim().split(" "); | ||||||
|  |         Assert.isTrue(keys.length == 2, "腾讯云短信 apiKey 配置格式错误,请配置 为[secretId sdkAppId]"); | ||||||
|  |         Assert.notBlank(keys[0], "腾讯云短信 secretId 不能为空"); | ||||||
|  |         Assert.notBlank(keys[1], "腾讯云短信 sdkAppId 不能为空"); | ||||||
|  |         result.setSdkAppId(keys[1]).setApiKey(keys[0]); | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,302 @@ | |||||||
|  | package cn.iocoder.yudao.framework.sms.core.client.impl.tencent; | ||||||
|  |  | ||||||
|  | import cn.hutool.core.lang.Assert; | ||||||
|  | import cn.hutool.core.util.StrUtil; | ||||||
|  | import cn.iocoder.yudao.framework.common.core.KeyValue; | ||||||
|  | import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; | ||||||
|  | import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; | ||||||
|  | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||||||
|  | import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult; | ||||||
|  | import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO; | ||||||
|  | import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO; | ||||||
|  | import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO; | ||||||
|  | import cn.iocoder.yudao.framework.sms.core.client.impl.AbstractSmsClient; | ||||||
|  | import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum; | ||||||
|  | import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; | ||||||
|  | import com.fasterxml.jackson.annotation.JsonFormat; | ||||||
|  | import com.fasterxml.jackson.annotation.JsonProperty; | ||||||
|  | import com.google.common.annotations.VisibleForTesting; | ||||||
|  | import com.tencentcloudapi.common.Credential; | ||||||
|  | import com.tencentcloudapi.common.exception.TencentCloudSDKException; | ||||||
|  | import com.tencentcloudapi.sms.v20210111.SmsClient; | ||||||
|  | import com.tencentcloudapi.sms.v20210111.models.*; | ||||||
|  | import lombok.Data; | ||||||
|  |  | ||||||
|  | import java.util.Date; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.function.Function; | ||||||
|  | import java.util.function.Supplier; | ||||||
|  |  | ||||||
|  | import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; | ||||||
|  | import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 腾讯云短信功能实现 | ||||||
|  |  * <p> | ||||||
|  |  * 参见 https://cloud.tencent.com/document/product/382/52077 | ||||||
|  |  * | ||||||
|  |  * @author shiwp | ||||||
|  |  */ | ||||||
|  | public class TencentSmsClient extends AbstractSmsClient { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 调用成功 code | ||||||
|  |      */ | ||||||
|  |     public static final String API_SUCCESS_CODE = "Ok"; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * REGION,使用南京 | ||||||
|  |      */ | ||||||
|  |     private static final String ENDPOINT = "ap-nanjing"; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 是否国际/港澳台短信: | ||||||
|  |      * 0:表示国内短信。 | ||||||
|  |      * 1:表示国际/港澳台短信。 | ||||||
|  |      */ | ||||||
|  |     private static final long INTERNATIONAL = 0L; | ||||||
|  |  | ||||||
|  |     private SmsClient client; | ||||||
|  |  | ||||||
|  |     public TencentSmsClient(SmsChannelProperties properties) { | ||||||
|  |         super(properties, new TencentSmsCodeMapping()); | ||||||
|  |         Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected void doInit() { | ||||||
|  |         // 实例化一个认证对象,入参需要传入腾讯云账户密钥对 secretId,secretKey | ||||||
|  |         Credential credential = new Credential(properties.getApiKey(), properties.getApiSecret()); | ||||||
|  |         client = new SmsClient(credential, ENDPOINT); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId, | ||||||
|  |                                                         String mobile, | ||||||
|  |                                                         String apiTemplateId, | ||||||
|  |                                                         List<KeyValue<String, Object>> templateParams) throws Throwable { | ||||||
|  |         return invoke(() -> buildSendSmsRequest(sendLogId, mobile, apiTemplateId, templateParams), | ||||||
|  |                 this::doSendSms0, | ||||||
|  |                 response -> { | ||||||
|  |                     SendStatus sendStatus = response.getSendStatusSet()[0]; | ||||||
|  |                     return SmsCommonResult.build(sendStatus.getCode(), sendStatus.getMessage(), response.getRequestId(), | ||||||
|  |                             new SmsSendRespDTO().setSerialNo(sendStatus.getSerialNo()), codeMapping); | ||||||
|  |                 }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 腾讯云发放短信的时候,需要额外的参数 sdkAppId。 | ||||||
|  |      * 考虑到不破坏原有的 apiKey + apiSecret 的结构,所以将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。 | ||||||
|  |      * 因此,这边需要使用 TencentSmsChannelProperties 做拆分,重新封装到 properties 内。 | ||||||
|  |      * | ||||||
|  |      * @param properties 数据库中存储的短信渠道配置 | ||||||
|  |      * @return TencentSmsChannelProperties | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     protected SmsChannelProperties prepareProperties(SmsChannelProperties properties) { | ||||||
|  |         return TencentSmsChannelProperties.build(properties); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 调用腾讯云 SDK 发送短信 | ||||||
|  |      * | ||||||
|  |      * @param request 发送短信请求 | ||||||
|  |      * @return 发送短信响应 | ||||||
|  |      * @throws TencentCloudSDKException SDK 用来封装发送短信失败 | ||||||
|  |      */ | ||||||
|  |     private SendSmsResponse doSendSms0(SendSmsRequest request) throws TencentCloudSDKException { | ||||||
|  |         return client.SendSms(request); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 封装腾讯云发送短信请求 | ||||||
|  |      * | ||||||
|  |      * @param sendLogId      日志编号 | ||||||
|  |      * @param mobile         手机号 | ||||||
|  |      * @param apiTemplateId  短信 API 的模板编号 | ||||||
|  |      * @param templateParams 短信模板参数。通过 List 数组,保证参数的顺序 | ||||||
|  |      * @return 腾讯云发送短信请求 | ||||||
|  |      */ | ||||||
|  |     private SendSmsRequest buildSendSmsRequest(Long sendLogId, | ||||||
|  |                                                String mobile, | ||||||
|  |                                                String apiTemplateId, | ||||||
|  |                                                List<KeyValue<String, Object>> templateParams) { | ||||||
|  |         SendSmsRequest request = new SendSmsRequest(); | ||||||
|  |         request.setSmsSdkAppId(((TencentSmsChannelProperties) properties).getSdkAppId()); | ||||||
|  |         request.setPhoneNumberSet(new String[]{mobile}); | ||||||
|  |         request.setSignName(properties.getSignature()); | ||||||
|  |         request.setTemplateId(apiTemplateId); | ||||||
|  |         request.setTemplateParamSet(ArrayUtils.toArray(templateParams, e -> String.valueOf(e.getValue()))); | ||||||
|  |         request.setSessionContext(JsonUtils.toJsonString(new SessionContext().setLogId(sendLogId))); | ||||||
|  |         return request; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable { | ||||||
|  |         List<SmsReceiveStatus> callback = JsonUtils.parseArray(text, SmsReceiveStatus.class); | ||||||
|  |         return CollectionUtils.convertList(callback, status -> { | ||||||
|  |             SmsReceiveRespDTO data = new SmsReceiveRespDTO(); | ||||||
|  |             data.setErrorCode(status.getErrCode()).setErrorMsg(status.getDescription()); | ||||||
|  |             data.setReceiveTime(status.getReceiveTime()).setSuccess(SmsReceiveStatus.SUCCESS_CODE.equalsIgnoreCase(status.getStatus())); | ||||||
|  |             data.setMobile(status.getMobile()).setSerialNo(status.getSerialNo()); | ||||||
|  |             SessionContext context; | ||||||
|  |             Long logId; | ||||||
|  |             Assert.notNull(context = status.getSessionContext(), "回执信息中未解析出 context,请联系腾讯云小助手"); | ||||||
|  |             Assert.notNull(logId = context.getLogId(), "回执信息中未解析出 logId,请联系腾讯云小助手"); | ||||||
|  |             data.setLogId(logId); | ||||||
|  |             return data; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     protected SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) throws Throwable { | ||||||
|  |         return invoke(() -> this.buildSmsTemplateStatusRequest(apiTemplateId), | ||||||
|  |                 this::doGetSmsTemplate0, | ||||||
|  |                 response -> { | ||||||
|  |                     SmsTemplateRespDTO data = convertTemplateStatusDTO(response.getDescribeTemplateStatusSet()[0]); | ||||||
|  |                     return SmsCommonResult.build(API_SUCCESS_CODE, null, response.getRequestId(), data, codeMapping); | ||||||
|  |                 }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @VisibleForTesting | ||||||
|  |     SmsTemplateRespDTO convertTemplateStatusDTO(DescribeTemplateListStatus templateStatus) { | ||||||
|  |         if (templateStatus == null) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |         SmsTemplateAuditStatusEnum auditStatus; | ||||||
|  |         Assert.notNull(templateStatus.getStatusCode(), | ||||||
|  |                 StrUtil.format("短信模版审核状态为 null,模版 id{}", templateStatus.getTemplateId())); | ||||||
|  |         switch (templateStatus.getStatusCode().intValue()) { | ||||||
|  |             case -1: | ||||||
|  |                 auditStatus = SmsTemplateAuditStatusEnum.FAIL; | ||||||
|  |                 break; | ||||||
|  |             case 0: | ||||||
|  |                 auditStatus = SmsTemplateAuditStatusEnum.SUCCESS; | ||||||
|  |                 break; | ||||||
|  |             case 1: | ||||||
|  |                 auditStatus = SmsTemplateAuditStatusEnum.CHECKING; | ||||||
|  |                 break; | ||||||
|  |             default: | ||||||
|  |                 throw new IllegalStateException(StrUtil.format("不能解析短信模版审核状态{},模版 id{}", | ||||||
|  |                         templateStatus.getStatusCode(), templateStatus.getTemplateId())); | ||||||
|  |         } | ||||||
|  |         SmsTemplateRespDTO data = new SmsTemplateRespDTO(); | ||||||
|  |         data.setId(String.valueOf(templateStatus.getTemplateId())).setContent(templateStatus.getTemplateContent()); | ||||||
|  |         data.setAuditStatus(auditStatus.getStatus()).setAuditReason(templateStatus.getReviewReply()); | ||||||
|  |         return data; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 封装查询模版审核状态请求 | ||||||
|  |      * @param apiTemplateId api 的模版 id | ||||||
|  |      * @return 查询模版审核状态请求 | ||||||
|  |      */ | ||||||
|  |     private DescribeSmsTemplateListRequest buildSmsTemplateStatusRequest(String apiTemplateId) { | ||||||
|  |         DescribeSmsTemplateListRequest request = new DescribeSmsTemplateListRequest(); | ||||||
|  |         request.setTemplateIdSet(new Long[]{Long.parseLong(apiTemplateId)}); | ||||||
|  |         // 地区 0:表示国内短信。1:表示国际/港澳台短信。 | ||||||
|  |         request.setInternational(INTERNATIONAL); | ||||||
|  |         return request; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 调用腾讯云 SDK 查询短信模版状态 | ||||||
|  |      * | ||||||
|  |      * @param request 查询短信模版状态请求 | ||||||
|  |      * @return 查询短信模版状态响应 | ||||||
|  |      * @throws TencentCloudSDKException SDK 用来封装查询短信模版状态失败 | ||||||
|  |      */ | ||||||
|  |     private DescribeSmsTemplateListResponse doGetSmsTemplate0(DescribeSmsTemplateListRequest request) throws TencentCloudSDKException { | ||||||
|  |         return client.DescribeSmsTemplateList(request); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     <Q, P, R> SmsCommonResult<R> invoke(Supplier<Q> requestSupplier, | ||||||
|  |                                         SdkFunction<Q, P> responseSupplier, | ||||||
|  |                                         Function<P, SmsCommonResult<R>> resultGen) { | ||||||
|  |         // 构建请求body | ||||||
|  |         Q request = requestSupplier.get(); | ||||||
|  |         P response; | ||||||
|  |         // 调用腾讯云发送短信 | ||||||
|  |         try { | ||||||
|  |             response = responseSupplier.apply(request); | ||||||
|  |         } catch (TencentCloudSDKException e) { | ||||||
|  |             // 调用异常,封装结果 | ||||||
|  |             return SmsCommonResult.build(e.getErrorCode(), e.getMessage(), e.getRequestId(), null, codeMapping); | ||||||
|  |         } | ||||||
|  |         return resultGen.apply(response); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Data | ||||||
|  |     private static class SmsReceiveStatus { | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * 短信接受成功 code | ||||||
|  |          */ | ||||||
|  |         public static final String SUCCESS_CODE = "SUCCESS"; | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * 用户实际接收到短信的时间 | ||||||
|  |          */ | ||||||
|  |         @JsonProperty("user_receive_time") | ||||||
|  |         @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) | ||||||
|  |         private Date receiveTime; | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * 国家(或地区)码 | ||||||
|  |          */ | ||||||
|  |         @JsonProperty("nationcode") | ||||||
|  |         private String nationCode; | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * 手机号码 | ||||||
|  |          */ | ||||||
|  |         private String mobile; | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * 实际是否收到短信接收状态,SUCCESS(成功)、FAIL(失败) | ||||||
|  |          */ | ||||||
|  |         @JsonProperty("report_status") | ||||||
|  |         private String status; | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * 用户接收短信状态码错误信息 | ||||||
|  |          */ | ||||||
|  |         @JsonProperty("errmsg") | ||||||
|  |         private String errCode; | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * 用户接收短信状态描述 | ||||||
|  |          */ | ||||||
|  |         @JsonProperty("description") | ||||||
|  |         private String description; | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * 本次发送标识 ID(与发送接口返回的SerialNo对应) | ||||||
|  |          */ | ||||||
|  |         @JsonProperty("sid") | ||||||
|  |         private String serialNo; | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * 用户的 session 内容(与发送接口的请求参数SessionContext一致) | ||||||
|  |          */ | ||||||
|  |         @JsonProperty("ext") | ||||||
|  |         private SessionContext sessionContext; | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @VisibleForTesting | ||||||
|  |     @Data | ||||||
|  |     static class SessionContext { | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * 发送短信记录id | ||||||
|  |          */ | ||||||
|  |         private Long logId; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private interface SdkFunction<T, R> { | ||||||
|  |         R apply(T t) throws TencentCloudSDKException; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,50 @@ | |||||||
|  | package cn.iocoder.yudao.framework.sms.core.client.impl.tencent; | ||||||
|  |  | ||||||
|  | import cn.iocoder.yudao.framework.common.exception.ErrorCode; | ||||||
|  | import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; | ||||||
|  | import cn.iocoder.yudao.framework.sms.core.client.SmsCodeMapping; | ||||||
|  | import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; | ||||||
|  |  | ||||||
|  | import static cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.*; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 腾讯云的 SmsCodeMapping 实现类 | ||||||
|  |  * | ||||||
|  |  * 参见 https://cloud.tencent.com/document/api/382/52075#.E5.85.AC.E5.85.B1.E9.94.99.E8.AF.AF.E7.A0.81 | ||||||
|  |  * | ||||||
|  |  * @author : shiwp | ||||||
|  |  */ | ||||||
|  | public class TencentSmsCodeMapping implements SmsCodeMapping { | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public ErrorCode apply(String apiCode) { | ||||||
|  |         switch (apiCode) { | ||||||
|  |             case TencentSmsClient.API_SUCCESS_CODE: return GlobalErrorCodeConstants.SUCCESS; | ||||||
|  |             case "FailedOperation.ContainSensitiveWord": return SMS_SEND_CONTENT_INVALID; | ||||||
|  |             case "FailedOperation.JsonParseFail": | ||||||
|  |             case "MissingParameter.EmptyPhoneNumberSet": | ||||||
|  |             case "LimitExceeded.PhoneNumberCountLimit": | ||||||
|  |             case "FailedOperation.FailResolvePacket": return GlobalErrorCodeConstants.BAD_REQUEST; | ||||||
|  |             case "FailedOperation.InsufficientBalanceInSmsPackage": return SMS_ACCOUNT_MONEY_NOT_ENOUGH; | ||||||
|  |             case "FailedOperation.MarketingSendTimeConstraint": return SMS_SEND_MARKET_LIMIT_CONTROL; | ||||||
|  |             case "FailedOperation.PhoneNumberInBlacklist": return SMS_MOBILE_BLACK; | ||||||
|  |             case "FailedOperation.SignatureIncorrectOrUnapproved": return SMS_SIGN_INVALID; | ||||||
|  |             case "FailedOperation.MissingTemplateToModify": | ||||||
|  |             case "FailedOperation.TemplateIncorrectOrUnapproved": return SMS_TEMPLATE_INVALID; | ||||||
|  |             case "InvalidParameterValue.IncorrectPhoneNumber": return SMS_MOBILE_INVALID; | ||||||
|  |             case "InvalidParameterValue.SdkAppIdNotExist": return SMS_APP_ID_INVALID; | ||||||
|  |             case "InvalidParameterValue.TemplateParameterLengthLimit": | ||||||
|  |             case "InvalidParameterValue.TemplateParameterFormatError": return SMS_TEMPLATE_PARAM_ERROR; | ||||||
|  |             case "LimitExceeded.PhoneNumberDailyLimit": return SMS_SEND_DAY_LIMIT_CONTROL; | ||||||
|  |             case "LimitExceeded.PhoneNumberThirtySecondLimit": | ||||||
|  |             case "LimitExceeded.PhoneNumberOneHourLimit": return SMS_SEND_BUSINESS_LIMIT_CONTROL; | ||||||
|  |             case "UnauthorizedOperation.RequestPermissionDeny": | ||||||
|  |             case "FailedOperation.ForbidAddMarketingTemplates": | ||||||
|  |             case "FailedOperation.NotEnterpriseCertification": | ||||||
|  |             case "UnauthorizedOperation.IndividualUserMarketingSmsPermissionDeny": return SMS_PERMISSION_DENY; | ||||||
|  |             case "UnauthorizedOperation.RequestIpNotInWhitelist": return SMS_IP_DENY; | ||||||
|  |             case "AuthFailure.SecretIdNotFound": return SMS_ACCOUNT_INVALID; | ||||||
|  |         } | ||||||
|  |         return SmsFrameworkErrorCodeConstants.SMS_UNKNOWN; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -35,7 +35,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE | |||||||
|  * 云片短信客户端的实现类 |  * 云片短信客户端的实现类 | ||||||
|  * |  * | ||||||
|  * @author zzf |  * @author zzf | ||||||
|  * @date 9:48 2021/3/5 |  * @since 9:48 2021/3/5 | ||||||
|  */ |  */ | ||||||
| @Slf4j | @Slf4j | ||||||
| public class YunpianSmsClient extends AbstractSmsClient { | public class YunpianSmsClient extends AbstractSmsClient { | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ import lombok.Getter; | |||||||
|  * 短信渠道枚举 |  * 短信渠道枚举 | ||||||
|  * |  * | ||||||
|  * @author zzf |  * @author zzf | ||||||
|  * @date 2021/1/25 10:56 |  * @since 2021/1/25 10:56 | ||||||
|  */ |  */ | ||||||
| @Getter | @Getter | ||||||
| @AllArgsConstructor | @AllArgsConstructor | ||||||
| @@ -17,7 +17,7 @@ public enum SmsChannelEnum { | |||||||
|     DEBUG_DING_TALK("DEBUG_DING_TALK", "调试(钉钉)"), |     DEBUG_DING_TALK("DEBUG_DING_TALK", "调试(钉钉)"), | ||||||
|     YUN_PIAN("YUN_PIAN", "云片"), |     YUN_PIAN("YUN_PIAN", "云片"), | ||||||
|     ALIYUN("ALIYUN", "阿里云"), |     ALIYUN("ALIYUN", "阿里云"), | ||||||
| //    TENCENT("TENCENT", "腾讯云"), |     TENCENT("TENCENT", "腾讯云"), | ||||||
| //    HUA_WEI("HUA_WEI", "华为云"), | //    HUA_WEI("HUA_WEI", "华为云"), | ||||||
|     ; |     ; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -26,6 +26,9 @@ public interface SmsFrameworkErrorCodeConstants { | |||||||
|  |  | ||||||
|     ErrorCode SMS_SEND_CONTENT_INVALID = new ErrorCode(2001000104, "短信内容有敏感词"); |     ErrorCode SMS_SEND_CONTENT_INVALID = new ErrorCode(2001000104, "短信内容有敏感词"); | ||||||
|  |  | ||||||
|  |     // 腾讯云:为避免骚扰用户,营销短信只允许在8点到22点发送。 | ||||||
|  |     ErrorCode SMS_SEND_MARKET_LIMIT_CONTROL = new ErrorCode(2001000105, "营销短信发送时间限制"); | ||||||
|  |  | ||||||
|     // ========== 模板相关 2001000200 ========== |     // ========== 模板相关 2001000200 ========== | ||||||
|     ErrorCode SMS_TEMPLATE_INVALID = new ErrorCode(2001000200, "短信模板不合法"); // 包括短信模板不存在 |     ErrorCode SMS_TEMPLATE_INVALID = new ErrorCode(2001000200, "短信模板不合法"); // 包括短信模板不存在 | ||||||
|     ErrorCode SMS_TEMPLATE_PARAM_ERROR = new ErrorCode(2001000201, "模板参数不正确"); |     ErrorCode SMS_TEMPLATE_PARAM_ERROR = new ErrorCode(2001000201, "模板参数不正确"); | ||||||
| @@ -41,6 +44,7 @@ public interface SmsFrameworkErrorCodeConstants { | |||||||
|     ErrorCode SMS_API_PARAM_ERROR = new ErrorCode(2001000900, "请求参数缺失"); |     ErrorCode SMS_API_PARAM_ERROR = new ErrorCode(2001000900, "请求参数缺失"); | ||||||
|     ErrorCode SMS_MOBILE_INVALID = new ErrorCode(2001000901, "手机格式不正确"); |     ErrorCode SMS_MOBILE_INVALID = new ErrorCode(2001000901, "手机格式不正确"); | ||||||
|     ErrorCode SMS_MOBILE_BLACK = new ErrorCode(2001000902, "手机号在黑名单中"); |     ErrorCode SMS_MOBILE_BLACK = new ErrorCode(2001000902, "手机号在黑名单中"); | ||||||
|  |     ErrorCode SMS_APP_ID_INVALID = new ErrorCode(2001000903, "SdkAppId不合法"); | ||||||
|  |  | ||||||
|     ErrorCode EXCEPTION = new ErrorCode(2001000999, "调用异常"); |     ErrorCode EXCEPTION = new ErrorCode(2001000999, "调用异常"); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ import javax.validation.constraints.NotNull; | |||||||
|  * 短信渠道配置类 |  * 短信渠道配置类 | ||||||
|  * |  * | ||||||
|  * @author zzf |  * @author zzf | ||||||
|  * @date 2021/1/25 17:01 |  * @since 2021/1/25 17:01 | ||||||
|  */ |  */ | ||||||
| @Data | @Data | ||||||
| @Validated | @Validated | ||||||
| @@ -40,9 +40,9 @@ public class SmsChannelProperties { | |||||||
|     @NotEmpty(message = "短信 API 的账号不能为空") |     @NotEmpty(message = "短信 API 的账号不能为空") | ||||||
|     private String apiKey; |     private String apiKey; | ||||||
|     /** |     /** | ||||||
|      * 短信 API 的秘钥 |      * 短信 API 的密钥 | ||||||
|      */ |      */ | ||||||
|     @NotEmpty(message = "短信 API 的秘钥不能为空") |     @NotEmpty(message = "短信 API 的密钥不能为空") | ||||||
|     private String apiSecret; |     private String apiSecret; | ||||||
|     /** |     /** | ||||||
|      * 短信发送回调 URL |      * 短信发送回调 URL | ||||||
|   | |||||||
| @@ -0,0 +1,222 @@ | |||||||
|  | package cn.iocoder.yudao.framework.sms.core.client.impl.tencent; | ||||||
|  |  | ||||||
|  | import cn.hutool.core.util.ReflectUtil; | ||||||
|  | import cn.hutool.core.util.StrUtil; | ||||||
|  | import cn.iocoder.yudao.framework.common.core.KeyValue; | ||||||
|  | import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; | ||||||
|  | import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; | ||||||
|  | import cn.iocoder.yudao.framework.common.util.collection.MapUtils; | ||||||
|  | import cn.iocoder.yudao.framework.common.util.date.DateUtils; | ||||||
|  | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||||||
|  | import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult; | ||||||
|  | import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO; | ||||||
|  | import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO; | ||||||
|  | import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO; | ||||||
|  | import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum; | ||||||
|  | import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; | ||||||
|  | import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; | ||||||
|  | import com.google.common.collect.Lists; | ||||||
|  | import com.tencentcloudapi.sms.v20210111.SmsClient; | ||||||
|  | import com.tencentcloudapi.sms.v20210111.models.DescribeSmsTemplateListResponse; | ||||||
|  | import com.tencentcloudapi.sms.v20210111.models.DescribeTemplateListStatus; | ||||||
|  | import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse; | ||||||
|  | import com.tencentcloudapi.sms.v20210111.models.SendStatus; | ||||||
|  | import org.junit.jupiter.api.Test; | ||||||
|  | import org.mockito.InjectMocks; | ||||||
|  | import org.mockito.Mock; | ||||||
|  |  | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
|  | import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; | ||||||
|  | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; | ||||||
|  | import static org.junit.jupiter.api.Assertions.*; | ||||||
|  | import static org.junit.jupiter.api.Assertions.assertThrows; | ||||||
|  | import static org.mockito.ArgumentMatchers.argThat; | ||||||
|  | import static org.mockito.Mockito.when; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * {@link TencentSmsClient} 的单元测试 | ||||||
|  |  * | ||||||
|  |  * @author shiwp | ||||||
|  |  */ | ||||||
|  | public class TencentSmsClientTest extends BaseMockitoUnitTest { | ||||||
|  |  | ||||||
|  |     private final SmsChannelProperties properties = new SmsChannelProperties() | ||||||
|  |             .setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey,避免构建报错 | ||||||
|  |             .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 | ||||||
|  |             .setSignature("芋道源码"); | ||||||
|  |  | ||||||
|  |     @InjectMocks | ||||||
|  |     private TencentSmsClient smsClient = new TencentSmsClient(properties); | ||||||
|  |  | ||||||
|  |     @Mock | ||||||
|  |     private SmsClient client; | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     public void testDoInit() { | ||||||
|  |         // 准备参数 | ||||||
|  |         // mock 方法 | ||||||
|  |  | ||||||
|  |         // 调用 | ||||||
|  |         smsClient.doInit(); | ||||||
|  |         // 断言 | ||||||
|  |         assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client")); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     public void testRefresh() { | ||||||
|  |         // 准备参数 | ||||||
|  |         SmsChannelProperties p = new SmsChannelProperties() | ||||||
|  |                 .setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey,避免构建报错 | ||||||
|  |                 .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 | ||||||
|  |                 .setSignature("芋道源码"); | ||||||
|  |         // 调用 | ||||||
|  |         smsClient.refresh(p); | ||||||
|  |         // 断言 | ||||||
|  |         assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client")); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     public void testDoSendSms() throws Throwable { | ||||||
|  |         // 准备参数 | ||||||
|  |         Long sendLogId = randomLongId(); | ||||||
|  |         String mobile = randomString(); | ||||||
|  |         String apiTemplateId = randomString(); | ||||||
|  |         List<KeyValue<String, Object>> templateParams = Lists.newArrayList( | ||||||
|  |                 new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); | ||||||
|  |         String requestId = randomString(); | ||||||
|  |         String serialNo = randomString(); | ||||||
|  |         // mock 方法 | ||||||
|  |         SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> { | ||||||
|  |             o.setRequestId(requestId); | ||||||
|  |             SendStatus[] sendStatuses = new SendStatus[1]; | ||||||
|  |             o.setSendStatusSet(sendStatuses); | ||||||
|  |             SendStatus sendStatus = new SendStatus(); | ||||||
|  |             sendStatuses[0] = sendStatus; | ||||||
|  |             sendStatus.setCode(TencentSmsClient.API_SUCCESS_CODE); | ||||||
|  |             sendStatus.setMessage("send success"); | ||||||
|  |             sendStatus.setSerialNo(serialNo); | ||||||
|  |         }); | ||||||
|  |         when(client.SendSms(argThat(request -> { | ||||||
|  |             assertEquals(mobile, request.getPhoneNumberSet()[0]); | ||||||
|  |             assertEquals(properties.getSignature(), request.getSignName()); | ||||||
|  |             assertEquals(apiTemplateId, request.getTemplateId()); | ||||||
|  |             assertEquals(toJsonString(ArrayUtils.toArray(new ArrayList<>(MapUtils.convertMap(templateParams).values()), String::valueOf)), | ||||||
|  |                     toJsonString(request.getTemplateParamSet())); | ||||||
|  |             assertEquals(sendLogId, ReflectUtil.getFieldValue(JsonUtils.parseObject(request.getSessionContext(), TencentSmsClient.SessionContext.class), "logId")); | ||||||
|  |             return true; | ||||||
|  |         }))).thenReturn(response); | ||||||
|  |  | ||||||
|  |         // 调用 | ||||||
|  |         SmsCommonResult<SmsSendRespDTO> result = smsClient.doSendSms(sendLogId, mobile, | ||||||
|  |                 apiTemplateId, templateParams); | ||||||
|  |         // 断言 | ||||||
|  |         assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode()); | ||||||
|  |         assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg()); | ||||||
|  |         assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); | ||||||
|  |         assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); | ||||||
|  |         assertEquals(response.getRequestId(), result.getApiRequestId()); | ||||||
|  |         // 断言结果 | ||||||
|  |         assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getData().getSerialNo()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     public void testDoTParseSmsReceiveStatus() throws Throwable { | ||||||
|  |         // 准备参数 | ||||||
|  |         String text = "[\n" + | ||||||
|  |                 "    {\n" + | ||||||
|  |                 "        \"user_receive_time\": \"2015-10-17 08:03:04\",\n" + | ||||||
|  |                 "        \"nationcode\": \"86\",\n" + | ||||||
|  |                 "        \"mobile\": \"13900000001\",\n" + | ||||||
|  |                 "        \"report_status\": \"SUCCESS\",\n" + | ||||||
|  |                 "        \"errmsg\": \"DELIVRD\",\n" + | ||||||
|  |                 "        \"description\": \"用户短信送达成功\",\n" + | ||||||
|  |                 "        \"sid\": \"12345\",\n" + | ||||||
|  |                 "        \"ext\": {\"logId\":\"67890\"}\n" + | ||||||
|  |                 "    }\n" + | ||||||
|  |                 "]"; | ||||||
|  |         // mock 方法 | ||||||
|  |  | ||||||
|  |         // 调用 | ||||||
|  |         List<SmsReceiveRespDTO> statuses = smsClient.doParseSmsReceiveStatus(text); | ||||||
|  |         // 断言 | ||||||
|  |         assertEquals(1, statuses.size()); | ||||||
|  |         assertTrue(statuses.get(0).getSuccess()); | ||||||
|  |         assertEquals("DELIVRD", statuses.get(0).getErrorCode()); | ||||||
|  |         assertEquals("用户短信送达成功", statuses.get(0).getErrorMsg()); | ||||||
|  |         assertEquals("13900000001", statuses.get(0).getMobile()); | ||||||
|  |         assertEquals(DateUtils.buildTime(2015, 10, 17, 8, 3, 4), statuses.get(0).getReceiveTime()); | ||||||
|  |         assertEquals("12345", statuses.get(0).getSerialNo()); | ||||||
|  |         assertEquals(67890L, statuses.get(0).getLogId()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     public void testDoGetSmsTemplate() throws Throwable { | ||||||
|  |         // 准备参数 | ||||||
|  |         Long apiTemplateId = randomLongId(); | ||||||
|  |         String requestId = randomString(); | ||||||
|  |  | ||||||
|  |         // mock 方法 | ||||||
|  |         DescribeSmsTemplateListResponse response = randomPojo(DescribeSmsTemplateListResponse.class, o -> { | ||||||
|  |             DescribeTemplateListStatus[] describeTemplateListStatuses = new DescribeTemplateListStatus[1]; | ||||||
|  |             DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus(); | ||||||
|  |             templateStatus.setTemplateId(apiTemplateId); | ||||||
|  |             templateStatus.setStatusCode(0L);// 设置模板通过 | ||||||
|  |             describeTemplateListStatuses[0] = templateStatus; | ||||||
|  |             o.setDescribeTemplateStatusSet(describeTemplateListStatuses); | ||||||
|  |             o.setRequestId(requestId); | ||||||
|  |         }); | ||||||
|  |         when(client.DescribeSmsTemplateList(argThat(request -> { | ||||||
|  |             assertEquals(apiTemplateId, request.getTemplateIdSet()[0]); | ||||||
|  |             return true; | ||||||
|  |         }))).thenReturn(response); | ||||||
|  |  | ||||||
|  |         // 调用 | ||||||
|  |         SmsCommonResult<SmsTemplateRespDTO> result = smsClient.doGetSmsTemplate(apiTemplateId.toString()); | ||||||
|  |         // 断言 | ||||||
|  |         assertEquals(TencentSmsClient.API_SUCCESS_CODE, result.getApiCode()); | ||||||
|  |         assertNull(result.getApiMsg()); | ||||||
|  |         assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); | ||||||
|  |         assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); | ||||||
|  |         assertEquals(response.getRequestId(), result.getApiRequestId()); | ||||||
|  |         // 断言结果 | ||||||
|  |         assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateId().toString(), result.getData().getId()); | ||||||
|  |         assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateContent(), result.getData().getContent()); | ||||||
|  |         assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus()); | ||||||
|  |         assertEquals(response.getDescribeTemplateStatusSet()[0].getReviewReply(), result.getData().getAuditReason()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     public void testConvertSuccessTemplateStatus() { | ||||||
|  |         testTemplateStatus(SmsTemplateAuditStatusEnum.SUCCESS, 0L); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     public void testConvertCheckingTemplateStatus() { | ||||||
|  |         testTemplateStatus(SmsTemplateAuditStatusEnum.CHECKING, 1L); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     public void testConvertFailTemplateStatus() { | ||||||
|  |         testTemplateStatus(SmsTemplateAuditStatusEnum.FAIL, -1L); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     public void testConvertUnknownTemplateStatus() { | ||||||
|  |         DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus(); | ||||||
|  |         templateStatus.setStatusCode(3L); | ||||||
|  |         Long templateId = randomLongId(); | ||||||
|  |         // 调用,并断言结果 | ||||||
|  |         assertThrows(IllegalStateException.class, () -> smsClient.convertTemplateStatusDTO(templateStatus), | ||||||
|  |                 StrUtil.format("不能解析短信模版审核状态[3],模版id[{}]", templateId)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void testTemplateStatus(SmsTemplateAuditStatusEnum expected, Long value) { | ||||||
|  |         DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus(); | ||||||
|  |         templateStatus.setStatusCode(value); | ||||||
|  |         SmsTemplateRespDTO result = smsClient.convertTemplateStatusDTO(templateStatus); | ||||||
|  |         assertEquals(expected.getStatus(), result.getAuditStatus()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,50 @@ | |||||||
|  | package cn.iocoder.yudao.framework.sms.core.client.impl.tencent; | ||||||
|  |  | ||||||
|  | import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; | ||||||
|  | import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; | ||||||
|  | import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; | ||||||
|  | import org.junit.jupiter.api.Test; | ||||||
|  | import org.mockito.InjectMocks; | ||||||
|  |  | ||||||
|  | import static org.junit.jupiter.api.Assertions.assertEquals; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * {@link TencentSmsCodeMapping} 的单元测试 | ||||||
|  |  * | ||||||
|  |  * @author : shiwp | ||||||
|  |  */ | ||||||
|  | public class TencentSmsCodeMappingTest extends BaseMockitoUnitTest { | ||||||
|  |  | ||||||
|  |     @InjectMocks | ||||||
|  |     private TencentSmsCodeMapping codeMapping; | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     public void testApply() { | ||||||
|  |         assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply(TencentSmsClient.API_SUCCESS_CODE)); | ||||||
|  |         assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID, codeMapping.apply("FailedOperation.ContainSensitiveWord")); | ||||||
|  |         assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("FailedOperation.JsonParseFail")); | ||||||
|  |         assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("MissingParameter.EmptyPhoneNumberSet")); | ||||||
|  |         assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("LimitExceeded.PhoneNumberCountLimit")); | ||||||
|  |         assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("FailedOperation.FailResolvePacket")); | ||||||
|  |         assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("FailedOperation.InsufficientBalanceInSmsPackage")); | ||||||
|  |         assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_MARKET_LIMIT_CONTROL, codeMapping.apply("FailedOperation.MarketingSendTimeConstraint")); | ||||||
|  |         assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_BLACK, codeMapping.apply("FailedOperation.PhoneNumberInBlacklist")); | ||||||
|  |         assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("FailedOperation.SignatureIncorrectOrUnapproved")); | ||||||
|  |         assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("FailedOperation.MissingTemplateToModify")); | ||||||
|  |         assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("FailedOperation.TemplateIncorrectOrUnapproved")); | ||||||
|  |         assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID, codeMapping.apply("InvalidParameterValue.IncorrectPhoneNumber")); | ||||||
|  |         assertEquals(SmsFrameworkErrorCodeConstants.SMS_APP_ID_INVALID, codeMapping.apply("InvalidParameterValue.SdkAppIdNotExist")); | ||||||
|  |         assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("InvalidParameterValue.TemplateParameterLengthLimit")); | ||||||
|  |         assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("InvalidParameterValue.TemplateParameterFormatError")); | ||||||
|  |         assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_DAY_LIMIT_CONTROL, codeMapping.apply("LimitExceeded.PhoneNumberDailyLimit")); | ||||||
|  |         assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("LimitExceeded.PhoneNumberThirtySecondLimit")); | ||||||
|  |         assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("LimitExceeded.PhoneNumberOneHourLimit")); | ||||||
|  |         assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("UnauthorizedOperation.RequestPermissionDeny")); | ||||||
|  |         assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("FailedOperation.ForbidAddMarketingTemplates")); | ||||||
|  |         assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("FailedOperation.NotEnterpriseCertification")); | ||||||
|  |         assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("UnauthorizedOperation.IndividualUserMarketingSmsPermissionDeny")); | ||||||
|  |         assertEquals(SmsFrameworkErrorCodeConstants.SMS_IP_DENY, codeMapping.apply("UnauthorizedOperation.RequestIpNotInWhitelist")); | ||||||
|  |         assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("AuthFailure.SecretIdNotFound")); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -75,7 +75,7 @@ public class TenantSecurityWebFilter extends ApiRequestFilter { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         //检查是否是忽略的 URL, 如果是则允许访问 |         // 如果非允许忽略租户的 URL,则校验租户是否合法 | ||||||
|         if (!isIgnoreUrl(request)) { |         if (!isIgnoreUrl(request)) { | ||||||
|             // 2. 如果请求未带租户的编号,不允许访问。 |             // 2. 如果请求未带租户的编号,不允许访问。 | ||||||
|             if (tenantId == null) { |             if (tenantId == null) { | ||||||
| @@ -92,6 +92,10 @@ public class TenantSecurityWebFilter extends ApiRequestFilter { | |||||||
|                 ServletUtils.writeJSON(response, result); |                 ServletUtils.writeJSON(response, result); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  |         } else { // 如果是允许忽略租户的 URL,若未传递租户编号,则默认忽略租户编号,避免报错 | ||||||
|  |             if (tenantId == null) { | ||||||
|  |                 TenantContextHolder.setIgnore(true); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // 继续过滤 |         // 继续过滤 | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> { | |||||||
|         String dir = StrUtil.removeSuffix(filePath, fileName); |         String dir = StrUtil.removeSuffix(filePath, fileName); | ||||||
|         boolean success = ftp.upload(dir, fileName, new ByteArrayInputStream(content)); |         boolean success = ftp.upload(dir, fileName, new ByteArrayInputStream(content)); | ||||||
|         if (!success) { |         if (!success) { | ||||||
|             throw new FtpException(StrUtil.format("上海文件到目标目录 ({}) 失败", filePath)); |             throw new FtpException(StrUtil.format("上传文件到目标目录 ({}) 失败", filePath)); | ||||||
|         } |         } | ||||||
|         // 拼接返回路径 |         // 拼接返回路径 | ||||||
|         return super.formatFileUrl(config.getDomain(), path); |         return super.formatFileUrl(config.getDomain(), path); | ||||||
|   | |||||||
| @@ -75,12 +75,20 @@ public interface BaseMapperX<T> extends BaseMapper<T> { | |||||||
|         return selectList(new LambdaQueryWrapper<T>().in(field, values)); |         return selectList(new LambdaQueryWrapper<T>().in(field, values)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * 逐条插入,适合少量数据插入,或者对性能要求不高的场景 | ||||||
|  |      * | ||||||
|  |      * 如果大量,请使用 {@link com.baomidou.mybatisplus.extension.service.impl.ServiceImpl#saveBatch(Collection)} 方法 | ||||||
|  |      * 使用示例,可见 RoleMenuBatchInsertMapper、UserRoleBatchInsertMapper 类 | ||||||
|  |      * | ||||||
|  |      * @param entities 实体们 | ||||||
|  |      */ | ||||||
|     default void insertBatch(Collection<T> entities) { |     default void insertBatch(Collection<T> entities) { | ||||||
|         // TODO 芋艿:修改成支持批量的 |  | ||||||
|         entities.forEach(this::insert); |         entities.forEach(this::insert); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     default void updateBatch(T update) { |     default void updateBatch(T update) { | ||||||
|         update(update, new QueryWrapper<>()); |         update(update, new QueryWrapper<>()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,58 @@ | |||||||
|  | package cn.iocoder.yudao.framework.mybatis.core.type; | ||||||
|  |  | ||||||
|  | import cn.hutool.core.collection.CollUtil; | ||||||
|  | import cn.hutool.core.util.StrUtil; | ||||||
|  | import org.apache.ibatis.type.JdbcType; | ||||||
|  | import org.apache.ibatis.type.MappedJdbcTypes; | ||||||
|  | import org.apache.ibatis.type.MappedTypes; | ||||||
|  | import org.apache.ibatis.type.TypeHandler; | ||||||
|  |  | ||||||
|  | import java.sql.CallableStatement; | ||||||
|  | import java.sql.PreparedStatement; | ||||||
|  | import java.sql.ResultSet; | ||||||
|  | import java.sql.SQLException; | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * List<String> 的类型转换器实现类,对应数据库的 varchar 类型 | ||||||
|  |  * | ||||||
|  |  * @author 永不言败 | ||||||
|  |  * @since 2022 3/23 12:50:15 | ||||||
|  |  */ | ||||||
|  | @MappedJdbcTypes(JdbcType.VARCHAR) | ||||||
|  | @MappedTypes(List.class) | ||||||
|  | public class StringLiSTTypeHandler implements TypeHandler<List<String>> { | ||||||
|  |  | ||||||
|  |     private static final String COMMA = ","; | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void setParameter(PreparedStatement ps, int i, List<String> strings, JdbcType jdbcType) throws SQLException { | ||||||
|  |         // 设置占位符 | ||||||
|  |         ps.setString(i, CollUtil.join(strings, COMMA)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public List<String> getResult(ResultSet rs, String columnName) throws SQLException { | ||||||
|  |         String value = rs.getString(columnName); | ||||||
|  |         return getResult(value); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public List<String> getResult(ResultSet rs, int columnIndex) throws SQLException { | ||||||
|  |         String value = rs.getString(columnIndex); | ||||||
|  |         return getResult(value); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public List<String> getResult(CallableStatement cs, int columnIndex) throws SQLException { | ||||||
|  |         String value = cs.getString(columnIndex); | ||||||
|  |         return getResult(value); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private List<String> getResult(String value) { | ||||||
|  |         if (value == null) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |         return StrUtil.splitTrim(value, COMMA); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -26,6 +26,12 @@ | |||||||
|             <groupId>org.redisson</groupId> |             <groupId>org.redisson</groupId> | ||||||
|             <artifactId>redisson-spring-boot-starter</artifactId> |             <artifactId>redisson-spring-boot-starter</artifactId> | ||||||
|         </dependency> |         </dependency> | ||||||
|  |  | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>org.springframework.boot</groupId> | ||||||
|  |             <artifactId>spring-boot-starter-cache</artifactId> <!-- 实现对 Caches 的自动化配置 --> | ||||||
|  |         </dependency> | ||||||
|  |  | ||||||
|     </dependencies> |     </dependencies> | ||||||
|  |  | ||||||
| </project> | </project> | ||||||
|   | |||||||
| @@ -0,0 +1,48 @@ | |||||||
|  | package cn.iocoder.yudao.framework.redis.config; | ||||||
|  |  | ||||||
|  | import org.springframework.boot.autoconfigure.cache.CacheProperties; | ||||||
|  | import org.springframework.cache.annotation.EnableCaching; | ||||||
|  | import org.springframework.context.annotation.Bean; | ||||||
|  | import org.springframework.context.annotation.Configuration; | ||||||
|  | import org.springframework.context.annotation.Primary; | ||||||
|  | import org.springframework.data.redis.cache.RedisCacheConfiguration; | ||||||
|  | import org.springframework.data.redis.serializer.RedisSerializationContext; | ||||||
|  | import org.springframework.data.redis.serializer.RedisSerializer; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Cache 配置类,基于 Redis 实现 | ||||||
|  |  */ | ||||||
|  | @Configuration | ||||||
|  | @EnableCaching | ||||||
|  | public class YudaoCacheAutoConfiguration { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * RedisCacheConfiguration Bean | ||||||
|  |      * | ||||||
|  |      * 参考 org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration 的 createConfiguration 方法 | ||||||
|  |      */ | ||||||
|  |     @Bean | ||||||
|  |     @Primary | ||||||
|  |     public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { | ||||||
|  |         // 设置使用 JSON 序列化方式 | ||||||
|  |         RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); | ||||||
|  |         config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json())); | ||||||
|  |  | ||||||
|  |         // 设置 CacheProperties.Redis 的属性 | ||||||
|  |         CacheProperties.Redis redisProperties = cacheProperties.getRedis(); | ||||||
|  |         if (redisProperties.getTimeToLive() != null) { | ||||||
|  |             config = config.entryTtl(redisProperties.getTimeToLive()); | ||||||
|  |         } | ||||||
|  |         if (redisProperties.getKeyPrefix() != null) { | ||||||
|  |             config = config.prefixCacheNameWith(redisProperties.getKeyPrefix()); | ||||||
|  |         } | ||||||
|  |         if (!redisProperties.isCacheNullValues()) { | ||||||
|  |             config = config.disableCachingNullValues(); | ||||||
|  |         } | ||||||
|  |         if (!redisProperties.isUseKeyPrefix()) { | ||||||
|  |             config = config.disableKeyPrefix(); | ||||||
|  |         } | ||||||
|  |         return config; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -1,6 +1,5 @@ | |||||||
| package cn.iocoder.yudao.framework.redis.config; | package cn.iocoder.yudao.framework.redis.config; | ||||||
|  |  | ||||||
| import lombok.extern.slf4j.Slf4j; |  | ||||||
| import org.springframework.context.annotation.Bean; | import org.springframework.context.annotation.Bean; | ||||||
| import org.springframework.context.annotation.Configuration; | import org.springframework.context.annotation.Configuration; | ||||||
| import org.springframework.data.redis.connection.RedisConnectionFactory; | import org.springframework.data.redis.connection.RedisConnectionFactory; | ||||||
| @@ -11,7 +10,6 @@ import org.springframework.data.redis.serializer.RedisSerializer; | |||||||
|  * Redis 配置类 |  * Redis 配置类 | ||||||
|  */ |  */ | ||||||
| @Configuration | @Configuration | ||||||
| @Slf4j |  | ||||||
| public class YudaoRedisAutoConfiguration { | public class YudaoRedisAutoConfiguration { | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|   | |||||||
| @@ -1,2 +1,3 @@ | |||||||
| org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ | ||||||
|   cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration |   cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration,\ | ||||||
|  |   cn.iocoder.yudao.framework.redis.config.YudaoCacheAutoConfiguration | ||||||
|   | |||||||
| @@ -0,0 +1 @@ | |||||||
|  | <http://www.iocoder.cn/Spring-Boot/Cache/?yudao> | ||||||
| @@ -37,10 +37,10 @@ public class SecurityProperties { | |||||||
|     @NotNull(message = "mock 模式的开关不能为空") |     @NotNull(message = "mock 模式的开关不能为空") | ||||||
|     private Boolean mockEnable; |     private Boolean mockEnable; | ||||||
|     /** |     /** | ||||||
|      * mock 模式的秘钥 |      * mock 模式的密钥 | ||||||
|      * 一定要配置秘钥,保证安全性 |      * 一定要配置密钥,保证安全性 | ||||||
|      */ |      */ | ||||||
|     @NotEmpty(message = "mock 模式的秘钥不能为空") // 这里设置了一个默认值,因为实际上只有 mockEnable 为 true 时才需要配置。 |     @NotEmpty(message = "mock 模式的密钥不能为空") // 这里设置了一个默认值,因为实际上只有 mockEnable 为 true 时才需要配置。 | ||||||
|     private String mockSecret = "yudaoyuanma"; |     private String mockSecret = "yudaoyuanma"; | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,7 +6,6 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | |||||||
| import com.fasterxml.jackson.annotation.JsonIgnore; | import com.fasterxml.jackson.annotation.JsonIgnore; | ||||||
| import lombok.Data; | import lombok.Data; | ||||||
| import org.springframework.security.core.GrantedAuthority; | import org.springframework.security.core.GrantedAuthority; | ||||||
| import org.springframework.security.core.authority.SimpleGrantedAuthority; |  | ||||||
| import org.springframework.security.core.userdetails.UserDetails; | import org.springframework.security.core.userdetails.UserDetails; | ||||||
|  |  | ||||||
| import java.util.*; | import java.util.*; | ||||||
| @@ -61,10 +60,6 @@ public class LoginUser implements UserDetails { | |||||||
|      * 部门编号 |      * 部门编号 | ||||||
|      */ |      */ | ||||||
|     private Long deptId; |     private Long deptId; | ||||||
|     /** |  | ||||||
|      * 所属岗位 |  | ||||||
|      */ |  | ||||||
|     private Set<Long> postIds; |  | ||||||
|  |  | ||||||
|     // ========== 上下文 ========== |     // ========== 上下文 ========== | ||||||
|     /** |     /** | ||||||
|   | |||||||
| @@ -21,6 +21,17 @@ | |||||||
|             <artifactId>yudao-common</artifactId> |             <artifactId>yudao-common</artifactId> | ||||||
|         </dependency> |         </dependency> | ||||||
|  |  | ||||||
|  |         <!-- DB 相关 --> | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>cn.iocoder.boot</groupId> | ||||||
|  |             <artifactId>yudao-spring-boot-starter-mybatis</artifactId> | ||||||
|  |         </dependency> | ||||||
|  |  | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>cn.iocoder.boot</groupId> | ||||||
|  |             <artifactId>yudao-spring-boot-starter-redis</artifactId> | ||||||
|  |         </dependency> | ||||||
|  |  | ||||||
|         <!-- Test 测试相关 --> |         <!-- Test 测试相关 --> | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>org.mockito</groupId> |             <groupId>org.mockito</groupId> | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| package cn.iocoder.yudao.module.pay.test; | package cn.iocoder.yudao.framework.test.core.ut; | ||||||
| 
 | 
 | ||||||
| import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration; | import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration; | ||||||
| import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration; | import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration; | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| package cn.iocoder.yudao.module.bpm.test; | package cn.iocoder.yudao.framework.test.core.ut; | ||||||
| 
 | 
 | ||||||
| import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration; | import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration; | ||||||
| import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration; | import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration; | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| package cn.iocoder.yudao.module.system.test; | package cn.iocoder.yudao.framework.test.core.ut; | ||||||
| 
 | 
 | ||||||
| import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; | import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; | ||||||
| import cn.iocoder.yudao.framework.test.config.RedisTestConfiguration; | import cn.iocoder.yudao.framework.test.config.RedisTestConfiguration; | ||||||
| @@ -2,21 +2,18 @@ package cn.iocoder.yudao.framework.apilog.core.filter; | |||||||
|  |  | ||||||
| import cn.hutool.core.exceptions.ExceptionUtil; | import cn.hutool.core.exceptions.ExceptionUtil; | ||||||
| import cn.hutool.core.map.MapUtil; | import cn.hutool.core.map.MapUtil; | ||||||
| import cn.hutool.core.util.StrUtil; |  | ||||||
| import cn.hutool.extra.servlet.ServletUtil; | import cn.hutool.extra.servlet.ServletUtil; | ||||||
| import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; |  | ||||||
| import cn.iocoder.yudao.framework.common.pojo.CommonResult; |  | ||||||
| import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService; | import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService; | ||||||
| import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateReqDTO; | import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateReqDTO; | ||||||
| import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; | import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; | ||||||
| import cn.iocoder.yudao.framework.web.config.WebProperties; | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||||||
| import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; |  | ||||||
| import cn.iocoder.yudao.framework.common.util.date.DateUtils; | import cn.iocoder.yudao.framework.common.util.date.DateUtils; | ||||||
| import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; | ||||||
| import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; | import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; | ||||||
| import lombok.RequiredArgsConstructor; | import cn.iocoder.yudao.framework.web.config.WebProperties; | ||||||
|  | import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter; | ||||||
|  | import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; | ||||||
| import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||||
| import org.springframework.web.filter.OncePerRequestFilter; |  | ||||||
|  |  | ||||||
| import javax.servlet.FilterChain; | import javax.servlet.FilterChain; | ||||||
| import javax.servlet.ServletException; | import javax.servlet.ServletException; | ||||||
| @@ -26,27 +23,24 @@ import java.io.IOException; | |||||||
| import java.util.Date; | import java.util.Date; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
|  |  | ||||||
| import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.*; | import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * API 访问日志 Filter |  * API 访问日志 Filter | ||||||
|  * |  * | ||||||
|  * @author 芋道源码 |  * @author 芋道源码 | ||||||
|  */ |  */ | ||||||
| @RequiredArgsConstructor |  | ||||||
| @Slf4j | @Slf4j | ||||||
| public class ApiAccessLogFilter extends OncePerRequestFilter { | public class ApiAccessLogFilter extends ApiRequestFilter { | ||||||
|  |  | ||||||
|     private final WebProperties webProperties; |  | ||||||
|     private final String applicationName; |     private final String applicationName; | ||||||
|  |  | ||||||
|     private final ApiAccessLogFrameworkService apiAccessLogFrameworkService; |     private final ApiAccessLogFrameworkService apiAccessLogFrameworkService; | ||||||
|  |  | ||||||
|     @Override |     public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogFrameworkService apiAccessLogFrameworkService) { | ||||||
|     protected boolean shouldNotFilter(HttpServletRequest request) { |         super(webProperties); | ||||||
|         // 只过滤 API 请求的地址 |         this.applicationName = applicationName; | ||||||
|         return !StrUtil.startWithAny(request.getRequestURI(), webProperties.getAppApi().getPrefix(), |         this.apiAccessLogFrameworkService = apiAccessLogFrameworkService; | ||||||
|                 webProperties.getAppApi().getPrefix()); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|   | |||||||
| @@ -11,8 +11,8 @@ | |||||||
|     <modules> |     <modules> | ||||||
|         <module>yudao-module-bpm-api</module> |         <module>yudao-module-bpm-api</module> | ||||||
|         <module>yudao-module-bpm-base</module> |         <module>yudao-module-bpm-base</module> | ||||||
|         <module>yudao-module-bpm-impl-flowable</module> |         <module>yudao-module-bpm-biz-flowable</module> | ||||||
|         <module>yudao-module-bpm-impl-activiti</module> |         <module>yudao-module-bpm-biz-activiti</module> | ||||||
|     </modules> |     </modules> | ||||||
|     <artifactId>yudao-module-bpm</artifactId> |     <artifactId>yudao-module-bpm</artifactId> | ||||||
|     <packaging>pom</packaging> |     <packaging>pom</packaging> | ||||||
| @@ -24,9 +24,9 @@ | |||||||
|         bpm 解释:https://baike.baidu.com/item/BPM/1933 |         bpm 解释:https://baike.baidu.com/item/BPM/1933 | ||||||
|  |  | ||||||
|         目前提供两套实现方案: |         目前提供两套实现方案: | ||||||
|             1. 基于 Activiti 7 实现的 yudao-module-bpm-impl-activiti |             1. 基于 Activiti 7 实现的 yudao-module-bpm-biz-activiti | ||||||
|             2. 基于 Flowable 6 实现的 yudao-module-bpm-impl-flowable |             2. 基于 Flowable 6 实现的 yudao-module-bpm-biz-flowable | ||||||
|         两套实现会存在共享的逻辑,所以会继承 yudao-module-impl-base |         两套实现会存在共享的逻辑,所以会继承 yudao-module-bpm-base | ||||||
|     </description> |     </description> | ||||||
|  |  | ||||||
| </project> | </project> | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| package cn.iocoder.yudao.module.bpm.dal.mysql.definition; | package cn.iocoder.yudao.module.bpm.dal.mysql.definition; | ||||||
|  |  | ||||||
|  | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; | ||||||
| import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO; | import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO; | ||||||
| import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | ||||||
| import org.apache.ibatis.annotations.Mapper; | import org.apache.ibatis.annotations.Mapper; | ||||||
|   | |||||||
| @@ -1,15 +1,15 @@ | |||||||
| package cn.iocoder.yudao.module.bpm.service.definition; | package cn.iocoder.yudao.module.bpm.service.definition; | ||||||
|  |  | ||||||
| import cn.hutool.core.util.RandomUtil; | import cn.hutool.core.util.RandomUtil; | ||||||
| import cn.iocoder.yudao.module.bpm.test.BaseDbUnitTest; | import cn.iocoder.yudao.framework.common.pojo.PageResult; | ||||||
|  | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||||||
|  | import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; | ||||||
| import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormCreateReqVO; | import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormCreateReqVO; | ||||||
| import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormPageReqVO; | import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormPageReqVO; | ||||||
| import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormUpdateReqVO; | import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormUpdateReqVO; | ||||||
| import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; | import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; | ||||||
| import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmFormMapper; | import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmFormMapper; | ||||||
| import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmFormFieldRespDTO; | import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmFormFieldRespDTO; | ||||||
| import cn.iocoder.yudao.framework.common.pojo.PageResult; |  | ||||||
| import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |  | ||||||
| import org.junit.jupiter.api.Test; | import org.junit.jupiter.api.Test; | ||||||
| import org.springframework.context.annotation.Import; | import org.springframework.context.annotation.Import; | ||||||
|  |  | ||||||
| @@ -18,12 +18,12 @@ import java.util.List; | |||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
| import java.util.stream.Stream; | import java.util.stream.Stream; | ||||||
|  |  | ||||||
| import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.FORM_NOT_EXISTS; |  | ||||||
| import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; | import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; | ||||||
| import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; | import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals; | ||||||
| import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; | import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; | ||||||
| import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; | ||||||
| import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; | ||||||
|  | import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.FORM_NOT_EXISTS; | ||||||
| import static org.junit.jupiter.api.Assertions.*; | import static org.junit.jupiter.api.Assertions.*; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|   | |||||||
| @@ -1,24 +1,24 @@ | |||||||
| package cn.iocoder.yudao.module.bpm.service.definition; | package cn.iocoder.yudao.module.bpm.service.definition; | ||||||
|  |  | ||||||
| import cn.iocoder.yudao.module.bpm.test.BaseDbUnitTest; | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | ||||||
|  | import cn.iocoder.yudao.framework.common.pojo.PageResult; | ||||||
|  | import cn.iocoder.yudao.framework.common.util.date.DateUtils; | ||||||
|  | import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; | ||||||
|  | import cn.iocoder.yudao.framework.test.core.util.AssertUtils; | ||||||
|  | import cn.iocoder.yudao.framework.test.core.util.RandomUtils; | ||||||
| import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group.BpmUserGroupCreateReqVO; | import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group.BpmUserGroupCreateReqVO; | ||||||
| import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group.BpmUserGroupPageReqVO; | import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group.BpmUserGroupPageReqVO; | ||||||
| import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group.BpmUserGroupUpdateReqVO; | import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group.BpmUserGroupUpdateReqVO; | ||||||
| import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO; | import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO; | ||||||
| import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmUserGroupMapper; | import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmUserGroupMapper; | ||||||
| import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |  | ||||||
| import cn.iocoder.yudao.framework.common.pojo.PageResult; |  | ||||||
| import cn.iocoder.yudao.framework.common.util.date.DateUtils; |  | ||||||
| import cn.iocoder.yudao.framework.test.core.util.AssertUtils; |  | ||||||
| import cn.iocoder.yudao.framework.test.core.util.RandomUtils; |  | ||||||
| import org.junit.jupiter.api.Assertions; | import org.junit.jupiter.api.Assertions; | ||||||
| import org.junit.jupiter.api.Test; | import org.junit.jupiter.api.Test; | ||||||
| import org.springframework.context.annotation.Import; | import org.springframework.context.annotation.Import; | ||||||
|  |  | ||||||
| import javax.annotation.Resource; | import javax.annotation.Resource; | ||||||
|  |  | ||||||
| import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.USER_GROUP_NOT_EXISTS; |  | ||||||
| import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; | import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; | ||||||
|  | import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.USER_GROUP_NOT_EXISTS; | ||||||
|  |  | ||||||
| /** | /** | ||||||
| * {@link BpmUserGroupServiceImpl} 的单元测试类 | * {@link BpmUserGroupServiceImpl} 的单元测试类 | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ | |||||||
|         <version>${revision}</version> |         <version>${revision}</version> | ||||||
|     </parent> |     </parent> | ||||||
|     <modelVersion>4.0.0</modelVersion> |     <modelVersion>4.0.0</modelVersion> | ||||||
|     <artifactId>yudao-module-bpm-impl-activiti</artifactId> |     <artifactId>yudao-module-bpm-biz-activiti</artifactId> | ||||||
|     <packaging>jar</packaging> |     <packaging>jar</packaging> | ||||||
| 
 | 
 | ||||||
|     <name>${project.artifactId}</name> |     <name>${project.artifactId}</name> | ||||||
| @@ -215,6 +215,9 @@ public class BpmModelServiceImpl  implements BpmModelService { | |||||||
|         if (oldDefinition == null) { |         if (oldDefinition == null) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |         if(oldDefinition.isSuspended()) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|         processDefinitionService.updateProcessDefinitionState(oldDefinition.getId(), SuspensionState.SUSPENDED.getStateCode()); |         processDefinitionService.updateProcessDefinitionState(oldDefinition.getId(), SuspensionState.SUSPENDED.getStateCode()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @@ -103,6 +103,9 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ | |||||||
|         } |         } | ||||||
|         // 执行查询 |         // 执行查询 | ||||||
|         List<ProcessDefinition> processDefinitions = definitionQuery.list(); |         List<ProcessDefinition> processDefinitions = definitionQuery.list(); | ||||||
|  |         if (CollUtil.isEmpty(processDefinitions)) { | ||||||
|  |             return Collections.emptyList(); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         // 获得 BpmProcessDefinitionDO Map |         // 获得 BpmProcessDefinitionDO Map | ||||||
|         List<BpmProcessDefinitionExtDO> processDefinitionDOs = processDefinitionMapper.selectListByProcessDefinitionIds( |         List<BpmProcessDefinitionExtDO> processDefinitionDOs = processDefinitionMapper.selectListByProcessDefinitionIds( | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 Hobo
					Hobo