最新公众号管理平台后端

This commit is contained in:
sangchengzhi
2026-01-14 18:06:06 +08:00
commit e4826053ba
4700 changed files with 247006 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
package com.jojubanking.boot.framework.sms.config;
import com.jojubanking.boot.framework.sms.core.client.SmsClientFactory;
import com.jojubanking.boot.framework.sms.core.client.impl.SmsClientFactoryImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 短信配置类
*
* @author TW
*/
@Configuration
public class JojuSmsAutoConfiguration {
@Bean
public SmsClientFactory smsClientFactory() {
return new SmsClientFactoryImpl();
}
}

View File

@@ -0,0 +1,54 @@
package com.jojubanking.boot.framework.sms.core.client;
import com.jojubanking.boot.framework.common.core.KeyValue;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsReceiveRespDTO;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsSendRespDTO;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsTemplateRespDTO;
import java.util.List;
/**
* 短信客户端,用于对接各短信平台的 SDK实现短信发送等功能
*
* @author zzf
* @since 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);
}

View File

@@ -0,0 +1,36 @@
package com.jojubanking.boot.framework.sms.core.client;
import com.jojubanking.boot.framework.sms.core.property.SmsChannelProperties;
/**
* 短信客户端的工厂接口
*
* @author zzf
* @since 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);
}

View File

@@ -0,0 +1,17 @@
package com.jojubanking.boot.framework.sms.core.client;
import com.jojubanking.boot.framework.common.exception.ErrorCode;
import com.jojubanking.boot.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
import java.util.function.Function;
/**
* 将 API 的错误码,转换为通用的错误码
*
* @see SmsCommonResult
* @see SmsFrameworkErrorCodeConstants
*
* @author TW
*/
public interface SmsCodeMapping extends Function<String, ErrorCode> {
}

View File

@@ -0,0 +1,70 @@
package com.jojubanking.boot.framework.sms.core.client;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.lang.Assert;
import com.jojubanking.boot.framework.common.exception.ErrorCode;
import com.jojubanking.boot.framework.common.pojo.CommonResult;
import com.jojubanking.boot.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.experimental.Accessors;
/**
* 短信的 CommonResult 拓展类
*
* 考虑到不同的平台,返回的 code 和 msg 是不同的,所以统一额外返回 {@link #apiCode} 和 {@link #apiMsg} 字段
*
* 另外,一些短信平台(例如说阿里云、腾讯云)会返回一个请求编号,用于排查请求失败的问题,我们设置到 {@link #apiRequestId} 字段
*
* @author TW
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Accessors(chain = 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;
}
}

View File

@@ -0,0 +1,50 @@
package com.jojubanking.boot.framework.sms.core.client.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.Date;
/**
* 消息接收 Response DTO
*
* @author TW
*/
@Data
@Accessors(chain = true)
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;
}

View File

@@ -0,0 +1,20 @@
package com.jojubanking.boot.framework.sms.core.client.dto;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 短信发送 Response DTO
*
* @author TW
*/
@Data
@Accessors(chain = true)
public class SmsSendRespDTO {
/**
* 短信 API 发送返回的序号
*/
private String serialNo;
}

View File

@@ -0,0 +1,35 @@
package com.jojubanking.boot.framework.sms.core.client.dto;
import com.jojubanking.boot.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 短信模板 Response DTO
*
* @author TW
*/
@Data
@Accessors(chain = true)
public class SmsTemplateRespDTO {
/**
* 模板编号
*/
private String id;
/**
* 短信内容
*/
private String content;
/**
* 审核状态
*
* 枚举 {@link SmsTemplateAuditStatusEnum}
*/
private Integer auditStatus;
/**
* 审核未通过的理由
*/
private String auditReason;
}

View File

