[REFACTOR] 優化資料庫查詢效能:在多個 Service 與 Controller 中加入 select 欄位限制,並新增租戶資料表索引 Migration。

This commit is contained in:
2026-03-09 14:59:37 +08:00
parent 89291918fd
commit a987f4345e
13 changed files with 143 additions and 24 deletions

View File

@@ -20,6 +20,7 @@ Skills 位於 `.agents/skills/`,採漸進式揭露以節省 Token。
| 按鈕樣式、表格規範、圖標、分頁、Badge、Toast、表單、UI 統一、頁面佈局、`button-filled-*``button-outlined-*``lucide-react`、色彩系統 | **客戶端後台 UI 統一規範** | `.agents/skills/ui-consistency/SKILL.md` |
| Git 分支、commit、push、合併、部署、`feature/``hotfix/``develop``main` | **Git 分支管理與開發規範** | `.agents/skills/git-workflows/SKILL.md` |
| E2E、端到端測試、Playwright、`spec.ts`、功能驗證、自動化測試、回歸測試 | **E2E 端到端測試規範** | `.agents/skills/e2e-testing/SKILL.md` |
| 查詢、撈資料、Query、Controller、下拉選單、Eloquent、N+1、`->get()`、select | **Eloquent 與 MySQL 查詢優化規範** | `/home/mama/.gemini/antigravity/global_skills/eloquent-optimization/SKILL.md` |
---
@@ -43,6 +44,10 @@ Skills 位於 `.agents/skills/`,採漸進式揭露以節省 Token。
必須讀取:
1. **git-workflows** — 分支命名與 commit 格式
### 🔴 新增或修改 API 與 Controller 撈取資料庫邏輯時
必須讀取:
1. **eloquent-optimization** — 確認查詢是否有做適當的 `select` 優化與 N+1 檢查
---
## 注意事項

View File

@@ -151,7 +151,7 @@ class RoleController extends Controller
*/
private function getGroupedPermissions()
{
$allPermissions = Permission::orderBy('name')->get();
$allPermissions = Permission::select('id', 'name', 'guard_name')->orderBy('name')->get();
$grouped = [];
foreach ($allPermissions as $permission) {

View File

@@ -16,7 +16,7 @@ class CoreService implements CoreServiceInterface
*/
public function getUsersByIds(array $ids): Collection
{
return User::whereIn('id', $ids)->get();
return User::select('id', 'name')->whereIn('id', $ids)->get();
}
/**
@@ -37,7 +37,7 @@ class CoreService implements CoreServiceInterface
*/
public function getAllUsers(): Collection
{
return User::all();
return User::select('id', 'name')->get();
}
public function ensureSystemUserExists()

View File

@@ -63,7 +63,7 @@ class AdjustDocController extends Controller
return Inertia::render('Inventory/Adjust/Index', [
'docs' => $docs,
'warehouses' => Warehouse::all()->map(fn($w) => ['id' => (string)$w->id, 'name' => $w->name]),
'warehouses' => Warehouse::select('id', 'name')->get()->map(fn($w) => ['id' => (string)$w->id, 'name' => $w->name]),
'filters' => $request->only(['warehouse_id', 'search', 'per_page']),
]);
}

View File

@@ -67,7 +67,7 @@ class CountDocController extends Controller
return Inertia::render('Inventory/Count/Index', [
'docs' => $docs,
'warehouses' => Warehouse::all()->map(fn($w) => ['id' => (string)$w->id, 'name' => $w->name]),
'warehouses' => Warehouse::select('id', 'name')->get()->map(fn($w) => ['id' => (string)$w->id, 'name' => $w->name]),
'filters' => $request->only(['warehouse_id', 'search', 'per_page']),
]);
}

View File

