目前系统集成短信似乎是必不可少的部分,由于各种云平台都提供了不同的短信通道,这里我们增加多租户多通道的短信验证码,并增加配置项,使系统可以支持多家云平台提供的短信服务。这里以阿里云和腾讯云为例,集成短信通知服务。

1、在GitEgg-Platform中新建gitegg-platform-sms基础工程,定义抽象方法和配置类

SmsSendService发送短信抽象接口:

/**
* 短信发送接口
*/
public interface SmsSendService { /**
* 发送单个短信
* @param smsData
* @param phoneNumber
* @return
*/
default SmsResponse sendSms(SmsData smsData, String phoneNumber){
if (StrUtil.isEmpty(phoneNumber)) {
return new SmsResponse();
}
return this.sendSms(smsData, Collections.singletonList(phoneNumber));
} /**
* 群发发送短信
* @param smsData
* @param phoneNumbers
* @return
*/
SmsResponse sendSms(SmsData smsData, Collection<string> phoneNumbers); }

SmsResultCodeEnum定义短信发送结果

/**
* @ClassName: ResultCodeEnum
* @Description: 自定义返回码枚举
* @author GitEgg
* @date 2020年09月19日 下午11:49:45
*/
@Getter
@AllArgsConstructor
public enum SmsResultCodeEnum { /**
* 成功
*/
SUCCESS(200, "操作成功"), /**
* 系统繁忙,请稍后重试
*/
ERROR(429, "短信发送失败,请稍后重试"), /**
* 系统错误
*/
PHONE_NUMBER_ERROR(500, "手机号错误"); public int code; public String msg;
}

2、新建gitegg-platform-sms-aliyun工程,实现阿里云短信发送接口

AliyunSmsProperties配置类

@Data
@Component
@ConfigurationProperties(prefix = "sms.aliyun")
public class AliyunSmsProperties { /**
* product
*/
private String product = "Dysmsapi"; /**
* domain
*/
private String domain = "dysmsapi.aliyuncs.com"; /**
* regionId
*/
private String regionId = "cn-hangzhou"; /**
* accessKeyId
*/
private String accessKeyId; /**
* accessKeySecret
*/
private String accessKeySecret; /**
* 短信签名
*/
private String signName;
}

AliyunSmsSendServiceImpl阿里云短信发送接口实现类

/**
* 阿里云短信发送
*/
@Slf4j
@AllArgsConstructor
public class AliyunSmsSendServiceImpl implements SmsSendService { private static final String successCode = "OK"; private final AliyunSmsProperties properties; private final IAcsClient acsClient; @Override
public SmsResponse sendSms(SmsData smsData, Collection<string> phoneNumbers) {
SmsResponse smsResponse = new SmsResponse();
SendSmsRequest request = new SendSmsRequest();
request.setSysMethod(MethodType.POST);
request.setPhoneNumbers(StrUtil.join(",", phoneNumbers));
request.setSignName(properties.getSignName());
request.setTemplateCode(smsData.getTemplateId());
request.setTemplateParam(JsonUtils.mapToJson(smsData.getParams()));
try {
SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
if (null != sendSmsResponse && !StringUtils.isEmpty(sendSmsResponse.getCode())) {
if (this.successCode.equals(sendSmsResponse.getCode())) {
smsResponse.setSuccess(true);
} else {
log.error("Send Aliyun Sms Fail: [code={}, message={}]", sendSmsResponse.getCode(), sendSmsResponse.getMessage());
}
smsResponse.setCode(sendSmsResponse.getCode());
smsResponse.setMessage(sendSmsResponse.getMessage());
}
} catch (Exception e) {
e.printStackTrace();
log.error("Send Aliyun Sms Fail: {}", e);
smsResponse.setMessage("Send Aliyun Sms Fail!");
}
return smsResponse;
} }

3、新建gitegg-platform-sms-tencent工程,实现腾讯云短信发送接口

TencentSmsProperties配置类

