diff --git a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/service/merchant/config/MchAppNotifyConfigService.java b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/service/merchant/config/MchAppNotifyConfigService.java index d745cf479..6e753e1ea 100644 --- a/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/service/merchant/config/MchAppNotifyConfigService.java +++ b/daxpay-payment/daxpay-payment-admin/src/main/java/cn/daxpay/open/payment/admin/service/merchant/config/MchAppNotifyConfigService.java @@ -14,8 +14,10 @@ import org.springframework.stereotype.Service; /// # 商户应用事件通知配置服务 /// -/// 应用级通用事件通知配置, 与支付订单级回调并行, 当前版本仅维护配置数据, -/// 发送链路(任务/重试/记录)后续阶段实现 +/// 应用级通用事件通知配置, 与支付订单级回调并行。 +/// 配置由 [cn.daxpay.open.payment.trade.notice.service.NoticeDispatcher] 读取: +/// notifyWay=http 走 HTTP 回调(notifyUrl), notifyWay=mq 走 MQ 推送(发布到 daxpay.notice. Topic), +/// 按订阅事件(subscribedEvents)前缀匹配触发, 发送/重试/记录由 NoticeSendEngine 统一负责 @Slf4j @Service @RequiredArgsConstructor diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/command/NoticeDispatchCommand.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/command/NoticeDispatchCommand.java index 5c4b3549d..714b6f11e 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/command/NoticeDispatchCommand.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/command/NoticeDispatchCommand.java @@ -1,7 +1,8 @@ package cn.daxpay.open.payment.trade.notice.command; import cn.daxpay.open.platform.core.enums.pay.notice.NoticeContentModeEnum; -import cn.daxpay.open.platform.core.enums.pay.notice.NoticeProtocolEnum; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeFormatEnum; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeTransportEnum; import lombok.Data; import lombok.experimental.Accessors; @@ -30,8 +31,11 @@ public class NoticeDispatchCommand { /// 订单级 notifyUrl(可空) private String orderNotifyUrl; - /// 通知协议,默认 SYSTEM - private NoticeProtocolEnum protocol = NoticeProtocolEnum.SYSTEM; + /// 传输通道,默认 HTTP + private NoticeTransportEnum transport = NoticeTransportEnum.HTTP; + + /// 报文格式,默认 SYSTEM + private NoticeFormatEnum format = NoticeFormatEnum.SYSTEM; /// 内容策略 private NoticeContentModeEnum contentMode = NoticeContentModeEnum.SNAPSHOT; @@ -39,6 +43,6 @@ public class NoticeDispatchCommand { /// 快照 JSON 或引用指针 JSON private String contentOrRef; - /// 协议适配层自带 URL(protocol 非 SYSTEM 时使用) + /// 协议适配层自带 URL(format 非 SYSTEM 时使用,如易支付) private String protocolNotifyUrl; } diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/dao/MchNoticeTaskManager.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/dao/MchNoticeTaskManager.java index 90348f025..305231586 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/dao/MchNoticeTaskManager.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/dao/MchNoticeTaskManager.java @@ -5,11 +5,15 @@ import cn.daxpay.open.payment.trade.notice.param.MchNoticeTaskQuery; import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager; import cn.daxpay.open.platform.common.mybatisplus.query.generator.QueryGenerator; import cn.daxpay.open.platform.common.mybatisplus.util.MpUtil; +import cn.daxpay.open.platform.core.annotation.IgnoreTenant; import cn.daxpay.open.platform.core.rest.param.PageParam; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.springframework.stereotype.Repository; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.List; import java.util.Optional; /// # 商户出站通知任务管理 @@ -27,14 +31,29 @@ public class MchNoticeTaskManager extends BaseManager findByIdempotentKey(String mchNo, String appId, String event, - String bizNo, String protocol, String source) { + String bizNo, String transport, String format, String source) { return lambdaQuery() .eq(MchNoticeTask::getMchNo, mchNo) .eq(MchNoticeTask::getAppId, appId) .eq(MchNoticeTask::getEvent, event) .eq(MchNoticeTask::getBizNo, bizNo) - .eq(MchNoticeTask::getProtocol, protocol) + .eq(MchNoticeTask::getTransport, transport) + .eq(MchNoticeTask::getFormat, format) .eq(MchNoticeTask::getSource, source) .oneOpt(); } + + /// 扫描未成功且(nextTime 为空 或 nextTime <= now)的孤儿任务(MQ 投递失败兜底) + /// + /// 覆盖 [cn.daxpay.open.payment.trade.notice.service.NoticeTaskScheduleService#scheduleImmediateAfterCommit] + /// 投递 MQ 失败导致任务卡在 success=false、nextTime=null 的场景(全仓无其他扫描入口)。 + /// 跨租户扫描(定时任务无 HTTP 上下文), 单次上限 limit 防积压爆量。 + @IgnoreTenant + public List findStaleUnsent(int limit) { + return listLimit(limit, q -> q + .eq(MchNoticeTask::isSuccess, false) + .and(w -> w.isNull(MchNoticeTask::getNextTime) + .or().le(MchNoticeTask::getNextTime, OffsetDateTime.now(ZoneOffset.UTC))) + .orderByAsc(MchNoticeTask::getCreateTime)); + } } diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/entity/MchNoticeTask.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/entity/MchNoticeTask.java index fe2b51aa0..9f9473169 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/entity/MchNoticeTask.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/entity/MchNoticeTask.java @@ -6,8 +6,9 @@ import cn.daxpay.open.payment.trade.notice.result.MchNoticeTaskResult; import cn.daxpay.open.platform.common.mybatisplus.function.ToResult; import cn.daxpay.open.platform.core.enums.pay.notice.NoticeContentModeEnum; import cn.daxpay.open.platform.core.enums.pay.notice.NoticeEventEnum; -import cn.daxpay.open.platform.core.enums.pay.notice.NoticeProtocolEnum; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeFormatEnum; import cn.daxpay.open.platform.core.enums.pay.notice.NoticeSourceEnum; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeTransportEnum; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.FieldStrategy; @@ -41,9 +42,13 @@ public class MchNoticeTask extends MchBaseEntity implements ToResult stale = mchNoticeTaskManager.findStaleUnsent(500); + if (stale.isEmpty()) { + return; + } + log.info("通知兜底扫描命中 {} 笔孤儿任务, 重投 MQ", stale.size()); + for (MchNoticeTask task : stale) { + try { + noticeTaskScheduleService.sendNow(task.getId()); + } catch (Exception e) { + // 单笔重投失败不阻断整批(MQ 仍不可用时下轮再试) + log.warn("通知兜底重投失败 taskId={}", task.getId(), e); + } + } + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/param/MchNoticeTaskQuery.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/param/MchNoticeTaskQuery.java index c2328bf4e..cdea4bc1e 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/param/MchNoticeTaskQuery.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/param/MchNoticeTaskQuery.java @@ -29,8 +29,12 @@ public class MchNoticeTaskQuery { private String event; @QueryParam(type = QueryParam.CompareTypeEnum.EQ) - @Schema(description = "通知协议") - private String protocol; + @Schema(description = "传输通道 (http/mq)") + private String transport; + + @QueryParam(type = QueryParam.CompareTypeEnum.EQ) + @Schema(description = "报文格式 (system/easy_pay)") + private String format; @QueryParam(type = QueryParam.CompareTypeEnum.EQ) @Schema(description = "URL来源") diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/payload/NoticeEnvelope.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/payload/NoticeEnvelope.java new file mode 100644 index 000000000..bb331e177 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/payload/NoticeEnvelope.java @@ -0,0 +1,27 @@ +package cn.daxpay.open.payment.trade.notice.payload; + +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 商户出站通知投递信封 +/// +/// 由 [NoticePayloadBuilder] 按 format 组装, 描述一次投递的请求形态。 +/// 传输通道 [cn.daxpay.open.payment.trade.notice.transport.NoticeTransportSender] 据此投递: +/// - HTTP: 按 method 发请求 (POST 用 body, GET 用 url 含 query) +/// - MQ: 忽略 method, 将 body 发布到 task.url(Topic) +@Data +@Accessors(chain = true) +public class NoticeEnvelope { + + /// HTTP 方法 (POST / GET), 仅 HTTP 传输使用; MQ 传输忽略 + private String method; + + /// HTTP 完整请求 URL (GET 时含 query); MQ 时通常为 null + private String url; + + /// 请求体 (POST JSON 或 MQ 推送的消息体); GET 时为 null + private String body; + + /// 请求摘要(截断), 便于排查 + private String requestDigest; +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/payload/NoticePayloadBuilder.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/payload/NoticePayloadBuilder.java new file mode 100644 index 000000000..9a5dc3ff3 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/payload/NoticePayloadBuilder.java @@ -0,0 +1,16 @@ +package cn.daxpay.open.payment.trade.notice.payload; + +import cn.daxpay.open.payment.trade.notice.entity.MchNoticeTask; + +/// # 商户出站通知报文构建器 +/// +/// 按 [cn.daxpay.open.platform.core.enums.pay.notice.NoticeFormatEnum] 路由, +/// 仅负责组装报文内容 ([NoticeEnvelope]), 与传输通道正交 +public interface NoticePayloadBuilder { + + /// 报文格式编码(与 NoticeFormatEnum.code 对齐: system / easy_pay) + String format(); + + /// 组装投递信封 + NoticeEnvelope build(MchNoticeTask task); +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/payload/SystemPayloadBuilder.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/payload/SystemPayloadBuilder.java new file mode 100644 index 000000000..dfd494d52 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/payload/SystemPayloadBuilder.java @@ -0,0 +1,55 @@ +package cn.daxpay.open.payment.trade.notice.payload; + +import cn.daxpay.open.payment.common.result.DaxNoticeResult; +import cn.daxpay.open.payment.common.util.JsonSignStrUtil; +import cn.daxpay.open.payment.common.util.PaySignUtil; +import cn.daxpay.open.payment.trade.notice.entity.MchNoticeTask; +import cn.daxpay.open.platform.common.config.properties.PlatformConfigProperties; +import cn.daxpay.open.platform.common.json.util.JacksonUtil; +import cn.daxpay.open.platform.core.code.CommonCode; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeFormatEnum; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +/// # 标准 DaxPay 签名 JSON 报文构建器 +/// +/// 组装 [DaxNoticeResult] JSON + 平台私钥 RSA 签名, 产 POST 信封。 +/// 对外报文 protocol 字段取自 task.format(值为 system), 保持商户侧契约不变 +@Slf4j +@Component +@RequiredArgsConstructor +public class SystemPayloadBuilder implements NoticePayloadBuilder { + + private final PlatformConfigProperties platformConfigProperties; + + @Override + public String format() { + return NoticeFormatEnum.SYSTEM.getCode(); + } + + @Override + public NoticeEnvelope build(MchNoticeTask task) { + var data = JsonSignStrUtil.buildSortedMap(task.getContent()); + var notice = new DaxNoticeResult<>(CommonCode.SUCCESS_CODE, data, CommonCode.SUCCESS_MSG) + .setEvent(task.getEvent()) + .setProtocol(task.getFormat()) + .setMchNo(task.getMchNo()) + .setAppId(task.getAppId()); + notice.setResTime(OffsetDateTime.now(ZoneOffset.UTC)); + notice.setReqId(MDC.get(CommonCode.TRACE_ID)); + String privateKey = platformConfigProperties.getKeyConfig().getPrivateKey(); + notice.setSign(PaySignUtil.sign(notice, privateKey)); + String requestJson = JacksonUtil.toJson(notice); + return new NoticeEnvelope() + .setMethod("POST") + .setUrl(task.getUrl()) + .setBody(requestJson) + .setRequestDigest(StrUtil.sub(requestJson, 0, 500)); + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/protocol/NoticeProtocolSender.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/protocol/NoticeProtocolSender.java deleted file mode 100644 index 691e0bceb..000000000 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/protocol/NoticeProtocolSender.java +++ /dev/null @@ -1,31 +0,0 @@ -package cn.daxpay.open.payment.trade.notice.protocol; - -import cn.daxpay.open.payment.trade.notice.entity.MchNoticeTask; -import lombok.Data; -import lombok.experimental.Accessors; - -/// # 商户出站通知协议发送器 -/// -/// 按 protocol 精确路由;禁止 fan-out 遍历全部插件 -public interface NoticeProtocolSender { - - /// 协议编码(与 NoticeProtocolEnum.code 对齐) - String protocol(); - - /// 执行一次 HTTP/协议发送 - NoticeSendResult send(MchNoticeTask task); - - /// 单次发送结果 - @Data - @Accessors(chain = true) - class NoticeSendResult { - /// 是否业务 Ack 成功 - private boolean success; - /// HTTP 状态码(可空) - private Integer httpStatus; - /// 错误或非 SUCCESS 响应摘要 - private String errorMsg; - /// 请求摘要(便于排查) - private String requestDigest; - } -} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/protocol/SystemHttpSignedSender.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/protocol/SystemHttpSignedSender.java deleted file mode 100644 index 05054d1aa..000000000 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/protocol/SystemHttpSignedSender.java +++ /dev/null @@ -1,79 +0,0 @@ -package cn.daxpay.open.payment.trade.notice.protocol; - -import cn.daxpay.open.payment.common.result.DaxNoticeResult; -import cn.daxpay.open.payment.common.util.JsonSignStrUtil; -import cn.daxpay.open.payment.common.util.PaySignUtil; -import cn.daxpay.open.payment.trade.notice.entity.MchNoticeTask; -import cn.daxpay.open.platform.common.config.properties.PlatformConfigProperties; -import cn.daxpay.open.platform.common.json.util.JacksonUtil; -import cn.daxpay.open.platform.core.code.CommonCode; -import cn.daxpay.open.platform.core.enums.pay.notice.NoticeProtocolEnum; -import cn.hutool.core.util.StrUtil; -import cn.hutool.http.ContentType; -import cn.hutool.http.HttpResponse; -import cn.hutool.http.HttpUtil; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.slf4j.MDC; -import org.springframework.stereotype.Component; - -import java.time.OffsetDateTime; -import java.time.ZoneOffset; - -/// # 标准 DaxPay 签名 JSON 出站发送器 -/// -/// Ack 规则:HTTP 2xx 且 body trim 后忽略大小写等于 SUCCESS -@Slf4j -@Component -@RequiredArgsConstructor -public class SystemHttpSignedSender implements NoticeProtocolSender { - - private final PlatformConfigProperties platformConfigProperties; - - @Override - public String protocol() { - return NoticeProtocolEnum.SYSTEM.getCode(); - } - - @Override - public NoticeSendResult send(MchNoticeTask task) { - NoticeSendResult result = new NoticeSendResult(); - String body = null; - Integer httpStatus = null; - try { - var data = JsonSignStrUtil.buildSortedMap(task.getContent()); - var notice = new DaxNoticeResult<>(CommonCode.SUCCESS_CODE, data, CommonCode.SUCCESS_MSG) - .setEvent(task.getEvent()) - .setProtocol(task.getProtocol()) - .setMchNo(task.getMchNo()) - .setAppId(task.getAppId()); - notice.setResTime(OffsetDateTime.now(ZoneOffset.UTC)); - notice.setReqId(MDC.get(CommonCode.TRACE_ID)); - String privateKey = platformConfigProperties.getKeyConfig().getPrivateKey(); - notice.setSign(PaySignUtil.sign(notice, privateKey)); - String requestJson = JacksonUtil.toJson(notice); - result.setRequestDigest(StrUtil.sub(requestJson, 0, 500)); - HttpResponse response = HttpUtil.createPost(task.getUrl()) - .body(requestJson, ContentType.JSON.getValue()) - .timeout(15000) - .execute(); - httpStatus = response.getStatus(); - body = response.body(); - } catch (Exception e) { - log.error("系统协议通知发送失败, taskId={}, bizNo={}", task.getId(), task.getBizNo(), e); - result.setSuccess(false) - .setHttpStatus(httpStatus) - .setErrorMsg(e.getMessage()); - return result; - } - result.setHttpStatus(httpStatus); - boolean ack = httpStatus != null && httpStatus >= 200 && httpStatus < 300 - && StrUtil.equalsIgnoreCase(StrUtil.trim(body), "SUCCESS"); - result.setSuccess(ack); - if (!ack) { - result.setErrorMsg(StrUtil.blankToDefault(StrUtil.sub(body, 0, 300), - "httpStatus=" + httpStatus)); - } - return result; - } -} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/result/MchNoticeTaskResult.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/result/MchNoticeTaskResult.java index 517d88279..869dae311 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/result/MchNoticeTaskResult.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/result/MchNoticeTaskResult.java @@ -18,14 +18,6 @@ import java.time.OffsetDateTime; @Schema(title = "商户出站通知任务") public class MchNoticeTaskResult extends MchBaseResult { - /// 商户名称(由 mchNo 翻译) - @Trans( - entity = MerchantInfo.class, - source = MchBaseResult.Fields.mchNo, - result = MerchantInfo.Fields.mchName) - @Schema(description = "商户名称") - private String mchName; - @Schema(description = "应用号") private String appId; @@ -38,8 +30,11 @@ public class MchNoticeTaskResult extends MchBaseResult { @Schema(description = "通知事件码") private String event; - @Schema(description = "通知协议") - private String protocol; + @Schema(description = "传输通道 (http/mq)") + private String transport; + + @Schema(description = "报文格式 (system/easy_pay)") + private String format; @Schema(description = "URL来源") private String source; @@ -50,7 +45,7 @@ public class MchNoticeTaskResult extends MchBaseResult { @Schema(description = "通知内容") private String content; - @Schema(description = "商户接收地址") + @Schema(description = "目标地址 (HTTP回调URL或MQ Topic名)") private String url; @Schema(description = "是否发送成功") diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeDispatcher.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeDispatcher.java index 3054cdb8a..3681684ae 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeDispatcher.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeDispatcher.java @@ -5,9 +5,11 @@ import cn.daxpay.open.payment.merchant.entity.config.MchAppNotifyConfig; import cn.daxpay.open.payment.trade.notice.command.NoticeDispatchCommand; import cn.daxpay.open.payment.trade.notice.dao.MchNoticeTaskManager; import cn.daxpay.open.payment.trade.notice.entity.MchNoticeTask; +import cn.daxpay.open.payment.trade.runtime.mq.PayArtemisConstants; import cn.daxpay.open.platform.core.enums.pay.notice.NoticeContentModeEnum; -import cn.daxpay.open.platform.core.enums.pay.notice.NoticeProtocolEnum; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeFormatEnum; import cn.daxpay.open.platform.core.enums.pay.notice.NoticeSourceEnum; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeTransportEnum; import cn.hutool.core.util.StrUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -18,7 +20,10 @@ import java.util.Objects; /// # 商户出站通知派发器 /// -/// SYSTEM:尝试创建 order + app 两条任务;其它协议:仅 protocol 任务 +/// 决定一次事件生成几条任务、各任务用何种传输通道 [NoticeTransportEnum] + 报文格式 [NoticeFormatEnum]: +/// - 订单级 (ORDER): 走订单传入的 notifyUrl, 恒 HTTP + SYSTEM +/// - 应用级 (APP): 走 [MchAppNotifyConfig], 按 notifyWay(http/mq) 决定 transport +/// - 协议级 (PROTOCOL): 协议适配层自带 URL (如易支付), HTTP + 对应 format @Slf4j @Service @RequiredArgsConstructor @@ -38,42 +43,58 @@ public class NoticeDispatcher { log.warn("出站通知命令缺少必要字段, skip"); return; } - NoticeProtocolEnum protocol = command.getProtocol() == null - ? NoticeProtocolEnum.SYSTEM : command.getProtocol(); + NoticeTransportEnum transport = command.getTransport() == null + ? NoticeTransportEnum.HTTP : command.getTransport(); + NoticeFormatEnum format = command.getFormat() == null + ? NoticeFormatEnum.SYSTEM : command.getFormat(); NoticeContentModeEnum contentMode = command.getContentMode() == null ? NoticeContentModeEnum.SNAPSHOT : command.getContentMode(); - if (protocol == NoticeProtocolEnum.SYSTEM) { - tryCreate(command, NoticeSourceEnum.ORDER, protocol, contentMode, command.getOrderNotifyUrl()); + // SYSTEM 格式: 订单级 + 应用级 双轨并行 (订单级恒 HTTP) + if (format == NoticeFormatEnum.SYSTEM) { + tryCreate(command, NoticeSourceEnum.ORDER, NoticeTransportEnum.HTTP, format, contentMode, command.getOrderNotifyUrl()); tryCreateApp(command, contentMode); return; } - tryCreate(command, NoticeSourceEnum.PROTOCOL, protocol, contentMode, command.getProtocolNotifyUrl()); + // 其它格式 (如 easy_pay): 协议适配层自带 URL, 单条 PROTOCOL 任务 + tryCreate(command, NoticeSourceEnum.PROTOCOL, transport, format, contentMode, command.getProtocolNotifyUrl()); } - /// 应用级订阅 + /// 应用级订阅: 按 [MchAppNotifyConfig].notifyWay 决定传输通道 private void tryCreateApp(NoticeDispatchCommand command, NoticeContentModeEnum contentMode) { MchAppNotifyConfig config = notifyConfigManager.findByAppId(command.getAppId()).orElse(null); - if (config == null || !Boolean.TRUE.equals(config.getStatus()) || StrUtil.isBlank(config.getNotifyUrl())) { + if (config == null || !Boolean.TRUE.equals(config.getStatus())) { return; } if (!matchSubscribed(config.getSubscribedEvents(), command.getEvent())) { return; } - tryCreate(command, NoticeSourceEnum.APP, NoticeProtocolEnum.SYSTEM, contentMode, config.getNotifyUrl()); + // 按配置的通知方式决定传输通道与目标地址 + if (NoticeTransportEnum.MQ.getCode().equals(config.getNotifyWay())) { + // MQ 方式: 目标地址为按应用隔离的 Topic + String topic = PayArtemisConstants.MCH_NOTICE_TOPIC_PREFIX + "." + command.getAppId(); + tryCreate(command, NoticeSourceEnum.APP, NoticeTransportEnum.MQ, NoticeFormatEnum.SYSTEM, contentMode, topic); + } else { + // HTTP 方式(默认): 目标地址为配置的回调 URL + if (StrUtil.isBlank(config.getNotifyUrl())) { + return; + } + tryCreate(command, NoticeSourceEnum.APP, NoticeTransportEnum.HTTP, NoticeFormatEnum.SYSTEM, contentMode, config.getNotifyUrl()); + } } /// 创建任务(幂等)并投递 private void tryCreate(NoticeDispatchCommand command, NoticeSourceEnum source, - NoticeProtocolEnum protocol, NoticeContentModeEnum contentMode, String url) { + NoticeTransportEnum transport, NoticeFormatEnum format, + NoticeContentModeEnum contentMode, String url) { if (StrUtil.isBlank(url)) { - log.debug("出站通知跳过(无URL): event={}, bizNo={}, source={}", + log.debug("出站通知跳过(无目标地址): event={}, bizNo={}, source={}", command.getEvent(), command.getBizNo(), source.getCode()); return; } var existing = taskManager.findByIdempotentKey( command.getMchNo(), command.getAppId(), command.getEvent(), - command.getBizNo(), protocol.getCode(), source.getCode()); + command.getBizNo(), transport.getCode(), format.getCode(), source.getCode()); if (existing.isPresent()) { log.info("出站通知任务已存在, 跳过创建: event={}, bizNo={}, source={}", command.getEvent(), command.getBizNo(), source.getCode()); @@ -86,7 +107,8 @@ public class NoticeDispatcher { .setBizId(command.getBizId()) .setBizNo(command.getBizNo()) .setEvent(command.getEvent()) - .setProtocol(protocol.getCode()) + .setTransport(transport.getCode()) + .setFormat(format.getCode()) .setSource(source.getCode()) .setContentMode(contentMode.getCode()) .setContent(command.getContentOrRef()) @@ -96,8 +118,8 @@ public class NoticeDispatcher { .setDelayCount(0); taskManager.save(task); scheduleService.scheduleImmediateAfterCommit(task.getId()); - log.info("注册出站通知: event={}, bizNo={}, protocol={}, source={}", - command.getEvent(), command.getBizNo(), protocol.getCode(), source.getCode()); + log.info("注册出站通知: event={}, bizNo={}, transport={}, format={}, source={}", + command.getEvent(), command.getBizNo(), transport.getCode(), format.getCode(), source.getCode()); } /// 订阅匹配:精确事件码,或前缀(pay 匹配 pay.*) diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeRetryPolicy.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeRetryPolicy.java index e70ab3968..e1b72ed96 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeRetryPolicy.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeRetryPolicy.java @@ -1,21 +1,26 @@ package cn.daxpay.open.payment.trade.notice.service; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeTransportEnum; import org.springframework.stereotype.Component; import java.util.Map; /// # 商户出站通知重试策略 /// -/// 仿微信通知节奏,约 16 次延时重试,合计约 24h+ -/// 间隔:15s/15s/30s/3m/10m/20m/30m×3/60m/3h×3/6h×… +/// 按传输通道 [NoticeTransportEnum] 区分: +/// - HTTP: 仿微信通知节奏, 约 16 次延时重试, 合计约 24h+ (业务 ACK 失败兜底) +/// - MQ: 仅 3 次短间隔重试 (publish 失败兜底; 消费侧失败由商户/MQ 自身负责) @Component public class NoticeRetryPolicy { - /// 最大延时重试次数 - public static final int MAX_DELAY_COUNT = 16; + /// HTTP 最大延时重试次数 + public static final int MAX_DELAY_COUNT_HTTP = 16; - /// key: 延时次数(1起), value: 下次间隔秒数 - private static final Map DELAY_SECONDS = Map.ofEntries( + /// MQ 最大延时重试次数 + public static final int MAX_DELAY_COUNT_MQ = 3; + + /// HTTP 间隔 (仿微信): 15s/15s/30s/3m/10m/20m/30m×3/60m/3h×3/6h×… + private static final Map DELAY_SECONDS_HTTP = Map.ofEntries( Map.entry(1, 15), Map.entry(2, 15), Map.entry(3, 30), @@ -34,15 +39,30 @@ public class NoticeRetryPolicy { Map.entry(16, 6 * 60 * 60) ); + /// MQ 间隔: 10s/30s/60s + private static final Map DELAY_SECONDS_MQ = Map.of( + 1, 10, + 2, 30, + 3, 60 + ); + + /// 各传输通道最大延时重试次数 + public int maxDelayCount(String transport) { + return NoticeTransportEnum.MQ.getCode().equals(transport) ? MAX_DELAY_COUNT_MQ : MAX_DELAY_COUNT_HTTP; + } + /// 是否还可继续延时重试(delayCount 为已完成的延时次数) - public boolean canRetry(int delayCount) { - return delayCount < MAX_DELAY_COUNT; + public boolean canRetry(String transport, int delayCount) { + return delayCount < maxDelayCount(transport); } /// 获取下一次延时间隔秒数 /// - /// @param nextDelayCount 即将执行的延时序号(1..16) - public int nextDelaySeconds(int nextDelayCount) { - return DELAY_SECONDS.getOrDefault(nextDelayCount, 6 * 60 * 60); + /// @param nextDelayCount 即将执行的延时序号 (1..max) + public int nextDelaySeconds(String transport, int nextDelayCount) { + if (NoticeTransportEnum.MQ.getCode().equals(transport)) { + return DELAY_SECONDS_MQ.getOrDefault(nextDelayCount, 60); + } + return DELAY_SECONDS_HTTP.getOrDefault(nextDelayCount, 6 * 60 * 60); } } diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeSendEngine.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeSendEngine.java index 53f21f4cb..2268b73d5 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeSendEngine.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/NoticeSendEngine.java @@ -5,7 +5,10 @@ import cn.daxpay.open.payment.trade.notice.dao.MchNoticeRecordManager; import cn.daxpay.open.payment.trade.notice.dao.MchNoticeTaskManager; import cn.daxpay.open.payment.trade.notice.entity.MchNoticeRecord; import cn.daxpay.open.payment.trade.notice.entity.MchNoticeTask; -import cn.daxpay.open.payment.trade.notice.protocol.NoticeProtocolSender; +import cn.daxpay.open.payment.trade.notice.payload.NoticeEnvelope; +import cn.daxpay.open.payment.trade.notice.payload.NoticePayloadBuilder; +import cn.daxpay.open.payment.trade.notice.transport.NoticeSendResult; +import cn.daxpay.open.payment.trade.notice.transport.NoticeTransportSender; import cn.daxpay.open.platform.core.enums.pay.notice.NoticeSendTypeEnum; import cn.hutool.core.util.StrUtil; import lombok.extern.slf4j.Slf4j; @@ -20,7 +23,7 @@ import java.util.stream.Collectors; /// # 商户出站通知发送引擎 /// -/// 唯一负责:选 Sender、写流水、更新任务、排期重试 +/// 唯一负责: 选 PayloadBuilder(按 format) 组装信封 → 选 TransportSender(按 transport) 投递 → 写流水 → 更新任务 → 排期重试 @Slf4j @Service public class NoticeSendEngine { @@ -30,21 +33,31 @@ public class NoticeSendEngine { private final NoticeRetryPolicy retryPolicy; private final NoticeTaskScheduleService scheduleService; private final PaymentContext paymentContext; - private final Map senderMap; + private final Map payloadBuilderMap; + private final Map transportSenderMap; public NoticeSendEngine(MchNoticeTaskManager taskManager, MchNoticeRecordManager recordManager, NoticeRetryPolicy retryPolicy, NoticeTaskScheduleService scheduleService, PaymentContext paymentContext, - List senders) { + List payloadBuilders, + List transportSenders) { this.taskManager = taskManager; this.recordManager = recordManager; this.retryPolicy = retryPolicy; this.scheduleService = scheduleService; this.paymentContext = paymentContext; - this.senderMap = senders.stream() - .collect(Collectors.toMap(NoticeProtocolSender::protocol, Function.identity(), (a, b) -> a)); + this.payloadBuilderMap = payloadBuilders.stream() + .collect(Collectors.toMap(NoticePayloadBuilder::format, Function.identity(), (a, b) -> { + log.warn("NoticePayloadBuilder format 冲突, 保留前者: {}", a.format()); + return a; + })); + this.transportSenderMap = transportSenders.stream() + .collect(Collectors.toMap(NoticeTransportSender::transport, Function.identity(), (a, b) -> { + log.warn("NoticeTransportSender transport 冲突, 保留前者: {}", a.transport()); + return a; + })); } /// 自动发送(消费端入口) @@ -89,7 +102,6 @@ public class NoticeSendEngine { } log.info("手动重发已成功任务: taskId={}", taskId); } - NoticeProtocolSender sender = senderMap.get(task.getProtocol()); OffsetDateTime sendTime = OffsetDateTime.now(ZoneOffset.UTC); int reqCount = (task.getSendCount() == null ? 0 : task.getSendCount()) + 1; MchNoticeRecord record = new MchNoticeRecord(); @@ -99,18 +111,37 @@ public class NoticeSendEngine { .setReqCount(reqCount) .setSendType(autoSend ? NoticeSendTypeEnum.AUTO.getCode() : NoticeSendTypeEnum.MANUAL.getCode()); - if (sender == null) { - log.error("未找到通知协议 Sender: protocol={}, taskId={}", task.getProtocol(), taskId); - record.setSuccess(false).setErrorMsg("protocol sender not found: " + task.getProtocol()); + NoticePayloadBuilder payloadBuilder = payloadBuilderMap.get(task.getFormat()); + if (payloadBuilder == null) { + log.error("未找到通知报文构建器: format={}, taskId={}", task.getFormat(), taskId); + record.setSuccess(false).setErrorMsg("payload builder not found: " + task.getFormat()); + failUpdate(task, sendTime, autoSend, record); + return; + } + NoticeTransportSender transportSender = transportSenderMap.get(task.getTransport()); + if (transportSender == null) { + log.error("未找到通知传输发送器: transport={}, taskId={}", task.getTransport(), taskId); + record.setSuccess(false).setErrorMsg("transport sender not found: " + task.getTransport()); failUpdate(task, sendTime, autoSend, record); return; } - NoticeProtocolSender.NoticeSendResult sendResult; + // 先组装信封, 再投递 (format 与 transport 正交) + NoticeEnvelope envelope; try { - sendResult = sender.send(task); + envelope = payloadBuilder.build(task); } catch (Exception e) { - log.error("出站通知 Sender 异常: taskId={}", taskId, e); + log.error("出站通知报文组装异常: taskId={}", taskId, e); + record.setSuccess(false).setErrorMsg(e.getMessage()); + failUpdate(task, sendTime, autoSend, record); + return; + } + + NoticeSendResult sendResult; + try { + sendResult = transportSender.send(task, envelope); + } catch (Exception e) { + log.error("出站通知投递异常: taskId={}", taskId, e); record.setSuccess(false).setErrorMsg(e.getMessage()); failUpdate(task, sendTime, autoSend, record); return; @@ -134,7 +165,7 @@ public class NoticeSendEngine { failUpdate(task, sendTime, autoSend, record); } - /// 失败:更新任务并按需排期重试 + /// 失败:更新任务并按需排期重试 (重试节奏按 transport 区分) private void failUpdate(MchNoticeTask task, OffsetDateTime sendTime, boolean autoSend, MchNoticeRecord record) { int reqCount = record.getReqCount() == null ? 1 : record.getReqCount(); task.setSendCount(reqCount).setLatestTime(sendTime); @@ -145,12 +176,13 @@ public class NoticeSendEngine { if (!autoSend) { task.setSuccess(false); } + String transport = task.getTransport(); if (autoSend && !task.isSuccess()) { int delayCount = task.getDelayCount() == null ? 0 : task.getDelayCount(); - if (retryPolicy.canRetry(delayCount)) { + if (retryPolicy.canRetry(transport, delayCount)) { int next = delayCount + 1; task.setDelayCount(next); - int delaySeconds = retryPolicy.nextDelaySeconds(next); + int delaySeconds = retryPolicy.nextDelaySeconds(transport, next); task.setNextTime(sendTime.plusSeconds(delaySeconds)); taskManager.updateById(task); recordManager.save(record); diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/TradeNoticeBridge.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/TradeNoticeBridge.java index b9e30e054..40e92bf85 100644 --- a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/TradeNoticeBridge.java +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/service/TradeNoticeBridge.java @@ -14,7 +14,8 @@ import cn.daxpay.open.payment.trade.enums.PayTradeTypeEnum; import cn.daxpay.open.platform.common.json.util.JacksonUtil; import cn.daxpay.open.platform.core.enums.pay.notice.NoticeContentModeEnum; import cn.daxpay.open.platform.core.enums.pay.notice.NoticeEventEnum; -import cn.daxpay.open.platform.core.enums.pay.notice.NoticeProtocolEnum; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeFormatEnum; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeTransportEnum; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -52,7 +53,7 @@ public class TradeNoticeBridge { .setBizId(order.getId()) .setBizNo(order.getOrderNo()) .setOrderNotifyUrl(order.getNotifyUrl()) - .setProtocol(NoticeProtocolEnum.SYSTEM) + .setTransport(NoticeTransportEnum.HTTP).setFormat(NoticeFormatEnum.SYSTEM) .setContentMode(NoticeContentModeEnum.SNAPSHOT) .setContentOrRef(content)); return; @@ -70,7 +71,7 @@ public class TradeNoticeBridge { .setBizId(order.getId()) .setBizNo(order.getOrderNo()) .setOrderNotifyUrl(order.getNotifyUrl()) - .setProtocol(NoticeProtocolEnum.SYSTEM) + .setTransport(NoticeTransportEnum.HTTP).setFormat(NoticeFormatEnum.SYSTEM) .setContentMode(NoticeContentModeEnum.SNAPSHOT) .setContentOrRef(content)); } @@ -88,7 +89,7 @@ public class TradeNoticeBridge { .setBizId(refundOrder.getId()) .setBizNo(refundOrder.getRefundNo()) .setOrderNotifyUrl(refundOrder.getNotifyUrl()) - .setProtocol(NoticeProtocolEnum.SYSTEM) + .setTransport(NoticeTransportEnum.HTTP).setFormat(NoticeFormatEnum.SYSTEM) .setContentMode(NoticeContentModeEnum.SNAPSHOT) .setContentOrRef(content)); } diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/transport/HttpTransportSender.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/transport/HttpTransportSender.java new file mode 100644 index 000000000..40b754b48 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/transport/HttpTransportSender.java @@ -0,0 +1,62 @@ +package cn.daxpay.open.payment.trade.notice.transport; + +import cn.daxpay.open.payment.trade.notice.entity.MchNoticeTask; +import cn.daxpay.open.payment.trade.notice.payload.NoticeEnvelope; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeTransportEnum; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.ContentType; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/// # HTTP 传输发送器 +/// +/// 按 [NoticeEnvelope].method 投递 (POST JSON / GET), ACK 规则: HTTP 2xx 且 body trim 后忽略大小写等于 SUCCESS。 +/// system 与 easy_pay 两种报文格式共用本发送器 +@Slf4j +@Component +public class HttpTransportSender implements NoticeTransportSender { + + @Override + public String transport() { + return NoticeTransportEnum.HTTP.getCode(); + } + + @Override + public NoticeSendResult send(MchNoticeTask task, NoticeEnvelope envelope) { + NoticeSendResult result = new NoticeSendResult(); + String body = null; + Integer httpStatus = null; + try { + HttpResponse response; + if ("GET".equalsIgnoreCase(envelope.getMethod())) { + response = HttpUtil.createGet(envelope.getUrl()).timeout(15000).execute(); + } else { + response = HttpUtil.createPost(envelope.getUrl()) + .body(envelope.getBody(), ContentType.JSON.getValue()) + .timeout(15000) + .execute(); + } + httpStatus = response.getStatus(); + body = response.body(); + } catch (Exception e) { + log.error("HTTP 通知发送失败, taskId={}, bizNo={}, url={}", + task.getId(), task.getBizNo(), task.getUrl(), e); + result.setRequestDigest(envelope.getRequestDigest()); + return result.setSuccess(false) + .setHttpStatus(httpStatus) + .setErrorMsg(e.getMessage()); + } + result.setRequestDigest(envelope.getRequestDigest()); + result.setHttpStatus(httpStatus); + boolean ack = httpStatus != null && httpStatus >= 200 && httpStatus < 300 + && StrUtil.equalsIgnoreCase(StrUtil.trim(body), "SUCCESS"); + result.setSuccess(ack); + if (!ack) { + result.setErrorMsg(StrUtil.blankToDefault(StrUtil.sub(body, 0, 300), + "httpStatus=" + httpStatus)); + } + return result; + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/transport/MqTransportSender.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/transport/MqTransportSender.java new file mode 100644 index 000000000..a4e045457 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/transport/MqTransportSender.java @@ -0,0 +1,43 @@ +package cn.daxpay.open.payment.trade.notice.transport; + +import cn.daxpay.open.payment.trade.notice.entity.MchNoticeTask; +import cn.daxpay.open.payment.trade.notice.payload.NoticeEnvelope; +import cn.daxpay.open.platform.common.artemis.service.ArtemisTemplateService; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeTransportEnum; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/// # MQ 传输发送器 +/// +/// 将 [NoticeEnvelope].body 发布到 task.url(Artemis Topic, 如 daxpay.notice.), +/// publish 成功即视为投递成功(ACK 语义对齐 Stripe EventBridge: 推到事件总线即完成, 消费侧失败由商户/MQ 自身负责)。 +/// 商户侧用 JMS 持久订阅消费, 离线不丢消息 +@Slf4j +@Component +@RequiredArgsConstructor +public class MqTransportSender implements NoticeTransportSender { + + private final ArtemisTemplateService artemisTemplateService; + + @Override + public String transport() { + return NoticeTransportEnum.MQ.getCode(); + } + + @Override + public NoticeSendResult send(MchNoticeTask task, NoticeEnvelope envelope) { + NoticeSendResult result = new NoticeSendResult(); + result.setRequestDigest(envelope.getRequestDigest()); + try { + // 投递到商户通知 Topic (task.url 在 MQ 方式下存 Topic 名) + artemisTemplateService.sendTopic(task.getUrl(), envelope.getBody()); + result.setSuccess(true); + } catch (Exception e) { + log.error("MQ 通知投递失败, taskId={}, bizNo={}, topic={}", + task.getId(), task.getBizNo(), task.getUrl(), e); + result.setSuccess(false).setErrorMsg(e.getMessage()); + } + return result; + } +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/transport/NoticeSendResult.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/transport/NoticeSendResult.java new file mode 100644 index 000000000..2fdd18cbe --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/transport/NoticeSendResult.java @@ -0,0 +1,24 @@ +package cn.daxpay.open.payment.trade.notice.transport; + +import lombok.Data; +import lombok.experimental.Accessors; + +/// # 商户出站通知单次发送结果 +/// +/// 由 [NoticeTransportSender] 返回, 引擎据此落流水与排期重试 +@Data +@Accessors(chain = true) +public class NoticeSendResult { + + /// 是否业务 Ack 成功 (HTTP: 2xx + body=SUCCESS; MQ: publish 成功) + private boolean success; + + /// HTTP 状态码(MQ 投递时可空) + private Integer httpStatus; + + /// 错误或非 SUCCESS 响应摘要 + private String errorMsg; + + /// 请求摘要(便于排查) + private String requestDigest; +} diff --git a/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/transport/NoticeTransportSender.java b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/transport/NoticeTransportSender.java new file mode 100644 index 000000000..a1dc5e840 --- /dev/null +++ b/daxpay-payment/daxpay-payment-core/src/main/java/cn/daxpay/open/payment/trade/notice/transport/NoticeTransportSender.java @@ -0,0 +1,17 @@ +package cn.daxpay.open.payment.trade.notice.transport; + +import cn.daxpay.open.payment.trade.notice.entity.MchNoticeTask; +import cn.daxpay.open.payment.trade.notice.payload.NoticeEnvelope; + +/// # 商户出站通知传输发送器 +/// +/// 按 [cn.daxpay.open.platform.core.enums.pay.notice.NoticeTransportEnum] 路由, +/// 负责把已组装好的 [NoticeEnvelope] 投递出去 (HTTP 回调 / MQ 推送), 与报文格式正交 +public interface NoticeTransportSender { + + /// 传输通道编码(与 NoticeTransportEnum.code 对齐: http / mq) + String transport(); + + /// 执行一次投递 + NoticeSendResult send(MchNoticeTask task, NoticeEnvelope envelope); +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_event.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_event.json index d13d7f922..181783017 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_event.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_event.json @@ -3,5 +3,9 @@ "pay.fail": "Pay Fail", "pay.close": "Pay Close", "refund.success": "Refund Success", - "refund.close": "Refund Close" + "refund.close": "Refund Close", + "pay.timeout": "Pay Timeout Close", + "pay.cancel": "Pay Cancel", + "refund.fail": "Refund Fail", + "risk.hit": "Risk Hit" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_protocol.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_format.json similarity index 100% rename from daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_protocol.json rename to daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_format.json diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_transport.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_transport.json new file mode 100644 index 000000000..629f09c8d --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/en-US/enum/notice_transport.json @@ -0,0 +1,4 @@ +{ + "http": "HTTP Callback", + "mq": "MQ Push" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_event.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_event.json index d13d7f922..0f3f0d1b9 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_event.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_event.json @@ -3,5 +3,9 @@ "pay.fail": "Pay Fail", "pay.close": "Pay Close", "refund.success": "Refund Success", - "refund.close": "Refund Close" + "refund.close": "Refund Close", + "pay.timeout": "Pembayaran Tutup Timeout", + "pay.cancel": "Pembayaran Dibatalkan", + "refund.fail": "Pengembalian Gagal", + "risk.hit": "Risiko Terdeteksi" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_protocol.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_format.json similarity index 56% rename from daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_protocol.json rename to daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_format.json index 48eee3cc2..9ae3f1a99 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_protocol.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_format.json @@ -1,4 +1,4 @@ { - "system": "System", + "system": "Sistem", "easy_pay": "EasyPay" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_transport.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_transport.json new file mode 100644 index 000000000..90fa5344d --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_transport.json @@ -0,0 +1,4 @@ +{ + "http": "Callback HTTP", + "mq": "Push MQ" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_event.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_event.json index d13d7f922..67290ebe6 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_event.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_event.json @@ -3,5 +3,9 @@ "pay.fail": "Pay Fail", "pay.close": "Pay Close", "refund.success": "Refund Success", - "refund.close": "Refund Close" + "refund.close": "Refund Close", + "pay.timeout": "支払タイムアウト終了", + "pay.cancel": "支払取消", + "refund.fail": "返金失敗", + "risk.hit": "リスク検知" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_format.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_format.json new file mode 100644 index 000000000..52764e7d3 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_format.json @@ -0,0 +1,4 @@ +{ + "system": "システムプロトコル", + "easy_pay": "EasyPayプロトコル" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_transport.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_transport.json new file mode 100644 index 000000000..a2712e1f8 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_transport.json @@ -0,0 +1,4 @@ +{ + "http": "HTTPコールバック", + "mq": "MQプッシュ" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_event.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_event.json index d13d7f922..1e6f035f9 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_event.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_event.json @@ -3,5 +3,9 @@ "pay.fail": "Pay Fail", "pay.close": "Pay Close", "refund.success": "Refund Success", - "refund.close": "Refund Close" + "refund.close": "Refund Close", + "pay.timeout": "결제 시간 초과 종료", + "pay.cancel": "결제 취소", + "refund.fail": "환불 실패", + "risk.hit": "리스크 적중" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_format.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_format.json new file mode 100644 index 000000000..78001e682 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_format.json @@ -0,0 +1,4 @@ +{ + "system": "시스템 프로토콜", + "easy_pay": "EasyPay 프로토콜" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_transport.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_transport.json new file mode 100644 index 000000000..78429ff5b --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ko-KR/enum/notice_transport.json @@ -0,0 +1,4 @@ +{ + "http": "HTTP 콜백", + "mq": "MQ 푸시" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_event.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_event.json index d13d7f922..2430e85dd 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_event.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_event.json @@ -3,5 +3,9 @@ "pay.fail": "Pay Fail", "pay.close": "Pay Close", "refund.success": "Refund Success", - "refund.close": "Refund Close" + "refund.close": "Refund Close", + "pay.timeout": "Tutup Pembayaran Tamat Masa", + "pay.cancel": "Pembayaran Dibatalkan", + "refund.fail": "Bayaran Balik Gagal", + "risk.hit": "Risiko Dikesan" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_protocol.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_format.json similarity index 56% rename from daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_protocol.json rename to daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_format.json index 48eee3cc2..9ae3f1a99 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_protocol.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_format.json @@ -1,4 +1,4 @@ { - "system": "System", + "system": "Sistem", "easy_pay": "EasyPay" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_transport.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_transport.json new file mode 100644 index 000000000..629f09c8d --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ms-MY/enum/notice_transport.json @@ -0,0 +1,4 @@ +{ + "http": "HTTP Callback", + "mq": "MQ Push" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_event.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_event.json index d13d7f922..a6c0aee3e 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_event.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_event.json @@ -3,5 +3,9 @@ "pay.fail": "Pay Fail", "pay.close": "Pay Close", "refund.success": "Refund Success", - "refund.close": "Refund Close" + "refund.close": "Refund Close", + "pay.timeout": "ปิดการชำระหมดเวลา", + "pay.cancel": "ยกเลิกการชำระ", + "refund.fail": "คืนเงินล้มเหลว", + "risk.hit": "ตรวจพบความเสี่ยง" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_protocol.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_format.json similarity index 50% rename from daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_protocol.json rename to daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_format.json index 48eee3cc2..21b9f2291 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/id-ID/enum/notice_protocol.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_format.json @@ -1,4 +1,4 @@ { - "system": "System", + "system": "ระบบ", "easy_pay": "EasyPay" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_protocol.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_protocol.json deleted file mode 100644 index 48eee3cc2..000000000 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_protocol.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "system": "System", - "easy_pay": "EasyPay" -} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_transport.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_transport.json new file mode 100644 index 000000000..629f09c8d --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/th-TH/enum/notice_transport.json @@ -0,0 +1,4 @@ +{ + "http": "HTTP Callback", + "mq": "MQ Push" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_event.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_event.json index d13d7f922..69dc986c6 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_event.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_event.json @@ -3,5 +3,9 @@ "pay.fail": "Pay Fail", "pay.close": "Pay Close", "refund.success": "Refund Success", - "refund.close": "Refund Close" + "refund.close": "Refund Close", + "pay.timeout": "Đóng thanh toán hết hạn", + "pay.cancel": "Hủy thanh toán", + "refund.fail": "Hoàn tiền thất bại", + "risk.hit": "Rủi ro phát hiện" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_protocol.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_format.json similarity index 50% rename from daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_protocol.json rename to daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_format.json index 48eee3cc2..bf4b12a3a 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/ja-JP/enum/notice_protocol.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_format.json @@ -1,4 +1,4 @@ { - "system": "System", + "system": "Hệ thống", "easy_pay": "EasyPay" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_protocol.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_protocol.json deleted file mode 100644 index 48eee3cc2..000000000 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_protocol.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "system": "System", - "easy_pay": "EasyPay" -} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_transport.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_transport.json new file mode 100644 index 000000000..47efdbf16 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/vi-VN/enum/notice_transport.json @@ -0,0 +1,4 @@ +{ + "http": "Callback HTTP", + "mq": "Đẩy MQ" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_event.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_event.json index da90e149f..b97585b1d 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_event.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_event.json @@ -3,5 +3,9 @@ "pay.fail": "支付失败", "pay.close": "支付关闭", "refund.success": "退款成功", - "refund.close": "退款关闭" + "refund.close": "退款关闭", + "pay.timeout": "支付超时关闭", + "pay.cancel": "支付撤销", + "refund.fail": "退款失败", + "risk.hit": "风控命中" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_protocol.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_format.json similarity index 100% rename from daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_protocol.json rename to daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_format.json diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_transport.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_transport.json new file mode 100644 index 000000000..6c82318f4 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-CN/enum/notice_transport.json @@ -0,0 +1,4 @@ +{ + "http": "HTTP回调", + "mq": "MQ推送" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_event.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_event.json index bbdf62942..fad568389 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_event.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_event.json @@ -3,5 +3,9 @@ "pay.fail": "支付失敗", "pay.close": "支付關閉", "refund.success": "退款成功", - "refund.close": "退款關閉" + "refund.close": "退款關閉", + "pay.timeout": "支付逾時關閉", + "pay.cancel": "支付撤銷", + "refund.fail": "退款失敗", + "risk.hit": "風控命中" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_format.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_format.json new file mode 100644 index 000000000..bff89fba8 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_format.json @@ -0,0 +1,4 @@ +{ + "system": "系統協議", + "easy_pay": "易支付協議" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_protocol.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_protocol.json deleted file mode 100644 index 672f53303..000000000 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_protocol.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "system": "系统协议", - "easy_pay": "易支付协议" -} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_transport.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_transport.json new file mode 100644 index 000000000..3c5ad8716 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-HK/enum/notice_transport.json @@ -0,0 +1,4 @@ +{ + "http": "HTTP回調", + "mq": "MQ推送" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_event.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_event.json index bbdf62942..fad568389 100644 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_event.json +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_event.json @@ -3,5 +3,9 @@ "pay.fail": "支付失敗", "pay.close": "支付關閉", "refund.success": "退款成功", - "refund.close": "退款關閉" + "refund.close": "退款關閉", + "pay.timeout": "支付逾時關閉", + "pay.cancel": "支付撤銷", + "refund.fail": "退款失敗", + "risk.hit": "風控命中" } diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_format.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_format.json new file mode 100644 index 000000000..bff89fba8 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_format.json @@ -0,0 +1,4 @@ +{ + "system": "系統協議", + "easy_pay": "易支付協議" +} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_protocol.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_protocol.json deleted file mode 100644 index 672f53303..000000000 --- a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_protocol.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "system": "系统协议", - "easy_pay": "易支付协议" -} diff --git a/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_transport.json b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_transport.json new file mode 100644 index 000000000..261a58ef5 --- /dev/null +++ b/daxpay-platform/daxpay-platform-common/common-i18n/src/main/resources/i18n/zh-TW/enum/notice_transport.json @@ -0,0 +1,4 @@ +{ + "http": "HTTP回呼", + "mq": "MQ推送" +} diff --git a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/CallbackNoticeTypeEnum.java b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/CallbackNoticeTypeEnum.java index 4d5a967c6..dc361a84f 100644 --- a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/CallbackNoticeTypeEnum.java +++ b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/CallbackNoticeTypeEnum.java @@ -4,9 +4,9 @@ import cn.daxpay.open.platform.core.i18n.I18nSupport; import lombok.Getter; import lombok.RequiredArgsConstructor; -/// # 回调通知类型(已演进为 [NoticeProtocolEnum]) +/// # 回调通知类型(已演进为 [NoticeFormatEnum]) /// -/// @deprecated 使用 [NoticeProtocolEnum] +/// @deprecated 使用 [NoticeFormatEnum] @Deprecated @Getter @RequiredArgsConstructor diff --git a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeEventEnum.java b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeEventEnum.java index 4165d4715..4326f393b 100644 --- a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeEventEnum.java +++ b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeEventEnum.java @@ -16,14 +16,20 @@ public enum NoticeEventEnum implements I18nSupport { PAY_SUCCESS("pay.success"), /// 支付失败 PAY_FAIL("pay.fail"), - /// 支付关闭 + /// 支付关闭(主动关单, 业务单容器态 CLOSED) PAY_CLOSE("pay.close"), + /// 支付超时关闭(业务单容器态 EXPIRED, 区别于主动关单) + PAY_TIMEOUT("pay.timeout"), + /// 支付撤销(资金态 CANCEL 终态) + PAY_CANCEL("pay.cancel"), /// 退款成功 REFUND_SUCCESS("refund.success"), /// 退款失败 REFUND_FAIL("refund.fail"), /// 退款关闭 REFUND_CLOSE("refund.close"), + /// 风控命中(黑名单/海外 IP 等规则触发) + RISK_HIT("risk.hit"), ; /// 编码 diff --git a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeFormatEnum.java b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeFormatEnum.java new file mode 100644 index 000000000..7910cb280 --- /dev/null +++ b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeFormatEnum.java @@ -0,0 +1,30 @@ +package cn.daxpay.open.platform.core.enums.pay.notice; + +import cn.daxpay.open.platform.core.i18n.I18nSupport; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/// # 商户出站通知报文格式 +/// +/// 字典: notice_format +/// 仅描述报文如何组装(JSON / GET query 等)与签名方式, 与传输通道 [NoticeTransportEnum] 正交 +/// 演进自原 [NoticeProtocolEnum], 拆分后 protocol 不再一维承担「报文格式 + 传输通道 + 路由」三重职责 +@Getter +@RequiredArgsConstructor +public enum NoticeFormatEnum implements I18nSupport { + + /// 标准 DaxPay 签名 JSON 报文 + SYSTEM("system"), + /// 易支付兼容协议 GET query 报文 + EASY_PAY("easy_pay"), + ; + + /// 编码 + private final String code; + + /// 翻译 key 前缀 + @Override + public String getI18nPrefix() { + return "enum.notice_format"; + } +} diff --git a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeProtocolEnum.java b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeProtocolEnum.java deleted file mode 100644 index 3010e3b8d..000000000 --- a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeProtocolEnum.java +++ /dev/null @@ -1,29 +0,0 @@ -package cn.daxpay.open.platform.core.enums.pay.notice; - -import cn.daxpay.open.platform.core.i18n.I18nSupport; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -/// # 商户出站通知协议 -/// -/// 字典: notice_protocol -/// 演进自原 CallbackNoticeTypeEnum,表示报文协议而非「回调/订阅」双轨 -@Getter -@RequiredArgsConstructor -public enum NoticeProtocolEnum implements I18nSupport { - - /// 标准 DaxPay 签名 JSON 回调 - SYSTEM("system"), - /// 易支付兼容协议 GET 回调 - EASY_PAY("easy_pay"), - ; - - /// 编码 - private final String code; - - /// 翻译 key 前缀 - @Override - public String getI18nPrefix() { - return "enum.notice_protocol"; - } -} diff --git a/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeTransportEnum.java b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeTransportEnum.java new file mode 100644 index 000000000..be7240785 --- /dev/null +++ b/daxpay-platform/daxpay-platform-core/src/main/java/cn/daxpay/open/platform/core/enums/pay/notice/NoticeTransportEnum.java @@ -0,0 +1,30 @@ +package cn.daxpay.open.platform.core.enums.pay.notice; + +import cn.daxpay.open.platform.core.i18n.I18nSupport; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/// # 商户出站通知传输通道 +/// +/// 字典: notice_transport +/// 与报文格式 [NoticeFormatEnum] 正交: 决定通知如何投递(HTTP 回调 / MQ 推送), +/// 报文内容由 format 决定, 二者组合如 `http+system` / `mq+system` +@Getter +@RequiredArgsConstructor +public enum NoticeTransportEnum implements I18nSupport { + + /// HTTP 异步回调(POST JSON 或 GET query, 由 format 决定) + HTTP("http"), + /// MQ 推送(发布到 Artemis Topic, 商户自行订阅消费) + MQ("mq"), + ; + + /// 编码 + private final String code; + + /// 翻译 key 前缀 + @Override + public String getI18nPrefix() { + return "enum.notice_transport"; + } +} diff --git a/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/notice/EasyPayNoticeSender.java b/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/notice/EasyPayPayloadBuilder.java similarity index 56% rename from daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/notice/EasyPayNoticeSender.java rename to daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/notice/EasyPayPayloadBuilder.java index 02f9f29ef..f827cc558 100644 --- a/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/notice/EasyPayNoticeSender.java +++ b/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/notice/EasyPayPayloadBuilder.java @@ -1,9 +1,10 @@ package cn.daxpay.open.plugin.easypay.notice; import cn.daxpay.open.payment.trade.notice.entity.MchNoticeTask; -import cn.daxpay.open.payment.trade.notice.protocol.NoticeProtocolSender; +import cn.daxpay.open.payment.trade.notice.payload.NoticeEnvelope; +import cn.daxpay.open.payment.trade.notice.payload.NoticePayloadBuilder; import cn.daxpay.open.platform.common.json.util.JacksonUtil; -import cn.daxpay.open.platform.core.enums.pay.notice.NoticeProtocolEnum; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeFormatEnum; import cn.daxpay.open.plugin.easypay.dao.EasyPayOrderManager; import cn.daxpay.open.plugin.easypay.entity.EasyPayOrder; import cn.daxpay.open.plugin.easypay.enums.EasyPayApiVersionEnum; @@ -13,8 +14,6 @@ import cn.daxpay.open.plugin.easypay.service.config.EasyPayCredentialService; import cn.daxpay.open.plugin.easypay.util.EasyPayUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.URLUtil; -import cn.hutool.http.HttpResponse; -import cn.hutool.http.HttpUtil; import cn.hutool.json.JSONUtil; import tools.jackson.core.type.TypeReference; import lombok.RequiredArgsConstructor; @@ -27,14 +26,14 @@ import java.time.format.DateTimeFormatter; import java.util.Objects; import java.util.TreeMap; -/// # 易支付协议出站发送器 +/// # 易支付协议报文构建器 /// -/// content_mode=ref:content 存 EasyPayOrder.id;发送时实时组装 V1/V2 GET 回调 -/// Ack:HTTP 2xx 且 body=SUCCESS(忽略大小写) +/// content_mode=ref: content 存 EasyPayOrder.id; 构建时实时组装 V1/V2 GET 回调信封 (含签名)。 +/// HTTP 投递与 ACK 判定由 [cn.daxpay.open.payment.trade.notice.transport.HttpTransportSender] 统一处理 @Slf4j @Component @RequiredArgsConstructor -public class EasyPayNoticeSender implements NoticeProtocolSender { +public class EasyPayPayloadBuilder implements NoticePayloadBuilder { private static final DateTimeFormatter NORM = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.of("Asia/Shanghai")); @@ -43,28 +42,35 @@ public class EasyPayNoticeSender implements NoticeProtocolSender { private final EasyPayCredentialService easyPayCredentialService; @Override - public String protocol() { - return NoticeProtocolEnum.EASY_PAY.getCode(); + public String format() { + return NoticeFormatEnum.EASY_PAY.getCode(); } @Override - public NoticeSendResult send(MchNoticeTask task) { - NoticeSendResult result = new NoticeSendResult(); + public NoticeEnvelope build(MchNoticeTask task) { Long easyPayOrderId = JSONUtil.parseObj(task.getContent()).getLong("id"); if (easyPayOrderId == null) { - return result.setSuccess(false).setErrorMsg("easy pay ref missing id"); + throw new IllegalStateException("easy pay ref missing id"); } EasyPayOrder order = easyPayOrderManager.findByIdNotTenant(easyPayOrderId).orElse(null); if (order == null) { - return result.setSuccess(false).setErrorMsg("easy pay order not found: " + easyPayOrderId); + throw new IllegalStateException("easy pay order not found: " + easyPayOrderId); } - if (Objects.equals(order.getApiVersion(), EasyPayApiVersionEnum.V1.getCode())) { - return sendV1(task, order, result); - } - return sendV2(task, order, result); + Object callback = Objects.equals(order.getApiVersion(), EasyPayApiVersionEnum.V1.getCode()) + ? buildV1(order) : buildV2(order); + // 转 TreeMap 拼接 query (按 key 排序) + TreeMap map = JacksonUtil.toBean(JacksonUtil.toJson(callback), + new TypeReference>() {}); + String query = URLUtil.buildQuery(map, StandardCharsets.UTF_8); + String baseUrl = task.getUrl(); + String fullUrl = baseUrl.contains("?") ? baseUrl + "&" + query : baseUrl + "?" + query; + return new NoticeEnvelope() + .setMethod("GET") + .setUrl(fullUrl) + .setRequestDigest(StrUtil.sub(fullUrl, 0, 500)); } - private NoticeSendResult sendV1(MchNoticeTask task, EasyPayOrder order, NoticeSendResult result) { + private EasyPayCallbackV1Result buildV1(EasyPayOrder order) { var credential = easyPayCredentialService.getAndCheck(order.getPid()); var callback = new EasyPayCallbackV1Result() .setPid(order.getPid()) @@ -78,10 +84,10 @@ public class EasyPayNoticeSender implements NoticeProtocolSender { .setSignType("MD5"); // 仅一次 MD5 签名(修复商业版重复 setSign) callback.setSign(EasyPayUtil.signByMd5(callback, credential.getMd5Key())); - return doGet(task.getUrl(), callback, result); + return callback; } - private NoticeSendResult sendV2(MchNoticeTask task, EasyPayOrder order, NoticeSendResult result) { + private EasyPayCallbackV2Result buildV2(EasyPayOrder order) { var credential = easyPayCredentialService.getAndCheck(order.getPid()); var callback = new EasyPayCallbackV2Result() .setPid(order.getPid()) @@ -99,32 +105,6 @@ public class EasyPayNoticeSender implements NoticeProtocolSender { .setTimestamp(String.valueOf(System.currentTimeMillis() / 1000)) .setSignType("RSA"); callback.setSign(EasyPayUtil.signByRsa(callback, credential.getPlatformPrivateKey())); - return doGet(task.getUrl(), callback, result); - } - - private NoticeSendResult doGet(String baseUrl, Object callback, NoticeSendResult result) { - String body = null; - Integer httpStatus = null; - try { - TreeMap map = JacksonUtil.toBean(JacksonUtil.toJson(callback), - new TypeReference>() {}); - String query = URLUtil.buildQuery(map, StandardCharsets.UTF_8); - String fullUrl = baseUrl.contains("?") ? baseUrl + "&" + query : baseUrl + "?" + query; - result.setRequestDigest(StrUtil.sub(fullUrl, 0, 500)); - HttpResponse response = HttpUtil.createGet(fullUrl).timeout(15000).execute(); - httpStatus = response.getStatus(); - body = response.body(); - } catch (Exception e) { - log.error("易支付通知发送失败, url={}", baseUrl, e); - return result.setSuccess(false).setHttpStatus(httpStatus).setErrorMsg(e.getMessage()); - } - result.setHttpStatus(httpStatus); - boolean ack = httpStatus != null && httpStatus >= 200 && httpStatus < 300 - && StrUtil.equalsIgnoreCase(StrUtil.trim(body), "SUCCESS"); - result.setSuccess(ack); - if (!ack) { - result.setErrorMsg(StrUtil.blankToDefault(StrUtil.sub(body, 0, 300), "httpStatus=" + httpStatus)); - } - return result; + return callback; } } diff --git a/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/strategy/EasyPayPluginStrategy.java b/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/strategy/EasyPayPluginStrategy.java index fe5afd1d2..053102178 100644 --- a/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/strategy/EasyPayPluginStrategy.java +++ b/daxpay-plugin/daxpay-plugin-easypay/src/main/java/cn/daxpay/open/plugin/easypay/strategy/EasyPayPluginStrategy.java @@ -8,7 +8,8 @@ import cn.daxpay.open.payment.trade.order.entity.PayTrade; import cn.daxpay.open.platform.common.json.util.JacksonUtil; import cn.daxpay.open.platform.core.enums.pay.notice.NoticeContentModeEnum; import cn.daxpay.open.platform.core.enums.pay.notice.NoticeEventEnum; -import cn.daxpay.open.platform.core.enums.pay.notice.NoticeProtocolEnum; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeFormatEnum; +import cn.daxpay.open.platform.core.enums.pay.notice.NoticeTransportEnum; import cn.daxpay.open.platform.core.enums.pay.trade.TradeSourceEnum; import cn.daxpay.open.plugin.easypay.entity.EasyPayOrder; import cn.daxpay.open.plugin.easypay.service.order.EasyPayOrderService; @@ -82,7 +83,8 @@ public class EasyPayPluginStrategy implements AbsPayPluginStrategy { .setEvent(NoticeEventEnum.PAY_SUCCESS.getCode()) .setBizId(easyPayOrder.getId()) .setBizNo(easyPayOrder.getTradeNo() != null ? easyPayOrder.getTradeNo() : easyPayOrder.getOutTradeNo()) - .setProtocol(NoticeProtocolEnum.EASY_PAY) + .setTransport(NoticeTransportEnum.HTTP) + .setFormat(NoticeFormatEnum.EASY_PAY) .setContentMode(NoticeContentModeEnum.REF) .setContentOrRef(content) .setProtocolNotifyUrl(easyPayOrder.getNotifyUrl()));