短信提交 2021-03-28,增加发送日志

This commit is contained in:
YunaiV
2021-03-28 22:49:14 +08:00
parent 46ed64ba40
commit 515fca5c41
34 changed files with 579 additions and 965 deletions

View File

@@ -1,67 +0,0 @@
package cn.iocoder.dashboard.framework.sms.client;
import cn.iocoder.dashboard.framework.sms.core.SmsBody;
import cn.iocoder.dashboard.framework.sms.core.SmsResult;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperty;
import lombok.extern.slf4j.Slf4j;
/**
* 抽象短息客户端
*
* @author zzf
* @date 2021/2/1 9:28
*/
@Slf4j
public abstract class AbstractSmsClient implements SmsClient {
/**
* 短信渠道参数
*/
protected final SmsChannelProperty channelVO;
/**
* 短信客户端有参构造函数
*
* @param property 短信配置
*/
public AbstractSmsClient(SmsChannelProperty property) {
this.channelVO = property;
}
public SmsChannelProperty getProperty() {
return channelVO;
}
@Override
public final SmsResult send(String templateApiId, SmsBody smsBody, String target) {
SmsResult result;
try {
beforeSend(templateApiId, smsBody, target);
result = doSend(templateApiId, smsBody, target);
afterSend(templateApiId, smsBody, target, result);
} catch (Exception e) {
// exception handle
log.debug(e.getMessage(), e);
return SmsResult.failResult("发送异常: " + e.getMessage());
}
return result;
}
/**
* 发送消息
*
* @param templateApiId 短信模板唯一标识
* @param smsBody 消息内容
* @param targetPhone 发送对象手机号
* @return 短信发送结果
* @throws Exception 调用发送失败,抛出异常
*/
protected abstract SmsResult doSend(String templateApiId, SmsBody smsBody, String targetPhone) throws Exception;
protected void beforeSend(String templateApiId, SmsBody smsBody, String targetPhone) throws Exception {
}
protected void afterSend(String templateApiId, SmsBody smsBody, String targetPhone, SmsResult result) throws Exception {
}
}

View File

@@ -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();
}
}

View File

@@ -1,38 +0,0 @@
package cn.iocoder.dashboard.framework.sms.core;
import cn.iocoder.dashboard.util.json.JsonUtils;
import lombok.Data;
import java.util.Map;
/**
* 消息内容实体类
*/
@Data
public class SmsBody {
/**
* 消息日志id
*/
private Long smsLogId;
/**
* 模板编码
*/
private String templateCode;
/**
* 模板编码
*/
private String templateContent;
/**
* 参数列表
*/
private Map<String, String> params;
public String getParamsStr() {
return JsonUtils.toJsonString(params);
}
}

View File

