[REFACTOR] 優化資料庫查詢效能:在多個 Service 與 Controller 中加入 select 欄位限制,並新增租戶資料表索引 Migration。
This commit is contained in:
@@ -20,6 +20,7 @@ Skills 位於 `.agents/skills/`,採漸進式揭露以節省 Token。
|
|||||||
| 按鈕樣式、表格規範、圖標、分頁、Badge、Toast、表單、UI 統一、頁面佈局、`button-filled-*`、`button-outlined-*`、`lucide-react`、色彩系統 | **客戶端後台 UI 統一規範** | `.agents/skills/ui-consistency/SKILL.md` |
|
| 按鈕樣式、表格規範、圖標、分頁、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` |
|
| 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` |
|
| 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 格式
|
1. **git-workflows** — 分支命名與 commit 格式
|
||||||
|
|
||||||
|
### 🔴 新增或修改 API 與 Controller 撈取資料庫邏輯時
|
||||||
|
必須讀取:
|
||||||
|
1. **eloquent-optimization** — 確認查詢是否有做適當的 `select` 優化與 N+1 檢查
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 注意事項
|
## 注意事項
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ class RoleController extends Controller
|
|||||||
*/
|
*/
|
||||||
private function getGroupedPermissions()
|
private function getGroupedPermissions()
|
||||||
{
|
{
|
||||||
$allPermissions = Permission::orderBy('name')->get();
|
$allPermissions = Permission::select('id', 'name', 'guard_name')->orderBy('name')->get();
|
||||||
$grouped = [];
|
$grouped = [];
|
||||||
|
|
||||||
foreach ($allPermissions as $permission) {
|
foreach ($allPermissions as $permission) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class CoreService implements CoreServiceInterface
|
|||||||
*/
|
*/
|
||||||
public function getUsersByIds(array $ids): Collection
|
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
|
public function getAllUsers(): Collection
|
||||||
{
|
{
|
||||||
return User::all();
|
return User::select('id', 'name')->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ensureSystemUserExists()
|
public function ensureSystemUserExists()
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class AdjustDocController extends Controller
|
|||||||
|
|
||||||
return Inertia::render('Inventory/Adjust/Index', [
|
return Inertia::render('Inventory/Adjust/Index', [
|
||||||
'docs' => $docs,
|
'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']),
|
'filters' => $request->only(['warehouse_id', 'search', 'per_page']),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class CountDocController extends Controller
|
|||||||
|
|
||||||
return Inertia::render('Inventory/Count/Index', [
|
return Inertia::render('Inventory/Count/Index', [
|
||||||
'docs' => $docs,
|
'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']),
|
'filters' => $request->only(['warehouse_id', 'search', 'per_page']),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class InventoryController extends Controller
|
|||||||
'inventories.lastIncomingTransaction',
|
'inventories.lastIncomingTransaction',
|
||||||
'inventories.lastOutgoingTransaction'
|
'inventories.lastOutgoingTransaction'
|
||||||
]);
|
]);
|
||||||
$allProducts = Product::with('category')->get();
|
$allProducts = Product::select('id', 'name', 'code', 'category_id')->with('category:id,name')->get();
|
||||||
|
|
||||||
// 1. 準備 availableProducts
|
// 1. 準備 availableProducts
|
||||||
$availableProducts = $allProducts->map(function ($product) {
|
$availableProducts = $allProducts->map(function ($product) {
|
||||||
@@ -167,8 +167,8 @@ class InventoryController extends Controller
|
|||||||
public function create(Warehouse $warehouse)
|
public function create(Warehouse $warehouse)
|
||||||
{
|
{
|
||||||
// ... (unchanged) ...
|
// ... (unchanged) ...
|
||||||
$products = Product::with(['baseUnit', 'largeUnit'])
|
$products = Product::select('id', 'name', 'code', 'barcode', 'base_unit_id', 'large_unit_id', 'conversion_rate', 'cost_price')
|
||||||
->select('id', 'name', 'code', 'barcode', 'base_unit_id', 'large_unit_id', 'conversion_rate', 'cost_price')
|
->with(['baseUnit:id,name', 'largeUnit:id,name'])
|
||||||
->get()
|
->get()
|
||||||
->map(function ($product) {
|
->map(function ($product) {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -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', [
|
return Inertia::render('Product/Index', [
|
||||||
'products' => $products,
|
'products' => $products,
|
||||||
'categories' => Category::where('is_active', true)->get()->map(fn($c) => (object)['id' => $c->id, 'name' => $c->name]),
|
'categories' => $categories->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]),
|
'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']),
|
'filters' => $request->only(['search', 'category_id', 'per_page', 'sort_field', 'sort_direction']),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -172,8 +172,8 @@ class ProductController extends Controller
|
|||||||
public function create(): Response
|
public function create(): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('Product/Create', [
|
return Inertia::render('Product/Create', [
|
||||||
'categories' => Category::where('is_active', true)->get()->map(fn($c) => (object)['id' => $c->id, 'name' => $c->name]),
|
'categories' => Category::select('id', 'name')->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]),
|
'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,
|
'wholesale_price' => (float) $product->wholesale_price,
|
||||||
'is_active' => (bool) $product->is_active,
|
'is_active' => (bool) $product->is_active,
|
||||||
],
|
],
|
||||||
'categories' => Category::where('is_active', true)->get()->map(fn($c) => (object)['id' => $c->id, 'name' => $c->name]),
|
'categories' => Category::select('id', 'name')->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]),
|
'units' => Unit::select('id', 'name', 'code')->get()->map(fn($u) => (object)['id' => (string) $u->id, 'name' => $u->name, 'code' => $u->code]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class SafetyStockController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index(Warehouse $warehouse)
|
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) {
|
$availableProducts = $allProducts->map(function ($product) {
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class TransferOrderController extends Controller
|
|||||||
|
|
||||||
return Inertia::render('Inventory/Transfer/Index', [
|
return Inertia::render('Inventory/Transfer/Index', [
|
||||||
'orders' => $orders,
|
'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']),
|
'filters' => $request->only(['search', 'warehouse_id', 'per_page']),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class InventoryService implements InventoryServiceInterface
|
|||||||
{
|
{
|
||||||
public function getAllWarehouses()
|
public function getAllWarehouses()
|
||||||
{
|
{
|
||||||
return Warehouse::all();
|
return Warehouse::select('id', 'name', 'code', 'type')->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTopInventoryValue(int $limit = 5): \Illuminate\Support\Collection
|
public function getTopInventoryValue(int $limit = 5): \Illuminate\Support\Collection
|
||||||
@@ -38,12 +38,14 @@ class InventoryService implements InventoryServiceInterface
|
|||||||
|
|
||||||
public function getAllProducts()
|
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()
|
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 = [])
|
public function getInventoriesByIds(array $ids, array $with = [])
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ class PurchaseOrderController extends Controller
|
|||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
// 1. 獲取廠商(無關聯)
|
// 1. 獲取廠商(無關聯)
|
||||||
$vendors = Vendor::all();
|
$vendors = Vendor::select('id', 'name')->get();
|
||||||
|
|
||||||
// 2. 手動注入:獲取 Pivot 資料
|
// 2. 手動注入:獲取 Pivot 資料
|
||||||
$vendorIds = $vendors->pluck('id')->toArray();
|
$vendorIds = $vendors->pluck('id')->toArray();
|
||||||
@@ -379,7 +379,7 @@ class PurchaseOrderController extends Controller
|
|||||||
$order = PurchaseOrder::with(['items'])->findOrFail($id);
|
$order = PurchaseOrder::with(['items'])->findOrFail($id);
|
||||||
|
|
||||||
// 2. 獲取廠商與商品(與 create 邏輯一致)
|
// 2. 獲取廠商與商品(與 create 邏輯一致)
|
||||||
$vendors = Vendor::all();
|
$vendors = Vendor::select('id', 'name')->get();
|
||||||
$vendorIds = $vendors->pluck('id')->toArray();
|
$vendorIds = $vendors->pluck('id')->toArray();
|
||||||
$pivots = DB::table('product_vendor')->whereIn('vendor_id', $vendorIds)->get();
|
$pivots = DB::table('product_vendor')->whereIn('vendor_id', $vendorIds)->get();
|
||||||
$productIds = $pivots->pluck('product_id')->unique()->toArray();
|
$productIds = $pivots->pluck('product_id')->unique()->toArray();
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class PurchaseReturnController extends Controller
|
|||||||
{
|
{
|
||||||
// 取得可用的倉庫與廠商資料供前端選單使用
|
// 取得可用的倉庫與廠商資料供前端選單使用
|
||||||
$warehouses = $this->inventoryService->getAllWarehouses();
|
$warehouses = $this->inventoryService->getAllWarehouses();
|
||||||
$vendors = Vendor::all();
|
$vendors = Vendor::select('id', 'name')->get();
|
||||||
|
|
||||||
// 手動注入:獲取廠商商品 (與 PurchaseOrderController 邏輯一致)
|
// 手動注入:獲取廠商商品 (與 PurchaseOrderController 邏輯一致)
|
||||||
$vendorIds = $vendors->pluck('id')->toArray();
|
$vendorIds = $vendors->pluck('id')->toArray();
|
||||||
@@ -157,7 +157,7 @@ class PurchaseReturnController extends Controller
|
|||||||
});
|
});
|
||||||
|
|
||||||
$warehouses = $this->inventoryService->getAllWarehouses();
|
$warehouses = $this->inventoryService->getAllWarehouses();
|
||||||
$vendors = Vendor::all();
|
$vendors = Vendor::select('id', 'name')->get();
|
||||||
|
|
||||||
// 手動注入:獲取廠商商品 (與 create 邏輯一致)
|
// 手動注入:獲取廠商商品 (與 create 邏輯一致)
|
||||||
$vendorIds = $vendors->pluck('id')->toArray();
|
$vendorIds = $vendors->pluck('id')->toArray();
|
||||||
|
|||||||
@@ -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']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user