iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
Build on Google AI

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

【Day 17】實戰操作全流程示範:新職工註冊審核、預約週期、權限異動與管理員月結報表

  • 分享至 

  • xImage
  •  

今天我們透過一次完整的實際操作示範,從新職工「阿樂」申請加入機構開始,一步步走完「身分審核 -> 登入與預約 -> 改期規章與簽到 -> 帳戶停用/移除 -> 月底月結單」的完整營運生命週期,並附上全流程架構圖。


1. 系統整體架構圖 (System Architecture)

我們的系統採用輕量化、無伺服器(Serverless)導向的微服務構想,將前端 UI、邏輯控制層與資料持久層解耦。

┌─────────────────────────────────────────────────────────────┐
│                       用戶端 (Client Layer)                  │
│                                                             │
│   ┌─────────────────────┐       ┌───────────────────────┐   │
│   │   Web 預約前端介面    │       │  Telegram Bot 機器人   │   │
│   └──────────┬──────────┘       └───────────┬───────────┘   │
└──────────────┼──────────────────────────────┼───────────────┘
    (HTTP POST │ / REST)             (Webhook │ / HTTPS) 
               ▼                              ▼ 
┌─────────────────────────────────────────────────────────────┐
│                     應用程式層 (App Layer)                    │
│                                                             │
│              ┌───────────────────────────────┐              │
│              │      Google Apps Script       │              │
│              │     (或後端 Node.js API)       │              │
│              └──────────────┬────────────────┘              │
└─────────────────────────────┼───────────────────────────────┘
                              │
               ┌──────────────┼──────────────┐
               │              │              │
               ▼              ▼              ▼
┌──────────────────┐ ┌────────────────┐ ┌──────────────────┐
│ Google Calendar  │ │ Google Sheets  │ │ Telegram API     │
│ (日曆同步/排程)    │ │ (主資料庫/紀錄)  │ │ (即時推播通知)     │
└──────────────────┘ └────────────────┘ └──────────────────┘

架構元件說明:

  1. 用戶介面層:使用者可透過 Web 前端進行場地/資源預約;管理者與訂閱者則可透過 Telegram Bot 接獲即時訊息。
  2. 商業邏輯層:由後端 API (GAS / Node.js) 負責驗證身份、處理時間衝突檢測以及排程邏輯。
  3. 資料與外部服務層:使用 Google Sheets 儲存詳細預約單紀錄,同步寫入 Google Calendar 呈現行事曆,並呼叫 Telegram API 發送通知。

2. 核心預約業務流程圖 (Booking Flow)

當使用者在前端提出預約請求時,後端系統執行的完整檢驗與處理流程如下:

       [ 使用者送出預約表單 ]
                 │
                 ▼
       [ 後端接收 Payload ]
                 │
                 ▼
       /  檢查欄位是否完整? \
      <                       > ─── (否) ───► [ 回傳 400 錯誤訊息 ]
       \                      /
                 │ (是)
                 ▼
       / 時段是否重複/衝圖? \
      <                       > ─── (是) ───► [ 回傳 409 衝突通知 ]
       \                      /
                 │ (否)
                 ▼
      [ 寫入 Google Sheets ]
                 │
                 ▼
     [ 建立 Google Calendar 事件 ]
                 │
                 ▼
      [ 觸發 Telegram 機器人通知 ]
                 │
                 ▼
       [ 回傳 200 預約成功結果 ]


3. 時序圖:場地預約與通知推播 (Sequence Diagram)

以下呈現使用者、前端介面、後端服務、資料庫與 Telegram 通知模組之間的互動順序:

使用者 (User) Web 前端 後端 API 資料庫 (Sheets) Telegram Bot
─ 1. 填寫預約─►
. ─2. 送出 API (POST Request) ─►
. ─3. 查詢排程衝突─►
. ◄─ 4. 回傳查詢結果─
. ─5. 寫入預約紀錄─►
. ◄─6. 確認寫入成功─
. ─7. 發送管理員 通知推播 ─►
. ◄─8. 回傳成功─
. ◄─9. 顯示結果─

  1. 新職工電郵登記與驗證碼發送: 第一階段:身分註冊與安全核實.
    新職工阿樂(lok@ngo.org)首次開啟系統頁面,點擊「新職工註冊」並輸入機構 Email。系統會自動發送一組 6 位數一次性驗證碼 (OTP) 至其電子郵箱。
/**
 * 發送 Email 驗證碼 (OTP)
 */
