|
|
|
|
@ -4,9 +4,7 @@ import cn.hutool.core.lang.Assert;
|
|
|
|
|
import cn.hutool.core.util.StrUtil;
|
|
|
|
|
import cn.iocoder.yudao.framework.common.core.KeyValue;
|
|
|
|
|
import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
|
|
|
|
|
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
|
|
|
|
|
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
|
|
|
|
import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
|
|
|
|
|
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO;
|
|
|
|
|
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
|
|
|
|
|
import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO;
|
|
|
|
|
@ -17,23 +15,22 @@ import com.fasterxml.jackson.annotation.JsonFormat;
|
|
|
|
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
|
|
|
import com.google.common.annotations.VisibleForTesting;
|
|
|
|
|
import com.tencentcloudapi.common.Credential;
|
|
|
|
|
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
|
|
|
|
|
import com.tencentcloudapi.sms.v20210111.SmsClient;
|
|
|
|
|
import com.tencentcloudapi.sms.v20210111.models.*;
|
|
|
|
|
import lombok.Data;
|
|
|
|
|
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.function.Function;
|
|
|
|
|
import java.util.function.Supplier;
|
|
|
|
|
import java.util.Objects;
|
|
|
|
|
|
|
|
|
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
|
|
|
|
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
|
|
|
|
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 腾讯云短信功能实现
|
|
|
|
|
* <p>
|
|
|
|
|
* 参见 https://cloud.tencent.com/document/product/382/52077
|
|
|
|
|
*
|
|
|
|
|
* 参见 <a href="https://cloud.tencent.com/document/product/382/52077">文档</a>
|
|
|
|
|
*
|
|
|
|
|
* @author shiwp
|
|
|
|
|
*/
|
|
|
|
|
@ -42,7 +39,7 @@ public class TencentSmsClient extends AbstractSmsClient {
|
|
|
|
|
/**
|
|
|
|
|
* 调用成功 code
|
|
|
|
|
*/
|
|
|
|
|
public static final String API_SUCCESS_CODE = "Ok";
|
|
|
|
|
public static final String API_CODE_SUCCESS = "Ok";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* REGION,使用南京
|
|
|
|
|
@ -51,180 +48,103 @@ public class TencentSmsClient extends AbstractSmsClient {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 是否国际/港澳台短信:
|
|
|
|
|
*
|
|
|
|
|
* 0:表示国内短信。
|
|
|
|
|
* 1:表示国际/港澳台短信。
|
|
|
|
|
*/
|
|
|
|
|
private static final long INTERNATIONAL = 0L;
|
|
|
|
|
private static final long INTERNATIONAL_CHINA = 0L;
|
|
|
|
|
|
|
|
|
|
private SmsClient client;
|
|
|
|
|
|
|
|
|
|
public TencentSmsClient(SmsChannelProperties properties) {
|
|
|
|
|
super(properties, new TencentSmsCodeMapping());
|
|
|
|
|
super(properties);
|
|
|
|
|
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
|
|
|
|
|
validateSdkAppId(properties);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected void doInit() {
|
|
|
|
|
// 实例化一个认证对象,入参需要传入腾讯云账户密钥对 secretId,secretKey
|
|
|
|
|
Credential credential = new Credential(properties.getApiKey(), properties.getApiSecret());
|
|
|
|
|
Credential credential = new Credential(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 内。
|
|
|
|
|
* 参数校验腾讯云的 SDK AppId
|
|
|
|
|
*
|
|
|
|
|
* 原因是:腾讯云发放短信的时候,需要额外的参数 sdkAppId
|
|
|
|
|
*
|
|
|
|
|
* 解决方案:考虑到不破坏原有的 apiKey + apiSecret 的结构,所以将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。
|
|
|
|
|
*
|
|
|
|
|
* @param properties 数据库中存储的短信渠道配置
|
|
|
|
|
* @return TencentSmsChannelProperties
|
|
|
|
|
* @param properties 配置
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
protected SmsChannelProperties prepareProperties(SmsChannelProperties properties) {
|
|
|
|
|
return TencentSmsChannelProperties.build(properties);
|
|
|
|
|
private static void validateSdkAppId(SmsChannelProperties properties) {
|
|
|
|
|
String combineKey = properties.getApiKey();
|
|
|
|
|
Assert.notEmpty(combineKey, "apiKey 不能为空");
|
|
|
|
|
String[] keys = combineKey.trim().split(" ");
|
|
|
|
|
Assert.isTrue(keys.length == 2, "腾讯云短信 apiKey 配置格式错误,请配置 为[secretId sdkAppId]");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 调用腾讯云 SDK 发送短信
|
|
|
|
|
*
|
|
|
|
|
* @param request 发送短信请求
|
|
|
|
|
* @return 发送短信响应
|
|
|
|
|
* @throws TencentCloudSDKException SDK 用来封装发送短信失败
|
|
|
|
|
*/
|
|
|
|
|
private SendSmsResponse doSendSms0(SendSmsRequest request) throws TencentCloudSDKException {
|
|
|
|
|
return client.SendSms(request);
|
|
|
|
|
private String getSdkAppId() {
|
|
|
|
|
return StrUtil.subAfter(properties.getApiKey(), " ", true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 封装腾讯云发送短信请求
|
|
|
|
|
*
|
|
|
|
|
* @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) {
|
|
|
|
|
private String getApiKey() {
|
|
|
|
|
return StrUtil.subBefore(properties.getApiKey(), " ", true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public SmsSendRespDTO sendSms(Long sendLogId, String mobile,
|
|
|
|
|
String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
|
|
|
|
|
// 构建请求
|
|
|
|
|
SendSmsRequest request = new SendSmsRequest();
|
|
|
|
|
request.setSmsSdkAppId(((TencentSmsChannelProperties) properties).getSdkAppId());
|
|
|
|
|
request.setSmsSdkAppId(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;
|
|
|
|
|
// 执行请求
|
|
|
|
|
SendSmsResponse response = client.SendSms(request);
|
|
|
|
|
SendStatus status = response.getSendStatusSet()[0];
|
|
|
|
|
return new SmsSendRespDTO().setSuccess(Objects.equals(status.getCode(), API_CODE_SUCCESS)).setSerialNo(status.getSerialNo())
|
|
|
|
|
.setApiRequestId(response.getRequestId()).setApiCode(status.getCode()).setApiMsg(status.getMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable {
|
|
|
|
|
public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
|
|
|
|
|
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;
|
|
|
|
|
});
|
|
|
|
|
return convertList(callback, status -> new SmsReceiveRespDTO()
|
|
|
|
|
.setSuccess(SmsReceiveStatus.SUCCESS_CODE.equalsIgnoreCase(status.getStatus()))
|
|
|
|
|
.setErrorCode(status.getErrCode()).setErrorMsg(status.getDescription())
|
|
|
|
|
.setMobile(status.getMobile()).setReceiveTime(status.getReceiveTime())
|
|
|
|
|
.setSerialNo(status.getSerialNo()).setLogId(status.getSessionContext().getLogId()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@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) {
|
|
|
|
|
public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
|
|
|
|
|
// 构建请求
|
|
|
|
|
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);
|
|
|
|
|
request.setInternational(INTERNATIONAL_CHINA);
|
|
|
|
|
// 执行请求
|
|
|
|
|
DescribeSmsTemplateListResponse response = client.DescribeSmsTemplateList(request);
|
|
|
|
|
DescribeTemplateListStatus status = response.getDescribeTemplateStatusSet()[0];
|
|
|
|
|
if (status == null || status.getStatusCode() == null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return new SmsTemplateRespDTO().setId(status.getTemplateId().toString()).setContent(status.getTemplateContent())
|
|
|
|
|
.setAuditStatus(convertSmsTemplateAuditStatus(status.getStatusCode().intValue())).setAuditReason(status.getReviewReply());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
<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);
|
|
|
|
|
@VisibleForTesting
|
|
|
|
|
Integer convertSmsTemplateAuditStatus(int templateStatus) {
|
|
|
|
|
switch (templateStatus) {
|
|
|
|
|
case 1: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
|
|
|
|
|
case 0: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
|
|
|
|
|
case -1: return SmsTemplateAuditStatusEnum.FAIL.getStatus();
|
|
|
|
|
default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
|
|
|
|
|
}
|
|
|
|
|
return resultGen.apply(response);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Data
|
|
|
|
|
@ -278,7 +198,7 @@ public class TencentSmsClient extends AbstractSmsClient {
|
|
|
|
|
private String serialNo;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 用户的 session 内容(与发送接口的请求参数SessionContext一致)
|
|
|
|
|
* 用户的 session 内容(与发送接口的请求参数 SessionContext 一致)
|
|
|
|
|
*/
|
|
|
|
|
@JsonProperty("ext")
|
|
|
|
|
private SessionContext sessionContext;
|
|
|
|
|
@ -293,10 +213,7 @@ public class TencentSmsClient extends AbstractSmsClient {
|
|
|
|
|
* 发送短信记录id
|
|
|
|
|
*/
|
|
|
|
|
private Long logId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private interface SdkFunction<T, R> {
|
|
|
|
|
R apply(T t) throws TencentCloudSDKException;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|