Fullstack 2024

DenTalk

牙醫師專用平台,整合論壇、求職、商城、課程四大功能模組,支援多角色系統與完整金物流串接

DenTalk

專案概述

DenTalk 是一個專為牙醫師及相關領域打造的綜合性平台。 這是一個功能複雜、需求繁多的大型專案,整合了論壇、求職、商城、課程四大核心功能, 創造一個完整的牙醫師生態圈。

專案類型: Fullstack 全端開發

開發時間: 2024

主要技術: Laravel, Astro, 綠界金物流

核心功能: 論壇、求職、商城、課程

平台特色

DenTalk 最大的特色在於其複雜的多角色架構。 這並不是由業主在後台上傳資料,然後顯示到前端的傳統模式, 而是作為一個平台,讓不同角色的前端用戶可以在上面刊登、使用

這使得 API 架構變得極為複雜,必須考慮:

  • 不同用戶視角 - 醫師、診所、講師、學員等多種身份
  • 不同需求用途 - 瀏覽、刊登、管理、購買等多種操作
  • 權限與資料可見性 - 根據身份決定可見與可操作的資料範圍

💬 論壇功能

符合權限的用戶可以在平台上刊登文章,討論牙醫相關議題。 系統提供彈性的身份顯示設定,用戶可以選擇使用自訂暱稱或完全匿名發文, 保護隱私的同時也能暢所欲言。

論壇具備完整的社交功能,包括按讚、留言、分享等互動機制。 文章依照討論主題分類,用戶可以依據感興趣的類別, 輕鬆找到相關討論並參與互動。

論壇功能

討論論壇介面

🛒 商城功能

擁有販賣資格的用戶可以建立自己的商城,類似蝦皮的賣家帳號概念。 系統提供完整的商店管理功能,包括設定寄件資訊、上架商品、填寫金流物流相關內容等。

商城支援多規格類型的產品展示,買家可以像在蝦皮一樣點選不同規格組合。 完整的購物車與結帳流程,並串接綠界金流與物流,提供安全便利的交易體驗。

商城功能

商品列表頁

商城管理

賣家後台管理

商品規格

多規格選擇

購物車

購物車系統

💼 求職功能

醫院、診所可以刊登職缺,而牙醫師、牙醫事相關領域人員、實習生則可以刊登履歷。 平台提供雙向媒合機制,讓雇主找到合適人才,求職者找到理想職缺。

列表頁具備細緻的篩選功能,可以根據地點、薪資、職務類別、評分高低等條件精準搜尋。

最特別的是針對牙醫產業客製化的班表功能。 一般牙醫診所的職缺可以區分為早診、午診、晚診, 系統提供行事曆班表設定,讓醫師能夠清楚看到可配合的上班時間, 大幅提升媒合效率。

求職功能

新增職缺

求職篩選

篩選功能

班表行事曆

特製班表

找人才

履歷列表

履歷詳情

履歷詳情

職缺詳情

職缺詳情

班表系統

班表系統

📚 課程功能

講師身份的用戶可以在平台上架課程,每一個課程可以設定多個場次循環, 同一個場次又能包含多個課程設定,提供彈性的課程安排。

系統提供QR Code 掃描點名功能,讓講師能夠快速完成學員簽到。

更進階的是完整的考卷系統。講師可以在每個課程場次結束後發布考試, 題型支援單選、多選、是非、申論等多種形式, 讓講師能夠自訂適合的測驗題目,檢驗學員學習成效。

課程功能

課程列表

考試系統

課程詳情頁

專案成果

4 大

核心功能模組

多角色

複雜權限系統

完整

金物流串接

客製化

產業專屬功能

多重身份的設定

User 資料表是系統中最頻繁被查詢的資源之一。為了減少查詢次數和 JSON 資料的消耗, 我們選擇將多重身份資訊儲存在同一張表的單一欄位中,採用 Bitmask(位元遮罩)的方式處理。

Bitmask 實作

以下是 EnumIdentity 的實作,透過位元運算(1 << n)定義 12 種不同身份類型, 包括牙醫師、診所、醫院、講師、學生等。每個身份對應一個獨立的位元位置,可以透過位元 OR 運算組合多重身份。

