iT邦幫忙

2025 iThome 鐵人賽

DAY 25
0
Software Development

我獨自開發 - 用 Supabase 打造全端應用系列 第 25

第二十五關 - 來企排隊: Supabase 快速建立即時通知功能

  • 分享至 

  • xImage
  •  

通知列表

當在餐廳排隊時,商家可以即時通知,而不需要一直刷新手機畫面。

在排隊系統中,當預約狀態改變時(比如商家確認預約、開始服務等),相關的人員會立即收到通知,就像 LINE 或 WhatsApp 的訊息推送一樣。

技術原理

傳統方式 vs 即時通知

傳統方式(需要手動刷新):

客戶 → 不斷詢問服務器「有新消息嗎?」
服務器 → 「沒有」「沒有」「沒有」「有!」

即時通知方式:

服務器 → 主動推送「你有新消息!」→ 客戶

Supabase Realtime 的優勢

Supabase 提供即時功能,就像建立一個「廣播電台」:

  • 每個用戶都有自己的「頻道」
  • 當有消息時,直接「廣播」給特定用戶
  • 用戶「收聽」自己的頻道,即時接收消息

實作步驟

步驟 1:建立即時通知 Hook

建立一個專門處理即時通知的 Hook,就像建立一個「通訊中心」:

// hooks/useRealtimeNotify.ts
import { useEffect } from "react";
import { supabase } from "@/lib/supabase/client";
import { useAuth } from "@/hooks/useAuth";

export function useRealtimeNotify() {
  const { user } = useAuth();

  // 監聽即時通知(像是「收聽廣播」)
  useEffect(() => {
    if (!user?.id) return;

    // 建立專屬頻道,就像每個人有自己的電話號碼
    const channel = supabase
      .channel(`user-${user.id}`)
      .on("broadcast", { event: "notification" }, (payload) => {
        const { title, message } = payload.payload;

        const notificationEvent = new CustomEvent("realtime-notification", {
          detail: {
            title,
            message,
            created_at: new Date().toISOString(),
          },
        });
        window.dispatchEvent(notificationEvent);
      })
      .subscribe();

    // 清理資源(離開時「掛斷電話」)
    return () => supabase.removeChannel(channel);
  }, [user?.id]);

  // 發送即時通知(像是「打電話給別人」)
  const sendNotification = async (
    userId: string,
    title: string,
    message: string
  ) => {
    await supabase.channel(`user-${userId}`).send({
      type: "broadcast",
      event: "notification",
      payload: { title, message, timestamp: Date.now() },
    });
  };

  return { sendNotification };
}

步驟 2:在預約頁面中使用

當客戶預約時,立即通知商家:

// app/my-bookings/page.tsx
import { useRealtimeNotify } from "@/hooks/useRealtimeNotify";

export default function MyBookingsPage() {
  const { sendNotification } = useRealtimeNotify();

  const handleSubmitBooking = async (bookingId: string) => {
    try {
      const booking = bookings.find((b) => b.id === bookingId);

      // 1. 先更新資料庫
      await submitBookingFromHook(bookingId);

      // 2. 發送即時通知給商家
      if (booking?.store_owner_id) {
        await sendNotification(
          booking.store_owner_id,
          "預約已發送",
          `客戶已建立 ${booking.store_name} 的預約`
        );
      }
    } catch (error) {
      console.error("預約失敗:", error);
    }
  };
}

步驟 3:在商家管理頁面中使用

當商家確認預約時,立即通知客戶:

// app/received-bookings/page.tsx
import { useRealtimeNotify } from "@/hooks/useRealtimeNotify";

export default function ReceivedBookingsPage() {
  const { sendNotification } = useRealtimeNotify();

  const handleAcceptBooking = async (bookingId: string) => {
    try {
      const booking = bookings.find((b) => b.id === bookingId);

      // 1. 先更新資料庫
      await acceptBooking(bookingId);

      // 2. 發送即時通知給客戶
      if (booking?.user_id) {
        await sendNotification(
          booking.user_id,
          "預約已確認!",
          `${booking.store_name} 已確認您的預約,號碼牌:${booking.queue_number}`
        );
      }
    } catch (error) {
      console.error("確認預約失敗:", error);
    }
  };
}

步驟 4:在通知列表中監聽

讓通知頁面能即時更新:

// app/notifications/page.tsx
import { useEffect, useState } from 'react';
import { useRealtimeNotify } from '@/hooks/useRealtimeNotify';

export default function NotificationsPage() {
  // 從伺服器端獲取初始通知列表
  const [notifications, setNotifications] = useState<Notification[]>([]);

  // 啟動即時通知監聽
  useRealtimeNotify();

  useEffect(() => {
    const handleRealtimeNotification = (event: CustomEvent<Partial<Notification>>) => {
      const { title, message, created_at } = event.detail;

      // 使用收到的資料建立新的通知物件
      const newNotification: Notification = {
        title: title || '新通知',
        message: message || '',
        created_at: created_at || new Date().toISOString(),
      };

      // 將新通知加到列表頂部,讓使用者馬上看到
      setNotifications(prev => [newNotification, ...prev]);
    };

    window.addEventListener('realtime-notification', handleRealtimeNotification as EventListener);

    // 在元件卸載時移除監聽器,防止記憶體洩漏
    return () => {
      window.removeEventListener('realtime-notification', handleRealtimeNotification as EventListener);
    };
  }, []);

  return (
    <div className="p-4">
      <h1 className="text-2xl font-bold mb-4">通知</h1>
      <ul>
        {notifications.map((notification) => (
          <li key={notification.id} className="border-b p-2">
            <p className="font-semibold">{notification.title}</p>
            <p>{notification.message}</p>
            <p className="text-xs text-gray-500">{new Date(notification.created_at).toLocaleString()}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

完整通知流程

客戶預約餐廳

  1. 客戶提交預約

    客戶 → 發送預約 → 資料存入資料庫 → 觸發器自動建立通知
    
  2. 商家收到通知

    資料庫觸發器 → 建立通知記錄 → 即時推送給商家 → 商家看到「新預約請求」
    
  3. 商家確認預約

    商家點擊確認 → 更新預約狀態 → 發送即時通知 → 客戶立即收到「預約已確認」
    
  4. 服務開始

    商家點擊開始服務 → 發送即時通知 → 客戶收到「輪到您了!」
    

測試

1. 本地開發測試

# 啟動本地 Supabase
supabase start

# 在瀏覽器中打開應用,同時開啟多個分頁模擬不同用戶

2. Realtime Inspector

  • 在 Supabase Dashboard 中使用 Realtime Inspector
  • 監控頻道連線和訊息傳遞情況

總結

即時通知功能讓排隊系統更加人性化,用戶不再需要不斷刷新頁面等待更新。

... to be continued

有任何想討論歡迎留言,或需要指正的地方請鞭大力一點,歡迎訂閱、按讚加分享,分享給想要提升開發效率的朋友


上一篇
第二十四關 - 來企排隊: Supabase 快速建立商家列表和預約功能
系列文
我獨自開發 - 用 Supabase 打造全端應用25
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言