@Data
@Component
@ConfigurationProperties(prefix = "sms.tencent")
public class TencentSmsProperties { /* 填充请求参数,这里 request 对象的成员变量即对应接口的入参
* 您可以通过官网接口文档或跳转到 request 对象的定义处查看请求参数的定义
* 基本类型的设置:
* 帮助链接:
* 短信控制台:https://console.cloud.tencent.com/smsv2
* sms helper:https://cloud.tencent.com/document/product/382/3773 */
/* 短信应用 ID: 在 [短信控制台] 添加应用后生成的实际 SDKAppID,例如1400006666 */
private String SmsSdkAppId; /* 国际/港澳台短信 senderid: 国内短信填空,默认未开通,如需开通请联系 [sms helper] */
private String senderId; /* 短信码号扩展号: 默认未开通,如需开通请联系 [sms helper] */
private String extendCode; /**
* 短信签名
*/
private String signName;
}

TencentSmsSendServiceImpl腾讯云短信发送接口实现类

/**
* 腾讯云短信发送
*/
@Slf4j
@AllArgsConstructor
public class TencentSmsSendServiceImpl implements SmsSendService { private static final String successCode = "Ok"; private final TencentSmsProperties properties; private final SmsClient client; @Override
public SmsResponse sendSms(SmsData smsData, Collection<string> phoneNumbers) {
SmsResponse smsResponse = new SmsResponse(); SendSmsRequest request = new SendSmsRequest();
request.setSmsSdkAppid(properties.getSmsSdkAppId());
/* 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名,可登录 [短信控制台] 查看签名信息 */
request.setSign(properties.getSignName());
/* 国际/港澳台短信 senderid: 国内短信填空,默认未开通,如需开通请联系 [sms helper] */
if (!StringUtils.isEmpty(properties.getSenderId()))
{
request.setSenderId(properties.getSenderId());
}
request.setTemplateID(smsData.getTemplateId());
/* 下发手机号码,采用 e.164 标准,+[国家或地区码][手机号]
* 例如+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号*/
String[] phoneNumbersArray = (String[]) phoneNumbers.toArray();
request.setPhoneNumberSet(phoneNumbersArray);
/* 模板参数: 若无模板参数,则设置为空*/
String[] templateParams = new String[]{};
if (!CollectionUtils.isEmpty(smsData.getParams())) {
templateParams = (String[]) smsData.getParams().values().toArray();
}
request.setTemplateParamSet(templateParams);
try {
/* 通过 client 对象调用 SendSms 方法发起请求。注意请求方法名与请求对象是对应的
* 返回的 res 是一个 SendSmsResponse 类的实例,与请求对象对应 */
SendSmsResponse sendSmsResponse = client.SendSms(request);
//如果是批量发送,那么腾讯云短信会返回每条短信的发送状态,这里默认返回第一条短信的状态
if (null != sendSmsResponse && null != sendSmsResponse.getSendStatusSet()) {
SendStatus sendStatus = sendSmsResponse.getSendStatusSet()[0];
if (this.successCode.equals(sendStatus.getCode()))
{
smsResponse.setSuccess(true);
}
else
{
smsResponse.setCode(sendStatus.getCode());
smsResponse.setMessage(sendStatus.getMessage());
}
}
} catch (Exception e) {
e.printStackTrace();
log.error("Send Aliyun Sms Fail: {}", e);
smsResponse.setMessage("Send Aliyun Sms Fail!");
}
return smsResponse;
}
}

4、在GitEgg-Cloud中新建业务调用方法,这里要考虑到不同租户调用不同的短信配置进行短信发送,所以新建SmsFactory短信接口实例化工厂,根据不同的租户实例化不同的短信发送接口,这里以实例化com.gitegg.service.extension.sms.factory.SmsAliyunFactory类为例,进行实例化操作,实际使用中,这里需要配置和租户的对应关系,从租户的短信配置中获取。

