异步回调
概述
当订单状态发生变更时(如支付成功、支付失败、关闭、退款成功),DaxPay 系统会主动向商户发送异步通知。通知的「怎么投递」与「报文长什么样」是两个正交维度:传输通道(HTTP 回调 / MQ 推送)× 报文格式(系统协议 / 易支付协议)。
传输通道与报文格式
传输通道(transport) —— 决定通知如何投递:
| transport | 投递方式 | ACK 语义 | 重试 |
|---|---|---|---|
http | HTTP 请求(POST JSON 或 GET query,由报文格式决定) | HTTP 2xx 且响应体 trim 后为 SUCCESS | 16 次递增重试 |
mq | 发布到 Artemis Topic daxpay.notice.<appId> | publish 成功即视为投递成功 | 3 次短间隔重试 |
报文格式(format) —— 决定报文如何组装与签名:
| format | 适用场景 | 请求方式 | 报文格式 | 验签方式 |
|---|---|---|---|---|
system | 标准支付 API(/unipay/*)发起的订单 | POST | JSON body | RSA(SHA256withRSA) |
easy_pay | 易支付插件(/epay/*)发起的订单 | GET | URL query 参数 | MD5 / RSA(取决于 V1/V2) |
easy_pay 格式是 GET 请求
易支付兼容格式(format=easy_pay)的回调走 GET + URL query 参数,不是 POST JSON。对接易支付时需用 request.getParameter() 接收,而非 request.getReader()。
通知流程(system 协议)
- 商户在发起支付请求时传入
notifyUrl参数。 - 系统处理完业务后,向
notifyUrl发送 POST 请求(JSON 格式)。 - 商户系统接收通知,先验签,再处理业务逻辑。
- 商户系统处理后,返回
SUCCESS字符串(大小写不敏感,前后空格会被 trim)。 - 如果商户系统返回非
SUCCESS或 HTTP 状态非 2xx 或超时未响应,系统按策略重试。
应用级订阅(并行通知)
除订单级 notifyUrl(每笔订单传入)外,商户还可在「异步通知配置」中为应用配置统一通知(HTTP 回调地址或 MQ 推送)+ 订阅事件。同一业务事件会并行生成订单级与应用级两条独立通知任务:
- 订单级(source=order):走支付请求传入的
notifyUrl,恒http + system - 应用级(source=app):走应用配置,按
notifyWay(http/mq)决定传输通道
可能收到两份通知
若商户既在订单传了 notifyUrl,又配置了应用级订阅,同一事件会分别发到订单级 URL 和应用级地址(两份独立通知,各自重试)。这是预期行为(双轨并行),便于商户同时推送到对账系统与业务系统。仅订单级 URL 为空 / 应用级未配置时才单发一路。
应用级配置见管理端「应用管理 → 异步通知配置」。
重试策略
固定 16 次重试,间隔递增(实现于 NoticeRetryPolicy,通过 Artemis 延时队列调度):
| 重试次数 | 间隔 | 累计耗时 |
|---|---|---|
| 1 | 15s | 15s |
| 2 | 15s | 30s |
| 3 | 30s | 1min |
| 4 | 3min | 4min |
| 5 | 10min | 14min |
| 6 | 20min | 34min |
| 7~9 | 30min × 3 | ~2h4min |
| 10 | 60min | ~3h4min |
| 11~13 | 3h × 3 | ~12h4min |
| 14~16 | 6h × 3 | ~30h4min |
- 最大重试次数:16 次(超过后停止,标记为发送失败)
- 仅自动发送路径会重试:商户在管理端「手动重发通知」失败后不会自动重试,需再次手动触发
- ACK 判定:HTTP 状态 2xx 且 响应体 trim 后忽略大小写等于
SUCCESS,两者缺一即视为失败并重试
MQ 推送方式的重试
transport=mq 时仅当 publish 到 Topic 失败(broker 故障/网络异常)才重试 3 次,间隔 10s / 30s / 60s;消费侧消费失败由商户/MQ 自身机制负责(建议使用 JMS 持久订阅,离线不丢消息)。
MQ 推送方式
当应用「异步通知配置」选择 notifyWay=mq 时,平台将通知消息(system 格式的 DaxNoticeResult JSON,含 RSA 签名)发布到按应用隔离的 Topic,商户自行订阅消费。
- Topic 命名:
daxpay.notice.<appId>,每个应用独立隔离 - 消息体:与 HTTP 回调完全相同的 DaxNoticeResult JSON,验签逻辑一致
- ACK 语义:publish 成功即视为投递完成(对齐 Stripe EventBridge 模型,推到事件总线即完成)
- broker 要求:Artemis address 需配为 multicast 路由类型,商户侧用持久订阅(Durable Subscriber)
Java JMS 消费示例:
@JmsListener(
destination = "daxpay.notice.APP001", // 对应 appId
containerFactory = "topicListenerFactory", // pubSubDomain=true 的 Topic 监听工厂
subscription = "daxpay-notify-app001" // 持久订阅名(同一 appId 多实例去重)
)
public void onNotice(String body) {
// body 为 DaxNoticeResult JSON, 验签逻辑与 HTTP 回调完全一致
// 参见下方「签名验证」
}回调消息格式
请求方式:POST Content-Type:application/json
报文结构(DaxNoticeResult)
回调报文继承 DaxResult,增加事件与商户字段:
| 参数 | 类型 | 描述 |
|---|---|---|
| event | string | 通知事件码(见下方事件类型) |
| protocol | string | 通知协议(system / easy_pay) |
| mchNo | string | 商户号 |
| appId | string | 应用 ID |
| code | int | 状态码(0 = 成功) |
| msg | string | 提示信息 |
| data | object | 业务数据(订单/退款单快照) |
| sign | string | 平台 RSA 签名(Base64) |
| resTime | string | 通知时间(北京时间,yyyy-MM-dd HH:mm:ss) |
| reqId | string | 请求 ID |
事件类型(NoticeEventEnum)
| event | 说明 |
|---|---|
| pay.success | 支付成功 |
| pay.fail | 支付失败 |
| pay.close | 支付关闭(主动关单) |
| pay.timeout | 支付超时关闭(业务单超时未付自动关闭) |
| pay.cancel | 支付撤销(资金态 CANCEL) |
| refund.success | 退款成功 |
| refund.fail | 退款失败 |
| refund.close | 退款关闭 |
| transfer.success | 转账成功 |
| transfer.fail | 转账失败 |
| transfer.close | 转账关闭 |
| alloc.success | 分账成功 |
| alloc.fail | 分账失败 |
| risk.hit | 风控命中(黑名单/海外 IP 等规则触发) |
转账、分账通知分别取对应业务单携带的通知地址(分账请求时传入的
notifyUrl),并同样支持应用级订阅;风控事件(risk.hit)通过应用级订阅接收。分账通知快照含明细列表。
回调示例:支付成功
{
"event": "pay.success",
"protocol": "system",
"mchNo": "M200000001",
"appId": "APP001",
"code": 0,
"msg": "success",
"data": {
"orderNo": "P2024120112345700001",
"bizOrderNo": "ORDER20241201001",
"tradeNo": "T2024120112345700001",
"amount": 100,
"realAmount": 100,
"status": "success",
"method": "wechat_qr",
"payTime": "2024-12-01 12:00:00",
"attach": "{\"orderId\": 123}"
},
"sign": "Base64签名值",
"resTime": "2024-12-01 12:00:00",
"reqId": "NTF20241201001"
}回调示例:退款成功
{
"event": "refund.success",
"protocol": "system",
"mchNo": "M200000001",
"appId": "APP001",
"code": 0,
"msg": "success",
"data": {
"refundNo": "R2024120112345700001",
"bizRefundNo": "REFUND20241201001",
"bizOrderNo": "ORDER20241201001",
"amount": 100,
"status": "success",
"finishTime": "2024-12-01 14:00:00"
},
"sign": "Base64签名值",
"resTime": "2024-12-01 14:00:00",
"reqId": "NTF20241201002"
}签名验证
商户接收回调后,必须验证 sign 字段,确保消息由 DaxPay 平台发出且未被篡改。
验签步骤
- 获取回调 JSON 中的所有字段。
- 移除
sign字段。 - 排除值为空的字段。
- 将剩余字段按 key 的 ASCII 码升序排列。
- 拼接为
key1=value1&key2=value2格式。 - 使用平台公钥以
SHA256withRSA验签。
先验签再处理业务
务必先验签通过后再处理业务逻辑,避免伪造通知导致数据不一致。
验签代码示例参见 签名机制。
商户响应
验签通过且业务处理成功后,商户系统应返回:
- HTTP 状态码:2xx(200-299)
- 响应体:
SUCCESS(大小写不敏感,前后空格会被自动 trim)
SUCCESS返回非 SUCCESS 或 HTTP 非 2xx 或超时(15 秒),系统将按上述 重试策略 重新发送通知。