PAYATHON 2026

如何在 PAYGATE PAYMENT GATEWAY 中暂时冻结付款?

支付小周

结论

不要先扣款,再等待成团。更合适的做法是采用“预授权(Authorization)+ 延迟请款(Capture)”:

  1. 用户下单时仅进行预授权,暂时占用相应额度,不将资金结算给商户。
  2. 达到最低购买数量后,对所有有效的预授权执行 Capture。
  3. 如果未能成团,执行 Void/Reverse 释放预授权,而不是退款。

是否支持预授权、延迟 Capture、部分 Capture,以及授权最长保留时间,要看具体的 PayGate 产品、收单机构、支付方式和商户配置,不能只凭支付网关的名称判断。开发前应向 PayGate 或收单机构确认,商户账户是否已经开通相关交易类型。

如果 PayGate 不支持延迟 Capture,或者成团周期可能超过银行卡授权有效期,就应保存支付方式,在成团后再发起扣款,而不是长期冻结用户资金。

为什么不建议先扣款后退款

完成 Capture 后,交易已经进入清算流程。即使网站把订单标记为“待成团”,资金层面仍是已付款。未成团时只能退款,会产生这些问题:

  • 用户需要等待退款到账;
  • 商户可能仍要承担部分交易费用;
  • 大量退款会增加对账和风控压力;
  • 如果退款失败或银行卡状态发生变化,可能需要人工处理;
  • 用户账单上会先显示扣款,之后再显示退款。

预授权通常只会冻结持卡人的部分可用额度,并没有完成最终收款。取消预授权一般也不属于退款。

推荐的交易流程

1. 创建待成团订单

用户选择购买数量后,由服务端创建订单,并记录:

  • 商品 ID;
  • 用户购买数量;
  • 应付金额;
  • 成团批次 ID;
  • 订单状态;
  • 支付网关返回的交易引用;
  • 预授权到期时间。

不要信任前端提交的金额。服务端必须根据商品价格和数量重新计算应付金额。

2. 发起预授权

调用 PayGate 提供的授权接口。交易类型应与以下操作含义相同:

AUTH
AUTHORIZE
PREAUTH
Authorization Only
Manual Capture

实际名称必须以所用 PayGate 的 API 文档为准,不能假设接口一定使用这些参数。

授权成功后,保存网关返回的唯一交易标识,例如:

transaction_id
reference
authorization_id

不能只根据浏览器跳转结果判断授权是否成功。最终状态应以服务端回调、Webhook 或主动查询结果为准。

3. 原子化累计已授权数量

只有预授权成功的订单才能计入成团数量。数量累计要通过数据库事务或锁完成,以免多个用户同时付款时发生超卖。

示例逻辑如下:

BEGIN;

SELECT target_quantity, authorized_quantity, status
FROM group_deals
WHERE id = :deal_id
FOR UPDATE;

-- 校验活动仍在进行,并确认剩余可购买数量

UPDATE group_deals
SET authorized_quantity = authorized_quantity + :quantity
WHERE id = :deal_id;

COMMIT;

如果预授权成功,但数据库更新失败,应把该授权标记为“待释放”,再由后台补偿任务执行 Void/Reverse。

4. 达到数量后执行 Capture

累计有效数量达到管理员设定的最低数量后,应原子地把成团批次状态更新为 CAPTURING,然后逐笔执行 Capture。

以下是与具体 SDK 无关的伪代码:

async function authorizeOrder(order, paymentMethod) {
  const response = await paygate.authorize({
    amount: order.amount,
    currency: order.currency,
    paymentMethod,
    merchantReference: order.id,
    capture: false
  });

  if (response.status !== "AUTHORIZED") {
    throw new Error("Payment authorization failed");
  }

  await saveAuthorization(order.id, {
    gatewayTransactionId: response.transactionId,
    expiresAt: response.expiresAt ?? null
  });
}

async function captureOrder(order) {
  const response = await paygate.capture({
    transactionId: order.gatewayTransactionId,
    amount: order.amount,
    idempotencyKey: `capture:${order.id}`
  });

  await recordCaptureResult(order.id, response);
}

其中的 authorize、capture、capture: false 和 idempotencyKey 仅用于说明业务结构,不代表 PayGate 的实际接口名称。接入时必须替换为官方 API 中定义的名称。

5. 未成团时释放授权

活动结束后,如果购买数量不足,应逐笔取消尚未 Capture 的预授权:

async function releaseOrderAuthorization(order) {
  const response = await paygate.voidAuthorization({
    transactionId: order.gatewayTransactionId,
    idempotencyKey: `void:${order.id}`
  });

  await recordVoidResult(order.id, response);
}

网关可能将这项操作称为 Void、Reverse、Authorization Reversal 或 Cancel Authorization。这里应使用专门的授权撤销接口,不要误用退款接口。

必须先确认的 PayGate 能力

