🎰 年會抽獎系統完整邏輯

第一階段(普獎)→ 第二階段(大獎)跨階段中獎資格全流程解析

🔵 Phase 1 — 普獎抽獎 🔴 Phase 2 — 大獎抽獎 🟣 跨階段重複中獎已啟用
graph LR A["📋 大會報到"] --> B["🎁 Phase 1 普獎抽獎"] B --> C["🎭 表演節目"] C --> D["🗳️ 投票"] D --> E["📍 在場確認"] E --> F["🏆 Phase 2 大獎抽獎"] style A fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd style B fill:#1f3b5a,stroke:#3b82f6,color:#93c5fd style C fill:#3b1f5a,stroke:#a855f7,color:#d8b4fe style D fill:#5a3b1f,stroke:#f59e0b,color:#fcd34d style E fill:#1f5a3b,stroke:#34d399,color:#6ee7b7 style F fill:#5a1f1f,stroke:#ef4444,color:#fca5a5
1 Phase 1 — 大會報到(Event Check-in)
大會報到美人魚

📋 活動的第一道門檻:大會報到

大會報到是整個年會抽獎流程的起始步驟,也是確認參與者身份與出席的第一道關卡。當參加者抵達會場後,透過掃描專屬的 QR Code 或由志工協助,系統將自動記錄其 event_checked_in = True 的狀態。這個步驟是後續所有流程的前提條件,無論是節目報到、投票還是抽獎,都必須先通過大會報到才能參與。系統會同步記錄報到時間 event_checkin_at 及報到方式 event_checkin_method(可能為 QR 掃描、手動輸入帳號等),作為出席紀錄的一部分。在全流程模式 full_flow 下,大會報到完成後,參加者的手機頁面將自動引導至下一步驟——節目報到。系統同時支援代理報到功能 operator_proxy_checkin,讓執行秘書可以透過操作員後台為社員批量報到,特別適用於年長社友或不熟悉手機操作的參加者。報到數據會即時反映在管理員控制台的統計面板上,讓大會工作人員可以隨時掌握到場率。

flowchart TD A["參加者抵達會場"] --> B{"掃描 QR Code
或輸入帳號"} B --> C["系統驗證身份"] C --> D{"是否有效參加者?"} D -- 是 --> E["設定 event_checked_in = True"] E --> F["記錄報到時間與方式"] F --> G["✅ 大會報到完成"] D -- 否 --> H["❌ 查無此人"] G --> I["引導至下一步驟"] style A fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd style E fill:#0d3b1f,stroke:#34d399,color:#6ee7b7 style G fill:#0d3b1f,stroke:#34d399,color:#6ee7b7 style H fill:#5a1f1f,stroke:#ef4444,color:#fca5a5
2 Phase 1 — 第一階段的抽獎(大會報到獎)
普獎抽獎美人魚

🎁 第一階段普獎抽獎機制

第一階段普獎抽獎是年會的核心環節之一。管理員透過後台的抽獎控制面板,選擇 Phase 1 的獎品進行抽獎。系統的核心函式 get_raffle_eligible_attendees(phase=1) 會從資料庫中篩選出所有符合 Phase 1 要求的參加者——只需完成大會報到event_checked_in = True)即可。Phase 1 不要求節目報到、不要求投票、不要求在場確認,這是最寬鬆的資格門檻,目的是讓所有到場的參加者都有機會獲得普獎。系統採用密碼學安全的隨機數生成器 SystemRandom 進行抽獎,確保公平性。每次抽獎會標記中獎者的 has_drawn = True,並建立 RaffleResult 記錄(包含 phase=1)。普獎通常數量較多(如 200 份),可使用批量抽獎 API draw_batch 或一次性全抽 draw_all_remaining 來加速流程。中獎者在同一階段內不會重複中獎,但最關鍵的改動是——Phase 1 中獎者不再被排除於 Phase 2 之外,只要在場確認即可繼續參加大獎抽獎。

