From 40033887400c7120c9a79a0482403d02dee8eaeb Mon Sep 17 00:00:00 2001 From: hewencai <2357300448@qq.com> Date: Tue, 25 Nov 2025 19:08:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E9=9B=86=E6=88=90=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E4=BA=91mas=E7=9F=AD=E4=BF=A1=E5=B9=B3=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/client/impl/CmccMasSmsClient.java | 222 ++++++++++++++++++ .../client/impl/SmsClientFactoryImpl.java | 1 + .../sms/core/enums/SmsChannelEnum.java | 1 + .../service/sms/SmsSendServiceImpl.java | 2 +- 4 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/CmccMasSmsClient.java diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/CmccMasSmsClient.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/CmccMasSmsClient.java new file mode 100644 index 00000000..1821ca16 --- /dev/null +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/CmccMasSmsClient.java @@ -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> 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 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 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> templateParams) { + // 简单实现:直接返回模板ID作为内容 + // 实际使用时需要根据业务需求构建短信内容 + if (templateParams == null || templateParams.isEmpty()) { + return apiTemplateId; + } + + // 替换模板参数,支持 {{key}} 格式 + String content = apiTemplateId; + for (KeyValue 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; + } +} diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java index 2ff5951a..f59fc53b 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/client/impl/SmsClientFactoryImpl.java @@ -81,6 +81,7 @@ public class SmsClientFactoryImpl implements SmsClientFactory { case TENCENT: return new TencentSmsClient(properties); case HUAWEI: return new HuaweiSmsClient(properties); case QINIU: return new QiniuSmsClient(properties); + case CMCC_MAS: return new CmccMasSmsClient(properties); } // 创建失败,错误日志 + 抛出异常 log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties); diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/enums/SmsChannelEnum.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/enums/SmsChannelEnum.java index 6031396a..77ea96da 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/enums/SmsChannelEnum.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/framework/sms/core/enums/SmsChannelEnum.java @@ -19,6 +19,7 @@ public enum SmsChannelEnum { TENCENT("TENCENT", "腾讯云"), HUAWEI("HUAWEI", "华为云"), QINIU("QINIU", "七牛云"), + CMCC_MAS("CMCC_MAS", "中国移动云MAS"), ; /** diff --git a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsSendServiceImpl.java b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsSendServiceImpl.java index 678c146b..66ab0b7c 100644 --- a/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsSendServiceImpl.java +++ b/zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/sms/SmsSendServiceImpl.java @@ -94,6 +94,7 @@ public class SmsSendServiceImpl implements SmsSendService { Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus()) && CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus()); String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams); + Long sendLogId = smsLogService.createSmsLog(mobile, userId, userType, isSend, template, content, templateParams); // 发送 MQ 消息,异步执行发送短信 @@ -183,7 +184,6 @@ public class SmsSendServiceImpl implements SmsSendService { if (CollUtil.isEmpty(receiveResults)) { return; } - // 更新短信日志的接收结果. 因为量一般不大,所以先使用 for 循环更新 receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(result.getLogId(), result.getSuccess(), result.getReceiveTime(), result.getErrorCode(), result.getErrorMsg())); }