mirror of
https://gitee.com/hhyykk/ipms-sjy.git
synced 2025-09-08 14:12:29 +08:00
项目结构调整 x 16 : 将 monitor、sms、dict 等组件拆分出去
This commit is contained in:
70
yudao-framework/yudao-spring-boot-starter-biz-sms/pom.xml
Normal file
70
yudao-framework/yudao-spring-boot-starter-biz-sms/pom.xml
Normal file
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-framework</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>yudao-spring-boot-starter-biz-sms</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${artifactId}</name>
|
||||
<description>短信拓展,支持阿里云、云片</description>
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring 核心 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test 测试相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类相关 -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>jakarta.validation</groupId>
|
||||
<artifactId>jakarta.validation-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 三方云服务相关 -->
|
||||
|
||||
<!-- SMS SDK begin -->
|
||||
<dependency>
|
||||
<groupId>com.yunpian.sdk</groupId>
|
||||
<artifactId>yunpian-java-sdk</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>aliyun-java-sdk-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
|
||||
</dependency>
|
||||
<!-- SMS SDK end -->
|
||||
</dependencies>
|
||||
</project>
|
@@ -0,0 +1,21 @@
|
||||
package cn.iocoder.dashboard.framework.sms.config;
|
||||
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.impl.SmsClientFactoryImpl;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 短信配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration
|
||||
public class SmsConfiguration {
|
||||
|
||||
@Bean
|
||||
public SmsClientFactory smsClientFactory() {
|
||||
return new SmsClientFactoryImpl();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.client;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 短信客户端接口
|
||||
*
|
||||
* @author zzf
|
||||
* @date 2021/1/25 14:14
|
||||
*/
|
||||
public interface SmsClient {
|
||||
|
||||
/**
|
||||
* 获得渠道编号
|
||||
*
|
||||
* @return 渠道编号
|
||||
*/
|
||||
Long getId();
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*
|
||||
* @param logId 日志编号
|
||||
* @param mobile 手机号
|
||||
* @param apiTemplateId 短信 API 的模板编号
|
||||
* @param templateParams 短信模板参数。通过 List 数组,保证参数的顺序
|
||||
* @return 短信发送结果
|
||||
*/
|
||||
SmsCommonResult<SmsSendRespDTO> sendSms(Long logId, String mobile, String apiTemplateId,
|
||||
List<KeyValue<String, Object>> templateParams);
|
||||
|
||||
/**
|
||||
* 解析接收短信的接收结果
|
||||
*
|
||||
* @param text 结果
|
||||
* @return 结果内容
|
||||
* @throws Throwable 当解析 text 发生异常时,则会抛出异常
|
||||
*/
|
||||
List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) throws Throwable;
|
||||
|
||||
/**
|
||||
* 查询指定的短信模板
|
||||
*
|
||||
* @param apiTemplateId 短信 API 的模板编号
|
||||
* @return 短信模板
|
||||
*/
|
||||
SmsCommonResult<SmsTemplateRespDTO> getSmsTemplate(String apiTemplateId);
|
||||
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.client;
|
||||
|
||||
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
|
||||
|
||||
/**
|
||||
* 短信客户端工厂接口
|
||||
*
|
||||
* @author zzf
|
||||
* @date 2021/1/28 14:01
|
||||
*/
|
||||
public interface SmsClientFactory {
|
||||
|
||||
/**
|
||||
* 获得短信 Client
|
||||
*
|
||||
* @param channelId 渠道编号
|
||||
* @return 短信 Client
|
||||
*/
|
||||
SmsClient getSmsClient(Long channelId);
|
||||
|
||||
/**
|
||||
* 获得短信 Client
|
||||
*
|
||||
* @param channelCode 渠道编码
|
||||
* @return 短信 Client
|
||||
*/
|
||||
SmsClient getSmsClient(String channelCode);
|
||||
|
||||
/**
|
||||
* 创建短信 Client
|
||||
*
|
||||
* @param properties 配置对象
|
||||
*/
|
||||
void createOrUpdateSmsClient(SmsChannelProperties properties);
|
||||
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.client;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
||||
import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* 将 API 的错误码,转换为通用的错误码
|
||||
*
|
||||
* @see SmsCommonResult
|
||||
* @see SmsFrameworkErrorCodeConstants
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface SmsCodeMapping extends Function<String, ErrorCode> {
|
||||
}
|
@@ -0,0 +1,68 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.client;
|
||||
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* 短信的 CommonResult 拓展类
|
||||
*
|
||||
* 考虑到不同的平台,返回的 code 和 msg 是不同的,所以统一额外返回 {@link #apiCode} 和 {@link #apiMsg} 字段
|
||||
*
|
||||
* 另外,一些短信平台(例如说阿里云、腾讯云)会返回一个请求编号,用于排查请求失败的问题,我们设置到 {@link #apiRequestId} 字段
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class SmsCommonResult<T> extends CommonResult<T> {
|
||||
|
||||
/**
|
||||
* API 返回错误码
|
||||
*
|
||||
* 由于第三方的错误码可能是字符串,所以使用 String 类型
|
||||
*/
|
||||
private String apiCode;
|
||||
/**
|
||||
* API 返回提示
|
||||
*/
|
||||
private String apiMsg;
|
||||
|
||||
/**
|
||||
* API 请求编号
|
||||
*/
|
||||
private String apiRequestId;
|
||||
|
||||
private SmsCommonResult() {
|
||||
}
|
||||
|
||||
public static <T> SmsCommonResult<T> build(String apiCode, String apiMsg, String apiRequestId,
|
||||
T data, SmsCodeMapping codeMapping) {
|
||||
Assert.notNull(codeMapping, "参数 codeMapping 不能为空");
|
||||
SmsCommonResult<T> result = new SmsCommonResult<T>().setApiCode(apiCode).setApiMsg(apiMsg).setApiRequestId(apiRequestId);
|
||||
result.setData(data);
|
||||
// 翻译错误码
|
||||
if (codeMapping != null) {
|
||||
ErrorCode errorCode = codeMapping.apply(apiCode);
|
||||
if (errorCode == null) {
|
||||
errorCode = SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
|
||||
}
|
||||
result.setCode(errorCode.getCode()).setMsg(errorCode.getMsg());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> SmsCommonResult<T> error(Throwable ex) {
|
||||
SmsCommonResult<T> result = new SmsCommonResult<>();
|
||||
result.setCode(SmsFrameworkErrorCodeConstants.EXCEPTION.getCode());
|
||||
result.setMsg(ExceptionUtil.getRootCauseMessage(ex));
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.client.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 消息接收 Response DTO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class SmsReceiveRespDTO {
|
||||
|
||||
/**
|
||||
* 是否接收成功
|
||||
*/
|
||||
private Boolean success;
|
||||
/**
|
||||
* API 接收结果的编码
|
||||
*/
|
||||
private String errorCode;
|
||||
/**
|
||||
* API 接收结果的说明
|
||||
*/
|
||||
private String errorMsg;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
private String mobile;
|
||||
/**
|
||||
* 用户接收时间
|
||||
*/
|
||||
private Date receiveTime;
|
||||
|
||||
/**
|
||||
* 短信 API 发送返回的序号
|
||||
*/
|
||||
private String serialNo;
|
||||
/**
|
||||
* 短信日志编号
|
||||
*
|
||||
* 对应 SysSmsLogDO 的编号
|
||||
*/
|
||||
private Long logId;
|
||||
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.client.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 短信发送 Response DTO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class SmsSendRespDTO {
|
||||
|
||||
/**
|
||||
* 短信 API 发送返回的序号
|
||||
*/
|
||||
private String serialNo;
|
||||
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.client.dto;
|
||||
|
||||
import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 短信模板 Response DTO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class SmsTemplateRespDTO {
|
||||
|
||||
/**
|
||||
* 模板编号
|
||||
*/
|
||||
private String id;
|
||||
/**
|
||||
* 短信内容
|
||||
*/
|
||||
private String content;
|
||||
/**
|
||||
* 审核状态
|
||||
*
|
||||
* 枚举 {@link SmsTemplateAuditStatusEnum}
|
||||
*/
|
||||
private Integer auditStatus;
|
||||
/**
|
||||
* 审核未通过的理由
|
||||
*/
|
||||
private String auditReason;
|
||||
|
||||
}
|
@@ -0,0 +1,122 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.client.impl;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.SmsClient;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
|
||||
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 短信客户端抽象类
|
||||
*
|
||||
* @author zzf
|
||||
* @date 2021/2/1 9:28
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractSmsClient implements SmsClient {
|
||||
|
||||
/**
|
||||
* 短信渠道配置
|
||||
*/
|
||||
protected volatile SmsChannelProperties properties;
|
||||
/**
|
||||
* 错误码枚举类
|
||||
*/
|
||||
protected final SmsCodeMapping codeMapping;
|
||||
|
||||
/**
|
||||
* 短信客户端有参构造函数
|
||||
*
|
||||
* @param properties 短信配置
|
||||
*/
|
||||
public AbstractSmsClient(SmsChannelProperties properties, SmsCodeMapping codeMapping) {
|
||||
this.properties = properties;
|
||||
this.codeMapping = codeMapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
public final void init() {
|
||||
doInit();
|
||||
log.info("[init][配置({}) 初始化完成]", properties);
|
||||
}
|
||||
|
||||
public final void refresh(SmsChannelProperties properties) {
|
||||
// 判断是否更新
|
||||
if (properties.equals(this.properties)) {
|
||||
return;
|
||||
}
|
||||
log.info("[refresh][配置({})发生变化,重新初始化]", properties);
|
||||
this.properties = properties;
|
||||
// 初始化
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义初始化
|
||||
*/
|
||||
protected abstract void doInit();
|
||||
|
||||
@Override
|
||||
public Long getId() {
|
||||
return properties.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final SmsCommonResult<SmsSendRespDTO> sendSms(Long logId, String mobile,
|
||||
String apiTemplateId, List<KeyValue<String, Object>> templateParams) {
|
||||
// 执行短信发送
|
||||
SmsCommonResult<SmsSendRespDTO> result;
|
||||
try {
|
||||
result = doSendSms(logId, mobile, apiTemplateId, templateParams);
|
||||
} catch (Throwable ex) {
|
||||
// 打印异常日志
|
||||
log.error("[sendSms][发送短信异常,sendLogId({}) mobile({}) apiTemplateId({}) templateParams({})]",
|
||||
logId, mobile, apiTemplateId, templateParams, ex);
|
||||
// 封装返回
|
||||
return SmsCommonResult.error(ex);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected abstract SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
|
||||
String apiTemplateId, List<KeyValue<String, Object>> templateParams)
|
||||
throws Throwable;
|
||||
|
||||
@Override
|
||||
public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) throws Throwable {
|
||||
try {
|
||||
return doParseSmsReceiveStatus(text);
|
||||
} catch (Throwable ex) {
|
||||
log.error("[parseSmsReceiveStatus][text({}) 解析发生异常]", text, ex);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable;
|
||||
|
||||
@Override
|
||||
public SmsCommonResult<SmsTemplateRespDTO> getSmsTemplate(String apiTemplateId) {
|
||||
// 执行短信发送
|
||||
SmsCommonResult<SmsTemplateRespDTO> result;
|
||||
try {
|
||||
result = doGetSmsTemplate(apiTemplateId);
|
||||
} catch (Throwable ex) {
|
||||
// 打印异常日志
|
||||
log.error("[getSmsTemplate][获得短信模板({}) 发生异常]", apiTemplateId, ex);
|
||||
// 封装返回
|
||||
return SmsCommonResult.error(ex);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected abstract SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) throws Throwable;
|
||||
|
||||
}
|
@@ -0,0 +1,90 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.client.impl;
|
||||
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.SmsClient;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.SmsClientFactory;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun.AliyunSmsClient;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian.YunpianSmsClient;
|
||||
import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum;
|
||||
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/**
|
||||
* 短信客户端工厂接口
|
||||
*
|
||||
* @author zzf
|
||||
*/
|
||||
@Validated
|
||||
@Slf4j
|
||||
public class SmsClientFactoryImpl implements SmsClientFactory {
|
||||
|
||||
/**
|
||||
* 短信客户端 Map
|
||||
* key:渠道编号,使用 {@link SmsChannelProperties#getId()}
|
||||
*/
|
||||
private final ConcurrentMap<Long, AbstractSmsClient> channelIdClients = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 短信客户端 Map
|
||||
* key:渠道编码,使用 {@link SmsChannelProperties#getCode()} ()}
|
||||
*
|
||||
* 注意,一些场景下,需要获得某个渠道类型的客户端,所以需要使用它。
|
||||
* 例如说,解析短信接收结果,是相对通用的,不需要使用某个渠道编号的 {@link #channelIdClients}
|
||||
*/
|
||||
private final ConcurrentMap<String, AbstractSmsClient> channelCodeClients = new ConcurrentHashMap<>();
|
||||
|
||||
public SmsClientFactoryImpl() {
|
||||
// 初始化 channelCodeClients 集合
|
||||
Arrays.stream(SmsChannelEnum.values()).forEach(channel -> {
|
||||
// 创建一个空的 SmsChannelProperties 对象
|
||||
SmsChannelProperties properties = new SmsChannelProperties().setCode(channel.getCode())
|
||||
.setApiKey("default").setApiSecret("default");
|
||||
// 创建 Sms 客户端
|
||||
AbstractSmsClient smsClient = createSmsClient(properties);
|
||||
channelCodeClients.put(channel.getCode(), smsClient);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public SmsClient getSmsClient(Long channelId) {
|
||||
return channelIdClients.get(channelId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SmsClient getSmsClient(String channelCode) {
|
||||
return channelCodeClients.get(channelCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createOrUpdateSmsClient(SmsChannelProperties properties) {
|
||||
AbstractSmsClient client = channelIdClients.get(properties.getId());
|
||||
if (client == null) {
|
||||
client = this.createSmsClient(properties);
|
||||
client.init();
|
||||
channelIdClients.put(client.getId(), client);
|
||||
} else {
|
||||
client.refresh(properties);
|
||||
}
|
||||
}
|
||||
|
||||
private AbstractSmsClient createSmsClient(SmsChannelProperties properties) {
|
||||
SmsChannelEnum channelEnum = SmsChannelEnum.getByCode(properties.getCode());
|
||||
Assert.notNull(channelEnum, String.format("渠道类型(%s) 为空", channelEnum));
|
||||
// 创建客户端
|
||||
switch (channelEnum) {
|
||||
case ALIYUN: return new AliyunSmsClient(properties);
|
||||
case YUN_PIAN: return new YunpianSmsClient(properties);
|
||||
case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties);
|
||||
}
|
||||
// 创建失败,错误日志 + 抛出异常
|
||||
log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties);
|
||||
throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", properties));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,212 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient;
|
||||
import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
|
||||
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
|
||||
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import com.aliyuncs.AcsRequest;
|
||||
import com.aliyuncs.AcsResponse;
|
||||
import com.aliyuncs.DefaultAcsClient;
|
||||
import com.aliyuncs.IAcsClient;
|
||||
import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest;
|
||||
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
|
||||
import com.aliyuncs.exceptions.ClientException;
|
||||
import com.aliyuncs.profile.DefaultProfile;
|
||||
import com.aliyuncs.profile.IClientProfile;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 阿里短信客户端的实现类
|
||||
*
|
||||
* @author zzf
|
||||
* @date 2021/1/25 14:17
|
||||
*/
|
||||
@Slf4j
|
||||
public class AliyunSmsClient extends AbstractSmsClient {
|
||||
|
||||
/**
|
||||
* REGION, 使用杭州
|
||||
*/
|
||||
private static final String ENDPOINT = "cn-hangzhou";
|
||||
|
||||
/**
|
||||
* 阿里云客户端
|
||||
*/
|
||||
private volatile IAcsClient client;
|
||||
|
||||
public AliyunSmsClient(SmsChannelProperties properties) {
|
||||
super(properties, new AliyunSmsCodeMapping());
|
||||
Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
|
||||
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
IClientProfile profile = DefaultProfile.getProfile(ENDPOINT, properties.getApiKey(), properties.getApiSecret());
|
||||
client = new DefaultAcsClient(profile);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
|
||||
String apiTemplateId, List<KeyValue<String, Object>> templateParams) {
|
||||
// 构建参数
|
||||
SendSmsRequest request = new SendSmsRequest();
|
||||
request.setPhoneNumbers(mobile);
|
||||
request.setSignName(properties.getSignature());
|
||||
request.setTemplateCode(apiTemplateId);
|
||||
request.setTemplateParam(JsonUtils.toJsonString(MapUtils.convertMap(templateParams)));
|
||||
request.setOutId(String.valueOf(sendLogId));
|
||||
// 执行请求
|
||||
return invoke(request, response -> new SmsSendRespDTO().setSerialNo(response.getBizId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable {
|
||||
List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);
|
||||
return statuses.stream().map(status -> {
|
||||
SmsReceiveRespDTO resp = new SmsReceiveRespDTO();
|
||||
resp.setSuccess(status.getSuccess());
|
||||
resp.setErrorCode(status.getErrCode()).setErrorMsg(status.getErrMsg());
|
||||
resp.setMobile(status.getPhoneNumber()).setReceiveTime(status.getReportTime());
|
||||
resp.setSerialNo(status.getBizId()).setLogId(Long.valueOf(status.getOutId()));
|
||||
return resp;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) {
|
||||
// 构建参数
|
||||
QuerySmsTemplateRequest request = new QuerySmsTemplateRequest();
|
||||
request.setTemplateCode(apiTemplateId);
|
||||
// 执行请求
|
||||
return invoke(request, response -> {
|
||||
SmsTemplateRespDTO data = new SmsTemplateRespDTO();
|
||||
data.setId(response.getTemplateCode()).setContent(response.getTemplateContent());
|
||||
data.setAuditStatus(convertSmsTemplateAuditStatus(response.getTemplateStatus())).setAuditReason(response.getReason());
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Integer convertSmsTemplateAuditStatus(Integer templateStatus) {
|
||||
switch (templateStatus) {
|
||||
case 0: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
|
||||
case 1: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
|
||||
case 2: return SmsTemplateAuditStatusEnum.FAIL.getStatus();
|
||||
default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
<T extends AcsResponse, R> SmsCommonResult<R> invoke(AcsRequest<T> request, Function<T, R> responseConsumer) {
|
||||
try {
|
||||
// 执行发送. 由于阿里云 sms 短信没有统一的 Response,但是有统一的 code、message、requestId 属性,所以只好反射
|
||||
T sendResult = client.getAcsResponse(request);
|
||||
String code = (String) ReflectUtil.getFieldValue(sendResult, "code");
|
||||
String message = (String) ReflectUtil.getFieldValue(sendResult, "message");
|
||||
String requestId = (String) ReflectUtil.getFieldValue(sendResult, "requestId");
|
||||
// 解析结果
|
||||
R data = null;
|
||||
if (Objects.equals(code, "OK")) { // 请求成功的情况下
|
||||
data = responseConsumer.apply(sendResult);
|
||||
}
|
||||
// 拼接结果
|
||||
return SmsCommonResult.build(code, message, requestId, data, codeMapping);
|
||||
} catch (ClientException ex) {
|
||||
return SmsCommonResult.build(ex.getErrCode(), formatResultMsg(ex), ex.getRequestId(), null, codeMapping);
|
||||
}
|
||||
}
|
||||
|
||||
private static String formatResultMsg(ClientException ex) {
|
||||
if (StrUtil.isEmpty(ex.getErrorDescription())) {
|
||||
return ex.getErrMsg();
|
||||
}
|
||||
return ex.getErrMsg() + " => " + ex.getErrorDescription();
|
||||
}
|
||||
|
||||
/**
|
||||
* 短信接收状态
|
||||
*
|
||||
* 参见 https://help.aliyun.com/document_detail/101867.html 文档
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public static class SmsReceiveStatus {
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@JsonProperty("phone_number")
|
||||
private String phoneNumber;
|
||||
/**
|
||||
* 发送时间
|
||||
*/
|
||||
@JsonProperty("send_time")
|
||||
@JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
|
||||
private Date sendTime;
|
||||
/**
|
||||
* 状态报告时间
|
||||
*/
|
||||
@JsonProperty("report_time")
|
||||
@JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
|
||||
private Date reportTime;
|
||||
/**
|
||||
* 是否接收成功
|
||||
*/
|
||||
private Boolean success;
|
||||
/**
|
||||
* 状态报告说明
|
||||
*/
|
||||
@JsonProperty("err_msg")
|
||||
private String errMsg;
|
||||
/**
|
||||
* 状态报告编码
|
||||
*/
|
||||
@JsonProperty("err_code")
|
||||
private String errCode;
|
||||
/**
|
||||
* 发送序列号
|
||||
*/
|
||||
@JsonProperty("biz_id")
|
||||
private String bizId;
|
||||
/**
|
||||
* 用户序列号
|
||||
*
|
||||
* 这里我们传递的是 SysSmsLogDO 的日志编号
|
||||
*/
|
||||
@JsonProperty("out_id")
|
||||
private String outId;
|
||||
/**
|
||||
* 短信长度,例如说 1、2、3
|
||||
*
|
||||
* 140 字节算一条短信,短信长度超过 140 字节时会拆分成多条短信发送
|
||||
*/
|
||||
@JsonProperty("sms_size")
|
||||
private Integer smsSize;
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
||||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping;
|
||||
|
||||
import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* 阿里云的 SmsCodeMapping 实现类
|
||||
*
|
||||
* 参见 https://help.aliyun.com/document_detail/101346.htm 文档
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class AliyunSmsCodeMapping implements SmsCodeMapping {
|
||||
|
||||
@Override
|
||||
public ErrorCode apply(String apiCode) {
|
||||
switch (apiCode) {
|
||||
case "OK": return GlobalErrorCodeConstants.SUCCESS;
|
||||
case "isv.ACCOUNT_NOT_EXISTS":
|
||||
case "isv.ACCOUNT_ABNORMAL":
|
||||
case "MissingAccessKeyId": return SMS_ACCOUNT_INVALID;
|
||||
case "isp.RAM_PERMISSION_DENY": return SMS_PERMISSION_DENY;
|
||||
case "isv.INVALID_JSON_PARAM":
|
||||
case "isv.INVALID_PARAMETERS": return SMS_API_PARAM_ERROR;
|
||||
case "isv.BUSINESS_LIMIT_CONTROL": return SMS_SEND_BUSINESS_LIMIT_CONTROL;
|
||||
case "isv.DAY_LIMIT_CONTROL": return SMS_SEND_DAY_LIMIT_CONTROL;
|
||||
case "isv.SMS_CONTENT_ILLEGAL": return SMS_SEND_CONTENT_INVALID;
|
||||
case "isv.SMS_TEMPLATE_ILLEGAL": return SMS_TEMPLATE_INVALID;
|
||||
case "isv.SMS_SIGNATURE_ILLEGAL":
|
||||
case "isv.SIGN_NAME_ILLEGAL":
|
||||
case "isv.SMS_SIGN_ILLEGAL": return SMS_SIGN_INVALID;
|
||||
case "isv.AMOUNT_NOT_ENOUGH":
|
||||
case "isv.OUT_OF_SERVICE": return SMS_ACCOUNT_MONEY_NOT_ENOUGH;
|
||||
case "isv.MOBILE_NUMBER_ILLEGAL": return SMS_MOBILE_INVALID;
|
||||
case "isv.TEMPLATE_MISSING_PARAMETERS": return SMS_TEMPLATE_PARAM_ERROR;
|
||||
}
|
||||
return SMS_UNKNOWN;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.client.impl.debug;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
||||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
|
||||
|
||||
/**
|
||||
* 钉钉的 SmsCodeMapping 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class DebugDingTalkCodeMapping implements SmsCodeMapping {
|
||||
|
||||
@Override
|
||||
public ErrorCode apply(String apiCode) {
|
||||
return Objects.equals(apiCode, "0") ? GlobalErrorCodeConstants.SUCCESS : SMS_UNKNOWN;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,96 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.client.impl.debug;
|
||||
|
||||
import cn.hutool.core.codec.Base64;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import cn.hutool.crypto.digest.HmacAlgorithm;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient;
|
||||
import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
|
||||
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
|
||||
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 基于钉钉 WebHook 实现的调试的短信客户端实现类
|
||||
*
|
||||
* 考虑到省钱,我们使用钉钉 WebHook 模拟发送短信,方便调试。
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class DebugDingTalkSmsClient extends AbstractSmsClient {
|
||||
|
||||
public DebugDingTalkSmsClient(SmsChannelProperties properties) {
|
||||
super(properties, new DebugDingTalkCodeMapping());
|
||||
Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
|
||||
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
|
||||
String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
|
||||
// 构建请求
|
||||
String url = buildUrl("robot/send");
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("msgtype", "text");
|
||||
String content = String.format("【模拟短信】\n手机号:%s\n短信日志编号:%d\n模板参数:%s",
|
||||
mobile, sendLogId, MapUtils.convertMap(templateParams));
|
||||
params.put("text", MapUtil.builder().put("content", content).build());
|
||||
// 执行请求
|
||||
String responseText = HttpUtil.post(url, JsonUtils.toJsonString(params));
|
||||
// 解析结果
|
||||
Map<?, ?> responseObj = JsonUtils.parseObject(responseText, Map.class);
|
||||
return SmsCommonResult.build(MapUtil.getStr(responseObj, "errcode"), MapUtil.getStr(responseObj, "errorMsg"),
|
||||
null, new SmsSendRespDTO().setSerialNo(StrUtil.uuid()), codeMapping);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建请求地址
|
||||
*
|
||||
* 参见 https://developers.dingtalk.com/document/app/custom-robot-access/title-nfv-794-g71 文档
|
||||
*
|
||||
* @param path 请求路径
|
||||
* @return 请求地址
|
||||
*/
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private String buildUrl(String path) {
|
||||
// 生成 timestamp
|
||||
long timestamp = System.currentTimeMillis();
|
||||
// 生成 sign
|
||||
String secret = properties.getApiSecret();
|
||||
String stringToSign = timestamp + "\n" + secret;
|
||||
byte[] signData = DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.bytes(secret)).digest(stringToSign);
|
||||
String sign = Base64.encode(signData);
|
||||
// 构建最终 URL
|
||||
return String.format("https://oapi.dingtalk.com/%s?access_token=%s×tamp=%d&sign=%s",
|
||||
path, properties.getApiKey(), timestamp, sign);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable {
|
||||
throw new UnsupportedOperationException("模拟短信客户端,暂时无需解析回调");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) {
|
||||
SmsTemplateRespDTO data = new SmsTemplateRespDTO().setId(apiTemplateId).setContent("")
|
||||
.setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason("");
|
||||
return SmsCommonResult.build("0", "success", null, data, codeMapping);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,204 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.core.util.URLUtil;
|
||||
import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient;
|
||||
import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
|
||||
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.yunpian.sdk.YunpianClient;
|
||||
import com.yunpian.sdk.constant.YunpianConstant;
|
||||
import com.yunpian.sdk.model.Result;
|
||||
import com.yunpian.sdk.model.Template;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 云片短信客户端的实现类
|
||||
*
|
||||
* @author zzf
|
||||
* @date 9:48 2021/3/5
|
||||
*/
|
||||
@Slf4j
|
||||
public class YunpianSmsClient extends AbstractSmsClient {
|
||||
|
||||
/**
|
||||
* 云信短信客户端
|
||||
*/
|
||||
private volatile YunpianClient client;
|
||||
|
||||
public YunpianSmsClient(SmsChannelProperties properties) {
|
||||
super(properties, new YunpianSmsCodeMapping());
|
||||
Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doInit() {
|
||||
YunpianClient oldClient = client;
|
||||
// 初始化新的客户端
|
||||
YunpianClient newClient = new YunpianClient(properties.getApiKey());
|
||||
newClient.init();
|
||||
this.client = newClient;
|
||||
// 销毁老的客户端
|
||||
if (oldClient != null) {
|
||||
oldClient.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
|
||||
String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
|
||||
return invoke(() -> {
|
||||
Map<String, String> request = new HashMap<>();
|
||||
request.put(YunpianConstant.MOBILE, mobile);
|
||||
request.put(YunpianConstant.TPL_ID, apiTemplateId);
|
||||
request.put(YunpianConstant.TPL_VALUE, formatTplValue(templateParams));
|
||||
request.put(YunpianConstant.UID, String.valueOf(sendLogId));
|
||||
request.put(YunpianConstant.CALLBACK_URL, properties.getCallbackUrl());
|
||||
return client.sms().tpl_single_send(request);
|
||||
}, response -> new SmsSendRespDTO().setSerialNo(String.valueOf(response.getSid())));
|
||||
}
|
||||
|
||||
private static String formatTplValue(List<KeyValue<String, Object>> templateParams) {
|
||||
if (CollUtil.isEmpty(templateParams)) {
|
||||
return "";
|
||||
}
|
||||
// 参考 https://www.yunpian.com/official/document/sms/zh_cn/introduction_demos_encode_sample 格式化
|
||||
StringJoiner joiner = new StringJoiner("&");
|
||||
templateParams.forEach(param -> joiner.add(String.format("#%s#=%s", param.getKey(), URLUtil.encode(String.valueOf(param.getValue())))));
|
||||
return joiner.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable {
|
||||
List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);
|
||||
return statuses.stream().map(status -> {
|
||||
SmsReceiveRespDTO resp = new SmsReceiveRespDTO();
|
||||
resp.setSuccess(Objects.equals(status.getReportStatus(), "SUCCESS"));
|
||||
resp.setErrorCode(status.getErrorMsg()).setErrorMsg(status.getErrorDetail());
|
||||
resp.setMobile(status.getMobile()).setReceiveTime(status.getUserReceiveTime());
|
||||
resp.setSerialNo(String.valueOf(status.getSid())).setLogId(status.getUid());
|
||||
return resp;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) throws Throwable {
|
||||
return invoke(() -> {
|
||||
Map<String, String> request = new HashMap<>();
|
||||
request.put(YunpianConstant.APIKEY, properties.getApiKey());
|
||||
request.put(YunpianConstant.TPL_ID, apiTemplateId);
|
||||
return client.tpl().get(request);
|
||||
}, response -> {
|
||||
Template template = response.get(0);
|
||||
return new SmsTemplateRespDTO().setId(String.valueOf(template.getTpl_id())).setContent(template.getTpl_content())
|
||||
.setAuditStatus(convertSmsTemplateAuditStatus(template.getCheck_status())).setAuditReason(template.getReason());
|
||||
});
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Integer convertSmsTemplateAuditStatus(String checkStatus) {
|
||||
switch (checkStatus) {
|
||||
case "CHECKING": return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
|
||||
case "SUCCESS": return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
|
||||
case "FAIL": return SmsTemplateAuditStatusEnum.FAIL.getStatus();
|
||||
default: throw new IllegalArgumentException(String.format("未知审核状态(%s)", checkStatus));
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
<T, R> SmsCommonResult<R> invoke(Supplier<Result<T>> requestConsumer, Function<T, R> responseConsumer) throws Throwable {
|
||||
// 执行请求
|
||||
Result<T> result = requestConsumer.get();
|
||||
if (result.getThrowable() != null) {
|
||||
throw result.getThrowable();
|
||||
}
|
||||
// 解析结果
|
||||
R data = null;
|
||||
if (result.getData() != null) {
|
||||
data = responseConsumer.apply(result.getData());
|
||||
}
|
||||
// 拼接结果
|
||||
return SmsCommonResult.build(String.valueOf(result.getCode()), formatResultMsg(result), null, data, codeMapping);
|
||||
}
|
||||
|
||||
private static String formatResultMsg(Result<?> sendResult) {
|
||||
if (StrUtil.isEmpty(sendResult.getDetail())) {
|
||||
return sendResult.getMsg();
|
||||
}
|
||||
return sendResult.getMsg() + " => " + sendResult.getDetail();
|
||||
}
|
||||
|
||||
/**
|
||||
* 短信接收状态
|
||||
*
|
||||
* 参见 https://www.yunpian.com/official/document/sms/zh_cn/domestic_push_report 文档
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public static class SmsReceiveStatus {
|
||||
|
||||
/**
|
||||
* 接收状态
|
||||
*
|
||||
* 目前仅有 SUCCESS / FAIL,所以使用 Boolean 接收
|
||||
*/
|
||||
@JsonProperty("report_status")
|
||||
private String reportStatus;
|
||||
/**
|
||||
* 接收手机号
|
||||
*/
|
||||
private String mobile;
|
||||
/**
|
||||
* 运营商返回的代码,如:"DB:0103"
|
||||
*
|
||||
* 由于不同运营商信息不同,此字段仅供参考;
|
||||
*/
|
||||
@JsonProperty("error_msg")
|
||||
private String errorMsg;
|
||||
/**
|
||||
* 运营商反馈代码的中文解释
|
||||
*
|
||||
* 默认不推送此字段,如需推送,请联系客服
|
||||
*/
|
||||
@JsonProperty("error_detail")
|
||||
private String errorDetail;
|
||||
/**
|
||||
* 短信编号
|
||||
*/
|
||||
private Long sid;
|
||||
/**
|
||||
* 用户自定义 id
|
||||
*
|
||||
* 这里我们传递的是 SysSmsLogDO 的日志编号
|
||||
*/
|
||||
private Long uid;
|
||||
/**
|
||||
* 用户接收时间
|
||||
*/
|
||||
@JsonProperty("user_receive_time")
|
||||
@JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
|
||||
private Date userReceiveTime;
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.SmsCodeMapping;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.SUCCESS;
|
||||
import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.*;
|
||||
import static com.yunpian.sdk.constant.Code.*;
|
||||
|
||||
/**
|
||||
* 云片的 SmsCodeMapping 实现类
|
||||
*
|
||||
* 参见 https://www.yunpian.com/official/document/sms/zh_CN/returnvalue_common 文档
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class YunpianSmsCodeMapping implements SmsCodeMapping {
|
||||
|
||||
@Override
|
||||
public ErrorCode apply(String apiCode) {
|
||||
int code = Integer.parseInt(apiCode);
|
||||
switch (code) {
|
||||
case OK: return SUCCESS;
|
||||
case ARGUMENT_MISSING: return SMS_API_PARAM_ERROR;
|
||||
case BAD_ARGUMENT_FORMAT: return SMS_TEMPLATE_PARAM_ERROR;
|
||||
case TPL_NOT_FOUND:
|
||||
case TPL_NOT_VALID: return SMS_TEMPLATE_INVALID;
|
||||
case MONEY_NOT_ENOUGH: return SMS_ACCOUNT_MONEY_NOT_ENOUGH;
|
||||
case BLACK_WORD: return SMS_SEND_CONTENT_INVALID;
|
||||
case DUP_IN_SHORT_TIME:
|
||||
case TOO_MANY_TIME_IN_5:
|
||||
case DAY_LIMIT_PER_MOBILE:
|
||||
case HOUR_LIMIT_PER_MOBILE: return SMS_SEND_BUSINESS_LIMIT_CONTROL;
|
||||
case BLACK_PHONE_FILTER: return SMS_MOBILE_BLACK;
|
||||
case SIGN_NOT_MATCH:
|
||||
case BAD_SIGN_FORMAT:
|
||||
case SIGN_NOT_VALID: return SMS_SIGN_INVALID;
|
||||
case BAD_API_KEY: return SMS_ACCOUNT_INVALID;
|
||||
case API_NOT_ALLOWED: return SMS_PERMISSION_DENY;
|
||||
case IP_NOT_ALLOWED: return SMS_IP_DENY;
|
||||
}
|
||||
return SMS_UNKNOWN;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.enums;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 短信渠道枚举
|
||||
*
|
||||
* @author zzf
|
||||
* @date 2021/1/25 10:56
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum SmsChannelEnum {
|
||||
|
||||
DEBUG_DING_TALK("DEBUG_DING_TALK", "调试(钉钉)"),
|
||||
YUN_PIAN("YUN_PIAN", "云片"),
|
||||
ALIYUN("ALIYUN", "阿里云"),
|
||||
// TENCENT("TENCENT", "腾讯云"),
|
||||
// HUA_WEI("HUA_WEI", "华为云"),
|
||||
;
|
||||
|
||||
/**
|
||||
* 编码
|
||||
*/
|
||||
private final String code;
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
public static SmsChannelEnum getByCode(String code) {
|
||||
return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values());
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.enums;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
|
||||
|
||||
/**
|
||||
* 短信框架的错误码枚举
|
||||
*
|
||||
* 短信框架,使用 2-001-000-000 段
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface SmsFrameworkErrorCodeConstants {
|
||||
|
||||
ErrorCode SMS_UNKNOWN = new ErrorCode(2001000000, "未知错误,需要解析");
|
||||
|
||||
// ========== 权限 / 限流等相关 2001000100 ==========
|
||||
|
||||
ErrorCode SMS_PERMISSION_DENY = new ErrorCode(2001000100, "没有发送短信的权限");
|
||||
// 云片:可以配置 IP 白名单,只有在白名单中才可以发送短信
|
||||
ErrorCode SMS_IP_DENY = new ErrorCode(2001000100, "IP 不允许发送短信");
|
||||
|
||||
// 阿里云:将短信发送频率限制在正常的业务限流范围内。默认短信验证码:使用同一签名,对同一个手机号验证码,支持 1 条 / 分钟,5 条 / 小时,累计 10 条 / 天。
|
||||
ErrorCode SMS_SEND_BUSINESS_LIMIT_CONTROL = new ErrorCode(2001000102, "指定手机的发送限流");
|
||||
// 阿里云:已经达到您在控制台设置的短信日发送量限额值。在国内消息设置 > 安全设置,修改发送总量阈值。
|
||||
ErrorCode SMS_SEND_DAY_LIMIT_CONTROL = new ErrorCode(2001000103, "每天的发送限流");
|
||||
|
||||
ErrorCode SMS_SEND_CONTENT_INVALID = new ErrorCode(2001000104, "短信内容有敏感词");
|
||||
|
||||
// ========== 模板相关 2001000200 ==========
|
||||
ErrorCode SMS_TEMPLATE_INVALID = new ErrorCode(2001000200, "短信模板不合法"); // 包括短信模板不存在
|
||||
ErrorCode SMS_TEMPLATE_PARAM_ERROR = new ErrorCode(2001000201, "模板参数不正确");
|
||||
|
||||
// ========== 签名相关 2001000300 ==========
|
||||
ErrorCode SMS_SIGN_INVALID = new ErrorCode(2001000300, "短信签名不可用");
|
||||
|
||||
// ========== 账户相关 2001000400 ==========
|
||||
ErrorCode SMS_ACCOUNT_MONEY_NOT_ENOUGH = new ErrorCode(2001000400, "账户余额不足");
|
||||
ErrorCode SMS_ACCOUNT_INVALID = new ErrorCode(2001000401, "apiKey 不存在");
|
||||
|
||||
// ========== 其它相关 2001000900 开头 ==========
|
||||
ErrorCode SMS_API_PARAM_ERROR = new ErrorCode(2001000900, "请求参数缺失");
|
||||
ErrorCode SMS_MOBILE_INVALID = new ErrorCode(2001000901, "手机格式不正确");
|
||||
ErrorCode SMS_MOBILE_BLACK = new ErrorCode(2001000902, "手机号在黑名单中");
|
||||
|
||||
ErrorCode EXCEPTION = new ErrorCode(2001000999, "调用异常");
|
||||
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 短信模板的审核状态枚举
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum SmsTemplateAuditStatusEnum {
|
||||
|
||||
CHECKING(1),
|
||||
SUCCESS(2),
|
||||
FAIL(3);
|
||||
|
||||
private final Integer status;
|
||||
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.property;
|
||||
|
||||
import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum;
|
||||
import lombok.Data;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 短信渠道配置类
|
||||
*
|
||||
* @author zzf
|
||||
* @date 2021/1/25 17:01
|
||||
*/
|
||||
@Data
|
||||
@Validated
|
||||
public class SmsChannelProperties {
|
||||
|
||||
/**
|
||||
* 渠道编号
|
||||
*/
|
||||
@NotNull(message = "短信渠道 ID 不能为空")
|
||||
private Long id;
|
||||
/**
|
||||
* 短信签名
|
||||
*/
|
||||
@NotEmpty(message = "短信签名不能为空")
|
||||
private String signature;
|
||||
/**
|
||||
* 渠道编码
|
||||
*
|
||||
* 枚举 {@link SmsChannelEnum}
|
||||
*/
|
||||
@NotEmpty(message = "渠道编码不能为空")
|
||||
private String code;
|
||||
/**
|
||||
* 短信 API 的账号
|
||||
*/
|
||||
@NotEmpty(message = "短信 API 的账号不能为空")
|
||||
private String apiKey;
|
||||
/**
|
||||
* 短信 API 的秘钥
|
||||
*/
|
||||
@NotEmpty(message = "短信 API 的秘钥不能为空")
|
||||
private String apiSecret;
|
||||
/**
|
||||
* 短信发送回调 URL
|
||||
*/
|
||||
private String callbackUrl;
|
||||
|
||||
}
|
@@ -0,0 +1,224 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun;
|
||||
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.iocoder.dashboard.framework.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
|
||||
import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
|
||||
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
|
||||
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
|
||||
import com.aliyuncs.AcsRequest;
|
||||
import com.aliyuncs.IAcsClient;
|
||||
import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest;
|
||||
import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateResponse;
|
||||
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
|
||||
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
|
||||
import com.aliyuncs.exceptions.ClientException;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentMatcher;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR;
|
||||
import static cn.iocoder.dashboard.framework.test.core.util.RandomUtils.*;
|
||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link AliyunSmsClient} 的单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class AliyunSmsClientTest extends BaseMockitoUnitTest {
|
||||
|
||||
private final SmsChannelProperties properties = new SmsChannelProperties()
|
||||
.setApiKey(randomString()) // 随机一个 apiKey,避免构建报错
|
||||
.setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错
|
||||
.setSignature("芋道源码");
|
||||
|
||||
@InjectMocks
|
||||
private final AliyunSmsClient smsClient = new AliyunSmsClient(properties);
|
||||
|
||||
@Mock
|
||||
private IAcsClient client;
|
||||
|
||||
@Test
|
||||
public void testDoInit() {
|
||||
// 准备参数
|
||||
// mock 方法
|
||||
|
||||
// 调用
|
||||
smsClient.doInit();
|
||||
// 断言
|
||||
assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "acsClient"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testDoSendSms() throws ClientException {
|
||||
// 准备参数
|
||||
Long sendLogId = randomLongId();
|
||||
String mobile = randomString();
|
||||
String apiTemplateId = randomString();
|
||||
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
|
||||
new KeyValue<>("code", 1234), new KeyValue<>("op", "login"));
|
||||
// mock 方法
|
||||
SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> o.setCode("OK"));
|
||||
when(client.getAcsResponse(argThat((ArgumentMatcher<SendSmsRequest>) acsRequest -> {
|
||||
assertEquals(mobile, acsRequest.getPhoneNumbers());
|
||||
assertEquals(properties.getSignature(), acsRequest.getSignName());
|
||||
assertEquals(apiTemplateId, acsRequest.getTemplateCode());
|
||||
assertEquals(toJsonString(MapUtils.convertMap(templateParams)), acsRequest.getTemplateParam());
|
||||
assertEquals(sendLogId.toString(), acsRequest.getOutId());
|
||||
return true;
|
||||
}))).thenReturn(response);
|
||||
|
||||
// 调用
|
||||
SmsCommonResult<SmsSendRespDTO> result = smsClient.doSendSms(sendLogId, mobile,
|
||||
apiTemplateId, templateParams);
|
||||
// 断言
|
||||
assertEquals(response.getCode(), result.getApiCode());
|
||||
assertEquals(response.getMessage(), result.getApiMsg());
|
||||
assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
|
||||
assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
|
||||
assertEquals(response.getRequestId(), result.getApiRequestId());
|
||||
// 断言结果
|
||||
assertEquals(response.getBizId(), result.getData().getSerialNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoTParseSmsReceiveStatus() throws Throwable {
|
||||
// 准备参数
|
||||
String text = "[\n" +
|
||||
" {\n" +
|
||||
" \"phone_number\" : \"13900000001\",\n" +
|
||||
" \"send_time\" : \"2017-01-01 11:12:13\",\n" +
|
||||
" \"report_time\" : \"2017-02-02 22:23:24\",\n" +
|
||||
" \"success\" : true,\n" +
|
||||
" \"err_code\" : \"DELIVERED\",\n" +
|
||||
" \"err_msg\" : \"用户接收成功\",\n" +
|
||||
" \"sms_size\" : \"1\",\n" +
|
||||
" \"biz_id\" : \"12345\",\n" +
|
||||
" \"out_id\" : \"67890\"\n" +
|
||||
" }\n" +
|
||||
"]";
|
||||
// mock 方法
|
||||
|
||||
// 调用
|
||||
List<SmsReceiveRespDTO> statuses = smsClient.doParseSmsReceiveStatus(text);
|
||||
// 断言
|
||||
assertEquals(1, statuses.size());
|
||||
assertTrue(statuses.get(0).getSuccess());
|
||||
assertEquals("DELIVERED", statuses.get(0).getErrorCode());
|
||||
assertEquals("用户接收成功", statuses.get(0).getErrorMsg());
|
||||
assertEquals("13900000001", statuses.get(0).getMobile());
|
||||
assertEquals(DateUtils.buildTime(2017, 2, 2, 22, 23, 24), statuses.get(0).getReceiveTime());
|
||||
assertEquals("12345", statuses.get(0).getSerialNo());
|
||||
assertEquals(67890L, statuses.get(0).getLogId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoGetSmsTemplate() throws ClientException {
|
||||
// 准备参数
|
||||
String apiTemplateId = randomString();
|
||||
// mock 方法
|
||||
QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> {
|
||||
o.setCode("OK");
|
||||
o.setTemplateStatus(1); // 设置模板通过
|
||||
});
|
||||
when(client.getAcsResponse(argThat((ArgumentMatcher<QuerySmsTemplateRequest>) acsRequest -> {
|
||||
assertEquals(apiTemplateId, acsRequest.getTemplateCode());
|
||||
return true;
|
||||
}))).thenReturn(response);
|
||||
|
||||
// 调用
|
||||
SmsCommonResult<SmsTemplateRespDTO> result = smsClient.doGetSmsTemplate(apiTemplateId);
|
||||
// 断言
|
||||
assertEquals(response.getCode(), result.getApiCode());
|
||||
assertEquals(response.getMessage(), result.getApiMsg());
|
||||
assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
|
||||
assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
|
||||
assertEquals(response.getRequestId(), result.getApiRequestId());
|
||||
// 断言结果
|
||||
assertEquals(response.getTemplateCode(), result.getData().getId());
|
||||
assertEquals(response.getTemplateContent(), result.getData().getContent());
|
||||
assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus());
|
||||
assertEquals(response.getReason(), result.getData().getAuditReason());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertSmsTemplateAuditStatus() {
|
||||
assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(),
|
||||
smsClient.convertSmsTemplateAuditStatus(0));
|
||||
assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(),
|
||||
smsClient.convertSmsTemplateAuditStatus(1));
|
||||
assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(),
|
||||
smsClient.convertSmsTemplateAuditStatus(2));
|
||||
assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus(3),
|
||||
"未知审核状态(3)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testInvoke_throwable() throws ClientException {
|
||||
// 准备参数
|
||||
QuerySmsTemplateRequest request = new QuerySmsTemplateRequest();
|
||||
// mock 方法
|
||||
ClientException ex = new ClientException("isv.INVALID_PARAMETERS", "参数不正确", randomString());
|
||||
when(client.getAcsResponse(any(AcsRequest.class))).thenThrow(ex);
|
||||
|
||||
// 调用,并断言异常
|
||||
SmsCommonResult<?> result = smsClient.invoke(request,null);
|
||||
// 断言
|
||||
assertEquals(ex.getErrCode(), result.getApiCode());
|
||||
assertEquals(ex.getErrMsg(), result.getApiMsg());
|
||||
assertEquals(SMS_API_PARAM_ERROR.getCode(), result.getCode());
|
||||
assertEquals(SMS_API_PARAM_ERROR.getMsg(), result.getMsg());
|
||||
assertEquals(ex.getRequestId(), result.getApiRequestId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvoke_success() throws ClientException {
|
||||
// 准备参数
|
||||
QuerySmsTemplateRequest request = new QuerySmsTemplateRequest();
|
||||
Function<QuerySmsTemplateResponse, SmsTemplateRespDTO> responseConsumer = response -> {
|
||||
SmsTemplateRespDTO data = new SmsTemplateRespDTO();
|
||||
data.setId(response.getTemplateCode()).setContent(response.getTemplateContent());
|
||||
data.setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(response.getReason());
|
||||
return data;
|
||||
};
|
||||
// mock 方法
|
||||
QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> {
|
||||
o.setCode("OK");
|
||||
o.setTemplateStatus(1); // 设置模板通过
|
||||
});
|
||||
when(client.getAcsResponse(any(AcsRequest.class))).thenReturn(response);
|
||||
|
||||
// 调用
|
||||
SmsCommonResult<SmsTemplateRespDTO> result = smsClient.invoke(request, responseConsumer);
|
||||
// 断言
|
||||
assertEquals(response.getCode(), result.getApiCode());
|
||||
assertEquals(response.getMessage(), result.getApiMsg());
|
||||
assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
|
||||
assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
|
||||
assertEquals(response.getRequestId(), result.getApiRequestId());
|
||||
// 断言结果
|
||||
assertEquals(response.getTemplateCode(), result.getData().getId());
|
||||
assertEquals(response.getTemplateContent(), result.getData().getContent());
|
||||
assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus());
|
||||
assertEquals(response.getReason(), result.getData().getAuditReason());
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun;
|
||||
|
||||
import cn.iocoder.dashboard.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* {@link AliyunSmsCodeMapping} 的单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class AliyunSmsCodeMappingTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private AliyunSmsCodeMapping codeMapping;
|
||||
|
||||
@Test
|
||||
public void testApply() {
|
||||
assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply("OK"));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("MissingAccessKeyId"));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("isv.ACCOUNT_NOT_EXISTS"));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("isv.ACCOUNT_ABNORMAL"));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_DAY_LIMIT_CONTROL, codeMapping.apply("isv.DAY_LIMIT_CONTROL"));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID, codeMapping.apply("isv.SMS_CONTENT_ILLEGAL"));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SMS_SIGN_ILLEGAL"));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SIGN_NAME_ILLEGAL"));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("isp.RAM_PERMISSION_DENY"));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("isv.OUT_OF_SERVICE"));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("isv.AMOUNT_NOT_ENOUGH"));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("isv.SMS_TEMPLATE_ILLEGAL"));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SMS_SIGNATURE_ILLEGAL"));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply("isv.INVALID_PARAMETERS"));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply("isv.INVALID_JSON_PARAM"));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID, codeMapping.apply("isv.MOBILE_NUMBER_ILLEGAL"));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("isv.TEMPLATE_MISSING_PARAMETERS"));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("isv.BUSINESS_LIMIT_CONTROL"));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,202 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian;
|
||||
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.iocoder.dashboard.framework.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.SmsCommonResult;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsReceiveRespDTO;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsSendRespDTO;
|
||||
import cn.iocoder.dashboard.framework.sms.core.client.dto.SmsTemplateRespDTO;
|
||||
import cn.iocoder.dashboard.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
|
||||
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
|
||||
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.yunpian.sdk.YunpianClient;
|
||||
import com.yunpian.sdk.api.SmsApi;
|
||||
import com.yunpian.sdk.api.TplApi;
|
||||
import com.yunpian.sdk.constant.YunpianConstant;
|
||||
import com.yunpian.sdk.model.Result;
|
||||
import com.yunpian.sdk.model.SmsSingleSend;
|
||||
import com.yunpian.sdk.model.Template;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static cn.iocoder.dashboard.framework.test.core.util.RandomUtils.*;
|
||||
import static com.yunpian.sdk.constant.Code.OK;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 对 {@link YunpianSmsClient} 的单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class YunpianSmsClientTest extends BaseMockitoUnitTest {
|
||||
|
||||
private final SmsChannelProperties properties = new SmsChannelProperties()
|
||||
.setApiKey(randomString()); // 随机一个 apiKey,避免构建报错
|
||||
|
||||
@InjectMocks
|
||||
private final YunpianSmsClient smsClient = new YunpianSmsClient(properties);
|
||||
|
||||
@Mock
|
||||
private YunpianClient client;
|
||||
|
||||
@Test
|
||||
public void testDoInit() {
|
||||
// 准备参数
|
||||
// mock 方法
|
||||
|
||||
// 调用
|
||||
smsClient.doInit();
|
||||
// 断言
|
||||
assertNotEquals(client, ReflectUtil.getFieldValue(smsClient, "client"));
|
||||
verify(client, times(1)).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testDoSendSms() throws Throwable {
|
||||
// 准备参数
|
||||
Long sendLogId = randomLongId();
|
||||
String mobile = randomString();
|
||||
String apiTemplateId = randomString();
|
||||
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
|
||||
new KeyValue<>("code", 1234), new KeyValue<>("op", "login"));
|
||||
// mock sms 方法
|
||||
SmsApi smsApi = mock(SmsApi.class);
|
||||
when(client.sms()).thenReturn(smsApi);
|
||||
// mock tpl_single_send 方法
|
||||
Map<String, String> request = new HashMap<>();
|
||||
request.put(YunpianConstant.MOBILE, mobile);
|
||||
request.put(YunpianConstant.TPL_ID, apiTemplateId);
|
||||
request.put(YunpianConstant.TPL_VALUE, "#code#=1234&#op#=login");
|
||||
request.put(YunpianConstant.UID, String.valueOf(sendLogId));
|
||||
request.put(YunpianConstant.CALLBACK_URL, properties.getCallbackUrl());
|
||||
Result<SmsSingleSend> responseResult = randomPojo(Result.class, SmsSingleSend.class,
|
||||
o -> o.setCode(OK)); // API 发送成功的 code
|
||||
when(smsApi.tpl_single_send(eq(request))).thenReturn(responseResult);
|
||||
|
||||
// 调用
|
||||
SmsCommonResult<SmsSendRespDTO> result = smsClient.doSendSms(sendLogId, mobile,
|
||||
apiTemplateId, templateParams);
|
||||
// 断言
|
||||
assertEquals(String.valueOf(responseResult.getCode()), result.getApiCode());
|
||||
assertEquals(responseResult.getMsg() + " => " + responseResult.getDetail(), result.getApiMsg());
|
||||
assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
|
||||
assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
|
||||
assertNull(result.getApiRequestId());
|
||||
// 断言结果
|
||||
assertEquals(String.valueOf(responseResult.getData().getSid()), result.getData().getSerialNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoParseSmsReceiveStatus() throws Throwable {
|
||||
// 准备参数
|
||||
String text = "[{\"sid\":9527,\"uid\":1024,\"user_receive_time\":\"2014-03-17 22:55:21\",\"error_msg\":\"\",\"mobile\":\"15205201314\",\"report_status\":\"SUCCESS\"}]";
|
||||
// mock 方法
|
||||
|
||||
// 调用
|
||||
|
||||
// 断言
|
||||
// 调用
|
||||
List<SmsReceiveRespDTO> statuses = smsClient.doParseSmsReceiveStatus(text);
|
||||
// 断言
|
||||
assertEquals(1, statuses.size());
|
||||
assertTrue(statuses.get(0).getSuccess());
|
||||
assertEquals("", statuses.get(0).getErrorCode());
|
||||
assertNull(statuses.get(0).getErrorMsg());
|
||||
assertEquals("15205201314", statuses.get(0).getMobile());
|
||||
assertEquals(DateUtils.buildTime(2014, 3, 17, 22, 55, 21), statuses.get(0).getReceiveTime());
|
||||
assertEquals("9527", statuses.get(0).getSerialNo());
|
||||
assertEquals(1024L, statuses.get(0).getLogId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testDoGetSmsTemplate() throws Throwable {
|
||||
// 准备参数
|
||||
String apiTemplateId = randomString();
|
||||
// mock tpl 方法
|
||||
TplApi tplApi = mock(TplApi.class);
|
||||
when(client.tpl()).thenReturn(tplApi);
|
||||
// mock get 方法
|
||||
Map<String, String> request = new HashMap<>();
|
||||
request.put(YunpianConstant.APIKEY, properties.getApiKey());
|
||||
request.put(YunpianConstant.TPL_ID, apiTemplateId);
|
||||
Result<List<Template>> responseResult = randomPojo(Result.class, List.class, o -> {
|
||||
o.setCode(OK); // API 发送成功的 code
|
||||
o.setData(randomPojoList(Template.class, t -> t.setCheck_status("SUCCESS")));
|
||||
});
|
||||
when(tplApi.get(eq(request))).thenReturn(responseResult);
|
||||
|
||||
// 调用
|
||||
SmsCommonResult<SmsTemplateRespDTO> result = smsClient.doGetSmsTemplate(apiTemplateId);
|
||||
// 断言
|
||||
assertEquals(String.valueOf(responseResult.getCode()), result.getApiCode());
|
||||
assertEquals(responseResult.getMsg() + " => " + responseResult.getDetail(), result.getApiMsg());
|
||||
assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
|
||||
assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
|
||||
assertNull(result.getApiRequestId());
|
||||
// 断言结果
|
||||
Template template = responseResult.getData().get(0);
|
||||
assertEquals(template.getTpl_id().toString(), result.getData().getId());
|
||||
assertEquals(template.getTpl_content(), result.getData().getContent());
|
||||
assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus());
|
||||
assertEquals(template.getReason(), result.getData().getAuditReason());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertSmsTemplateAuditStatus() {
|
||||
assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(),
|
||||
smsClient.convertSmsTemplateAuditStatus("CHECKING"));
|
||||
assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(),
|
||||
smsClient.convertSmsTemplateAuditStatus("SUCCESS"));
|
||||
assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(),
|
||||
smsClient.convertSmsTemplateAuditStatus("FAIL"));
|
||||
assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus("test"),
|
||||
"未知审核状态(test)");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvoke_throwable() {
|
||||
// 准备参数
|
||||
Supplier<Result<Object>> requestConsumer =
|
||||
() -> new Result<>().setThrowable(new NullPointerException());
|
||||
// mock 方法
|
||||
|
||||
// 调用,并断言异常
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> smsClient.invoke(requestConsumer, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testInvoke_success() throws Throwable {
|
||||
// 准备参数
|
||||
Result<SmsSingleSend> responseResult = randomPojo(Result.class, SmsSingleSend.class, o -> o.setCode(OK));
|
||||
Supplier<Result<SmsSingleSend>> requestConsumer = () -> responseResult;
|
||||
Function<SmsSingleSend, SmsSendRespDTO> responseConsumer =
|
||||
smsSingleSend -> new SmsSendRespDTO().setSerialNo(String.valueOf(responseResult.getData().getSid()));
|
||||
// mock 方法
|
||||
|
||||
// 调用
|
||||
SmsCommonResult<SmsSendRespDTO> result = smsClient.invoke(requestConsumer, responseConsumer);
|
||||
// 断言
|
||||
assertEquals(String.valueOf(responseResult.getCode()), result.getApiCode());
|
||||
assertEquals(responseResult.getMsg() + " => " + responseResult.getDetail(), result.getApiMsg());
|
||||
assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
|
||||
assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
|
||||
assertNull(result.getApiRequestId());
|
||||
assertEquals(String.valueOf(responseResult.getData().getSid()), result.getData().getSerialNo());
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian;
|
||||
|
||||
import cn.iocoder.dashboard.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.dashboard.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
|
||||
import static com.yunpian.sdk.constant.Code.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* {@link YunpianSmsCodeMapping} 的单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
class YunpianSmsCodeMappingTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private YunpianSmsCodeMapping codeMapping;
|
||||
|
||||
@Test
|
||||
public void testApply() {
|
||||
assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply(String.valueOf(OK)));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply(String.valueOf(ARGUMENT_MISSING)));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply(String.valueOf(BAD_ARGUMENT_FORMAT)));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply(String.valueOf(MONEY_NOT_ENOUGH)));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply(String.valueOf(TPL_NOT_FOUND)));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply(String.valueOf(TPL_NOT_VALID)));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(DUP_IN_SHORT_TIME)));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(TOO_MANY_TIME_IN_5)));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(DAY_LIMIT_PER_MOBILE)));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply(String.valueOf(HOUR_LIMIT_PER_MOBILE)));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_BLACK, codeMapping.apply(String.valueOf(BLACK_PHONE_FILTER)));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply(String.valueOf(SIGN_NOT_MATCH)));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply(String.valueOf(SIGN_NOT_VALID)));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply(String.valueOf(BAD_SIGN_FORMAT)));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply(String.valueOf(BAD_API_KEY)));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply(String.valueOf(API_NOT_ALLOWED)));
|
||||
assertEquals(SmsFrameworkErrorCodeConstants.SMS_IP_DENY, codeMapping.apply(String.valueOf(IP_NOT_ALLOWED)));
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user