避免重复付款:幂等性如何保证一个订单只对应一笔付款
超时、重复点击和自动重试都可能让一个订单产生两笔付款。本文说明 merchant_reference 和 Idempotency-Key 请求头如何让每个 PAYWISE 请求都可以安全地重发。
发布于 07 十月 2026 阅读约 4 分钟
网络从来不是完美的。您的服务器发送请求创建付款,连接突然断开,您没有收到任何回复。付款到底创建了没有?如果直接重试,可能会出现一个订单两笔付款,顾客甚至可能付两次钱。
解决办法的名字有点长,但道理很简单:幂等性。如果一个请求发送两次和发送一次的效果相同,它就是幂等的。

重复从哪里来
- 超时。 请求已到达 PAYWISE,但回复没有回到您这里。
- 自动重试。 您的 HTTP 库或任务队列在出错后自动再试。
- 重复点击。 顾客按了两次"付款",或刷新了结账页面。
- 两台服务器。 两个进程同时处理同一个订单。
付款:您的订单号就是钥匙
创建交易时,您会发送 merchant_reference:您自己的订单号。PAYWISE 用它来识别重复请求:
| 您发送 | PAYWISE 回复 |
|---|---|
新的 merchant_reference |
201 Created,新交易 |
相同的 merchant_reference,内容相同 |
200 OK,返回原来的交易,不会新建 |
相同的 merchant_reference,内容不同(例如金额不同) |
409 DUPLICATE_REFERENCE |
所以超时之后,您可以放心地再次发送完全相同的请求:
curl -X POST https://paywise.asia/api/v1/transactions \
-H "Authorization: Bearer pw_test_..." \
-H "Content-Type: application/json" \
-d '{"merchant_reference":"ORDER-1042","customer_id":"user-42","amount":150000,"payment_method":"QRIS"}'
每个环境(沙盒和正式)各自记录订单号,因此测试不会占用正式环境的订单号。
什么时候使用新的订单号
有一个例外。如果 PAYWISE 返回 502 PROVIDER_ERROR 或 504 PROVIDER_TIMEOUT,该交易已被标记为失败,错误信息也会说明这一点。请使用新的 merchant_reference 重试,例如 ORDER-1042-2,并在您自己的数据库中保存它与订单的关联。已过期的付款也一样:请新建付款,不要重复使用旧的订单号。
提现:Idempotency-Key 请求头
资金转出同样需要这种保护。POST /api/v1/withdrawals 必须带 Idempotency-Key 请求头:8 到 64 个字符(字母、数字和 . _ : -),每笔提现唯一。
curl -X POST https://paywise.asia/api/v1/withdrawals \
-H "Authorization: Bearer pw_live_..." \
-H "Idempotency-Key: payout-2026-10-07-0001" \
-H "Content-Type: application/json" \
-d '{"payment_method":"USDT","amount":250,"wallet_address":"0x...","network":"BEP20"}'
- 相同的 key 和相同的内容,返回原来的提现(
200)。 - 相同的 key 但内容不同,返回
409 DUPLICATE_REQUEST。 409 WITHDRAWAL_BUSY表示同一时间有另一笔提现正在创建:稍等片刻,然后用同一个 key 重试。
请在第一次发送之前生成 key,并与这笔提现一起保存在数据库中。每次重试都重新生成的 key 起不到任何保护作用。
Webhook:您这边也要幂等
幂等性是双向的。如果您的服务器没有响应 webhook,PAYWISE 会重新发送,所以同一个 payment.paid 事件可能到达不止一次。请让您的处理程序可以安全地重复执行:
- 验证签名(方法)。
- 按
merchant_reference找到订单。 - 如果订单已经付款,回复
200并结束。不要重复发货或重复发邮件。 - 否则在一个数据库事务中标记为已付款,然后回复
200。
检查清单
- 使用真实订单号作为
merchant_reference,不要每次请求都用随机值。 - 超时和网络错误时,用相同的请求重试。
- 只有在付款失败或过期后才使用新的订单号。
- 发送提现前先保存它的
Idempotency-Key。 - 让 webhook 处理程序忽略已经处理过的事件。

