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

作者:全栈程序猿 时间:2022-12-04 13:22:53 

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

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

来源:https://www.cnblogs.com/FullStackProgrammer/p/15623875.html

标签:SpringCloud,短信通知服务,微服务
0
投稿

猜你喜欢

  • Java中ThreadLocal避免内存泄漏的方法详解

    2023-04-02 12:51:42
  • Android编程实现获取当前连接wifi名字的方法

    2023-11-24 15:41:50
  • 对dbunit进行mybatis DAO层Excel单元测试(必看篇)

    2023-08-19 02:37:19
  • java pdf加水印的方法

    2022-09-03 06:06:50
  • Android 顶部标题栏随滑动时的渐变隐藏和渐变显示效果

    2023-11-26 07:48:39
  • 简单分析Java的求值策略原理

    2022-03-22 19:14:00
  • Spring boot @RequestBody数据传递过程详解

    2022-09-14 12:23:37
  • Flutter 状态管理的实现

    2023-08-21 02:38:33
  • C#基于QRCode实现动态生成自定义二维码图片功能示例

    2023-04-03 04:08:43
  • C#将DataTable转换成list的方法

    2023-03-06 01:20:45
  • java guava主要功能介绍及使用心得总结

    2023-07-30 11:01:54
  • android studio xml文件实现添加注释

    2022-10-11 14:24:26
  • SpringBoot整合screw实现数据库文档自动生成的示例代码

    2023-11-29 05:30:15
  • 详解Java中的JDK、JRE、JVM

    2023-07-10 04:38:50
  • 使用TypeScript开发微信小程序的方法

    2023-08-30 10:42:03
  • Winform之TextBox输入日期格式验证yyyy-mm-dd

    2023-04-14 21:35:05
  • java中的枚举类型详细介绍

    2023-06-26 22:31:26
  • springboot返回图片流的实现示例

    2023-11-23 17:30:08
  • C# Winform按钮中图片实现左图右字的效果实例

    2022-05-01 12:51:41
  • springboot实现通过路径从磁盘直接读取图片

    2023-09-01 03:01:01
  • asp之家 软件编程 m.aspxhome.com