iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0

在完成了上篇的前端 FullCalendar 日曆互動介面後,我們進一步解決 NGO 場地管理中常見的「佔位不使用」(No-show)與資源浪費問題。

今天我們將開發 模組 E:現場簽到與自動釋放機制 (CheckInService.gs)。透過生成的專屬 QR Code 簽到連結、逾時未簽到自動釋放(Auto-release) 以及 Telegram 警示推播,確保資源能有效率地輪轉給有需要的團隊。

  1. 簽到與資源釋放核心邏輯設計
    系統的簽到與自動釋放流程如下:

簽到 QR Code 生成:預約成功後,系統產生包含 bookingId 與安全驗證碼(Token)的簽到網址與 QR Code。
現場掃碼簽到:使用者到達現場後,掃描貼在門口的 QR Code,系統更新 Sheet 狀態為 CHECKED_IN 並記錄時間。
時基觸發器(Time-driven Trigger):排程每 15 分鐘檢查一次所有當天預約。若超過預定開始時間 15 分鐘仍未簽到,系統將自動:
將狀態變更為 CANCELLED_NO_SHOW。
清除 FullCalendar 佔用時段,釋放場地給其他使用者。
發送 Telegram 警示通知給申請人與管理員。
2. 後端 Service 完整程式碼 (CheckInService.gs)
請在 Apps Script 專案中新增 CheckInService.gs 檔案,並寫入以下程式碼:

/**

  • CheckInService.gs
  • 負責處理預約的 QR Code 簽到、簽退以及逾時未簽到自動釋放邏輯
    */

/**

    1. 執行現場簽到驗證
  • @param {string} bookingId - 預約單號
  • @returns {Object} 簽到結果
    */
    function processCheckIn(bookingId) {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Bookings");
    const data = sheet.getDataRange().getValues();
    const now = new Date();

// 搜尋目標預約單 (假設 Col 0 為 BookingID)
for (let i = 1; i < data.length; i++) {
if (data[i][0] === bookingId) {
const currentStatus = data[i][6]; // Col 6: Status

  if (currentStatus === "CHECKED_IN") {
    return { success: false, message: "⚠️ 此預約已完成簽到,請勿重複簽到。" };
  }
  
  if (currentStatus === "CANCELLED_NO_SHOW" || currentStatus === "CANCELLED") {
    return { success: false, message: "❌ 此預約已被取消或因逾時未簽到而釋放。" };
  }

  // 更新狀態與簽到時間
  sheet.getRange(i + 1, 7).setValue("CHECKED_IN"); // Col 7: Status
  sheet.getRange(i + 1, 8).setValue(now);          // Col 8: CheckInTime

  const userEmail = data[i][1];
  const roomId = data[i][2];

  // 發送 Telegram 簽到成功通知
  const notifyMessage = `✅ <b>【場地簽到成功 notification】</b>\n\n` +
                        `📌 預約單號:${bookingId}\n` +
                        `🏢 房間:${roomId}\n` +
                        `👤 申請人:${userEmail}\n` +
                        `⏰ 簽到時間:${Utilities.formatDate(now, "GMT+8", "yyyy-MM-dd HH:mm:ss")}`;
  
  sendTelegramNotification(notifyMessage);

  return {
    success: true,
    message: `🎉 預約 ${bookingId} 簽到成功!祝您使用順利。`
  };
}

}

return { success: false, message: "❌ 找不到對應的預約紀錄。" };
}

