iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
Build on Google AI

零預算 NGO 數位轉型挑戰:30 天打造智慧訂房系統系列 第 11 篇

【Day 11】即時通知整合:利用 Telegram Bot 與 Email 實現預約雙軌自動化推播

  • 分享至 

  • xImage
  •  

昨天,我們建立了後端防鎖與衝突檢查機制,確保系統在 concurrent(高並發)預約時能夠做到零重複預約(No Overbooking)。

今天,我們將串接 Telegram Bot 與 GAS 原生 Email 服務(MailApp)。當申請人提交預約時,後端將即時進行審核與衝突比對:若預約成功,系統會自動寫入 Google 行事曆並同步透過 Telegram 與 Email 推播通知給申請人;若預約失敗,則直接在網頁端給予即時反饋。


一、 系統預約流程設計

本系統的預約與通知流程如下:

  ┌──────────────────────────────────────────────────────────┐
  │ 1. 查看 Google Calendar 看板(確定可預約時段)               │
  └────────────────────────────┬─────────────────────────────┘
                               │
                               ▼
  ┌──────────────────────────────────────────────────────────┐
  │ 2. 於 WebApp 填寫預約表單(包含 Email 與 Telegram Chat ID)  │
  └────────────────────────────┬─────────────────────────────┘
                               │
                               ▼
  ┌──────────────────────────────────────────────────────────┐
  │ 3. 後端審核與衝突比對 (LockService)                         │
  └───────────────┬──────────────────────────┬───────────────┘
                  │                          │
           【預約失敗 (時段重覆)】          【預約成功】
                  │                          │
                  ▼                          ▼
  ┌────────────────────────┐  ┌─────────────────────────────┐
  │ 網頁端即時彈窗反饋錯誤     │  │ 1. 寫入 Google Calendar     │
  │ (不發送推播通知)        │  │ 2. Telegram Bot 直播推播     │
  └────────────────────────┘  │ 3. 發送 Email 個人備忘        │
                              │ 4. 網頁顯示預約成功資訊        │
                              └─────────────────────────────┘


二、 前置準備:Telegram Bot 設定

為了讓 Telegram Bot 能夠直接發送個人訊息給申請人,需完成以下設定:

  1. 建立 Bot: 在 Telegram 搜尋 @BotFather,發送 /newbot 指令,依提示完成建立並取得 Bot Token。
  2. 申請人取得 Chat ID: 申請人只需在 Telegram 搜尋 @userinfobot 或發送 /start 給你建立的 Bot,即可取得個人專屬的 Chat ID。
  3. 表單欄位: 在預約網頁表單中,新增 Email 與 Telegram Chat ID 兩個必填/選填欄位,以便後端推播。

三、 GAS 後端通知模組 (NotificationService.gs)

新增 NotificationService.gs 檔案,專門處理 Telegram 與 Email 的發送邏輯:

// 填入從 BotFather 取得的 Telegram Bot Token
const TELEGRAM_BOT_TOKEN = 'YOUR_TELEGRAM_BOT_TOKEN';

/**
 * 預約成功:觸發 Telegram + Email 個人通知
 * @param {Object} data 預約表單資料
 */
function sendSuccessNotifications(data) {
  // 1. 若有提供 Telegram Chat ID,發送 Telegram 直播訊息
  if (data.telegramChatId) {
    sendTelegramDirectMessage(data);
  }
  
  // 2. 若有提供 Email,發送 Email 個人確認信
  if (data.applicantEmail) {
    sendEmailConfirmation(data);
  }
}

/**
 * 發送 Telegram 個人訊息給申請人
 */
