Spree Checkout 如何移除两个结账步骤
结论
Spree::Order 的 state machine 写法并不是唯一的问题。出现“完全没有效果”时,更常见的原因是 order_decorator.rb 没有被 Rails 加载,或者它在 Spree::Order 定义 state machine 之前执行,之后又被 Spree 自身的代码覆盖。
在 Spree 1.1.2 和 Rails 3.2 中,建议这样处理:
- 将文件放在
app/models/spree/order_decorator.rb。 - 使用
config.to_prepare显式加载 decorator。 - 加载 decorator 前先加载
spree/order。 - 清除原 state machine,再重新定义
:statestate machine。 - 重启 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 原来的结账流程。
备注:内容仅供参考。