@Component
public class SmsFactory { private final ISmsTemplateService smsTemplateService; /**
* SmsSendService 缓存
*/
private final Map<long, smssendservice=""> SmsSendServiceMap = new ConcurrentHashMap<>(); public SmsFactory(ISmsTemplateService smsTemplateService) {
this.smsTemplateService = smsTemplateService;
} /**
* 获取 SmsSendService
*
* @param smsTemplateDTO 短信模板
* @return SmsSendService
*/
public SmsSendService getSmsSendService(SmsTemplateDTO smsTemplateDTO) { //根据channelId获取对应的发送短信服务接口,channelId是唯一的,每个租户有其自有的channelId
Long channelId = smsTemplateDTO.getChannelId();
SmsSendService smsSendService = SmsSendServiceMap.get(channelId);
if (null == smsSendService) {
Class cls = null;
try {
cls = Class.forName("com.gitegg.service.extension.sms.factory.SmsAliyunFactory");
Method staticMethod = cls.getDeclaredMethod("getSmsSendService", SmsTemplateDTO.class);
smsSendService = (SmsSendService) staticMethod.invoke(cls,smsTemplateDTO);
SmsSendServiceMap.put(channelId, smsSendService);
} catch (ClassNotFoundException | NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} }
return smsSendService;
}
}
/**
* 阿里云短信服务接口工厂类
*/
public class SmsAliyunFactory { public static SmsSendService getSmsSendService(SmsTemplateDTO sms) {
AliyunSmsProperties aliyunSmsProperties = new AliyunSmsProperties();
aliyunSmsProperties.setAccessKeyId(sms.getSecretId());
aliyunSmsProperties.setAccessKeySecret(sms.getSecretKey());
aliyunSmsProperties.setRegionId(sms.getRegionId());
aliyunSmsProperties.setSignName(sms.getSignName());
IClientProfile profile = DefaultProfile.getProfile(aliyunSmsProperties.getRegionId(), aliyunSmsProperties.getAccessKeyId(), aliyunSmsProperties.getAccessKeySecret());
IAcsClient acsClient = new DefaultAcsClient(profile);
return new AliyunSmsSendServiceImpl(aliyunSmsProperties, acsClient);
} }
/**
* 腾讯云短信服务接口工厂类
*/
public class SmsTencentFactory { public static SmsSendService getSmsSendService(SmsTemplateDTO sms) { TencentSmsProperties tencentSmsProperties = new TencentSmsProperties();
tencentSmsProperties.setSmsSdkAppId(sms.getSecretId());
tencentSmsProperties.setExtendCode(sms.getSecretKey());
tencentSmsProperties.setSenderId(sms.getRegionId());
tencentSmsProperties.setSignName(sms.getSignName()); /* 必要步骤:
* 实例化一个认证对象,入参需要传入腾讯云账户密钥对 secretId 和 secretKey
* 本示例采用从环境变量读取的方式,需要预先在环境变量中设置这两个值
* 您也可以直接在代码中写入密钥对,但需谨防泄露,不要将代码复制、上传或者分享给他人
* CAM 密钥查询:https://console.cloud.tencent.com/cam/capi
*/
Credential cred = new Credential(sms.getSecretId(), sms.getSecretKey());
// 实例化一个 http 选项,可选,无特殊需求时可以跳过
HttpProfile httpProfile = new HttpProfile();
// 设置代理
// httpProfile.setProxyHost("host");
// httpProfile.setProxyPort(port);
/* SDK 默认使用 POST 方法。
* 如需使用 GET 方法,可以在此处设置,但 GET 方法无法处理较大的请求 */
httpProfile.setReqMethod("POST");
/* SDK 有默认的超时时间,非必要请不要进行调整
* 如有需要请在代码中查阅以获取最新的默认值 */
httpProfile.setConnTimeout(60);
/* SDK 会自动指定域名,通常无需指定域名,但访问金融区的服务时必须手动指定域名
* 例如 SMS 的上海金融区域名为 sms.ap-shanghai-fsi.tencentcloudapi.com */
if (!StringUtils.isEmpty(sms.getRegionId()))
{
httpProfile.setEndpoint(sms.getRegionId());
} /* 非必要步骤:
* 实例化一个客户端配置对象,可以指定超时时间等配置 */
ClientProfile clientProfile = new ClientProfile();
/* SDK 默认用 TC3-HMAC-SHA256 进行签名
* 非必要请不要修改该字段 */
clientProfile.setSignMethod("HmacSHA256");
clientProfile.setHttpProfile(httpProfile);
/* 实例化 SMS 的 client 对象
* 第二个参数是地域信息,可以直接填写字符串 ap-guangzhou,或者引用预设的常量 */
SmsClient client = new SmsClient(cred, "",clientProfile); return new TencentSmsSendServiceImpl(tencentSmsProperties, client);
}
}

