PayZuDocs
最佳实践

说明如何用 clientReference 避免重复调用造成重复扣款或重复入账。

幂等性是指多次调用同一操作与只调用一次具有相同效果的保证。没有它,重试会变成重复扣款、重复入账和丢失退款。

需要幂等性的场景

场景无幂等性有幂等性
应用在 POST /pix 后崩溃,但不知道是否送达生成 2 笔扣款PayZu 返回已存在的那笔
POST /pix 超时,但 QR 已生成客户看到 2 个不同的 QRPayZu 返回同一笔交易
重试 job 触发同一笔扣款 2 次2 笔扣款,客服困扰1 笔扣款,客户正常支付
同一 callback 到达 2 次(超时后重试)订单标记已付款 2 次忽略重复的那次
交易经历 PENDING → COMPLETED → REFUNDED可能忽略退款每次状态转换只处理一次

创建时的 clientReference

clientReference在创建扣款、Pix 付款或转账时定义的外部幂等标识符。PayZu 按账户 + clientReference 去重:同一个 key 只会在您自己的账户内冲突,如果交易已创建,则返回已存在的那笔。

如何生成

模式何时使用
order-{orderId}每个订单 1 笔扣款。推荐。
payout-{payoutId}每次申请 1 笔 Pix 付款。
subscription-{subId}-{period}周期性扣款(每个周期 1 笔)。
retry-{orderId}-{attempt}当您需要在彻底失败后强制发起新扣款时。
transfer-{from}-{to}-{date}按天幂等的内部转账。

绝对不要使用 Date.now()uuid() 或其他随机值作为 clientReference。重试会生成不同的值,PayZu 会创建重复扣款,这正好破坏了您想要的那个保证。

curl -X POST https://api.payzu.processamento.com/v1/pix \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 99.90,
    "clientReference": "order-1234",
    "callbackUrl": "https://seusite.com.br/webhooks/payzu"
  }'

其他语言的相同请求请见教程接收 Pix 付款

callback 去重

同一 callback 可能多次到达:

去重键不能只用 id:那样会因为已经看过 COMPLETED 而忽略 REFUNDED 的 callback,退款就不会入账。请按投递来源构造去重键:

  • 注册的 webhook:使用 id 加上 X-Callback-Event header。有三个事件(TRANSACTION_SUSPECTED_FRAUDTRANSACTION_SUSPECTED_FRAUD_REVERSALINFRACTION_CHANGED)不改变交易 status,若用 id + status 会把这些投递当成重复而丢弃。
  • 交易的 callbackUrl:没有事件 header,使用 id + status。当请求体带有 infraction 对象时,请把 infraction.status 也加入键中,否则争议更新会丢失。

实现

import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
const TTL_30_DIAS = 30 * 86400;

type PayzuCallback = {
  id: string;
  type: 'DEPOSIT' | 'WITHDRAW';
  method: 'PIX' | 'BANK_SLIP' | 'INTERNAL_TRANSFER';
  status: 'PENDING' | 'COMPLETED' | 'CANCELED' | 'WAITING_FOR_REFUND' | 'REFUNDED' | 'EXPIRED' | 'ERROR';
  clientReference?: string;
};

async function handleCallback(tx: PayzuCallback) {
  const dedupeKey = `payzu:${tx.id}:${tx.status}`;
  const isFirstTime = await redis.set(dedupeKey, '1', 'EX', TTL_30_DIAS, 'NX');
  if (!isFirstTime) return;

  await processTransaction(tx);
}

常见陷阱

陷阱症状
每次重试使用随机的 clientReference扣款重复,客户困惑
仅用 id 去重(不带 status)退款不入账,出现"幽灵"退款
去重 TTL 过短延迟重试导致重新处理
内存中去重(本地 Map)重启后全部重新处理
因认为"会变化"而用 Date.now() 重新生成 clientReference不触发幂等性,产生新扣款

本页内容