@@ -1,133 +0,0 @@
package cn.iocoder.dashboard.framework.sms.core;
import cn.iocoder.dashboard.common.exception.ServiceException;
import cn.iocoder.dashboard.framework.sms.client.AbstractSmsClient;
import cn.iocoder.dashboard.framework.sms.client.impl.ali.AliyunSmsClient;
import cn.iocoder.dashboard.framework.sms.client.impl.yunpian.YunpianSmsClient;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsChannelEnum;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperty;
import cn.iocoder.dashboard.framework.sms.core.property.SmsTemplateProperty;
import cn.iocoder.dashboard.util.json.JsonUtils;
import org.springframework.stereotype.Component;
import javax.servlet.ServletRequest;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
/**
* 短信客户端工厂
*
* @author zzf
* @date 2021/1/28 14:01
*/
@Component
public class SmsClientFactory {
/**
* channelId: client map
* 保存 渠道id: 对应短信客户端 的map
*/
private final Map<Long, AbstractSmsClient> smsSenderMap = new ConcurrentHashMap<>(8);
/**
* templateCode: TemplateProperty map
* 保存 模板编码:模板信息 的map
*/
private final Map<String, SmsTemplateProperty> templatePropertyMap = new ConcurrentHashMap<>(16);
/**
* 创建短信客户端
*
* @param propertyVO 参数对象
* @return 客户端id(默认channelId)
*/
public Long createClient(SmsChannelProperty propertyVO) {
AbstractSmsClient sender = createClient(SmsChannelEnum.getByCode(propertyVO.getCode()), propertyVO);
smsSenderMap.put(propertyVO.getId(), sender);
return propertyVO.getId();
}
private AbstractSmsClient createClient(SmsChannelEnum channelEnum, SmsChannelProperty channelVO) {
if (channelEnum == null) {
throw new ServiceException(INVALID_CHANNEL_CODE);
}
switch (channelEnum) {
case ALIYUN:
return new AliyunSmsClient(channelVO);
case YUN_PIAN:
return new YunpianSmsClient(channelVO);
// TODO fill more channel
default:
break;
}
throw new ServiceException(SMS_SENDER_NOT_FOUND);
}
/**
* 获取短信客户端
*
* @param channelId 渠道id
* @return 短信id
*/
public AbstractSmsClient getClient(Long channelId) {
return smsSenderMap.get(channelId);
}
/**
* 添加或修改短信模板信息缓存
*/
public void addOrUpdateTemplateCache(Collection<SmsTemplateProperty> templateProperties) {
templateProperties.forEach(this::addOrUpdateTemplateCache);
}
/**
* 添加或修改短信模板信息缓存
*/
public void addOrUpdateTemplateCache(SmsTemplateProperty templateProperty) {
templatePropertyMap.put(templateProperty.getCode(), templateProperty);
}
/**
* 根据短信模板编码获取模板唯一标识
*
* @param templateCode 短信模板编码
* @return 短信id
*/
public String getTemplateApiIdByCode(String templateCode) {
SmsTemplateProperty smsTemplateProperty = templatePropertyMap.get(templateCode);
if (smsTemplateProperty == null) {
throw new ServiceException(SMS_TEMPLATE_NOT_EXISTS);
}
return smsTemplateProperty.getApiTemplateId();
}
/**
* 从短信发送回调函数请求中获取用于唯一确定一条send_lod的apiId
*
* @param callbackRequest 短信发送回调函数请求
* @return 第三方平台短信唯一标识
*/
public SmsResultDetail getSmsResultDetailFromCallbackQuery(ServletRequest callbackRequest) {
for (Long channelId : smsSenderMap.keySet()) {
AbstractSmsClient smsClient = smsSenderMap.get(channelId);
try {
SmsResultDetail smsSendResult = smsClient.smsSendCallbackHandle(callbackRequest);
if (smsSendResult != null) {
return smsSendResult;
}
} catch (Exception ignored) {
}
}
throw new IllegalArgumentException("getSmsResultDetailFromCallbackQuery fail! don't match SmsClient by RequestParam: "
+ JsonUtils.toJsonString(callbackRequest.getParameterMap()));
}
}

View File

@@ -15,4 +15,5 @@ public interface SmsConstants {
String COMMA = ",";
String SUCCESS = "SUCCESS";
}

View File

