Rails StateMachine 无法从 :new 转换状态
结论
transition :new => :pending_refund_details 这条定义没有问题。异常通常出在调用位置:在 before_save 回调中调用事件方法,会让保存流程重入;也可能是同一次保存重复触发了 refund_details_not_found。
StateMachine 的事件方法会执行状态转换。与 ActiveRecord 集成时,它通常还会触发 save。因此,不要在 before_save 中直接调用会保存记录的事件方法。可以在保存流程之外触发事件,也可以通过 state_event,让当前这次保存完成转换。
推荐调用方式
在业务代码中直接触发事件:
return_order.refund_details_not_found!
带 ! 的方法在转换失败时会抛出 StateMachine::InvalidTransition。如果希望失败时返回 false,可以使用非 bang 版本:
if return_order.refund_details_not_found
# 转换成功
else
# 当前状态不允许转换,或保存失败
end
也可以先检查事件能否执行:
if return_order.can_refund_details_not_found?
return_order.refund_details_not_found!
end
不要在 before_save 中再次启动保存
下面的写法容易造成保存重入或重复转换:
before_save :set_refund_state
def set_refund_state
refund_details_not_found!
end
refund_details_not_found! 不只是修改字段。它会按照状态机配置执行转换并持久化记录,而这段代码运行时,当前记录已经处于 save 回调中。
更合适的做法是把触发条件放到业务层:
if return_order.refund_details_missing?
return_order.refund_details_not_found!
else
return_order.save!
end
必须随当前保存触发时
可以设置 StateMachine 提供的事件属性,让当前这次 save 执行转换:
return_order.state_event = 'refund_details_not_found'
return_order.save!
如果确实需要在回调中决定触发哪个事件,建议在 before_validation 阶段设置事件名称,并加上明确条件,避免反复赋值:
before_validation :select_state_event
private
def select_state_event
if state == 'new' && refund_details_missing? && state_event.blank?
self.state_event = 'refund_details_not_found'
end
end
可用的事件属性名称取决于状态机绑定的属性。默认状态属性是 state,对应的事件属性为 state_event。如果状态机定义在其他属性上,例如:
state_machine :status, initial: :new do
event :refund_details_not_found do
transition new: :pending_refund_details
end
end
对应的事件属性通常是:
return_order.status_event = 'refund_details_not_found'
return_order.save!
完整配置示例
state_machine :state, initial: :new do
event :all_products_received do
transition [:new, :pending_refund_details, :canceled] => :pending_payment
end
event :refund_details_not_found do
transition new: :pending_refund_details
end
event :order_return_canceled do
transition new: :canceled
end
event :mistaked_canceled do
transition canceled: :buyer_dispatched
end
event :payment_made do
transition pending_payment: :payment_complete
end
before_transition to: :canceled do |return_order|
return_order.total = 0
end
before_transition to: :pending_payment do |return_order|
return_order.pending_on = Time.current
end
before_transition to: :payment_complete do |return_order|
return_order.completed_on = Time.current
end
end
调用:
return_order.reload
if return_order.can_refund_details_not_found?
return_order.refund_details_not_found!
end
仍然报错时检查
确认以下几点:
-
数据库中的实际状态是
"new":return_order.reload.state -
没有其他
before_save、before_validation或状态机回调提前修改状态。 -
控制器、服务对象和模型回调没有重复触发同一个事件。
-
状态机绑定的属性与数据库列一致。使用
status列时,必须声明state_machine :status。 -
不要直接调用类似
refund_details_not_found_transition的内部转换对象。应调用事件方法或设置事件属性。 -
使用
refund_details_not_found!时,异常也可能由验证失败或回调阻止保存引起。此时应检查模型的errors.full_messages。
核心原则是:把状态机事件作为一次完整的业务操作触发,不要在正在执行的 before_save 中再次启动该事件的保存流程。
备注:内容仅供参考。
