1、结构化调整;
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 不触达第三方,无需查询模板。
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user