@@ -0,0 +1,127 @@
package com.jojubanking.boot.framework.sms.core.client.impl;
import com.jojubanking.boot.framework.common.core.KeyValue;
import com.jojubanking.boot.framework.sms.core.client.SmsClient;
import com.jojubanking.boot.framework.sms.core.client.SmsCodeMapping;
import com.jojubanking.boot.framework.sms.core.client.SmsCommonResult;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsReceiveRespDTO;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsSendRespDTO;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsTemplateRespDTO;
import com.jojubanking.boot.framework.sms.core.property.SmsChannelProperties;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
/**
* 短信客户端的抽象类,提供模板方法,减少子类的冗余代码
*
* @author zzf
* @since 2021/2/1 9:28
*/
@Slf4j
public abstract class AbstractSmsClient implements SmsClient {
/**
* 短信渠道配置
*/
protected volatile SmsChannelProperties properties;
/**
* 错误码枚举类
*/
protected final SmsCodeMapping codeMapping;
public AbstractSmsClient(SmsChannelProperties properties, SmsCodeMapping codeMapping) {
this.properties = prepareProperties(properties);
this.codeMapping = codeMapping;
}
/**
* 初始化
*/
public final void init() {
doInit();
log.info("[init][配置({}) 初始化完成]", properties);
}
/**
* 自定义初始化
*/
protected abstract void doInit();
public final void refresh(SmsChannelProperties properties) {
// 判断是否更新
if (properties.equals(this.properties)) {
return;
}
log.info("[refresh][配置({})发生变化,重新初始化]", 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();
}
@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;
}

View File

@@ -0,0 +1,92 @@
package com.jojubanking.boot.framework.sms.core.client.impl;
import com.jojubanking.boot.framework.sms.core.client.SmsClient;
import com.jojubanking.boot.framework.sms.core.client.SmsClientFactory;
import com.jojubanking.boot.framework.sms.core.client.impl.aliyun.AliyunSmsClient;
import com.jojubanking.boot.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient;
import com.jojubanking.boot.framework.sms.core.client.impl.tencent.TencentSmsClient;
import com.jojubanking.boot.framework.sms.core.client.impl.yunpian.YunpianSmsClient;
import com.jojubanking.boot.framework.sms.core.enums.SmsChannelEnum;
import com.jojubanking.boot.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 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);
case TENCENT: return new TencentSmsClient(properties);
}
// 创建失败,错误日志 + 抛出异常
log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties);
throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", properties));
}
}

View File

@@ -0,0 +1,212 @@
package com.jojubanking.boot.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 com.jojubanking.boot.framework.common.core.KeyValue;
import com.jojubanking.boot.framework.sms.core.client.SmsCommonResult;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsReceiveRespDTO;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsSendRespDTO;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsTemplateRespDTO;
import com.jojubanking.boot.framework.sms.core.client.impl.AbstractSmsClient;
import com.jojubanking.boot.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import com.jojubanking.boot.framework.sms.core.property.SmsChannelProperties;
import com.jojubanking.boot.framework.common.util.collection.MapUtils;
import com.jojubanking.boot.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 com.jojubanking.boot.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
import static com.jojubanking.boot.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
/**
* 阿里短信客户端的实现类
*
* @author zzf
* @since 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 TW
*/
@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;
}
}

View File

@@ -0,0 +1,42 @@
package com.jojubanking.boot.framework.sms.core.client.impl.aliyun;
import com.jojubanking.boot.framework.common.exception.ErrorCode;
import com.jojubanking.boot.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.jojubanking.boot.framework.sms.core.client.SmsCodeMapping;
import com.jojubanking.boot.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
/**
* 阿里云的 SmsCodeMapping 实现类
*
* 参见 https://help.aliyun.com/document_detail/101346.htm 文档
*
* @author TW
*/
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 SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID;
case "isp.RAM_PERMISSION_DENY": return SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY;
case "isv.INVALID_JSON_PARAM":
case "isv.INVALID_PARAMETERS": return SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR;
case "isv.BUSINESS_LIMIT_CONTROL": return SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL;
case "isv.DAY_LIMIT_CONTROL": return SmsFrameworkErrorCodeConstants.SMS_SEND_DAY_LIMIT_CONTROL;
case "isv.SMS_CONTENT_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID;
case "isv.SMS_TEMPLATE_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID;
case "isv.SMS_SIGNATURE_ILLEGAL":
case "isv.SIGN_NAME_ILLEGAL":
case "isv.SMS_SIGN_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID;
case "isv.AMOUNT_NOT_ENOUGH":
case "isv.OUT_OF_SERVICE": return SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH;
case "isv.MOBILE_NUMBER_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID;
case "isv.TEMPLATE_MISSING_PARAMETERS": return SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR;
}
return SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
}
}

View File

