1、结构化调整;

This commit is contained in:
2026-03-31 19:58:48 +08:00
parent ebdbdbeb41
commit e78565ea5a
369 changed files with 3790 additions and 1195 deletions

View File

@@ -0,0 +1,27 @@
package com.njcn.msgpush.module.push.client.factory;
import cn.hutool.core.util.StrUtil;
/**
* 工厂侧通用辅助方法。
*/
public final class ProviderFactorySupport {
private ProviderFactorySupport() {
}
/**
* 只要任一配置值为空白,就认为当前渠道配置不完整。
*/
public static boolean hasBlank(String... values) {
if (values == null || values.length == 0) {
return true;
}
for (String value : values) {
if (StrUtil.isBlank(value)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,21 @@
package com.njcn.msgpush.module.push.client.sender;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 发送结果大类。
* 这里只描述“主流程该怎么走”,不承载第三方原始错误细节。
*/
@Getter
@AllArgsConstructor
public enum SendOutcome {
SUCCESS("发送成功"),
ACCEPTED("第三方已受理"),
FAILED("发送失败"),
RETRYABLE_FAILED("发送失败,可重试"),
UNSUPPORTED_CHANNEL("当前服务商不支持该发送渠道"),
CONFIG_INVALID("当前服务商渠道配置不完整");
private final String desc;
}

View File

@@ -0,0 +1,109 @@
package com.njcn.msgpush.module.push.client.sender;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 统一发送结果。
* outcome 负责主流程分支判断;
* errorCode/message 负责平台内部归类和中文展示;
* retryable 负责决定是否进入重试队列。
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SendResult {
/**
* 结果分类,供主流程分支判断使用。
*/
private SendOutcome outcome;
/**
* 平台统一错误码。
*/
private String errorCode;
/**
* 中文错误说明。
*/
private String message;
/**
* 服务商原始错误码。
*/
private String providerRawCode;
/**
* 是否允许进入重试队列。
*/
private boolean retryable;
/**
* 第三方消息 ID。
*/
private String thirdPartyId;
/**
* 发送时间。
*/
private LocalDateTime sendTime;
/**
* 耗时,单位毫秒。
*/
private Integer costTime;
/**
* 第三方已明确返回成功。
*/
public static SendResult success(LocalDateTime sendTime, Integer costTime, String thirdPartyId) {
return SendResult.builder()
.outcome(SendOutcome.SUCCESS)
.sendTime(sendTime)
.costTime(costTime)
.thirdPartyId(thirdPartyId)
.build();
}
/**
* 第三方已受理,请等待异步回执或延迟查询更新最终状态。
*/
public static SendResult accepted(LocalDateTime sendTime, Integer costTime, String thirdPartyId) {
return SendResult.builder()
.outcome(SendOutcome.ACCEPTED)
.sendTime(sendTime)
.costTime(costTime)
.thirdPartyId(thirdPartyId)
.build();
}
/**
* 服务商不支持该渠道能力。
*/
public static SendResult unsupported(String message) {
return SendResult.builder()
.outcome(SendOutcome.UNSUPPORTED_CHANNEL)
.errorCode("UNSUPPORTED_CHANNEL")
.message(message)
.retryable(false)
.build();
}
/**
* 服务商理论支持该能力,但当前配置不完整或初始化失败。
*/
public static SendResult configInvalid(String message) {
return SendResult.builder()
.outcome(SendOutcome.CONFIG_INVALID)
.errorCode("CONFIG_INVALID")
.message(message)
.retryable(false)
.build();
}
}

View File

@@ -0,0 +1,30 @@
package com.njcn.msgpush.module.push.client.sender.fallback;
import com.njcn.msgpush.module.push.client.sender.SendOutcome;
import com.njcn.msgpush.module.push.client.sender.SendResult;
/**
* 用于承载“发送前即可确定”的固定结果。
* 例如:当前服务商不支持该渠道,或当前渠道配置不完整。
*/
public abstract class AbstractFixedResultSender {
private final SendOutcome outcome;
private final String message;
protected AbstractFixedResultSender(SendOutcome outcome, String message) {
if (!SendOutcome.UNSUPPORTED_CHANNEL.equals(outcome) && !SendOutcome.CONFIG_INVALID.equals(outcome)) {
throw new IllegalArgumentException("固定结果 sender 仅支持不支持渠道或配置无效场景");
}
this.outcome = outcome;
this.message = message;
}
protected SendResult buildResult() {
if (SendOutcome.UNSUPPORTED_CHANNEL.equals(outcome)) {
return SendResult.unsupported(message);
}
return SendResult.configInvalid(message);
}
}

View File

@@ -0,0 +1,21 @@
package com.njcn.msgpush.module.push.client.sender.fallback;
import com.njcn.msgpush.module.push.client.sender.AppPushSender;
import com.njcn.msgpush.module.push.client.sender.SendOutcome;
import com.njcn.msgpush.module.push.client.sender.SendResult;
import com.njcn.msgpush.module.push.dal.dataobject.message.MessageRecordDO;
/**
* APP 推送渠道固定结果 sender。
*/
public class FixedResultAppPushSender extends AbstractFixedResultSender implements AppPushSender {
public FixedResultAppPushSender(SendOutcome outcome, String message) {
super(outcome, message);
}
@Override
public SendResult appPush(MessageRecordDO messageRecord) {
return this.buildResult();
}
}

View File

@@ -0,0 +1,23 @@
package com.njcn.msgpush.module.push.client.sender.fallback;
import com.njcn.msgpush.module.push.client.sender.EmailSender;
import com.njcn.msgpush.module.push.client.sender.SendOutcome;
import com.njcn.msgpush.module.push.client.sender.SendResult;
import com.njcn.msgpush.module.push.dal.dataobject.message.MessageRecordDO;
import java.util.Map;
/**
* 邮件渠道固定结果 sender。
*/
public class FixedResultEmailSender extends AbstractFixedResultSender implements EmailSender {
public FixedResultEmailSender(SendOutcome outcome, String message) {
super(outcome, message);
}
@Override
public SendResult sendEmail(MessageRecordDO messageRecord, Map<String, Object> params) {
return this.buildResult();
}
}

View File

@@ -0,0 +1,39 @@
package com.njcn.msgpush.module.push.client.sender.fallback;
import com.njcn.msgpush.module.push.client.sender.SendOutcome;
import com.njcn.msgpush.module.push.client.sender.SendResult;
import com.njcn.msgpush.module.push.client.sender.SmsSender;
import com.njcn.msgpush.module.push.dal.dataobject.message.MessageRecordDO;
import java.util.ArrayList;
import java.util.List;
/**
* 短信渠道固定结果 sender。
* 用于替代返回 null让主流程始终拿到可执行对象。
*/
public class FixedResultSmsSender extends AbstractFixedResultSender implements SmsSender {
public FixedResultSmsSender(SendOutcome outcome, String message) {
super(outcome, message);
}
@Override
public SendResult sendSms(MessageRecordDO messageRecord) {
return this.buildResult();
}
@Override
public List<SendResult> sendBatchSms(List<MessageRecordDO> messageList) {
List<SendResult> results = new ArrayList<>(messageList.size());
for (int i = 0; i < messageList.size(); i++) {
results.add(this.buildResult());
}
return results;
}
@Override
public void queryTemplate(String templateIdentifier) {
// 固定结果 sender 不触达第三方,无需查询模板。
}
}

View File

@@ -0,0 +1,101 @@
package com.njcn.msgpush.module.push.client.sender.impl.apppush;
import com.getui.push.v2.sdk.ApiHelper;
import com.getui.push.v2.sdk.GtApiConfiguration;
import com.getui.push.v2.sdk.api.PushApi;
import com.getui.push.v2.sdk.common.ApiResult;
import com.getui.push.v2.sdk.dto.req.Audience;
import com.getui.push.v2.sdk.dto.req.Settings;
import com.getui.push.v2.sdk.dto.req.message.PushDTO;
import com.getui.push.v2.sdk.dto.req.message.PushMessage;
import com.getui.push.v2.sdk.dto.req.message.android.GTNotification;
import com.njcn.msgpush.module.push.client.sender.AppPushSender;
import com.njcn.msgpush.module.push.client.sender.SendResult;
import com.njcn.msgpush.module.push.client.sender.Sender;
import com.njcn.msgpush.module.push.client.setting.impl.UniPushAppPushSetting;
import com.njcn.msgpush.module.push.dal.dataobject.message.MessageRecordDO;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Map;
@Slf4j
public class UniPushAppPushSender implements AppPushSender {
private final Sender sender;
private PushApi pushApi;
public UniPushAppPushSender(UniPushAppPushSetting uniPushAppPushSetting, Sender sender) {
this.sender = sender;
try {
GtApiConfiguration gtApiConfiguration = new GtApiConfiguration();
gtApiConfiguration.setAppId(uniPushAppPushSetting.getAppId());
gtApiConfiguration.setAppKey(uniPushAppPushSetting.getAppKey());
gtApiConfiguration.setMasterSecret(uniPushAppPushSetting.getMasterSecret());
gtApiConfiguration.setDomain("https://restapi.getui.com/v2/");
ApiHelper apiHelper = ApiHelper.build(gtApiConfiguration);
this.pushApi = apiHelper.creatApi(PushApi.class);
} catch (Exception e) {
log.error("UniPush 客户端初始化失败", e);
this.pushApi = null;
}
}
@Override
public SendResult appPush(MessageRecordDO message) {
if (pushApi == null) {
return SendResult.configInvalid("当前服务商 APP 推送渠道初始化失败");
}
try {
LocalDateTime now = LocalDateTime.now();
long start = now.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
PushDTO<Audience> pushDTO = this.buildPushDTO(message.getTitle(), message.getContent());
Audience audience = new Audience();
audience.addCid(message.getReceiver());
pushDTO.setAudience(audience);
ApiResult<Map<String, Map<String, String>>> apiResult = pushApi.pushToSingleByCid(pushDTO);
LocalDateTime end = LocalDateTime.now();
int costTime = (int) (end.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() - start);
message.setSendTime(now);
message.setCostTime(costTime);
if (apiResult.isSuccess()) {
// UniPush 同步接口返回成功时,当前平台将其视为已受理。
return SendResult.accepted(now, costTime, null);
}
// 个推原始错误码在这里统一映射为平台错误码和中文错误信息。
return this.sender.buildFailureResult(
message,
String.valueOf(apiResult.getCode()),
apiResult.getMsg(),
"THIRD_PARTY_CALL_FAILED",
"第三方服务调用失败",
false
);
} catch (Exception e) {
log.error("UniPush 推送失败", e);
return this.sender.buildCallFailedResult("第三方服务调用失败");
}
}
private PushDTO<Audience> buildPushDTO(String title, String content) {
PushDTO<Audience> pushDTO = new PushDTO<>();
pushDTO.setRequestId(String.valueOf(System.currentTimeMillis()));
Settings settings = new Settings();
settings.setTtl(3600000);
pushDTO.setSettings(settings);
PushMessage pushMessage = new PushMessage();
GTNotification notification = new GTNotification();
notification.setTitle(title);
notification.setBody(content);
notification.setClickType("startapp");
pushMessage.setNotification(notification);
pushDTO.setPushMessage(pushMessage);
return pushDTO;
}
}

View File

@@ -0,0 +1,110 @@
package com.njcn.msgpush.module.push.client.sender.impl.email;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.aliyun.dm20151123.Client;
import com.aliyun.dm20151123.models.SingleSendMailRequest;
import com.aliyun.dm20151123.models.SingleSendMailResponse;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.models.RuntimeOptions;
import com.njcn.msgpush.module.push.client.sender.EmailSender;
import com.njcn.msgpush.module.push.client.sender.SendResult;
import com.njcn.msgpush.module.push.client.sender.Sender;
import com.njcn.msgpush.module.push.client.setting.impl.AliYunMailSetting;
import com.njcn.msgpush.module.push.dal.dataobject.message.MessageRecordDO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@Slf4j
public class AliyunEmailSender implements EmailSender {
private static final String ACCOUNT_NAME = "accountName";
private static final String REPLY_TO_ADDRESS = "replyToAddress";
private static final String FROM_ALIAS = "fromAlias";
private final Sender sender;
private Client emailClient;
public AliyunEmailSender(AliYunMailSetting aliYunMailSetting, Sender sender) {
this.sender = sender;
if (ObjectUtil.isNotNull(aliYunMailSetting)) {
Config config = new Config()
.setAccessKeyId(aliYunMailSetting.getAccessKeyId())
.setAccessKeySecret(aliYunMailSetting.getAccessKeySecret())
.setRegionId(aliYunMailSetting.getRegionId())
.setEndpoint(aliYunMailSetting.getEndpoint());
try {
this.emailClient = new Client(config);
} catch (Exception e) {
log.error("阿里云邮件客户端初始化失败", e);
this.emailClient = null;
}
}
}
@Override
public SendResult sendEmail(MessageRecordDO message, Map<String, Object> params) {
if (emailClient == null) {
return SendResult.configInvalid("当前服务商邮件渠道初始化失败");
}
Future<SendResult> future = this.sender.MSG_PUSH_THREAD_POOL_EXECUTOR.submit(() -> {
RuntimeOptions runtimeOptions = new RuntimeOptions();
runtimeOptions.autoretry = true;
JSONObject jsonObject = JSON.parseObject(message.getExtraInfo());
SingleSendMailRequest request = new SingleSendMailRequest()
.setAccountName(jsonObject.getString(ACCOUNT_NAME))
.setAddressType(1)
.setReplyToAddress(jsonObject.getBooleanValue(REPLY_TO_ADDRESS))
.setToAddress(message.getReceiver())
.setSubject(message.getTitle())
.setHtmlBody(message.getContent())
.setTextBody("")
.setFromAlias(jsonObject.getString(FROM_ALIAS));
try {
LocalDateTime now = LocalDateTime.now();
long start = now.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
SingleSendMailResponse response = this.emailClient.singleSendMailWithOptions(request, runtimeOptions);
LocalDateTime end = LocalDateTime.now();
int costTime = (int) (end.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() - start);
message.setSendTime(now);
message.setCostTime(costTime);
if (HttpStatus.OK.value() == response.getStatusCode()) {
// 邮件接口同步返回成功时,当前平台直接认定本次发送成功。
return SendResult.success(now, costTime, null);
}
// 邮件服务失败同样统一转换为平台错误码和中文错误信息。
return this.sender.buildFailureResult(
message,
String.valueOf(response.getStatusCode()),
null,
"THIRD_PARTY_CALL_FAILED",
"第三方服务调用失败",
false
);
} catch (Exception e) {
log.error("阿里云邮件发送失败", e);
return this.sender.buildCallFailedResult("第三方服务调用失败");
}
});
try {
return future.get(3, TimeUnit.SECONDS);
} catch (Exception e) {
log.error("阿里云邮件发送超时或执行异常", e);
return this.sender.buildTimeoutResult();
}
}
}

View File

@@ -0,0 +1,156 @@
package com.njcn.msgpush.module.push.client.sender.impl.sms;
import cn.hutool.core.util.ObjectUtil;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.QuerySendDetailsRequest;
import com.aliyun.dysmsapi20170525.models.QuerySendDetailsResponse;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.models.RuntimeOptions;
import com.njcn.msgpush.module.push.client.sender.SendResult;
import com.njcn.msgpush.module.push.client.sender.Sender;
import com.njcn.msgpush.module.push.client.sender.SmsSender;
import com.njcn.msgpush.module.push.client.setting.impl.AliYunMailSetting;
import com.njcn.msgpush.module.push.dal.dataobject.message.MessageRecordDO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@Slf4j
public class AliyunSmsSender implements SmsSender {
public static final String OK = "OK";
private final Sender sender;
private Client smsClient;
public AliyunSmsSender(AliYunMailSetting aliYunSmsSetting, Sender sender) {
this.sender = sender;
if (ObjectUtil.isNotNull(aliYunSmsSetting)) {
Config config = new Config()
.setAccessKeyId(aliYunSmsSetting.getAccessKeyId())
.setAccessKeySecret(aliYunSmsSetting.getAccessKeySecret())
.setRegionId(aliYunSmsSetting.getRegionId())
.setEndpoint(aliYunSmsSetting.getEndpoint());
try {
this.smsClient = new Client(config);
} catch (Exception e) {
log.error("阿里云短信客户端初始化失败", e);
this.smsClient = null;
}
}
}
@Override
public SendResult sendSms(MessageRecordDO message) {
if (smsClient == null) {
return SendResult.configInvalid("当前服务商短信渠道初始化失败");
}
Future<SendResult> future = this.sender.MSG_PUSH_THREAD_POOL_EXECUTOR.submit(() -> {
RuntimeOptions runtimeOptions = new RuntimeOptions();
runtimeOptions.autoretry = true;
SendSmsRequest request = new SendSmsRequest()
.setPhoneNumbers(message.getReceiver())
.setSignName(message.getTitle())
.setTemplateCode(message.getTemplateCode())
.setTemplateParam(message.getTemplateParams());
try {
LocalDateTime now = LocalDateTime.now();
long start = now.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
SendSmsResponse response = this.smsClient.sendSmsWithOptions(request, runtimeOptions);
LocalDateTime end = LocalDateTime.now();
int costTime = (int) (end.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() - start);
message.setSendTime(now);
message.setCostTime(costTime);
if (HttpStatus.OK.value() == response.getStatusCode() && response.getBody() != null && OK.equals(response.getBody().getCode())) {
String bizId = response.getBody().getBizId();
message.setThirdPartyId(bizId);
// 阿里云短信同步接口只表示“已受理”,最终是否送达依赖后续回执查询。
this.getDownInfo(bizId, message);
return SendResult.accepted(now, costTime, bizId);
}
String providerRawCode = response.getBody() == null ? null : response.getBody().getCode();
String providerRawMessage = response.getBody() == null ? null : response.getBody().getMessage();
// 第三方失败统一在这里映射为平台错误码和中文错误信息。
return this.sender.buildFailureResult(
message,
providerRawCode,
providerRawMessage,
"THIRD_PARTY_CALL_FAILED",
"第三方服务调用失败",
false
);
} catch (Exception e) {
log.error("阿里云短信发送失败", e);
return this.sender.buildCallFailedResult("第三方服务调用失败");
}
});
try {
return future.get(3, TimeUnit.SECONDS);
} catch (Exception e) {
log.error("阿里云短信发送超时或执行异常", e);
return this.sender.buildTimeoutResult();
}
}
@Override
public List<SendResult> sendBatchSms(List<MessageRecordDO> messageList) {
List<SendResult> results = new ArrayList<>();
for (MessageRecordDO message : messageList) {
results.add(this.sendSms(message));
}
return results;
}
@Override
public void queryTemplate(String templateIdentifier) {
}
private void getDownInfo(String bizId, MessageRecordDO message) {
// 回执存在延迟,这里延后查询,避免刚发送完就拿不到最终状态。
this.sender.MSG_CALLBACK_THREAD_POOL_SCHEDULER.schedule(() -> {
QuerySendDetailsRequest request = new QuerySendDetailsRequest()
.setPhoneNumber(message.getReceiver())
.setBizId(bizId)
.setSendDate(message.getSendTime().format(DateTimeFormatter.ofPattern("yyyyMMdd")))
.setCurrentPage(1L)
.setPageSize(10L);
try {
QuerySendDetailsResponse response = this.smsClient.querySendDetails(request);
if (response.getBody() == null || response.getBody().getSmsSendDetailDTOs() == null || response.getBody().getSmsSendDetailDTOs().getSmsSendDetailDTO() == null) {
return;
}
response.getBody().getSmsSendDetailDTOs().getSmsSendDetailDTO().forEach(detail -> {
if (!"DELIVERED".equals(detail.getErrCode())) {
SendResult failedResult = this.sender.buildFailureResult(
message,
detail.getErrCode(),
null,
"THIRD_PARTY_CALLBACK_FAILED",
"短信回执返回失败",
false
);
this.sender.applyCallbackResult(message, failedResult);
} else {
// 送达成功后,统一复用主流程成功状态更新逻辑。
this.sender.applyCallbackResult(
message,
SendResult.success(message.getSendTime(), message.getCostTime(), message.getThirdPartyId())
);
}
});
} catch (Exception e) {
log.error("阿里云短信回执查询失败", e);
}
}, 20, TimeUnit.SECONDS);
}
}

View File

@@ -0,0 +1,225 @@
package com.njcn.msgpush.module.push.client.sender.impl.sms;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.njcn.msgpush.module.push.client.sender.SendResult;
import com.njcn.msgpush.module.push.client.sender.Sender;
import com.njcn.msgpush.module.push.client.sender.SmsSender;
import com.njcn.msgpush.module.push.client.setting.impl.TelecomSmsSetting;
import com.njcn.msgpush.module.push.dal.dataobject.message.MessageRecordDO;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static com.alibaba.fastjson.JSON.toJSON;
@Data
@Slf4j
public class TelecomSmsSender implements SmsSender {
private static final String CONTENT_TYPE = "application/json;charset=utf-8";
private final TelecomSmsSetting telecomSmsSetting;
private final Sender sender;
@Data
private static class TelecomSmsSendResponse {
private String status;
private Double balance;
private List<TelecomSmsSendDetailRes> list;
}
@Data
private static class TelecomSmsSendDetailRes {
private String mid;
private String mobile;
private Integer result;
}
@Data
private static class TelecomSmsSelectResponse {
private String status;
private Double balance;
private List<TelecomSmsSelectDetailRes> list;
}
@Data
private static class TelecomSmsSelectDetailRes {
private String apmid;
private String apSubmitTime;
private String mobile;
private Integer status;
private String stat;
private String deliverTime;
}
public TelecomSmsSender(TelecomSmsSetting telecomSmsSetting, Sender sender) {
this.telecomSmsSetting = telecomSmsSetting;
this.sender = sender;
}
@Override
public SendResult sendSms(MessageRecordDO message) {
Future<SendResult> future = this.sender.MSG_PUSH_THREAD_POOL_EXECUTOR.submit(() -> {
Map<String, Object> request = new HashMap<>();
boolean isTemplateSend = StrUtil.isNotBlank(message.getTemplateCode());
request.put("account", telecomSmsSetting.getAccount());
request.put("password", telecomSmsSetting.getPassword());
request.put("extno", telecomSmsSetting.getExtno());
if (isTemplateSend) {
request.put("action", "templatep2p");
Map<String, Object> templateJsonMap = new HashMap<>();
templateJsonMap.put("templateID", message.getTemplateCode());
JSONObject jsonObject = JSON.parseObject(message.getTemplateParams());
Map<String, String> variable = new HashMap<>();
variable.put("mobile", message.getReceiver());
jsonObject.forEach((key, value) -> variable.put(key, value.toString()));
templateJsonMap.put("variable", "[" + toJSON(variable) + "]");
request.put("templateJson", toJSON(templateJsonMap));
} else {
request.put("action", "send");
request.put("mobile", message.getReceiver());
request.put("content", message.getContent());
}
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", CONTENT_TYPE);
try {
LocalDateTime now = LocalDateTime.now();
long start = now.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
ResponseEntity<String> response = this.sender.restTemplateUtil.post(
telecomSmsSetting.getApiUrl(),
request,
headers,
String.class
);
LocalDateTime end = LocalDateTime.now();
int costTime = (int) (end.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() - start);
message.setSendTime(now);
message.setCostTime(costTime);
TelecomSmsSendResponse sendResponse = JSON.parseObject(response.getBody(), TelecomSmsSendResponse.class);
TelecomSmsSendDetailRes detailRes = sendResponse == null || sendResponse.getList() == null || sendResponse.getList().isEmpty()
? null
: sendResponse.getList().get(0);
if (response.getStatusCode() == HttpStatus.OK && detailRes != null && detailRes.getResult() != null && detailRes.getResult() == 0) {
message.setThirdPartyId(detailRes.getMid());
// 电信短信同步返回成功同样只代表已受理,最终状态以后续回执为准。
this.getDownInfo(detailRes.getMid(), message);
return SendResult.accepted(now, costTime, detailRes.getMid());
}
String providerRawCode = detailRes == null || detailRes.getResult() == null ? null : String.valueOf(detailRes.getResult());
// 电信原始结果码在这里统一映射为平台错误码和中文错误信息。
return this.sender.buildFailureResult(
message,
providerRawCode,
null,
"THIRD_PARTY_CALL_FAILED",
"第三方服务调用失败",
false
);
} catch (Exception e) {
log.error("电信短信发送失败", e);
return this.sender.buildCallFailedResult("第三方服务调用失败");
}
});
try {
return future.get(3, TimeUnit.SECONDS);
} catch (Exception e) {
log.error("电信短信发送超时或执行异常", e);
return this.sender.buildTimeoutResult();
}
}
@Override
public List<SendResult> sendBatchSms(List<MessageRecordDO> messageList) {
List<SendResult> results = new ArrayList<>();
for (MessageRecordDO message : messageList) {
results.add(this.sendSms(message));
}
return results;
}
@Override
public void queryTemplate(String templateIdentifier) {
Map<String, Object> request = new HashMap<>();
request.put("action", "templateSelect");
request.put("account", telecomSmsSetting.getAccount());
request.put("password", telecomSmsSetting.getPassword());
request.put("templateJson", StrUtil.isBlank(templateIdentifier) ? 0 : templateIdentifier);
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", CONTENT_TYPE);
this.sender.restTemplateUtil.post(
telecomSmsSetting.getApiUrl(),
request,
headers,
String.class
);
}
private void getDownInfo(String mid, MessageRecordDO message) {
// 回执查询延后执行,给第三方落库和状态变更留出时间。
this.sender.MSG_CALLBACK_THREAD_POOL_SCHEDULER.schedule(() -> {
Map<String, Object> request = new HashMap<>();
request.put("action", "select");
request.put("account", telecomSmsSetting.getAccount());
request.put("password", telecomSmsSetting.getPassword());
request.put("date", message.getSendTime().format(DateTimeFormatter.ofPattern("yyyyMMdd")));
request.put("condition", "APMID");
request.put("valueList", mid);
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", CONTENT_TYPE);
try {
ResponseEntity<String> response = this.sender.restTemplateUtil.post(
telecomSmsSetting.getApiUrl(),
request,
headers,
String.class
);
TelecomSmsSelectResponse selectResponse = JSON.parseObject(response.getBody(), TelecomSmsSelectResponse.class);
if (selectResponse == null || selectResponse.getList() == null || selectResponse.getList().isEmpty()) {
return;
}
TelecomSmsSelectDetailRes detailRes = selectResponse.getList().get(0);
if (detailRes.getStatus() == 5) {
SendResult failedResult = this.sender.buildFailureResult(
message,
detailRes.getStat(),
null,
"THIRD_PARTY_CALLBACK_FAILED",
"短信回执返回失败",
false
);
this.sender.applyCallbackResult(message, failedResult);
} else if (detailRes.getStatus() == 4) {
// 回执确认成功后,复用统一成功落库逻辑。
this.sender.applyCallbackResult(
message,
SendResult.success(message.getSendTime(), message.getCostTime(), message.getThirdPartyId())
);
}
} catch (Exception e) {
log.error("电信短信回执查询失败", e);
}
}, 20, TimeUnit.SECONDS);
}
}