[REFACTOR] 統一訂單同步 API 錯誤回應與修正 Linter 警告
This commit is contained in:
@@ -58,29 +58,35 @@ class SyncOrderAction
|
||||
];
|
||||
}
|
||||
|
||||
// --- 預檢 (Pre-flight check) N+1 優化 ---
|
||||
// --- 預檢 (Pre-flight check) 僅使用 product_id ---
|
||||
$items = $data['items'];
|
||||
$posProductIds = array_column($items, 'pos_product_id');
|
||||
$targetErpIds = array_column($items, 'product_id');
|
||||
|
||||
// 一次性查出所有相關的 Product
|
||||
$products = $this->productService->findByExternalPosIds($posProductIds)->keyBy('external_pos_id');
|
||||
$productsById = $this->productService->findByIds($targetErpIds)->keyBy('id');
|
||||
|
||||
$resolvedProducts = [];
|
||||
$missingIds = [];
|
||||
foreach ($posProductIds as $id) {
|
||||
if (!$products->has($id)) {
|
||||
$missingIds[] = $id;
|
||||
|
||||
foreach ($items as $index => $item) {
|
||||
$productId = $item['product_id'];
|
||||
$product = $productsById->get($productId);
|
||||
|
||||
if ($product) {
|
||||
$resolvedProducts[$index] = $product;
|
||||
} else {
|
||||
$missingIds[] = $productId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($missingIds)) {
|
||||
// 回報所有缺漏的 ID
|
||||
throw ValidationException::withMessages([
|
||||
'items' => ["The following products are not found: " . implode(', ', $missingIds) . ". Please sync products first."]
|
||||
'items' => ["The following product IDs are not found: " . implode(', ', array_unique($missingIds)) . ". Please ensure these products exist in the system."]
|
||||
]);
|
||||
}
|
||||
|
||||
// --- 執行寫入交易 ---
|
||||
$result = DB::transaction(function () use ($data, $items, $products) {
|
||||
$result = DB::transaction(function () use ($data, $items, $resolvedProducts) {
|
||||
// 1. 建立訂單
|
||||
$order = SalesOrder::create([
|
||||
'external_order_id' => $data['external_order_id'],
|
||||
@@ -108,11 +114,12 @@ class SyncOrderAction
|
||||
|
||||
// 3. 處理訂單明細
|
||||
$orderItemsData = [];
|
||||
foreach ($items as $itemData) {
|
||||
$product = $products->get($itemData['pos_product_id']);
|
||||
foreach ($items as $index => $itemData) {
|
||||
$product = $resolvedProducts[$index];
|
||||
|
||||
$qty = $itemData['qty'];
|
||||
$price = $itemData['price'];
|
||||
$batchNumber = $itemData['batch_number'] ?? null;
|
||||
$lineTotal = $qty * $price;
|
||||
$totalAmount += $lineTotal;
|
||||
|
||||
@@ -134,9 +141,10 @@ class SyncOrderAction
|
||||
$qty,
|
||||
"POS Order: " . $order->external_order_id,
|
||||
true,
|
||||
null,
|
||||
null, // Slot (location)
|
||||
\App\Modules\Integration\Models\SalesOrder::class,
|
||||
$order->id
|
||||
$order->id,
|
||||
$batchNumber
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,13 @@ class InventorySyncController extends Controller
|
||||
* @param string $warehouseCode
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function show(string $warehouseCode): JsonResponse
|
||||
public function show(\Illuminate\Http\Request $request, string $warehouseCode): JsonResponse
|
||||
{
|
||||
// 透過 Service 調用跨模組庫存查詢功能
|
||||
$inventoryData = $this->inventoryService->getPosInventoryByWarehouseCode($warehouseCode);
|
||||
// 透過 Service 調用跨模組庫存查詢功能,傳入篩選條件
|
||||
$inventoryData = $this->inventoryService->getPosInventoryByWarehouseCode(
|
||||
$warehouseCode,
|
||||
$request->only(['product_id', 'barcode', 'code', 'external_pos_id'])
|
||||
);
|
||||
|
||||
// 若回傳 null,表示尋無此倉庫代碼
|
||||
if (is_null($inventoryData)) {
|
||||
@@ -40,9 +43,18 @@ class InventorySyncController extends Controller
|
||||
'warehouse_code' => $warehouseCode,
|
||||
'data' => $inventoryData->map(function ($item) {
|
||||
return [
|
||||
'product_id' => $item->product_id,
|
||||
'external_pos_id' => $item->external_pos_id,
|
||||
'product_code' => $item->product_code,
|
||||
'product_name' => $item->product_name,
|
||||
'barcode' => $item->barcode,
|
||||
'category_name' => $item->category_name ?? '未分類',
|
||||
'unit_name' => $item->unit_name ?? '個',
|
||||
'price' => (float) $item->price,
|
||||
'brand' => $item->brand,
|
||||
'specification' => $item->specification,
|
||||
'batch_number' => $item->batch_number,
|
||||
'expiry_date' => $item->expiry_date,
|
||||
'quantity' => (float) $item->total_quantity,
|
||||
];
|
||||
})
|
||||
|
||||
@@ -23,8 +23,8 @@ class ProductSyncController extends Controller
|
||||
'name' => 'required|string|max:255',
|
||||
'price' => 'nullable|numeric|min:0|max:99999999.99',
|
||||
'barcode' => 'nullable|string|max:100',
|
||||
'category' => 'nullable|string|max:100',
|
||||
'unit' => 'nullable|string|max:100',
|
||||
'category' => 'required|string|max:100',
|
||||
'unit' => 'required|string|max:100',
|
||||
'brand' => 'nullable|string|max:100',
|
||||
'specification' => 'nullable|string|max:255',
|
||||
'cost_price' => 'nullable|numeric|min:0|max:99999999.99',
|
||||
@@ -41,6 +41,8 @@ class ProductSyncController extends Controller
|
||||
'data' => [
|
||||
'id' => $product->id,
|
||||
'external_pos_id' => $product->external_pos_id,
|
||||
'code' => $product->code,
|
||||
'barcode' => $product->barcode,
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
@@ -50,4 +52,63 @@ class ProductSyncController extends Controller
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜尋商品(供外部 API 使用)。
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'product_id' => 'nullable|integer',
|
||||
'external_pos_id' => 'nullable|string|max:255',
|
||||
'barcode' => 'nullable|string|max:100',
|
||||
'code' => 'nullable|string|max:100',
|
||||
'category' => 'nullable|string|max:100',
|
||||
'updated_after' => 'nullable|date',
|
||||
'per_page' => 'nullable|integer|min:1|max:100',
|
||||
]);
|
||||
|
||||
try {
|
||||
$perPage = $request->input('per_page', 50);
|
||||
$products = $this->productService->searchProducts($request->all(), $perPage);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => $products->getCollection()->map(function ($product) {
|
||||
return [
|
||||
'id' => $product->id,
|
||||
'code' => $product->code,
|
||||
'barcode' => $product->barcode,
|
||||
'name' => $product->name,
|
||||
'external_pos_id' => $product->external_pos_id,
|
||||
'category_name' => $product->category?->name ?? '未分類',
|
||||
'brand' => $product->brand,
|
||||
'specification' => $product->specification,
|
||||
'unit_name' => $product->baseUnit?->name ?? '個',
|
||||
'price' => (float) $product->price,
|
||||
'cost_price' => (float) $product->cost_price,
|
||||
'member_price' => (float) $product->member_price,
|
||||
'wholesale_price' => (float) $product->wholesale_price,
|
||||
'is_active' => (bool) $product->is_active,
|
||||
'updated_at' => $product->updated_at->format('Y-m-d H:i:s'),
|
||||
];
|
||||
}),
|
||||
'meta' => [
|
||||
'current_page' => $products->currentPage(),
|
||||
'last_page' => $products->lastPage(),
|
||||
'per_page' => $products->perPage(),
|
||||
'total' => $products->total(),
|
||||
]
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Product Search Failed', ['error' => $e->getMessage()]);
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Search failed: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ class SyncOrderRequest extends FormRequest
|
||||
'payment_method' => 'nullable|string|in:cash,credit_card,line_pay,ecpay,transfer,other',
|
||||
'sold_at' => 'nullable|date',
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.pos_product_id' => 'required|string',
|
||||
'items.*.product_id' => 'required|integer',
|
||||
'items.*.batch_number' => 'nullable|string',
|
||||
'items.*.qty' => 'required|numeric|min:0.0001',
|
||||
'items.*.price' => 'required|numeric|min:0',
|
||||
];
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Modules\Integration\Controllers\InventorySyncController;
|
||||
Route::prefix('api/v1/integration')
|
||||
->middleware(['api', 'throttle:integration', 'integration.tenant', 'auth:sanctum'])
|
||||
->group(function () {
|
||||
Route::get('products', [ProductSyncController::class, 'index']);
|
||||
Route::post('products/upsert', [ProductSyncController::class, 'upsert']);
|
||||
Route::post('orders', [OrderSyncController::class, 'store']);
|
||||
Route::post('vending/orders', [VendingOrderSyncController::class, 'store']);
|
||||
|
||||
@@ -21,9 +21,12 @@ interface InventoryServiceInterface
|
||||
* @param string|null $reason
|
||||
* @param bool $force
|
||||
* @param string|null $slot
|
||||
* @param string|null $referenceType
|
||||
* @param int|string|null $referenceId
|
||||
* @param string|null $batchNumber
|
||||
* @return void
|
||||
*/
|
||||
public function decreaseStock(int $productId, int $warehouseId, float $quantity, ?string $reason = null, bool $force = false, ?string $slot = null, ?string $referenceType = null, $referenceId = null): void;
|
||||
public function decreaseStock(int $productId, int $warehouseId, float $quantity, ?string $reason = null, bool $force = false, ?string $slot = null, ?string $referenceType = null, $referenceId = null, ?string $batchNumber = null): void;
|
||||
|
||||
/**
|
||||
* Get all active warehouses.
|
||||
@@ -162,9 +165,10 @@ interface InventoryServiceInterface
|
||||
* Get inventory summary (group by product) for a specific warehouse code
|
||||
*
|
||||
* @param string $code
|
||||
* @param array $filters
|
||||
* @return \Illuminate\Support\Collection|null
|
||||
*/
|
||||
public function getPosInventoryByWarehouseCode(string $code);
|
||||
public function getPosInventoryByWarehouseCode(string $code, array $filters = []);
|
||||
|
||||
/**
|
||||
* 處理批量入庫邏輯 (含批號產生與現有批號累加)。
|
||||
|
||||
@@ -31,6 +31,14 @@ interface ProductServiceInterface
|
||||
*/
|
||||
public function findByExternalPosIds(array $externalPosIds);
|
||||
|
||||
/**
|
||||
* 透過多個 ERP 內部 ID 查找產品。
|
||||
*
|
||||
* @param array $ids
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public function findByIds(array $ids);
|
||||
|
||||
/**
|
||||
* 透過多個 ERP 商品代碼查找產品(供販賣機 API 使用)。
|
||||
*
|
||||
@@ -78,4 +86,13 @@ interface ProductServiceInterface
|
||||
* @return \App\Modules\Inventory\Models\Product|null
|
||||
*/
|
||||
public function findByBarcodeOrCode(?string $barcode, ?string $code);
|
||||
|
||||
/**
|
||||
* 搜尋商品(供外部 API 使用)。
|
||||
*
|
||||
* @param array $filters
|
||||
* @param int $perPage
|
||||
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||||
*/
|
||||
public function searchProducts(array $filters, int $perPage = 50);
|
||||
}
|
||||
|
||||
@@ -87,42 +87,58 @@ class InventoryService implements InventoryServiceInterface
|
||||
return $stock >= $quantity;
|
||||
}
|
||||
|
||||
public function decreaseStock(int $productId, int $warehouseId, float $quantity, ?string $reason = null, bool $force = false, ?string $slot = null, ?string $referenceType = null, $referenceId = null): void
|
||||
public function decreaseStock(int $productId, int $warehouseId, float $quantity, ?string $reason = null, bool $force = false, ?string $slot = null, ?string $referenceType = null, $referenceId = null, ?string $batchNumber = null): void
|
||||
{
|
||||
DB::transaction(function () use ($productId, $warehouseId, $quantity, $reason, $force, $slot, $referenceType, $referenceId) {
|
||||
$query = Inventory::where('product_id', $productId)
|
||||
->where('warehouse_id', $warehouseId)
|
||||
->where('quantity', '>', 0);
|
||||
|
||||
if ($slot) {
|
||||
$query->where('location', $slot);
|
||||
}
|
||||
DB::transaction(function () use ($productId, $warehouseId, $quantity, $reason, $force, $slot, $referenceType, $referenceId, $batchNumber) {
|
||||
$defaultBatch = 'NO-BATCH';
|
||||
$targetBatch = $batchNumber ?? $defaultBatch;
|
||||
$remainingToDecrease = $quantity;
|
||||
|
||||
$inventories = $query->lockForUpdate()
|
||||
// 1. 優先嘗試扣除指定批號(或預設的 NO-BATCH)
|
||||
$inventories = Inventory::where('product_id', $productId)
|
||||
->where('warehouse_id', $warehouseId)
|
||||
->where('batch_number', $targetBatch)
|
||||
->where('quantity', '>', 0)
|
||||
->when($slot, fn($q) => $q->where('location', $slot))
|
||||
->lockForUpdate()
|
||||
->orderBy('arrival_date', 'asc')
|
||||
->get();
|
||||
|
||||
$remainingToDecrease = $quantity;
|
||||
|
||||
foreach ($inventories as $inventory) {
|
||||
if ($remainingToDecrease <= 0) break;
|
||||
|
||||
$decreaseAmount = min($inventory->quantity, $remainingToDecrease);
|
||||
$this->decreaseInventoryQuantity($inventory->id, $decreaseAmount, $reason, $referenceType, $referenceId);
|
||||
$remainingToDecrease -= $decreaseAmount;
|
||||
}
|
||||
|
||||
// 2. 如果還有剩餘且剛才不是扣 NO-BATCH,則嘗試從 NO-BATCH 補位
|
||||
if ($remainingToDecrease > 0 && $targetBatch !== $defaultBatch) {
|
||||
$fallbackInventories = Inventory::where('product_id', $productId)
|
||||
->where('warehouse_id', $warehouseId)
|
||||
->where('batch_number', $defaultBatch)
|
||||
->where('quantity', '>', 0)
|
||||
->when($slot, fn($q) => $q->where('location', $slot))
|
||||
->lockForUpdate()
|
||||
->orderBy('arrival_date', 'asc')
|
||||
->get();
|
||||
|
||||
foreach ($fallbackInventories as $inventory) {
|
||||
if ($remainingToDecrease <= 0) break;
|
||||
$decreaseAmount = min($inventory->quantity, $remainingToDecrease);
|
||||
$this->decreaseInventoryQuantity($inventory->id, $decreaseAmount, $reason, $referenceType, $referenceId);
|
||||
$remainingToDecrease -= $decreaseAmount;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 處理最終仍不足的情況
|
||||
if ($remainingToDecrease > 0) {
|
||||
if ($force) {
|
||||
// Find any existing inventory record in this warehouse/slot to subtract from, or create one
|
||||
$query = Inventory::where('product_id', $productId)
|
||||
->where('warehouse_id', $warehouseId);
|
||||
|
||||
if ($slot) {
|
||||
$query->where('location', $slot);
|
||||
}
|
||||
|
||||
$inventory = $query->first();
|
||||
// 強制模式下,若指定批號或 NO-BATCH 均不足,統一在 NO-BATCH 建立/扣除負庫存
|
||||
$inventory = Inventory::where('product_id', $productId)
|
||||
->where('warehouse_id', $warehouseId)
|
||||
->where('batch_number', $defaultBatch)
|
||||
->when($slot, fn($q) => $q->where('location', $slot))
|
||||
->first();
|
||||
|
||||
if (!$inventory) {
|
||||
$inventory = Inventory::create([
|
||||
@@ -132,7 +148,7 @@ class InventoryService implements InventoryServiceInterface
|
||||
'quantity' => 0,
|
||||
'unit_cost' => 0,
|
||||
'total_value' => 0,
|
||||
'batch_number' => 'POS-AUTO-' . ($slot ? $slot . '-' : '') . time(),
|
||||
'batch_number' => $defaultBatch,
|
||||
'arrival_date' => now(),
|
||||
'origin_country' => 'TW',
|
||||
'quality_status' => 'normal',
|
||||
@@ -141,7 +157,10 @@ class InventoryService implements InventoryServiceInterface
|
||||
|
||||
$this->decreaseInventoryQuantity($inventory->id, $remainingToDecrease, $reason, $referenceType, $referenceId);
|
||||
} else {
|
||||
throw new \Exception("庫存不足,無法扣除所有請求的數量。");
|
||||
$context = ($targetBatch !== $defaultBatch)
|
||||
? "批號 {$targetBatch} 或 {$defaultBatch}"
|
||||
: "{$defaultBatch}";
|
||||
throw new \Exception("庫存不足,無法扣除所有請求的數量 ({$context})。");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -658,7 +677,7 @@ class InventoryService implements InventoryServiceInterface
|
||||
* @param string $code
|
||||
* @return \Illuminate\Support\Collection|null
|
||||
*/
|
||||
public function getPosInventoryByWarehouseCode(string $code)
|
||||
public function getPosInventoryByWarehouseCode(string $code, array $filters = [])
|
||||
{
|
||||
$warehouse = Warehouse::where('code', $code)->first();
|
||||
|
||||
@@ -666,19 +685,60 @@ class InventoryService implements InventoryServiceInterface
|
||||
return null;
|
||||
}
|
||||
|
||||
// 整理該倉庫的庫存,以 product_id 進行 GROUP BY 並加總 quantity
|
||||
return DB::table('inventories')
|
||||
$query = DB::table('inventories')
|
||||
->join('products', 'inventories.product_id', '=', 'products.id')
|
||||
->leftJoin('categories', 'products.category_id', '=', 'categories.id')
|
||||
->leftJoin('units', 'products.base_unit_id', '=', 'units.id')
|
||||
->where('inventories.warehouse_id', $warehouse->id)
|
||||
->whereNull('inventories.deleted_at')
|
||||
->whereNull('products.deleted_at')
|
||||
->select(
|
||||
'products.id as product_id',
|
||||
'products.external_pos_id',
|
||||
'products.code as product_code',
|
||||
'products.name as product_name',
|
||||
'products.barcode',
|
||||
'categories.name as category_name',
|
||||
'units.name as unit_name',
|
||||
'products.price',
|
||||
'products.brand',
|
||||
'products.specification',
|
||||
'inventories.batch_number',
|
||||
'inventories.expiry_date',
|
||||
DB::raw('SUM(inventories.quantity) as total_quantity')
|
||||
);
|
||||
|
||||
// 加入條件篩選
|
||||
if (!empty($filters['product_id'])) {
|
||||
$query->where('products.id', $filters['product_id']);
|
||||
}
|
||||
|
||||
if (!empty($filters['external_pos_id'])) {
|
||||
$query->where('products.external_pos_id', $filters['external_pos_id']);
|
||||
}
|
||||
|
||||
if (!empty($filters['barcode'])) {
|
||||
$query->where('products.barcode', $filters['barcode']);
|
||||
}
|
||||
|
||||
if (!empty($filters['code'])) {
|
||||
$query->where('products.code', $filters['code']);
|
||||
}
|
||||
|
||||
return $query->groupBy(
|
||||
'inventories.product_id',
|
||||
'products.external_pos_id',
|
||||
'products.code',
|
||||
'products.name',
|
||||
'products.barcode',
|
||||
'categories.name',
|
||||
'units.name',
|
||||
'products.price',
|
||||
'products.brand',
|
||||
'products.specification',
|
||||
'inventories.batch_number',
|
||||
'inventories.expiry_date'
|
||||
)
|
||||
->groupBy('inventories.product_id', 'products.external_pos_id', 'products.code', 'products.name')
|
||||
->get();
|
||||
}
|
||||
|
||||
|
||||
@@ -38,9 +38,22 @@ class ProductService implements ProductServiceInterface
|
||||
|
||||
// Map allowed fields
|
||||
$product->name = $data['name'];
|
||||
$product->barcode = $data['barcode'] ?? $product->barcode;
|
||||
$product->price = $data['price'] ?? 0;
|
||||
|
||||
// Handle Barcode
|
||||
if (!empty($data['barcode'])) {
|
||||
$product->barcode = $data['barcode'];
|
||||
} elseif (empty($product->barcode)) {
|
||||
$product->barcode = $this->generateRandomBarcode();
|
||||
}
|
||||
|
||||
// Handle Code (SKU)
|
||||
if (!empty($data['code'])) {
|
||||
$product->code = $data['code'];
|
||||
} elseif (empty($product->code)) {
|
||||
$product->code = $this->generateRandomCode();
|
||||
}
|
||||
|
||||
// Map newly added extended fields
|
||||
if (isset($data['brand'])) $product->brand = $data['brand'];
|
||||
if (isset($data['specification'])) $product->specification = $data['specification'];
|
||||
@@ -48,11 +61,6 @@ class ProductService implements ProductServiceInterface
|
||||
if (isset($data['member_price'])) $product->member_price = $data['member_price'];
|
||||
if (isset($data['wholesale_price'])) $product->wholesale_price = $data['wholesale_price'];
|
||||
|
||||
// Generate Code if missing (use code or external_id)
|
||||
if (empty($product->code)) {
|
||||
$product->code = $data['code'] ?? $product->external_pos_id;
|
||||
}
|
||||
|
||||
// Handle Category — 每次同步都更新(若有傳入)
|
||||
if (!empty($data['category']) || empty($product->category_id)) {
|
||||
$categoryName = $data['category'] ?? '未分類';
|
||||
@@ -100,6 +108,17 @@ class ProductService implements ProductServiceInterface
|
||||
return Product::whereIn('external_pos_id', $externalPosIds)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 透過多個 ERP 內部 ID 查找產品。
|
||||
*
|
||||
* @param array $ids
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public function findByIds(array $ids)
|
||||
{
|
||||
return Product::whereIn('id', $ids)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 透過多個 ERP 商品代碼查找產品(供販賣機 API 使用)。
|
||||
*
|
||||
@@ -171,11 +190,58 @@ class ProductService implements ProductServiceInterface
|
||||
{
|
||||
$product = null;
|
||||
if (!empty($barcode)) {
|
||||
$product = Product::where('barcode', $barcode)->first();
|
||||
$product = Product::query()->where('barcode', $barcode)->first();
|
||||
}
|
||||
if (!$product && !empty($code)) {
|
||||
$product = Product::where('code', $code)->first();
|
||||
$product = Product::query()->where('code', $code)->first();
|
||||
}
|
||||
return $product;
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜尋商品(供外部 API 使用)。
|
||||
*
|
||||
* @param array $filters
|
||||
* @param int $perPage
|
||||
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||||
*/
|
||||
public function searchProducts(array $filters, int $perPage = 50)
|
||||
{
|
||||
/** @var \Illuminate\Database\Eloquent\Builder $query */
|
||||
$query = Product::query()
|
||||
->with(['category', 'baseUnit'])
|
||||
->where('is_active', true);
|
||||
|
||||
// 1. 精準過濾 (ID, 條碼, 代碼, 外部 ID)
|
||||
if (!empty($filters['product_id'])) {
|
||||
$query->where('id', $filters['product_id']);
|
||||
}
|
||||
if (!empty($filters['barcode'])) {
|
||||
$query->where('barcode', $filters['barcode']);
|
||||
}
|
||||
if (!empty($filters['code'])) {
|
||||
$query->where('code', $filters['code']);
|
||||
}
|
||||
if (!empty($filters['external_pos_id'])) {
|
||||
$query->where('external_pos_id', $filters['external_pos_id']);
|
||||
}
|
||||
|
||||
// 3. 分類過濾
|
||||
if (!empty($filters['category'])) {
|
||||
$categoryName = $filters['category'];
|
||||
$query->whereHas('category', function ($q) use ($categoryName) {
|
||||
$q->where('name', $categoryName);
|
||||
});
|
||||
}
|
||||
|
||||
// 4. 增量同步 (Updated After)
|
||||
if (!empty($filters['updated_after'])) {
|
||||
$query->where('updated_at', '>=', $filters['updated_after']);
|
||||
}
|
||||
|
||||
// 4. 排序 (預設按更新時間降冪)
|
||||
$query->orderBy('updated_at', 'desc');
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user