2026年1月20日 星期二

🚀 從零開始打造雲端預約系統:Laravel 11 + Filament v3 多租戶架構實戰全紀錄

🚀 從零開始打造雲端預約系統: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 ScopeMiddleware 機制,是實作資料隔離的天然利器。

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

PHP
// app/Models/Traits/BelongsToTenant.php
trait BelongsToTenant {
    public function tenant(): BelongsTo {
        return $this->belongsTo(Tenant::class);
    }
}

2. 全域作用域 (Global Scope) 的深度保護

為了防止開發人員在寫 User::all() 時漏掉 where('tenant_id', ...),我們在模型層級加上了嚴格的過濾:

PHP
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 上是災難性的。我們必須實作「租戶隔離 + 角色過濾」的雙重查詢:

PHP
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 顏色。為了讓租戶能自訂主題色,我們在 AdminPanelProviderboot 方法中使用 RenderHook

PHP
FilamentView::registerRenderHook(
    'panels::styles.after',
    fn(): string => Blade::render('<style>:root { --primary-500: {{ $color }}; }</style>', [
        'color' => Filament::getTenant()?->settings['brand_color'] ?? '#6366f1'
    ])
);

2. 租戶自訂品牌名稱

透過閉包(Closure),我們可以讓後台的標題隨租戶切換而變動:

PHP
->brandName(fn() => Filament::getTenant()?->name ?? 'SaaS Control Center')

五、 效能優化與規模化 (Scalability)

當系統併發量上升時,我們需要考慮以下層面:

1. 預約衝突處理 (Concurrency)

在預約系統中,兩個人同時預約同一個教練的同一個時段是致命傷。我們在資料庫層級使用了 悲觀鎖 (Pessimistic Locking) 或在應用層實作 Atomic Locks (Redis)

PHP
$lock = Cache::lock('appointment_staff_'.$staffId.'_'.$time, 10);
if ($lock->get()) {
    // 執行預約邏輯
    $lock->release();
}

2. 搜尋優化

當 User 表成長到數十萬筆(含租戶所有客戶)時,Filament Table 的搜尋會變慢。


六、 踩坑記錄與解決方案

  1. Icon 方法變動: 在 Filament v2 習慣寫 icon(),但在 v3 中,組件內部的 icon 定義更加明確。請統一使用 prefixIcon() 提升質感。

  2. 時區地獄: 租戶可能跨國。資料庫統一存 UTC,顯示時透過 Carbon 動態轉換為租戶設定的 timezone

  3. 多租戶選單隱藏: 超級管理員不需要看到雜亂的租戶切換器。使用 tenantMenu(false) 並在 TenantResource 建立一鍵跳轉按鈕,能極大提升操作體驗。


七、 結語

開發一個多租戶預約系統,本質上是在處理**「邊界」**。

Laravel 11 與 Filament 3 的組合,讓我們能把 80% 的精力放在業務邏輯上,而剩下的 20% 則用於打磨極致的使用者體驗。

沒有留言:

張貼留言

🚀 從零開始打造雲端預約系統:Laravel 11 + Filament v3 多租戶架構實戰全紀錄

🚀 從零開始打造雲端預約系統:Laravel 11 + Filament v3 多租戶架構實戰全紀錄 前言:為什麼選擇 SaaS 多租戶架構? 在現代軟體開發中,「預約系統」是一個經典但極具挑戰性的題目。無論是連鎖診所、美容工作室,還是高端健身房,他們的核心需求都是「時間管理」...