2025年6月27日 星期五

從零到一打造多租戶 SaaS 平台:一個 Laravel + Vue.js 的餐飲電商實戰案例

從零到一打造多租戶 SaaS 平台:一個 Laravel + Vue.js 的餐飲電商實戰案例

嗨!各位 Laravel 的同好們,大家好!

你是否曾經想過,如何用 Laravel 打造一個可以同時服務多個客戶,又各自擁有獨立資料的 SaaS (Software as a Service) 平台?

今天,我想跟大家分享一個我最近整理的開源專案 ECommerceTenancy,它是一個專為餐飲業設計的多租戶電商平台原型。別看它名字有點硬,它可是個麻雀雖小,五臟俱全的寶藏專案。

這個專案不只教你 CRUD,更帶你深入探討:

  • 多租戶架構:用子域名動態切換客戶資料,超帥氣!

  • 前後端協作:如何用 WebSocket 實現即時叫號系統?

  • 實戰監控:用 Prometheus 和 Grafana 讓你的老闆一眼看出 API 狀況。

準備好了嗎?讓我們一起來趟技術深度之旅吧!

第一站:架構設計的腦力激盪 - 為什麼選擇 Row-based 多租戶?

在設計多租戶架構時,我們有幾種選擇:

  1. Schema-based:每個租戶一個 Schema。

  2. Database-based:每個租戶一個資料庫。

  3. Row-based:所有租戶共用資料表,用 tenant_id 欄位區分。

在這個專案中,我選擇了 Row-based。為什麼呢?

  • 成本效益:省錢!所有租戶共用一個資料庫實例,部署和維護成本低。

  • 部署簡單:一套程式碼、一個資料庫,部署流程超簡潔。

當然,這也帶來了挑戰:如何確保資料不會「串門子」?

我的解法是:全域作用域 (Global Scope) + Middleware

  1. Middleware 的魔術:

    當使用者透過 tenanta.localhost 訪問時,我會用一個 Laravel Middleware 擷取出 tenanta 這個租戶識別碼。

    PHP
    // 類似這樣的邏輯,在 Middleware 中解析子域名
    $tenantId = app('tenant_resolver')->resolveFromSubdomain($request->getHost());
    // 將租戶 ID 設為全域可存取
    TenantContext::setTenantId($tenantId);
    
  2. Eloquent Global Scope 的神助攻:

    我為所有需要租戶隔離的 Eloquent Model(像是 Product、Order、Table)都加上了一個 TenantScope。

    PHP
    // 在你的 App\Models\Product.php 中
    protected static function booted()
    {
        static::addGlobalScope(new TenantScope());
    }
    
    // TenantScope.php 內部大概長這樣
    public function apply(Builder $builder, Model $model)
    {
        if ($tenantId = TenantContext::getTenantId()) {
            $builder->where('tenant_id', $tenantId);
        }
    }
    

    這樣一來,無論你下什麼查詢,Laravel 都會自動幫你加上 WHERE tenant_id = 'xxx' 的條件,從根源上確保了資料隔離,再也不用擔心手滑忘記加條件啦!

第二站:前後端即時串接 - 打造即時叫號系統

餐飲業最常見的痛點之一就是叫號。客人坐在位子上等,如何讓他們即時看到叫號進度?

ECommerceTenancy 採用了 Laravel Echo + WebSocket 來實現這個即時叫號功能。

  1. 後端事件觸發:

    當後端新增或更新一個叫號時 (例如:服務生在後台點擊「下一個號碼」),我們就觸發一個 Laravel Event。

    PHP
    // 在你的 QueueController.php 中
    // 處理叫號更新的邏輯...
    // ...
    event(new QueueUpdated($newQueueNumber));
    
  2. 後端廣播:

    這個 QueueUpdated 事件會被 BroadcastServiceProvider 捕捉,並透過 WebSocket 服務(例如 Pusher 或 laravel-websockets)廣播出去。

    PHP
    // 在你的 QueueUpdated.php Event 中
    use Illuminate\Broadcasting\Channel;
    // ...
    public function broadcastOn(): Channel
    {
        // 讓所有人都監聽同一個公開頻道
        return new Channel('public.queue.updates');
    }
    
  3. 前端即時監聽:

    在 Vue.js 前端,我們的 QueueDisplay.vue 組件會使用 laravel-echo 監聽這個頻道。

    JavaScript
    // frontend/src/components/QueueDisplay.vue
    import Echo from 'laravel-echo';
    // ...
    mounted() {
        window.Echo.channel('public.queue.updates')
            .listen('QueueUpdated', (e) => {
                console.log('叫號更新了!', e.queueNumber);
                // 更新 Vue 元件的 data,讓畫面即時渲染
                this.currentNumber = e.queueNumber;
            });
    }
    

這個流程簡潔、高效,將後端業務邏輯與前端 UI 更新完美解耦,讓開發變得優雅又快樂。

第三站:運維工程師的救星 - 監控整合

身為一個資深工程師,寫出高品質的程式碼只是基本,讓系統穩定運行才是王道。

ECommerceTenancy 整合了 PrometheusGrafana,讓你輕鬆掌握系統脈動。

我寫了一個 RecordMetrics Middleware,它會在每個 API 請求結束時,自動將請求數據傳送給 Prometheus。

PHP
// backend/app/Http/Middleware/RecordMetrics.php
// 核心邏輯就是捕捉請求方法、路徑、狀態碼,並記錄到 Prometheus
$counter = $registry->getOrRegisterCounter(
    'ecommerce_platform',
    'http_requests_total',
    'Total HTTP requests to the application',
    ['method', 'endpoint', 'status_code']
);
$counter->inc([$request->method(), $request->path(), (string) $response->getStatusCode()]);

透過 Docker Compose,我們一鍵啟動 Prometheus 和 Grafana 服務。你只需要在 Grafana 裡面設定一個 Dashboard,就可以看到每個 API 的請求量、錯誤率、延遲時間等關鍵指標。

這不僅能幫助你快速排查問題,還能為未來的系統優化提供數據依據。

總結

ECommerceTenancy 專案是一個濃縮了現代 Laravel 開發精華的實戰案例。它涵蓋了多租戶架構、即時通訊、監控整合、JWT 認證、RBAC 權限控制、CI/CD 等多個領域。

如果你是 Laravel 工程師,希望提升自己的架構設計能力,或是正在尋找一個可以應用到實際專案中的完整範本,我強烈推薦你下載程式碼、親自動手 run 起來!

專案傳送門: ECommerceTenancy GitHub Repo (請替換為您的 repo 連結)

希望這篇文章對你有所啟發。如果你有任何問題或想法,歡迎留言交流!我們下次見!

沒有留言:

張貼留言

熱門文章