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

目前系统集成短信似乎是必不可少的部分,由于各种云平台都提供了不同的短信通道,这里我们增加多租户多通道的短信验证码,并增加配置项,使系统可以支持多家云平台提供的短信服务。这里以阿里云和腾讯云为例,集成短信通知服务。
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 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 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 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 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业务短信发送接口定义
/** * * 短信发送接口定义 *
* * @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 短信发送接口实现类
/** * 【SpringCloud微服务实战——搭建企业级开发框架(二十五)(实现多租户多平台短信通知服务)】 * 短信发送接口实现类 *
* * @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 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 smsDataMap = new HashMap<>(); smsDataMap.put(SmsConstant.SMS_CAPTCHA_TEMPLATE_CODE, verificationCode); List 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 = "https://www.it610.com/feign/sms") @RequiredArgsConstructor(onConstructor_ = @Autowired) @Api(value = "https://www.it610.com/article/SmsFeign|提供微服务调用接口") @RefreshScope public class SmsFeign {private final ISmsService smsService; @GetMapping(value = "https://www.it610.com/send/normal") @ApiOperation(value = "https://www.it610.com/article/发送普通短信", notes = "发送普通短信") Result 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 = "https://www.it610.com/send/verification/code") @ApiOperation(value = "https://www.it610.com/article/发送短信验证码", notes = "发送短信验证码") Result sendSmsVerificationCode(@RequestParam("smsCode") String smsCode, @RequestParam("phoneNumber") String phoneNumber) { SmsResponse smsResponse = smsService.sendSmsVerificationCode(smsCode, phoneNumber); return Result.data(smsResponse); }@GetMapping(value = "https://www.it610.com/check/verification/code") @ApiOperation(value = "https://www.it610.com/article/校验短信验证码", notes = "校验短信验证码") Result 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

    推荐阅读