feat:集成移动云mas短信平台

This commit is contained in:
hewencai
2025-11-25 19:08:55 +08:00
parent eab968da72
commit 4003388740
4 changed files with 225 additions and 1 deletions

View File

@@ -0,0 +1,222 @@
package com.zt.plat.module.system.framework.sms.core.client.impl;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.zt.plat.framework.common.core.KeyValue;
import com.zt.plat.framework.common.util.http.HttpUtils;
import com.zt.plat.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
import com.zt.plat.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
import com.zt.plat.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
import com.zt.plat.module.system.framework.sms.core.property.SmsChannelProperties;
import lombok.extern.slf4j.Slf4j;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 中国移动云MAS短信客户端实现类
*
* @author zt-team
* @since 2025-01-19
*/
@Slf4j
public class CmccMasSmsClient extends AbstractSmsClient {
private static final String URL = "https://112.35.10.201:28888/sms/submit";
private static final String RESPONSE_SUCCESS = "success";
public CmccMasSmsClient(SmsChannelProperties properties) {
super(properties);
Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
validateCmccMasConfig(properties);
}
/**
* 参数校验中国移动云MAS的配置
*
* 原因是中国移动云MAS需要三个参数ecName、apId、secretKey
*
* 解决方案:考虑到不破坏原有的 apiKey + apiSecret 的结构,所以将 ecName 和 apId 拼接到 apiKey 字段中,格式为 "ecName apId"。
* secretKey 存储在 apiSecret 字段中。
*
* @param properties 配置
*/
private static void validateCmccMasConfig(SmsChannelProperties properties) {
String combineKey = properties.getApiKey();
Assert.notEmpty(combineKey, "apiKey 不能为空");
String[] keys = combineKey.trim().split(" ");
Assert.isTrue(keys.length == 2, "中国移动云MAS apiKey 配置格式错误,请配置为 [ecName apId]");
}
/**
* 获取 ecName企业名称
*/
private String getEcName() {
return StrUtil.subBefore(properties.getApiKey(), " ", true);
}
/**
* 获取 apId应用ID
*/
private String getApId() {
return StrUtil.subAfter(properties.getApiKey(), " ", true);
}
/**
* 获取 secretKey密钥
*/
private String getSecretKey() {
return properties.getApiSecret();
}
/**
* 发送短信
*
* @param logId 日志ID
* @param mobile 手机号
* @param apiTemplateId 模板ID本平台不使用模板传入内容
* @param templateParams 模板参数
* @return 发送结果
*/
@Override
public SmsSendRespDTO sendSms(Long logId, String mobile, String apiTemplateId,
List<KeyValue<String, Object>> templateParams) throws Throwable {
// 1. 构建短信内容
String content = buildContent(apiTemplateId, templateParams);
// 2. 计算MAC校验值
String mac = calculateMac(mobile, content);
// 3. 构建请求参数
JSONObject requestBody = new JSONObject();
requestBody.set("ecName", getEcName()); // 企业名称
requestBody.set("apId", getApId()); // 应用ID
requestBody.set("secretKey", getSecretKey()); // 密钥
requestBody.set("sign", properties.getSignature()); // 签名编码
requestBody.set("mobiles", mobile);
requestBody.set("content", content);
requestBody.set("addSerial", "");
requestBody.set("mac", mac);
log.info("[sendSms][发送短信 {}]", JSONUtil.toJsonStr(requestBody));
// 4. Base64编码请求体
String encodedBody = Base64.encode(requestBody.toString());
log.info("[sendSms][Base64编码后: {}]", encodedBody);
// 5. 构建请求头需要JWT Token
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + getJwtToken());
headers.put("Content-Type", "text/plain");
// 6. 发起请求
String responseBody = HttpUtils.post(URL, headers, encodedBody);
JSONObject response = JSONUtil.parseObj(responseBody);
log.info("[sendSms][收到响应 - {}]", response);
// 7. 解析响应
return new SmsSendRespDTO()
.setSuccess(response.getBool("success", false))
.setSerialNo(response.getStr("msgGroup"))
.setApiCode(response.getStr("rspcod"))
.setApiMsg(response.getStr("message", "未知错误"));
}
/**
* 解析短信接收状态回调
*
* @param text 回调文本
* @return 接收状态列表
*/
@Override
public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) throws Throwable {
// TODO: 根据移动云MAS回调格式实现
log.warn("[parseSmsReceiveStatus][暂未实现短信状态回调解析]");
return Collections.emptyList();
}
/**
* 查询短信模板
*
* @param apiTemplateId 模板ID
* @return 模板信息
*/
@Override
public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
// 移动云MAS不使用模板机制直接发送内容
log.debug("[getSmsTemplate][中国移动云MAS不支持模板查询]");
return null;
}
/**
* 计算MAC校验值
* 算法MD5(ecName + apId + secretKey + mobiles + content + sign + addSerial)
*
* @param mobile 手机号
* @param content 短信内容
* @return MAC校验值
*/
private String calculateMac(String mobile, String content) {
String rawString = getEcName() // ecName
+ getApId() // apId
+ getSecretKey() // secretKey
+ mobile // mobiles
+ content // content
+ properties.getSignature() // sign
+ ""; // addSerial
String mac = DigestUtil.md5Hex(rawString).toLowerCase();
log.debug("[calculateMac][原始字符串长度: {}, MAC: {}]", rawString.length(), mac);
return mac;
}
/**
* 构建短信内容
*
* @param apiTemplateId 模板ID
* @param templateParams 模板参数
* @return 短信内容
*/
private String buildContent(String apiTemplateId, List<KeyValue<String, Object>> templateParams) {
// 简单实现直接返回模板ID作为内容
// 实际使用时需要根据业务需求构建短信内容
if (templateParams == null || templateParams.isEmpty()) {
return apiTemplateId;
}
// 替换模板参数,支持 {{key}} 格式
String content = apiTemplateId;
for (KeyValue<String, Object> param : templateParams) {
String placeholder = "{{" + param.getKey() + "}}";
String value = String.valueOf(param.getValue());
content = content.replace(placeholder, value);
}
return content;
}
/**
* 获取JWT Token
* TODO: 实现Token获取逻辑可能需要
* 1. 调用认证接口获取Token
* 2. 缓存Token并在过期前自动刷新
* 3. 处理Token失效情况
*
* @return JWT Token
*/
private String getJwtToken() {
// 临时实现:从配置中读取或调用认证接口获取
// 实际生产环境需要实现完整的Token管理机制
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.example.token";
log.warn("[getJwtToken][使用临时Token生产环境需实现完整的Token获取机制]");
return token;
}
}

