目前系統整合簡訊似乎是必不可少的部分,由於各種雲平臺都提供了不同的簡訊通道,這裡我們增加多租戶多通道的簡訊驗證碼,並增加配置項,使系統可以支援多家雲平臺提供的簡訊服務。這裡以阿里雲和騰訊云為例,整合簡訊通知服務。
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);
}
}