🚀 從零開始打造雲端預約系統:Laravel 11 + Filament v3 多租戶架構實戰全紀錄
前言:為什麼選擇 SaaS 多租戶架構?
在現代軟體開發中,「預約系統」是一個經典但極具挑戰性的題目。無論是連鎖診所、美容工作室,還是高端健身房,他們的核心需求都是「時間管理」與「客戶關係維護」。然而,作為開發者,如果我們為每個客戶都部署一套獨立的環境,維護成本將會隨著客戶數量增長而呈指數級上升。
這就是 SaaS (Software as a Service) 多租戶架構的價值所在。我們目標是打造一個系統,讓數百個租戶(Tenants)共享同一份程式碼與資料庫節點,但在邏輯上,他們彼此像是處於平行的宇宙,互不干擾。
您可以透過以下 GitHub 連結檢閱本專案的原始碼:https://github.com/BpsEason/MultiTenant-Booking.git
一、 技術選型:為什麼是 Laravel 11 與 Filament v3?
在架構設計初期,選型決定了開發的速度與長期維護的難度。
1. 後端基石:Laravel 11
Laravel 11 簡化了應用程式的目錄結構,移除了過多的 Boilerplate。對於多租戶系統而言,其強大的 Global Scope 與 Middleware 機制,是實作資料隔離的天然利器。
2. 管理藝術:Filament v3
Filament 不僅僅是一個後台框架,它更像是一套 TALL Stack (Tailwind, Alpine.js, Laravel, Livewire) 的高級抽象。在 v3 中,它原生支援了多租戶模式(Tenancy),讓我們可以輕鬆定義「租戶上下文」,並在切換租戶時自動更新 UI 與權限。
3. 排程與動態:FullCalendar & Livewire 3
預約系統的核心是「直覺」。Livewire 3 讓 PHP 開發者能在不寫複雜 JavaScript 的情況下,實作 SPA 等級的互動。搭配 saade/filament-fullcalendar,我們能將枯燥的資料表格轉換成充滿活力的時間軸。
二、 核心架構設計:資料隔離的藝術
多租戶最關鍵的問題在於:如何確保租戶 A 絕對看不到租戶 B 的資料?
1. 單一資料庫與 tenant_id 策略
我們選擇了「共享資料庫、邏輯隔離」的方案。這種方案在成本與管理上最平衡(相比於每個租戶獨立 DB)。
我們定義了一個 BusinessResource 作為所有資源的父類別。所有涉及租戶的 Model 都要具備 tenant_id:
// app/Models/Traits/BelongsToTenant.php
trait BelongsToTenant {
public function tenant(): BelongsTo {
return $this->belongsTo(Tenant::class);
}
}
2. 全域作用域 (Global Scope) 的深度保護
為了防止開發人員在寫 User::all() 時漏掉 where('tenant_id', ...),我們在模型層級加上了嚴格的過濾:
protected static function booted()
{
static::addGlobalScope('tenant', function (Builder $builder) {
if (auth()->check() && $tenant = Filament::getTenant()) {
$builder->where('tenant_id', $tenant->id);
}
});
}
三、 權限管理:Spatie Shield 的實戰配置
在預約系統中,角色定義往往非常細碎。
Super Admin: 擁有「上帝視角」。他們不屬於特定租戶,負責監控全站營收與租戶健康度。
Tenant Admin (店長): 負責該店的營運設定、員工排班、服務價格調整。
Receptionist (櫃檯人員): 處理日常預約、客戶簽到。
Staff (員工/教練): 僅能查看自己的課表。
角色過濾的坑
在開發過程中,我發現若僅依賴 tenant_id,在「指派員工」的下拉選單中會出現「客戶」。這在 UI 上是災難性的。我們必須實作「租戶隔離 + 角色過濾」的雙重查詢:
Forms\Components\Select::make('staff_id')
->relationship(
name: 'staff',
titleAttribute: 'name',
modifyQueryUsing: fn(Builder $query) => $query
->where('tenant_id', Filament::getTenant()?->id)
->whereHas('roles', fn($q) => $q->whereIn('name', ['staff', 'receptionist', 'manager']))
)
四、 品牌白標化 (White-labeling):讓系統像租戶自己開發的
SaaS 成功的秘訣之一是「歸屬感」。我們透過 Tenant 模型的 settings JSON 欄位實作了動態視覺。
1. 動態顏色注入
Filament 預設使用 Tailwind 顏色。為了讓租戶能自訂主題色,我們在 AdminPanelProvider 的 boot 方法中使用 RenderHook:
FilamentView::registerRenderHook(
'panels::styles.after',
fn(): string => Blade::render('<style>:root { --primary-500: {{ $color }}; }</style>', [
'color' => Filament::getTenant()?->settings['brand_color'] ?? '#6366f1'
])
);
2. 租戶自訂品牌名稱
透過閉包(Closure),我們可以讓後台的標題隨租戶切換而變動:
->brandName(fn() => Filament::getTenant()?->name ?? 'SaaS Control Center')
五、 效能優化與規模化 (Scalability)
當系統併發量上升時,我們需要考慮以下層面:
1. 預約衝突處理 (Concurrency)
在預約系統中,兩個人同時預約同一個教練的同一個時段是致命傷。我們在資料庫層級使用了 悲觀鎖 (Pessimistic Locking) 或在應用層實作 Atomic Locks (Redis):
$lock = Cache::lock('appointment_staff_'.$staffId.'_'.$time, 10);
if ($lock->get()) {
// 執行預約邏輯
$lock->release();
}
2. 搜尋優化
當 User 表成長到數十萬筆(含租戶所有客戶)時,Filament Table 的搜尋會變慢。
Database Indexing: 對
tenant_id與email建立複合索引。Scout Search: 當資料量更大時,導入 Algolia 或 Meilisearch。
六、 踩坑記錄與解決方案
Icon 方法變動: 在 Filament v2 習慣寫
icon(),但在 v3 中,組件內部的 icon 定義更加明確。請統一使用prefixIcon()提升質感。多租戶選單隱藏: 超級管理員不需要看到雜亂的租戶切換器。使用
tenantMenu(false)並在TenantResource建立一鍵跳轉按鈕,能極大提升操作體驗。
七、 結語
開發一個多租戶預約系統,本質上是在處理**「邊界」**。
處理資料的物理邊界(Database Isolation)。
處理使用者的權限邊界(RBAC)。
處理品牌的視覺邊界(Theming)。
Laravel 11 與 Filament 3 的組合,讓我們能把 80% 的精力放在業務邏輯上,而剩下的 20% 則用於打磨極致的使用者體驗。