
在 laravel 库存系统中,删除已 finalized_at 的收货单(Receipt)或销售单(Sale)时,需同步反向修正对应商品库存(如加回已扣减的库存),否则将导致库存数据失真;本文提供符合 Eloquent 惯例、事务安全且易于维护的控制器层解决方案。
在 laravel 库存系统中,删除已 `finalized_at` 的收货单(receipt)或销售单(sale)时,需同步反向修正对应商品库存(如加回已扣减的库存),否则将导致库存数据失真;本文提供符合 eloquent 惯例、事务安全且易于维护的控制器层解决方案。
在库存类应用中,单据的「终态性」至关重要:一旦收货单或销售单被标记为已结算(即 finalized_at 不为空),其对库存的影响即生效。此时若直接删除单据而不还原库存变更,将造成数据库状态与实际业务严重偏离——例如,已出库的 10 台设备被删除后,库存仍显示为 -10,破坏数据一致性。
因此,正确的做法是在 destroy 方法中先判断单据是否已结算,再执行逆向库存操作,最后才调用 $model->delete()。该逻辑应置于控制器中(而非模型的 deleting 事件),原因在于:
- ✅ 控制器层更贴近业务上下文,便于统一处理权限、日志、通知等横切关注点;
- ✅ 避免在 Eloquent 事件中隐式修改关联模型(如 $product->save()),防止意外触发其他监听器或验证;
- ✅ 易于单元测试和调试,行为明确、无副作用。
以下是推荐实现(以 ReceiptController@destroy 为例):
// app/Http/Controllers/ReceiptController.php public function destroy(Receipt $receipt) { // 启用数据库事务确保原子性(强烈建议) DB::transaction(function () use ($receipt) { if ($receipt->finalized_at) { foreach ($receipt->products as $receivedProduct) { $product = $receivedProduct->product; $product->stock -= $receivedProduct->stock; $product->stock_defective -= $receivedProduct->stock_defective; $product->save(); } } $receipt->delete(); // 软删除?请确认是否启用 SoftDeletes }); return redirect() ->route('receipts.index') ->withStatus('Receipt successfully removed.'); }
同理,SaleController@destroy 需执行库存回加与客户余额返还:
// app/Http/Controllers/SaleController.php public function destroy(Sale $sale) { DB::transaction(function () use ($sale) { if ($sale->finalized_at) { foreach ($sale->products as $soldProduct) { $product = $soldProduct->product; $product->stock += $soldProduct->qty; $product->save(); } // 还原客户账户余额(注意:需确保 client 关系已加载) $sale->client->balance += $sale->total_amount; $sale->client->save(); } $sale->delete(); }); return redirect() ->route('sales.index') ->withStatus('The sale record has been successfully deleted.'); }
⚠️ 关键注意事项:
- 务必使用 DB::transaction():库存回滚与单据删除必须原子执行,避免部分成功导致数据不一致;
- 预加载关联关系:确保 $receipt->products 和 $sale->products 已通过 with(‘products.product’) 预加载,否则循环中会触发 N+1 查询;可在 Route Model Binding 或查询构造时优化:
// 在控制器方法参数中直接预加载(Laravel 9+) public function destroy(Receipt $receipt) { $receipt->load('products.product'); // ... } - 软删除兼容性:若 Receipt/Sale 启用了 SoftDeletes,$receipt->delete() 仅软删,需确认业务是否允许软删后库存仍保持“已结算”状态;如需硬删,请显式调用 $receipt->forceDelete() 并补充相应校验;
- 权限与审计:生产环境应在 destroy 前添加授权检查(如 authorize(‘delete’, $receipt))及操作日志记录(如使用 Log::info() 记录回滚详情)。
综上,该方案以清晰的控制流、显式的事务边界和可预测的副作用,兼顾了数据一致性、代码可读性与 Laravel 最佳实践。无需侵入模型层或引入复杂事件机制,即可稳健解决库存反向修正问题。