@@ -1,7 +1,8 @@
package cn.iocoder.dashboard.framework.sms.core;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsSendFailureTypeEnum;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
@@ -9,33 +10,64 @@ import java.io.Serializable;
* 消息内容实体类
*/
@Data
@Accessors(chain = true)
public class SmsResult implements Serializable {
/**
* 是否成功(发送短信的请求是否成功)
* 是否成功
*
* 注意,是调用 API 短信平台的请求是否成功
*/
private Boolean success;
/**
* 发送失败的类型
*
* 枚举 {@link SmsSendFailureTypeEnum#getType()}
*/
private Integer sendFailureType;
/**
* 发送失败的提示
*
* 一般情况下,使用 {@link SmsSendFailureTypeEnum#getMsg()}
* 异常情况下,通过格式化 Exception 的提示存储
*/
private String sendFailureMsg;
/**
* 第三方唯一标识
* 短信 API 发送的错误码
*
* 由于第三方的错误码可能是字符串,所以使用 String 类型
*/
private String apiId;
private String apiSendCode;
/**
* 状态码
* 短信 API 发送的提示
*/
private String code;
private String apiSendMsg;
/**
* 提示
* 短信 API 发送返回的唯一请求 ID
*
* 用于和短信 API 进行定位于排错
*/
private String message;
private String apiRequestId;
/**
* 短信 API 发送返回的序号
*
* 用于和短信 API 平台的发送记录关联
*/
private String apiSerialNo;
public static SmsResult failResult(String message) {
SmsResult resultBody = new SmsResult();
resultBody.setSuccess(false);
resultBody.setMessage(message);
return resultBody;
private SmsResult() {
}
public static SmsResult success(SmsSendFailureTypeEnum sendFailureType,
String apiSendCode, String apiSendMsg, String apiRequestId, String apiSerialNo) {
return new SmsResult().setSuccess(true).setSendFailureType(sendFailureType.getType()).setSendFailureMsg(sendFailureType.getMsg())
.setApiSendCode(apiSendCode).setApiSendMsg(apiSendMsg).setApiRequestId(apiRequestId).setApiSerialNo(apiSerialNo);
}
public static SmsResult error(Throwable ex) {
return new SmsResult().setSuccess(false)
.setSendFailureType(SmsSendFailureTypeEnum.SMS_SEND_EXCEPTION.getType())
.setSendFailureMsg(ExceptionUtil.getRootCauseMessage(ex));
}
}

View File

@@ -1,28 +1,36 @@
package cn.iocoder.dashboard.framework.sms.client;
package cn.iocoder.dashboard.framework.sms.core.client;
import cn.iocoder.dashboard.framework.sms.core.SmsBody;
import cn.iocoder.dashboard.framework.sms.core.SmsResult;
import cn.iocoder.dashboard.framework.sms.core.SmsResultDetail;
import javax.servlet.ServletRequest;
import java.util.Map;
/**
* 短信接口
* 短信客户端接口
*
* @author zzf
* @date 2021/1/25 14:14
*/
public interface SmsClient {
/**
* 获得渠道编号
*
* @return 渠道编号
*/
Long getId();
/**
* 发送消息
*
* @param templateApiId 短信模板唯一标识
* @param smsBody 消息内容
* @param targets 发送对象列表
* @param sendLogId 发送日志编号
* @param mobile 手机号
* @param apiTemplateId 短信 API 的模板编号
* @param templateParams 短信模板参数
* @return 短信发送结果
*/
SmsResult send(String templateApiId, SmsBody smsBody, String targets);
SmsResult send(Long sendLogId, String mobile, String apiTemplateId, Map<String, Object> templateParams);
// TODO FROM 芋艿 to ZZF是不是可以改成意图更明确的解析返回结果例如说 parseXXXX
/**

View File

@@ -0,0 +1,28 @@
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 properties 配置对象
*/
void createOrUpdateSmsClient(SmsChannelProperties properties);
}

View File

@@ -0,0 +1,89 @@
package cn.iocoder.dashboard.framework.sms.core.client.impl;
import cn.iocoder.dashboard.framework.sms.core.SmsResult;
import cn.iocoder.dashboard.framework.sms.core.client.SmsClient;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
* 短信客户端抽象类
*
* @author zzf
* @date 2021/2/1 9:28
*/
@Slf4j
public abstract class AbstractSmsClient implements SmsClient {
/**
* 短信渠道配置
*/
protected volatile SmsChannelProperties properties;
/**
* 短信客户端有参构造函数
*
* @param properties 短信配置
*/
public AbstractSmsClient(SmsChannelProperties properties) {
this.properties = properties;
}
/**
* 初始化
*/
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 SmsResult send(Long sendLogId, String mobile, String apiTemplateId, Map<String, Object> templateParams) {
SmsResult result;
try {
result = doSend(sendLogId, mobile, apiTemplateId, templateParams);
} catch (Throwable ex) {
// 打印异常日志
log.error("[send][发送短信异常sendLogId({}) mobile({}) apiTemplateId({}) templateParams({})]",
sendLogId, mobile, apiTemplateId, templateParams, ex);
// 封装返回
return SmsResult.error(ex);
}
return result;
}
/**
* 发送消息
*
* @param sendLogId 发送日志编号
* @param mobile 手机号
* @param apiTemplateId 短信 API 的模板编号
* @param templateParams 短信模板参数
* @return 短信发送结果
*/
protected abstract SmsResult doSend(Long sendLogId, String mobile, String apiTemplateId, Map<String, Object> templateParams)
throws Throwable;
}

View File

@@ -0,0 +1,83 @@
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.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.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 短信客户端工厂接口
*
* @author zzf
*/
@Validated
@Slf4j
public class SmsClientFactoryImpl implements SmsClientFactory {
/**
* 短信客户端 Map
* key渠道编号使用 {@link SmsChannelProperties#getId()}
*/
private final Map<Long, AbstractSmsClient> clients = new ConcurrentHashMap<>();
@Override
public SmsClient getSmsClient(Long channelId) {
return clients.get(channelId);
}
@Override
public void createOrUpdateSmsClient(SmsChannelProperties properties) {
AbstractSmsClient client = clients.get(properties.getId());
if (client == null) {
client = this.createSmsClient(properties);
clients.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);
}
// 创建失败,错误日志 + 抛出异常
log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties);
throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", properties));
}
// /**
// * 从短信发送回调函数请求中获取用于唯一确定一条send_lod的apiId
// *
// * @param callbackRequest 短信发送回调函数请求
// * @return 第三方平台短信唯一标识
// */
// public SmsResultDetail getSmsResultDetailFromCallbackQuery(ServletRequest callbackRequest) {
// for (Long channelId : clients.keySet()) {
// AbstractSmsClient smsClient = clients.get(channelId);
// try {
// SmsResultDetail smsSendResult = smsClient.smsSendCallbackHandle(callbackRequest);
// if (smsSendResult != null) {
// return smsSendResult;
// }
// } catch (Exception ignored) {
// }
// }
// throw new IllegalArgumentException("getSmsResultDetailFromCallbackQuery fail! don't match SmsClient by RequestParam: "
// + JsonUtils.toJsonString(callbackRequest.getParameterMap()));
// }
}