@@ -0,0 +1,22 @@
package com.jojubanking.boot.framework.sms.core.client.impl.debug;
import com.jojubanking.boot.framework.common.exception.ErrorCode;
import com.jojubanking.boot.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.jojubanking.boot.framework.sms.core.client.SmsCodeMapping;
import com.jojubanking.boot.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
import java.util.Objects;
/**
* 钉钉的 SmsCodeMapping 实现类
*
* @author TW
*/
public class DebugDingTalkCodeMapping implements SmsCodeMapping {
@Override
public ErrorCode apply(String apiCode) {
return Objects.equals(apiCode, "0") ? GlobalErrorCodeConstants.SUCCESS : SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
}
}

View File

@@ -0,0 +1,96 @@
package com.jojubanking.boot.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 com.jojubanking.boot.framework.common.core.KeyValue;
import com.jojubanking.boot.framework.sms.core.client.SmsCommonResult;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsReceiveRespDTO;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsSendRespDTO;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsTemplateRespDTO;
import com.jojubanking.boot.framework.sms.core.client.impl.AbstractSmsClient;
import com.jojubanking.boot.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import com.jojubanking.boot.framework.sms.core.property.SmsChannelProperties;
import com.jojubanking.boot.framework.common.util.collection.MapUtils;
import com.jojubanking.boot.framework.common.util.json.JsonUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 基于钉钉 WebHook 实现的调试的短信客户端实现类
*
* 考虑到省钱,我们使用钉钉 WebHook 模拟发送短信,方便调试。
*
* @author TW
*/
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&timestamp=%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);
}
}

View File

@@ -0,0 +1,43 @@
package com.jojubanking.boot.framework.sms.core.client.impl.tencent;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.Assert;
import com.jojubanking.boot.framework.sms.core.property.SmsChannelProperties;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 腾讯云短信配置实现类
* 腾讯云发送短信时,需要额外的参数 sdkAppId,
*
* @author shiwp
*/
@Data
@Accessors(chain = true)
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;
}
}

View File

@@ -0,0 +1,304 @@
package com.jojubanking.boot.framework.sms.core.client.impl.tencent;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.jojubanking.boot.framework.common.core.KeyValue;
import com.jojubanking.boot.framework.common.util.collection.ArrayUtils;
import com.jojubanking.boot.framework.common.util.collection.CollectionUtils;
import com.jojubanking.boot.framework.common.util.json.JsonUtils;
import com.jojubanking.boot.framework.sms.core.client.SmsCommonResult;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsReceiveRespDTO;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsSendRespDTO;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsTemplateRespDTO;
import com.jojubanking.boot.framework.sms.core.client.impl.AbstractSmsClient;
import com.jojubanking.boot.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import com.jojubanking.boot.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 lombok.experimental.Accessors;
import java.util.Date;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
import static com.jojubanking.boot.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
import static com.jojubanking.boot.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() {
// 实例化一个认证对象,入参需要传入腾讯云账户密钥对 secretIdsecretKey
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
@Accessors(chain = true)
static class SessionContext {
/**
* 发送短信记录id
*/
private Long logId;
}
private interface SdkFunction<T, R> {
R apply(T t) throws TencentCloudSDKException;
}
}

View File

@@ -0,0 +1,50 @@
package com.jojubanking.boot.framework.sms.core.client.impl.tencent;
import com.jojubanking.boot.framework.common.exception.ErrorCode;
import com.jojubanking.boot.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.jojubanking.boot.framework.sms.core.client.SmsCodeMapping;
import com.jojubanking.boot.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
import static com.jojubanking.boot.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;
}
}

View File

@@ -0,0 +1,205 @@
package com.jojubanking.boot.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 com.jojubanking.boot.framework.common.core.KeyValue;
import com.jojubanking.boot.framework.sms.core.client.SmsCommonResult;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsReceiveRespDTO;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsSendRespDTO;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsTemplateRespDTO;
import com.jojubanking.boot.framework.sms.core.client.impl.AbstractSmsClient;
import com.jojubanking.boot.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import com.jojubanking.boot.framework.sms.core.property.SmsChannelProperties;
import com.jojubanking.boot.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 com.jojubanking.boot.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
import static com.jojubanking.boot.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
/**
* 云片短信客户端的实现类
*
* @author zzf
* @since 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 TW
*/
@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;
}
}

View File