5、定义短信发送接口及实现类

ISmsService业务短信发送接口定义

/**
* <p>
* 短信发送接口定义
* </p>
*
* @author GitEgg
* @since 2021-01-25
*/
public interface ISmsService { /**
* 发送短信
*
* @param smsCode
* @param smsData
* @param phoneNumbers
* @return
*/
SmsResponse sendSmsNormal(String smsCode, String smsData, String phoneNumbers); /**
* 发送短信验证码
*
* @param smsCode
* @param phoneNumber
* @return
*/
SmsResponse sendSmsVerificationCode( String smsCode, String phoneNumber); /**
* 校验短信验证码
*
* @param smsCode
* @param phoneNumber
* @return
*/
boolean checkSmsVerificationCode(String smsCode, String phoneNumber, String verificationCode); }

SmsServiceImpl 短信发送接口实现类

/**
* <p>
* 短信发送接口实现类
* </p>
*
* @author GitEgg
* @since 2021-01-25
*/
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class SmsServiceImpl implements ISmsService { private final SmsFactory smsFactory; private final ISmsTemplateService smsTemplateService; private final RedisTemplate redisTemplate; @Override
public SmsResponse sendSmsNormal(String smsCode, String smsData, String phoneNumbers) {
SmsResponse smsResponse = new SmsResponse();
try {
QuerySmsTemplateDTO querySmsTemplateDTO = new QuerySmsTemplateDTO();
querySmsTemplateDTO.setSmsCode(smsCode);
//获取短信code的相关信息,租户信息会根据mybatis plus插件获取
SmsTemplateDTO smsTemplateDTO = smsTemplateService.querySmsTemplate(querySmsTemplateDTO);
ObjectMapper mapper = new ObjectMapper();
Map smsDataMap = mapper.readValue(smsData, Map.class); List<string> phoneNumberList = JsonUtils.jsonToList(phoneNumbers, String.class);
SmsData smsDataParam = new SmsData();
smsDataParam.setTemplateId(smsTemplateDTO.getTemplateId());
smsDataParam.setParams(smsDataMap);
SmsSendService smsSendService = smsFactory.getSmsSendService(smsTemplateDTO);
smsResponse = smsSendService.sendSms(smsDataParam, phoneNumberList);
} catch (Exception e) {
smsResponse.setMessage("短信发送失败");
e.printStackTrace();
}
return smsResponse;
} @Override
public SmsResponse sendSmsVerificationCode(String smsCode, String phoneNumber) {
String verificationCode = RandomUtil.randomNumbers(6);
Map<string, string=""> smsDataMap = new HashMap<>();
smsDataMap.put(SmsConstant.SMS_CAPTCHA_TEMPLATE_CODE, verificationCode);
List<string> phoneNumbers = Arrays.asList(phoneNumber);
SmsResponse smsResponse = this.sendSmsNormal(smsCode, JsonUtils.mapToJson(smsDataMap), JsonUtils.listToJson(phoneNumbers));
if (null != smsResponse && smsResponse.isSuccess()) {
// 将短信验证码存入redis并设置过期时间为5分钟
redisTemplate.opsForValue().set(SmsConstant.SMS_CAPTCHA_KEY + smsCode + phoneNumber, verificationCode, 30,
TimeUnit.MINUTES);
}
return smsResponse;
} @Override
public boolean checkSmsVerificationCode(String smsCode, String phoneNumber, String verificationCode) {
String verificationCodeRedis = (String) redisTemplate.opsForValue().get(SmsConstant.SMS_CAPTCHA_KEY + smsCode + phoneNumber);
if (!StrUtil.isAllEmpty(verificationCodeRedis, verificationCode) && verificationCode.equalsIgnoreCase(verificationCodeRedis)) {
return true;
}
return false;
}
}

6、新建SmsFeign类,供其他微服务调用发送短信

