PAYATHON 2026

Spree Checkout 如何移除两个结账步骤

支付老李

结论

Spree::Order 的 state machine 写法并不是唯一的问题。出现“完全没有效果”时,更常见的原因是 order_decorator.rb 没有被 Rails 加载,或者它在 Spree::Order 定义 state machine 之前执行,之后又被 Spree 自身的代码覆盖。

在 Spree 1.1.2 和 Rails 3.2 中,建议这样处理:

  1. 将文件放在 app/models/spree/order_decorator.rb。
  2. 使用 config.to_prepare 显式加载 decorator。
  3. 加载 decorator 前先加载 spree/order。
  4. 清除原 state machine,再重新定义 :state state machine。
  5. 重启 Rails 进程,并通过控制台检查实际注册的状态和转换。

只把文件放进 app/models,并不能保证 Rails 会自动执行它。文件名是 order_decorator.rb,但文件中没有定义自动加载器所期待的 OrderDecorator 或 Spree::OrderDecorator 常量。

下面的示例假设要移除 address 和 delivery 两个步骤,将流程改为:

cart -> payment -> confirm(可选)-> complete

正确加载 decorator

创建文件:

config/initializers/spree_decorators.rb

内容如下:

Rails.application.config.to_prepare do
  require_dependency 'spree/order'

  decorator = Rails.root.join(
    'app',
    'models',
    'spree',
    'order_decorator.rb'
  ).to_s

  load decorator
end

这里使用 load,是为了兼容 Rails 3.2 开发环境中的类重载。它在生产环境中也能正常工作,只会在初始化时执行一次。

Decorator 文件放在:

app/models/spree/order_decorator.rb

不要同时保留 app/models/order_decorator.rb 和 app/models/spree/order_decorator.rb 两份实现,否则可能重复覆盖 state machine。

修改 initializer 后,需要彻底重启 Rails server、console 以及类似 Spring 的预加载进程。Rails 3.2 默认没有 Spring,但如果项目使用了其他预加载工具,也要一并停止。

Decorator 示例

# app/models/spree/order_decorator.rb

Rails.logger.info '[Spree] loading order_decorator.rb'

Spree::Order.class_eval do
  StateMachine::Machine.ignore_method_conflicts = true

  state_machines.clear

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

    event :next do
      transition :from => 'cart',
                 :to => 'payment',
                 :if => :payment_required?

      transition :from => 'cart',
                 :to => 'complete'

      transition :from => 'payment',
                 :to => 'confirm',
                 :if => lambda { |order|
                   order.payment_method &&
                     order.payment_method.payment_profiles_supported?
                 }

      transition :from => 'payment',
                 :to => 'complete'

      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

    before_transition :to => 'complete' do |order|
      begin
        order.process_payments!
      rescue Spree::Core::GatewayError
        !!Spree::Config[:allow_checkout_on_gateway_error]
      end
    end

    after_transition :to => 'payment',  :do => :create_shipment!
    after_transition :to => 'complete', :do => :finalize!
    after_transition :to => 'resumed',  :do => :after_resume
    after_transition :to => 'canceled', :do => :after_cancel
  end
end

显式写成:

state_machine :state

比依赖 state_machine gem 的默认属性更清楚,也方便在控制台中通过 state_machines[:state] 检查。

原实现中的问题

第一段代码看起来并不是完整的 Ruby 代码:

class Engine   'cart') do

这里至少缺少合法的类继承或 state_machine 调用。完整的扩展 Engine 通常类似:

module SpreeCustomExtension
  class Engine < Rails::Engine
  end
end

不过,覆盖一个模型不需要专门创建 Engine。直接通过 config.to_prepare 加载 decorator 更简单。

还需要确认第一段代码中的这些 callback 是否确实存在:

:complete_order
:process_payment

Spree 1.1.x 的 Spree::Order 未必提供这两个名称的方法。第二段代码使用的 process_payments! 和 finalize! 更接近这一流程所需的方法。

第二段代码本身比较合理,但仍有两个隐患:

  • 文件可能从未加载。
  • decorator 可能先执行,随后 Spree 加载 Spree::Order,重新建立原来的 state machine。

显式执行:

require_dependency 'spree/order'

可以先加载原模型,再覆盖它。

原代码还保留了:

after_transition :to => 'delivery', :do => :create_tax_charge!

新流程不会进入 delivery,所以这个 callback 也不会执行。应将它删除,或者把相关税费处理移到实际会经过的状态。具体放在哪个状态,取决于商店的税务、地址和配送逻辑,不能只靠修改状态名称来决定。

如何确认 decorator 已加载

先查看 Rails 日志中是否出现:

[Spree] loading order_decorator.rb

也可以临时在文件顶部加入更明显的调试代码:

warn '[Spree] order decorator loaded'

如果启动 server 或 console 时完全没有输出,说明文件没有执行。这时应先检查 initializer 路径,不要继续修改 state machine。

还可以在 Rails console 中直接检查文件是否加载过:

$LOADED_FEATURES.grep(/order_decorator/)

不过,上面的实现使用了 load,$LOADED_FEATURES 不一定会记录该文件,因此日志标记更可靠。

如何检查 state machine 是否已覆盖

启动控制台:

bundle exec rails console

取得 state machine:

machine = Spree::Order.state_machines[:state]

检查状态:

machine.states.map { |state| state.name }

预期结果中不应再有 address 和 delivery,大致会包含:

[
  :cart,
  :payment,
  :confirm,
  :complete,
  :canceled,
  :resumed,
  :awaiting_return,
  :returned
]

state_machine gem 的不同补丁版本可能返回字符串而不是符号,因此要关注状态名称,不必要求数组的表现形式完全一致。

还可以创建或查找一个测试订单,检查当前可以触发的事件:

order = Spree::Order.last

order.state
order.state_events
order.can_next?

对于处于 cart 状态的订单,可以查看下一次转换:

order.state_transitions

如果当前订单满足付款条件,next 应指向 payment;如果不需要付款,则可能直接指向 complete。

不要在生产控制台中直接执行下面的代码,以免修改真实订单:

order.next!

应在开发环境或测试环境中创建专用订单进行验证。

跳过 address 和 delivery 的业务影响

修改 state machine 只能改变页面的跳转顺序,不会自动补齐被跳过步骤原本负责收集的数据。

跳过 address 和 delivery 后,还要逐项确认:

  • bill_address 和 ship_address 从哪里获得。
  • 订单是否只包含不需要配送的商品。
  • Shipment 和 ShippingRate 是否已经生成。
  • payment_required? 是否符合预期。
  • 税费计算是否依赖收货地址或 delivery 状态。
  • 支付方式是否要求 billing address。
  • 完成订单前的 validations 是否仍能通过。

如果销售实体商品,直接跳过这两个步骤通常会造成地址、配送方式、运费或税费缺失。此时需要在进入 checkout 前,从用户资料或其他业务流程中补齐订单数据,不能只删除状态。

如果只是想隐藏页面,但仍然需要地址和配送数据,更稳妥的做法是自动填写数据并选择配送方式,同时让状态机继续经过这些状态。隐藏页面和彻底删除状态是两个不同的需求。

最后,还应补充 state machine 测试,至少覆盖:

cart -> payment
payment -> confirm
payment -> complete
confirm -> complete

同时确认不存在:

cart -> address
address -> delivery

这样才能确认 decorator 不只是被读取,而是已经覆盖了 Spree 原来的结账流程。

备注:内容仅供参考。