@@ -41,7 +41,7 @@ class InventoryController extends Controller
'inventories.lastIncomingTransaction',
'inventories.lastOutgoingTransaction'
]);
$allProducts = Product::with('category')->get();
$allProducts = Product::select('id', 'name', 'code', 'category_id')->with('category:id,name')->get();
// 1. 準備 availableProducts
$availableProducts = $allProducts->map(function ($product) {
@@ -167,8 +167,8 @@ class InventoryController extends Controller
public function create(Warehouse $warehouse)
{
// ... (unchanged) ...
$products = Product::with(['baseUnit', 'largeUnit'])
->select('id', 'name', 'code', 'barcode', 'base_unit_id', 'large_unit_id', 'conversion_rate', 'cost_price')
$products = Product::select('id', 'name', 'code', 'barcode', 'base_unit_id', 'large_unit_id', 'conversion_rate', 'cost_price')
->with(['baseUnit:id,name', 'largeUnit:id,name'])
->get()
->map(function ($product) {
return [

View File

@@ -112,12 +112,12 @@ class ProductController extends Controller
];
});
$categories = Category::where('is_active', true)->get();
$categories = Category::select('id', 'name')->where('is_active', true)->get();
return Inertia::render('Product/Index', [
'products' => $products,
'categories' => Category::where('is_active', true)->get()->map(fn($c) => (object)['id' => $c->id, 'name' => $c->name]),
'units' => Unit::all()->map(fn($u) => (object)['id' => (string) $u->id, 'name' => $u->name, 'code' => $u->code]),
'categories' => $categories->map(fn($c) => (object)['id' => $c->id, 'name' => $c->name]),
'units' => Unit::select('id', 'name', 'code')->get()->map(fn($u) => (object)['id' => (string) $u->id, 'name' => $u->name, 'code' => $u->code]),
'filters' => $request->only(['search', 'category_id', 'per_page', 'sort_field', 'sort_direction']),
]);
}
@@ -172,8 +172,8 @@ class ProductController extends Controller
public function create(): Response
{
return Inertia::render('Product/Create', [
'categories' => Category::where('is_active', true)->get()->map(fn($c) => (object)['id' => $c->id, 'name' => $c->name]),
'units' => Unit::all()->map(fn($u) => (object)['id' => (string) $u->id, 'name' => $u->name, 'code' => $u->code]),
'categories' => Category::select('id', 'name')->where('is_active', true)->get()->map(fn($c) => (object)['id' => $c->id, 'name' => $c->name]),
'units' => Unit::select('id', 'name', 'code')->get()->map(fn($u) => (object)['id' => (string) $u->id, 'name' => $u->name, 'code' => $u->code]),
]);
}
@@ -231,8 +231,8 @@ class ProductController extends Controller
'wholesale_price' => (float) $product->wholesale_price,
'is_active' => (bool) $product->is_active,
],
'categories' => Category::where('is_active', true)->get()->map(fn($c) => (object)['id' => $c->id, 'name' => $c->name]),
'units' => Unit::all()->map(fn($u) => (object)['id' => (string) $u->id, 'name' => $u->name, 'code' => $u->code]),
'categories' => Category::select('id', 'name')->where('is_active', true)->get()->map(fn($c) => (object)['id' => $c->id, 'name' => $c->name]),
'units' => Unit::select('id', 'name', 'code')->get()->map(fn($u) => (object)['id' => (string) $u->id, 'name' => $u->name, 'code' => $u->code]),
]);
}

View File

