mirror of
https://gitee.com/huangge1199_admin/vue-pro.git
synced 2025-01-19 03:30:06 +08:00
Merge remote-tracking branch 'origin/master' into feature/bpm-back
This commit is contained in:
commit
4015724417
4
Jenkinsfile
vendored
4
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/'
|
||||||
}
|
}
|
||||||
@ -57,4 +57,4 @@ pipeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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>
|
||||||
@ -25,8 +25,9 @@
|
|||||||
<mysql.version>5.1.46</mysql.version>
|
<mysql.version>5.1.46</mysql.version>
|
||||||
<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>
|
||||||
|
<mybatis-plus-generator.version>3.5.2</mybatis-plus-generator.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 定时任务相关 -->
|
||||||
@ -45,6 +46,7 @@
|
|||||||
<activiti.version>7.1.0.M6</activiti.version>
|
<activiti.version>7.1.0.M6</activiti.version>
|
||||||
<flowable.version>6.7.0</flowable.version>
|
<flowable.version>6.7.0</flowable.version>
|
||||||
<!-- 工具类相关 -->
|
<!-- 工具类相关 -->
|
||||||
|
<jasypt-spring-boot-starter.version>3.0.4</jasypt-spring-boot-starter.version>
|
||||||
<lombok.version>1.18.20</lombok.version>
|
<lombok.version>1.18.20</lombok.version>
|
||||||
<mapstruct.version>1.4.1.Final</mapstruct.version>
|
<mapstruct.version>1.4.1.Final</mapstruct.version>
|
||||||
<hutool.version>5.6.1</hutool.version>
|
<hutool.version>5.6.1</hutool.version>
|
||||||
@ -60,6 +62,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>
|
||||||
@ -191,6 +194,11 @@
|
|||||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||||
<version>${mybatis-plus.version}</version>
|
<version>${mybatis-plus.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-generator</artifactId> <!-- 代码生成器,使用它解析表结构 -->
|
||||||
|
<version>${mybatis-plus-generator.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.baomidou</groupId>
|
<groupId>com.baomidou</groupId>
|
||||||
<artifactId>dynamic-datasource-spring-boot-starter</artifactId> <!-- 多数据源 -->
|
<artifactId>dynamic-datasource-spring-boot-starter</artifactId> <!-- 多数据源 -->
|
||||||
@ -427,6 +435,12 @@
|
|||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ulisesbocchio</groupId>
|
||||||
|
<artifactId>jasypt-spring-boot-starter</artifactId> <!-- 加解密 -->
|
||||||
|
<version>${jasypt-spring-boot-starter.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.iocoder.boot</groupId>
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
<artifactId>yudao-spring-boot-starter-excel</artifactId>
|
<artifactId>yudao-spring-boot-starter-excel</artifactId>
|
||||||
@ -552,6 +566,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 = ?
|
||||||
|
@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
|
|||||||
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
|
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayQrPayClient;
|
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayQrPayClient;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayWapPayClient;
|
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayWapPayClient;
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXNativePayClient;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPayClientConfig;
|
import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPayClientConfig;
|
||||||
import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPubPayClient;
|
import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPubPayClient;
|
||||||
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
|
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
|
||||||
@ -63,6 +64,7 @@ public class PayClientFactoryImpl implements PayClientFactory {
|
|||||||
case WX_PUB: return (AbstractPayClient<Config>) new WXPubPayClient(channelId, (WXPayClientConfig) config);
|
case WX_PUB: return (AbstractPayClient<Config>) new WXPubPayClient(channelId, (WXPayClientConfig) config);
|
||||||
case WX_LITE: return (AbstractPayClient<Config>) new WXPubPayClient(channelId, (WXPayClientConfig) config);
|
case WX_LITE: return (AbstractPayClient<Config>) new WXPubPayClient(channelId, (WXPayClientConfig) config);
|
||||||
case WX_APP: return (AbstractPayClient<Config>) new WXPubPayClient(channelId, (WXPayClientConfig) config);
|
case WX_APP: return (AbstractPayClient<Config>) new WXPubPayClient(channelId, (WXPayClientConfig) config);
|
||||||
|
case WX_NATIVE: return (AbstractPayClient<Config>) new WXNativePayClient(channelId, (WXPayClientConfig) config);
|
||||||
case ALIPAY_WAP: return (AbstractPayClient<Config>) new AlipayWapPayClient(channelId, (AlipayPayClientConfig) config);
|
case ALIPAY_WAP: return (AbstractPayClient<Config>) new AlipayWapPayClient(channelId, (AlipayPayClientConfig) config);
|
||||||
case ALIPAY_QR: return (AbstractPayClient<Config>) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config);
|
case ALIPAY_QR: return (AbstractPayClient<Config>) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config);
|
||||||
case ALIPAY_APP: return (AbstractPayClient<Config>) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config);
|
case ALIPAY_APP: return (AbstractPayClient<Config>) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config);
|
||||||
|
@ -18,6 +18,7 @@ import lombok.SneakyThrows;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||||
@ -54,7 +55,8 @@ public abstract class AbstractAlipayClient extends AbstractPayClient<AlipayPayCl
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception {
|
public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws Exception {
|
||||||
Map<String, String> params = data.getParams();
|
Map<String, String> params = strToMap(data.getBody());
|
||||||
|
|
||||||
return PayOrderNotifyRespDTO.builder().orderExtensionNo(params.get("out_trade_no"))
|
return PayOrderNotifyRespDTO.builder().orderExtensionNo(params.get("out_trade_no"))
|
||||||
.channelOrderNo(params.get("trade_no")).channelUserId(params.get("seller_id"))
|
.channelOrderNo(params.get("trade_no")).channelUserId(params.get("seller_id"))
|
||||||
.tradeStatus(params.get("trade_status"))
|
.tradeStatus(params.get("trade_status"))
|
||||||
@ -64,7 +66,7 @@ public abstract class AbstractAlipayClient extends AbstractPayClient<AlipayPayCl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
|
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
|
||||||
Map<String, String> params = notifyData.getParams();
|
Map<String, String> params = strToMap(notifyData.getBody());
|
||||||
PayRefundNotifyDTO notifyDTO = PayRefundNotifyDTO.builder().channelOrderNo(params.get("trade_no"))
|
PayRefundNotifyDTO notifyDTO = PayRefundNotifyDTO.builder().channelOrderNo(params.get("trade_no"))
|
||||||
.tradeNo(params.get("out_trade_no"))
|
.tradeNo(params.get("out_trade_no"))
|
||||||
.reqNo(params.get("out_biz_no"))
|
.reqNo(params.get("out_biz_no"))
|
||||||
@ -128,4 +130,17 @@ public abstract class AbstractAlipayClient extends AbstractPayClient<AlipayPayCl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Map<String, String> strToMap(String s) {
|
||||||
|
Map<String, String> stringStringMap = new HashMap<>();
|
||||||
|
//调整时间格式
|
||||||
|
String s3 = s.replaceAll("%3A", ":");
|
||||||
|
//获取map
|
||||||
|
String s4 = s3.replace("+", " ");
|
||||||
|
String[] split = s4.split("&");
|
||||||
|
for (String s1 : split) {
|
||||||
|
String[] split1 = s1.split("=");
|
||||||
|
stringStringMap.put(split1[0], split1[1]);
|
||||||
|
}
|
||||||
|
return stringStringMap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,141 @@
|
|||||||
|
package cn.iocoder.yudao.framework.pay.core.client.impl.wx;
|
||||||
|
|
||||||
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import cn.hutool.core.lang.Assert;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.io.FileUtils;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.client.dto.*;
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
|
||||||
|
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
|
||||||
|
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
|
||||||
|
import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult;
|
||||||
|
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
|
||||||
|
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
|
||||||
|
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
|
||||||
|
import com.github.binarywang.wxpay.config.WxPayConfig;
|
||||||
|
import com.github.binarywang.wxpay.constant.WxPayConstants;
|
||||||
|
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||||
|
import com.github.binarywang.wxpay.service.WxPayService;
|
||||||
|
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||||
|
import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.CODE_SUCCESS;
|
||||||
|
import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.MESSAGE_SUCCESS;
|
||||||
|
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class WXNativePayClient extends AbstractPayClient<WXPayClientConfig> {
|
||||||
|
private WxPayService client;
|
||||||
|
|
||||||
|
public WXNativePayClient(Long channelId, WXPayClientConfig config) {
|
||||||
|
super(channelId, PayChannelEnum.WX_NATIVE.getCode(), config, new WXCodeMapping());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doInit() {
|
||||||
|
WxPayConfig payConfig = new WxPayConfig();
|
||||||
|
BeanUtil.copyProperties(config, payConfig, "keyContent");
|
||||||
|
payConfig.setTradeType(WxPayConstants.TradeType.NATIVE); // 设置使用 native 支付方式
|
||||||
|
// if (StrUtil.isNotEmpty(config.getKeyContent())) {
|
||||||
|
// payConfig.setKeyContent(config.getKeyContent().getBytes(StandardCharsets.UTF_8));
|
||||||
|
// }
|
||||||
|
if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) {
|
||||||
|
// weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
|
||||||
|
payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
|
||||||
|
// weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
|
||||||
|
payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
|
||||||
|
}
|
||||||
|
// 真实客户端
|
||||||
|
this.client = new WxPayServiceImpl();
|
||||||
|
client.setConfig(payConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PayCommonResult<String> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
||||||
|
// 这里原生的返回的是支付的 url 所以直接使用string接收
|
||||||
|
//"invokeResponse": "weixin://wxpay/bizpayurl?pr=EGYAem7zz"
|
||||||
|
String responseV3;
|
||||||
|
try {
|
||||||
|
switch (config.getApiVersion()) {
|
||||||
|
case WXPayClientConfig.API_VERSION_V2:
|
||||||
|
responseV3 = unifiedOrderV2(reqDTO).getCodeUrl();
|
||||||
|
break;
|
||||||
|
case WXPayClientConfig.API_VERSION_V3:
|
||||||
|
responseV3 = this.unifiedOrderV3(reqDTO);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||||
|
}
|
||||||
|
} catch (WxPayException e) {
|
||||||
|
log.error("[unifiedOrder][request({}) 发起支付失败,原因({})]", toJsonString(reqDTO), e);
|
||||||
|
return PayCommonResult.build(ObjectUtils.defaultIfNull(e.getErrCode(), e.getReturnCode(), "CustomErrorCode"),
|
||||||
|
ObjectUtils.defaultIfNull(e.getErrCodeDes(), e.getCustomErrorMsg()), null, codeMapping);
|
||||||
|
}
|
||||||
|
return PayCommonResult.build(CODE_SUCCESS, MESSAGE_SUCCESS, responseV3, codeMapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
private WxPayNativeOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||||
|
//前端
|
||||||
|
String trade_type = reqDTO.getChannelExtras().get("trade_type");
|
||||||
|
// 构建 WxPayUnifiedOrderRequest 对象
|
||||||
|
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest
|
||||||
|
.newBuilder()
|
||||||
|
.outTradeNo(reqDTO.getMerchantOrderId())
|
||||||
|
.body(reqDTO.getBody())
|
||||||
|
.totalFee(reqDTO.getAmount().intValue()) // 单位分
|
||||||
|
.timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
|
||||||
|
.spbillCreateIp(reqDTO.getUserIp())
|
||||||
|
.notifyUrl(reqDTO.getNotifyUrl())
|
||||||
|
.productId(trade_type)
|
||||||
|
.build();
|
||||||
|
// 执行请求
|
||||||
|
return client.createOrder(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String unifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||||
|
// 构建 WxPayUnifiedOrderRequest 对象
|
||||||
|
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
|
||||||
|
request.setOutTradeNo(reqDTO.getMerchantOrderId());
|
||||||
|
request.setDescription(reqDTO.getBody());
|
||||||
|
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount().intValue())); // 单位分
|
||||||
|
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
|
||||||
|
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||||
|
// 执行请求
|
||||||
|
// log.info("支付字段request:{}",request.getTimeExpire());
|
||||||
|
|
||||||
|
return client.createOrderV3(TradeTypeEnum.NATIVE, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PayOrderNotifyRespDTO parseOrderNotify(PayNotifyDataDTO data) throws WxPayException {
|
||||||
|
WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(data.getBody());
|
||||||
|
Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS");
|
||||||
|
// 转换结果
|
||||||
|
return PayOrderNotifyRespDTO.builder().orderExtensionNo(notifyResult.getOutTradeNo())
|
||||||
|
.channelOrderNo(notifyResult.getTransactionId()).channelUserId(notifyResult.getOpenid())
|
||||||
|
.successTime(DateUtil.parse(notifyResult.getTimeEnd(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
|
||||||
|
.data(data.getBody()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PayRefundNotifyDTO parseRefundNotify(PayNotifyDataDTO notifyData) {
|
||||||
|
//TODO 需要实现
|
||||||
|
throw new UnsupportedOperationException("需要实现");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||||
|
//TODO 需要实现
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -98,7 +98,7 @@ public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
|
|||||||
// TODO 芋艿:貌似没 title?
|
// TODO 芋艿:貌似没 title?
|
||||||
.body(reqDTO.getBody())
|
.body(reqDTO.getBody())
|
||||||
.totalFee(reqDTO.getAmount().intValue()) // 单位分
|
.totalFee(reqDTO.getAmount().intValue()) // 单位分
|
||||||
.timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyyMMddHHmmss"))
|
.timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"))
|
||||||
.spbillCreateIp(reqDTO.getUserIp())
|
.spbillCreateIp(reqDTO.getUserIp())
|
||||||
.openid(getOpenid(reqDTO))
|
.openid(getOpenid(reqDTO))
|
||||||
.notifyUrl(reqDTO.getNotifyUrl())
|
.notifyUrl(reqDTO.getNotifyUrl())
|
||||||
@ -114,7 +114,7 @@ public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
|
|||||||
// TODO 芋艿:貌似没 title?
|
// TODO 芋艿:貌似没 title?
|
||||||
request.setDescription(reqDTO.getBody());
|
request.setDescription(reqDTO.getBody());
|
||||||
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount().intValue())); // 单位分
|
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount().intValue())); // 单位分
|
||||||
request.setTimeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyyMMddHHmmss"));
|
request.setTimeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyy-MM-dd'T'HH:mm:ssXXX"));
|
||||||
request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
|
request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
|
||||||
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
|
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
|
||||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||||
|
@ -20,6 +20,8 @@ public enum PayChannelEnum {
|
|||||||
WX_PUB("wx_pub", "微信 JSAPI 支付", WXPayClientConfig.class), // 公众号网页
|
WX_PUB("wx_pub", "微信 JSAPI 支付", WXPayClientConfig.class), // 公众号网页
|
||||||
WX_LITE("wx_lite", "微信小程序支付", WXPayClientConfig.class),
|
WX_LITE("wx_lite", "微信小程序支付", WXPayClientConfig.class),
|
||||||
WX_APP("wx_app", "微信 App 支付", WXPayClientConfig.class),
|
WX_APP("wx_app", "微信 App 支付", WXPayClientConfig.class),
|
||||||
|
WX_NATIVE("wx_native", "微信 native 支付", WXPayClientConfig.class),
|
||||||
|
|
||||||
|
|
||||||
ALIPAY_PC("alipay_pc", "支付宝 PC 网站支付", AlipayPayClientConfig.class),
|
ALIPAY_PC("alipay_pc", "支付宝 PC 网站支付", AlipayPayClientConfig.class),
|
||||||
ALIPAY_WAP("alipay_wap", "支付宝 Wap 网站支付", AlipayPayClientConfig.class),
|
ALIPAY_WAP("alipay_wap", "支付宝 Wap 网站支付", AlipayPayClientConfig.class),
|
||||||
|
@ -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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
package cn.iocoder.yudao.framework.social.config;
|
package cn.iocoder.yudao.framework.social.config;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ReflectUtil;
|
||||||
import cn.iocoder.yudao.framework.social.core.YudaoAuthRequestFactory;
|
import cn.iocoder.yudao.framework.social.core.YudaoAuthRequestFactory;
|
||||||
|
import com.xkcoding.http.HttpUtil;
|
||||||
|
import com.xkcoding.http.support.hutool.HutoolImpl;
|
||||||
import com.xkcoding.justauth.autoconfigure.JustAuthProperties;
|
import com.xkcoding.justauth.autoconfigure.JustAuthProperties;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import me.zhyd.oauth.cache.AuthStateCache;
|
import me.zhyd.oauth.cache.AuthStateCache;
|
||||||
@ -23,6 +26,9 @@ public class YudaoSocialAutoConfiguration {
|
|||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(prefix = "justauth", value = "enabled", havingValue = "true", matchIfMissing = true)
|
@ConditionalOnProperty(prefix = "justauth", value = "enabled", havingValue = "true", matchIfMissing = true)
|
||||||
public YudaoAuthRequestFactory yudaoAuthRequestFactory(JustAuthProperties properties, AuthStateCache authStateCache) {
|
public YudaoAuthRequestFactory yudaoAuthRequestFactory(JustAuthProperties properties, AuthStateCache authStateCache) {
|
||||||
|
// 需要修改 HttpUtil 使用的实现,避免类报错
|
||||||
|
HttpUtil.setHttp(new HutoolImpl());
|
||||||
|
// 创建 YudaoAuthRequestFactory
|
||||||
return new YudaoAuthRequestFactory(properties, authStateCache);
|
return new YudaoAuthRequestFactory(properties, authStateCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -38,6 +38,11 @@
|
|||||||
<artifactId>mysql-connector-java</artifactId>
|
<artifactId>mysql-connector-java</artifactId>
|
||||||
<version>${mysql.version}</version>
|
<version>${mysql.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.oracle.database.jdbc</groupId>
|
||||||
|
<artifactId>ojdbc8</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.alibaba</groupId>
|
<groupId>com.alibaba</groupId>
|
||||||
<artifactId>druid-spring-boot-starter</artifactId>
|
<artifactId>druid-spring-boot-starter</artifactId>
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package cn.iocoder.yudao.framework.mybatis.core.util;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JDBC 工具类
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
public class JdbcUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断连接是否正确
|
||||||
|
*
|
||||||
|
* @param url 数据源连接
|
||||||
|
* @param username 账号
|
||||||
|
* @param password 密码
|
||||||
|
* @return 是否正确
|
||||||
|
*/
|
||||||
|
public static boolean isConnectionOK(String url, String username, String password) {
|
||||||
|
try (Connection ignored = DriverManager.getConnection(url, username, password)) {
|
||||||
|
return true;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
Loading…
Reference in New Issue
Block a user