iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
Build on Google AI

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

【Day 12】後台管理與預預約維護:實作「全區查詢、自主改期/取消」與管理員「週期性活動」排程

  • 分享至 

  • xImage
  •  

昨天我們完成了預約成功時的 Telegram Bot 與 Email 雙軌即時通知。今天我們將重點轉向系統的 「後台管理與預約維護模組」。

為了滿足實際營運需求,我們對「一般職工」與「系統管理員」的權限與業務邏輯進行了清晰的切割:

  • 一般職工: 可查詢全機構的所有預約(方便避開熱門時段與協調行程);對於「自己」的預約,允許進行取消與改期,但「改期」必須嚴格通過時間防呆與防重複預約(Overbooking)檢查。
  • 系統管理員 (Admin): 具備全域 CRUD 權限(可強制修訂或刪除任何預約),並可設定「週期性活動/例行維護」,批次寫入 Google Calendar 鎖定時段。

一、 角色權限矩陣 (Role-Based Access Control)

功能操作 一般職工 系統管理員 (Admin) 後端核心邏輯與規則
預約場地 (Create) ✅ (僅單次預約) ✅ (單次 / 週期活動) 管理員可使用 createEventSeries 批次鎖定固定時段
查詢紀錄 (Read) ✅ (可查詢全機構所有預約) ✅ (全機構所有預約與 Log) 開放全區看板,協助職工即時掌握場地動態
修改/改期 (Update) ⚠️ (僅限自己的預約) ✅ (可強制修改任意預約) 職工改期時,必須與新建立預約一樣通過 LockService 衝突檢核
取消/刪除 (Delete) ⚠️ (僅限自己的預約) ✅ (可強制刪除任意預約) 刪除時自動同步移除 Google Calendar 事件並發送通知

二、 一般職工「改期與取消」防呆機制

一般職工在進行「改期」時,系統會比照全新預約的最高安全標準處理:

  1. 身份與權限驗證: 比對申請者 Email,確保一般職工僅能異動「自己」的預約紀錄。
  2. 互斥鎖與衝突檢查 (LockService): 當進行改期時,系統先取得併發鎖,並檢查目標新時段是否已存在其他預約(自動排除當前修改的事件本體),徹底避免改期導致的重複預約。
  3. Google 行事曆雙向同步: 改期或取消成功後,Google Calendar 上的事件時間會同步更新或直接刪除。

三、 GAS 後端服務實作 (ManageService.gs)

請在 Google Apps Script 專案中建立或更新 ManageService.gs:

// ===== 系統設定與驗證 =====
const ADMIN_SECRET_KEY = 'YOUR_SUPER_ADMIN_KEY_1234';

/**
 * 驗證是否為管理員
 */
function isSystemAdmin(data) {
  return data.adminKey === ADMIN_SECRET_KEY;
}

/**
 * 1. 【查詢 - Read】全機構未來預約看板 (一般職工與管理員皆可調用)
 */
function getAllFutureBookings() {
  try {
    const calendar = CalendarApp.getCalendarById(CALENDAR_ID);
    const now = new Date();
    const futureLimit = new Date(now.getTime() + 60 * 24 * 60 * 60 * 1000); // 查詢未來 60 天
    const events = calendar.getEvents(now, futureLimit);

    const bookings = events.map(event => ({
      eventId: event.getId(),
      roomName: event.getLocation() || event.getTitle(),
      title: event.getTitle(),
      startTime: Utilities.formatDate(event.getStartTime(), 'GMT+8', "yyyy-MM-dd HH:mm"),
      endTime: Utilities.formatDate(event.getEndTime(), 'GMT+8', "yyyy-MM-dd HH:mm"),
      description: event.getDescription() || ''
    }));

    return createJsonResponse({ status: 'SUCCESS', bookings: bookings });
  } catch (error) {
    return createJsonResponse({ status: 'ERROR', message: '查詢預約失敗:' + error.toString() });
  }
}

/**
 * 2. 【改期 - Update】職工變更自身預約時段 (含 LockService 防衝突檢查)
 */
