PAYATHON 2026

Rails 应用的 Billing plugins 该如何选择?

支付阿杰

结论

对于新项目,不建议直接在 Service Merchant 和早期版本的 SaaS Rails Kit 之间选择。两者都是 Rails 生态中出现较早的方案,能否适配当前的 Rails 版本、支付网关 API、强客户认证(SCA)以及 Webhook 安全要求,需要逐项确认。

更稳妥的方案是:

  • 使用支付服务商官方维护的 Ruby SDK。
  • 在 Rails 中自行实现订阅、账单状态和 Webhook 处理层。
  • 如果想减少开发工作,再考虑仍在持续维护、测试覆盖完整的 billing gem 或商业模板。

如果必须维护老项目,可以根据现有需求判断:

  • 如果只需要支付网关抽象,团队也愿意自行实现订阅流程,可以评估 Service Merchant。
  • 如果需要账户、套餐、订阅等较完整的 SaaS 基础结构,并且已经确认代码适配现有 Rails 版本,可以评估 SaaS Rails Kit。
  • 不要只根据“免费”或“付费”做决定。维护状态、升级成本和支付合规风险通常更值得关注。

两类方案的区别

这两个方案处理的并不是同一层问题。

Service Merchant

这类插件更接近支付基础设施层,主要负责封装支付网关调用。应用仍要处理许多业务逻辑,包括:

  • 套餐与价格管理
  • 客户和支付平台账号的关联
  • 订阅状态同步
  • 试用期和优惠
  • 升级、降级与取消
  • 支付失败后的重试与限制
  • Webhook 验签和幂等处理
  • 发票、退款及对账

它的好处是控制空间大,也不会受到模板结构的强约束,但开发和长期维护工作更多。

SaaS Rails Kit

商业 Rails kit 通常是 SaaS 应用的起步模板,不只封装支付网关调用。它可能已经提供账户、套餐和订阅等结构,但实际功能取决于购买的版本,不能默认每个版本都支持当前的支付接口。

商业模板主要用于节省初期开发时间,并不意味着它天然更安全或可靠。评估时需要确认:

  • 最近一次更新日期
  • 支持的 Rails 和 Ruby 版本
  • 支持哪些支付服务商及 API 版本
  • 是否包含数据库迁移和升级说明
  • 是否处理 Webhook 验签与重复投递
  • 是否有自动化测试
  • 许可证是否允许当前部署方式
  • 停止维护后能否继续修改源代码

如果无法确认这些信息,购买模板可能会给项目带来难以升级的遗留代码。

推荐的实现方式

以第三方支付平台托管的支付页面为例,Rails 应用最好只保存支付平台标识符和本地业务状态,不直接接触银行卡号。

简化后的数据模型可以这样设计:

# app/models/account.rb
class Account < ApplicationRecord
  has_one :subscription, dependent: :destroy
end
# app/models/subscription.rb
class Subscription < ApplicationRecord
  belongs_to :account

  enum :status, {
    incomplete: "incomplete",
    trialing: "trialing",
    active: "active",
    past_due: "past_due",
    canceled: "canceled"
  }

  validates :provider, presence: true
  validates :provider_subscription_id, uniqueness: true, allow_nil: true
end

建议至少保存这些字段:

create_table :subscriptions do |t|
  t.references :account, null: false, foreign_key: true
  t.string :provider, null: false
  t.string :provider_customer_id
  t.string :provider_subscription_id
  t.string :provider_price_id
  t.string :status, null: false, default: "incomplete"
  t.datetime :current_period_ends_at
  t.datetime :canceled_at
  t.timestamps
end

add_index :subscriptions, :provider_subscription_id, unique: true

不要只在前端保存套餐价格,也不要允许客户端直接提交任意 price_id。服务端应根据内部套餐代码完成映射:

