PAYATHON 2026

打车业务中支付宝商户代扣签约关闭后的扣款方案

支付小周

结论

通常,用户可以在支付宝 App 中主动关闭商户代扣签约。签约失效后,商户无法再使用原支付许可发起代扣。即使行程已经开始,后续调用同步或异步支付代扣接口也可能失败。

支付宝一般不提供绕过用户解约并继续强制代扣的接口。打车平台需要自行设计兜底流程,包括确认签约状态、正常发起扣款、扣款失败后将订单转为待支付,并引导用户重新签约或主动付款。

用户能否在乘车途中解约,以及关闭签约后何时生效,取决于商户接入的代扣产品、签约协议和支付宝开放平台的当前文档。

为什么会出现这种情况

商户代扣是一种支付授权,并不保证未来订单一定能够完成付款。用户关闭签约后,原授权通常不能再用于发起新的扣款。

需要分清以下三个状态:

  • 行程订单已经创建,不等于支付授权仍然有效。
  • 支付许可已经创建,不等于后续扣款一定成功。
  • 扣款请求已经受理,不等于最终支付成功。异步代扣仍需等待通知或主动查询结果。

即使用户没有关闭签约,代扣也可能因余额不足、支付工具不可用、风控限制或系统异常而失败。因此,业务上不能把“签约成功”当成“订单一定能够收款”。

推荐处理流程

1. 保存签约和支付许可信息

签约成功后,应保存支付宝返回的协议号、签约状态、用户标识和支付许可标识。具体字段名称以实际接入产品的接口文档为准。

不要只在本地保存一个布尔值,例如 signed=true。用户可能直接在支付宝侧关闭签约,导致本地状态过期。

2. 行程开始前检查签约状态

如果接入的产品提供签约查询接口,可以在派单或行程开始前查询一次。发现签约已经失效时,应立即提示用户重新签约或改用其他支付方式。

这项检查只能降低风险,无法保证行程结束时授权依然有效,因为用户可能在检查后关闭签约。

3. 行程结束后正常发起代扣

平台应使用唯一的商户订单号发起扣款,并保存请求流水。即使同步接口返回成功,也要根据产品规则判断该状态是否代表最终支付成功。

异步代扣接口通常只表示请求已被受理,最终结果要以支付宝异步通知或交易查询结果为准。

4. 对失败原因进行分类

扣款失败后,不要无条件重复调用:

  • 如果明确表示签约或支付许可已经失效,应停止使用原授权重试。
  • 如果明确表示支付失败,应将订单转为待支付。
  • 如果返回处理中或结果未知,应先调用交易查询接口确认结果。
  • 如果发生网络超时,应使用原商户订单号查询或进行幂等重试,避免重复扣款。
  • 如果遇到系统繁忙等可恢复错误,应按照接口文档规定的策略退避重试。

不同代扣产品的错误码可能不同。判断时应以接口实际返回的 code、sub_code 及对应文档为准,不能只看错误文案。

5. 提供主动支付兜底

确认无法完成代扣后,可以向用户展示未支付的行程订单,并提供以下一种或多种付款方式:

  • 使用支付宝普通支付完成付款;
  • 重新签约后再次授权扣款;
  • 使用平台支持的其他支付方式;
  • 要求用户在下次叫车前结清欠款。

重新签约后产生的新协议或支付许可,不应直接视为用户同意扣除历史订单费用。能否用于补扣历史费用,需要根据签约页面展示的授权范围、产品规则和合规要求确定。

业务处理示例

下面的代码仅用于展示状态机和幂等处理方式,不对应支付宝 SDK 的某个特定版本。实际接口名称、请求类和字段以商户所接入签约产品的官方文档为准。

public DeductResult settleRide(RideOrder order) {
    // 同一个行程始终使用固定的商户订单号
    String outTradeNo = order.getPaymentOrderNo();

    // 防止重复扣款
    PaymentRecord existing = paymentRepository.findByOutTradeNo(outTradeNo);
    if (existing != null && existing.isPaid()) {
        return DeductResult.success(existing.getTradeNo());
    }

    Agreement agreement = agreementRepository.findByUserId(order.getUserId());
    if (agreement == null || !agreement.isLocallyAvailable()) {
        markPendingPayment(order, "NO_AVAILABLE_AGREEMENT");
        return DeductResult.requireUserPayment();
    }

    try {
        AlipayDeductResponse response = alipayClient.deduct(
            outTradeNo,
            order.getPayAmount(),
            agreement.getAgreementNo()
        );

        if (response.isSuccess()) {
            markPaid(order, response.getTradeNo());
            return DeductResult.success(response.getTradeNo());
        }

        if (isAgreementInvalid(response.getCode(), response.getSubCode())) {
            agreementRepository.markInvalid(agreement.getAgreementNo());
            markPendingPayment(order, "AGREEMENT_INVALID");
            return DeductResult.requireUserPayment();
        }

        if (isResultUnknown(response.getCode(), response.getSubCode())) {
            return queryAndResolve(outTradeNo, order);
        }

        markPendingPayment(order, response.getSubCode());
        return DeductResult.requireUserPayment();
    } catch (TimeoutException exception) {
        // 超时不能直接换新订单号再次扣款,应先查询原订单
        return queryAndResolve(outTradeNo, order);
    }
}

处理异步通知时,还需要完成验签、幂等和金额校验:

public String handleAlipayNotify(Map<String, String> params) {
    if (!verifyAlipaySignature(params)) {
        return "failure";
    }

    String outTradeNo = params.get("out_trade_no");
    String tradeNo = params.get("trade_no");
    String tradeStatus = params.get("trade_status");
    String totalAmount = params.get("total_amount");

    PaymentRecord payment = paymentRepository.findByOutTradeNo(outTradeNo);
    if (payment == null) {
        return "failure";
    }

    if (!payment.getExpectedAmount().toPlainString().equals(totalAmount)) {
        return "failure";
    }

    if (isSuccessfulTradeStatus(tradeStatus)) {
        // 更新操作必须具备幂等性
        paymentRepository.markPaidIfUnpaid(outTradeNo, tradeNo);
    }

    return "success";
}

isAgreementInvalid、isResultUnknown 和 isSuccessfulTradeStatus 必须根据实际接入产品的状态码和交易状态实现,不能直接套用其他支付宝产品的错误码。

注意事项

  • 不要根据缓存中的签约状态认定扣款一定能够成功。
  • 代扣失败后不要持续重试,否则可能触发风控或造成重复扣款。
  • 遇到网络超时或同步响应异常时,应先查询原订单,不要直接生成新的商户订单号。
  • 收到异步通知后必须验签,并校验商户号、订单号、金额和交易状态。
  • 订单、扣款请求和异步通知都必须进行幂等处理。
  • 如果支付许可对金额、次数、时间或业务场景有限制,扣款必须严格遵守许可范围。
  • 用户关闭签约后,应及时停止展示“自动扣款已开启”,并提供清晰的欠款支付入口。
  • 如果业务需要降低逃单风险,可以在行程开始前采用信用评估、保证金、预授权或其他合规措施。具体是否支持,取决于商户实际开通的支付宝产品,不能将普通商户代扣当作资金冻结能力。

备注:内容仅供参考。