function sendVerificationCode(email) {
  const code = Math.floor(100000 + Math.random() * 900000).toString();
  const cache = CacheService.getScriptCache();
  cache.put(`OTP_${email}`, code, 600); // 有效時間 10 分鐘

  const subject = '【房間預約系統】新職工電郵驗證碼';
  const body = `親愛的職工您好,\n\n您的系統註冊驗證碼為:${code}\n請於 10 分鐘內於頁面輸入此驗證碼完成核實。`;
  
  GmailApp.sendEmail(email, subject, body);
  return { success: true, message: '驗證碼已發送至您的電郵' };
}

  1. 填寫驗證碼、管理員審批與登入阻擋: 第一階段:身分註冊與安全核實.
    阿樂在前端介面填入驗證碼。後端核實無誤後,將帳戶寫入 User_Roles 資料表(狀態為 PENDING_APPROVAL),並即時發送審批通知給管理員。此時阿樂無法登入系統,需待管理員完成審核並收到開通通知後方可登入。
/**
 * 核實驗證碼、建立待審核帳戶並發送審批通知給管理員
 */
function verifyOTPAndRegister(email, inputCode, department) {
  const cache = CacheService.getScriptCache();
  const savedCode = cache.get(`OTP_${email}`);

  if (!savedCode || savedCode !== inputCode) {
    throw new Error('VERIFICATION_FAILED: 驗證碼錯誤或已過期');
  }

  // 寫入 User_Roles,初始狀態為 PENDING_APPROVAL
  const ss = SpreadsheetApp.openById(getEnvConfig().SPREADSHEET_ID);
  const sheet = ss.getSheetByName('User_Roles');
  sheet.appendRow([email, 'USER', department, 'PENDING_APPROVAL', new Date()]);

  // 即時發送審批通知給管理員(Email + Telegram)
  const adminEmail = getEnvConfig().ADMIN_EMAIL;
  const approvalSubject = '【系統審批通知】有新職工申請系統權限';
  const approvalBody = `管理員您好:\n\n新職工申請資訊如下:\n- Email: ${email}\n- 部門: ${department}\n\n請前往後端管理主頁進行審核開通。`;
  
  GmailApp.sendEmail(adminEmail, approvalSubject, approvalBody);
  sendTelegramNotice(` Notification: 新職工註冊審批申請\nEmail: ${email}\n部門: ${department}\n請管理員前往後端審核。`);

  return { status: 'PENDING_APPROVAL', message: '驗證成功!申請已送出,請等待管理員審核開通通知後方可登入。' };
}

/**
 * 管理員開通職工帳戶權限
 */
function approveUserAccount(adminEmail, targetEmail) {
  const ss = SpreadsheetApp.openById(getEnvConfig().SPREADSHEET_ID);
  const sheet = ss.getSheetByName('User_Roles');
  const data = sheet.getDataRange().getValues();

  for (let i = 1; i < data.length; i++) {
    if (data[i][0].toLowerCase() === targetEmail.toLowerCase()) {
      sheet.getRange(i + 1, 4).setValue('ACTIVE'); // 更新狀態為 ACTIVE
      
      // 發送開通通知 Email 給職工
      GmailApp.sendEmail(
        targetEmail, 
        '【房間預約系統】帳戶開通通知', 
        '親愛的職工您好:\n\n您的系統帳戶已通過管理員審核,現在可以正式登入系統並使用預約服務!'
      );
      return { success: true, message: `已成功開通 ${targetEmail} 之帳戶權限。` };
    }
  }
}

  1. 查詢可預約房間、新預約與簽到: 第二階段:職工日常預約與改期流程.
    阿樂收到開通通知後登入系統,為小組會議查詢並預約房間:
  • 1. 查詢房間:阿樂選擇日期與時間,系統列出 A1、A2、B1、B2(可容納 20 人)及洽談室(可容納 4 人)的即時空置狀態。
  • 2. 發起預約:選擇 A1 房間,因時段空置,後端自動核准並建立預約單(狀態 APPROVED),同步生成 QR Code 與通知。
/**
 * 職工發起新預約
 */
function submitNewBooking(userEmail, roomId, startTime, endTime, purpose) {
  if (!isRoomVacant(roomId, startTime, endTime)) {
    throw new Error('CONFLICT: 該時段已被預約,請選擇其他時段');
  }

  const bookingId = 'BK_' + new Date().getTime();
  const ss = SpreadsheetApp.openById(getEnvConfig().SPREADSHEET_ID);
  const sheet = ss.getSheetByName('Bookings');

  sheet.appendRow([
    bookingId,
    roomId,
    userEmail,
    new Date(startTime),
    new Date(endTime),
    'APPROVED',
    'NOT_CHECKED_IN',
    purpose,
    new Date(),
    '' // 備註/酌情處理欄位
  ]);

  return { bookingId, status: 'APPROVED' };
}

  1. 預約改期營運機制與 24 小時取消限制: 第二階段:職工日常預約與改期流程.

