支付回调

支付成功后,网关向商户配置的回调地址发送 POST 请求。回调是入账的唯一依据。 用户可能付完款就关掉页面,所以页面跳转、前端轮询结果都不能用来入账。

接收回调

Node.js(Express)
Python(FastAPI)
// 签名针对请求体原文,webhook 路由必须挂 express.raw()(不要全局挂 express.json())
app.post('/wantu/webhook', express.raw({ type: '*/*' }), async (req, res) => {
  let event
  try {
    event = pay.webhook.verify({ headers: req.headers, rawBody: req.body.toString() })
  } catch (err) {
    return res.sendStatus(400)   // 验签失败:不处理业务,网关会重试
  }

  if (event.type === 'order.paid') {
    const data = event.data
    // 入账三件套:查到本地订单 → 金额一致 → 事务内幂等入账
    // UPDATE orders SET status='paid' WHERE out_trade_no=? AND status='created'
    // 更新行数为 1 才执行加积分;为 0 说明是重复回调,直接返回 200
  }
  res.sendStatus(200)
})

各框架取原始请求体的方式:Express 用 express.raw()(并且不要全局挂 express.json(), 会提前吃掉原文),Koa 用 raw-body,FastAPI 用 await request.body(), Next.js App Router 用 await request.text(),Nest 开启 rawBody: true

处理步骤

  1. 验签。verify 抛错时返回 400,不处理业务
  2. 核对金额。事件里的 amount 必须与本地订单金额一致
  3. 幂等入账。以 out_trade_no 建唯一约束,重复回调直接返回 200
  4. 返回 200。耗时逻辑先落库再异步处理,不要让回调等待

事件类型

事件类型触发时机data 主要字段
order.paid支付成功order_idout_trade_noamountpaid_atchannel_trade_noattach
refund.succeeded退款成功refund_idout_trade_noout_refund_noamountrefunded_at
refund.failed退款终态失败(商户后台审批拒绝 / 14 天超时自动拒绝 / 渠道失败),资金未动refund_idout_trade_noout_refund_noamount

订单关闭不推送回调,需要时用查单接口获取。收到 refund.failed 表示这笔退款不会到账, 处理方式见查单与退款

重试机制

回调返回非 2xx 或 10 秒超时,网关按 1 分钟、5 分钟、10 分钟、30 分钟、1 小时、 3 小时、6 小时、12 小时的间隔重试,共 8 次。

这意味着同一事件一定会重复送达,入账幂等不是可选项。全部重试失败的事件顽兔侧 会告警处理,商户也可以随时用查单接口核对状态。

验签规则

用 SDK 时不需要关心本节。自行验签的参考:

  • 请求头:X-Wantu-Timestamp(秒级时间戳)、X-Wantu-NonceX-Wantu-Signature
  • 签名串三行拼接:timestamp + "\n" + nonce + "\n" + 请求体原文
  • 算法:HMAC-SHA256,输出 hex 小写
  • 密钥:配置了 webhook_secret 用它,否则用 api_secret
  • 时间戳与当前时间相差超过 300 秒应拒绝