View File

@@ -81,6 +81,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory {
case TENCENT: return new TencentSmsClient(properties); case TENCENT: return new TencentSmsClient(properties);
case HUAWEI: return new HuaweiSmsClient(properties); case HUAWEI: return new HuaweiSmsClient(properties);
case QINIU: return new QiniuSmsClient(properties); case QINIU: return new QiniuSmsClient(properties);
case CMCC_MAS: return new CmccMasSmsClient(properties);
} }
// 创建失败,错误日志 + 抛出异常 // 创建失败,错误日志 + 抛出异常
log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties); log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties);

View File

@@ -19,6 +19,7 @@ public enum SmsChannelEnum {
TENCENT("TENCENT", "腾讯云"), TENCENT("TENCENT", "腾讯云"),
HUAWEI("HUAWEI", "华为云"), HUAWEI("HUAWEI", "华为云"),
QINIU("QINIU", "七牛云"), QINIU("QINIU", "七牛云"),
CMCC_MAS("CMCC_MAS", "中国移动云MAS"),
; ;
/** /**

View File

@@ -94,6 +94,7 @@ public class SmsSendServiceImpl implements SmsSendService {
Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus()) Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus())
&& CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus()); && CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus());
String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams); String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams);
Long sendLogId = smsLogService.createSmsLog(mobile, userId, userType, isSend, template, content, templateParams); Long sendLogId = smsLogService.createSmsLog(mobile, userId, userType, isSend, template, content, templateParams);
// 发送 MQ 消息,异步执行发送短信 // 发送 MQ 消息,异步执行发送短信
@@ -183,7 +184,6 @@ public class SmsSendServiceImpl implements SmsSendService {
if (CollUtil.isEmpty(receiveResults)) { if (CollUtil.isEmpty(receiveResults)) {
return; return;
} }
// 更新短信日志的接收结果. 因为量一般不大,所以先使用 for 循环更新
receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(result.getLogId(), receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(result.getLogId(),
result.getSuccess(), result.getReceiveTime(), result.getErrorCode(), result.getErrorMsg())); result.getSuccess(), result.getReceiveTime(), result.getErrorCode(), result.getErrorMsg()));
} }