PAYATHON 2026

如何自定义 Spree checkout process 并移除结账步骤?

支付阿杰

结论

在 Spree 1.1.0 中,仅仅把文件放进 app/models,再定义一个新的 Engine 模块,并不会自动修改 Spree::Order。要让自定义结账状态机生效,至少要处理好以下几点:

  1. 装饰对象必须是 Spree::Order,不能是普通的 Order,也不能是一个没有被使用的新 Engine 类。
  2. 使用 config.to_prepare 显式加载 decorator,以免开发环境重新加载后失效。
  3. 重新定义状态机前,先移除原有的 :state 状态机,否则新的 transition 可能会叠加到旧状态机上。
  4. 同时调整付款、配送和订单完成逻辑。删除页面步骤,不代表订单不再需要 shipment 或 payment。

如果只是删除几个特定步骤,不建议直接修改 Spree gem 的源码。将改动放在应用自己的 decorator 和 initializer 中,更便于升级和回滚。

正确加载 Order decorator

建议将文件放在:

app/models/spree/order_decorator.rb

然后在 config/application.rb 中显式加载:

# config/application.rb

module YourApplication
  class Application < Rails::Application
    config.to_prepare do
      decorator = Rails.root.join(
        'app',
        'models',
        'spree',
        'order_decorator.rb'
      )

      load decorator
    end
  end
end

也可以把 config.to_prepare 放进一个正确初始化的 Rails Engine。文件的具体位置不是重点。它必须在 Spree::Order 加载后执行,而且开发环境每次重新加载类时,都要重新执行。

原来的 order_decorator.rb 虽然定义了一个模块和 Engine 类,却没有可靠地修改 Spree::Order,所以 checkout process 看起来没有任何变化。

重新定义 Spree 1.1.0 的状态机

Spree 1.1.0 使用的是较早期的 checkout 实现。不要直接照搬新版本文档中的 checkout_flow、go_to_state 或 remove_checkout_step。不同 Spree 版本对这些 API 的支持和具体行为并不一致。

如果目标流程是:

cart -> confirm -> complete

可以按下面的方式装饰 Spree::Order:

# app/models/spree/order_decorator.rb

Spree::Order.class_eval do
  # 防止新状态机与 Spree 原有的 :state 状态机叠加。
  state_machines.delete(:state) if respond_to?(:state_machines)

  state_machine :state,
                :initial => 'cart',
                :use_transactions => false do

    event :next do
      transition :from => 'cart',    :to => 'confirm'
      transition :from => 'confirm', :to => 'complete'
    end

    event :cancel do
      transition :to => 'canceled', :if => :allow_cancel?
    end

    event :return do
      transition :from => 'awaiting_return', :to => 'returned'
    end

    event :resume do
      transition :from => 'canceled',
                 :to => 'resumed',
                 :if => :allow_resume?
    end

    event :authorize_return do
      transition :to => 'awaiting_return'
    end

    after_transition :to => 'complete', :do => :finalize!
    after_transition :to => 'canceled', :do => :after_cancel
  end
end

这段代码只展示状态机的结构,不能不加判断地用于所有商店。实际使用时,要根据业务需要恢复税费、库存、付款和配送等必要回调。

修改后需要完整重启 Rails 进程。旧的 Spring、控制台或开发服务器进程可能仍保留着原来的 Spree::Order 类定义。

可以先在 Rails console 中确认 decorator 是否已经生效:

Spree::Order.state_machines[:state].states.map(&:name)

再查看 transition:

Spree::Order.state_machines[:state].events[:next].branches

如果输出中仍然只有原来的 address、delivery、payment 等跳转,说明 decorator 没有加载,或者加载顺序有误。

不要保留已删除步骤的回调

原代码包含以下回调:

after_transition :to => 'payment', :do => :create_shipment!
before_transition :to => 'complete', :do => :process_payment

状态机中已经没有 payment 时,第一个回调永远不会执行。第二个回调仍会尝试处理付款。如果订单没有付款记录或付款方式,就可能无法完成。

因此,移除 payment step 时,必须先明确对应的业务含义:

  • 如果订单总额始终为零,可以让 payment_required? 返回 false,同时避免调用付款处理。
  • 如果订单仍需收费,只是不让顾客选择付款方式,就应该由系统自动创建默认 payment,不能直接删除付款逻辑。
  • 如果采用线下付款,可以配置相应的 payment method,再自动分配给订单。
  • 不能只删除付款页面,却仍然假设 process_payments! 一定能够成功。

例如,如果系统确实只处理免费订单,可以增加一个完成条件:

before_transition :to => 'complete' do |order|
  order.total.to_d == 0
end

这只是一个保护条件,并不是完整的免费订单实现。还需要检查 Spree 1.1.0 中订单完成、payment state 和库存处理之间的关系。

移除 delivery step 的注意事项

delivery step 通常不只是显示配送方式,还会生成 shipment、选择 shipping method 并计算运费。

如果销售的是虚拟商品,可以让订单不需要配送,同时确保相关 line item 不会被 Spree 当成需要 shipment 的实体商品。

如果销售的是实体商品,只是不希望顾客选择配送方式,可以采用下面的处理方式:

  1. 根据地址自动选择默认 shipping method。
  2. 自动创建或更新 shipment。
  3. 计算运费。
  4. 跳过 delivery 页面。

对应逻辑大致如下:

def assign_default_shipping_method
  shipment = shipments.first
  return unless shipment

  shipping_method = Spree::ShippingMethod.first
  return unless shipping_method

  shipment.shipping_method = shipping_method
  shipment.save!
end

这段代码只是示意。Spree 1.1.0 中 shipment、rate 和 shipping method 的关联,需要结合项目实际安装的版本和扩展来确认。不能直接把“取第一个配送方式”当成生产环境策略。

还要检查 CheckoutController

状态机修改正确后,checkout controller 通常会根据 order.state 渲染对应模板。如果项目或扩展覆盖了 Spree::CheckoutController,还要检查其中是否写死了以下内容:

  • 可以访问的 checkout states;
  • 每个 state 允许提交的参数;
  • before_filter;
  • state 对应的模板名称;
  • 失败后跳回的步骤。

如果保留 confirm,需要确认对应模板存在:

app/views/spree/checkout/edit.html.erb
app/views/spree/checkout/_confirm.html.erb

具体文件结构可能会因 Spree 1.1.x 的补丁版本或所用扩展而不同,应以项目实际使用的 spree_core 和 spree_frontend 源码为准。

推荐的实施顺序

先把流程缩减到最小范围,不要一次性修改所有付款、配送和完成回调:

  1. 确认 decorator 已加载。
  2. 只修改 next 的 transition。
  3. 在 console 中确认状态和 transition。
  4. 使用零金额订单测试 cart -> confirm -> complete。
  5. 再实现自动付款或免费订单逻辑。
  6. 最后处理 shipment、运费、库存恢复、取消和退款流程。

Spree 1.1.0 已经是很早期的版本,网上许多教程针对的是后来加入 checkout_flow DSL 的版本。查阅资料时,要确认示例明确适用于 Spree 1.1.x。否则代码语法即使看起来合理,也可能根本不会被当前版本调用。最可靠的参考,是项目 Gemfile.lock 中锁定版本对应的 Spree::Order、Spree::CheckoutController 和相关测试代码。

备注:内容仅供参考。