function sendTelegramDirectMessage(data) {
  const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`;

  const messageText = 
    `🎉 *【場地預約成功通知】*\n\n` +
    `📍 *預約場地:* ${data.roomName}\n` +
    `👤 *申請人:* ${data.applicantName}\n` +
    `⏰ *開始時間:* ${data.startTime.replace('T', ' ')}\n` +
    `⌛ *結束時間:* ${data.endTime.replace('T', ' ')}\n` +
    `📝 *活動用途:* ${data.purpose}\n\n` +
    `👉 *備忘提醒:* 行事曆事件已順利建立,請準時使用。`;

  const payload = {
    chat_id: data.telegramChatId, // 直接發送給申請人個人的 Chat ID
    text: messageText,
    parse_mode: 'Markdown'
  };

  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  try {
    const response = UrlFetchApp.fetch(url, options);
    Logger.log("✅ Telegram 個人通知發送結果: " + response.getContentText());
  } catch (error) {
    Logger.log("❌ Telegram 發送失敗: " + error.toString());
  }
}

/**
 * 發送 Email 個人備忘至申請人信箱
 */
function sendEmailConfirmation(data) {
  const subject = `【預約成功】已為您確認 ${data.roomName} 場地預約`;
  const htmlBody = `
    <div style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
      <h2 style="color: #2b5876;">您好,${data.applicantName}:</h2>
      <p>您已成功完成場地預約,詳細資訊如下:</p>
      <table style="width: 100%; border-collapse: collapse; margin: 15px 0;">
        <tr><td style="padding: 8px; border-bottom: 1px solid #ddd;"><b>預約場地:</b></td><td style="padding: 8px; border-bottom: 1px solid #ddd;">${data.roomName}</td></tr>
        <tr><td style="padding: 8px; border-bottom: 1px solid #ddd;"><b>使用時間:</b></td><td style="padding: 8px; border-bottom: 1px solid #ddd;">${data.startTime.replace('T', ' ')} ~ ${data.endTime.replace('T', ' ')}</td></tr>
        <tr><td style="padding: 8px; border-bottom: 1px solid #ddd;"><b>活動用途:</b></td><td style="padding: 8px; border-bottom: 1px solid #ddd;">${data.purpose}</td></tr>
      </table>
      <p style="color: #666; font-size: 0.9em;">此信件為系統自動發送,該預約已同步寫入 Google 行事曆看板。</p>
    </div>
  `;

  try {
    MailApp.sendEmail({
      to: data.applicantEmail,
      subject: subject,
      htmlBody: htmlBody
    });
    Logger.log("✅ 預約確認信已寄送至: " + data.applicantEmail);
  } catch (error) {
    Logger.log("❌ Email 發送失敗: " + error.toString());
  }
}


四、 完整預約主流程整合 (Code.gs)

將審核邏輯、Google 日曆同步、雙軌推播與網頁即時反饋整合成完整的服務端處理函式:

/**
 * 處理預約表單提交 (含衝突檢查、日曆同步與通知)
 */
function handleCreateBookingWithLock(data) {
  const lock = LockService.getScriptLock();
  
  // 等待最多 10 秒以取得鎖定
  if (!lock.waitLock(10000)) {
    return createJsonResponse({
      status: 'ERROR',
      message: '系統繁忙中,請稍後再試。'
    });
  }

  try {
    const calendar = CalendarApp.getCalendarById(CALENDAR_ID);
    const start = new Date(data.startTime);
    const end = new Date(data.endTime);

    // 1. 審核:時段衝突比對 (Overbooking Check)
    const conflicts = calendar.getEvents(start, end);
    if (conflicts.length > 0) {
      // ❌ 預約失敗:解鎖並即時回傳錯誤訊息,由前端網頁直接顯示反饋
      lock.releaseLock();
      return createJsonResponse({
        status: 'ERROR',
        message: '預約失敗:該時段已被其他人預約,請重新選擇其他時間。'
      });
    }

    // 2. 審核通過:建立 Google Calendar 事件
    const eventTitle = `【預約】${data.roomName} - ${data.applicantName}`;
    const eventDescription = `預約人:${data.applicantName}\n用途:${data.purpose}\nEmail:${data.applicantEmail}`;
    
    const event = calendar.createEvent(eventTitle, start, end, {
      description: eventDescription,
      location: data.roomName
    });

    // 3. 釋放鎖定
    lock.releaseLock();

    // 4. 🎉 預約成功:發送 Email 與 Telegram 給申請人
    sendSuccessNotifications(data);

    // 5. 回傳成功狀態與資訊至前端網頁
    return createJsonResponse({
      status: 'SUCCESS',
      eventId: event.getId(),
      message: '預約成功!已同步發送通知至您的 Email 與 Telegram。'
    });

  } catch (error) {
    lock.releaseLock();
    return createJsonResponse({
      status: 'ERROR',
      message: '系統發生異常:' + error.toString()
    });
  }
}


五、 前端網頁即時反饋處理 (React 範例)

當前端呼叫後端 API 後,根據回傳的 status 給予使用者即時反饋:

async function submitBooking(formData) {
  try {
    const response = await fetch(GAS_API_URL, {
      method: 'POST',
      body: JSON.stringify(formData)
    });
    
    const result = await response.json();

    if (result.status === 'SUCCESS') {
      // 成功處理:顯示成功訊息與提示通知已寄出
      alert(`✅ ${result.message}`);
      // 清空表單或跳轉頁面
    } else {
      // ❌ 失敗處理:即時在網頁畫面上彈出錯誤提示
      alert(`⚠️ ${result.message}`);
    }
  } catch (err) {
    alert("網絡連線異常,請稍後再試。");
  }
}


六、 結語

今天我們完成了一套流暢的預約與通知機制:

  1. 使用者可先透過 Google Calendar 查看空檔。
  2. 在網頁填表申請後,由後端進行自動審核。
  3. 若衝突失敗,網頁即時跳出提示反饋;若成功,則同步寫入行事曆並直接透過 Telegram Bot 與 Email 將結果推播給申請人個人!

明日繼續……


上一篇
【Day 10】極端狀況防禦策略:運用 LockService 與衝突檢查打造「零重複預約」系統
系列文
零預算 NGO 數位轉型挑戰:30 天打造智慧訂房系統 共 11 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言