@@ -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 ;
}
}