昨天我們完成了預約成功時的 Telegram Bot 與 Email 雙軌即時通知。今天我們將重點轉向系統的 「後台管理與預約維護模組」。
為了滿足實際營運需求,我們對「一般職工」與「系統管理員」的權限與業務邏輯進行了清晰的切割:
| 功能操作 | 一般職工 | 系統管理員 (Admin) | 後端核心邏輯與規則 |
|---|---|---|---|
| 預約場地 (Create) | ✅ (僅單次預約) | ✅ (單次 / 週期活動) | 管理員可使用 createEventSeries 批次鎖定固定時段 |
| 查詢紀錄 (Read) | ✅ (可查詢全機構所有預約) | ✅ (全機構所有預約與 Log) | 開放全區看板,協助職工即時掌握場地動態 |
| 修改/改期 (Update) | ⚠️ (僅限自己的預約) | ✅ (可強制修改任意預約) | 職工改期時,必須與新建立預約一樣通過 LockService 衝突檢核 |
| 取消/刪除 (Delete) | ⚠️ (僅限自己的預約) | ✅ (可強制刪除任意預約) | 刪除時自動同步移除 Google Calendar 事件並發送通知 |
一般職工在進行「改期」時,系統會比照全新預約的最高安全標準處理:
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() });
}
}
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() });
}
}
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);
}
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);
}
}
我們建構了權限分明且兼具靈活性的預約維護模組:
createEventSeries API,讓管理者輕鬆批次建立例行會議與保養時段。