PAYATHON 2026

如何申请小额免密支付并实现多次自动扣款

支付小周

结论

只开通普通支付或”小额免密”能力,通常无法实现滴滴、外卖平台那种订单完成后自动扣款。完整方案需要满足以下条件:

  1. 商户已开通支付平台提供的免密代扣、委托扣款或先享后付类产品;
  2. 用户完成明确授权,商户保存支付平台返回的授权协议号;
  3. 每次行程或订单完成后,服务端根据真实订单发起扣款;
  4. 支付平台允许商户在授权协议有效期内多次扣款。

一天内能否多次扣款,不能只看每笔金额是否低于单笔限额,还要考虑支付平台的单日累计限额、扣款次数限制、授权协议约束、商户风控规则和用户账户状态。具体额度和频次以所接支付平台的产品协议及商户后台配置为准。

为什么不能直接使用普通小额免密支付

普通支付通常要求用户逐笔确认并完成付款。自动扣款允许商户在用户不再次输入密码的情况下主动发起扣款,因此有更严格的风险和合规要求。

这类业务通常分为两个阶段:

  • 签约阶段:用户确认服务内容、扣款场景、金额规则和解约方式,支付平台生成授权协议。
  • 扣款阶段:订单达到结算条件后,商户使用授权协议号发起扣款。

用户曾经支付过一次,或者在客户端开启了”小额免密”,并不表示商户已经取得主动扣款权限。商户也不能通过保存用户密码、支付凭证或银行卡敏感信息绕过正式签约流程。

申请和接入步骤

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 必须使用同一支付平台对应产品的官方接口,不能混用普通支付接口。
  • 限额、频次、行业准入和重试规则可能因支付平台、商户等级及用户账户而异,应以正式开通后的商户协议、后台配置和官方接口文档为准。

备注:内容仅供参考。