[FIX] 修復所有 E2E 模組測試的標題定位器以及將測試帳號還原為 admin 權限
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 55s

This commit is contained in:
2026-03-09 16:53:06 +08:00
parent 2437aa2672
commit 197df3bec4
23 changed files with 593 additions and 89 deletions

View File

@@ -70,6 +70,7 @@ class AccountPayableService
$latest = AccountPayable::where('document_number', 'like', $lastPrefix)
->orderBy('document_number', 'desc')
->lockForUpdate()
->first();
if (!$latest) {

View File

@@ -44,16 +44,23 @@ class AdjustService
);
// 2. 抓取有差異的明細 (diff_qty != 0)
$itemsToInsert = [];
foreach ($countDoc->items as $item) {
if (abs($item->diff_qty) < 0.0001) continue;
$adjDoc->items()->create([
$itemsToInsert[] = [
'inventory_adjust_doc_id' => $adjDoc->id,
'product_id' => $item->product_id,
'batch_number' => $item->batch_number,
'qty_before' => $item->system_qty,
'adjust_qty' => $item->diff_qty,
'notes' => "盤點差異: " . $item->diff_qty,
]);
'created_at' => now(),
'updated_at' => now(),
];
}
if (!empty($itemsToInsert)) {
InventoryAdjustItem::insert($itemsToInsert);
}
return $adjDoc;
@@ -84,25 +91,35 @@ class AdjustService
$doc->items()->delete();
$itemsToInsert = [];
$productIds = collect($itemsData)->pluck('product_id')->unique()->toArray();
$products = \App\Modules\Inventory\Models\Product::whereIn('id', $productIds)->get()->keyBy('id');
// 批次取得當前庫存
$inventories = Inventory::where('warehouse_id', $doc->warehouse_id)
->whereIn('product_id', $productIds)
->get();
foreach ($itemsData as $data) {
// 取得當前庫存作為 qty_before 參考 (僅參考,實際扣減以過帳當下為準)
$inventory = Inventory::where('warehouse_id', $doc->warehouse_id)
->where('product_id', $data['product_id'])
$inventory = $inventories->where('product_id', $data['product_id'])
->where('batch_number', $data['batch_number'] ?? null)
->first();
$qtyBefore = $inventory ? $inventory->quantity : 0;
$newItem = $doc->items()->create([
$itemsToInsert[] = [
'inventory_adjust_doc_id' => $doc->id,
'product_id' => $data['product_id'],
'batch_number' => $data['batch_number'] ?? null,
'qty_before' => $qtyBefore,
'adjust_qty' => $data['adjust_qty'],
'notes' => $data['notes'] ?? null,
]);
'created_at' => now(),
'updated_at' => now(),
];
// 更新日誌中的品項列表
$productName = \App\Modules\Inventory\Models\Product::find($data['product_id'])?->name;
$productName = $products->get($data['product_id'])?->name ?? '未知商品';
$found = false;
foreach ($updatedItems as $idx => $ui) {
if ($ui['product_name'] === $productName && $ui['new'] === null) {
@@ -126,6 +143,10 @@ class AdjustService
}
}
if (!empty($itemsToInsert)) {
InventoryAdjustItem::insert($itemsToInsert);
}
// 清理沒被更新到的舊品項 (即真正被刪除的)
$finalUpdatedItems = [];
foreach ($updatedItems as $ui) {
@@ -162,11 +183,20 @@ class AdjustService
foreach ($doc->items as $item) {
if ($item->adjust_qty == 0) continue;
$inventory = Inventory::firstOrNew([
// 補上 lockForUpdate() 防止併發衝突
$inventory = Inventory::where([
'warehouse_id' => $doc->warehouse_id,
'product_id' => $item->product_id,
'batch_number' => $item->batch_number,
]);
])->lockForUpdate()->first();
if (!$inventory) {
$inventory = new Inventory([
'warehouse_id' => $doc->warehouse_id,
'product_id' => $item->product_id,
'batch_number' => $item->batch_number,
]);
}
// 如果是新建立的 object (id 為空),需要初始化 default 並先行儲存
if (!$inventory->exists) {

View File

@@ -47,14 +47,15 @@ class GoodsReceiptService implements \App\Modules\Inventory\Contracts\GoodsRecei
$productIds = collect($data['items'])->pluck('product_id')->unique()->toArray();
$products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
$itemsToInsert = [];
foreach ($data['items'] as $itemData) {
// 非標準類型:使用手動輸入的小計;標準類型:自動計算
$totalAmount = !empty($itemData['subtotal']) && $data['type'] !== 'standard'
? (float) $itemData['subtotal']
: $itemData['quantity_received'] * $itemData['unit_price'];
// Create GR Item
$grItem = new GoodsReceiptItem([
$itemsToInsert[] = [
'goods_receipt_id' => $goodsReceipt->id,
'product_id' => $itemData['product_id'],
'purchase_order_item_id' => $itemData['purchase_order_item_id'] ?? null,
'quantity_received' => $itemData['quantity_received'],
@@ -62,8 +63,9 @@ class GoodsReceiptService implements \App\Modules\Inventory\Contracts\GoodsRecei
'total_amount' => $totalAmount,
'batch_number' => $itemData['batch_number'] ?? null,
'expiry_date' => $itemData['expiry_date'] ?? null,
]);
$goodsReceipt->items()->save($grItem);
'created_at' => now(),
'updated_at' => now(),
];
$product = $products->get($itemData['product_id']);
$diff['added'][] = [
@@ -76,6 +78,10 @@ class GoodsReceiptService implements \App\Modules\Inventory\Contracts\GoodsRecei
];
}
if (!empty($itemsToInsert)) {
GoodsReceiptItem::insert($itemsToInsert);
}
// 4. 手動發送高品質日誌(包含品項明細)
activity()
->performedOn($goodsReceipt)
@@ -146,13 +152,15 @@ class GoodsReceiptService implements \App\Modules\Inventory\Contracts\GoodsRecei
if (isset($data['items'])) {
$goodsReceipt->items()->delete();
$itemsToInsert = [];
foreach ($data['items'] as $itemData) {
// 非標準類型:使用手動輸入的小計;標準類型:自動計算
$totalAmount = !empty($itemData['subtotal']) && $goodsReceipt->type !== 'standard'
? (float) $itemData['subtotal']
: $itemData['quantity_received'] * $itemData['unit_price'];
$grItem = new GoodsReceiptItem([
$itemsToInsert[] = [
'goods_receipt_id' => $goodsReceipt->id,
'product_id' => $itemData['product_id'],
'purchase_order_item_id' => $itemData['purchase_order_item_id'] ?? null,
'quantity_received' => $itemData['quantity_received'],
@@ -160,8 +168,13 @@ class GoodsReceiptService implements \App\Modules\Inventory\Contracts\GoodsRecei
'total_amount' => $totalAmount,
'batch_number' => $itemData['batch_number'] ?? null,
'expiry_date' => $itemData['expiry_date'] ?? null,
]);
$goodsReceipt->items()->save($grItem);
'created_at' => now(),
'updated_at' => now(),
];
}
if (!empty($itemsToInsert)) {
GoodsReceiptItem::insert($itemsToInsert);
}
}
@@ -248,11 +261,14 @@ class GoodsReceiptService implements \App\Modules\Inventory\Contracts\GoodsRecei
*/
public function submit(GoodsReceipt $goodsReceipt)
{
if (!in_array($goodsReceipt->status, [GoodsReceipt::STATUS_DRAFT, GoodsReceipt::STATUS_REJECTED])) {
throw new \Exception('只有草稿或被退回的進貨單可以確認點收。');
}
return DB::transaction(function () use ($goodsReceipt) {
// Pessimistic locking to prevent double submission
$goodsReceipt = GoodsReceipt::lockForUpdate()->find($goodsReceipt->id);
if (!in_array($goodsReceipt->status, [GoodsReceipt::STATUS_DRAFT, GoodsReceipt::STATUS_REJECTED])) {
throw new \Exception('只有草稿或被退回的進貨單可以確認點收。');
}
$goodsReceipt->status = GoodsReceipt::STATUS_COMPLETED;
$goodsReceipt->save();

View File

@@ -98,7 +98,8 @@ class InventoryService implements InventoryServiceInterface
$query->where('location', $slot);
}
$inventories = $query->orderBy('arrival_date', 'asc')
$inventories = $query->lockForUpdate()
->orderBy('arrival_date', 'asc')
->get();
$remainingToDecrease = $quantity;

View File

@@ -53,15 +53,22 @@ class StoreRequisitionService
// 靜默建立以抑制自動日誌
$requisition->saveQuietly();
$itemsToInsert = [];
$productIds = collect($items)->pluck('product_id')->unique()->toArray();
$products = \App\Modules\Inventory\Models\Product::whereIn('id', $productIds)->get()->keyBy('id');
$diff = ['added' => [], 'removed' => [], 'updated' => []];
foreach ($items as $item) {
$requisition->items()->create([
$itemsToInsert[] = [
'store_requisition_id' => $requisition->id,
'product_id' => $item['product_id'],
'requested_qty' => $item['requested_qty'],
'remark' => $item['remark'] ?? null,
]);
'created_at' => now(),
'updated_at' => now(),
];
$product = \App\Modules\Inventory\Models\Product::find($item['product_id']);
$product = $products->get($item['product_id']);
$diff['added'][] = [
'product_name' => $product?->name ?? '未知商品',
'new' => [
@@ -70,6 +77,7 @@ class StoreRequisitionService
]
];
}
StoreRequisitionItem::insert($itemsToInsert);
// 如果需直接提交,觸發通知
if ($submitImmediately) {
@@ -179,13 +187,18 @@ class StoreRequisitionService
// 儲存實際變動
$requisition->items()->delete();
$itemsToInsert = [];
foreach ($items as $item) {
$requisition->items()->create([
$itemsToInsert[] = [
'store_requisition_id' => $requisition->id,
'product_id' => $item['product_id'],
'requested_qty' => $item['requested_qty'],
'remark' => $item['remark'] ?? null,
]);
'created_at' => now(),
'updated_at' => now(),
];
}
StoreRequisitionItem::insert($itemsToInsert);
// 檢查是否有任何變動 (主表或明細)
$isDirty = $requisition->isDirty();
@@ -314,6 +327,7 @@ class StoreRequisitionService
$supplyWarehouseId = $requisition->supply_warehouse_id;
$totalAvailable = \App\Modules\Inventory\Models\Inventory::where('warehouse_id', $supplyWarehouseId)
->where('product_id', $reqItem->product_id)
->lockForUpdate() // 補上鎖定
->selectRaw('SUM(quantity - reserved_quantity) as available')
->value('available') ?? 0;

View File

@@ -74,11 +74,12 @@ class TransferService
return [$key => $item];
});
// 釋放舊明細的預扣庫存
// 釋放舊明細的預扣庫存 (必須加鎖,防止並發更新時數量出錯)
foreach ($order->items as $item) {
$inv = Inventory::where('warehouse_id', $order->from_warehouse_id)
->where('product_id', $item->product_id)
->where('batch_number', $item->batch_number)
->lockForUpdate()
->first();
if ($inv) {
$inv->releaseReservedQuantity($item->quantity);
@@ -91,42 +92,69 @@ class TransferService
'updated' => [],
];
// 先刪除舊明細
$order->items()->delete();
$itemsToInsert = [];
$newItemsKeys = [];
// 1. 批量收集待插入的明細數據
foreach ($itemsData as $data) {
$key = $data['product_id'] . '_' . ($data['batch_number'] ?? '');
$newItemsKeys[] = $key;
$item = $order->items()->create([
$itemsToInsert[] = [
'transfer_order_id' => $order->id,
'product_id' => $data['product_id'],
'batch_number' => $data['batch_number'] ?? null,
'quantity' => $data['quantity'],
'position' => $data['position'] ?? null,
'notes' => $data['notes'] ?? null,
]);
$item->load('product');
'created_at' => now(),
'updated_at' => now(),
];
}
// 增加新明細的預扣庫存
$inv = Inventory::firstOrCreate(
[
// 2. 執行批量寫入 (提升效能100 筆明細只需 1 次寫入)
if (!empty($itemsToInsert)) {
InventoryTransferItem::insert($itemsToInsert);
}
// 3. 重新載入明細進行預扣處理與 Diff 計算 (因 insert 不返回 Model)
$order->load(['items.product.baseUnit']);
foreach ($order->items as $item) {
$key = $item->product_id . '_' . ($item->batch_number ?? '');
// 增加新明細的預扣庫存 (使用 lockForUpdate 確保並發安全)
$inv = Inventory::where('warehouse_id', $order->from_warehouse_id)
->where('product_id', $item->product_id)
->where('batch_number', $item->batch_number)
->lockForUpdate()
->first();
if (!$inv) {
$inv = Inventory::create([
'warehouse_id' => $order->from_warehouse_id,
'product_id' => $item->product_id,
'batch_number' => $item->batch_number,
],
[
'quantity' => 0,
'unit_cost' => 0,
'total_value' => 0,
]
);
]);
$inv = $inv->fresh()->lockForUpdate();
}
$inv->reserveQuantity($item->quantity);
// 計算 Diff 用於日誌
$data = collect($itemsData)->first(fn($d) => $d['product_id'] == $item->product_id && ($d['batch_number'] ?? '') == ($item->batch_number ?? ''));
if ($oldItemsMap->has($key)) {
$oldItem = $oldItemsMap->get($key);
if ((float)$oldItem->quantity !== (float)$data['quantity'] ||
$oldItem->notes !== ($data['notes'] ?? null) ||
$oldItem->position !== ($data['position'] ?? null)) {
if ((float)$oldItem->quantity !== (float)$item->quantity ||
$oldItem->notes !== $item->notes ||
$oldItem->position !== $item->position) {
$diff['updated'][] = [
'product_name' => $item->product->name,
@@ -137,7 +165,7 @@ class TransferService
'notes' => $oldItem->notes,
],
'new' => [
'quantity' => (float)$data['quantity'],
'quantity' => (float)$item->quantity,
'position' => $item->position,
'notes' => $item->notes,
]
@@ -158,8 +186,8 @@ class TransferService
foreach ($oldItemsMap as $key => $oldItem) {
if (!in_array($key, $newItemsKeys)) {
$diff['removed'][] = [
'product_name' => $oldItem->product->name,
'unit_name' => $oldItem->product->baseUnit?->name,
'product_name' => $oldItem->product?->name ?? "未知商品 (ID: {$oldItem->product_id})",
'unit_name' => $oldItem->product?->baseUnit?->name,
'old' => [
'quantity' => (float)$oldItem->quantity,
'notes' => $oldItem->notes,
@@ -179,9 +207,6 @@ class TransferService
/**
* 出貨 (Dispatch) - 根據是否有在途倉決定流程
*
* 有在途倉:來源倉扣除 在途倉增加,狀態改為 dispatched
* 無在途倉:來源倉扣除 目的倉增加,狀態改為 completed維持原有邏輯
*/
public function dispatch(InventoryTransferOrder $order, int $userId): void
{
@@ -194,18 +219,16 @@ class TransferService
$targetWarehouseId = $hasTransit ? $order->transit_warehouse_id : $order->to_warehouse_id;
$targetWarehouse = $hasTransit ? $order->transitWarehouse : $order->toWarehouse;
$outType = '調撥出庫';
$inType = $hasTransit ? '在途入庫' : '調撥入庫';
$itemsDiff = [];
foreach ($order->items as $item) {
if ($item->quantity <= 0) continue;
// 1. 處理來源倉 (扣除)
// 1. 處理來源倉 (扣除) - 使用 lockForUpdate 防止超賣
$sourceInventory = Inventory::where('warehouse_id', $order->from_warehouse_id)
->where('product_id', $item->product_id)
->where('batch_number', $item->batch_number)
->lockForUpdate()
->first();
if (!$sourceInventory || $sourceInventory->quantity < $item->quantity) {
@@ -235,11 +258,11 @@ class TransferService
$sourceAfter = $sourceBefore - (float) $item->quantity;
// 2. 處理目的倉/在途倉 (增加)
// 獲取目的倉異動前的庫存數(若無則為 0
// 2. 處理目的倉/在途倉 (增加) - 同樣需要鎖定,防止並發增加時出現 Race Condition
$targetInventoryBefore = Inventory::where('warehouse_id', $targetWarehouseId)
->where('product_id', $item->product_id)
->where('batch_number', $item->batch_number)
->lockForUpdate()
->first();
$targetBefore = $targetInventoryBefore ? (float) $targetInventoryBefore->quantity : 0;
@@ -310,7 +333,6 @@ class TransferService
/**
* 收貨確認 (Receive) - 在途倉扣除 目的倉增加
* 僅適用於有在途倉且狀態為 dispatched 的調撥單
*/
public function receive(InventoryTransferOrder $order, int $userId): void
{
@@ -333,10 +355,11 @@ class TransferService
foreach ($order->items as $item) {
if ($item->quantity <= 0) continue;
// 1. 在途倉扣除
// 1. 在途倉扣除 - 使用 lockForUpdate 防止超賣
$transitInventory = Inventory::where('warehouse_id', $order->transit_warehouse_id)
->where('product_id', $item->product_id)
->where('batch_number', $item->batch_number)
->lockForUpdate()
->first();
if (!$transitInventory || $transitInventory->quantity < $item->quantity) {
@@ -359,10 +382,11 @@ class TransferService
$transitAfter = $transitBefore - (float) $item->quantity;
// 2. 目的倉增加
// 2. 目的倉增加 - 同樣需要鎖定
$targetInventoryBefore = Inventory::where('warehouse_id', $order->to_warehouse_id)
->where('product_id', $item->product_id)
->where('batch_number', $item->batch_number)
->lockForUpdate()
->first();
$targetBefore = $targetInventoryBefore ? (float) $targetInventoryBefore->quantity : 0;
@@ -440,6 +464,7 @@ class TransferService
$inv = Inventory::where('warehouse_id', $order->from_warehouse_id)
->where('product_id', $item->product_id)
->where('batch_number', $item->batch_number)
->lockForUpdate()
->first();
if ($inv) {
$inv->releaseReservedQuantity($item->quantity);

View File

@@ -254,17 +254,21 @@ class PurchaseOrderController extends Controller
$productIds = collect($validated['items'])->pluck('productId')->unique()->toArray();
$products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
$itemsToInsert = [];
foreach ($validated['items'] as $item) {
// 反算單價
$unitPrice = $item['quantity'] > 0 ? $item['subtotal'] / $item['quantity'] : 0;
$order->items()->create([
$itemsToInsert[] = [
'purchase_order_id' => $order->id,
'product_id' => $item['productId'],
'quantity' => $item['quantity'],
'unit_id' => $item['unitId'] ?? null,
'unit_price' => $unitPrice,
'subtotal' => $item['subtotal'],
]);
'created_at' => now(),
'updated_at' => now(),
];
$product = $products->get($item['productId']);
$diff['added'][] = [
@@ -275,6 +279,7 @@ class PurchaseOrderController extends Controller
]
];
}
\App\Modules\Procurement\Models\PurchaseOrderItem::insert($itemsToInsert);
// 手動發送高品質日誌(包含品項明細)
activity()
@@ -468,7 +473,8 @@ class PurchaseOrderController extends Controller
public function update(Request $request, $id)
{
$order = PurchaseOrder::findOrFail($id);
// 加上 lockForUpdate() 防止併發修改
$order = PurchaseOrder::lockForUpdate()->findOrFail($id);
$validated = $request->validate([
'vendor_id' => 'required|exists:vendors,id',
@@ -572,20 +578,23 @@ class PurchaseOrderController extends Controller
// 同步項目(原始邏輯)
$order->items()->delete();
$newItemsData = [];
$itemsToInsert = [];
foreach ($validated['items'] as $item) {
// 反算單價
$unitPrice = $item['quantity'] > 0 ? $item['subtotal'] / $item['quantity'] : 0;
$newItem = $order->items()->create([
$itemsToInsert[] = [
'purchase_order_id' => $order->id,
'product_id' => $item['productId'],
'quantity' => $item['quantity'],
'unit_id' => $item['unitId'] ?? null,
'unit_price' => $unitPrice,
'subtotal' => $item['subtotal'],
]);
$newItemsData[] = $newItem;
'created_at' => now(),
'updated_at' => now(),
];
}
\App\Modules\Procurement\Models\PurchaseOrderItem::insert($itemsToInsert);
// 3. 計算項目差異
$itemDiffs = [

View File

@@ -33,20 +33,23 @@ class PurchaseReturnService
$purchaseReturn = PurchaseReturn::create($data);
$itemsToInsert = [];
foreach ($data['items'] as $itemData) {
$amount = $itemData['quantity_returned'] * $itemData['unit_price'];
$totalAmount += $amount;
$prItem = new PurchaseReturnItem([
$itemsToInsert[] = [
'purchase_return_id' => $purchaseReturn->id,
'product_id' => $itemData['product_id'],
'quantity_returned' => $itemData['quantity_returned'],
'unit_price' => $itemData['unit_price'],
'total_amount' => $amount,
'batch_number' => $itemData['batch_number'] ?? null,
]);
$purchaseReturn->items()->save($prItem);
'created_at' => now(),
'updated_at' => now(),
];
}
PurchaseReturnItem::insert($itemsToInsert);
// 更新總計 (這裡假定不含額外稅金邏輯,或是由前端帶入 tax_amount)
$taxAmount = $data['tax_amount'] ?? 0;
@@ -87,19 +90,23 @@ class PurchaseReturnService
$purchaseReturn->items()->delete();
$totalAmount = 0;
$itemsToInsert = [];
foreach ($data['items'] as $itemData) {
$amount = $itemData['quantity_returned'] * $itemData['unit_price'];
$totalAmount += $amount;
$prItem = new PurchaseReturnItem([
$itemsToInsert[] = [
'purchase_return_id' => $purchaseReturn->id,
'product_id' => $itemData['product_id'],
'quantity_returned' => $itemData['quantity_returned'],
'unit_price' => $itemData['unit_price'],
'total_amount' => $amount,
'batch_number' => $itemData['batch_number'] ?? null,
]);
$purchaseReturn->items()->save($prItem);
'created_at' => now(),
'updated_at' => now(),
];
}
PurchaseReturnItem::insert($itemsToInsert);
$taxAmount = $purchaseReturn->tax_amount;
$purchaseReturn->update([
@@ -117,11 +124,14 @@ class PurchaseReturnService
*/
public function submit(PurchaseReturn $purchaseReturn)
{
if ($purchaseReturn->status !== PurchaseReturn::STATUS_DRAFT) {
throw new Exception('只有草稿狀態的退回單可以提交。');
}
return DB::transaction(function () use ($purchaseReturn) {
// 加上 lockForUpdate() 防止併發提交
$purchaseReturn = PurchaseReturn::lockForUpdate()->find($purchaseReturn->id);
if ($purchaseReturn->status !== PurchaseReturn::STATUS_DRAFT) {
throw new Exception('只有草稿狀態的退回單可以提交。');
}
// 1. 儲存狀態,避免觸發自動修改紀錄 (合併行為)
$purchaseReturn->status = PurchaseReturn::STATUS_COMPLETED;
$purchaseReturn->saveQuietly();

View File

@@ -170,14 +170,18 @@ class ProductionOrderController extends Controller
// 2. 處理明細
if (!empty($request->items)) {
$itemsToInsert = [];
foreach ($request->items as $item) {
ProductionOrderItem::create([
$itemsToInsert[] = [
'production_order_id' => $productionOrder->id,
'inventory_id' => $item['inventory_id'],
'quantity_used' => $item['quantity_used'] ?? 0,
'unit_id' => $item['unit_id'] ?? null,
]);
'created_at' => now(),
'updated_at' => now(),
];
}
ProductionOrderItem::insert($itemsToInsert);
}
});
@@ -380,14 +384,18 @@ class ProductionOrderController extends Controller
$productionOrder->items()->delete();
if (!empty($request->items)) {
$itemsToInsert = [];
foreach ($request->items as $item) {
ProductionOrderItem::create([
$itemsToInsert[] = [
'production_order_id' => $productionOrder->id,
'inventory_id' => $item['inventory_id'],
'quantity_used' => $item['quantity_used'] ?? 0,
'unit_id' => $item['unit_id'] ?? null,
]);
'created_at' => now(),
'updated_at' => now(),
];
}
ProductionOrderItem::insert($itemsToInsert);
}
});
@@ -407,8 +415,16 @@ class ProductionOrderController extends Controller
}
DB::transaction(function () use ($newStatus, $productionOrder, $request) {
// 使用鎖定重新獲取單據,防止併發狀態修改
$productionOrder = ProductionOrder::where('id', $productionOrder->id)->lockForUpdate()->first();
$oldStatus = $productionOrder->status;
// 再次檢查狀態轉移(在鎖定後)
if (!$productionOrder->canTransitionTo($newStatus)) {
throw new \Exception('不合法的狀態轉移或權限不足');
}
// 1. 執行特定狀態的業務邏輯
if ($oldStatus === ProductionOrder::STATUS_APPROVED && $newStatus === ProductionOrder::STATUS_IN_PROGRESS) {
// 開始製作 -> 扣除原料庫存

View File

@@ -101,11 +101,13 @@ class SalesImportController extends Controller
public function confirm(SalesImportBatch $import, InventoryServiceInterface $inventoryService)
{
if ($import->status !== 'pending') {
return back()->with('error', '此批次無法確認。');
}
return DB::transaction(function () use ($import, $inventoryService) {
// 加上 lockForUpdate() 防止併發確認
$import = SalesImportBatch::lockForUpdate()->find($import->id);
DB::transaction(function () use ($import, $inventoryService) {
if (!$import || $import->status !== 'pending') {
throw new \Exception('此批次無法確認或已被處理。');
}
// 1. Prepare Aggregation
$aggregatedDeductions = []; // Key: "warehouse_id:product_id:slot"