---
# System prepended metadata

title: 尾牙簽到及抽獎系統
tags: [GSS]

---

﻿# 尾牙簽到及抽獎系統

> 版本：2.4.0  
> 更新日期：2026-01-16  
> 對應檔案：`Code.gs`, `Index.html`, `SignIn.html`

## 目錄

[整體架構](#整體架構)  
[簽到資料流程圖](#簽到資料流程圖)  
[抽獎資料流程圖](#抽獎資料流程圖)  
[獎項來源與規則系統](#獎項來源與規則系統)  
[動態佇列機制](#動態佇列機制)  
[Property 機制說明](#Property-機制說明)  
[Cache 機制說明](#Cache-機制說明)  
[Lock 與併發控制](#Lock-與併發控制)  
[部署與設定](#部署與設定)  
[維運指令](#維運指令)

---

## 技術堆疊

### 前端

| 技術 | 用途 | 說明 |
|------|------|------|
| **Vue 3** (CDN) | 前端框架 | Composition API 響應式設計 |
| **Tailwind CSS** (CDN) | 樣式框架 | Utility-first CSS |
| **google.script.run** | 前後端通訊 | GAS 原生 RPC 機制 |

### 後端

| 技術 | 用途 | 說明 |
|------|------|------|
| **Google Apps Script (GAS)** | 後端服務 | 提供 Web App 與 API 端點 |
| **Google Sheets API** | 資料持久化 | 主資料表及 9 個 Mirror 分流表 |
| **ScriptProperties** | 狀態管理 | 儲存動態佇列、系統狀態等 |
| **CacheService** | 效能優化 | 多層快取減少試算表 I/O |
| **LockService** | 併發控制 | 確保原子性操作 |
| **Time-driven Triggers** | 排程任務 | 每分鐘刷新佇列與同步資料 |

### 資料儲存層級

```mermaid
graph TB
    subgraph "Layer 1: CacheService"
        C1[記憶體快取]
        C1 --> |TTL| C1a[30秒 ~ 6小時]
        C1 --> |限制| C1b[單鍵 100KB]
        C1 --> |用途| C1c[熱資料快取]
    end
    
    subgraph "Layer 2: ScriptProperties"
        P1[持久化 Key-Value]
        P1 --> |限制| P1a[單鍵 9KB / 總量 500KB]
        P1 --> |用途| P1b[動態佇列 / 系統狀態]
    end
    
    subgraph "Layer 3: Google Sheets"
        S1[結構化儲存]
        S1 --> |Master| S1a[核心資料]
        S1 --> |Mirror| S1b[分流顯示 x9]
    end
    
    C1 --> P1
    P1 --> S1
```

---

## 整體架構

```mermaid
graph TB
    subgraph "前端 Browser"
        SignIn[SignIn.html<br/>簽到頁面]
        Admin[Index.html<br/>管理控制台]
    end

    subgraph "後端 Google Apps Script"
        WebApp[doGet<br/>Web App 進入點]
        
        subgraph "API Layer"
            SignInAPI[signIn]
            LotteryAPI[runLottery]
            VoidAPI[voidAndRefresh]
            StatsAPI[getSignInStats]
            ConfigAPI[forceReloadConfig]
            ToggleAPI[toggleRegistration]
        end
        
        subgraph "Service Layer"
            ValidateService[validateEmployee]
            EnqueueService[enqueueSignIn]
            FlushQueue[flushQueueToSheet]
            SyncService[syncToMirrorsProcess]
            SourceService[getCandidatesBySource]
            RulesService[applySourceRules]
        end
        
        subgraph "Data Access Layer"
            CacheLayer[CacheService]
            PropsLayer[ScriptProperties]
            SheetLayer[SpreadsheetApp]
        end
    end

    subgraph "資料儲存"
        MasterSheet[(Master Sheet)]
        MirrorSheets[(Mirror Sheets x9)]
    end

    subgraph "排程任務"
        Trigger1[scheduledFlushQueue<br/>每 5 分鐘]
        Trigger2[triggerSyncJob<br/>每分鐘]
    end

    SignIn -->|google.script.run| SignInAPI
    Admin -->|google.script.run| LotteryAPI
    Admin -->|google.script.run| VoidAPI
    
    WebApp --> SignIn
    WebApp --> Admin
    
    SignInAPI --> ValidateService
    SignInAPI --> EnqueueService
    LotteryAPI --> FlushQueue
    LotteryAPI --> SourceService
    SourceService --> RulesService
    
    ValidateService --> CacheLayer
    EnqueueService --> PropsLayer
    FlushQueue --> SheetLayer
    
    CacheLayer --> PropsLayer
    PropsLayer --> SheetLayer
    SheetLayer --> MasterSheet
    
    SyncService --> MirrorSheets
    Trigger1 --> FlushQueue
    Trigger2 --> SyncService
```

### 工作表結構

| 工作表名稱 | 用途 | 欄位 |
|-----------|------|------|
| `ValidEmployees` | 合格員工名單 | BG, BU, Dept, EmpID, ChName, EnName, ..., Location |
| `Employees` | 已簽到員工 | 員工編號, 姓名, 部門, 簽到時間, 工作地點 |
| `Prize` | 獎項設定 | PrizeID, PrizeName, Count, SoundURL, Source |
| `Winners` | 中獎紀錄 | 時間戳記, 獎項名稱, 員工編號, 姓名, 部門, Status, Audit |

---

## 簽到資料流程圖

```mermaid
sequenceDiagram
    autonumber
    participant U as 使用者
    participant F as 前端 Vue
    participant G as GAS API
    participant C as CacheService
    participant P as ScriptProperties
    participant S as Google Sheets

    U->>F: 輸入員編 + 姓名
    F->>F: 表單驗證 + 節流控制 2s
    
    Note over F: 指數退避重試機制<br/>最多 3 次，基礎延遲 1.5s

    F->>G: signIn(empId, empName)
    
    G->>C: 檢查 SIGNED_{empId} 快取
    alt 快取命中 已簽到
        C-->>G: 已簽到標記
        G-->>F: success false, alreadySigned true
        F-->>U: 顯示「已簽到」
    else 快取未命中
        G->>P: 檢查 IS_REGISTRATION_CLOSED
        alt 已截止
            P-->>G: true
            G-->>F: success false, registrationClosed true
            F-->>U: 顯示「簽到已截止」
        else 開放中
            G->>C: 讀取 VALID_EMP_MAP 分片
            alt 快取命中
                C-->>G: employeeMap
            else 快取未命中
                G->>S: 讀取 ValidEmployees
                S-->>G: 員工資料
                G->>C: 分片存入快取 TTL 6hr
            end
            
            G->>G: validateEmployee 比對姓名
            
            alt 驗證失敗
                G-->>F: success false, error
            else 驗證成功
                G->>G: tryLock 3s
                G->>C: 雙重檢查 SIGNED_{empId}
                alt 已在快取中
                    G-->>F: success false, alreadySigned true
                else 尚未簽到
                    G->>P: enqueueSignIn 寫入動態佇列
                    G->>C: 設定 SIGNED_{empId} = 1 TTL 6hr
                    G-->>F: success true
                    F->>F: 播放煙火動畫
                    F-->>U: 顯示「簽到成功」
                end
            end
        end
    end

    Note over P,S: 背景排程 每 5 分鐘
    P->>G: scheduledFlushQueue
    G->>P: readAllQueueEntries
    G->>C: 讀取員工 Map Hydration
    G->>S: 批次寫入 Employees 表
    G->>P: cleanupProcessedQueueShards
```

### 簽到流程關鍵設計

**雙重檢查機制 (Double-checked Locking)**

1. **Lock 前檢查**：快取 `SIGNED_{empId}` 快速過濾
2. **Lock 後檢查**：確保等待 Lock 期間未被其他請求簽到

**最小化佇列格式**

```javascript
// 佇列項目只存最小資訊 (i: ID, t: Timestamp)
{ i: "0010", t: "2026-01-07T12:00:00.000Z" }
```

**指數退避公式**

```javascript
delay = min(baseDelay × 1.5^(attempt-1) + random(0, jitterMax), maxDelay)
```

---

## 抽獎資料流程圖

```mermaid
sequenceDiagram
    autonumber
    participant A as 管理員
    participant F as 前端 Vue
    participant G as GAS API
    participant C as CacheService
    participant P as ScriptProperties
    participant S as Google Sheets
    participant M as Mirror Sheets

    A->>F: 選擇獎項 + 設定人數
    F->>F: 驗證剩餘名額
    A->>F: 點擊「開始抽獎」
    
    F->>G: runLottery(prizeName, count)
    
    G->>G: acquireLockWithBackoff LOTTERY config
    
    G->>P: flushQueueToSheet 確保資料同步
    
    G->>C: getCachedPrizes 讀取獎項
    G->>G: getSourceConfig 取得來源設定
    
    G->>C: getPrizeWinnerCounts
    G->>G: 計算剩餘名額
    
    alt 名額不足
        G-->>F: Error 獎項已抽完
    else 名額充足
        G->>C: getCandidatesBySource 載入候選人
        G->>C: getWinnerIdsSet 讀取已中獎
        G->>P: getLotteryCutoffTime
        
        G->>G: applySourceRules 套用規則過濾
        
        alt 候選人不足
            G-->>F: Error 合格候選人數不足
        else 候選人充足
            G->>G: shuffleArray Fisher-Yates
            G->>G: 取前 N 位
            
            G->>S: 批次寫入 Winners 表
            
            G->>C: updateWinnerIdsCache
            G->>C: 更新 PRIZE_WINNER_COUNTS
            G->>P: 持久化 WINNER_IDS
            
            G->>P: markSyncRequired
            
            G-->>F: 回傳新中獎者列表
            F->>F: 播放音效 + 更新 UI
            F-->>A: 顯示中獎名單
            
            Note over G,M: 背景同步
            G->>M: syncToMirrorsProcess
        end
    end
```

### 抽獎流程關鍵設計

**資格截止時間**

- 管理員執行 `toggleRegistration(true)` 時自動記錄 `LOTTERY_CUTOFF_TIME`
- 抽獎時套用 `excludeAfterCutoff` 規則排除截止後簽到者

**Fisher-Yates 洗牌**

```javascript
function shuffleArray(array) {
    for (let i = array.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [array[i], array[j]] = [array[j], array[i]];
    }
}
```

**Mirror 同步策略**

- 批次同步（每批 3 個試算表）
- 節流控制（10 秒內不重複同步）
- 使用 Sheets API 提升效能

---

## 獎項來源與規則系統

### 設計目的

支援不同獎項使用不同的候選人來源與篩選規則，實現靈活的抽獎邏輯。

### PRIZE_SOURCES 設定結構

```javascript
PRIZE_SOURCES: {
    Employees: {
        sheetName: 'Employees',      // 資料來源工作表
        columns: {
            id: 0,                   // 員工編號欄位索引
            name: 1,                 // 姓名欄位索引
            dept: 2,                 // 部門欄位索引
            signInTime: 3            // 簽到時間欄位索引
        },
        rules: ['excludeWinners', 'excludeAfterCutoff'],  // 套用規則
        displayName: '員工抽獎',     // 前端顯示名稱
        cacheKey: 'EMPLOYEE_DATA',   // 快取鍵名稱
        cacheTTL: 1800               // 快取時間（秒）
    }
}
```

### LOTTERY_RULES 規則定義

```javascript
LOTTERY_RULES: {
    excludeWinners: {
        name: '排除已中獎',
        description: '已中獎的人不可再次中獎',
        filter: function(candidate, context) {
            return !context.winnerIds.has(String(candidate.id));
        }
    },
    excludeAfterCutoff: {
        name: '排除遲到簽到',
        description: '簽到截止後才簽到的人不具抽獎資格',
        filter: function(candidate, context) {
            if (!context.cutoffTime) return true;
            if (!candidate.signInTime) return true;
            const signInTimestamp = new Date(candidate.signInTime).getTime();
            return signInTimestamp <= context.cutoffTime;
        }
    }
}
```

### 規則套用流程

```mermaid
graph TB
    subgraph "來源資料載入"
        S[getCandidatesBySource]
        S --> D[從 Sheet 讀取原始資料]
        D --> N[標準化候選人格式]
    end
    
    subgraph "規則套用"
        N --> R[applySourceRules]
        R --> R1{excludeWinners}
        R1 --> |移除已中獎| R2{excludeAfterCutoff}
        R2 --> |移除遲到者| F[過濾後候選人]
    end
    
    subgraph "上下文準備"
        C[context 物件]
        C --> C1[winnerIds: Set]
        C --> C2[cutoffTime: timestamp]
        C --> C3[prizeName: string]
    end
    
    C --> R
```

### 擴展新來源範例

```javascript
// 在 CONFIG.PRIZE_SOURCES 新增
VIPGuests: {
    sheetName: 'VIPGuests',
    columns: { id: 0, name: 1, dept: 2 },
    rules: ['excludeWinners'],  // VIP 不套用遲到規則
    displayName: 'VIP 貴賓抽獎',
    cacheTTL: 600
}
```

---

## 動態佇列機制

### 設計目的

解決多人同時簽到時的高併發寫入問題，避免 Google Sheets API 限制導致失敗。

### 佇列寫入與刷新機制

採用「**非同步寫入**」策略，將高併發的簽到請求暫存於 `ScriptProperties` 的動態分片佇列中，再透過排程或特定觸發事件，批次將資料刷新至 Google Sheets。

```mermaid
sequenceDiagram
    autonumber
    participant T as Trigger/Caller
    participant L as LockService
    participant P as ScriptProperties
    participant C as CacheService
    participant S as Google Sheets

    Note over T: 觸發刷新 (定時/強制)

    T->>L: 嘗試獲取 Lock
    alt Lock 获取失敗
        L-->>T: 放棄/等待
    else Lock 获取成功
        T->>P: readAllQueueEntries (讀取 Q_TAIL 到 Q_HEAD)
        P-->>T: 回傳 Raw Entries [{i, t}...]
        
        Note right of T: Critical Section 結束 (僅限 flushQueue)

        loop 資料處理 (Hydration)
            T->>C: 讀取員工資料 Map (getValidEmployeeMap)
            Note over T: 將 ID (i) 轉換為<br/>姓名/部門/地點
        end

        T->>S: 讀取現有 ID (防止重複)
        
        loop 去重與過濾
            Note over T: 1. 檢查 Sheet 是否已有 ID<br/>2. 檢查本批次是否重複
        end

        alt 有新資料
            T->>S: 批次寫入 Range.setValues()
            Note over S: 寫入 Employees 工作表
            T->>C: 清除 EMPLOYEE_DATA 快取
        end

        T->>P: cleanupProcessedQueueShards (推進 Tail 指標)
        T->>P: 更新 LAST_FLUSH_TIME
        
        L->>T: 釋放 Lock
    end
```

接著，簡單介紹使用的兩種刷新模式，分別對應「背景自動處理」與「關鍵操作強制同步」。

| 功能 | `flushQueue()` | `flushQueueToSheet()` |
| --- | --- | --- |
| **主要用途** | **定時觸發器** | **抽獎/統計/管理操作** |
| **Lock 策略** | **兩段式處理**：<br>1. 快速鎖定 (Critical Section) 讀取並移動指標。<br>2. 釋放鎖後再執行緩慢的 Sheet I/O。 | **全程鎖定 (Atomic)**：<br>從讀取佇列到寫入 Sheet 全程持有鎖，確保操作原子性。 |
| **優點** | **高吞吐量**：大幅減少 Lock 佔用時間，避免阻塞前端簽到請求。 | **高一致性**：確保函式返回時，資料絕對已寫入 Sheet。 |
| **資料安全** | 寫入失敗時會觸發 `BACKUP_QUEUE_FAIL` 備份機制。 | 依賴呼叫方的 Try-Catch 處理。 |

### 動態分片架構

```mermaid
graph TB
    subgraph "簽到請求"
        R1[Request 1]
        R2[Request 2]
        RN[Request N]
    end
    
    subgraph "ScriptProperties 動態佇列"
        IDX[Q_HEAD_IDX / Q_TAIL_IDX]
        Q0[Q_SHARD_0<br/>JSON Array]
        Q1[Q_SHARD_1<br/>JSON Array]
        QN[Q_SHARD_N<br/>...]
    end
    
    subgraph "定時刷新"
        T[scheduledFlushQueue<br/>每 5 分鐘]
    end
    
    subgraph "Google Sheets"
        S[(Employees)]
    end
    
    R1 -->|enqueueSignIn| Q0
    R2 -->|enqueueSignIn| Q0
    RN -->|超過 8.5KB 自動切換| Q1
    
    T -->|flushQueue| Q0
    T -->|flushQueue| Q1
    Q0 -->|Batch Write| S
    Q1 -->|Batch Write| S
```

### 佇列指標管理

| Property 鍵 | 說明 |
|------------|------|
| `Q_HEAD_IDX` | 當前寫入的佇列分片索引 |
| `Q_TAIL_IDX` | 當前讀取/刷新的佇列分片索引 |
| `Q_SHARD_{N}` | 第 N 個佇列分片資料 |

### 分片切換邏輯

```javascript
function enqueueSignIn(empId, empData, props) {
    let headIdx = parseInt(props.getProperty('Q_HEAD_IDX') || '0', 10);
    let queueKey = 'Q_SHARD_' + headIdx;
    let queueJson = props.getProperty(queueKey) || '[]';

    // 超過 8.5KB 限制時切換到下一個分片
    if (queueJson.length > 8500) {
        headIdx++;
        queueKey = 'Q_SHARD_' + headIdx;
        queueJson = '[]';
        props.setProperty('Q_HEAD_IDX', String(headIdx));
    }
    
    // 寫入最小化格式
    const entry = {
        i: empId,
        t: new Date().toISOString()
    };
    
    const queue = JSON.parse(queueJson);
    queue.push(entry);
    props.setProperty(queueKey, JSON.stringify(queue));
}
```

### Hydration 資料還原

```mermaid
graph LR
    Q[佇列項目<br/>i, t] --> H{Hydration}
    M[員工 Map<br/>empId → 完整資料] --> H
    H --> R[完整紀錄<br/>寫入試算表]
```

刷新時從 `ValidEmployees` 快取的 Map 還原完整員工資料，確保資料完整性。

---

## Property 機制說明

### 設計目的

利用 `ScriptProperties` 作為中間層：

1. 單鍵 9KB 限制 → 動態分片儲存
2. 跨請求狀態共享 → 持久化
3. 減少試算表讀取 → 快速查詢

### Property 鍵值一覽

| 鍵名 | 用途 | 格式 |
|------|------|------|
| `Q_HEAD_IDX` | 佇列寫入分片索引 | Number String |
| `Q_TAIL_IDX` | 佇列讀取分片索引 | Number String |
| `Q_SHARD_{N}` | 佇列分片資料 | JSON Array |
| `WINNER_IDS` | 已中獎 ID 列表 | JSON Array |
| `IS_REGISTRATION_CLOSED` | 簽到狀態 | "true" / "false" |
| `LOTTERY_CUTOFF_TIME` | 抽獎資格截止時間 | Timestamp String |
| `SYNC_REQUIRED` | 同步標記 | "true" / "false" |
| `LAST_SYNC_TIME` | 上次同步時間 | Timestamp String |
| `LAST_FLUSH_TIME` | 上次佇列刷新時間 | Timestamp String |
| `CACHE_REBUILD_LOCK` | 快取重建鎖 | Timestamp String |
| `ADMIN_TOKEN` | 管理員驗證 Token | String |

### 空間估算

- 單一佇列項目：約 80-100 bytes
- 單片 8.5KB 可容納：約 85-100 個簽到
- 10 片總量：約 85KB（在 500KB 總限制內）

---

## Cache 機制說明

### 設計目的

1. **減少試算表 I/O**：多人同時查詢會超出 API 限制
2. **加速驗證**：員工 Map 快取命中率極高
3. **防止雪崩**：分片儲存 + 重建鎖

### 快取鍵值一覽

| 鍵名 | TTL | 用途 |
|------|-----|------|
| `VALID_EMP_MAP` | 6 hr | 合格員工 Map 主快取 |
| `VALID_EMP_MAP_{N}` | 6 hr | 員工 Map 分片（每片 300 員工） |
| `VALID_EMP_MAP_COUNT` | 6 hr | 員工 Map 分片數量 |
| `VALID_EMPLOYEE_COUNT` | 5 min | 合格員工總數 |
| `EMPLOYEE_DATA` | 30 min | 已簽到員工資料 |
| `PRIZE_DATA` | 5 min | 獎項設定 |
| `PRIZE_WINNER_COUNTS_CACHE` | 30 sec | 各獎項中獎人數 |
| `WINNER_IDS_CACHE` | 1 min | 已中獎者 ID |
| `SIGNED_{empId}` | 6 hr | 單一員工簽到標記 |
| `HTML_SIGNIN_CACHE` | 10 min | 編譯後簽到頁面 HTML |
| `HTML_ADMIN_CACHE` | 10 min | 編譯後管理頁面 HTML |

### 快取層級讀取流程

```mermaid
graph TB
    R[讀取請求] --> C1{主快取<br/>命中?}
    C1 -->|是| Return1[返回快取]
    C1 -->|否| C2{分片快取<br/>完整?}
    C2 -->|是| Combine[組合分片]
    Combine --> Return2[返回]
    C2 -->|否| Lock{重建鎖<br/>存在?}
    Lock -->|是| Wait[等待 500ms]
    Wait --> C1
    Lock -->|否| Rebuild[從 Sheet 重建]
    Rebuild --> Cache[寫入快取]
    Cache --> Return3[返回]
```

### 員工 Map 分片快取

當員工數量較多時，自動分片儲存（每片 300 員工）：

```mermaid
graph LR
    M[1000 員工 Map] --> Split[分片]
    Split --> C1[VALID_EMP_MAP_1<br/>員工 1-300]
    Split --> C2[VALID_EMP_MAP_2<br/>員工 301-600]
    Split --> CC[VALID_EMP_MAP_COUNT = 2]
```

---

## Lock 與併發控制

### Lock 配置

```javascript
LOCK_CONFIG: {
    // 簽到用 Lock（快速失敗）
    SIGNIN_WAIT_MS: 3000,
    
    // 抽獎用 Lock（指數退避）
    LOTTERY: {
        MAX_RETRIES: 3,
        BASE_TIMEOUT_MS: 5000,
        TIMEOUT_INCREMENT_MS: 2000,
        BASE_RETRY_DELAY_MS: 200,
        MAX_RETRY_DELAY_MS: 1000
    },
    
    // 一般操作用 Lock
    DEFAULT: {
        MAX_RETRIES: 4,
        BASE_TIMEOUT_MS: 2000,
        TIMEOUT_INCREMENT_MS: 1000,
        BASE_RETRY_DELAY_MS: 100,
        MAX_RETRY_DELAY_MS: 1000
    }
}
```

### 指數退避策略

```mermaid
graph TB
    subgraph "抽獎 Lock 策略"
        A1[嘗試 1: 5000ms]
        A2[嘗試 2: 7000ms]
        A3[嘗試 3: 9000ms]
        
        A1 -->|失敗| D1[延遲 200-400ms]
        D1 --> A2
        A2 -->|失敗| D2[延遲 400-800ms]
        D2 --> A3
    end
```

### 關鍵 Lock 點

| 操作 | Lock 類型 | 策略 | 說明 |
|------|----------|------|------|
| 簽到 | Script Lock | 3s 單次嘗試 | 快速失敗避免堆積 |
| 抽獎 | Script Lock | 指數退避 3 次 | 確保抽獎完成 |
| 作廢 | Script Lock | 指數退避 4 次 | 保護狀態更新 |
| 刷新佇列 | Script Lock | 20s 單次 | 排程專用 |
| 同步 Mirror | Script Lock | 5s 單次 | 快速嘗試 |

---

## 部署與設定

### 前置需求

1. 啟用 Google Sheets API 服務
2. 準備 Master Sheet 及 9 個 Mirror Sheet
3. 準備員工名單來源表

### 部署步驟

```bash
# 1. 執行環境設定
setupEnvironment()

# 2. 安裝定時觸發器
installTimeTrigger()

# 3. 部署 Web App
# - 執行身分：我
# - 存取權限：所有人

# 4. 取得管理員連結
getAdminToken()
```

### URL 格式

```
# 簽到頁面
https://script.google.com/macros/s/{DEPLOY_ID}/exec

# 管理控制台
https://script.google.com/macros/s/{DEPLOY_ID}/exec?page=admin&token={ADMIN_TOKEN}
```

### GAS 配額限制

| 項目 | 免費額度 | 本系統使用 |
|------|---------|-----------|
| Properties 單鍵 | 9 KB | 動態分片 8.5KB |
| Properties 總量 | 500 KB | 約 50 KB |
| Cache 單鍵 | 100 KB | 分片處理 |
| Web App 執行時間 | 30 秒 | 約 5 秒 |
| 觸發器執行時間 | 90 分鐘/天 | 約 10 分鐘/天 |

---

## 維運指令

```javascript
// 健康檢查
healthCheck()

// 強制刷新佇列
forceFlushQueue()

// 重建所有快取
rebuildAllCaches()

// 從試算表重建已中獎 ID
rebuildWinnerIdsFromSheet()

// 清除所有快取
clearAllCaches()

// 清除所有簽到資料（危險）
clearAllSignIns()

// 強制重新載入設定
forceReloadConfig()

// 列出所有資源鍵值（除錯用）
listAllResourceKeys()
```