function handleRescheduleBooking(data) {
  const lock = LockService.getScriptLock();
  if (!lock.waitLock(10000)) { // 等待最多 10 秒
    return createJsonResponse({ status: 'ERROR', message: '系統繁忙中,請稍後再試。' });
  }

  try {
    const calendar = CalendarApp.getCalendarById(CALENDAR_ID);
    const event = calendar.getEventById(data.eventId);

    if (!event) {
      lock.releaseLock();
      return createJsonResponse({ status: 'ERROR', message: '找不到欲修改的預約紀錄。' });
    }

    // 權限驗證:非管理員僅能修改自己的預約
    const description = event.getDescription() || '';
    if (!description.includes(`Email:${data.applicantEmail}`) && !isSystemAdmin(data)) {
      lock.releaseLock();
      return createJsonResponse({ status: 'FORBIDDEN', message: '無權限修改此筆預約!' });
    }

    const newStart = new Date(data.newStartTime);
    const newEnd = new Date(data.newEndTime);

    // 衝突檢查:確認新選擇的時段是否已被佔用 (排除自身事件)
    const existingEvents = calendar.getEvents(newStart, newEnd);
    const hasConflict = existingEvents.some(evt => 
      evt.getId() !== data.eventId && 
      (evt.getLocation() === data.roomName || evt.getTitle().includes(`[${data.roomName}]`))
    );

    if (hasConflict) {
      lock.releaseLock();
      return createJsonResponse({ status: 'ERROR', message: '改期失敗:新選擇的時間區段已被其他人預約。' });
    }

    // 同步更新 Google Calendar 事件時間
    event.setTime(newStart, newEnd);
    lock.releaseLock();

    return createJsonResponse({
      status: 'SUCCESS',
      message: '預約已成功改期!新時段已同步更新至 Google 日曆。'
    });

  } catch (error) {
    lock.releaseLock();
    return createJsonResponse({ status: 'ERROR', message: '改期程序發生錯誤:' + error.toString() });
  }
}

/**
 * 3. 【取消/刪除 - Delete】一鍵取消預約並刪除 Google 日曆事件
 */
function handleCancelBooking(data) {
  try {
    const calendar = CalendarApp.getCalendarById(CALENDAR_ID);
    const event = calendar.getEventById(data.eventId);

    if (!event) {
      return createJsonResponse({ status: 'ERROR', message: '找不到欲取消的預約紀錄。' });
    }

    // 權限驗證:僅能取消自己的預約 (管理員除外)
    const description = event.getDescription() || '';
    if (!description.includes(`Email:${data.applicantEmail}`) && !isSystemAdmin(data)) {
      return createJsonResponse({ status: 'FORBIDDEN', message: '無權限取消此筆預約!' });
    }

    // 從 Google Calendar 徹底刪除該事件
    event.deleteEvent();

    return createJsonResponse({ status: 'SUCCESS', message: '預約已成功取消,並已從 Google 日曆中同步移除。' });
  } catch (error) {
    return createJsonResponse({ status: 'ERROR', message: '取消預約失敗:' + error.toString() });
  }
}

/**
 * 4. 【管理員專屬 - Create 週期活動】預設每週/例行性排他活動 (Recurring Events)
 */
function handleCreateRecurringEventAdmin(data) {
  if (!isSystemAdmin(data)) {
    return createJsonResponse({ status: 'FORBIDDEN', message: '無管理員權限!' });
  }

  try {
    const calendar = CalendarApp.getCalendarById(CALENDAR_ID);
    const start = new Date(data.startTime);
    const end = new Date(data.endTime);
    const title = `【例行保養/固定會議】${data.roomName}`;

    // 建立 Google Calendar 週期重複規則 (每週重複)
    let recurrence = CalendarApp.newRecurrence().addWeeklyRule();
    if (data.repeatWeeks) {
      recurrence = recurrence.times(data.repeatWeeks);
    }

    // 建立系列事件 (EventSeries) 進行批次排他佔用
    const eventSeries = calendar.createEventSeries(title, start, end, recurrence, {
      location: data.roomName,
      description: `管理員預設週期活動:${data.purpose || '固定佔用'}`
    });

    return createJsonResponse({
      status: 'SUCCESS',
      seriesId: eventSeries.getId(),
      message: `已成功預設未來 ${data.repeatWeeks || 12} 週的例行週期活動!`
    });
  } catch (error) {
    return createJsonResponse({ status: 'ERROR', message: '建立週期活動失敗:' + error.toString() });
  }
}


