Merge branch 'upstream/master'
# Conflicts: # yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/dept/UserPostDO.java # yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/dept/UserPostMapper.java
This commit is contained in:
commit
1cd4338857
|
@ -21,7 +21,7 @@ pipeline {
|
|||
// GitHub 账号名
|
||||
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/'
|
||||
}
|
||||
|
@ -57,4 +57,4 @@ pipeline {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
10
README.md
10
README.md
|
@ -7,19 +7,21 @@
|
|||
|
||||
## 🐯 平台简介
|
||||
|
||||
**芋道**,一套**全部开源**的**企业级**的快速开发平台,毫无保留给个人及企业免费使用。
|
||||
**芋道**,以开发者为中心,打造中国第一流的快速开发平台,全部开源,个人与企业可 100% 免费使用。
|
||||
|
||||
> 有任何问题,或者想要的功能,可以在 _Issues_ 中提给艿艿。
|
||||
>
|
||||
> 😜 给项目点点 Star 吧,这对我们真的很重要!
|
||||
|
||||
* 前端采用 [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) ,正在支持 Vue 3 + ElementUI Plus 最新方案。
|
||||
* 后端采用 Spring Boot、MySQL + MyBatis Plus、Redis + Redisson。
|
||||
* 权限认证使用 Spring Security & Token & Redis,支持多终端、多种用户的认证系统。
|
||||
* 支持加载动态权限菜单,按钮级别权限控制,本地缓存提升性能。
|
||||
* 支持 SaaS 多租户系统,可自定义每个租户的权限,提供透明化的多租户底层封装。
|
||||
* 工作流使用 Activiti ,支持动态表单、在线设计流程、多种任务分配方式。
|
||||
* 工作流使用 Activiti + Flowable,支持动态表单、在线设计流程、多种任务分配方式。
|
||||
* 高效率开发,使用代码生成器可以一键生成前后端代码 + 单元测试 + 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 | |
|
||||
| [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) |
|
||||
|
|
|
@ -1,20 +1,15 @@
|
|||
#!/bin/bash
|
||||
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)
|
||||
# 基础路径
|
||||
BASE_PATH=/media/pi/KINGTON/data/work/projects/yudao-admin-server
|
||||
BASE_PATH=/work/projects/yudao-server
|
||||
# 编译后 jar 的地址。部署时,Jenkins 会上传 jar 包到该目录下
|
||||
SOURCE_PATH=$BASE_PATH/build
|
||||
# 服务名称。同时约定部署服务的 jar 包名字也为它。
|
||||
SERVER_NAME=yudao-admin-server
|
||||
SERVER_NAME=yudao-server
|
||||
# 环境
|
||||
PROFILES_ACTIVE=dev
|
||||
PROFILES_ACTIVE=development
|
||||
# 健康检查 URL
|
||||
HEALTH_CHECK_URL=http://127.0.0.1:48080/actuator/health/
|
||||
|
||||
|
@ -62,7 +57,7 @@ function transfer() {
|
|||
echo "[transfer] 转移 $SERVER_NAME.jar 完成"
|
||||
}
|
||||
|
||||
# 停止
|
||||
# 停止:优雅关闭之前已经启动的服务
|
||||
function stop() {
|
||||
echo "[stop] 开始停止 $BASE_PATH/$SERVER_NAME"
|
||||
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]"
|
||||
kill -15 $PID
|
||||
# 等待最大 60 秒,直到关闭完成。
|
||||
for ((i = 0; i < 60; i++))
|
||||
# 等待最大 120 秒,直到关闭完成。
|
||||
for ((i = 0; i < 120; i++))
|
||||
do
|
||||
sleep 1
|
||||
PID=$(ps -ef | grep $BASE_PATH/$SERVER_NAME | grep -v "grep" | awk '{print $2}')
|
||||
|
@ -95,7 +90,7 @@ function stop() {
|
|||
fi
|
||||
}
|
||||
|
||||
# 启动
|
||||
# 启动:启动后端项目
|
||||
function start() {
|
||||
# 开启启动前,打印启动参数
|
||||
echo "[start] 开始启动 $BASE_PATH/$SERVER_NAME"
|
||||
|
@ -108,13 +103,13 @@ function start() {
|
|||
echo "[start] 启动 $BASE_PATH/$SERVER_NAME 完成"
|
||||
}
|
||||
|
||||
# 健康检查
|
||||
# 健康检查:自动判断后端项目是否正常启动
|
||||
function healthCheck() {
|
||||
# 如果配置健康检查,则进行健康检查
|
||||
if [ -n "$HEALTH_CHECK_URL" ]; then
|
||||
# 健康检查最大 60 秒,直到健康检查通过
|
||||
# 健康检查最大 120 秒,直到健康检查通过
|
||||
echo "[healthCheck] 开始通过 $HEALTH_CHECK_URL 地址,进行健康检查";
|
||||
for ((i = 0; i < 60; i++))
|
||||
for ((i = 0; i < 120; i++))
|
||||
do
|
||||
# 请求健康检查地址,只获取状态码。
|
||||
result=`curl -I -m 10 -o /dev/null -s -w %{http_code} $HEALTH_CHECK_URL || echo "000"`
|
||||
|
@ -138,11 +133,11 @@ function healthCheck() {
|
|||
else
|
||||
tail -n 10 nohup.out
|
||||
fi
|
||||
# 如果未配置健康检查,则 slepp 60 秒,人工看日志是否部署成功。
|
||||
# 如果未配置健康检查,则 sleep 120 秒,人工看日志是否部署成功。
|
||||
else
|
||||
echo "[healthCheck] HEALTH_CHECK_URL 未配置,开始 sleep 60 秒";
|
||||
sleep 60
|
||||
echo "[healthCheck] sleep 60 秒完成,查看日志,自行判断是否启动成功";
|
||||
echo "[healthCheck] HEALTH_CHECK_URL 未配置,开始 sleep 120 秒";
|
||||
sleep 120
|
||||
echo "[healthCheck] sleep 120 秒完成,查看日志,自行判断是否启动成功";
|
||||
tail -n 50 nohup.out
|
||||
fi
|
||||
}
|
||||
|
@ -159,7 +154,7 @@ function deploy() {
|
|||
# 启动 Java 服务
|
||||
start
|
||||
# 健康检查
|
||||
# healthCheck
|
||||
healthCheck
|
||||
}
|
||||
|
||||
deploy
|
||||
|
|
2
pom.xml
2
pom.xml
|
@ -25,7 +25,7 @@
|
|||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<properties>
|
||||
<revision>1.6.1-snapshot</revision>
|
||||
<revision>1.6.2-snapshot</revision>
|
||||
<!-- Maven 相关 -->
|
||||
<java.version>1.8</java.version>
|
||||
<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>
|
||||
|
||||
<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 相关 -->
|
||||
<knife4j.version>3.0.2</knife4j.version>
|
||||
<swagger-annotations.version>1.5.22</swagger-annotations.version>
|
||||
|
@ -26,7 +26,7 @@
|
|||
<druid.version>1.2.8</druid.version>
|
||||
<mybatis-plus.version>3.4.3.4</mybatis-plus.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 配置中心相关 -->
|
||||
<apollo.version>1.9.2</apollo.version>
|
||||
<!-- Job 定时任务相关 -->
|
||||
|
@ -60,6 +60,7 @@
|
|||
<minio.version>8.2.2</minio.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>
|
||||
<tencentcloud-sdk-java.version>3.1.471</tencentcloud-sdk-java.version>
|
||||
<yunpian-java-sdk.version>1.2.7</yunpian-java-sdk.version>
|
||||
<justauth.version>1.4.0</justauth.version>
|
||||
</properties>
|
||||
|
@ -552,6 +553,11 @@
|
|||
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
|
||||
<version>${aliyun-java-sdk-dysmsapi.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.tencentcloudapi</groupId>
|
||||
<artifactId>tencentcloud-sdk-java</artifactId>
|
||||
<version>${tencentcloud-sdk-java.version}</version>
|
||||
</dependency>
|
||||
<!-- SMS SDK end -->
|
||||
|
||||
<dependency>
|
||||
|
|
|
@ -1,8 +1,19 @@
|
|||
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.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.Function;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
|
||||
/**
|
||||
* Array 工具类
|
||||
|
@ -30,4 +41,16 @@ public class ArrayUtils {
|
|||
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 from.stream()
|
||||
.collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList())));
|
||||
.collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList())));
|
||||
}
|
||||
|
||||
// 暂时没想好名字,先以 2 结尾噶
|
||||
|
@ -169,4 +169,5 @@ public class CollectionUtils {
|
|||
public static <T> Collection<T> singleton(T deptId) {
|
||||
return deptId == null ? Collections.emptyList() : Collections.singleton(deptId);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
package cn.iocoder.yudao.framework.common.util.http;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.map.TableMap;
|
||||
import cn.hutool.core.net.url.UrlBuilder;
|
||||
import cn.hutool.core.util.ReferenceUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
|
||||
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.IoUtil;
|
||||
import cn.hutool.core.util.CharsetUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
|
|
@ -35,7 +35,7 @@ import java.util.Set;
|
|||
* 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。
|
||||
*
|
||||
* 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改?
|
||||
* 1. 一般情况下,dept_id 不进行修改,则会导致用户看到之前的数据。【yudao-admin-server 采用该方案】
|
||||
* 1. 一般情况下,dept_id 不进行修改,则会导致用户看到之前的数据。【yudao-server 采用该方案】
|
||||
* 2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 DeptDataPermissionRule 的实现代码】
|
||||
* 1)编写洗数据的脚本,将 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.AlipayQrPayClient;
|
||||
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.WXPubPayClient;
|
||||
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_LITE: 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_QR: 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 java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||
|
@ -54,7 +55,8 @@ public abstract class AbstractAlipayClient extends AbstractPayClient<AlipayPayCl
|
|||
*/
|
||||
@Override
|
||||
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"))
|
||||
.channelOrderNo(params.get("trade_no")).channelUserId(params.get("seller_id"))
|
||||
.tradeStatus(params.get("trade_status"))
|
||||
|
@ -64,7 +66,7 @@ public abstract class AbstractAlipayClient extends AbstractPayClient<AlipayPayCl
|
|||
|
||||
@Override
|
||||
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"))
|
||||
.tradeNo(params.get("out_trade_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)
|
||||
private String privateCertContent;
|
||||
/**
|
||||
* apiV3 秘钥值
|
||||
* apiV3 密钥值
|
||||
*/
|
||||
@NotBlank(message = "apiV3 秘钥值 不能为空", groups = V3.class)
|
||||
@NotBlank(message = "apiV3 密钥值 不能为空", groups = V3.class)
|
||||
private String apiV3Key;
|
||||
|
||||
/**
|
||||
|
|
|
@ -98,7 +98,7 @@ public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
|
|||
// TODO 芋艿:貌似没 title?
|
||||
.body(reqDTO.getBody())
|
||||
.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())
|
||||
.openid(getOpenid(reqDTO))
|
||||
.notifyUrl(reqDTO.getNotifyUrl())
|
||||
|
@ -114,7 +114,7 @@ public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
|
|||
// TODO 芋艿:貌似没 title?
|
||||
request.setDescription(reqDTO.getBody());
|
||||
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.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
|
|
|
@ -20,6 +20,8 @@ public enum PayChannelEnum {
|
|||
WX_PUB("wx_pub", "微信 JSAPI 支付", WXPayClientConfig.class), // 公众号网页
|
||||
WX_LITE("wx_lite", "微信小程序支付", WXPayClientConfig.class),
|
||||
WX_APP("wx_app", "微信 App 支付", WXPayClientConfig.class),
|
||||
WX_NATIVE("wx_native", "微信 native 支付", WXPayClientConfig.class),
|
||||
|
||||
|
||||
ALIPAY_PC("alipay_pc", "支付宝 PC 网站支付", AlipayPayClientConfig.class),
|
||||
ALIPAY_WAP("alipay_wap", "支付宝 Wap 网站支付", AlipayPayClientConfig.class),
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>短信拓展,支持阿里云、云片</description>
|
||||
<description>短信拓展,支持阿里云、云片、腾讯云</description>
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<dependencies>
|
||||
|
@ -77,6 +77,10 @@
|
|||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.tencentcloudapi</groupId>
|
||||
<artifactId>tencentcloud-sdk-java</artifactId>
|
||||
</dependency>
|
||||
<!-- SMS SDK end -->
|
||||
</dependencies>
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import java.util.List;
|
|||
* 短信客户端,用于对接各短信平台的 SDK,实现短信发送等功能
|
||||
*
|
||||
* @author zzf
|
||||
* @date 2021/1/25 14:14
|
||||
* @since 2021/1/25 14:14
|
||||
*/
|
||||
public interface SmsClient {
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
|
|||
* 短信客户端的工厂接口
|
||||
*
|
||||
* @author zzf
|
||||
* @date 2021/1/28 14:01
|
||||
* @since 2021/1/28 14:01
|
||||
*/
|
||||
public interface SmsClientFactory {
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import java.util.List;
|
|||
* 短信客户端的抽象类,提供模板方法,减少子类的冗余代码
|
||||
*
|
||||
* @author zzf
|
||||
* @date 2021/2/1 9:28
|
||||
* @since 2021/2/1 9:28
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractSmsClient implements SmsClient {
|
||||
|
@ -31,7 +31,7 @@ public abstract class AbstractSmsClient implements SmsClient {
|
|||
protected final SmsCodeMapping codeMapping;
|
||||
|
||||
public AbstractSmsClient(SmsChannelProperties properties, SmsCodeMapping codeMapping) {
|
||||
this.properties = properties;
|
||||
this.properties = prepareProperties(properties);
|
||||
this.codeMapping = codeMapping;
|
||||
}
|
||||
|
||||
|
@ -54,11 +54,21 @@ public abstract class AbstractSmsClient implements SmsClient {
|
|||
return;
|
||||
}
|
||||
log.info("[refresh][配置({})发生变化,重新初始化]", properties);
|
||||
this.properties = properties;
|
||||
this.properties = prepareProperties(properties);
|
||||
// 初始化
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 在赋值给{@link this#properties}前,子类可根据需要预处理短信渠道配置
|
||||
*
|
||||
* @param properties 数据库中存储的短信渠道配置
|
||||
* @return 满足子类实现的短信渠道配置
|
||||
*/
|
||||
protected SmsChannelProperties prepareProperties(SmsChannelProperties properties) {
|
||||
return properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long 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.impl.aliyun.AliyunSmsClient;
|
||||
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.enums.SmsChannelEnum;
|
||||
import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
|
||||
|
@ -44,7 +45,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory {
|
|||
Arrays.stream(SmsChannelEnum.values()).forEach(channel -> {
|
||||
// 创建一个空的 SmsChannelProperties 对象
|
||||
SmsChannelProperties properties = new SmsChannelProperties().setCode(channel.getCode())
|
||||
.setApiKey("default").setApiSecret("default");
|
||||
.setApiKey("default default").setApiSecret("default");
|
||||
// 创建 Sms 客户端
|
||||
AbstractSmsClient smsClient = createSmsClient(properties);
|
||||
channelCodeClients.put(channel.getCode(), smsClient);
|
||||
|
@ -81,6 +82,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory {
|
|||
case ALIYUN: return new AliyunSmsClient(properties);
|
||||
case YUN_PIAN: return new YunpianSmsClient(properties);
|
||||
case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties);
|
||||
case TENCENT: return new TencentSmsClient(properties);
|
||||
}
|
||||
// 创建失败,错误日志 + 抛出异常
|
||||
log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties);
|
||||
|
|
|
@ -41,7 +41,7 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE
|
|||
* 阿里短信客户端的实现类
|
||||
*
|
||||
* @author zzf
|
||||
* @date 2021/1/25 14:17
|
||||
* @since 2021/1/25 14:17
|
||||
*/
|
||||
@Slf4j
|
||||
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
|
||||
* @date 9:48 2021/3/5
|
||||
* @since 9:48 2021/3/5
|
||||
*/
|
||||
@Slf4j
|
||||
public class YunpianSmsClient extends AbstractSmsClient {
|
||||
|
|
|
@ -8,7 +8,7 @@ import lombok.Getter;
|
|||
* 短信渠道枚举
|
||||
*
|
||||
* @author zzf
|
||||
* @date 2021/1/25 10:56
|
||||
* @since 2021/1/25 10:56
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
|
@ -17,7 +17,7 @@ public enum SmsChannelEnum {
|
|||
DEBUG_DING_TALK("DEBUG_DING_TALK", "调试(钉钉)"),
|
||||
YUN_PIAN("YUN_PIAN", "云片"),
|
||||
ALIYUN("ALIYUN", "阿里云"),
|
||||
// TENCENT("TENCENT", "腾讯云"),
|
||||
TENCENT("TENCENT", "腾讯云"),
|
||||
// HUA_WEI("HUA_WEI", "华为云"),
|
||||
;
|
||||
|
||||
|
|
|
@ -26,6 +26,9 @@ public interface SmsFrameworkErrorCodeConstants {
|
|||
|
||||
ErrorCode SMS_SEND_CONTENT_INVALID = new ErrorCode(2001000104, "短信内容有敏感词");
|
||||
|
||||
// 腾讯云:为避免骚扰用户,营销短信只允许在8点到22点发送。
|
||||
ErrorCode SMS_SEND_MARKET_LIMIT_CONTROL = new ErrorCode(2001000105, "营销短信发送时间限制");
|
||||
|
||||
// ========== 模板相关 2001000200 ==========
|
||||
ErrorCode SMS_TEMPLATE_INVALID = new ErrorCode(2001000200, "短信模板不合法"); // 包括短信模板不存在
|
||||
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_MOBILE_INVALID = new ErrorCode(2001000901, "手机格式不正确");
|
||||
ErrorCode SMS_MOBILE_BLACK = new ErrorCode(2001000902, "手机号在黑名单中");
|
||||
ErrorCode SMS_APP_ID_INVALID = new ErrorCode(2001000903, "SdkAppId不合法");
|
||||
|
||||
ErrorCode EXCEPTION = new ErrorCode(2001000999, "调用异常");
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import javax.validation.constraints.NotNull;
|
|||
* 短信渠道配置类
|
||||
*
|
||||
* @author zzf
|
||||
* @date 2021/1/25 17:01
|
||||
* @since 2021/1/25 17:01
|
||||
*/
|
||||
@Data
|
||||
@Validated
|
||||
|
@ -40,9 +40,9 @@ public class SmsChannelProperties {
|
|||
@NotEmpty(message = "短信 API 的账号不能为空")
|
||||
private String apiKey;
|
||||
/**
|
||||
* 短信 API 的秘钥
|
||||
* 短信 API 的密钥
|
||||
*/
|
||||
@NotEmpty(message = "短信 API 的秘钥不能为空")
|
||||
@NotEmpty(message = "短信 API 的密钥不能为空")
|
||||
private String apiSecret;
|
||||
/**
|
||||
* 短信发送回调 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;
|
||||
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
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 lombok.extern.slf4j.Slf4j;
|
||||
import me.zhyd.oauth.cache.AuthStateCache;
|
||||
|
@ -23,6 +26,9 @@ public class YudaoSocialAutoConfiguration {
|
|||
@Bean
|
||||
@ConditionalOnProperty(prefix = "justauth", value = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public YudaoAuthRequestFactory yudaoAuthRequestFactory(JustAuthProperties properties, AuthStateCache authStateCache) {
|
||||
// 需要修改 HttpUtil 使用的实现,避免类报错
|
||||
HttpUtil.setHttp(new HutoolImpl());
|
||||
// 创建 YudaoAuthRequestFactory
|
||||
return new YudaoAuthRequestFactory(properties, authStateCache);
|
||||
}
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@ public class TenantSecurityWebFilter extends ApiRequestFilter {
|
|||
}
|
||||
}
|
||||
|
||||
//检查是否是忽略的 URL, 如果是则允许访问
|
||||
// 如果非允许忽略租户的 URL,则校验租户是否合法
|
||||
if (!isIgnoreUrl(request)) {
|
||||
// 2. 如果请求未带租户的编号,不允许访问。
|
||||
if (tenantId == null) {
|
||||
|
@ -92,6 +92,10 @@ public class TenantSecurityWebFilter extends ApiRequestFilter {
|
|||
ServletUtils.writeJSON(response, result);
|
||||
return;
|
||||
}
|
||||
} else { // 如果是允许忽略租户的 URL,若未传递租户编号,则默认忽略租户编号,避免报错
|
||||
if (tenantId == null) {
|
||||
TenantContextHolder.setIgnore(true);
|
||||
}
|
||||
}
|
||||
|
||||
// 继续过滤
|
||||
|
|
|
@ -75,12 +75,20 @@ public interface BaseMapperX<T> extends BaseMapper<T> {
|
|||
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) {
|
||||
// TODO 芋艿:修改成支持批量的
|
||||
entities.forEach(this::insert);
|
||||
}
|
||||
|
||||
default void updateBatch(T update) {
|
||||
update(update, new QueryWrapper<>());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package cn.iocoder.yudao.framework.mybatis.core.type;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.apache.ibatis.type.MappedJdbcTypes;
|
||||
import org.apache.ibatis.type.MappedTypes;
|
||||
import org.apache.ibatis.type.TypeHandler;
|
||||
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* List<String> 的类型转换器实现类,对应数据库的 varchar 类型
|
||||
*
|
||||
* @author 永不言败
|
||||
* @since 2022 3/23 12:50:15
|
||||
*/
|
||||
@MappedJdbcTypes(JdbcType.VARCHAR)
|
||||
@MappedTypes(List.class)
|
||||
public class StringLiSTTypeHandler implements TypeHandler<List<String>> {
|
||||
|
||||
private static final String COMMA = ",";
|
||||
|
||||
@Override
|
||||
public void setParameter(PreparedStatement ps, int i, List<String> strings, JdbcType jdbcType) throws SQLException {
|
||||
// 设置占位符
|
||||
ps.setString(i, CollUtil.join(strings, COMMA));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getResult(ResultSet rs, String columnName) throws SQLException {
|
||||
String value = rs.getString(columnName);
|
||||
return getResult(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||
String value = rs.getString(columnIndex);
|
||||
return getResult(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||
String value = cs.getString(columnIndex);
|
||||
return getResult(value);
|
||||
}
|
||||
|
||||
private List<String> getResult(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return StrUtil.splitTrim(value, COMMA);
|
||||
}
|
||||
}
|
|
@ -26,6 +26,12 @@
|
|||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-cache</artifactId> <!-- 实现对 Caches 的自动化配置 -->
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</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;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
|
@ -11,7 +10,6 @@ import org.springframework.data.redis.serializer.RedisSerializer;
|
|||
* Redis 配置类
|
||||
*/
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public class YudaoRedisAutoConfiguration {
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
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 模式的开关不能为空")
|
||||
private Boolean mockEnable;
|
||||
/**
|
||||
* mock 模式的秘钥
|
||||
* 一定要配置秘钥,保证安全性
|
||||
* mock 模式的密钥
|
||||
* 一定要配置密钥,保证安全性
|
||||
*/
|
||||
@NotEmpty(message = "mock 模式的秘钥不能为空") // 这里设置了一个默认值,因为实际上只有 mockEnable 为 true 时才需要配置。
|
||||
@NotEmpty(message = "mock 模式的密钥不能为空") // 这里设置了一个默认值,因为实际上只有 mockEnable 为 true 时才需要配置。
|
||||
private String mockSecret = "yudaoyuanma";
|
||||
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
|||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.Data;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import java.util.*;
|
||||
|
@ -61,10 +60,6 @@ public class LoginUser implements UserDetails {
|
|||
* 部门编号
|
||||
*/
|
||||
private Long deptId;
|
||||
/**
|
||||
* 所属岗位
|
||||
*/
|
||||
private Set<Long> postIds;
|
||||
|
||||
// ========== 上下文 ==========
|
||||
/**
|
||||
|
|
|
@ -21,6 +21,17 @@
|
|||
<artifactId>yudao-common</artifactId>
|
||||
</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 测试相关 -->
|
||||
<dependency>
|
||||
<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.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.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.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.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
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.dto.ApiAccessLogCreateReqDTO;
|
||||
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
|
||||
import cn.iocoder.yudao.framework.web.config.WebProperties;
|
||||
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
|
||||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
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 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 org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
|
@ -26,27 +23,24 @@ import java.io.IOException;
|
|||
import java.util.Date;
|
||||
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
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ApiAccessLogFilter extends OncePerRequestFilter {
|
||||
public class ApiAccessLogFilter extends ApiRequestFilter {
|
||||
|
||||
private final WebProperties webProperties;
|
||||
private final String applicationName;
|
||||
|
||||
private final ApiAccessLogFrameworkService apiAccessLogFrameworkService;
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
// 只过滤 API 请求的地址
|
||||
return !StrUtil.startWithAny(request.getRequestURI(), webProperties.getAppApi().getPrefix(),
|
||||
webProperties.getAppApi().getPrefix());
|
||||
public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
|
||||
super(webProperties);
|
||||
this.applicationName = applicationName;
|
||||
this.apiAccessLogFrameworkService = apiAccessLogFrameworkService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
<modules>
|
||||
<module>yudao-module-bpm-api</module>
|
||||
<module>yudao-module-bpm-base</module>
|
||||
<module>yudao-module-bpm-impl-flowable</module>
|
||||
<module>yudao-module-bpm-impl-activiti</module>
|
||||
<module>yudao-module-bpm-biz-flowable</module>
|
||||
<module>yudao-module-bpm-biz-activiti</module>
|
||||
</modules>
|
||||
<artifactId>yudao-module-bpm</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
@ -24,9 +24,9 @@
|
|||
bpm 解释:https://baike.baidu.com/item/BPM/1933
|
||||
|
||||
目前提供两套实现方案:
|
||||
1. 基于 Activiti 7 实现的 yudao-module-bpm-impl-activiti
|
||||
2. 基于 Flowable 6 实现的 yudao-module-bpm-impl-flowable
|
||||
两套实现会存在共享的逻辑,所以会继承 yudao-module-impl-base
|
||||
1. 基于 Activiti 7 实现的 yudao-module-bpm-biz-activiti
|
||||
2. 基于 Flowable 6 实现的 yudao-module-bpm-biz-flowable
|
||||
两套实现会存在共享的逻辑,所以会继承 yudao-module-bpm-base
|
||||
</description>
|
||||
|
||||
</project>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
package cn.iocoder.yudao.module.bpm.service.definition;
|
||||
|
||||
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.BpmFormPageReqVO;
|
||||
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.mysql.definition.BpmFormMapper;
|
||||
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.springframework.context.annotation.Import;
|
||||
|
||||
|
@ -18,12 +18,12 @@ import java.util.List;
|
|||
import java.util.stream.Collectors;
|
||||
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.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.RandomUtils.randomLongId;
|
||||
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.*;
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
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.BpmUserGroupPageReqVO;
|
||||
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.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.Test;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
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.module.bpm.enums.ErrorCodeConstants.USER_GROUP_NOT_EXISTS;
|
||||
|
||||
/**
|
||||
* {@link BpmUserGroupServiceImpl} 的单元测试类
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>yudao-module-bpm-impl-activiti</artifactId>
|
||||
<artifactId>yudao-module-bpm-biz-activiti</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
|
@ -215,6 +215,9 @@ public class BpmModelServiceImpl implements BpmModelService {
|
|||
if (oldDefinition == null) {
|
||||
return;
|
||||
}
|
||||
if(oldDefinition.isSuspended()) {
|
||||
return;
|
||||
}
|
||||
processDefinitionService.updateProcessDefinitionState(oldDefinition.getId(), SuspensionState.SUSPENDED.getStateCode());
|
||||
}
|
||||
|
|
@ -103,6 +103,9 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
|
|||
}
|
||||
// 执行查询
|
||||
List<ProcessDefinition> processDefinitions = definitionQuery.list();
|
||||
if (CollUtil.isEmpty(processDefinitions)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 获得 BpmProcessDefinitionDO Map
|
||||
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