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

网友投稿 638 2022-11-18

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

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 phoneaGeutwXWqkNumbers);

}

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 (thaGeutwXWqkis.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-Chttp://loud中新建业务调用方法,这里要考虑到不同租户调用不同的短信配置进行短信发送,所以新建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 短信发送接口实现类

/**

*

* 短信发送接口实现类

*

*

* @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 = "/feign/sms")

@RequiredArgsConstructor(onConstructor_ = @Autowired)

@Api(value = "SmsFeign|提供微服务调用接口")

@RefreshScope

public class SmsFeign {

private final ISmsService smsService;

@GetMapping(value = "/send/normal")

@ApiOperation(value = "发送普通短信", 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 = "/send/verification/code")

@ApiOperation(value = "发送短信验证码", notes = "发送短信验证码")

Result 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 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

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:MuWu的卡牌游戏 | 前缀后缀
下一篇:畅通工程再续
相关文章

 发表评论

暂时没有评论,来抢沙发吧~