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