联系 PayGate 或收单机构时,可以直接确认以下问题:

  • 商户账户是否支持 authorization-only transaction?
  • 是否支持 separate authorization and capture?
  • 预授权的有效期是多少?
  • 不同卡组织和支付方式的有效期是否不同?
  • 能否延长授权或重新授权?
  • 是否支持 partial capture 和 multiple capture?
  • Capture 金额能否小于授权金额?
  • 取消预授权应调用哪个接口?
  • 授权、Capture 和 Void 的手续费如何计算?
  • 是否提供 Webhook 或服务端交易状态查询接口?
  • 是否支持幂等键?如果不支持,如何避免重复 Capture?
  • 用户完成 3-D Secure 后,延迟 Capture 是否仍然有效?

如果 PayGate 只提供即时销售交易,例如单一的 SALE 或即时扣款接口,没有相互独立的授权和 Capture 操作,就无法直接实现这种资金冻结模式。

成团周期较长时的替代方案

银行卡预授权不能无限期托管资金。授权可能在若干天后到期,具体期限不能自行假设,发卡行也可能提前释放用户额度。因此,预授权更适合成团窗口较短的活动。

如果活动可能持续较长时间,可以采用以下流程:

  1. 用户下单时完成身份验证;
  2. 通过网关保存支付方式,或取得可以重复使用的支付令牌;
  3. 不在业务数据库中保存卡号、CVV 等敏感信息;
  4. 达到成团数量后,使用令牌发起实际扣款;
  5. 如果某位用户扣款失败,通知其重新付款或启用候补订单。

示意代码如下:

async function registerCommitment(order, paymentMethod) {
  const setup = await paygate.setupPaymentMethod({
    paymentMethod,
    customerId: order.customerId,
    merchantReference: order.id
  });

  await savePaymentToken(order.id, setup.paymentMethodToken);
}

async function chargeAfterDealSucceeds(order) {
  return paygate.charge({
    amount: order.amount,
    currency: order.currency,
    paymentMethodToken: order.paymentMethodToken,
    merchantReference: order.id,
    idempotencyKey: `charge:${order.id}`
  });
}

这种方式不会冻结用户额度,但成团后可能扣款失败。还要确认 PayGate 是否支持 tokenization、后续扣款和 merchant-initiated transaction,以及当地法规和 3-D Secure 规则是否允许这种使用方式。

不要自行保管资金或银行卡信息

“冻结付款”并不是先把钱收进网站账户,再决定是否转给商户。除非使用具备相应资质的托管、市场平台或 escrow 产品,否则自行代收并长期持有资金可能带来支付合规问题。

也不要为了成团后再扣款而保存以下数据:

PAN
CVV/CVC
完整磁道数据
银行卡照片

应使用支付网关提供的托管支付页面、前端令牌化组件或支付令牌,减少 PCI DSS 合规范围和数据泄露风险。

系统实现中的关键注意事项

Capture 不是全有或全无

一个成团批次可能包含多笔预授权。即使所有预授权都成功,后续 Capture 时仍可能出现部分成功、部分失败。因此,批次状态不能只有“成功”和“失败”,建议至少包括:

OPEN
AUTHORIZED
CAPTURING
CAPTURED
PARTIALLY_CAPTURED
EXPIRED
CANCELING
CANCELED

对于部分 Capture 失败的情况,需要提前制定处理规则,例如重试、补单、启用候补用户,或者撤销尚未 Capture 的订单。已经成功 Capture 的交易不能通过 Void 恢复为预授权,只能走退款流程。

所有支付操作都要幂等

网络超时并不代表网关没有处理请求。Capture、Void 和后续扣款都必须使用幂等键。如果网关不支持幂等键,就要在重试前通过商户订单号查询交易状态,防止重复扣款。

预授权到期必须监控

系统应保存授权时间和预计到期时间,并定期检查:

if (authorization.expiresAt <= renewalThreshold) {
  await markOrderForReview(order.id);
}

只有网关明确支持时才能重新授权,不能假设旧授权到期后可以无感续期。

库存与付款状态必须分开

建议分别维护:

  • 用户承诺购买数量;
  • 已预授权数量;
  • 已成功 Capture 数量;
  • 已释放数量;
  • 实际可交付库存。

成团判断应以“有效且未过期的预授权数量”为主,不能只统计已提交的订单数量。

最终建议

如果成团期限较短,优先采用预授权加延迟 Capture。这是最接近“暂时冻结付款”的标准支付方案。未成团时直接撤销授权,不需要退款。

如果成团期限可能超过授权有效期,则应使用支付令牌,在成团后再扣款。同时要考虑部分用户扣款失败的情况,并设计候补、补款和超时机制。

开发前,应让 PayGate 或收单机构以书面形式确认商户账户支持的交易类型、授权期限、撤销方式和 Capture 规则。如果现有 PayGate 接入不具备这些能力,就需要更换支持 manual capture 或平台型支付流程的支付服务,不能只靠网站业务代码模拟资金冻结。

备注:内容仅供参考。