flowchart TD A["管理員啟動 Phase 1 抽獎"] --> B["get_raffle_eligible_attendees(phase=1)"] B --> C["篩選條件:
✅ event_checked_in 大會報到
❌ 不需 show_checked_in
❌ 不需 has_voted
❌ 不需 presence_confirmed"] C --> D["排除同階段已中獎者
(RaffleResult.phase == 1)"] D --> E["附加獎品限制
(分區、eligibility_regions)"] E --> F{"有合格者?"} F -- 是 --> G["SystemRandom 隨機抽出"] G --> H["has_drawn = True"] H --> I["建立 RaffleResult(phase=1)"] I --> J["🎉 中獎!"] F -- 否 --> K["❌ 無合格者"] J --> L["中獎者仍保有 Phase 2 資格 🔑"] style A fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd style J fill:#0d5a1f,stroke:#34d399,color:#6ee7b7 style K fill:#5a1f1f,stroke:#ef4444,color:#fca5a5 style L fill:#3b1f5a,stroke:#a855f7,color:#d8b4fe
3 活動環節 — 表演節目(Show & Performance)
表演節目美人魚

🎭 普獎抽完後:精彩表演時間

節目報到是全流程模式 full_flowfull_flow_with_presence 模式下的第二道關卡。當大會正式進入節目表演階段後,管理員會開啟節目報到功能,參加者需要再次掃描 QR Code 或由志工協助確認,系統將設定 show_checked_in = True。此步驟的設計目的在於確保參加者確實留在會場觀賞節目,而非報到後即離開。在某些簡化模式下(如 event_onlydirect_vote),節目報到步驟會被自動跳過,系統會直接將狀態設為已完成。節目報到完成後,參加者才能進入投票環節。系統會檢查前一步驟的大會報到是否已完成——若未完成大會報到就直接嘗試節目報到,系統會友善提示「請先完成大會報到」。這個階層式的報到機制確保了活動參與的完整性,同時也讓主辦方可以精確統計各階段的在場人數。節目報到的時間戳記也會被保存,用於後續的出席分析報告。

flowchart TD A["節目開始"] --> B{"檢查模式"} B -- full_flow --> C{"大會報到完成?"} B -- event_only --> F["⏭️ 直接跳過"] B -- direct_vote --> F C -- 是 --> D["掃描 QR / 志工確認"] C -- 否 --> E["❌ 請先完成大會報到"] D --> G["設定 show_checked_in = True"] G --> H["✅ 節目報到完成"] H --> I["開放投票"] style A fill:#3b1f5a,stroke:#a855f7,color:#d8b4fe style G fill:#0d3b1f,stroke:#34d399,color:#6ee7b7 style H fill:#0d3b1f,stroke:#34d399,color:#6ee7b7 style E fill:#5a1f1f,stroke:#ef4444,color:#fca5a5 style F fill:#5a5a1f,stroke:#fbbf24,color:#fcd34d
4 活動環節 — 投票(Voting)
投票美人魚

🗳️ 表演結束:為喜愛的節目投票

投票是第一階段抽獎資格的最後一個前提條件。根據預設的階段要求 raffle_phase_requirements,Phase 1 要求 has_voted = True,意味著參加者必須完成投票才能進入普獎抽獎池。投票系統支援加權評分機制——不同職位的參加者(如社長、總監等)的投票分數會乘以對應的權重係數 vote_weight,確保公平性。投票結果會即時更新到計分板 Scoreboard 上,讓全場可以觀看各表演節目的得分排名。系統會在投票前檢查前置條件:在 full_flow 模式下必須先完成節目報到,在 event_only 模式下只需大會報到,在 direct_vote 模式下則無需任何報到步驟。當管理員將模式設為 disabledconfirm_presence 時,投票功能會被完全關閉。一旦參加者完成投票(即至少對一個表演投過分),系統會自動標記 has_voted = True,此參加者即正式具備第一階段抽獎資格。投票關閉時機通常在所有表演結束後,由管理員手動切換模式。

flowchart TD A["投票開始"] --> B{"前置條件檢查"} B -- full_flow --> C{"節目報到完成?"} B -- event_only --> D{"大會報到完成?"} B -- direct_vote --> E["直接投票"] C -- 是 --> E C -- 否 --> F["❌ 請完成節目報到"] D -- 是 --> E D -- 否 --> G["❌ 請完成大會報到"] E --> H["選擇表演 & 評分"] H --> I["計算加權分數"] I --> J["設定 has_voted = True"] J --> K["✅ 投票完成 — 具備 Phase 1 資格"] style E fill:#5a3b1f,stroke:#f59e0b,color:#fcd34d style J fill:#0d3b1f,stroke:#34d399,color:#6ee7b7 style K fill:#0d3b1f,stroke:#34d399,color:#6ee7b7 style F fill:#5a1f1f,stroke:#ef4444,color:#fca5a5 style G fill:#5a1f1f,stroke:#ef4444,color:#fca5a5
5 Phase 2 — 在場確認(Presence Confirmation)
在場確認美人魚

📍 第二階段關鍵前置:確認人在現場

在場確認是 Phase 2 大獎抽獎的獨有前置條件,也是整個系統中最重要的防弊機制。Phase 2 的階段要求中額外增加了 presence_confirmed = True,這意味著即使參加者已完成報到、投票並中了普獎,若未完成在場確認,仍然無法參加大獎抽獎。在場確認有多種實現方式:(1)QR Code 重新掃描——管理員將模式切換至 confirm_presencefull_flow_with_presence,參加者重新掃描入場 QR Code 即自動標記在場;(2)GPS 定位驗證——啟用 presence_gps_enabled 後,系統使用 Haversine 公式計算參加者手機座標與會場座標的距離,若在容許半徑內(預設 500 公尺)則確認在場;(3)志工手動確認——由現場志工透過操作員後台手動標記。管理員可以透過「開啟在場確認窗口」按鈕控制確認的時機——通常在普獎抽完後、大獎抽獎前開啟這個窗口,讓還在現場的人有 5-10 分鐘的時間完成確認。窗口關閉後,未確認的人將無法參加 Phase 2。管理員也可以使用 reset_all_presence 功能重置所有人的在場狀態,重新開始確認流程。

flowchart TD A["管理員開啟在場確認窗口"] --> B{"確認方式"} B --> C["📱 QR Code 重掃"] B --> D["📍 GPS 定位驗證"] B --> E["👤 志工手動確認"] C --> F["can_confirm_presence()"] D --> G["verify_gps_location(lat, lng)"] E --> F F --> H{"窗口開啟 & 模式允許?"} G --> I{"距離 ≤ 半徑?"} H -- 是 --> J["confirm_presence()"] H -- 否 --> K["❌ 窗口未開啟"] I -- 是 --> J I -- 否 --> L["❌ 超出會場範圍"] J --> M["presence_confirmed = True"] M --> N["✅ 在場確認完成 — 具備 Phase 2 資格"] style A fill:#5a1f1f,stroke:#ef4444,color:#fca5a5 style M fill:#0d3b1f,stroke:#34d399,color:#6ee7b7 style N fill:#0d3b1f,stroke:#34d399,color:#6ee7b7 style K fill:#5a1f1f,stroke:#ef4444,color:#fca5a5 style L fill:#5a1f1f,stroke:#ef4444,color:#fca5a5
6 Phase 2 — 合格者篩選邏輯
資格過濾美人魚

🔍 阻擋性獎品排除 + 在場確認

Phase 2 大獎抽獎採用精密的「阻擋性獎品」排除機制。每個 Phase 1 獎品都有 excludes_from_next_phase 屬性:普獎設為 False(中獎不阻擋 Phase 2),其他獎項(如特別獎)設為 True(中獎阻擋 Phase 2)。因此 Phase 2 的候選池 = 所有完成大會報到的人中,排除「中了阻擋性獎品的人」和「Phase 2 已中獎者」,再篩選 presence_confirmed = True。舉例:300 人大會報到 → 200 人中了普獎(不阻擋)→ 5 人中了特別獎(阻擋)→ Phase 2 候選池 = 300 - 5 = 295 人中有在場確認的人。普獎中獎者和完全沒中獎的人都可以參加 Phase 2。

flowchart TD A["Phase 2 合格者篩選"] --> B["所有大會報到者"] B --> C{"Phase 1 中獎類型?"} C -- "普獎 excludes=False" --> D["✅ 不阻擋"] C -- "其他獎 excludes=True" --> E["❌ 阻擋,排除"] C -- "未中獎" --> F["✅ 不影響"] D --> G["presence_confirmed ✅"] F --> G G --> H["排除 Phase 2 已中獎者"] H --> I["最終候選人名單"] style A fill:#5a1f1f,stroke:#ef4444,color:#fca5a5 style D fill:#0d3b1f,stroke:#34d399,color:#6ee7b7 style E fill:#5a1f1f,stroke:#ef4444,color:#fca5a5 style F fill:#0d3b1f,stroke:#34d399,color:#6ee7b7 style I fill:#0d5a1f,stroke:#34d399,color:#6ee7b7
7 Phase 2 — 大獎抽獎
大獎抽獎美人魚

🏆 萬眾矚目的大獎時刻

Phase 2 大獎抽獎是整個年會的高潮時刻。與普獎不同,大獎通常數量稀少但價值極高(例如 1 萬元禮券、高級家電等),因此管理員通常會逐一抽出 draw_phase/2 或針對特定獎品抽出 draw_prize/{prize_id},每次抽出一位中獎者搭配全場螢幕的動畫展示效果。系統的抽獎流程與 Phase 1 相同——呼叫 get_raffle_eligible_attendees(phase=2) 篩選合格者,然後使用 SystemRandom 隨機抽出。但因為 Phase 2 的篩選條件更嚴格(多了在場確認),合格者池相對較小,這也使得大獎更為珍貴。中獎者被抽出後,系統會建立 RaffleResult(phase=2) 記錄,並在管理員控制台和參加者螢幕上同步顯示中獎資訊。中獎者需要在現場完成領獎流程——包括到指定領獎站出示身份、在平板上簽名確認,系統會記錄領獎時間 claimed_at、領獎站 claim_source、以及電子簽名圖檔 signature_path。若中獎者未在規定時間內領獎,管理員可以執行棄獎 forfeit 操作,將該獎品名額退回可抽狀態,讓其他合格者有機會中獎。

flowchart TD A["管理員啟動 Phase 2 抽獎"] --> B["get_raffle_eligible_attendees(phase=2)"] B --> C["雙重篩選:大會報到 + 在場確認"] C --> D["附加獎品地區限制"] D --> E{"有合格者?"} E -- 是 --> F["🎰 SystemRandom 抽出"] F --> G["建立 RaffleResult(phase=2)"] G --> H["🏆 大獎中獎!"] H --> I["通知中獎者至領獎站"] I --> J{"限時內領獎?"} J -- 是 --> K["簽名確認 → claimed = True"] J -- 否 --> L["forfeit 棄獎退回"] L --> M["獎品回到可抽狀態"] E -- 否 --> N["❌ 無合格者"] style A fill:#5a1f1f,stroke:#ef4444,color:#fca5a5 style H fill:#5a5a1f,stroke:#fbbf24,color:#fcd34d style K fill:#0d3b1f,stroke:#34d399,color:#6ee7b7 style L fill:#5a3b1f,stroke:#f59e0b,color:#fcd34d style N fill:#5a1f1f,stroke:#ef4444,color:#fca5a5
8 跨階段中獎資格 — 核心改動
跨階段美人魚

🔗 普獎中獎者可參加大獎抽獎

這是本次系統升級的核心改動,也是解決「200 人普獎中獎後如何參加大獎抽獎」問題的關鍵邏輯。在舊版系統中,get_raffle_eligible_attendees() 函式使用全局排除機制——一旦參加者的 has_drawn = True 或已存在任何 RaffleResult 記錄,就會被排除在所有階段的抽獎池之外。這意味著 Phase 1 普獎的 200 位中獎者,即使仍在現場且已通過在場確認,也完全無法參加 Phase 2 大獎抽獎。新版系統改為按階段隔離排除——查詢已中獎者時加入 RaffleResult.phase == phase 條件,只排除同一階段的已中獎者。例如查詢 Phase 2 合格者時,只排除 Phase 2 已中獎者,Phase 1 的中獎者不受影響。同時,attendee_meets_raffle_requirements() 也移除了全局 has_drawn 檢查,讓跨階段資格判斷更加準確。舉例來說:假設有 300 位參加者全部完成報到與投票,其中 200 人在 Phase 1 中了普獎。在舊版系統中 Phase 2 只有 100 位合格者;在新版系統中,只要這 200 人之中有人通過在場確認,他們也會出現在 Phase 2 的合格者名單中,大幅增加了大獎抽獎的參與度。has_drawn 欄位仍然保留在系統中作為「曾經中獎」的輔助統計標記,用於管理後台的統計顯示,但不再作為跨階段排除的依據。

flowchart TD subgraph old ["❌ 舊版邏輯(全局排除)"] O1["Phase 1 普獎 200 人中獎"] O1 --> O2["has_drawn = True
RaffleResult 存在"] O2 --> O3["Phase 2 排除所有已中獎者"] O3 --> O4["Phase 2 剩餘 100 人"] end subgraph new ["✅ 新版邏輯(按階段隔離)"] N1["Phase 1 普獎 200 人中獎"] N1 --> N2["RaffleResult.phase = 1"] N2 --> N3["Phase 2 只排除 phase=2 已中獎"] N3 --> N4["200人有在場確認 → 仍可抽"] N4 --> N5["Phase 2 最多 300 人"] end style O4 fill:#5a1f1f,stroke:#ef4444,color:#fca5a5 style N5 fill:#0d5a1f,stroke:#34d399,color:#6ee7b7
📊 Phase 1 vs Phase 2 要求對照表
條件 Phase 1(普獎) Phase 2(大獎)
大會報到 event_checked_in ✅ 必須 ✅ 必須
節目報到 show_checked_in ❌ 不需要 ❌ 不需要
投票 has_voted ❌ 不需要 ❌ 不需要
在場確認 presence_confirmed ❌ 不需要 ✅ 必須
同階段重複中獎 ❌ 不允許 ❌ 不允許
普獎中獎者 ✅ 可參加 Phase 2(excludes_from_next_phase = False)
其他獎項中獎者 ❌ 不可參加 Phase 2(excludes_from_next_phase = True)
Phase 1 未中獎者 ✅ 可參加 Phase 2(在場確認即可)