class Billing::Plans
  PRICE_IDS = {
    "starter" => ENV.fetch("BILLING_STARTER_PRICE_ID"),
    "pro"     => ENV.fetch("BILLING_PRO_PRICE_ID")
  }.freeze

  def self.price_id_for(plan)
    PRICE_IDS.fetch(plan)
  end
end

控制器只接收系统支持的套餐名称:

class CheckoutsController < ApplicationController
  def create
    price_id = Billing::Plans.price_id_for(params.require(:plan))

    session = PaymentProvider::Checkout.create(
      customer_id: current_account.subscription&.provider_customer_id,
      price_id: price_id,
      success_url: billing_success_url,
      cancel_url: billing_cancel_url
    )

    redirect_to session.url, allow_other_host: true
  rescue KeyError
    render plain: "Unknown billing plan", status: :unprocessable_entity
  end
end

PaymentProvider::Checkout.create 只是用于说明系统边界的示例接口,并不是某个 gem 的真实 API。实际项目中,需要将它替换为支付服务商官方 SDK 提供的方法和参数。

Webhook 才是订阅状态的依据

用户从支付页面返回应用,不代表付款已经成功。最终状态应由经过验签的 Webhook 更新。

class BillingWebhooksController < ActionController::API
  def create
    event = Billing::WebhookVerifier.verify!(
      payload: request.raw_post,
      signature: request.headers["HTTP_PAYMENT_SIGNATURE"]
    )

    Billing::ProcessEvent.call(event)

    head :ok
  rescue Billing::InvalidSignature
    head :bad_request
  end
end

这里的 Billing::WebhookVerifier 同样是应用内部抽象。验签必须按照具体支付服务商的官方文档实现,不能只解析 JSON 就直接信任其中的状态。

事件处理还需要保证幂等,因为支付平台可能重复投递同一个事件:

class Billing::ProcessEvent
  def self.call(event)
    BillingEvent.transaction do
      record = BillingEvent.find_or_initialize_by(provider_event_id: event.id)
      return if record.processed_at?

      update_subscription(event)

      record.processed_at = Time.current
      record.save!
    end
  end

  def self.update_subscription(event)
    # 根据经过验签的事件类型更新本地订阅状态。
  end
end

数据库还应为 provider_event_id 建立唯一索引,避免并发请求重复执行:

add_index :billing_events, :provider_event_id, unique: true

实际选型检查表

决定使用插件、gem 或商业 kit 之前,建议先做一个小型验证项目,至少测试以下场景:

  1. 创建客户和订阅。
  2. 首次付款成功与失败。
  3. Webhook 重复投递。
  4. Webhook 乱序到达。
  5. 套餐升级和降级。
  6. 周期结束时取消。
  7. 支付失败后的状态变化。
  8. 退款和争议处理。
  9. 测试环境与生产环境的配置隔离。
  10. Rails、Ruby 或支付 API 升级后的兼容性。

还要检查依赖是否锁定在过旧的版本:

bundle outdated
bundle audit

运行 bundle audit 前,项目需要安装相应工具。它只能发现已经收录到漏洞数据库中的问题,不能证明依赖绝对安全。

注意事项

不要在 Rails 数据库、日志或错误追踪系统中保存完整银行卡号、CVC 等敏感信息。优先使用支付平台托管页面或其安全组件,缩小 PCI DSS 合规范围。

金额应以最小货币单位的整数保存,例如人民币分或美元美分,避免浮点数误差:

amount_cents = 1999

订阅权限也不能只看 canceled_at 是否为空。trialing、active、past_due 等状态如何影响访问权限,需要写成明确的业务规则。

支付代码最好放在 Billing 命名空间中,避免控制器和业务模型直接依赖某个 SDK。以后更换服务商、升级 API 或补充测试时,需要修改的范围会更小。

老项目可以结合现有代码评估 Service Merchant 或 SaaS Rails Kit。新项目则应优先采用仍在维护的支付 SDK 和方案,并重点考察 Webhook、幂等处理、状态同步和升级能力。

备注:内容仅供参考。