PAYATHON 2026

商户扣款开通订单号能否用于每月免密支付?

支付老李

结论

通常不能把开通包月服务时使用的订单号,继续用于以后每个月的免密扣款。

开通订单记录首次购买或签约。之后每次发起免密支付时,商户系统都应创建新的支付订单,并生成一个全局唯一的商户订单号。这些订单可以关联同一份包月服务或签约记录,但不能直接复用开通时的订单号。

具体使用哪些字段,以及订单号需要满足什么唯一性要求,应以支付渠道的接口文档为准。

为什么每次扣款都需要新订单

订单号通常用于唯一标识一笔支付交易。支付渠道会根据订单号进行幂等控制、状态查询、回调处理、退款和对账。

如果每个月都使用同一个订单号,可能会遇到这些问题:

  • 支付渠道可能把新的扣款请求当作重复请求,直接返回第一次交易的结果。
  • 一个订单号无法同时记录多个月份的扣款金额和支付状态。
  • 收到异步通知后,商户系统无法确定回调属于哪个账期。
  • 处理单月退款、关闭订单或异常补扣时,难以准确定位交易。
  • 对账时,每笔渠道流水无法对应到具体账期。

所以,每月创建新订单是在记录每一笔独立的资金交易,并非简单复制数据。

推荐的数据关系

可以将包月服务与每月支付分成两层:

  1. 订阅或签约记录:只创建一次,保存套餐、用户、签约协议号和扣款周期等信息。
  2. 支付订单:每个账期创建一条,保存本次金额、账期、状态、商户订单号和渠道交易号。

例如:

subscription
├── subscription_id: SUB202609100001
├── user_id: 10001
├── plan_id: MONTHLY_PRO
├── agreement_no: 渠道签约协议号
└── status: ACTIVE

payment_order
├── order_no: PAY202610100001
├── subscription_id: SUB202609100001
├── billing_period: 2026-10
├── amount: 3000
└── status: PENDING

套餐和用户信息可以保存在订阅记录中,月度订单通过 subscription_id 与之关联,不必完整复制初始订单的所有字段。

实现步骤

1. 开通包月服务

创建订阅记录,完成支付渠道要求的签约或首次支付流程,并保存渠道返回的协议号、签约状态等必要信息。

2. 到达每月扣款日

根据 subscription_id 和账期创建新的支付订单,同时生成唯一的商户订单号,例如:

PAY202610100001
PAY202611100001
PAY202612100001

不要只使用时间戳生成订单号,高并发时可能发生碰撞。可以选择数据库序列、UUID、雪花算法或可靠的分布式 ID 服务。

3. 防止同一账期重复创建

为 subscription_id 和 billing_period 添加联合唯一约束。任务重试时先查询是否已有订单,避免重复扣款。

CREATE UNIQUE INDEX uk_subscription_period
ON payment_order (subscription_id, billing_period);

4. 使用新订单号发起免密支付

每次请求支付渠道时,传入本次生成的订单号,并使用开通阶段取得的签约协议号或授权标识。

不同支付渠道的接口名称和参数并不相同,下面只展示通用结构:

{
  "out_trade_no": "PAY202610100001",
  "agreement_no": "AGREEMENT_EXAMPLE",
  "amount": 3000,
  "currency": "CNY",
  "subject": "包月服务 2026-10"
}

其中:

  • out_trade_no 是本次扣款的商户订单号,每次扣款都应不同。
  • agreement_no 是签约或代扣授权标识,在有效期内可以复用。
  • amount 的单位应以支付渠道文档为准,不能默认一定是”分”。

5. 根据回调更新订单

收到异步通知后,根据本次商户订单号查找对应订单,并保存渠道交易号:

PaymentOrder order = paymentOrderRepository.findByOrderNo(outTradeNo);

if (order == null) {
    throw new IllegalStateException("支付订单不存在");
}

if (order.isPaid()) {
    return; // 回调幂等处理
}

order.markPaid(channelTradeNo, paidAt);
paymentOrderRepository.save(order);

支付渠道可能重复发送回调,因此更新支付状态时也要做好幂等处理。

创建月度订单的示例

下面是一段简化的 Java 伪代码。订阅记录只创建一次,每个账期则创建一条独立的支付订单:

@Transactional
public PaymentOrder createMonthlyOrder(
        Subscription subscription,
        YearMonth billingPeriod) {

    Optional<PaymentOrder> existing =
            paymentOrderRepository.findBySubscriptionIdAndBillingPeriod(
                    subscription.getId(),
                    billingPeriod.toString()
            );

    if (existing.isPresent()) {
        return existing.get();
    }

    PaymentOrder order = new PaymentOrder();
    order.setOrderNo(orderNoGenerator.nextId());
    order.setSubscriptionId(subscription.getId());
    order.setBillingPeriod(billingPeriod.toString());
    order.setAmount(subscription.getMonthlyAmount());
    order.setStatus(PaymentStatus.PENDING);

    return paymentOrderRepository.save(order);
}

实际项目还需要使用数据库唯一约束兜底,不能只依赖”先查询、再插入”。否则在并发执行时,多个任务仍可能同时创建订单。

注意事项

  • 商户订单号与签约协议号是两个不同的概念。订单号标识一笔交易,协议号标识持续扣款授权。
  • 支付请求失败后,是否继续使用原订单号重试,应根据渠道的幂等规则决定。如果交易结果尚未确定,不要直接创建新订单再次扣款。
  • 同一账期补扣是否沿用原订单号,取决于支付渠道规则和系统的订单状态设计。如果上一笔交易已经明确失败,而且渠道要求使用新单号,可以创建新的扣款尝试记录。
  • 取消包月服务时,需要停止生成后续订单,并按照渠道要求解除或关闭代扣协议。
  • 月度订单只需保存与交易有关的快照字段。套餐详情等稳定信息可以通过 subscription_id 查询,减少不必要的数据冗余。
  • 金额、套餐名称等可能变化的信息,建议在支付订单中保存本次交易的快照,方便后续退款、审计和对账。

应当复用的是签约关系或代扣授权,不是开通时的支付订单号。每个月创建独立订单,扣款、回调、退款和对账才能清楚地一一对应。

备注:内容仅供参考。