From a987f4345e479844c50945c5eb7aeec7799a4cb1 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Mon, 9 Mar 2026 14:59:37 +0800 Subject: [PATCH] =?UTF-8?q?[REFACTOR]=20=E5=84=AA=E5=8C=96=E8=B3=87?= =?UTF-8?q?=E6=96=99=E5=BA=AB=E6=9F=A5=E8=A9=A2=E6=95=88=E8=83=BD=EF=BC=9A?= =?UTF-8?q?=E5=9C=A8=E5=A4=9A=E5=80=8B=20Service=20=E8=88=87=20Controller?= =?UTF-8?q?=20=E4=B8=AD=E5=8A=A0=E5=85=A5=20select=20=E6=AC=84=E4=BD=8D?= =?UTF-8?q?=E9=99=90=E5=88=B6=EF=BC=8C=E4=B8=A6=E6=96=B0=E5=A2=9E=E7=A7=9F?= =?UTF-8?q?=E6=88=B6=E8=B3=87=E6=96=99=E8=A1=A8=E7=B4=A2=E5=BC=95=20Migrat?= =?UTF-8?q?ion=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/rules/skill-trigger.md | 5 + .../Core/Controllers/RoleController.php | 2 +- app/Modules/Core/Services/CoreService.php | 4 +- .../Controllers/AdjustDocController.php | 2 +- .../Controllers/CountDocController.php | 2 +- .../Controllers/InventoryController.php | 6 +- .../Controllers/ProductController.php | 14 +-- .../Controllers/SafetyStockController.php | 2 +- .../Controllers/TransferOrderController.php | 2 +- .../Inventory/Services/InventoryService.php | 8 +- .../Controllers/PurchaseOrderController.php | 4 +- .../Controllers/PurchaseReturnController.php | 4 +- ...d_performance_indexes_to_tenant_tables.php | 112 ++++++++++++++++++ 13 files changed, 143 insertions(+), 24 deletions(-) create mode 100644 database/migrations/tenant/2026_03_09_141431_add_performance_indexes_to_tenant_tables.php diff --git a/.agents/rules/skill-trigger.md b/.agents/rules/skill-trigger.md index b06eddc..bbab2d1 100644 --- a/.agents/rules/skill-trigger.md +++ b/.agents/rules/skill-trigger.md @@ -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 檢查 + --- ## 注意事項 diff --git a/app/Modules/Core/Controllers/RoleController.php b/app/Modules/Core/Controllers/RoleController.php index 7d85be4..055630e 100644 --- a/app/Modules/Core/Controllers/RoleController.php +++ b/app/Modules/Core/Controllers/RoleController.php @@ -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) { diff --git a/app/Modules/Core/Services/CoreService.php b/app/Modules/Core/Services/CoreService.php index 7eaa72f..61ffcaf 100644 --- a/app/Modules/Core/Services/CoreService.php +++ b/app/Modules/Core/Services/CoreService.php @@ -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() diff --git a/app/Modules/Inventory/Controllers/AdjustDocController.php b/app/Modules/Inventory/Controllers/AdjustDocController.php index 51926ca..964e148 100644 --- a/app/Modules/Inventory/Controllers/AdjustDocController.php +++ b/app/Modules/Inventory/Controllers/AdjustDocController.php @@ -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']), ]); } diff --git a/app/Modules/Inventory/Controllers/CountDocController.php b/app/Modules/Inventory/Controllers/CountDocController.php index 16c555d..af85dcb 100644 --- a/app/Modules/Inventory/Controllers/CountDocController.php +++ b/app/Modules/Inventory/Controllers/CountDocController.php @@ -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']), ]); } diff --git a/app/Modules/Inventory/Controllers/InventoryController.php b/app/Modules/Inventory/Controllers/InventoryController.php index 6c5f157..2a0c5d5 100644 --- a/app/Modules/Inventory/Controllers/InventoryController.php +++ b/app/Modules/Inventory/Controllers/InventoryController.php @@ -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 [ diff --git a/app/Modules/Inventory/Controllers/ProductController.php b/app/Modules/Inventory/Controllers/ProductController.php index b4c7d8d..565ab6d 100644 --- a/app/Modules/Inventory/Controllers/ProductController.php +++ b/app/Modules/Inventory/Controllers/ProductController.php @@ -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]), ]); } diff --git a/app/Modules/Inventory/Controllers/SafetyStockController.php b/app/Modules/Inventory/Controllers/SafetyStockController.php index 462f5a5..90ec7b4 100644 --- a/app/Modules/Inventory/Controllers/SafetyStockController.php +++ b/app/Modules/Inventory/Controllers/SafetyStockController.php @@ -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) { diff --git a/app/Modules/Inventory/Controllers/TransferOrderController.php b/app/Modules/Inventory/Controllers/TransferOrderController.php index c67afa3..ab612a2 100644 --- a/app/Modules/Inventory/Controllers/TransferOrderController.php +++ b/app/Modules/Inventory/Controllers/TransferOrderController.php @@ -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']), ]); } diff --git a/app/Modules/Inventory/Services/InventoryService.php b/app/Modules/Inventory/Services/InventoryService.php index 316fb8c..9dd7d28 100644 --- a/app/Modules/Inventory/Services/InventoryService.php +++ b/app/Modules/Inventory/Services/InventoryService.php @@ -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 = []) diff --git a/app/Modules/Procurement/Controllers/PurchaseOrderController.php b/app/Modules/Procurement/Controllers/PurchaseOrderController.php index dd6b72f..8f495eb 100644 --- a/app/Modules/Procurement/Controllers/PurchaseOrderController.php +++ b/app/Modules/Procurement/Controllers/PurchaseOrderController.php @@ -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(); diff --git a/app/Modules/Procurement/Controllers/PurchaseReturnController.php b/app/Modules/Procurement/Controllers/PurchaseReturnController.php index c54d0e6..a0d72d8 100644 --- a/app/Modules/Procurement/Controllers/PurchaseReturnController.php +++ b/app/Modules/Procurement/Controllers/PurchaseReturnController.php @@ -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(); diff --git a/database/migrations/tenant/2026_03_09_141431_add_performance_indexes_to_tenant_tables.php b/database/migrations/tenant/2026_03_09_141431_add_performance_indexes_to_tenant_tables.php new file mode 100644 index 0000000..a063484 --- /dev/null +++ b/database/migrations/tenant/2026_03_09_141431_add_performance_indexes_to_tenant_tables.php @@ -0,0 +1,112 @@ +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']); + }); + } +};