如何自定义 Spree checkout process 并移除结账步骤?
结论
在 Spree 1.1.0 中,仅仅把文件放进 app/models,再定义一个新的 Engine 模块,并不会自动修改 Spree::Order。要让自定义结账状态机生效,至少要处理好以下几点:
- 装饰对象必须是
Spree::Order,不能是普通的Order,也不能是一个没有被使用的新Engine类。 - 使用
config.to_prepare显式加载 decorator,以免开发环境重新加载后失效。 - 重新定义状态机前,先移除原有的
:state状态机,否则新的 transition 可能会叠加到旧状态机上。 - 同时调整付款、配送和订单完成逻辑。删除页面步骤,不代表订单不再需要 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 的实体商品。
如果销售的是实体商品,只是不希望顾客选择配送方式,可以采用下面的处理方式:
- 根据地址自动选择默认 shipping method。
- 自动创建或更新 shipment。
- 计算运费。
- 跳过 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 源码为准。
推荐的实施顺序
先把流程缩减到最小范围,不要一次性修改所有付款、配送和完成回调:
- 确认 decorator 已加载。
- 只修改
next的 transition。 - 在 console 中确认状态和 transition。
- 使用零金额订单测试
cart -> confirm -> complete。 - 再实现自动付款或免费订单逻辑。
- 最后处理 shipment、运费、库存恢复、取消和退款流程。
Spree 1.1.0 已经是很早期的版本,网上许多教程针对的是后来加入 checkout_flow DSL 的版本。查阅资料时,要确认示例明确适用于 Spree 1.1.x。否则代码语法即使看起来合理,也可能根本不会被当前版本调用。最可靠的参考,是项目 Gemfile.lock 中锁定版本对应的 Spree::Order、Spree::CheckoutController 和相关测试代码。
备注:内容仅供参考。