預約改期與取消規章

  1. 取消原預約:不支援直接變更時間,職工須先取消原預約。成功後系統自動發出 「預約取消確認通知」。
  2. 60 天內重新預約:職工憑該筆取消紀錄,可在 60 天內 重新選擇並預訂新日期時段。
  3. 24 小時前取消限制(違規計費):
  • 取消須於原預約時間前 至少 1 天(24 小時) 提出。
  • 少於 1 天取消或逾期未簽到,系統判定為 「預約後不使用 (Late Cancel)」,照樣納入月結算計費。
  1. 管理員酌情權:如遇特殊緊急事故,管理員有權在後端系統勾選豁免,於月結單上剔除不作計費。
/**
 * 職工申請取消預約(含 24HR 檢測邏輯)
 */
function cancelBooking(userEmail, bookingId, reason) {
  const ss = SpreadsheetApp.openById(getEnvConfig().SPREADSHEET_ID);
  const sheet = ss.getSheetByName('Bookings');
  const data = sheet.getDataRange().getValues();

  for (let i = 1; i < data.length; i++) {
    if (data[i][0] === bookingId && data[i][2] === userEmail) {
      const bookingStart = new Date(data[i][3]);
      const now = new Date();
      const hoursDifference = (bookingStart - now) / (1000 * 60 * 60);

      let cancellationStatus = 'CANCELLED_VALID';
      let isBillable = false;

      // 檢測是否少於 24 小時
      if (hoursDifference < 24) {
        cancellationStatus = 'LATE_CANCELLED'; // 少於一天取消(算作預約不使用)
        isBillable = true;
      }

      sheet.getRange(i + 1, 6).setValue(cancellationStatus);
      
      const noticeMsg = isBillable 
        ? `您的預約 ${bookingId} 已取消。注意:因距原預約不足 24 小時,系統記錄為「預約不使用」並照常計費。您仍可在 60 天內改訂新日期。`
        : `您的預約 ${bookingId} 已成功取消。您可以在 60 天內重新改訂新日期與時段。`;

      GmailApp.sendEmail(userEmail, '【房間預約系統】預約取消確認通知', noticeMsg);
      return { success: true, isBillable, message: noticeMsg };
    }
  }
}

  1. 職工離職/休假權限暫停與徹底移除: 第三階段:管理員動態權限管理.
    若職工離職、長期休假或因故需調整權限,管理員可隨時於後端將其帳戶改為 SUSPENDED(暫停) 或 REMOVED(徹底移除),系統將即刻阻擋其登入與預約權限。
/**
 * 管理員隨時變更職工帳戶狀態(暫停或徹底移除)
 */
function updateUserStatus(adminEmail, targetEmail, newStatus) {
  // newStatus: 'SUSPENDED' (暫停) 或 'REMOVED' (移除) 或 'ACTIVE' (復權)
  const ss = SpreadsheetApp.openById(getEnvConfig().SPREADSHEET_ID);
  const sheet = ss.getSheetByName('User_Roles');
  const data = sheet.getDataRange().getValues();

  for (let i = 1; i < data.length; i++) {
    if (data[i][0].toLowerCase() === targetEmail.toLowerCase()) {
      if (newStatus === 'REMOVED') {
        sheet.deleteRow(i + 1); // 刪除資料列
        return { success: true, message: `已將職工 ${targetEmail} 之權限紀錄徹底移除。` };
      } else {
        sheet.getRange(i + 1, 4).setValue(newStatus); // 更新狀態為 SUSPENDED 或 ACTIVE
        return { success: true, message: `已將職工 ${targetEmail} 之帳戶狀態更新為:${newStatus}` };
      }
    }
  }
  throw new Error('NOT_FOUND: 找不到該職工帳戶');
}

  1. 月結報表、管理員酌情扣除與寄發月結單: 第四階段:月初管理員自動化結算與統計.
    每月 1 號,管理員點擊「生成上月租用報表」。系統自動統計全機構職工的房間使用時數。管理員可於介面上審視「少於 1 天取消 / 預約不使用」清單,行使酌情權勾選免除計費,最後向每位職工自動寄發 「個人租用對帳月結單」。
/**
 * 管理員行使酌情權:剔除不作計費
 */
function applyAdminDiscretion(bookingId, adminEmail, note) {
  const ss = SpreadsheetApp.openById(getEnvConfig().SPREADSHEET_ID);
  const sheet = ss.getSheetByName('Bookings');
  const data = sheet.getDataRange().getValues();

  for (let i = 1; i < data.length; i++) {
    if (data[i][0] === bookingId) {
      sheet.getRange(i + 1, 6).setValue('CANCELLED_WAIVED'); // 酌情豁免計費
      sheet.getRange(i + 1, 10).setValue(`管理員酌情豁免:${note}`);
      return { success: true, message: '已執行管理員酌情權,該筆預約於月結單上剔除不作計費。' };
    }
  }
}

