refactor: 重構 VendorProduct API 與新增進貨單重複檢查前端邏輯
1. 將 VendorProductController 中的 Eloquent 關聯操作改為透過 ProcurementService 使用 DB 操作,解除跨模組 Model 直接依賴。 2. ProcurementService 加入 vendor product 的資料存取方法。 3. 進貨單建立前端 (GoodsReceipt/Create.tsx) 新增重複進貨檢查與警告對話框邏輯。
This commit is contained in:
145
.agent/skills/cross-module-communication/SKILL.md
Normal file
145
.agent/skills/cross-module-communication/SKILL.md
Normal file
@@ -0,0 +1,145 @@
|
||||
---
|
||||
name: 跨模組調用與通訊規範 (Cross-Module Communication)
|
||||
description: 規範 Laravel Modular Monolith 架構下,不同業務模組中如何彼此調用資料與邏輯,包含禁止項目、Interface 實作、與 Service 綁定規則。
|
||||
---
|
||||
|
||||
# 跨模組調用與通訊規範 (Cross-Module Communication)
|
||||
|
||||
為了確保專案的「模組化單體架構 (Modular Monolith)」的獨立性與可維護性,當遇到**需要跨越不同業務模組存取資料或調用功能**的情境時,請嚴格遵守以下規範。
|
||||
|
||||
## 🚫 絕對禁止的行為 (Strict Prohibitions)
|
||||
|
||||
* **禁止跨模組 Eloquent 關聯**
|
||||
* **錯誤**:在 `app/Modules/Sales/Models/Order.php` 中撰寫 `public function product() { return $this->belongsTo(\App\Modules\Inventory\Models\Product::class); }`。
|
||||
* **原因**:這會造成資料庫查詢的強耦合。如果 `Inventory` 模組修改了 Schema,`Sales` 模組會無預警崩壞。
|
||||
* **禁止跨模組直接引入 (use) Model**
|
||||
* **錯誤**:在 `app/Modules/Procurement/Controllers/PurchaseOrderController.php` 頂端寫 `use App\Modules\Inventory\Models\Warehouse;`。
|
||||
* **禁止跨模組直接實例化 (new) Service**
|
||||
* **錯誤**:`$inventoryService = new \App\Modules\Inventory\Services\InventoryService();`
|
||||
|
||||
---
|
||||
|
||||
## ✅ 正確的跨模組調用流程:合約與依賴反轉
|
||||
|
||||
所有的跨模組資料交換與功能調用,必須透過**介面化通訊 (Contracts)** 進行。
|
||||
|
||||
### Step 1: 在被調用的模組定義合約 (Interface)
|
||||
|
||||
如果 `Inventory` 模組需要提供功能給外部使用,請在 `app/Modules/Inventory/Contracts/` 建立 Interface 檔案。
|
||||
|
||||
```php
|
||||
// app/Modules/Inventory/Contracts/InventoryServiceInterface.php
|
||||
namespace App\Modules\Inventory\Contracts;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
interface InventoryServiceInterface
|
||||
{
|
||||
/**
|
||||
* 取得可用的倉庫清單
|
||||
*
|
||||
* @return Collection 包含每個倉庫的 id, name, code 等基本資料
|
||||
*/
|
||||
public function getActiveWarehouses(): Collection;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: 實作介面並在自己模組的 ServiceProvider 註冊
|
||||
|
||||
由 `Inventory` 模組自己的 Service 來實作上述介面。
|
||||
|
||||
```php
|
||||
// app/Modules/Inventory/Services/InventoryService.php
|
||||
namespace App\Modules\Inventory\Services;
|
||||
|
||||
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
|
||||
use App\Modules\Inventory\Models\Warehouse;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class InventoryService implements InventoryServiceInterface
|
||||
{
|
||||
public function getActiveWarehouses(): Collection
|
||||
{
|
||||
// 建議只取出需要的欄位,或者轉換為 DTO / 陣列
|
||||
// 避免將完整的 Eloquent Model 實例拋出模組外
|
||||
return Warehouse::where('is_active', true)
|
||||
->select(['id', 'name', 'code'])
|
||||
->get();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
然後進入 `app/Modules/Inventory/InventoryServiceProvider.php` 完成綁定:
|
||||
|
||||
```php
|
||||
// app/Modules/Inventory/InventoryServiceProvider.php
|
||||
namespace App\Modules\Inventory;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
|
||||
use App\Modules\Inventory\Services\InventoryService;
|
||||
|
||||
class InventoryServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
// 綁定介面與實體
|
||||
$this->app->bind(InventoryServiceInterface::class, InventoryService::class);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: 調用方透過依賴注入 (DI) 使用服務
|
||||
|
||||
當 `Procurement` 模組需要取得倉庫資料時,禁止直接 new 服務或呼叫倉庫 Model。必須透過**建構子注入**或**方法注入**取得 `InventoryServiceInterface`。
|
||||
|
||||
```php
|
||||
// app/Modules/Procurement/Controllers/PurchaseOrderController.php
|
||||
namespace App\Modules\Procurement\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class PurchaseOrderController extends Controller
|
||||
{
|
||||
// 透過建構子注入介面
|
||||
public function __construct(
|
||||
protected InventoryServiceInterface $inventoryService
|
||||
) {}
|
||||
|
||||
public function create()
|
||||
{
|
||||
// 僅能呼叫介面有定義的方法
|
||||
$warehouses = $this->inventoryService->getActiveWarehouses();
|
||||
|
||||
return Inertia::render('Procurement/PurchaseOrder/Create', [
|
||||
'warehouses' => $warehouses
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 跨模組資料回傳的注意事項 (Data Hydration)
|
||||
|
||||
* **回傳純粹資料**:為了防止其他模組意外觸發 Lazy Loading (`$item->product->name`),請盡量在 Service 中就用 `with()` 載入好關聯,或者直接轉為原生的 Array、`stdClass`、或具體的 DTO。
|
||||
* **手動組合 (Manual Hydration)**:若某個頁面需要合併兩個模組的資料,這也是被允許的,但必須在 Controller 層級呼叫兩個不同的 Service Interface 後,手動合併。
|
||||
|
||||
### 範例:手動合併資料
|
||||
```php
|
||||
// 錯誤示範(禁止在 OrderService 中去查使用者的關聯)
|
||||
$orders = Order::with('user')->get(); // 如果 user 表在 Core 模組,這是不允許的
|
||||
|
||||
// 正確示範:在各自模組取資料,並手動組裝
|
||||
$orders = $this->orderService->getOrders();
|
||||
$userIds = $orders->pluck('user_id')->unique()->toArray();
|
||||
$users = $this->coreUserService->getUsersByIds($userIds)->keyBy('id');
|
||||
|
||||
$mergedData = $orders->map(function ($order) use ($users) {
|
||||
// 將使用者資料手動附加上去
|
||||
$order->user_name = $users->get($order->user_id)->name ?? 'Unknown';
|
||||
return $order;
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user