/**
* @ClassName: SmsFeign
* @Description: SmsFeign前端控制器
* @author gitegg
* @date 2019年5月18日 下午4:03:58
*/
@RestController
@RequestMapping(value = "/feign/sms")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
@Api(value = "SmsFeign|提供微服务调用接口")
@RefreshScope
public class SmsFeign { private final ISmsService smsService; @GetMapping(value = "/send/normal")
@ApiOperation(value = "发送普通短信", notes = "发送普通短信")
Result<object> sendSmsNormal(@RequestParam("smsCode") String smsCode, @RequestParam("smsData") String smsData, @RequestParam("phoneNumbers") String phoneNumbers) {
SmsResponse smsResponse = smsService.sendSmsNormal(smsCode, smsData, phoneNumbers);
return Result.data(smsResponse);
} @GetMapping(value = "/send/verification/code")
@ApiOperation(value = "发送短信验证码", notes = "发送短信验证码")
Result<object> sendSmsVerificationCode(@RequestParam("smsCode") String smsCode, @RequestParam("phoneNumber") String phoneNumber) {
SmsResponse smsResponse = smsService.sendSmsVerificationCode(smsCode, phoneNumber);
return Result.data(smsResponse);
} @GetMapping(value = "/check/verification/code")
@ApiOperation(value = "校验短信验证码", notes = "校验短信验证码")
Result<boolean> checkSmsVerificationCode(@RequestParam("smsCode") String smsCode, @RequestParam("phoneNumber") String phoneNumber, @RequestParam("verificationCode") String verificationCode) {
boolean checkResult = smsService.checkSmsVerificationCode(smsCode, phoneNumber, verificationCode);
return Result.data(checkResult);
}
}
项目源码:

Gitee: https://gitee.com/wmz1930/GitEgg

GitHub: https://github.com/wmz1930/GitEgg

