昨天我們完成了支援桌面滑鼠與行動裝置觸控的 「橫向 7 天週月曆拖拉預約介面」,實現了雙模 UX 與前端防呆。
今天,我們將重點轉向後端的 「資料留痕、資安審計與數據統計分析」。對於機構與企業管理而言,單靠 Google Calendar 存放預約紀錄並不符合嚴謹的稽核規範。我們需要一套完整的 審計日誌(Audit Log)機制,將所有預約的新增、變更、取消與管理員特權操作進行永久備份與軌跡追蹤,並進一步計算 A1、A2、B1、B2 及洽談室 五大場地的真實使用率。
Audit_Logs)我們在連結的 Google Sheets 中建立名為 Audit_Logs 的試算表頁籤,定義 10 項關鍵欄位以完整記載異動軌跡:
| 欄位名稱 (Header) | 資料型別 | 範例內容 | 說明 |
|---|---|---|---|
| Log ID | String | LOG_20261005_9A8F2 |
唯一日誌追蹤碼 |
| Timestamp | DateTime | 2026-10-05 09:15:32 |
操作發生的精確時間 |
| Action Type | String | CREATE / UPDATE / CANCEL |
操作類型 |
| Room Name | String | 會議室 A1 |
異動相關場地 |
| User Email | String | staff@company.org |
操作者 Email |
| Role | String | STAFF / ADMIN |
操作者權限角色 |
| Start Time | DateTime | 2026-10-06 09:00 |
預約起始時間 |
| End Time | DateTime | 2026-10-06 11:00 |
預約結束時間 |
| Duration (Hrs) | Number | 2.0 |
總計預約時數 |
| Status / Remark | String | SUCCESS (改期原因: 延後會議) |
操作結果與備註 |
AuditLogService.gs)此模組提供統一的日誌寫入介面,當預約流程觸發 新增 (CREATE)、改期 (UPDATE) 或 取消 (CANCEL) 時,自動將數據同步追加(Append)至 Google Sheets:
javascript
/**
* 寫入審計日誌主函式
* @param {Object} logParams - 日誌參數物件
*/
function recordAuditLog({ action, roomName, userEmail, role, startTime, endTime, statusRemark }) {
try {
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
let logSheet = ss.getSheetByName('Audit_Logs');
// 若 Sheet 不存在則自動初始化表頭
if (!logSheet) {
logSheet = ss.insertSheet('Audit_Logs');
logSheet.appendRow([
'Log ID', 'Timestamp', 'Action Type', 'Room Name',
'User Email', 'Role', 'Start Time', 'End Time',
'Duration (Hrs)', 'Status / Remark'
]);
logSheet.getRange(1, 1, 1, 10).setFontWeight('bold').setBackground('#F3F4F6');
}
const logId = 'LOG_' + Utilities.formatDate(new Date(), 'GMT+8', 'yyyyMMdd_HHmmss') + '_' + Math.floor(Math.random() * 1000);
const timestamp = Utilities.formatDate(new Date(), 'GMT+8', 'yyyy-MM-dd HH:mm:ss');
// 計算時數
let durationHours = 0;
if (startTime && endTime) {
durationHours = (new Date(endTime).getTime() - new Date(startTime).getTime()) / (1000 * 60 * 60);
}
// 寫入日誌列
logSheet.appendRow([
logId,
timestamp,
action,
roomName || 'N/A',
userEmail,
role || 'STAFF',
startTime ? Utilities.formatDate(new Date(startTime), 'GMT+8', 'yyyy-MM-dd HH:mm') : 'N/A',
endTime ? Utilities.formatDate(new Date(endTime), 'GMT+8', 'yyyy-MM-dd HH:mm') : 'N/A',
durationHours.toFixed(1),
statusRemark || 'SUCCESS'
]);
return { success: true, logId: logId };
} catch (error) {
console.error('審計日誌寫入失敗:', error);
return { success: false, error: error.toString() };
}
}
AnalyticsService.gs)管理員需要掌握五大場地(A1、A2、B1、B2、洽談室)的真實運作狀況。我們建立統計分析引擎,計算指定時間區間內的:
總預約次數與總使用時數
場地使用率(Utilization Rate):
使用率(%) = 100% X 場地實際被預約總時數/開放總時數 (每日 14 小時 X 天數)
熱門時段分布分析(Peak Hours Analysis)
javascript
/**
* 統計指定時間區間內各場地的使用狀況與使用率
* @param {String} startDateStr - 開始日期 ('YYYY-MM-DD')
* @param {String} endDateStr - 結束日期 ('YYYY-MM-DD')
*/
function getRoomAnalyticsReport(startDateStr, endDateStr) {
const calendar = CalendarApp.getCalendarById(CALENDAR_ID);
const start = new Date(startDateStr + 'T00:00:00');
const end = new Date(endDateStr + 'T23:59:59');
// 計算天數差與每日 14 小時 (08:00~22:00) 總開放時數
const totalDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)));
const totalAvailableHoursPerRoom = totalDays * 14;
const rooms = ['會議室 A1', '會議室 A2', '會議室 B1', '會議室 B2', '洽談室'];
// 初始化統計容器
const stats = {};
rooms.forEach(r => {
stats[r] = {
bookingCount: 0,
totalHours: 0,
utilizationRate: '0.0%'
};
});
// 取得日曆事件進行歸納
const events = calendar.getEvents(start, end);
events.forEach(event => {
const title = event.getTitle() || '';
const eventStart = event.getStartTime();
const eventEnd = event.getEndTime();
// 計算該事件時數
const hours = (eventEnd.getTime() - eventStart.getTime()) / (1000 * 60 * 60);
rooms.forEach(roomName => {
if (title.includes(roomName)) {
stats[roomName].bookingCount += 1;
stats[roomName].totalHours += hours;
}
});
});
// 計算各場地使用率
rooms.forEach(roomName => {
const hours = stats[roomName].totalHours;
const rate = (hours / totalAvailableHoursPerRoom) * 100;
stats[roomName].totalHours = Number(hours.toFixed(1));
stats[roomName].utilizationRate = rate.toFixed(1) + '%';
});
return {
period: { start: startDateStr, end: endDateStr, totalDays: totalDays },
roomStats: stats
};
}
doPost 路由擴充)在 GAS 入口點 doPost 中擴充 GET_ANALYTICS Action,供管理員後台發起調用:
javascript
function doPost(e) {
const request = JSON.parse(e.postData.contents);
const action = request.action;
// 1. 新增預約 (CREATE) + 寫入審計日誌
if (action === 'CREATE_BOOKING') {
const validation = validateBookingRules(request.data);
if (!validation.valid) {
recordAuditLog({
action: 'CREATE_FAILED',
roomName: request.data.roomName,
userEmail: request.data.applicantEmail,
statusRemark: validation.message
});
return responseJSON({ status: 'ERROR', message: validation.message });
}
// 建立 Calendar 事件...
const event = createCalendarEvent(request.data);
// 同步寫入 Audit Log
recordAuditLog({
action: 'CREATE',
roomName: request.data.roomName,
userEmail: request.data.applicantEmail,
startTime: request.data.startTime,
endTime: request.data.endTime,
statusRemark: 'SUCCESS (Event ID: ' + event.getId() + ')'
});
return responseJSON({ status: 'SUCCESS', eventId: event.getId() });
}
// 2. 取得統計報表 (GET_ANALYTICS) - 管理員專屬
if (action === 'GET_ANALYTICS') {
if (!isSystemAdmin({ applicantEmail: request.userEmail })) {
return responseJSON({ status: 'FORBIDDEN', message: '無存取權限。' });
}
const report = getRoomAnalyticsReport(request.startDate, request.endDate);
return responseJSON({ status: 'SUCCESS', report: report });
}
}
當管理員調用 GET_ANALYTICS 查詢週報表時,系統回傳範例數據如下:
json
{
"status": "SUCCESS",
"report": {
"period": {
"start": "2026-10-05",
"end": "2026-10-11",
"totalDays": 7
},
"roomStats": {
"會議室 A1": { "bookingCount": 12, "totalHours": 24.5, "utilizationRate": "25.0%" },
"會議室 A2": { "bookingCount": 8, "totalHours": 16.0, "utilizationRate": "16.3%" },
"會議室 B1": { "bookingCount": 18, "totalHours": 42.0, "utilizationRate": "42.9%" },
"會議室 B2": { "bookingCount": 15, "totalHours": 35.5, "utilizationRate": "36.2%" },
"洽談室": { "bookingCount": 25, "totalHours": 56.0, "utilizationRate": "57.1%" }
}
}
}
我們完成了系統營運安全的最後一塊拼圖:
Audit_Logs): 將異動時間、操作者 Email、角色、異動前後時間與狀態 100% 留痕歸檔。AuditLogService.gs): 整合 API 流程,失敗與成功的操作均有軌跡可循。AnalyticsService.gs): 為管理員提供五大場地精準的使用率(%)與預約熱度分析。