From 5acdd1554ef2f2c2540cf44207be2684282870f4 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 27 Jan 2024 11:25:43 +0800 Subject: [PATCH] ✨ 全局:将 sms 融合到 system 模块里,减少 starter --- yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/config/YudaoSmsAutoConfiguration.java | 21 --------------------- yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsClient.java | 54 ------------------------------------------------------ yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsClientFactory.java | 36 ------------------------------------ yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsReceiveRespDTO.java | 48 ------------------------------------------------ yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsSendRespDTO.java | 43 ------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsTemplateRespDTO.java | 33 --------------------------------- yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/AbstractSmsClient.java | 54 ------------------------------------------------------ yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/SmsClientFactoryImpl.java | 90 ------------------------------------------------------------------------------------------ yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java | 183 --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java | 96 ------------------------------------------------------------------------------------------------ yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClient.java | 219 --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsChannelEnum.java | 36 ------------------------------------ yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java | 50 -------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java | 21 --------------------- yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/property/SmsChannelProperties.java | 52 ---------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 1 - yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java | 187 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java | 230 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-module-system/yudao-module-system-biz/pom.xml | 12 ++++++++++++ yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java | 2 +- yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/sms/SmsChannelDO.java | 2 +- yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/SmsCodeConfiguration.java | 9 --------- yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/SmsCodeProperties.java | 41 ----------------------------------------- yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/config/SmsCodeProperties.java | 41 +++++++++++++++++++++++++++++++++++++++++ yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/config/SmsConfiguration.java | 23 +++++++++++++++++++++++ yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClient.java | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClientFactory.java | 36 ++++++++++++++++++++++++++++++++++++ yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/dto/SmsReceiveRespDTO.java | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/dto/SmsSendRespDTO.java | 43 +++++++++++++++++++++++++++++++++++++++++++ yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/dto/SmsTemplateRespDTO.java | 33 +++++++++++++++++++++++++++++++++ yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AbstractSmsClient.java | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java | 182 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java | 218 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/enums/SmsChannelEnum.java | 36 ++++++++++++++++++++++++++++++++++++ yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java | 21 +++++++++++++++++++++ yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/property/SmsChannelProperties.java | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelService.java | 2 +- yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceImpl.java | 6 +++--- yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsCodeServiceImpl.java | 2 +- yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImpl.java | 6 +++--- yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImpl.java | 6 +++--- yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java | 187 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java | 230 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceTest.java | 6 +++--- yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsCodeServiceImplTest.java | 2 +- yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImplTest.java | 6 +++--- yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImplTest.java | 6 +++--- 49 files changed, 1475 insertions(+), 1527 deletions(-) delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/config/YudaoSmsAutoConfiguration.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsClient.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsClientFactory.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsReceiveRespDTO.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsSendRespDTO.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsTemplateRespDTO.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/AbstractSmsClient.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/SmsClientFactoryImpl.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClient.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsChannelEnum.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/property/SmsChannelProperties.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java delete mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/SmsCodeConfiguration.java delete mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/SmsCodeProperties.java create mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/config/SmsCodeProperties.java create mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/config/SmsConfiguration.java create mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClient.java create mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClientFactory.java create mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/dto/SmsReceiveRespDTO.java create mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/dto/SmsSendRespDTO.java create mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/dto/SmsTemplateRespDTO.java create mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AbstractSmsClient.java create mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java create mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java create mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java create mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java create mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/enums/SmsChannelEnum.java create mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java create mode 100644 yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/property/SmsChannelProperties.java create mode 100644 yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java create mode 100644 yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/config/YudaoSmsAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/config/YudaoSmsAutoConfiguration.java deleted file mode 100644 index 9873c80..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/config/YudaoSmsAutoConfiguration.java +++ /dev/null @@ -1,21 +0,0 @@ -package cn.iocoder.yudao.framework.sms.config; - -import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory; -import cn.iocoder.yudao.framework.sms.core.client.impl.SmsClientFactoryImpl; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.context.annotation.Bean; - -/** - * 短信配置类 - * - * @author 芋道源码 - */ -@AutoConfiguration -public class YudaoSmsAutoConfiguration { - - @Bean - public SmsClientFactory smsClientFactory() { - return new SmsClientFactoryImpl(); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsClient.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsClient.java deleted file mode 100644 index ef945a3..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsClient.java +++ /dev/null @@ -1,54 +0,0 @@ -package cn.iocoder.yudao.framework.sms.core.client; - -import cn.iocoder.yudao.framework.common.core.KeyValue; -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; - -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 短信发送结果 - */ - SmsSendRespDTO sendSms(Long logId, String mobile, String apiTemplateId, - List> templateParams) throws Throwable; - - /** - * 解析接收短信的接收结果 - * - * @param text 结果 - * @return 结果内容 - * @throws Throwable 当解析 text 发生异常时,则会抛出异常 - */ - List parseSmsReceiveStatus(String text) throws Throwable; - - /** - * 查询指定的短信模板 - * - * @param apiTemplateId 短信 API 的模板编号 - * @return 短信模板 - */ - SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable; - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsClientFactory.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsClientFactory.java deleted file mode 100644 index d063e58..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsClientFactory.java +++ /dev/null @@ -1,36 +0,0 @@ -package cn.iocoder.yudao.framework.sms.core.client; - -import cn.iocoder.yudao.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); - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsReceiveRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsReceiveRespDTO.java deleted file mode 100644 index 4def4a2..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsReceiveRespDTO.java +++ /dev/null @@ -1,48 +0,0 @@ -package cn.iocoder.yudao.framework.sms.core.client.dto; - -import lombok.Data; - -import java.time.LocalDateTime; - -/** - * 消息接收 Response DTO - * - * @author 芋道源码 - */ -@Data -public class SmsReceiveRespDTO { - - /** - * 是否接收成功 - */ - private Boolean success; - /** - * API 接收结果的编码 - */ - private String errorCode; - /** - * API 接收结果的说明 - */ - private String errorMsg; - - /** - * 手机号 - */ - private String mobile; - /** - * 用户接收时间 - */ - private LocalDateTime receiveTime; - - /** - * 短信 API 发送返回的序号 - */ - private String serialNo; - /** - * 短信日志编号 - * - * 对应 SysSmsLogDO 的编号 - */ - private Long logId; - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsSendRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsSendRespDTO.java deleted file mode 100644 index ec51750..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsSendRespDTO.java +++ /dev/null @@ -1,43 +0,0 @@ -package cn.iocoder.yudao.framework.sms.core.client.dto; - -import lombok.Data; - -/** - * 短信发送 Response DTO - * - * @author 芋道源码 - */ -@Data -public class SmsSendRespDTO { - - /** - * 是否成功 - */ - private Boolean success; - - /** - * API 请求编号 - */ - private String apiRequestId; - - // ==================== 成功时字段 ==================== - - /** - * 短信 API 发送返回的序号 - */ - private String serialNo; - - // ==================== 失败时字段 ==================== - - /** - * API 返回错误码 - * - * 由于第三方的错误码可能是字符串,所以使用 String 类型 - */ - private String apiCode; - /** - * API 返回提示 - */ - private String apiMsg; - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsTemplateRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsTemplateRespDTO.java deleted file mode 100644 index 70e8df4..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsTemplateRespDTO.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.iocoder.yudao.framework.sms.core.client.dto; - -import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum; -import lombok.Data; - -/** - * 短信模板 Response DTO - * - * @author 芋道源码 - */ -@Data -public class SmsTemplateRespDTO { - - /** - * 模板编号 - */ - private String id; - /** - * 短信内容 - */ - private String content; - /** - * 审核状态 - * - * 枚举 {@link SmsTemplateAuditStatusEnum} - */ - private Integer auditStatus; - /** - * 审核未通过的理由 - */ - private String auditReason; - -} 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 deleted file mode 100644 index a3b92d5..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/AbstractSmsClient.java +++ /dev/null @@ -1,54 +0,0 @@ -package cn.iocoder.yudao.framework.sms.core.client.impl; - -import cn.iocoder.yudao.framework.sms.core.client.SmsClient; -import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; -import lombok.extern.slf4j.Slf4j; - -/** - * 短信客户端的抽象类,提供模板方法,减少子类的冗余代码 - * - * @author zzf - * @since 2021/2/1 9:28 - */ -@Slf4j -public abstract class AbstractSmsClient implements SmsClient { - - /** - * 短信渠道配置 - */ - protected volatile SmsChannelProperties properties; - - public AbstractSmsClient(SmsChannelProperties properties) { - this.properties = properties; - } - - /** - * 初始化 - */ - public final void init() { - doInit(); - log.debug("[init][配置({}) 初始化完成]", properties); - } - - /** - * 自定义初始化 - */ - protected abstract void doInit(); - - public final void refresh(SmsChannelProperties properties) { - // 判断是否更新 - if (properties.equals(this.properties)) { - return; - } - log.info("[refresh][配置({})发生变化,重新初始化]", properties); - this.properties = properties; - // 初始化 - this.init(); - } - - @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/SmsClientFactoryImpl.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/SmsClientFactoryImpl.java deleted file mode 100644 index abb7dda..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/SmsClientFactoryImpl.java +++ /dev/null @@ -1,90 +0,0 @@ -package cn.iocoder.yudao.framework.sms.core.client.impl; - -import cn.iocoder.yudao.framework.sms.core.client.SmsClient; -import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory; -import cn.iocoder.yudao.framework.sms.core.client.impl.aliyun.AliyunSmsClient; -import cn.iocoder.yudao.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient; -import cn.iocoder.yudao.framework.sms.core.client.impl.tencent.TencentSmsClient; -import cn.iocoder.yudao.framework.sms.core.enums.SmsChannelEnum; -import cn.iocoder.yudao.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 channelIdClients = new ConcurrentHashMap<>(); - - /** - * 短信客户端 Map - * key:渠道编码,使用 {@link SmsChannelProperties#getCode()} ()} - * - * 注意,一些场景下,需要获得某个渠道类型的客户端,所以需要使用它。 - * 例如说,解析短信接收结果,是相对通用的,不需要使用某个渠道编号的 {@link #channelIdClients} - */ - private final ConcurrentMap 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 DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties); - case TENCENT: return new TencentSmsClient(properties); - } - // 创建失败,错误日志 + 抛出异常 - log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties); - throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", properties)); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java deleted file mode 100644 index d32a5bb..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java +++ /dev/null @@ -1,183 +0,0 @@ -package cn.iocoder.yudao.framework.sms.core.client.impl.aliyun; - -import cn.hutool.core.lang.Assert; -import cn.iocoder.yudao.framework.common.core.KeyValue; -import cn.iocoder.yudao.framework.common.util.collection.MapUtils; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -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; -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 com.aliyuncs.DefaultAcsClient; -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.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.time.LocalDateTime; -import java.util.List; -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; - -/** - * 阿里短信客户端的实现类 - * - * @author zzf - * @since 2021/1/25 14:17 - */ -@Slf4j -public class AliyunSmsClient extends AbstractSmsClient { - - /** - * 调用成功 code - */ - public static final String API_CODE_SUCCESS = "OK"; - - /** - * REGION, 使用杭州 - */ - private static final String ENDPOINT = "cn-hangzhou"; - - /** - * 阿里云客户端 - */ - private volatile IAcsClient client; - - public AliyunSmsClient(SmsChannelProperties properties) { - super(properties); - 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 - public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, - List> templateParams) throws Throwable { - // 构建请求 - 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)); - // 执行请求 - SendSmsResponse response = client.getAcsResponse(request); - return new SmsSendRespDTO().setSuccess(Objects.equals(response.getCode(), API_CODE_SUCCESS)).setSerialNo(response.getBizId()) - .setApiRequestId(response.getRequestId()).setApiCode(response.getCode()).setApiMsg(response.getMessage()); - } - - @Override - public List parseSmsReceiveStatus(String text) { - List statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class); - return convertList(statuses, status -> new SmsReceiveRespDTO().setSuccess(status.getSuccess()) - .setErrorCode(status.getErrCode()).setErrorMsg(status.getErrMsg()) - .setMobile(status.getPhoneNumber()).setReceiveTime(status.getReportTime()) - .setSerialNo(status.getBizId()).setLogId(Long.valueOf(status.getOutId()))); - } - - @Override - public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { - // 构建请求 - QuerySmsTemplateRequest request = new QuerySmsTemplateRequest(); - request.setTemplateCode(apiTemplateId); - // 执行请求 - QuerySmsTemplateResponse response = client.getAcsResponse(request); - if (response.getTemplateStatus() == null) { - return null; - } - return new SmsTemplateRespDTO().setId(response.getTemplateCode()).setContent(response.getTemplateContent()) - .setAuditStatus(convertSmsTemplateAuditStatus(response.getTemplateStatus())).setAuditReason(response.getReason()); - } - - @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)); - } - } - - /** - * 短信接收状态 - * - * 参见 文档 - * - * @author 芋道源码 - */ - @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 LocalDateTime sendTime; - /** - * 状态报告时间 - */ - @JsonProperty("report_time") - @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) - private LocalDateTime 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; - - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java deleted file mode 100644 index 315edf9..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java +++ /dev/null @@ -1,96 +0,0 @@ -package cn.iocoder.yudao.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 cn.iocoder.yudao.framework.common.core.KeyValue; -import cn.iocoder.yudao.framework.common.util.collection.MapUtils; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -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; -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 java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -/** - * 基于钉钉 WebHook 实现的调试的短信客户端实现类 - * - * 考虑到省钱,我们使用钉钉 WebHook 模拟发送短信,方便调试。 - * - * @author 芋道源码 - */ -public class DebugDingTalkSmsClient extends AbstractSmsClient { - - public DebugDingTalkSmsClient(SmsChannelProperties properties) { - super(properties); - Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); - Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); - } - - @Override - protected void doInit() { - } - - @Override - public SmsSendRespDTO sendSms(Long sendLogId, String mobile, - String apiTemplateId, List> templateParams) throws Throwable { - // 构建请求 - String url = buildUrl("robot/send"); - Map 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); - String errorCode = MapUtil.getStr(responseObj, "errcode"); - return new SmsSendRespDTO().setSuccess(Objects.equals(errorCode, "0")).setSerialNo(StrUtil.uuid()) - .setApiCode(errorCode).setApiMsg(MapUtil.getStr(responseObj, "errorMsg")); - } - - /** - * 构建请求地址 - * - * 参见 文档 - * - * @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×tamp=%d&sign=%s", - path, properties.getApiKey(), timestamp, sign); - } - - @Override - public List parseSmsReceiveStatus(String text) { - throw new UnsupportedOperationException("模拟短信客户端,暂时无需解析回调"); - } - - @Override - public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) { - return new SmsTemplateRespDTO().setId(apiTemplateId).setContent("") - .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(""); - } - -} 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 deleted file mode 100644 index eeb287f..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClient.java +++ /dev/null @@ -1,219 +0,0 @@ -package cn.iocoder.yudao.framework.sms.core.client.impl.tencent; - -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.json.JsonUtils; -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; -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 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.sms.v20210111.SmsClient; -import com.tencentcloudapi.sms.v20210111.models.*; -import lombok.Data; - -import java.time.LocalDateTime; -import java.util.List; -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; - -/** - * 腾讯云短信功能实现 - * - * 参见 文档 - * - * @author shiwp - */ -public class TencentSmsClient extends AbstractSmsClient { - - /** - * 调用成功 code - */ - public static final String API_CODE_SUCCESS = "Ok"; - - /** - * REGION,使用南京 - */ - private static final String ENDPOINT = "ap-nanjing"; - - /** - * 是否国际/港澳台短信: - * - * 0:表示国内短信。 - * 1:表示国际/港澳台短信。 - */ - private static final long INTERNATIONAL_CHINA = 0L; - - private SmsClient client; - - public TencentSmsClient(SmsChannelProperties properties) { - super(properties); - Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); - validateSdkAppId(properties); - } - - @Override - protected void doInit() { - // 实例化一个认证对象,入参需要传入腾讯云账户密钥对 secretId,secretKey - Credential credential = new Credential(getApiKey(), properties.getApiSecret()); - client = new SmsClient(credential, ENDPOINT); - } - - /** - * 参数校验腾讯云的 SDK AppId - * - * 原因是:腾讯云发放短信的时候,需要额外的参数 sdkAppId - * - * 解决方案:考虑到不破坏原有的 apiKey + apiSecret 的结构,所以将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。 - * - * @param 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]"); - } - - private String getSdkAppId() { - return StrUtil.subAfter(properties.getApiKey(), " ", true); - } - - private String getApiKey() { - return StrUtil.subBefore(properties.getApiKey(), " ", true); - } - - @Override - public SmsSendRespDTO sendSms(Long sendLogId, String mobile, - String apiTemplateId, List> templateParams) throws Throwable { - // 构建请求 - SendSmsRequest request = new SendSmsRequest(); - 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))); - // 执行请求 - 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 - public List parseSmsReceiveStatus(String text) { - List callback = JsonUtils.parseArray(text, SmsReceiveStatus.class); - 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 - public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { - // 构建请求 - DescribeSmsTemplateListRequest request = new DescribeSmsTemplateListRequest(); - request.setTemplateIdSet(new Long[]{Long.parseLong(apiTemplateId)}); - 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()); - } - - @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)); - } - } - - @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 LocalDateTime 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 - static class SessionContext { - - /** - * 发送短信记录id - */ - private Long logId; - - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsChannelEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsChannelEnum.java deleted file mode 100644 index 66410bd..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsChannelEnum.java +++ /dev/null @@ -1,36 +0,0 @@ -package cn.iocoder.yudao.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", "调试(钉钉)"), - 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()); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java deleted file mode 100644 index b9a226f..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java +++ /dev/null @@ -1,50 +0,0 @@ -package cn.iocoder.yudao.framework.sms.core.enums; - -import cn.iocoder.yudao.framework.common.exception.ErrorCode; - -/** - * 短信框架的错误码枚举 - * - * 短信框架,使用 2-001-000-000 段 - * - * @author 芋道源码 - */ -public interface SmsFrameworkErrorCodeConstants { - - ErrorCode SMS_UNKNOWN = new ErrorCode(2_001_000_000, "未知错误,需要解析"); - - // ========== 权限 / 限流等相关 2-001-000-100 ========== - - ErrorCode SMS_PERMISSION_DENY = new ErrorCode(2_001_000_100, "没有发送短信的权限"); - ErrorCode SMS_IP_DENY = new ErrorCode(2_001_000_100, "IP 不允许发送短信"); - - // 阿里云:将短信发送频率限制在正常的业务限流范围内。默认短信验证码:使用同一签名,对同一个手机号验证码,支持 1 条 / 分钟,5 条 / 小时,累计 10 条 / 天。 - ErrorCode SMS_SEND_BUSINESS_LIMIT_CONTROL = new ErrorCode(2_001_000_102, "指定手机的发送限流"); - // 阿里云:已经达到您在控制台设置的短信日发送量限额值。在国内消息设置 > 安全设置,修改发送总量阈值。 - ErrorCode SMS_SEND_DAY_LIMIT_CONTROL = new ErrorCode(2_001_000_103, "每天的发送限流"); - - ErrorCode SMS_SEND_CONTENT_INVALID = new ErrorCode(2_001_000_104, "短信内容有敏感词"); - - // 腾讯云:为避免骚扰用户,营销短信只允许在8点到22点发送。 - ErrorCode SMS_SEND_MARKET_LIMIT_CONTROL = new ErrorCode(2_001_000_105, "营销短信发送时间限制"); - - // ========== 模板相关 2-001-000-200 ========== - ErrorCode SMS_TEMPLATE_INVALID = new ErrorCode(2_001_000_200, "短信模板不合法"); // 包括短信模板不存在 - ErrorCode SMS_TEMPLATE_PARAM_ERROR = new ErrorCode(2_001_000_201, "模板参数不正确"); - - // ========== 签名相关 2-001-000-300 ========== - ErrorCode SMS_SIGN_INVALID = new ErrorCode(2_001_000_300, "短信签名不可用"); - - // ========== 账户相关 2-001-000-400 ========== - ErrorCode SMS_ACCOUNT_MONEY_NOT_ENOUGH = new ErrorCode(2_001_000_400, "账户余额不足"); - ErrorCode SMS_ACCOUNT_INVALID = new ErrorCode(2_001_000_401, "apiKey 不存在"); - - // ========== 其它相关 2-001-000-900 开头 ========== - ErrorCode SMS_API_PARAM_ERROR = new ErrorCode(2_001_000_900, "请求参数缺失"); - ErrorCode SMS_MOBILE_INVALID = new ErrorCode(2_001_000_901, "手机格式不正确"); - ErrorCode SMS_MOBILE_BLACK = new ErrorCode(2_001_000_902, "手机号在黑名单中"); - ErrorCode SMS_APP_ID_INVALID = new ErrorCode(2_001_000_903, "SdkAppId不合法"); - - ErrorCode EXCEPTION = new ErrorCode(2_001_000_999, "调用异常"); - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java deleted file mode 100644 index 2555650..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java +++ /dev/null @@ -1,21 +0,0 @@ -package cn.iocoder.yudao.framework.sms.core.enums; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * 短信模板的审核状态枚举 - * - * @author 芋道源码 - */ -@AllArgsConstructor -@Getter -public enum SmsTemplateAuditStatusEnum { - - CHECKING(1), - SUCCESS(2), - FAIL(3); - - private final Integer status; - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/property/SmsChannelProperties.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/property/SmsChannelProperties.java deleted file mode 100644 index 491dd43..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/property/SmsChannelProperties.java +++ /dev/null @@ -1,52 +0,0 @@ -package cn.iocoder.yudao.framework.sms.core.property; - -import cn.iocoder.yudao.framework.sms.core.enums.SmsChannelEnum; -import lombok.Data; -import org.springframework.validation.annotation.Validated; - -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; - -/** - * 短信渠道配置类 - * - * @author zzf - * @since 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; - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index 7c9b740..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1 +0,0 @@ -cn.iocoder.yudao.framework.sms.config.YudaoSmsAutoConfiguration \ 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/aliyun/AliyunSmsClientTest.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java deleted file mode 100644 index c60cd26..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java +++ /dev/null @@ -1,187 +0,0 @@ -package cn.iocoder.yudao.framework.sms.core.client.impl.aliyun; - -import cn.hutool.core.util.ReflectUtil; -import cn.iocoder.yudao.framework.common.core.KeyValue; -import cn.iocoder.yudao.framework.common.util.collection.MapUtils; -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; -import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum; -import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; -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.google.common.collect.Lists; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentMatcher; -import org.mockito.InjectMocks; -import org.mockito.Mock; - -import java.time.LocalDateTime; -import java.util.List; - -import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.when; - -/** - * {@link AliyunSmsClient} 的单元测试 - * - * @author 芋道源码 - */ -public class AliyunSmsClientTest extends BaseMockitoUnitTest { - - private final SmsChannelProperties properties = new SmsChannelProperties() - .setApiKey(randomString()) // 随机一个 apiKey,避免构建报错 - .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 - .setSignature("芋道源码"); - - @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 - public void tesSendSms_success() throws Throwable { - // 准备参数 - Long sendLogId = randomLongId(); - String mobile = randomString(); - String apiTemplateId = randomString(); - List> 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) 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); - - // 调用 - SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, - apiTemplateId, templateParams); - // 断言 - assertTrue(result.getSuccess()); - assertEquals(response.getRequestId(), result.getApiRequestId()); - assertEquals(response.getCode(), result.getApiCode()); - assertEquals(response.getMessage(), result.getApiMsg()); - assertEquals(response.getBizId(), result.getSerialNo()); - } - - @Test - public void tesSendSms_fail() throws Throwable { - // 准备参数 - Long sendLogId = randomLongId(); - String mobile = randomString(); - String apiTemplateId = randomString(); - List> templateParams = Lists.newArrayList( - new KeyValue<>("code", 1234), new KeyValue<>("op", "login")); - // mock 方法 - SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> o.setCode("ERROR")); - when(client.getAcsResponse(argThat((ArgumentMatcher) 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); - - // 调用 - SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams); - // 断言 - assertFalse(result.getSuccess()); - assertEquals(response.getRequestId(), result.getApiRequestId()); - assertEquals(response.getCode(), result.getApiCode()); - assertEquals(response.getMessage(), result.getApiMsg()); - assertEquals(response.getBizId(), result.getSerialNo()); - } - - @Test - public void testParseSmsReceiveStatus() { - // 准备参数 - 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 statuses = smsClient.parseSmsReceiveStatus(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(LocalDateTime.of(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 testGetSmsTemplate() throws Throwable { - // 准备参数 - String apiTemplateId = randomString(); - // mock 方法 - QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> { - o.setCode("OK"); - o.setTemplateStatus(1); // 设置模板通过 - }); - when(client.getAcsResponse(argThat((ArgumentMatcher) acsRequest -> { - assertEquals(apiTemplateId, acsRequest.getTemplateCode()); - return true; - }))).thenReturn(response); - - // 调用 - SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId); - // 断言 - assertEquals(response.getTemplateCode(), result.getId()); - assertEquals(response.getTemplateContent(), result.getContent()); - assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus()); - assertEquals(response.getReason(), result.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)"); - } - -} 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 deleted file mode 100644 index d62eed1..0000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java +++ /dev/null @@ -1,230 +0,0 @@ -package cn.iocoder.yudao.framework.sms.core.client.impl.tencent; - -import cn.hutool.core.util.ReflectUtil; -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.MapUtils; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -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; -import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum; -import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; -import cn.iocoder.yudao.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.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; -import static org.junit.jupiter.api.Assertions.*; -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("芋道源码"); - - @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("芋道源码"); - // 调用 - smsClient.refresh(p); - // 断言 - assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client")); - } - - @Test - public void testDoSendSms_success() throws Throwable { - // 准备参数 - Long sendLogId = randomLongId(); - String mobile = randomString(); - String apiTemplateId = randomString(); - List> 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_CODE_SUCCESS); - 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); - - // 调用 - SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams); - // 断言 - assertTrue(result.getSuccess()); - assertEquals(response.getRequestId(), result.getApiRequestId()); - assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode()); - assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg()); - assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getSerialNo()); - } - - @Test - public void testDoSendSms_fail() throws Throwable { - // 准备参数 - Long sendLogId = randomLongId(); - String mobile = randomString(); - String apiTemplateId = randomString(); - List> 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("ERROR"); - 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); - - // 调用 - SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams); - // 断言 - assertFalse(result.getSuccess()); - assertEquals(response.getRequestId(), result.getApiRequestId()); - assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode()); - assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg()); - assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getSerialNo()); - } - - @Test - public void testParseSmsReceiveStatus() { - // 准备参数 - 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 statuses = smsClient.parseSmsReceiveStatus(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(LocalDateTime.of(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 testGetSmsTemplate() 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); - - // 调用 - SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId.toString()); - // 断言 - assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateId().toString(), result.getId()); - assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateContent(), result.getContent()); - assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus()); - assertEquals(response.getDescribeTemplateStatusSet()[0].getReviewReply(), result.getAuditReason()); - } - - @Test - public void testConvertSmsTemplateAuditStatus() { - assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), - smsClient.convertSmsTemplateAuditStatus(0)); - assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(), - smsClient.convertSmsTemplateAuditStatus(1)); - assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(), - smsClient.convertSmsTemplateAuditStatus(-1)); - assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus(3), - "未知审核状态(3)"); - } - -} diff --git a/yudao-module-system/yudao-module-system-biz/pom.xml b/yudao-module-system/yudao-module-system-biz/pom.xml index dc8a1b4..87c076c 100644 --- a/yudao-module-system/yudao-module-system-biz/pom.xml +++ b/yudao-module-system/yudao-module-system-biz/pom.xml @@ -127,6 +127,18 @@ wx-java-miniapp-spring-boot-starter + + com.aliyun + aliyun-java-sdk-core + + + com.aliyun + aliyun-java-sdk-dysmsapi + + + com.tencentcloudapi + tencentcloud-sdk-java-sms + diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java index 5a55352..f392ac7 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsCallbackController.java @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.system.controller.admin.sms; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; -import cn.iocoder.yudao.framework.sms.core.enums.SmsChannelEnum; +import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsChannelEnum; import cn.iocoder.yudao.module.system.service.sms.SmsSendService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/sms/SmsChannelDO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/sms/SmsChannelDO.java index c57ea5b..6ecb4f1 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/sms/SmsChannelDO.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/sms/SmsChannelDO.java @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.system.dal.dataobject.sms; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.framework.sms.core.enums.SmsChannelEnum; +import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsChannelEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/SmsCodeConfiguration.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/SmsCodeConfiguration.java deleted file mode 100644 index ddc70a3..0000000 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/SmsCodeConfiguration.java +++ /dev/null @@ -1,9 +0,0 @@ -package cn.iocoder.yudao.module.system.framework.sms; - -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -@Configuration(proxyBeanMethods = false) -@EnableConfigurationProperties(SmsCodeProperties.class) -public class SmsCodeConfiguration { -} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/SmsCodeProperties.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/SmsCodeProperties.java deleted file mode 100644 index f2c05a1..0000000 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/SmsCodeProperties.java +++ /dev/null @@ -1,41 +0,0 @@ -package cn.iocoder.yudao.module.system.framework.sms; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.validation.annotation.Validated; - -import jakarta.validation.constraints.NotNull; -import java.time.Duration; - -@ConfigurationProperties(prefix = "yudao.sms-code") -@Validated -@Data -public class SmsCodeProperties { - - /** - * 过期时间 - */ - @NotNull(message = "过期时间不能为空") - private Duration expireTimes; - /** - * 短信发送频率 - */ - @NotNull(message = "短信发送频率不能为空") - private Duration sendFrequency; - /** - * 每日发送最大数量 - */ - @NotNull(message = "每日发送最大数量不能为空") - private Integer sendMaximumQuantityPerDay; - /** - * 验证码最小值 - */ - @NotNull(message = "验证码最小值不能为空") - private Integer beginCode; - /** - * 验证码最大值 - */ - @NotNull(message = "验证码最大值不能为空") - private Integer endCode; - -} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/config/SmsCodeProperties.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/config/SmsCodeProperties.java new file mode 100644 index 0000000..16f15a5 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/config/SmsCodeProperties.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.system.framework.sms.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.constraints.NotNull; +import java.time.Duration; + +@ConfigurationProperties(prefix = "yudao.sms-code") +@Validated +@Data +public class SmsCodeProperties { + + /** + * 过期时间 + */ + @NotNull(message = "过期时间不能为空") + private Duration expireTimes; + /** + * 短信发送频率 + */ + @NotNull(message = "短信发送频率不能为空") + private Duration sendFrequency; + /** + * 每日发送最大数量 + */ + @NotNull(message = "每日发送最大数量不能为空") + private Integer sendMaximumQuantityPerDay; + /** + * 验证码最小值 + */ + @NotNull(message = "验证码最小值不能为空") + private Integer beginCode; + /** + * 验证码最大值 + */ + @NotNull(message = "验证码最大值不能为空") + private Integer endCode; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/config/SmsConfiguration.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/config/SmsConfiguration.java new file mode 100644 index 0000000..eb34775 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/config/SmsConfiguration.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.system.framework.sms.config; + +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.yudao.module.system.framework.sms.core.client.impl.SmsClientFactoryImpl; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 短信配置类,包括短信客户端、短信验证码两部分 + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(SmsCodeProperties.class) +public class SmsConfiguration { + + @Bean + public SmsClientFactory smsClientFactory() { + return new SmsClientFactoryImpl(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClient.java new file mode 100644 index 0000000..4622466 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClient.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.system.framework.sms.core.client; + +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.module.system.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 短信发送结果 + */ + SmsSendRespDTO sendSms(Long logId, String mobile, String apiTemplateId, + List> templateParams) throws Throwable; + + /** + * 解析接收短信的接收结果 + * + * @param text 结果 + * @return 结果内容 + * @throws Throwable 当解析 text 发生异常时,则会抛出异常 + */ + List parseSmsReceiveStatus(String text) throws Throwable; + + /** + * 查询指定的短信模板 + * + * @param apiTemplateId 短信 API 的模板编号 + * @return 短信模板 + */ + SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClientFactory.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClientFactory.java new file mode 100644 index 0000000..a113317 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/SmsClientFactory.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.system.framework.sms.core.client; + +import cn.iocoder.yudao.module.system.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); + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/dto/SmsReceiveRespDTO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/dto/SmsReceiveRespDTO.java new file mode 100644 index 0000000..c90a33e --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/dto/SmsReceiveRespDTO.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.system.framework.sms.core.client.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 消息接收 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SmsReceiveRespDTO { + + /** + * 是否接收成功 + */ + private Boolean success; + /** + * API 接收结果的编码 + */ + private String errorCode; + /** + * API 接收结果的说明 + */ + private String errorMsg; + + /** + * 手机号 + */ + private String mobile; + /** + * 用户接收时间 + */ + private LocalDateTime receiveTime; + + /** + * 短信 API 发送返回的序号 + */ + private String serialNo; + /** + * 短信日志编号 + * + * 对应 SysSmsLogDO 的编号 + */ + private Long logId; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/dto/SmsSendRespDTO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/dto/SmsSendRespDTO.java new file mode 100644 index 0000000..9f3094b --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/dto/SmsSendRespDTO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.system.framework.sms.core.client.dto; + +import lombok.Data; + +/** + * 短信发送 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SmsSendRespDTO { + + /** + * 是否成功 + */ + private Boolean success; + + /** + * API 请求编号 + */ + private String apiRequestId; + + // ==================== 成功时字段 ==================== + + /** + * 短信 API 发送返回的序号 + */ + private String serialNo; + + // ==================== 失败时字段 ==================== + + /** + * API 返回错误码 + * + * 由于第三方的错误码可能是字符串,所以使用 String 类型 + */ + private String apiCode; + /** + * API 返回提示 + */ + private String apiMsg; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/dto/SmsTemplateRespDTO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/dto/SmsTemplateRespDTO.java new file mode 100644 index 0000000..38a43aa --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/dto/SmsTemplateRespDTO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.system.framework.sms.core.client.dto; + +import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import lombok.Data; + +/** + * 短信模板 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SmsTemplateRespDTO { + + /** + * 模板编号 + */ + private String id; + /** + * 短信内容 + */ + private String content; + /** + * 审核状态 + * + * 枚举 {@link SmsTemplateAuditStatusEnum} + */ + private Integer auditStatus; + /** + * 审核未通过的理由 + */ + private String auditReason; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AbstractSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AbstractSmsClient.java new file mode 100644 index 0000000..3b6e0eb --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AbstractSmsClient.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; + +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient; +import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; +import lombok.extern.slf4j.Slf4j; + +/** + * 短信客户端的抽象类,提供模板方法,减少子类的冗余代码 + * + * @author zzf + * @since 2021/2/1 9:28 + */ +@Slf4j +public abstract class AbstractSmsClient implements SmsClient { + + /** + * 短信渠道配置 + */ + protected volatile SmsChannelProperties properties; + + public AbstractSmsClient(SmsChannelProperties properties) { + this.properties = properties; + } + + /** + * 初始化 + */ + public final void init() { + doInit(); + log.debug("[init][配置({}) 初始化完成]", properties); + } + + /** + * 自定义初始化 + */ + protected abstract void doInit(); + + public final void refresh(SmsChannelProperties properties) { + // 判断是否更新 + if (properties.equals(this.properties)) { + return; + } + log.info("[refresh][配置({})发生变化,重新初始化]", properties); + this.properties = properties; + // 初始化 + this.init(); + } + + @Override + public Long getId() { + return properties.getId(); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java new file mode 100644 index 0000000..7d01e6c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java @@ -0,0 +1,182 @@ +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; +import com.aliyuncs.DefaultAcsClient; +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.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.time.LocalDateTime; +import java.util.List; +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; + +/** + * 阿里短信客户端的实现类 + * + * @author zzf + * @since 2021/1/25 14:17 + */ +@Slf4j +public class AliyunSmsClient extends AbstractSmsClient { + + /** + * 调用成功 code + */ + public static final String API_CODE_SUCCESS = "OK"; + + /** + * REGION, 使用杭州 + */ + private static final String ENDPOINT = "cn-hangzhou"; + + /** + * 阿里云客户端 + */ + private volatile IAcsClient client; + + public AliyunSmsClient(SmsChannelProperties properties) { + super(properties); + 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 + public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, + List> templateParams) throws Throwable { + // 构建请求 + 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)); + // 执行请求 + SendSmsResponse response = client.getAcsResponse(request); + return new SmsSendRespDTO().setSuccess(Objects.equals(response.getCode(), API_CODE_SUCCESS)).setSerialNo(response.getBizId()) + .setApiRequestId(response.getRequestId()).setApiCode(response.getCode()).setApiMsg(response.getMessage()); + } + + @Override + public List parseSmsReceiveStatus(String text) { + List statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class); + return convertList(statuses, status -> new SmsReceiveRespDTO().setSuccess(status.getSuccess()) + .setErrorCode(status.getErrCode()).setErrorMsg(status.getErrMsg()) + .setMobile(status.getPhoneNumber()).setReceiveTime(status.getReportTime()) + .setSerialNo(status.getBizId()).setLogId(Long.valueOf(status.getOutId()))); + } + + @Override + public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { + // 构建请求 + QuerySmsTemplateRequest request = new QuerySmsTemplateRequest(); + request.setTemplateCode(apiTemplateId); + // 执行请求 + QuerySmsTemplateResponse response = client.getAcsResponse(request); + if (response.getTemplateStatus() == null) { + return null; + } + return new SmsTemplateRespDTO().setId(response.getTemplateCode()).setContent(response.getTemplateContent()) + .setAuditStatus(convertSmsTemplateAuditStatus(response.getTemplateStatus())).setAuditReason(response.getReason()); + } + + @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)); + } + } + + /** + * 短信接收状态 + * + * 参见 文档 + * + * @author 芋道源码 + */ + @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 LocalDateTime sendTime; + /** + * 状态报告时间 + */ + @JsonProperty("report_time") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private LocalDateTime 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; + + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java new file mode 100644 index 0000000..e9fcc6c --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/DebugDingTalkSmsClient.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; + +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 cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * 基于钉钉 WebHook 实现的调试的短信客户端实现类 + * + * 考虑到省钱,我们使用钉钉 WebHook 模拟发送短信,方便调试。 + * + * @author 芋道源码 + */ +public class DebugDingTalkSmsClient extends AbstractSmsClient { + + public DebugDingTalkSmsClient(SmsChannelProperties properties) { + super(properties); + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); + } + + @Override + protected void doInit() { + } + + @Override + public SmsSendRespDTO sendSms(Long sendLogId, String mobile, + String apiTemplateId, List> templateParams) throws Throwable { + // 构建请求 + String url = buildUrl("robot/send"); + Map 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); + String errorCode = MapUtil.getStr(responseObj, "errcode"); + return new SmsSendRespDTO().setSuccess(Objects.equals(errorCode, "0")).setSerialNo(StrUtil.uuid()) + .setApiCode(errorCode).setApiMsg(MapUtil.getStr(responseObj, "errorMsg")); + } + + /** + * 构建请求地址 + * + * 参见 文档 + * + * @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×tamp=%d&sign=%s", + path, properties.getApiKey(), timestamp, sign); + } + + @Override + public List parseSmsReceiveStatus(String text) { + throw new UnsupportedOperationException("模拟短信客户端,暂时无需解析回调"); + } + + @Override + public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) { + return new SmsTemplateRespDTO().setId(apiTemplateId).setContent("") + .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(""); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java new file mode 100644 index 0000000..94fe88d --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java @@ -0,0 +1,87 @@ +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; + +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient; +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsChannelEnum; +import cn.iocoder.yudao.module.system.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 channelIdClients = new ConcurrentHashMap<>(); + + /** + * 短信客户端 Map + * key:渠道编码,使用 {@link SmsChannelProperties#getCode()} ()} + * + * 注意,一些场景下,需要获得某个渠道类型的客户端,所以需要使用它。 + * 例如说,解析短信接收结果,是相对通用的,不需要使用某个渠道编号的 {@link #channelIdClients} + */ + private final ConcurrentMap 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 DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties); + case TENCENT: return new TencentSmsClient(properties); + } + // 创建失败,错误日志 + 抛出异常 + log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties); + throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", properties)); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java new file mode 100644 index 0000000..dc238be --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java @@ -0,0 +1,218 @@ +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; + +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.json.JsonUtils; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.yudao.module.system.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.sms.v20210111.SmsClient; +import com.tencentcloudapi.sms.v20210111.models.*; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; +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; + +/** + * 腾讯云短信功能实现 + * + * 参见 文档 + * + * @author shiwp + */ +public class TencentSmsClient extends AbstractSmsClient { + + /** + * 调用成功 code + */ + public static final String API_CODE_SUCCESS = "Ok"; + + /** + * REGION,使用南京 + */ + private static final String ENDPOINT = "ap-nanjing"; + + /** + * 是否国际/港澳台短信: + * + * 0:表示国内短信。 + * 1:表示国际/港澳台短信。 + */ + private static final long INTERNATIONAL_CHINA = 0L; + + private SmsClient client; + + public TencentSmsClient(SmsChannelProperties properties) { + super(properties); + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); + validateSdkAppId(properties); + } + + @Override + protected void doInit() { + // 实例化一个认证对象,入参需要传入腾讯云账户密钥对 secretId,secretKey + Credential credential = new Credential(getApiKey(), properties.getApiSecret()); + client = new SmsClient(credential, ENDPOINT); + } + + /** + * 参数校验腾讯云的 SDK AppId + * + * 原因是:腾讯云发放短信的时候,需要额外的参数 sdkAppId + * + * 解决方案:考虑到不破坏原有的 apiKey + apiSecret 的结构,所以将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。 + * + * @param 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]"); + } + + private String getSdkAppId() { + return StrUtil.subAfter(properties.getApiKey(), " ", true); + } + + private String getApiKey() { + return StrUtil.subBefore(properties.getApiKey(), " ", true); + } + + @Override + public SmsSendRespDTO sendSms(Long sendLogId, String mobile, + String apiTemplateId, List> templateParams) throws Throwable { + // 构建请求 + SendSmsRequest request = new SendSmsRequest(); + 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))); + // 执行请求 + 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 + public List parseSmsReceiveStatus(String text) { + List callback = JsonUtils.parseArray(text, SmsReceiveStatus.class); + 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 + public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable { + // 构建请求 + DescribeSmsTemplateListRequest request = new DescribeSmsTemplateListRequest(); + request.setTemplateIdSet(new Long[]{Long.parseLong(apiTemplateId)}); + 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()); + } + + @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)); + } + } + + @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 LocalDateTime 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 + static class SessionContext { + + /** + * 发送短信记录id + */ + private Long logId; + + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/enums/SmsChannelEnum.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/enums/SmsChannelEnum.java new file mode 100644 index 0000000..7bd1922 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/enums/SmsChannelEnum.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.system.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", "调试(钉钉)"), + 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()); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java new file mode 100644 index 0000000..489f69e --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.system.framework.sms.core.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信模板的审核状态枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum SmsTemplateAuditStatusEnum { + + CHECKING(1), + SUCCESS(2), + FAIL(3); + + private final Integer status; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/property/SmsChannelProperties.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/property/SmsChannelProperties.java new file mode 100644 index 0000000..0362b92 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/property/SmsChannelProperties.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.system.framework.sms.core.property; + +import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsChannelEnum; +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +/** + * 短信渠道配置类 + * + * @author zzf + * @since 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; + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelService.java index 13cd843..c33c775 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelService.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelService.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.system.service.sms; import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.sms.core.client.SmsClient; +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient; import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceImpl.java index c4d0889..cca3741 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceImpl.java @@ -3,9 +3,9 @@ package cn.iocoder.yudao.module.system.service.sms; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.framework.sms.core.client.SmsClient; -import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory; -import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient; +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsCodeServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsCodeServiceImpl.java index 065f5fd..5e6637f 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsCodeServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsCodeServiceImpl.java @@ -9,7 +9,7 @@ import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeValidateReqDTO; import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsCodeDO; import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsCodeMapper; import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; -import cn.iocoder.yudao.module.system.framework.sms.SmsCodeProperties; +import cn.iocoder.yudao.module.system.framework.sms.config.SmsCodeProperties; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImpl.java index b05d130..41f429e 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImpl.java @@ -8,9 +8,9 @@ import cn.iocoder.yudao.framework.common.core.KeyValue; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; -import cn.iocoder.yudao.framework.sms.core.client.SmsClient; -import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO; -import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO; import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO; import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO; import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImpl.java index 8096707..a8762a9 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImpl.java @@ -7,9 +7,9 @@ import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.framework.sms.core.client.SmsClient; -import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO; -import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO; import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateSaveReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO; diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java new file mode 100644 index 0000000..ac26d13 --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java @@ -0,0 +1,187 @@ +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; + +import cn.hutool.core.util.ReflectUtil; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; +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.google.common.collect.Lists; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.time.LocalDateTime; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.when; + +/** + * {@link AliyunSmsClient} 的单元测试 + * + * @author 芋道源码 + */ +public class AliyunSmsClientTest extends BaseMockitoUnitTest { + + private final SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey(randomString()) // 随机一个 apiKey,避免构建报错 + .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 + .setSignature("芋道源码"); + + @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 + public void tesSendSms_success() throws Throwable { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString(); + List> 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) 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); + + // 调用 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, + apiTemplateId, templateParams); + // 断言 + assertTrue(result.getSuccess()); + assertEquals(response.getRequestId(), result.getApiRequestId()); + assertEquals(response.getCode(), result.getApiCode()); + assertEquals(response.getMessage(), result.getApiMsg()); + assertEquals(response.getBizId(), result.getSerialNo()); + } + + @Test + public void tesSendSms_fail() throws Throwable { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString(); + List> templateParams = Lists.newArrayList( + new KeyValue<>("code", 1234), new KeyValue<>("op", "login")); + // mock 方法 + SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> o.setCode("ERROR")); + when(client.getAcsResponse(argThat((ArgumentMatcher) 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); + + // 调用 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams); + // 断言 + assertFalse(result.getSuccess()); + assertEquals(response.getRequestId(), result.getApiRequestId()); + assertEquals(response.getCode(), result.getApiCode()); + assertEquals(response.getMessage(), result.getApiMsg()); + assertEquals(response.getBizId(), result.getSerialNo()); + } + + @Test + public void testParseSmsReceiveStatus() { + // 准备参数 + 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 statuses = smsClient.parseSmsReceiveStatus(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(LocalDateTime.of(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 testGetSmsTemplate() throws Throwable { + // 准备参数 + String apiTemplateId = randomString(); + // mock 方法 + QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> { + o.setCode("OK"); + o.setTemplateStatus(1); // 设置模板通过 + }); + when(client.getAcsResponse(argThat((ArgumentMatcher) acsRequest -> { + assertEquals(apiTemplateId, acsRequest.getTemplateCode()); + return true; + }))).thenReturn(response); + + // 调用 + SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId); + // 断言 + assertEquals(response.getTemplateCode(), result.getId()); + assertEquals(response.getTemplateContent(), result.getContent()); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus()); + assertEquals(response.getReason(), result.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)"); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java new file mode 100644 index 0000000..e93435f --- /dev/null +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java @@ -0,0 +1,230 @@ +package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; + +import cn.hutool.core.util.ReflectUtil; +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.MapUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; +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.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +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("芋道源码"); + + @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("芋道源码"); + // 调用 + smsClient.refresh(p); + // 断言 + assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client")); + } + + @Test + public void testDoSendSms_success() throws Throwable { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString(); + List> 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_CODE_SUCCESS); + 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); + + // 调用 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams); + // 断言 + assertTrue(result.getSuccess()); + assertEquals(response.getRequestId(), result.getApiRequestId()); + assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode()); + assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg()); + assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getSerialNo()); + } + + @Test + public void testDoSendSms_fail() throws Throwable { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString(); + List> 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("ERROR"); + 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); + + // 调用 + SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams); + // 断言 + assertFalse(result.getSuccess()); + assertEquals(response.getRequestId(), result.getApiRequestId()); + assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode()); + assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg()); + assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getSerialNo()); + } + + @Test + public void testParseSmsReceiveStatus() { + // 准备参数 + 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 statuses = smsClient.parseSmsReceiveStatus(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(LocalDateTime.of(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 testGetSmsTemplate() 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); + + // 调用 + SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId.toString()); + // 断言 + assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateId().toString(), result.getId()); + assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateContent(), result.getContent()); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus()); + assertEquals(response.getDescribeTemplateStatusSet()[0].getReviewReply(), result.getAuditReason()); + } + + @Test + public void testConvertSmsTemplateAuditStatus() { + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), + smsClient.convertSmsTemplateAuditStatus(0)); + assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(), + smsClient.convertSmsTemplateAuditStatus(1)); + assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(), + smsClient.convertSmsTemplateAuditStatus(-1)); + assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus(3), + "未知审核状态(3)"); + } + +} diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceTest.java index 02842ea..1cc9152 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsChannelServiceTest.java @@ -3,9 +3,9 @@ package cn.iocoder.yudao.module.system.service.sms; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.framework.sms.core.client.SmsClient; -import cn.iocoder.yudao.framework.sms.core.client.SmsClientFactory; -import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties; +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient; +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClientFactory; +import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; import cn.iocoder.yudao.module.system.controller.admin.sms.vo.channel.SmsChannelSaveReqVO; diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsCodeServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsCodeServiceImplTest.java index 2888dce..ef9d2ff 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsCodeServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsCodeServiceImplTest.java @@ -9,7 +9,7 @@ import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeValidateReqDTO; import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsCodeDO; import cn.iocoder.yudao.module.system.dal.mysql.sms.SmsCodeMapper; import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; -import cn.iocoder.yudao.module.system.framework.sms.SmsCodeProperties; +import cn.iocoder.yudao.module.system.framework.sms.config.SmsCodeProperties; import com.baomidou.mybatisplus.annotation.DbType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImplTest.java index 6e35fe3..487c6f7 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImplTest.java @@ -4,9 +4,9 @@ import cn.hutool.core.map.MapUtil; import cn.iocoder.yudao.framework.common.core.KeyValue; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; -import cn.iocoder.yudao.framework.sms.core.client.SmsClient; -import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO; -import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsChannelDO; import cn.iocoder.yudao.module.system.dal.dataobject.sms.SmsTemplateDO; diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImplTest.java index b4d0c0e..22f737d 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsTemplateServiceImplTest.java @@ -5,9 +5,9 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; -import cn.iocoder.yudao.framework.sms.core.client.SmsClient; -import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO; -import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient; +import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO; +import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO; import cn.iocoder.yudao.module.system.controller.admin.sms.vo.template.SmsTemplateSaveReqVO; -- libgit2 0.21.4