新竹泳池預約系統 · 教練自動化代訂 · 完整架構文件
新竹自潛 / 人魚教練每次訂池,要登入泳池方系統、看檔期、選水道、下訂——五個教練各做一輪,又怕撞時段。hclane 把這流程合成一頁日曆:同步檔期、衝突檢查、最後一步教練自己 click 送出。
三層架構:前端 ↔ 後端 ↔ Notion + 泳池系統。CFA 在最外層攔截所有流量。
第一次使用 vs 回訪使用,流程差異主要在「連結泳池帳號」這一步。
五張 Notion DB 為單一真實來源。所有 API 讀寫都走 Notion API(內部 Pages Functions 用 service token)。
Slots.status available · pending(被某教練暫鎖)· booked · unavailable(泳池方標示不開放)
Bookings.status pending(鎖 slot 中)· awaiting_submit(系統準備好 Playwright form 等教練 click)· confirmed(泳池方回來 booking_id)· cancelled · failed
所有路徑都在 Cloudflare Pages Functions(functions/api/*.ts)。CFA 在 edge 攔截,到 Function 時 header 已含 Cf-Access-Authenticated-User-Email。
三個觸發點 → 同一個 sync 邏輯。60 秒節流避免暴力打泳池方。
A頁面打開時 — Dashboard 載入 → 後端檢查每個 pool 的 last_synced,超過 60s 就背景觸發
B手動「立即同步」按鈕 — 教練右上角按,跳過節流強制同步
C下訂前 — 預約流程內,submit 前再 sync 一次該 slot 確認還可用
POST /api/sync?pool=X304 cached(前端用 Notion 既有 Slots)200 { added, updated, removed }分兩階段:prepare(系統做好 90% 工作)→ submit(教練最後 click)。確認 token 防止 CSRF / 跨單誤觸。
① 泳池方系統若偵測「非人類自動下訂」可能 ban 教練帳號。半自動由教練本人按下最後送出鈕,行為對泳池系統來說是真實 click。
② 系統幫忙 90%:選泳池、選時段、選水道、填好表單;教練只看截圖然後 click confirm。沒有減少教練監督,但極大省時。
③ 出錯可以教練親眼看見再做決定(例如:泳池方臨時公告、價格變更)。
POST /api/bookings/reserveavailable 改成 pending(如果失敗 = 已被別人鎖)status=pending),回傳 booking_idPOST /api/bookings/prepare 進入 prepare 階段confirm_token(5 分鐘內有效),存 Booking.error_log(其實是 prepare state)POST /api/bookings/submit 帶 confirm_tokenconfirmed / Slot.status = booked / pool_booking_id 寫入主要是兩個教練同時搶同一個 slot的情境。用悲觀鎖 + 5 分鐘逾時自動釋放。
# 1. 嘗試把 Slot.status 從 available → pending PATCH notion/pages/{slot_id} if current.status == "available": set status = "pending" set locked_by = coach_id set locked_at = now() else: return CONFLICT 409 # 2. Booking 建好後若 prepare/submit 失敗,釋放鎖 ON FAILURE: set Slot.status = "available" set locked_by = null delete Booking # 3. 背景 worker(CF cron)每分鐘掃 WHERE Slot.status = "pending" AND locked_at < now() - 5min → 釋放:status = "available", locked_by = null → 也刪掉對應 Booking(status=pending)
因為泳池方系統是真實源頭,會有「hclane 顯示可訂,但實際 click submit 時被別人搶走」的小概率(教練 A 在 hclane 鎖了 slot,但其實泳池系統那邊某人直接登入該系統下訂;A 走到 submit 階段才發現)。處理方式:submit 失敗時清楚顯示「泳池方回應該時段已被預約」,自動 sync 一次,回到日曆讓教練重選。
三層信任:CFA (邊緣身份) → Pages Function (應用授權) → Notion service token (資料層)。教練的泳池憑證單獨加密。
Cloudflare Access 政策:allow email ends with @gmail.com + 在 Coaches table active=true 的清單裡。Function 收到的 header 含已驗證的 email,不可偽造。
用 Pages Function 環境變數 POOL_CREDS_KEY(AES-256-GCM key)加密 session cookies → 寫進 Notion 是 base64 ciphertext + IV,明碼從不離開記憶體。
Notion API token 只在 Function 環境變數,不暴露給前端。前端只能透過 /api/* 間接讀寫 Notion,所有授權都在後端二次檢查(不只信 CFA email)。
submit 階段用一次性 confirm_token(HMAC-signed,5 分鐘內有效)綁定 booking_id + coach email,避免別處連結誤觸下訂。
同教練同泳池 sync 60s 節流;submit 每教練每分鐘最多 3 次;連結泳池帳號每天最多 5 次(避免被當暴力登入)。
所有預約 / 取消 / 連結 / 解除動作寫進 Notion 的一個 Audit DB(or Booking.error_log),含時間 + email + IP + 結果。
每個錯誤情境都有明確 fallback 給教練的 UX。
分階段交付,每階段可獨立上線。
這些等 user 提供答案,才能進入 Phase 1 實作。
名稱、URL、是不是政府場館預約 / 健身館預約系統 / 私人開發。決定 connector 策略(fetch vs Playwright)。
email/password / 手機 OTP / Line login / Google OAuth?決定「連結泳池帳號」UX 設計。
如果有公開 API 最好;如果只有 web 那一定走 Playwright;mobile app 可能有 reverse engineering 的選項。
泳池系統有以水道為單位的預約嗎?還是只訂「時段」不分水道?影響 Slots.lane 是否需要。
新教練加入的流程:user 手動到 Notion 加 row 開 active?還是教練自己用 Gmail 登入後自動加入 + 等 admin 啟用?
除了教練,是否有「管理者」(也許是 user 本人)可以看所有教練的預約、強制取消、處理糾紛?
本系統 Non-goal 是不做付款,但要確認泳池方付款是「下訂後現場付」「下訂時刷卡」「會員預扣」哪一種,影響 UX 提示。