Rails Spree 无法从 payment 转换到 complete
结论
payment.state == "completed" 只表示这笔支付已经完成状态转换,不代表 Spree 已确认订单足额支付。
订单从 payment 进入 complete 状态时,Spree 通常还会检查订单应付金额、有效支付总额,以及是否有可处理的 payment。如果已完成 payment 的 amount 未覆盖订单余额,或者订单保存的 payment_total 尚未更新,订单仍会被判定为需要付款。此时唯一的 payment 已经处于 completed 状态,不能继续处理,最终可能出现 "No payment found"。
还要注意,Spree 订单的终态通常是 "complete",payment 的完成状态才是 "completed"。所以下面的判断也有问题:
order.state == "completed"
应根据项目使用的 Spree 版本检查 "complete",或者调用相应的谓词。
为什么已完成的 payment 仍然不够
订单状态机并非只检查:
order.payments.any?(&:completed?)
它要判断整个订单是否已经付清,其中包括:
- payment 是否属于当前订单;
- payment 是否属于有效支付;
payment.amount是否正确;- 有效支付总额是否覆盖
order.total; - 订单的
payment_total和payment_state是否已经重新计算; - 如果订单仍需付款,是否还有能够继续处理的 payment。
常见情况是 payment 已经变为 completed,但金额为 0、金额不足,或者订单的支付汇总字段仍是旧值。Spree 判断订单还有未付余额,便会寻找待处理的 payment。可当前 payment 已经完成,不能再次处理,于是报出 "No payment found"。
建议重点检查以下数值:
Rails.logger.info(
payment_id: payment.id,
payment_state: payment.state,
payment_amount: payment.amount,
order_total: order.total,
payment_total: order.payment_total,
outstanding_balance: order.outstanding_balance
)
不同 Spree 版本对 outstanding_balance 和支付统计方法的实现可能不同,但判断逻辑一致:支付状态已完成,不等于订单已经足额支付。
解决步骤
1. 创建 payment 时写入正确金额
不能只创建 payment 记录并修改其状态,还要确保金额与本次实际支付金额一致。例如:
payment = order.payments.create!(
payment_method: payment_method,
amount: order.outstanding_balance
)
如果业务允许部分支付,只有当所有有效且已完成 payment 的金额合计覆盖订单余额后,订单才能完成。
2. 完成 payment 后刷新订单支付汇总
支付状态改变后,重新加载订单,并调用当前 Spree 版本提供的订单更新器。常见写法如下:
payment.capture!
order.reload
Spree::OrderUpdater.new(order).update
order.reload
有些版本提供的方法可能叫作 update_payment_total、update_payment_state 或 update_with_updater!。具体应以项目当前安装版本中的 Spree::OrderUpdater API 为准,不要混用其他版本的示例。
刷新后再次确认:
Rails.logger.info(
order_total: order.total,
payment_total: order.payment_total,
payment_state: order.payment_state,
outstanding_balance: order.outstanding_balance
)
如果 outstanding_balance 仍然大于零,就不应强制调用 order.next!。应先查清金额或汇总数据为何不一致。
3. 使用正确的订单完成状态
payment 的完成状态通常是:
payment.state == "completed"
订单的完成状态通常是:
order.state == "complete"
因此,判断应改为:
order.next! unless order.state == "complete"
如果当前版本提供了谓词,优先使用:
order.next! unless order.complete?
参考实现
异步支付回调可以整理为:
def perform_payment(payment_id, state)
payment = Spree::Payment.find_by(id: payment_id)
return false unless payment
order = payment.order
payment.with_lock do
case state
when "accepted"
payment.started_processing! unless payment.processing?
payment.capture! unless payment.completed?
order.reload
Spree::OrderUpdater.new(order).update
order.reload
Rails.logger.info(
"PAYMENT RESULT => " \
"order_id: #{order.id}, " \
"order_state: #{order.state}, " \
"payment_state: #{payment.reload.state}, " \
"payment_amount: #{payment.amount}, " \
"order_total: #{order.total}, " \
"payment_total: #{order.payment_total}, " \
"outstanding_balance: #{order.outstanding_balance}"
)
if order.outstanding_balance.to_d <= 0
order.next! unless order.complete?
else
Rails.logger.error(
"Order #{order.number} still has an outstanding balance: " \
"#{order.outstanding_balance}"
)
return false
end
when "rejected"
payment.started_processing! unless payment.processing?
payment.failure! unless payment.failed?
end
end
true
rescue StandardError => e
Rails.logger.error(
"Error processing payment #{payment_id}: " \
"#{e.class}: #{e.message}"
)
false
end
processing?、failed?、complete? 等谓词是否可用,取决于具体的 Spree 版本。如果当前版本没有相应谓词,可以直接比较 state。
注意事项
不要反复调用 order.next! 来绕过支付检查。状态机报错通常意味着订单的支付金额或支付汇总尚未满足完成条件。
原代码还有两个容易忽略的问题:
Spree::Payment.find(payment_id)找不到记录时会直接抛出ActiveRecord::RecordNotFound,后面的return unless payment根本不会执行。这里应使用find_by,或者单独处理异常。- 不建议使用
rescue Exception,因为它会捕获系统退出、信号等不应被吞掉的异常。业务代码通常只需捕获StandardError。
如果金额、订单归属和支付汇总都正确,却仍然出现相同错误,就要检查当前 Spree 版本中从 payment 到 complete 的回调,特别是 payment_required?、process_payments! 和 Spree::OrderUpdater 的实现。自定义支付方式也应遵循该版本预期的支付流程,避免先手动将 payment 设为 completed,随后又由订单状态机将其作为待处理支付重复处理。
备注:内容仅供参考。