View File

@@ -1,19 +1,15 @@
package cn.iocoder.dashboard.framework.sms.client.impl.ali;
package cn.iocoder.dashboard.framework.sms.core.client.impl.aliyun;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateUtil;
import cn.iocoder.dashboard.framework.sms.client.AbstractSmsClient;
import cn.iocoder.dashboard.framework.sms.core.SmsBody;
import cn.iocoder.dashboard.framework.sms.core.SmsResult;
import cn.iocoder.dashboard.framework.sms.core.SmsResultDetail;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperty;
import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsSendStatusEnum;
import cn.iocoder.dashboard.util.json.JsonUtils;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.fasterxml.jackson.core.type.TypeReference;
@@ -28,7 +24,7 @@ import java.util.List;
import java.util.Map;
/**
* 阿里短信实现类
* 阿里短信客户端的实现类
*
* @author zzf
* @date 2021/1/25 14:17
@@ -36,54 +32,54 @@ import java.util.Map;
@Slf4j
public class AliyunSmsClient extends AbstractSmsClient {
private static final String OK = "OK";
private static final String PRODUCT = "Dystopi";
private static final String DOMAIN = "dysmsapi.aliyuncs.com";
private static final String ENDPOINT = "cn-hangzhou";
private final IAcsClient acsClient;
private static final String OK = "OK";
/**
* 构造阿里云短信发送处理
*
* @param channelVO 阿里云短信配置
* 阿里云客户端
*/
public AliyunSmsClient(SmsChannelProperty channelVO) {
super(channelVO);
private volatile IAcsClient acsClient;
String accessKeyId = channelVO.getApiKey();
String accessKeySecret = channelVO.getApiSecret();
public AliyunSmsClient(SmsChannelProperties properties) {
super(properties);
}
IClientProfile profile = DefaultProfile.getProfile(ENDPOINT, accessKeyId, accessKeySecret);
@Override
protected void doInit() {
IClientProfile profile = DefaultProfile.getProfile(ENDPOINT, properties.getApiKey(), properties.getApiSecret());
DefaultProfile.addEndpoint(ENDPOINT, PRODUCT, DOMAIN);
acsClient = new DefaultAcsClient(profile);
}
@Override
public SmsResult doSend(String templateApiId, SmsBody smsBody, String targetPhone) throws Exception {
SendSmsRequest request = new SendSmsRequest();
request.setSysMethod(MethodType.POST);
request.setPhoneNumbers(targetPhone);
request.setSignName(channelVO.getApiSignatureId());
request.setTemplateCode(templateApiId);
request.setTemplateParam(smsBody.getParamsStr());
SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
boolean success = OK.equals(sendSmsResponse.getCode());
if (!success) {
log.debug("send fail[code={}, message={}]", sendSmsResponse.getCode(), sendSmsResponse.getMessage());
}
return new SmsResult()
.setSuccess(success)
.setMessage(sendSmsResponse.getMessage())
.setCode(sendSmsResponse.getCode())
.setApiId(sendSmsResponse.getBizId());
protected SmsResult doSend(Long sendLogId, String mobile, String apiTemplateId, Map<String, Object> templateParams) throws Exception {
return null;
}
// @Override
// public SmsResult doSend(String templateApiId, SmsBody smsBody, String targetPhone) throws Exception {
// SendSmsRequest request = new SendSmsRequest();
// request.setSysMethod(MethodType.POST);
// request.setPhoneNumbers(targetPhone);
// request.setSignName(properties.getSignature());
// request.setTemplateCode(templateApiId);
// request.setTemplateParam(smsBody.getParamsStr());
// SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
//
// boolean success = OK.equals(sendSmsResponse.getCode());
// if (!success) {
// log.debug("send fail[code={}, message={}]", sendSmsResponse.getCode(), sendSmsResponse.getMessage());
// }
// return new SmsResult()
// .setSuccess(success)
// .setMessage(sendSmsResponse.getMessage())
// .setCode(sendSmsResponse.getCode())
// .setApiId(sendSmsResponse.getBizId());
// }
/**
* [{
* "send_time" : "2017-08-30 00:00:00",
@@ -131,8 +127,8 @@ public class AliyunSmsClient extends AbstractSmsClient {
public Integer getSendStatus() {
return ((Boolean) sendResultParamMap.get(CallbackField.SUCCESS))
? SysSmsSendStatusEnum.SEND_SUCCESS.getStatus()
: SysSmsSendStatusEnum.SEND_FAIL.getStatus();
? SysSmsSendStatusEnum.SUCCESS.getStatus()
: SysSmsSendStatusEnum.FAILURE.getStatus();
}
public String getBizId() {

View File

@@ -1,19 +1,20 @@
package cn.iocoder.dashboard.framework.sms.client.impl.yunpian;
package cn.iocoder.dashboard.framework.sms.core.client.impl.yunpian;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.iocoder.dashboard.framework.sms.client.AbstractSmsClient;
import cn.iocoder.dashboard.framework.sms.core.SmsBody;
import cn.hutool.core.util.URLUtil;
import cn.iocoder.dashboard.framework.sms.core.SmsConstants;
import cn.iocoder.dashboard.framework.sms.core.SmsResult;
import cn.iocoder.dashboard.framework.sms.core.SmsResultDetail;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperty;
import cn.iocoder.dashboard.framework.sms.core.client.impl.AbstractSmsClient;
import cn.iocoder.dashboard.framework.sms.core.enums.SmsSendFailureTypeEnum;
import cn.iocoder.dashboard.framework.sms.core.property.SmsChannelProperties;
import cn.iocoder.dashboard.modules.system.enums.sms.SysSmsSendStatusEnum;
import cn.iocoder.dashboard.util.json.JsonUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.yunpian.sdk.YunpianClient;
import com.yunpian.sdk.constant.Code;
import com.yunpian.sdk.constant.YunpianConstant;
import com.yunpian.sdk.model.Result;
import com.yunpian.sdk.model.SmsSingleSend;
@@ -25,9 +26,10 @@ import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
/**
* 云片短信实现类
* 云片短信客户端的实现类
*
* @author zzf
* @date 9:48 2021/3/5
@@ -35,71 +37,59 @@ import java.util.Map;
@Slf4j
public class YunpianSmsClient extends AbstractSmsClient {
private final YunpianClient client;
/**
* 云信短信客户端
*/
private volatile YunpianClient client;
private final TypeReference<List<Map<String, String>>> callbackType = new TypeReference<List<Map<String, String>>>() {
};
/**
* 构造云片短信发送处理
*
* @param channelVO 阿里云短信配置
*/
public YunpianSmsClient(SmsChannelProperty channelVO) {
super(channelVO);
client = new YunpianClient(channelVO.getApiKey());
public YunpianSmsClient(SmsChannelProperties properties) {
super(properties);
}
@Override
public SmsResult doSend(String templateApiId, SmsBody smsBody, String targetPhone) {
Map<String, String> paramMap = new HashMap<>();
paramMap.put(YunpianConstant.APIKEY, getProperty().getApiKey());
paramMap.put(YunpianConstant.MOBILE, String.join(SmsConstants.COMMA, targetPhone));
paramMap.put(YunpianConstant.TEXT, formatContent(smsBody));
paramMap.put(Helper.CALLBACK, getProperty().getCallbackUrl());
public void doInit() {
client = new YunpianClient(properties.getApiKey());
}
Result<SmsSingleSend> sendResult = client.sms().single_send(paramMap);
boolean success = sendResult.getCode().equals(Code.OK);
@Override
protected SmsResult doSend(Long sendLogId, String mobile, String apiTemplateId, Map<String, Object> templateParams) throws Throwable {
// 构建参数
Map<String, String> request = new HashMap<>();
request.put(YunpianConstant.APIKEY, properties.getApiKey());
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(Helper.CALLBACK, properties.getCallbackUrl());
if (!success) {
log.debug("send fail[code={}, message={}]", sendResult.getCode(), sendResult.getDetail());
// 执行发送
Result<SmsSingleSend> sendResult = client.sms().tpl_single_send(request);
if (sendResult.getThrowable() != null) {
throw sendResult.getThrowable();
}
return new SmsResult()
.setSuccess(success)
.setMessage(sendResult.getDetail())
.setCode(sendResult.getCode().toString())
.setApiId(sendResult.getData().getSid().toString());
// 解析结果
SmsSingleSend data = sendResult.getData();
return SmsResult.success(parseSendFailureType(sendResult), // API 短信平台解析成统一的错误码
String.valueOf(data.getCode()), data.getMsg(), null, String.valueOf(data.getSid()));
}
/**
* 格式化短信内容将参数注入到模板中
*
* @param smsBody 短信信息
* @return 格式化后的短信内容
*/
private String formatContent(SmsBody smsBody) {
StringBuilder result = new StringBuilder(smsBody.getTemplateContent());
smsBody.getParams().forEach((key, val) -> {
String param = parseParamToPlaceholder(key);
result.replace(result.indexOf(param), result.indexOf(param + param.length()), val);
});
return result.toString();
private static String formatTplValue(Map<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((key, value) -> joiner.add(String.format("#%s#=%s", key, URLUtil.encode(String.valueOf(value)))));
return joiner.toString();
}
/**
* 将指定参数改成对应的占位字符
* <p>
* 云片的是 #param# 的形式作为占位符
*
* @param key 参数名
* @return 对应的占位字符
*/
private String parseParamToPlaceholder(String key) {
return SmsConstants.JING_HAO + key + SmsConstants.JING_HAO;
private static SmsSendFailureTypeEnum parseSendFailureType(Result<SmsSingleSend> sendResult) {
return SmsSendFailureTypeEnum.SMS_UNKNOWN;
}
/**
* 云片的比较复杂又是加密又是套娃的
*/
@@ -109,7 +99,6 @@ public class YunpianSmsClient extends AbstractSmsClient {
return Helper.getSmsResultDetailByParam(map);
}
/**
* request 中获取请求中传入的短信发送结果信息
*
@@ -155,8 +144,8 @@ public class YunpianSmsClient extends AbstractSmsClient {
private static int getSendStatus(Map<String, String> map) {
String reportStatus = map.get(REPORT_STATUS);
return SmsConstants.SUCCESS.equals(reportStatus)
? SysSmsSendStatusEnum.SEND_SUCCESS.getStatus()
: SysSmsSendStatusEnum.SEND_FAIL.getStatus();
? SysSmsSendStatusEnum.SUCCESS.getStatus()
: SysSmsSendStatusEnum.FAILURE.getStatus();
}
public static SmsResultDetail getSmsResultDetailByParam(Map<String, String> map) {

View File

@@ -13,14 +13,23 @@ import lombok.Getter;
public enum SmsSendFailureTypeEnum {
// ========== 模板相关(100 开头) ==========
SMS_TEMPLATE_DISABLE(100), // 短信模板被禁用
SMS_CHANNEL_CLIENT_NOT_EXISTS(100, "短信渠道的客户端不存在"),
// ========== 其它相关 ==========
// ========== 模板相关(200 开头) ==========
SMS_TEMPLATE_DISABLE(200, "短信模板被禁用"),
// ========== 其它相关(900 开头) ==========
SMS_SEND_EXCEPTION(900, "发送异常"),
SMS_UNKNOWN(999, "未知错误,需要解析")
;
/**
* 失败类型
*/
private final int type;
/**
* 失败提示
*/
private final String msg;
}

View File

@@ -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;
}

View File

@@ -1,68 +0,0 @@
package cn.iocoder.dashboard.framework.sms.core.property;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.List;
/**
* 渠道(包含模板)信息VO类
*
* @author zzf
* @date 2021/1/25 17:01
*/
@Data
@EqualsAndHashCode
public class SmsChannelProperty implements Serializable {
/**
* id
*/
@NotNull(message = "短信渠道ID不能为空")
private Long id;
/**
* 编码(来自枚举类 阿里、华为、七牛等)
*/
@NotEmpty(message = "短信渠道编码不能为空")
private String code;
/**
* 渠道账号id
*/
@NotEmpty(message = "渠道账号id不能为空")
private String apiKey;
/**
* 渠道账号秘钥
*/
@NotEmpty(message = "渠道账号秘钥不能为空")
private String apiSecret;
/**
* 实际渠道签名唯一标识
*/
@NotEmpty(message = "实际渠道签名唯一标识不能为空")
private String apiSignatureId;
/**
* 签名值
*/
@NotEmpty(message = "签名值不能为空")
private String signature;
/**
* 是否拥有回调函数0否 1是
*/
@NotNull(message = "是否拥有回调函数不能为空")
private Integer hadCallback;
/**
* 短信发送回调url
*/
private String callbackUrl;
}

View File

@@ -1,47 +0,0 @@
package cn.iocoder.dashboard.framework.sms.core.property;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotEmpty;
/**
* 渠道模板VO类
*
* @author zzf
* @date 2021/1/25 17:03
*/
@Data
@EqualsAndHashCode
public class SmsTemplateProperty {
/**
* 渠道id
*/
@NotEmpty(message = "短信渠道编码不能为空")
private Long channelId;
/**
* 业务编码(来自数据字典, 用户自定义业务场景 一个场景可以有多个模板)
*/
private String bizCode;
/**
* 编码
*/
@NotEmpty(message = "短信模板编码不能为空")
private String code;
/**
* 实际渠道模板唯一标识
*/
@NotEmpty(message = "短信模板唯一标识不能为空")
private String apiTemplateId;
/**
* 内容
*/
@NotEmpty(message = "短信模板内容不能为空")
private String content;
}