Automated Recurring Billing 真的是全自动的吗?
结论
Automated Recurring Billing(ARB)会按照订阅计划,由支付网关自动发起扣款。正常情况下,Rails 应用无须使用 cron job 定期检查到期日期。使用 ARB 后,也不应再由应用主动扣款,否则可能造成重复收费。
这里的“自动”仅指周期性扣款。应用仍要负责:
- 创建、修改和取消订阅
- 保存网关返回的 customer、subscription 等标识
- 接收 webhook 或 Silent Post 等异步通知
- 记录每笔交易及其最终状态
- 处理扣款失败、退款、拒付和订阅到期
- 定期核对本地数据与支付网关数据
简单来说,支付网关负责安排扣款,应用负责维护业务状态和账务记录。
推荐的处理流程
用户购买订阅时,Rails 应用先在支付网关中创建 recurring billing subscription,并保存网关返回的订阅 ID。此后的扣款由支付网关按照设定的金额和周期自动执行。
每次扣款成功或失败后,应用通过 webhook 接收结果:
- 验证通知是否来自支付网关。
- 使用网关的 transaction ID 进行幂等检查。
- 创建或更新本地交易记录。
- 根据扣款结果更新订阅状态。
- 确认支付成功后,再延长用户的服务期限。
不要仅根据“预计扣款日”提前更新 paid_through。扣款可能因余额不足、银行卡过期、风控或网络问题而失败。
Rails 中的数据设计
本地至少要保存网关标识、订阅状态和交易记录。例如:
# subscriptions
gateway_customer_id
gateway_subscription_id
status
amount_cents
currency
current_period_end
# payment_transactions
subscription_id
gateway_transaction_id
amount_cents
currency
status
paid_at
raw_event_id
金额适合用最小货币单位的整数保存。例如,1000 表示 10.00,这样可以避免浮点数精度问题。
current_period_end 表示当前已经确认付款的服务期终点,不应作为下一次主动扣款的唯一依据。实际扣款计划仍以支付网关中的订阅配置为准。
创建订阅示例
不同支付网关使用的 SDK 和参数并不相同。下面的代码只说明 Rails 应用需要承担哪些职责,并非某个网关的完整 API:
class SubscriptionsController < ApplicationController
def create
result = PaymentGateway.create_subscription(
customer_token: current_user.gateway_customer_token,
amount_cents: 2_900,
currency: "USD",
interval: "month"
)
subscription = current_user.subscriptions.create!(
gateway_subscription_id: result.subscription_id,
amount_cents: 2_900,
currency: "USD",
status: "pending"
)
render json: {
id: subscription.id,
status: subscription.status
}, status: :created
end
end
订阅只需创建一次。后续每月扣款交给网关调度,Rails 不需要定时重复调用单次付款接口。
接收扣款结果
支付网关发起周期扣款后,应用应通过异步事件更新本地记录。下面仍是通用结构,事件名称和签名验证方式需要替换为实际网关的实现:
class PaymentWebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def create
payload = request.raw_post
signature = request.headers["Payment-Signature"]
event = PaymentGateway.verify_webhook!(
payload: payload,
signature: signature
)
case event.type
when "recurring_payment.succeeded"
record_successful_payment(event)
when "recurring_payment.failed"
record_failed_payment(event)
when "subscription.cancelled"
mark_subscription_cancelled(event)
end
head :ok
end
private
def record_successful_payment(event)
subscription = Subscription.find_by!(
gateway_subscription_id: event.subscription_id
)
transaction = subscription.payment_transactions.find_or_initialize_by(
gateway_transaction_id: event.transaction_id
)
return if transaction.persisted? && transaction.status == "succeeded"
Subscription.transaction do
transaction.update!(
amount_cents: event.amount_cents,
currency: event.currency,
status: "succeeded",
paid_at: event.paid_at,
raw_event_id: event.id
)
subscription.update!(
status: "active",
current_period_end: event.period_end
)
end
end
def record_failed_payment(event)
subscription = Subscription.find_by!(
gateway_subscription_id: event.subscription_id
)
subscription.payment_transactions.find_or_create_by!(
gateway_transaction_id: event.transaction_id
) do |transaction|
transaction.amount_cents = event.amount_cents
transaction.currency = event.currency
transaction.status = "failed"
transaction.raw_event_id = event.id
end
subscription.update!(status: "past_due")
end
def mark_subscription_cancelled(event)
Subscription.find_by!(
gateway_subscription_id: event.subscription_id
).update!(status: "cancelled")
end
end
应用必须验证 webhook 签名,不能因为 HTTP 请求的格式正确,就直接修改付款状态。处理过程也要具备幂等性,因为支付网关可能重复发送同一个事件。
cron job 是否还有必要
定时任务可以保留,但应当用于对账和修复状态,而不是发起常规的周期扣款。例如:
class ReconcileSubscriptionsJob < ApplicationJob
queue_as :default
def perform
Subscription.where(status: %w[pending active past_due])
.find_each do |subscription|
remote = PaymentGateway.retrieve_subscription(
subscription.gateway_subscription_id
)
subscription.update!(
status: remote.status,
current_period_end: remote.current_period_end
)
end
end
end
这类任务可以补救 webhook 延迟、通知丢失或应用短暂不可用等问题。运行频率应根据网关的 API 限流、事件投递机制和业务时效要求来确定。
什么时候适合自行调度扣款
如果支付网关不提供 recurring billing,或者网关的订阅功能无法表达业务所需的计费模型,才需要自行调度扣款。例如:
- 每个周期的金额由实时用量决定
- 扣款日期受复杂的业务规则控制
- 一个账期需要合并多项动态费用
- 需要统一调度多个支付渠道
- 网关只提供 tokenized payment,不提供订阅管理
在这些情况下,可以使用 cron job 或后台任务决定扣款时间,但应用还要处理幂等键、并发执行、失败重试、重复收费、银行卡失效、退款、拒付和审计记录。其复杂度远高于检查日期后调用一次付款接口。
注意事项
- 不要同时启用网关自动扣款和本地 cron 主动扣款。
- 不要把“已经发起扣款”当成“支付成功”。
- 不要只保存
paid_through和金额。还要保存订阅 ID、交易 ID、状态、币种及时间。 - webhook 返回成功前,应尽快持久化事件。耗时的业务处理可以交给后台任务。
- 使用 transaction ID 或 event ID,防止重复通知生成两笔本地交易。
- 取消本地订阅时,也要同步取消支付网关中的订阅。
- 对账任务只能用于补偿,不能取代 webhook 和完整的交易记录。
- ARB 的通知能力、重试策略和状态定义因支付网关而异,应以所用网关的当前文档为准。
如果使用的支付网关支持 Automated Recurring Billing,应用应负责创建和管理订阅,网关按计划扣款。应用通过异步通知记录结果,并用定时对账任务补上遗漏的数据。
备注:内容仅供参考。