<?php

namespace App\Enum;

use Illuminate\Support\Collection;

enum EnumIdentity: int
{
    case NULL = 0;
    case DENTIST = 1 << 0;              // 牙醫師
    case DENTAL_ASSISTANT = 1 << 1;     // 牙科助理
    case DENTAL_HYGIENIST = 1 << 2;     // 口腔衛生師
    case DENTAL_TECHNICIAN = 1 << 3;    // 牙體技術師
    case INTERN = 1 << 4;                // 實習生
    case STUDENT = 1 << 5;               // 學生
    case CLINIC = 1 << 6;                // 診所
    case HOSPITAL = 1 << 7;              // 醫院
    case VENDOR = 1 << 8;                // 廠商
    case INSTRUCTOR = 1 << 9;            // 講師
    case ASSOCIATION = 1 << 10;          // 公會
    case SOCIETY = 1 << 11;              // 協會

    public function getLabel(): string { /* ... */ }
    public function getGroup(): string { /* ... */ }

    // 權限判斷方法
    public function isJobProvider(): bool { /* 醫院、診所 */ }
    public function isJobSeeker(): bool { /* 牙醫師、助理等 */ }
    public function isCourseProvider(): bool { /* 講師 */ }
    public function canSell(): bool { /* 可販售權限 */ }

    // Bitmask 雙向轉換
    public static function fromBitmask(int $bitmask): Collection
    {
        // 將整數轉換為身份集合
        return collect(self::cases())
            ->filter(fn ($case) => ($bitmask & $case->value) !== 0);
    }

    public static function toBitmask(array|Collection $identities): int
    {
        // 將身份集合轉換為整數
        return collect($identities)
            ->map(fn ($identity) => $identity->value)
            ->reduce(fn ($carry, $val) => $carry | $val, 0);
    }
}

技術優勢

  • 資料庫效能 - 單一欄位儲存,減少 JOIN 查詢,提升查詢速度
  • 彈性組合 - 使用者可同時擁有多種身份(如:牙醫師 + 講師)
  • 權限管理 - 透過 isJobProvider()canSell() 等方法快速判斷權限
  • 身份分組 - getGroup() 方法可將相關身份歸類(如:牙醫事相關人員)

聊天室系統架構

考慮到 Pusher.js 的費用高昂,本專案採用 Laravel 官方推薦的 Laravel Reverb 自架 WebSocket Server,大幅降低營運成本。

架構說明

  • 前端用戶 ↔ Reverb - 前端透過 WebSocket 與 Reverb Server 建立持久連線
  • Laravel → Reverb - Laravel 後端透過 Broadcasting 將訊息推送至 Reverb
  • Reverb → 前端 - Reverb Server 即時轉發訊息給所有訂閱該頻道的前端用戶

技術優勢

  • 成本控制 - 自架 Server,無需支付第三方 WebSocket 服務費用
  • 官方支援 - Laravel 原生整合,API 使用簡單直覺
  • 擴展性佳 - 可依需求調整 Server 規格,靈活控制連線數
  • 即時通訊 - WebSocket 連線提供低延遲的雙向通訊體驗
聊天室系統架構

Laravel Reverb 架構圖

訂單複雜的狀態管理

商城功能涉及複雜的訂單狀態轉換,從下單、付款、出貨到完成,每個階段都有嚴格的狀態限制。 為了避免程式碼中充斥大量的 if/else 條件判斷,本專案採用 State Machine(狀態機)的設計模式。

State Machine 優勢

  • 清晰的狀態定義 - 明確定義所有可能的訂單狀態(待付款、已付款、配送中、已完成、已取消等)
  • 嚴格的轉換規則 - 限制狀態只能按照預定規則轉換,避免非法狀態出現
  • 易於維護 - 避免複雜的條件判斷巢狀,新增狀態或修改流程時更加安全
  • 程式碼可讀性 - 狀態轉換邏輯集中管理,容易理解整體業務流程

透過 State Machine 模式,即使訂單流程再複雜,也能保持程式碼的簡潔與可維護性。