SpringCloud微服务实战——搭建企业级开发框架(二十五):实现多租户多平台短信通知服务的更多相关文章

  1. SpringCloud微服务实战——搭建企业级开发框架(十五):集成Sentinel高可用流量管理框架【熔断降级】

      Sentinel除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一.由于调用关系的复杂性,如果调用链路中的某个资源不稳定,最终会导致请求发生堆积.Sentinel ...

  2. SpringCloud微服务实战——搭建企业级开发框架(十二):OpenFeign+Ribbon实现负载均衡

      Ribbon是Netflix下的负载均衡项目,它主要实现中间层应用程序的负载均衡.为Ribbon配置服务提供者地址列表后,Ribbon就会基于某种负载均衡算法,自动帮助服务调用者去请求.Ribbo ...

  3. SpringCloud微服务实战——搭建企业级开发框架(十):使用Nacos分布式配置中心

    随着业务的发展.微服务架构的升级,服务的数量.程序的配置日益增多(各种微服务.各种服务器地址.各种参数),传统的配置文件方式和数据库的方式已无法满足开发人员对配置管理的要求: 安全性:配置跟随源代码保 ...

  4. SpringCloud微服务实战——搭建企业级开发框架(十四):集成Sentinel高可用流量管理框架【限流】

      Sentinel 是面向分布式服务架构的高可用流量防护组件,主要以流量为切入点,从限流.流量整形.熔断降级.系统负载保护.热点防护等多个维度来帮助开发者保障微服务的稳定性. Sentinel 具有 ...

  5. SpringCloud微服务实战——搭建企业级开发框架(十六):集成Sentinel高可用流量管理框架【自定义返回消息】

    Sentinel限流之后,默认的响应消息为Blocked by Sentinel (flow limiting),对于系统整体功能提示来说并不统一,参考我们前面设置的统一响应及异常处理方式,返回相同的 ...

  6. SpringCloud微服务实战——搭建企业级开发框架(十九):Gateway使用knife4j聚合微服务文档

      本章介绍Spring Cloud Gateway网关如何集成knife4j,通过网关聚合所有的Swagger微服务文档 1.gitegg-gateway中引入knife4j依赖,如果没有后端代码编 ...

  7. SpringCloud微服务实战——搭建企业级开发框架(四十二):集成分布式任务调度平台XXL-JOB,实现定时任务功能

      定时任务几乎是每个业务系统必不可少的功能,计算到期时间.过期时间等,定时触发某项任务操作.在使用单体应用时,基本使用Spring提供的注解即可实现定时任务,而在使用微服务集群时,这种方式就要考虑添 ...

  8. SpringCloud微服务实战——搭建企业级开发框架(四十五):【微服务监控告警实现方式二】使用Actuator(Micrometer)+Prometheus+Grafana实现完整的微服务监控

      无论是使用SpringBootAdmin还是使用Prometheus+Grafana都离不开SpringBoot提供的核心组件Actuator.提到Actuator,又不得不提Micrometer ...

  9. SpringCloud微服务实战——搭建企业级开发框架(二):环境准备

    这里简单说明一下在Windows系统下开发SpringCloud项目所需要的的基本环境,这里只说明开发过程中基础必须的软件,其他扩展功能(Docker,k8s,MinIO,XXL-JOB,EKL,Ke ...

  10. SpringCloud微服务实战——搭建企业级开发框架(二十三):Gateway+OAuth2+JWT实现微服务统一认证授权

      OAuth2是一个关于授权的开放标准,核心思路是通过各类认证手段(具体什么手段OAuth2不关心)认证用户身份,并颁发token(令牌),使得第三方应用可以使用该token(令牌)在限定时间.限定 ...

随机推荐

  1. nginx搭建网站踩坑经历

    为了更好的阅读体验,请访问我的个人博客 前言 早上刷抖音刷到一个只需要三步的nginx搭建教程(视频地址),觉得有些离谱,跟着复现了一遍,果然很多地方不严谨并且省略了大量步骤,对于很多不了解linux ...

  2. Linux Manual

    man 命令用来访问存储在Linux系统上的手册页面.在想要查找的工具的名称前面输入man命 令,就可以找到那个工具相应的手册条目. 手册页不是唯一的参考资料.还有另一种叫作 info 页面的信息.可 ...

  3. 从零到熟悉,带你掌握Python len() 函数的使用

    摘要:本文为你带来如何找到长度内置数据类型的使用len() 使用len()与第三方数据类型 提供用于支持len()与用户定义的类. 本文分享自华为云社区<在 Python 中使用 len() 函 ...

  4. javascript-jquery对象的其他处理

    一.对元素进行遍历操作 如果要遍历一个jquery对象,对其中每个匹配元素进行相应处理,那么可以使用each()方法. $("div").each(function(index,e ...

  5. 第1次 Beta Scrum Meeting

    本次会议为Beta阶段第1次Scrum Meeting会议 会议概要 会议时间:2021年5月29日 会议地点:「腾讯会议」线上进行 会议时长:0.5小时 会议内容简介:本次会议为Beta阶段第1次会 ...

  6. java中延时队列的使用

    最近遇到这么一个需求,程序中有一个功能需要发送短信,当满足某些条件后,如果上一步的短信还没有发送出去,那么应该取消这个短信的发送.在翻阅java的api后,发现java中有一个延时队列可以解决这个问题 ...

  7. GT考试

    比较神仙的$dp+KMP+Matrix$综合题目,比较值得一写 $0x00$:首先我打了一个爆搜 不过对正解并无任何启发...(逗比发言请忽略) $0x01$:基础$dp$ 状态还是比较好设的, 考虑 ...

  8. 《手把手教你》系列技巧篇(三十六)-java+ selenium自动化测试-单选和多选按钮操作-番外篇(详解教程)

    1.简介 前边几篇文章是宏哥自己在本地弄了一个单选和多选的demo,然后又找了网上相关联的例子给小伙伴或童鞋们演示了一下如何自动化测试,这一篇宏哥在网上找了一个问卷调查,给小伙伴或童鞋们来演示一下.上 ...

  9. 翻转子串 牛客网 程序员面试金典 C++ Python

    反转子串 牛客网 程序员面试金典 C++ Python 题目描述 假定我们都知道非常高效的算法来检查一个单词是否为其他字符串的子串.请将这个算法编写成一个函数,给定两个字符串s1和s2,请编写代码检查 ...

  10. cf20B Equation(认真仔细题)

    题意: 求AX^2+BX+C=0的根 思路: 考虑到A,B,C所有可能的情况 代码: double a,b,c; int main(){ cin>>a>>b>>c; ...