@@ -19,7 +19,7 @@ class SafetyStockController extends Controller
*/
public function index(Warehouse $warehouse)
{
$allProducts = Product::with(['category', 'baseUnit'])->get();
$allProducts = Product::select('id', 'name', 'category_id', 'base_unit_id')->with(['category:id,name', 'baseUnit:id,name'])->get();
// 準備可選商品列表
$availableProducts = $allProducts->map(function ($product) {

View File

@@ -65,7 +65,7 @@ class TransferOrderController extends Controller
return Inertia::render('Inventory/Transfer/Index', [
'orders' => $orders,
'warehouses' => Warehouse::all()->map(fn($w) => ['id' => (string)$w->id, 'name' => $w->name]),
'warehouses' => Warehouse::select('id', 'name')->get()->map(fn($w) => ['id' => (string)$w->id, 'name' => $w->name]),
'filters' => $request->only(['search', 'warehouse_id', 'per_page']),
]);
}

View File

@@ -13,7 +13,7 @@ class InventoryService implements InventoryServiceInterface
{
public function getAllWarehouses()
{
return Warehouse::all();
return Warehouse::select('id', 'name', 'code', 'type')->get();
}
public function getTopInventoryValue(int $limit = 5): \Illuminate\Support\Collection
@@ -38,12 +38,14 @@ class InventoryService implements InventoryServiceInterface
public function getAllProducts()
{
return Product::with(['baseUnit', 'largeUnit'])->get();
return Product::select('id', 'name', 'code', 'base_unit_id', 'large_unit_id')
->with(['baseUnit:id,name', 'largeUnit:id,name'])
->get();
}
public function getUnits()
{
return \App\Modules\Inventory\Models\Unit::all();
return \App\Modules\Inventory\Models\Unit::select('id', 'name')->get();
}
public function getInventoriesByIds(array $ids, array $with = [])

View File

@@ -118,7 +118,7 @@ class PurchaseOrderController extends Controller
public function create()
{
// 1. 獲取廠商(無關聯)
$vendors = Vendor::all();
$vendors = Vendor::select('id', 'name')->get();
// 2. 手動注入:獲取 Pivot 資料
$vendorIds = $vendors->pluck('id')->toArray();
@@ -379,7 +379,7 @@ class PurchaseOrderController extends Controller
$order = PurchaseOrder::with(['items'])->findOrFail($id);
// 2. 獲取廠商與商品(與 create 邏輯一致)
$vendors = Vendor::all();
$vendors = Vendor::select('id', 'name')->get();
$vendorIds = $vendors->pluck('id')->toArray();
$pivots = DB::table('product_vendor')->whereIn('vendor_id', $vendorIds)->get();
$productIds = $pivots->pluck('product_id')->unique()->toArray();

View File

@@ -48,7 +48,7 @@ class PurchaseReturnController extends Controller
{
// 取得可用的倉庫與廠商資料供前端選單使用
$warehouses = $this->inventoryService->getAllWarehouses();
$vendors = Vendor::all();
$vendors = Vendor::select('id', 'name')->get();
// 手動注入:獲取廠商商品 (與 PurchaseOrderController 邏輯一致)
$vendorIds = $vendors->pluck('id')->toArray();
@@ -157,7 +157,7 @@ class PurchaseReturnController extends Controller
});
$warehouses = $this->inventoryService->getAllWarehouses();
$vendors = Vendor::all();
$vendors = Vendor::select('id', 'name')->get();
// 手動注入:獲取廠商商品 (與 create 邏輯一致)
$vendorIds = $vendors->pluck('id')->toArray();

View File

@@ -0,0 +1,112 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// 1. warehouses (倉庫)
Schema::table('warehouses', function (Blueprint $table) {
$table->index('type');
});
// 2. categories (分類)
Schema::table('categories', function (Blueprint $table) {
$table->index('is_active');
});
// 3. products (商品/原物料)
Schema::table('products', function (Blueprint $table) {
// is_active was added in a later migration, need to make sure column exists before indexing
// Same for brand if not added at start (but brand is in the create migration)
if (Schema::hasColumn('products', 'is_active')) {
$table->index('is_active');
}
$table->index('brand');
});
// 4. recipes (配方/BOM)
Schema::table('recipes', function (Blueprint $table) {
$table->index('is_active');
});
// 5. inventory_transactions (庫存異動紀錄)
Schema::table('inventory_transactions', function (Blueprint $table) {
$table->index('type');
$table->index('created_at');
});
// 6. purchase_orders (採購單)
Schema::table('purchase_orders', function (Blueprint $table) {
$table->index('status');
$table->index('expected_delivery_date');
$table->index('created_at');
});
// 7. production_orders (生產工單)
Schema::table('production_orders', function (Blueprint $table) {
$table->index('status');
$table->index('created_at');
});
// 8. sales_orders (門市/銷售單)
Schema::table('sales_orders', function (Blueprint $table) {
$table->index('status');
$table->index('sold_at');
$table->index('created_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('warehouses', function (Blueprint $table) {
$table->dropIndex(['type']);
});
Schema::table('categories', function (Blueprint $table) {
$table->dropIndex(['is_active']);
});
Schema::table('products', function (Blueprint $table) {
if (Schema::hasColumn('products', 'is_active')) {
$table->dropIndex(['is_active']);
}
$table->dropIndex(['brand']);
});
Schema::table('recipes', function (Blueprint $table) {
$table->dropIndex(['is_active']);
});
Schema::table('inventory_transactions', function (Blueprint $table) {
$table->dropIndex(['type']);
$table->dropIndex(['created_at']);
});
Schema::table('purchase_orders', function (Blueprint $table) {
$table->dropIndex(['status']);
$table->dropIndex(['expected_delivery_date']);
$table->dropIndex(['created_at']);
});
Schema::table('production_orders', function (Blueprint $table) {
$table->dropIndex(['status']);
$table->dropIndex(['created_at']);
});
Schema::table('sales_orders', function (Blueprint $table) {
$table->dropIndex(['status']);
$table->dropIndex(['sold_at']);
$table->dropIndex(['created_at']);
});
}
};