Merge branch 'dev' into test
This commit is contained in:
@@ -4,8 +4,8 @@ spring:
|
|||||||
cloud:
|
cloud:
|
||||||
nacos:
|
nacos:
|
||||||
server-addr: 172.16.46.63:30848 # Nacos 服务器地址
|
server-addr: 172.16.46.63:30848 # Nacos 服务器地址
|
||||||
username: # Nacos 账号
|
username: ${config.username} # Nacos 账号
|
||||||
password: # Nacos 密码
|
password: ${config.password} # Nacos 密码
|
||||||
discovery: # 【配置中心】配置项
|
discovery: # 【配置中心】配置项
|
||||||
namespace: ${config.namespace} # 命名空间。这里使用 maven Profile 资源过滤进行动态替换
|
namespace: ${config.namespace} # 命名空间。这里使用 maven Profile 资源过滤进行动态替换
|
||||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public enum SmsChannelEnum {
|
|||||||
TENCENT("TENCENT", "腾讯云"),
|
TENCENT("TENCENT", "腾讯云"),
|
||||||
HUAWEI("HUAWEI", "华为云"),
|
HUAWEI("HUAWEI", "华为云"),
|
||||||
QINIU("QINIU", "七牛云"),
|
QINIU("QINIU", "七牛云"),
|
||||||
|
CMCC_MAS("CMCC_MAS", "中国移动云MAS"),
|
||||||
;
|
;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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()));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user