@@ -0,0 +1,59 @@
package com.jojubanking.boot.framework.sms.core.client.impl.yunpian;
import com.jojubanking.boot.framework.common.exception.ErrorCode;
import com.jojubanking.boot.framework.sms.core.client.SmsCodeMapping;
import com.jojubanking.boot.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
import static com.jojubanking.boot.framework.common.exception.enums.GlobalErrorCodeConstants.SUCCESS;
import static com.yunpian.sdk.constant.Code.*;
/**
* 云片的 SmsCodeMapping 实现类
* <p>
* 参见 https://www.yunpian.com/official/document/sms/zh_CN/returnvalue_common 文档
*
* @author TW
*/
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 SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR;
case BAD_ARGUMENT_FORMAT:
return SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR;
case TPL_NOT_FOUND:
case TPL_NOT_VALID:
return SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID;
case MONEY_NOT_ENOUGH:
return SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH;
case BLACK_WORD:
return SmsFrameworkErrorCodeConstants.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 SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL;
case BLACK_PHONE_FILTER:
return SmsFrameworkErrorCodeConstants.SMS_MOBILE_BLACK;
case SIGN_NOT_MATCH:
case BAD_SIGN_FORMAT:
case SIGN_NOT_VALID:
return SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID;
case BAD_API_KEY:
return SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID;
case API_NOT_ALLOWED:
return SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY;
case IP_NOT_ALLOWED:
return SmsFrameworkErrorCodeConstants.SMS_IP_DENY;
default:
break;
}
return SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
}
}

View File

@@ -0,0 +1,37 @@
package com.jojubanking.boot.framework.sms.core.enums;
import cn.hutool.core.util.ArrayUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 短信渠道枚举
*
* @author zzf
* @since 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());
}
}

View File

@@ -0,0 +1,51 @@
package com.jojubanking.boot.framework.sms.core.enums;
import com.jojubanking.boot.framework.common.exception.ErrorCode;
/**
* 短信框架的错误码枚举
*
* 短信框架,使用 2-001-000-000 段
*
* @author TW
*/
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, "短信内容有敏感词");
// 腾讯云为避免骚扰用户营销短信只允许在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, "模板参数不正确");
// ========== 签名相关 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 SMS_APP_ID_INVALID = new ErrorCode(2001000903, "SdkAppId不合法");
ErrorCode EXCEPTION = new ErrorCode(2001000999, "调用异常");
}

View File

@@ -0,0 +1,21 @@
package com.jojubanking.boot.framework.sms.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 短信模板的审核状态枚举
*
* @author TW
*/
@AllArgsConstructor
@Getter
public enum SmsTemplateAuditStatusEnum {
CHECKING(1),
SUCCESS(2),
FAIL(3);
private final Integer status;
}

View File

@@ -0,0 +1,54 @@
package com.jojubanking.boot.framework.sms.core.property;
import com.jojubanking.boot.framework.sms.core.enums.SmsChannelEnum;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
* 短信渠道配置类
*
* @author zzf
* @since 2021/1/25 17:01
*/
@Data
@Validated
@Accessors(chain = true)
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;
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.jojubanking.boot.framework.sms.config.JojuSmsAutoConfiguration

View File

@@ -0,0 +1,55 @@
package com.jojubanking.boot.framework.sms.core.client.impl.aliyun;
import com.jojubanking.boot.framework.common.core.KeyValue;
import com.jojubanking.boot.framework.sms.core.client.SmsCommonResult;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsSendRespDTO;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsTemplateRespDTO;
import com.jojubanking.boot.framework.sms.core.client.impl.aliyun.AliyunSmsClient;
import com.jojubanking.boot.framework.sms.core.enums.SmsChannelEnum;
import com.jojubanking.boot.framework.sms.core.property.SmsChannelProperties;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
/**
* {@link AliyunSmsClient} 的集成测试
*/
public class AliyunSmsClientIntegrationTest {
private static AliyunSmsClient smsClient;
@BeforeAll
public static void before() {
// 创建配置类
SmsChannelProperties properties = new SmsChannelProperties();
properties.setId(1L);
properties.setSignature("Ballcat");
properties.setCode(SmsChannelEnum.ALIYUN.getCode());
properties.setApiKey(System.getenv("ALIYUN_ACCESS_KEY"));
properties.setApiSecret(System.getenv("ALIYUN_SECRET_KEY"));
// 创建客户端
smsClient = new AliyunSmsClient(properties);
smsClient.init();
}
@Test
public void testSendSms() {
List<KeyValue<String, Object>> templateParams = new ArrayList<>();
templateParams.add(new KeyValue<>("code", "1024"));
// templateParams.put("operation", "嘿嘿");
// SmsResult result = smsClient.send(1L, "15601691399", "4372216", templateParams);
SmsCommonResult<SmsSendRespDTO> result = smsClient.sendSms(1L, "15601691399",
"SMS_207945135", templateParams);
System.out.println(result);
}
@Test
public void testGetSmsTemplate() {
String apiTemplateId = "SMS_2079451351";
SmsCommonResult<SmsTemplateRespDTO> result = smsClient.getSmsTemplate(apiTemplateId);
System.out.println(result);
}
}

