PayZuDocs
最佳实践

资金与精度

说明金额以带小数的雷亚尔传输,以及如何在存储时保持精度。

PayZu Pix API 使用带小数位的雷亚尔。请注意:许多 PSP 以分为单位运作,因此如果您正在迁移或对比集成方案,这里的小数点不是可选项。

{ "amount": 99.90 }

代码中的小数精度

在 JavaScript 中,0.1 + 0.2 !== 0.3。在 Python 中 Decimal 是安全的,但 float 不是。在 SQL 中,FLOAT 会损失精度。

语言使用
JavaScript以分为单位的整数,或 decimal.js 库。
Python计算时使用 decimal.Decimal,仅在 API 处使用 float
Goshopspring/decimal 或以分为单位的整数。
JavaBigDecimal,绝不使用 double
PHPbcmath,或以分为单位的整数。
SQL/PostgresNUMERIC(15,2),绝不使用 FLOATREAL

推荐模式:内部以分存储

在数据库中以分为单位的整数存储,仅在 API 边界处转换:

function centsToReais(cents: number): number {
  return cents / 100;
}

function reaisToCents(reais: number): number {
  return Math.round(reais * 100);
}

await createPixCharge({
  amount: centsToReais(order.totalCents),
  clientReference: `order-${order.id}`,
});

const callbackAmountCents = reaisToCents(callback.amount);
if (callbackAmountCents !== order.totalCents) {
  throw new Error('callback 与订单金额不一致');
}
from decimal import Decimal

def cents_to_reais(cents: int) -> Decimal:
    return Decimal(cents) / Decimal(100)

def reais_to_cents(reais: float | Decimal) -> int:
    return int((Decimal(str(reais)) * Decimal(100)).quantize(Decimal('1')))

create_pix_charge({
    'amount': float(cents_to_reais(order.total_cents)),
    'clientReference': f'order-{order.id}',
})

各操作最低限额

PayZu 在服务端校验。低于最低限额的请求会返回 400 Bad Request

操作amount 最低额
POST /pix(收款)R$ 1,00
POST /withdraw(按密钥提现)R$ 0,01
POST /withdraw/qrcode(支付 QR Code)R$ 0,10
POST /internal-transferR$ 0,01

以上是 API schema 层面的最低额。你账户实际生效的最低与最高单笔金额在 GET /usercashInTicketMincashInTicketMaxcashOutTicketMincashOutTicketMax 中,调用按这些值校验——请在计算金额前先读取。被拒绝时消息中会给出实际生效的金额,errorCode 因路由而异:POST /pixPZD600POST /withdrawPOST /withdraw/qrcodePZS600POST /internal-transferPZC602

手续费

PayZu 收取的手续费会通过 callback 在 serviceFeeCharged 字段中返回(单位为雷亚尔)。

{
  "amount": 99.90,
  "serviceFeeCharged": 0.99,
  "status": "COMPLETED"
}

财务对账时,请考虑:

数值含义
amount客户支付或您付款的金额。
serviceFeeCharged该操作的 PayZu 手续费。
净额amount - serviceFeeCharged(入账)或 amount + serviceFeeCharged(总支出)。

校验 callback 中收到的金额

务必确认 callback 与订单一致。客户可能支付不同的金额(在某些场景下 Pix 允许 QR Code 不固定金额)。

async function handleDepositCallback(tx: PayzuCallback, order: Order) {
  const callbackCents = reaisToCents(tx.amount);
  if (callbackCents !== order.totalCents) {
    log.warn('金额不一致', {
      pedido: order.totalCents,
      recebido: callbackCents,
    });
    await flagForReview(order, tx);
    return;
  }
  await markOrderPaid(order, tx);
}

常见陷阱

陷阱症状
误以为是分,发送 amount: 9990向客户收取 R$ 9.990,00
在 Postgres 中将 amount 存为 FLOAT多行求和时丢失分
在 Python 中将 Decimalfloat 相加类型错误或精度丢失
直接使用 parseFloat(tx.amount) 而不四舍五入99.90 变成 99.9000000000001
不校验收到金额与预期金额部分支付被误判为已完成

本页内容