如何在 PAYGATE PAYMENT GATEWAY 中暂时冻结付款?
结论
不要先扣款,再等待成团。更合适的做法是采用“预授权(Authorization)+ 延迟请款(Capture)”:
- 用户下单时仅进行预授权,暂时占用相应额度,不将资金结算给商户。
- 达到最低购买数量后,对所有有效的预授权执行 Capture。
- 如果未能成团,执行 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 操作,就无法直接实现这种资金冻结模式。
成团周期较长时的替代方案
银行卡预授权不能无限期托管资金。授权可能在若干天后到期,具体期限不能自行假设,发卡行也可能提前释放用户额度。因此,预授权更适合成团窗口较短的活动。
如果活动可能持续较长时间,可以采用以下流程:
- 用户下单时完成身份验证;
- 通过网关保存支付方式,或取得可以重复使用的支付令牌;
- 不在业务数据库中保存卡号、CVV 等敏感信息;
- 达到成团数量后,使用令牌发起实际扣款;
- 如果某位用户扣款失败,通知其重新付款或启用候补订单。
示意代码如下:
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 或平台型支付流程的支付服务,不能只靠网站业务代码模拟资金冻结。
备注:内容仅供参考。
