diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/AbstractSmsClient.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/AbstractSmsClient.java index 0ef4869b4..13d1f7c8c 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/AbstractSmsClient.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/AbstractSmsClient.java @@ -31,7 +31,7 @@ public abstract class AbstractSmsClient implements SmsClient { protected final SmsCodeMapping codeMapping; public AbstractSmsClient(SmsChannelProperties properties, SmsCodeMapping codeMapping) { - this.properties = properties; + this.properties = prepareProperties(properties); this.codeMapping = codeMapping; } @@ -54,11 +54,21 @@ public abstract class AbstractSmsClient implements SmsClient { return; } log.info("[refresh][配置({})发生变化,重新初始化]", properties); - this.properties = 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(); diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClient.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClient.java index 43c94f776..6132beab6 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClient.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClient.java @@ -14,6 +14,7 @@ import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO; import cn.iocoder.yudao.framework.sms.core.client.impl.AbstractSmsClient; import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum; import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.yudao.framework.sms.core.property.TencentSmsChannelProperties; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.annotations.VisibleForTesting; @@ -42,21 +43,33 @@ public class TencentSmsClient extends AbstractSmsClient { private SmsClient client; + /** + * 调用成功 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; + public TencentSmsClient(SmsChannelProperties properties) { - // 腾讯云发放短信的时候,需要额外的参数 sdkAppId。考虑到不破坏原有的 apiKey + apiSecret 的结构,所以将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。 - // 因此,这边需要使用 TencentSmsChannelProperties 做拆分,重新封装到 properties 内。 - super(TencentSmsChannelProperties.build(properties), new TencentSmsCodeMapping()); + super(properties, new TencentSmsCodeMapping()); Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); } @Override protected void doInit() { - // init 或者 refresh 时,需要重新封装 properties - properties = TencentSmsChannelProperties.build(properties); // 实例化一个认证对象,入参需要传入腾讯云账户密钥对 secretId,secretKey Credential credential = new Credential(properties.getApiKey(), properties.getApiSecret()); - // TODO @FinallySays:那把 ap-nanjing 枚举下到这个类的静态变量里哈。 - client = new SmsClient(credential, "ap-nanjing"); + client = new SmsClient(credential, ENDPOINT); } @Override @@ -73,6 +86,20 @@ public class TencentSmsClient extends AbstractSmsClient { }); } + + /** + * 腾讯云发放短信的时候,需要额外的参数 sdkAppId。 + * 考虑到不破坏原有的 apiKey + apiSecret 的结构,所以将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。 + * 因此,这边需要使用 TencentSmsChannelProperties 做拆分,重新封装到 properties 内。 + * + * @param properties 数据库中存储的短信渠道配置 + * @return TencentSmsChannelProperties + */ + @Override + protected SmsChannelProperties prepareProperties(SmsChannelProperties properties) { + return TencentSmsChannelProperties.build(properties); + } + /** * 调用腾讯云 SDK 发送短信 * @@ -113,7 +140,7 @@ public class TencentSmsClient extends AbstractSmsClient { return CollectionUtils.convertList(callback, status -> { SmsReceiveRespDTO data = new SmsReceiveRespDTO(); data.setErrorCode(status.getErrCode()).setErrorMsg(status.getDescription()); - data.setReceiveTime(status.getReceiveTime()).setSuccess("SUCCESS".equalsIgnoreCase(status.getStatus())); + data.setReceiveTime(status.getReceiveTime()).setSuccess(SmsReceiveStatus.SUCCESS_CODE.equalsIgnoreCase(status.getStatus())); data.setMobile(status.getMobile()).setSerialNo(status.getSerialNo()); SessionContext context; Long logId; @@ -130,7 +157,7 @@ public class TencentSmsClient extends AbstractSmsClient { this::doGetSmsTemplate0, response -> { SmsTemplateRespDTO data = convertTemplateStatusDTO(response.getDescribeTemplateStatusSet()[0]); - return SmsCommonResult.build("Ok", null, response.getRequestId(), data, codeMapping); + return SmsCommonResult.build(API_SUCCESS_CODE, null, response.getRequestId(), data, codeMapping); }); } @@ -171,8 +198,7 @@ public class TencentSmsClient extends AbstractSmsClient { DescribeSmsTemplateListRequest request = new DescribeSmsTemplateListRequest(); request.setTemplateIdSet(new Long[]{Long.parseLong(apiTemplateId)}); // 地区 0:表示国内短信。1:表示国际/港澳台短信。 - // TODO @FinallySays:那把 0L 枚举下到这个类的静态变量里哈。 - request.setInternational(0L); + request.setInternational(INTERNATIONAL); return request; } @@ -206,6 +232,11 @@ public class TencentSmsClient extends AbstractSmsClient { @Data private static class SmsReceiveStatus { + /** + * 短信接受成功 code + */ + public static final String SUCCESS_CODE = "SUCCESS"; + /** * 用户实际接收到短信的时间 */ @@ -270,27 +301,4 @@ public class TencentSmsClient extends AbstractSmsClient { R apply(T t) throws TencentCloudSDKException; } - // TODO @FinallySays:要不单独一个类,不用作为内部类哈。这样,可能一看就知道,哟,腾讯短信是特殊的 - @Data - private static class TencentSmsChannelProperties extends SmsChannelProperties { - - private String 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; - } - } - - } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMapping.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMapping.java index c048bf855..05ad355ef 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMapping.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMapping.java @@ -19,7 +19,7 @@ public class TencentSmsCodeMapping implements SmsCodeMapping { @Override public ErrorCode apply(String apiCode) { switch (apiCode) { - case "Ok": return GlobalErrorCodeConstants.SUCCESS; + case TencentSmsClient.API_SUCCESS_CODE: return GlobalErrorCodeConstants.SUCCESS; case "FailedOperation.ContainSensitiveWord": return SMS_SEND_CONTENT_INVALID; case "FailedOperation.JsonParseFail": case "MissingParameter.EmptyPhoneNumberSet": diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/property/TencentSmsChannelProperties.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/property/TencentSmsChannelProperties.java new file mode 100644 index 000000000..1fc4b947e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/property/TencentSmsChannelProperties.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.framework.sms.core.property; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.lang.Assert; +import lombok.Data; + +/** + * 腾讯云短信配置实现类 + * 腾讯云发送短信时,需要额外的参数 sdkAppId, + * + * @author shiwp + */ +@Data + +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; + } +} \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java index ae23bf494..64305c7a0 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java @@ -64,6 +64,19 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client")); } + @Test + public void testRefresh() { + // 准备参数 + SmsChannelProperties p = new SmsChannelProperties() + .setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey,避免构建报错 + .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 + .setSignature("芋道源码"); + // 调用 + smsClient.refresh(p); + // 断言 + assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client")); + } + @Test public void testDoSendSms() throws Throwable { // 准备参数 @@ -81,7 +94,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { o.setSendStatusSet(sendStatuses); SendStatus sendStatus = new SendStatus(); sendStatuses[0] = sendStatus; - sendStatus.setCode("Ok"); + sendStatus.setCode(TencentSmsClient.API_SUCCESS_CODE); sendStatus.setMessage("send success"); sendStatus.setSerialNo(serialNo); }); @@ -162,7 +175,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { // 调用 SmsCommonResult result = smsClient.doGetSmsTemplate(apiTemplateId.toString()); // 断言 - assertEquals("Ok", result.getApiCode()); + assertEquals(TencentSmsClient.API_SUCCESS_CODE, result.getApiCode()); assertNull(result.getApiMsg()); assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); @@ -174,12 +187,23 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest { assertEquals(response.getDescribeTemplateStatusSet()[0].getReviewReply(), result.getData().getAuditReason()); } - // TODO @FinallySays:这个单测,按道理说应该是写成 4 个方法,每个对应一种情况。 @Test - public void testConvertTemplateStatusDTO() { + 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(); diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMappingTest.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMappingTest.java index a05cf1cf2..ebcdaf18a 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMappingTest.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMappingTest.java @@ -20,7 +20,7 @@ public class TencentSmsCodeMappingTest extends BaseMockitoUnitTest { @Test public void testApply() { - assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply("Ok")); + 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"));