/**

    1. 核心排程任務:自動排查逾時未簽到並釋放資源 (Auto-Release)
  • 建議設定 Time-driven Trigger 每 10~15 分鐘執行一次
    */
    function autoReleaseNoShowBookings() {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Bookings");
    const data = sheet.getDataRange().getValues();
    const now = new Date();

// 緩衝寬限時間:預定開始時間超過 15 分鐘未簽到即視為 No-Show
const GRACE_PERIOD_MS = 15 * 60 * 1000;

let releasedCount = 0;

for (let i = 1; i < data.length; i++) {
const bookingId = data[i][0];
const userEmail = data[i][1];
const roomId = data[i][2];
const startTime = new Date(data[i][3]); // Col 3: StartTime
const status = data[i][6]; // Col 6: Status

// 只排查狀態為 APPROVED / CONFIRMED 的預約
if (status === "APPROVED" || status === "CONFIRMED") {
  const timeDiff = now.getTime() - startTime.getTime();

  // 當前時間已超過預定開始時間 + 15 分鐘
  if (timeDiff > GRACE_PERIOD_MS) {
    // 1. 更新 Google Sheet 狀態為 CANCELLED_NO_SHOW
    sheet.getRange(i + 1, 7).setValue("CANCELLED_NO_SHOW");
    sheet.getRange(i + 1, 9).setValue(now); // Col 9: ReleaseTime

    releasedCount++;

    // 2. 發送 Telegram 警示通知
    const warningMessage = `🚨 <b>【資源逾時自動釋放警示】</b>\n\n` +
                           `📌 預約單號:${bookingId}\n` +
                           `🏢 房間:${roomId}\n` +
                           `👤 申請人:${userEmail}\n` +
                           `⏰ 原定時間:${Utilities.formatDate(startTime, "GMT+8", "HH:mm")}\n` +
                           `⚠️ 原因:已超過 15 分鐘未完成現場簽到,系統已自動釋放該時段供他人預約。`;

    sendTelegramNotification(warningMessage);
  }
}

}

Logger.log([Auto-Release Task] 掃描完成,共釋放 ${releasedCount} 筆逾時預約。);
}

/**

    1. 生成專屬 QR Code URL 輔助函式
  • @param {string} bookingId
  • @returns {string} QuickChart QR Code API 圖片網址
    */
    function generateCheckInQRCodeUrl(bookingId) {
    const webAppUrl = ScriptApp.getService().getUrl();
    const checkInUrl = ${webAppUrl}?action=checkin&bookingId=${encodeURIComponent(bookingId)};

// 使用 QuickChart 免費 API 生成 QR Code 圖檔
return https://quickchart.io/qr?text=${encodeURIComponent(checkInUrl)}&size=300;
}

  1. Web App 入口點整合 (Code.gs 擴充)
    請更新 Code.gs 中的 doGet 邏輯,讓使用者掃描 QR Code 開啟網址時,能直接進入簽到處理程序:

/**

  • Web App 入口點 (HTTP GET)
    */
    function doGet(e) {
    const action = e.parameter.action;
    const bookingId = e.parameter.bookingId;

// 處理 QR Code 簽到掃描
if (action === 'checkin' && bookingId) {
const result = processCheckIn(bookingId);

// 簡單回傳簽到結果 HTML 頁面
const output = `
  <!DOCTYPE html>
  <html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="https://cdn.tailwindcss.com"></script>
    <title>現場簽到結果</title>
  </head>
  <body class="bg-slate-100 flex items-center justify-center min-h-screen p-4">
    <div class="bg-white p-8 rounded-xl shadow-md text-center max-w-sm w-full">
      <h1 class="text-2xl font-bold mb-4 ${result.success ? 'text-emerald-600' : 'text-rose-600'}">
        ${result.success ? '✅ 簽到成功' : '❌ 簽到失敗'}
      </h1>
      <p class="text-slate-600 mb-6">${result.message}</p>
      <button onclick="window.close()" class="bg-slate-800 text-white px-4 py-2 rounded-lg text-sm">關閉頁面</button>
    </div>
  </body>
  </html>
`;
return HtmlService.createHtmlOutput(output);

}

// 預設渲染 FullCalendar 主介面 (Day 21)
return HtmlService.createHtmlOutputFromFile('Index')
.setTitle('NGO 房間與資源預約系統')
.setXframeOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

  1. 設定自動排查觸發器 (Time-Driven Trigger)
    要讓 autoReleaseNoShowBookings 能夠定時執行,可在 Apps Script 中完成設定:

開啟左側選單的 「觸發條件 (Triggers)」 ⏰。
點擊右下角 「新增觸發條件」。
設定參數如下:
選擇要執行的函式:autoReleaseNoShowBookings
選擇部署作業:Head
選擇活動來源:時間驅動 (Time-driven)
選擇時間型觸發條件類型:分鐘計時器 (Minute timer)
選擇分鐘間隔:每 15 分鐘
5. 小結
**簽到/簽退模組與 QR Code 自動釋放機制 (CheckInService.gs)**在今天完成了。透過 QR Code 現場簽到與自動化排程,能有效防止場地被幽靈預約佔用,顯著提升 NGO 空間資源的利用率。


上一篇
【Day 22】簽到/簽退模組與 QR Code 驗證
系列文
零預算 NGO 數位轉型挑戰:30 天打造智慧訂房系統 共 23 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言