View File

@@ -0,0 +1,46 @@
package com.jojubanking.boot.framework.sms.core.client.impl.debug;
import com.jojubanking.boot.framework.common.core.KeyValue;
import com.jojubanking.boot.framework.sms.core.client.SmsCommonResult;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsSendRespDTO;
import com.jojubanking.boot.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient;
import com.jojubanking.boot.framework.sms.core.enums.SmsChannelEnum;
import com.jojubanking.boot.framework.sms.core.property.SmsChannelProperties;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
/**
* {@link DebugDingTalkSmsClient} 的集成测试
*/
public class DebugDingTalkSmsClientIntegrationTest {
private static DebugDingTalkSmsClient smsClient;
@BeforeAll
public static void init() {
// 创建配置类
SmsChannelProperties properties = new SmsChannelProperties();
properties.setId(1L);
properties.setSignature("芋道");
properties.setCode(SmsChannelEnum.DEBUG_DING_TALK.getCode());
properties.setApiKey("696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859");
properties.setApiSecret("SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67");
// 创建客户端
smsClient = new DebugDingTalkSmsClient(properties);
smsClient.init();
}
@Test
public void testSendSms() {
List<KeyValue<String, Object>> templateParams = new ArrayList<>();
templateParams.add(new KeyValue<>("code", "1024"));
templateParams.add(new KeyValue<>("operation", "嘿嘿"));
// SmsResult result = smsClient.send(1L, "15601691399", "4372216", templateParams);
SmsCommonResult<SmsSendRespDTO> result = smsClient.sendSms(1L, "15601691399", "4383920", templateParams);
System.out.println(result);
}
}

View File

@@ -0,0 +1,53 @@
package com.jojubanking.boot.framework.sms.core.client.impl.yunpian;
import com.jojubanking.boot.framework.common.core.KeyValue;
import com.jojubanking.boot.framework.sms.core.client.SmsCommonResult;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsSendRespDTO;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsTemplateRespDTO;
import com.jojubanking.boot.framework.sms.core.client.impl.yunpian.YunpianSmsClient;
import com.jojubanking.boot.framework.sms.core.enums.SmsChannelEnum;
import com.jojubanking.boot.framework.sms.core.property.SmsChannelProperties;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
/**
* {@link YunpianSmsClient} 的集成测试
*/
public class YunpianSmsClientIntegrationTest {
private static YunpianSmsClient smsClient;
@BeforeAll
public static void init() {
// 创建配置类
SmsChannelProperties properties = new SmsChannelProperties();
properties.setId(1L);
properties.setSignature("芋道");
properties.setCode(SmsChannelEnum.YUN_PIAN.getCode());
properties.setApiKey("1555a14277cb8a608cf45a9e6a80d510");
// 创建客户端
smsClient = new YunpianSmsClient(properties);
smsClient.init();
}
@Test
public void testSendSms() {
List<KeyValue<String, Object>> templateParams = new ArrayList<>();
templateParams.add(new KeyValue<>("code", "1024"));
templateParams.add(new KeyValue<>("operation", "嘿嘿"));
// SmsResult result = smsClient.send(1L, "15601691399", "4372216", templateParams);
SmsCommonResult<SmsSendRespDTO> result = smsClient.sendSms(1L, "15601691399", "4383920", templateParams);
System.out.println(result);
}
@Test
public void testGetSmsTemplate() {
String apiTemplateId = "4383920";
SmsCommonResult<SmsTemplateRespDTO> result = smsClient.getSmsTemplate(apiTemplateId);
System.out.println(result);
}
}

View File

