Files
star-erp/.agent/skills/cross-module-communication/SKILL.md
sky121113 ad91b08dbc
All checks were successful
ERP-Deploy-Production / deploy-production (push) Successful in 1m9s
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m10s
refactor: 重構 VendorProduct API 與新增進貨單重複檢查前端邏輯
1. 將 VendorProductController 中的 Eloquent 關聯操作改為透過 ProcurementService 使用 DB 操作,解除跨模組 Model 直接依賴。
2. ProcurementService 加入 vendor product 的資料存取方法。
3. 進貨單建立前端 (GoodsReceipt/Create.tsx) 新增重複進貨檢查與警告對話框邏輯。
2026-02-25 11:11:28 +08:00

146 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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;
});
```