四、 主程式 API 分流路由 (Code.gs)

修改 Code.gs 中的 doPost 入口,統一接收與調用對應處理函式:

function doPost(e) {
  try {
    const data = JSON.parse(e.postData.contents);

    switch (data.action) {
      // --- 預約與查詢 ---
      case 'CREATE_BOOKING':
        return handleCreateBookingWithLock(data);
      
      case 'GET_ALL_BOOKINGS':
        return getAllFutureBookings(); // 全區看板查詢

      case 'RESCHEDULE_BOOKING':
        return handleRescheduleBooking(data); // 職工自主改期 (含驗證與 Lock 檢查)

      case 'CANCEL_BOOKING':
        return handleCancelBooking(data); // 職工取消 / 管理員刪除預約

      // --- 👑 管理員專屬功能 ---
      case 'ADMIN_CREATE_RECURRING':
        return handleCreateRecurringEventAdmin(data); // 預設週期活動

      default:
        return createJsonResponse({ status: 'ERROR', message: '無效的操作指令 (Invalid Action)' });
    }
  } catch (err) {
    return createJsonResponse({ status: 'ERROR', message: err.toString() });
  }
}


五、 前端操作範例 (React / JavaScript API 調用)

1. 一般職工發起「改期」範例

async function rescheduleMyBooking(eventId, newStart, newEnd, roomName, userEmail) {
  const payload = {
    action: 'RESCHEDULE_BOOKING',
    eventId: eventId,
    applicantEmail: userEmail,
    roomName: roomName,
    newStartTime: newStart, // 例如 '2026-10-05T14:00:00'
    newEndTime: newEnd
  };

  const response = await fetch(GAS_API_URL, {
    method: 'POST',
    body: JSON.stringify(payload)
  });
  
  const result = await response.json();
  alert(result.message);
}

2. 管理員預設「每週固定會議/保養」範例

async function setWeeklyMaintenance() {
  const payload = {
    action: 'ADMIN_CREATE_RECURRING',
    adminKey: 'YOUR_SUPER_ADMIN_KEY_1234',
    roomName: '會議室 A',
    startTime: '2026-10-06T09:00:00', // 第一個發生的時間點
    endTime: '2026-10-06T11:00:00',
    repeatWeeks: 12,                  // 自動向後重複 12 週
    purpose: '每週二設備例行維護'
  };

  const response = await fetch(GAS_API_URL, {
    method: 'POST',
    body: JSON.stringify(payload)
  });
  
  const result = await response.json();
  if (result.status === 'SUCCESS') {
    console.log('週期活動建置完成:', result.seriesId);
  }
}


六、 結語

我們建構了權限分明且兼具靈活性的預約維護模組:

  1. 全區透明看板: 所有職工皆可查詢機構所有場地的動態,大幅降低跨部門協調成本。
  2. 安全改期與一鍵取消: 職工可維護自身的預約,改期時強制通過互斥鎖檢查,確保排程絕不出錯。
  3. 管理員週期鎖定: 提供 createEventSeries API,讓管理者輕鬆批次建立例行會議與保養時段。

上一篇
【Day 11】即時通知整合:利用 Telegram Bot 與 Email 實現預約雙軌自動化推播
下一篇
【Day 13】全平台響應式 UI/UX:橫向 7 天週月曆、桌面/行動雙模拖拉(Mouse & Touch Drag-to-Select)與五房間切換
系列文
零預算 NGO 數位轉型挑戰:30 天打造智慧訂房系統 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言