@@ -0,0 +1,225 @@
package com.jojubanking.boot.framework.sms.core.client.impl.aliyun;
import cn.hutool.core.util.ReflectUtil;
import com.jojubanking.boot.framework.test.core.ut.BaseMockitoUnitTest;
import com.jojubanking.boot.framework.common.core.KeyValue;
import com.jojubanking.boot.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.jojubanking.boot.framework.sms.core.client.SmsCommonResult;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsReceiveRespDTO;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsSendRespDTO;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsTemplateRespDTO;
import com.jojubanking.boot.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import com.jojubanking.boot.framework.sms.core.property.SmsChannelProperties;
import com.jojubanking.boot.framework.common.util.collection.MapUtils;
import com.jojubanking.boot.framework.common.util.date.DateUtils;
import com.jojubanking.boot.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
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.Assertions;
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 com.jojubanking.boot.framework.common.util.json.JsonUtils.toJsonString;
import static com.jojubanking.boot.framework.test.core.util.RandomUtils.*;
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 TW
*/
public class AliyunSmsClientTest extends BaseMockitoUnitTest {
private final SmsChannelProperties properties = new SmsChannelProperties()
.setApiKey(randomString()) // 随机一个 apiKey避免构建报错
.setApiSecret(randomString()) // 随机一个 apiSecret避免构建报错
.setSignature("TW");
@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());
Assertions.assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR.getCode(), result.getCode());
Assertions.assertEquals(SmsFrameworkErrorCodeConstants.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());
}
}

View File

@@ -0,0 +1,43 @@
package com.jojubanking.boot.framework.sms.core.client.impl.aliyun;
import com.jojubanking.boot.framework.test.core.ut.BaseMockitoUnitTest;
import com.jojubanking.boot.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.jojubanking.boot.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 TW
*/
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"));
}
}

View File

@@ -0,0 +1,222 @@
package com.jojubanking.boot.framework.sms.core.client.impl.tencent;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.jojubanking.boot.framework.common.core.KeyValue;
import com.jojubanking.boot.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.jojubanking.boot.framework.common.util.collection.ArrayUtils;
import com.jojubanking.boot.framework.common.util.collection.MapUtils;
import com.jojubanking.boot.framework.common.util.date.DateUtils;
import com.jojubanking.boot.framework.common.util.json.JsonUtils;
import com.jojubanking.boot.framework.sms.core.client.SmsCommonResult;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsReceiveRespDTO;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsSendRespDTO;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsTemplateRespDTO;
import com.jojubanking.boot.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import com.jojubanking.boot.framework.sms.core.property.SmsChannelProperties;
import com.jojubanking.boot.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 com.jojubanking.boot.framework.common.util.json.JsonUtils.toJsonString;
import static com.jojubanking.boot.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("TW");
@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("TW");
// 调用
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());
}
}

View File

@@ -0,0 +1,50 @@
package com.jojubanking.boot.framework.sms.core.client.impl.tencent;
import com.jojubanking.boot.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.jojubanking.boot.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
import com.jojubanking.boot.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"));
}
}

View File

@@ -0,0 +1,202 @@
package com.jojubanking.boot.framework.sms.core.client.impl.yunpian;
import cn.hutool.core.util.ReflectUtil;
import com.jojubanking.boot.framework.test.core.ut.BaseMockitoUnitTest;
import com.jojubanking.boot.framework.common.core.KeyValue;
import com.jojubanking.boot.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.jojubanking.boot.framework.sms.core.client.SmsCommonResult;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsReceiveRespDTO;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsSendRespDTO;
import com.jojubanking.boot.framework.sms.core.client.dto.SmsTemplateRespDTO;
import com.jojubanking.boot.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import com.jojubanking.boot.framework.sms.core.property.SmsChannelProperties;
import com.jojubanking.boot.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 com.jojubanking.boot.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 TW
*/
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());
}
}

View File

@@ -0,0 +1,44 @@
package com.jojubanking.boot.framework.sms.core.client.impl.yunpian;
import com.jojubanking.boot.framework.test.core.ut.BaseMockitoUnitTest;
import com.jojubanking.boot.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.jojubanking.boot.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
import org.junit.jupiter.api.Assertions;
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 TW
*/
class YunpianSmsCodeMappingTest extends BaseMockitoUnitTest {
@InjectMocks
private YunpianSmsCodeMapping codeMapping;
@Test
public void testApply() {
assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply(String.valueOf(OK)));
Assertions.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)));
}
}