iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
Build on Google AI

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

【Day 14】資料留痕與審計日誌:實作 Google Sheets 審計日誌 (Audit Log) 與場地使用率統計分析

  • 分享至 

  • xImage
  •  

昨天我們完成了支援桌面滑鼠與行動裝置觸控的 「橫向 7 天週月曆拖拉預約介面」,實現了雙模 UX 與前端防呆。

今天,我們將重點轉向後端的 「資料留痕、資安審計與數據統計分析」。對於機構與企業管理而言,單靠 Google Calendar 存放預約紀錄並不符合嚴謹的稽核規範。我們需要一套完整的 審計日誌(Audit Log)機制,將所有預約的新增、變更、取消與管理員特權操作進行永久備份與軌跡追蹤,並進一步計算 A1、A2、B1、B2 及洽談室 五大場地的真實使用率。


一、 Google Sheets 審計日誌表架構設計 (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 (改期原因: 延後會議) 操作結果與備註

二、 GAS 審計日誌寫入模組 (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、洽談室)的真實運作狀況。我們建立統計分析引擎,計算指定時間區間內的:

  1. 總預約次數與總使用時數

  2. 場地使用率(Utilization Rate):

    使用率(%) = 100% X 場地實際被預約總時數/開放總時數 (每日 14 小時 X 天數)

  3. 熱門時段分布分析(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
  };
}


四、 管理員數據看板與 API 整合 (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 });
  }
}


五、 統計報表 JSON 輸出範例

當管理員調用 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%" }
    }
  }
}


六、 總結

我們完成了系統營運安全的最後一塊拼圖:

  1. Google Sheets 審計日誌 (Audit_Logs): 將異動時間、操作者 Email、角色、異動前後時間與狀態 100% 留痕歸檔。
  2. 自動化日誌模組 (AuditLogService.gs): 整合 API 流程,失敗與成功的操作均有軌跡可循。
  3. 數據統計與使用率計算 (AnalyticsService.gs): 為管理員提供五大場地精準的使用率(%)與預約熱度分析。

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

尚未有邦友留言

立即登入留言