Solidus / Spree 覆盖 CheckoutController#update 报错
结论
报错是因为缺少命名空间。OrderUpdateAttributes 实际定义在 Spree 命名空间中,因此装饰器里的代码应改为:
Spree::OrderUpdateAttributes.new(
@order,
update_params,
request_env: request.headers.env
).apply
如果你只是想在订单完成后执行某些操作,不建议复制并覆盖整个 CheckoutController#update。更稳妥的方式是为订单状态机添加完成回调。无论订单通过前台、后台、API 还是其他流程完成,回调都能执行,也不会在 Solidus 升级并修改 controller 实现后遗漏新增逻辑。
为什么原 controller 中可以省略命名空间
Solidus 原始 controller 通常定义在 Spree 模块中,例如:
module Spree
class CheckoutController < Spree::StoreController
def update
OrderUpdateAttributes.new(...)
end
end
end
按照 Ruby 的常量查找规则,这里的 OrderUpdateAttributes 会被解析为:
Spree::OrderUpdateAttributes
装饰器则从顶层调用:
Spree::CheckoutController.class_eval do
# ...
end
class_eval 的 block 会保留其定义位置的词法作用域,所以其中没有命名空间限定的 OrderUpdateAttributes 未必能解析到 Spree::OrderUpdateAttributes,最终会出现:
uninitialized constant OrderUpdateAttributes
这通常不表示 controller decorator 没有加载。装饰器如果没有加载,覆盖后的 update 方法就不会执行,自然也不会在这一行报错。
直接修复当前覆盖代码
改动最少的办法是补全常量名:
Spree::CheckoutController.class_eval do
def update
if Spree::OrderUpdateAttributes.new(
@order,
update_params,
request_env: request.headers.env
).apply
@order.temporary_address = !params[:save_user_address]
success =
if @order.state == 'confirm'
@order.complete
else
@order.next
end
unless success
flash[:error] = @order.errors.full_messages.join("\n")
redirect_to(checkout_state_path(@order.state))
return
end
if @order.completed?
@current_order = nil
flash.notice = Spree.t(:order_processed_successfully)
flash['order_completed'] = true
redirect_to completion_route
else
redirect_to checkout_state_path(@order.state)
end
else
render :edit
end
end
end
如果只是要修复当前异常,这样改就够了。不过,这个方法复制自 Solidus 的内部实现。以后升级 Solidus 时,需要对照新版源码检查其中的差异。
更合适的方案:监听订单完成状态
订单完成是领域事件,不应依附于某个 controller。可以在 Spree::Order 的状态机中注册回调:
Spree::Order.class_eval do
state_machine.after_transition to: :complete, do: :run_after_checkout_action
private
def run_after_checkout_action
MyOrderCompletionService.call(self)
end
end
再把业务逻辑放到独立的服务对象中:
class MyOrderCompletionService
def self.call(order)
new(order).call
end
def initialize(order)
@order = order
end
def call
# 订单完成后的操作
end
end
这样处理有几个好处:
- 无需复制 Solidus 的 checkout 流程。
- 不依赖某个特定的 controller 入口。
- 通过后台、API 等其他途径完成订单时也会触发。
- Solidus 升级后,即使内部 controller 发生变化,也不容易受到影响。
状态机 API 可能因 Solidus 及其状态机依赖的版本不同而有所变化。如果当前版本不支持上述 after_transition 写法,应以该版本中 Spree::Order 的状态机定义为准。整体思路不变,也就是把业务逻辑挂到订单进入 complete 状态的事件上。
装饰器的加载
Rails 4 的开发环境会重新加载常量。如果项目没有自动加载 decorator,可以在初始化配置中通过 to_prepare 加载:
Rails.application.config.to_prepare do
require_dependency Rails.root.join(
'app/controllers/spree/checkout_controller_decorator'
).to_s
end
模型装饰器也可以采用相同方式:
Rails.application.config.to_prepare do
require_dependency Rails.root.join(
'app/models/spree/order_decorator'
).to_s
end
是否需要手动配置,要看当前使用的 Solidus 版本和项目现有的 decorator 加载设置。由于这次错误已经出现在覆盖后的方法内部,说明该方法确实得到了执行。因此,应先修正 Spree::OrderUpdateAttributes,不用把排查重点放在加载配置上。
注意事项
完成回调可能会发送邮件、调用外部接口或创建其他记录,因此需要考虑幂等性。即使正常状态机只会完成一次订单,任务重试、数据修复或并发请求仍可能导致操作重复执行。可以保存处理标记,也可以把订单号用作外部系统的幂等键。
如果相关操作必须等数据库事务提交后才能运行,还应避免在事务提交前直接调用外部服务。回调可以先创建后台任务,再让任务在提交后执行。具体采用哪种事务回调方式,取决于项目使用的 Rails 和 Solidus 版本。
补上 Spree:: 就能解决当前异常。如果需求是处理“订单已完成”这个业务事件,使用状态机回调或独立的事件处理逻辑,会比覆盖整个 CheckoutController#update 更可靠。
备注:内容仅供参考。