/**
 * 每月 1 號自動執行:生成報表並寄發個人月結單
 */
function generateMonthlyReportAndSendStatements() {
  const ss = SpreadsheetApp.openById(getEnvConfig().SPREADSHEET_ID);
  const bookingSheet = ss.getSheetByName('Bookings');
  const data = bookingSheet.getDataRange().getValues();

  const now = new Date();
  const lastMonthYear = now.getMonth() === 0 ? now.getFullYear() - 1 : now.getFullYear();
  const lastMonth = now.getMonth() === 0 ? 11 : now.getMonth() - 1;

  const userStats = {}; 

  for (let i = 1; i < data.length; i++) {
    const userEmail = data[i][2];
    const startTime = new Date(data[i][3]);
    const endTime = new Date(data[i][4]);
    const status = data[i][5]; 
    const checkInStatus = data[i][6];

    if (startTime.getFullYear() === lastMonthYear && startTime.getMonth() === lastMonth) {
      const durationHours = (endTime - startTime) / (1000 * 60 * 60);

      if (!userStats[userEmail]) {
        userStats[userEmail] = { totalHours: 0, billableHours: 0, count: 0, checkedInCount: 0, lateCancelCount: 0, waivedCount: 0 };
      }

      userStats[userEmail].count += 1;

      if (status === 'APPROVED' || status === 'LATE_CANCELLED') {
        userStats[userEmail].billableHours += durationHours;
      }
      if (status === 'APPROVED') {
        userStats[userEmail].totalHours += durationHours;
      }
      if (checkInStatus === 'CHECKED_IN') {
        userStats[userEmail].checkedInCount += 1;
      }
      if (status === 'LATE_CANCELLED') {
        userStats[userEmail].lateCancelCount += 1;
      }
      if (status === 'CANCELLED_WAIVED') {
        userStats[userEmail].waivedCount += 1;
      }
    }
  }

  // 逐一寄發個人月結單 Email
  Object.keys(userStats).forEach(email => {
    const stat = userStats[email];
    const monthLabel = `${lastMonthYear} 年 ${lastMonth + 1} 月`;
    
    const emailContent = `
親愛的職工 (${email}) 您好:

以下是您於【${monthLabel}】的房間使用統計與月結對帳單:

【預約與使用統計】
• 總預約次數:${stat.count} 次
• 成功簽到次數:${stat.checkedInCount} 次
• 少於24小時取消 / 預約不使用次數:${stat.lateCancelCount} 次
• 管理員酌情豁免次數:${stat.waivedCount} 次

【租用費用計算】
• 實際使用時數:${stat.totalHours} 小時
• 應計費總時數(含預約不使用時數):${stat.billableHours} 小時

感謝您對機構資源的合規使用!
※ 備註:若有少於一天取消之紀錄但屬特殊緊急情況,請聯絡管理員評估執行酌情權調整。
    `;

    GmailApp.sendEmail(email, `【房間預約系統】${monthLabel} 個人租用對帳月結單`, emailContent);
  });

  return { totalUsersProcessed: Object.keys(userStats).length };
}


全流程營運與異動驗證對照表

營運階段 操作角色 觸發條件 / 動作 系統響應與驗證點
身分註冊 新職工 填寫 Email 申請 收到 6 位數 OTP 驗證碼
審批通知 系統 職工通過 OTP 核實 即時發送審批通知給管理員;職工狀態為 PENDING_APPROVAL 且無法登入
審核開通 管理員 後端點擊核准 狀態更新為 ACTIVE,發送開通 Email,職工方可登入
房間預約 職工 查詢 A1/A2/B1/B2/洽談室 空置時段預約直接成功 (APPROVED)
預約改期 職工 提前 $\ge 24$ 小時取消 收到取消成功通知,可於 60 天內 改訂新日期;不予計費
違規取消 職工 少於 24 小時取消 標記為 LATE_CANCELLED(預約不使用),照樣納入月結計費
離職/暫停 管理員 設定狀態為 SUSPENDED / REMOVED 系統即刻阻擋登入與預約權限
酌情扣除 管理員 後端勾選豁免 狀態轉為 CANCELLED_WAIVED,月結單上剔除不作計費
月底結算 管理員 執行月結統計 自動計算總計費時數,寄送個人 Email 月結對帳單


上一篇
【Day 16】Google OAuth / SSO 身分驗證、Google Cloud 設定與全功能角色權限控制(RBAC)
系列文
零預算 NGO 數位轉型挑戰:30 天打造智慧訂房系統 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言