如何申请小额免密支付并实现多次自动扣款
结论
只开通普通支付或”小额免密”能力,通常无法实现滴滴、外卖平台那种订单完成后自动扣款。完整方案需要满足以下条件:
- 商户已开通支付平台提供的免密代扣、委托扣款或先享后付类产品;
- 用户完成明确授权,商户保存支付平台返回的授权协议号;
- 每次行程或订单完成后,服务端根据真实订单发起扣款;
- 支付平台允许商户在授权协议有效期内多次扣款。
一天内能否多次扣款,不能只看每笔金额是否低于单笔限额,还要考虑支付平台的单日累计限额、扣款次数限制、授权协议约束、商户风控规则和用户账户状态。具体额度和频次以所接支付平台的产品协议及商户后台配置为准。
为什么不能直接使用普通小额免密支付
普通支付通常要求用户逐笔确认并完成付款。自动扣款允许商户在用户不再次输入密码的情况下主动发起扣款,因此有更严格的风险和合规要求。
这类业务通常分为两个阶段:
- 签约阶段:用户确认服务内容、扣款场景、金额规则和解约方式,支付平台生成授权协议。
- 扣款阶段:订单达到结算条件后,商户使用授权协议号发起扣款。
用户曾经支付过一次,或者在客户端开启了”小额免密”,并不表示商户已经取得主动扣款权限。商户也不能通过保存用户密码、支付凭证或银行卡敏感信息绕过正式签约流程。
申请和接入步骤
1. 确认业务场景
申请前,需要先整理以下信息:
- 扣款条件,例如行程结束或外卖订单完成;
- 单笔金额范围和预计日均扣款次数;
- 是否可能在一天内多次扣款;
- 签约前能否向用户清楚说明计费规则;
- 退款、投诉、解约及异常订单的处理机制;
- 是否涉及押金、预授权、分账或延迟结算。
支付平台通常会审核商户所属行业、经营资质、业务真实性、履约流程和投诉风险。并非所有行业都可以申请免密代扣能力。
2. 向支付平台申请对应产品
商户需要申请支持”用户授权后由商户主动扣款”的产品,不能只申请普通快捷支付、小额免密或付款码支付。
各平台的产品名称并不统一,常见名称包括:
- 委托代扣;
- 免密支付;
- 协议支付;
- 周期扣款;
- 先享后付或服务完成后扣款。
其中,“周期扣款”可能只适用于会员订阅或固定周期续费,未必支持按行程或订单不定期扣款。申请时应明确说明:“同一用户可能在一天内产生多笔独立订单,每笔订单完成后分别扣款。“再由支付平台确认相应产品是否支持该场景。
3. 完成用户签约
用户签约页面至少应清楚展示以下内容:
- 商户和服务名称;
- 自动扣款的触发条件;
- 计费方式及可能的金额范围;
- 单笔或累计限额;
- 扣款通知方式;
- 协议有效期;
- 查询账单、申请退款和关闭自动扣款的方法。
签约完成后,支付平台一般会返回授权协议号或签约标识。服务端应将其与平台用户标识、签约状态和有效期关联,但不能保存用户支付密码、银行卡验证码等敏感信息。
示例数据结构如下:
CREATE TABLE payment_agreement (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
provider VARCHAR(32) NOT NULL,
provider_user_id VARCHAR(128) NOT NULL,
agreement_no VARCHAR(128) NOT NULL UNIQUE,
status VARCHAR(16) NOT NULL,
signed_at TIMESTAMP NOT NULL,
expired_at TIMESTAMP NULL,
revoked_at TIMESTAMP NULL
);
4. 在订单完成后发起扣款
每个行程或外卖订单都应生成独立的商户支付单号。服务端需要确认订单已经完成、金额已经确定且授权仍然有效,然后再调用支付平台提供的扣款 API。
下面的 Java 示例与具体支付平台无关,仅用于说明业务流程。paymentClient.debit(...) 是占位接口,需要替换为所接平台官方 SDK 中的真实 API:
public DebitResult chargeCompletedOrder(Long orderId) {
Order order = orderRepository.findByIdForUpdate(orderId);
if (!order.isCompleted()) {
throw new IllegalStateException("订单尚未完成");
}
if (order.isPaid()) {
return DebitResult.alreadyPaid(order.getPaymentNo());
}
PaymentAgreement agreement =
agreementRepository.findActiveByUserId(order.getUserId());
if (agreement == null || !agreement.isValidAt(Instant.now())) {
throw new IllegalStateException("用户未签约或授权已失效");
}
long amountInCent = order.getPayableAmountInCent();
if (amountInCent <= 0 || amountInCent > configuredSingleLimitInCent) {
throw new IllegalArgumentException("扣款金额不符合业务限额");
}
String paymentNo = "PAY_" + order.getId();
DebitRequest request = new DebitRequest();
request.setOutTradeNo(paymentNo);
request.setAgreementNo(agreement.getAgreementNo());
request.setAmountInCent(amountInCent);
request.setSubject("订单 " + order.getOrderNo());
request.setNotifyUrl(paymentNotifyUrl);
DebitResult result = paymentClient.debit(request);
paymentRepository.saveRequest(order, paymentNo, result);
return result;
}
金额应统一使用最小货币单位,例如”分”,避免使用 float 或 double 造成精度误差:
long amountInCent = 2590L; // 25.90 元
如果必须处理十进制金额,应使用 BigDecimal:
BigDecimal amount = new BigDecimal("25.90");
long amountInCent = amount.movePointRight(2).longValueExact();
5. 通过异步通知确认最终结果
扣款 API 的同步返回值通常只表示请求已被受理,不能直接作为支付成功的最终依据。系统应验签并处理支付平台发送的异步通知,同时主动查询长时间处于未确定状态的交易。
@PostMapping("/payment/notify")
public String handlePaymentNotify(HttpServletRequest request) {
Map<String, String> params = extractParams(request);
if (!paymentClient.verifySignature(params)) {
return "FAIL";
}
String paymentNo = params.get("out_trade_no");
String providerTradeNo = params.get("trade_no");
String tradeStatus = params.get("trade_status");
if ("SUCCESS".equals(tradeStatus)) {
paymentService.markPaidIdempotently(paymentNo, providerTradeNo);
} else if ("FAILED".equals(tradeStatus)) {
paymentService.markFailedIdempotently(paymentNo);
}
return "SUCCESS";
}
以上通知字段、成功状态值和应答内容均为示意,必须按照支付平台的官方文档实现,不能直接照搬。
如何支持一天内多次扣款
要支持一天内多次扣款,需要为每笔真实业务订单单独发起支付,而不是重复使用同一个支付订单:
- 同一个有效授权协议可以关联多笔真实业务订单;
- 每笔业务订单都使用唯一的商户支付单号;
- 每笔扣款都有对应的订单、金额和履约记录;
- 每次扣款分别处理结果通知、退款和账务核对;
- 每次扣款都要校验单笔限额、单日累计金额和频次限制。
例如,用户一天完成三次行程,应创建三笔独立的支付记录:
行程 A -> PAY_20260913_0001 -> 18.50 元
行程 B -> PAY_20260913_0002 -> 32.00 元
行程 C -> PAY_20260913_0003 -> 12.80 元
同一个商户支付单号不能重复用于不同扣款。因网络超时需要重试时,应继续使用原支付单号,让支付平台和商户系统进行幂等判断,避免重复扣款。
必须处理的异常情况
授权失效
用户可能主动解约,也可能因账户变更导致协议失效。扣款前应检查本地授权状态。扣款失败后,还需要根据平台返回码更新协议状态,并引导用户重新签约或改用主动支付。
余额不足或支付工具不可用
免密授权不能保证每次扣款都成功。余额不足、银行卡受限、账户风控等情况都可能导致扣款失败。商户可以在平台规则允许的范围内有限重试,但不能无限期或高频率反复扣款。
重复通知和并发请求
支付平台可能重复发送同一条通知,订单完成事件也可能被多次消费。支付状态更新必须具备幂等性,可以通过唯一索引和事务进行控制:
CREATE UNIQUE INDEX uk_payment_out_trade_no
ON payment_record(out_trade_no);
CREATE UNIQUE INDEX uk_payment_provider_trade_no
ON payment_record(provider_trade_no);
同一订单出现并发请求时,只能有一个扣款流程进入执行状态。
退款与对账
每笔扣款都应支持独立退款,并保存以下信息:
- 业务订单号;
- 商户支付单号;
- 支付平台交易号;
- 授权协议号;
- 应付金额和实付金额;
- 扣款状态;
- 退款状态;
- 异步通知原始记录;
- 创建、支付和退款时间。
系统每天应下载或查询支付平台账单,并与本地支付记录对账,及时发现漏单、重复入账和金额不一致等问题。
注意事项
- 单笔金额未超过小额免密限额,不表示当天可以无限次扣款。
- 不要将大额订单拆成多笔小额交易来规避单笔限额。这样做可能触发风控,也可能违反产品协议。
- 发起免密扣款前,必须取得有效授权。扣款范围不能超出用户确认的业务场景。
- 用户关闭服务后,应及时停止新扣款,但关闭前已经发生的真实订单仍需按照协议处理。
- 扣款前后,应通过应用消息、短信或支付平台通知向用户展示扣款金额和订单信息。
- 签约、扣款、查询、解约和退款 API 必须使用同一支付平台对应产品的官方接口,不能混用普通支付接口。
- 限额、频次、行业准入和重试规则可能因支付平台、商户等级及用户账户而异,应以正式开通后的商户协议、后台配置和官方接口文档为准。
备注:内容仅供参考。