snowfishb 預約系統|架構圖

純前端 Cloudflare Pages + Google Apps Script + Google Sheet 三層架構,零伺服器維護

① 整體拓樸

Frontend
Cloudflare Pages靜態
Vanilla HTML/CSS/JS
無框架、無 build
inline CSS + inline JS
首屏單檔 100KB
localStorage 5min 快取
API 層
Google Apps Script/exec
單一 endpoint
?sheet= 路由
token 弱驗證
SF_TOKEN 寫死前端
POST 用 no-cors
fire-and-forget
資料層
Google SheetDB
4 個分頁當資料表
教練手動 Sheet 上排課
學生看到的就是 Sheet 內容
繳費狀態改文字
「未繳費 → 待確認 → 已確認」

② 資料模型(Google Sheet 分頁)

📅 團練行事曆
教練排好的場次(source of truth)
  • 日期 時間 泳池
  • 類型 團練 / 跟課
  • 成團人數 最低開團人
  • 滿團人數 滿額
  • 備註 狀態
🐟 團練預約
學生報名紀錄(每筆一人一場)
  • 日期 上課時段 地點
  • 名字 電話 聯絡方式
  • 繳費狀態 未繳費/待確認/已確認/已繳費
  • submittedAt
🌟 團練許願
尚未開課的需求收集(lead)
  • 綽號 手機號碼 聯絡方式
  • 希望日期 多選
  • 偏好時段 早/中/下午/晚
  • 希望地點 備註
  • altTraining 沒成團是否轉 1v1
🚫 封鎖時段
教練不可上課的日子
  • 日期
  • 前端在月曆上把該日標 ✕
🏊 泳池資料
不在 Sheet — 是純靜態 JS 檔
  • /js/pools-data.js
  • name nickname region
  • groupPrice memberPrice
  • includesEntry duration
  • 理由:價格半年才改一次,不需要每次都 fetch

③ Apps Script API(單一 /exec)

GET?sheet=<name>&token=<x>
回傳該 Sheet 分頁的 JSON 陣列(每列一個 object,欄位用中文 key)
POST{ sheet:'團練預約', date, pool, time, name, phone, contact, token }
新增一筆預約(繳費狀態預設 = 未繳費)
POST{ sheet:'團練許願', nickname, phone, dates, preferredTime, place, ... }
新增一筆許願
POST{ action:'deleteBooking', phone, date, time, pool, token }
學生自助刪除「未繳費」的預約
POST{ action:'updatePayment', phone, bookings[], totalAmt, payMethod, last5, token }
提交繳費資訊 → Sheet 上對應列的「繳費狀態」改成「待確認」

④ 核心流程

🐟 流程 A:學生預約
1進站 → 並行讀 行事曆+預約+封鎖
2前端即時算 booked / 成團狀態 / isFull
3點月曆 chip → 開 Modal
4填三欄 → POST 團練預約
5cache 失效 → 5 分鐘內仍看到舊資料,背景更新
🌟 流程 B:許願(還沒開團)
1選地區 → 動態 filter 泳池下拉
2多選日期 → 排除封鎖時段
3選偏好時段 + 是否轉 1v1
4POST 團練許願 → 教練看後決定排課
💰 流程 C:繳費(兩段式)
1輸入電話 → 撈所有未繳費預約
2勾場次 + 選身份(一般/鱈魚寶)+ 加裝備
3勾兩條同意條款(解鎖繳費區)
4前端算 sessionAmt + equipAmt = 總額
5填匯款後五碼 → POST updatePayment
6跳到查詢頁 → 看到「待教練確認」
🔍 流程 D:查詢/取消
1輸入電話 → 撈所有預約紀錄
2顯示成團狀態 + 繳費狀態
3未繳費 → 顯示 [前往繳費] [刪除] 按鈕
4刪除 → POST deleteBooking

⑤ 設計亮點(值得抄)

cache-first 渲染
先讀 localStorage 立即畫月曆,背景靜默更新後再 re-render。
體感秒開,又不會看到過期資料超過 5 分鐘。
🧮
狀態前端算
「成團狀態」不存在 Sheet,由前端用 min/max/booked 即時算。教練只要維護「最低/最高人數」,狀態自動更新。
🔌
單 endpoint 多 sheet 路由
一支 GAS 用 ?sheet=xxx 派發到不同分頁;POST 用 action 區分 insert/update/delete。
不用每個表單一支 GAS。
🧩
可注入式條款模組
agreement-flow.js 把「同意 → 解鎖區塊」抽成獨立元件,自帶 CSS。
任何表單頁加兩行就能套上。
📞
電話當主鍵
查詢、繳費、刪除全靠電話。
還處理 Sheet 把 0912 存成 912 的問題(自動補 0)。
💸
繳費 = 改狀態
不串金流,繳費只是把 Sheet 上一列從「未繳費 → 待確認」,教練看到匯款後手動改成「已確認」。
零金流串接成本。
🪄
資料分動靜兩種
會變的(行事曆/預約)放 Sheet;不太變的(泳池清單/價格)放 pools-data.js 純前端。
減少 GAS 讀取量。
🛡️
no-cors fire-and-forget
POST 用 mode:'no-cors' + text/plain,避開 CORS preflight。
代價:拿不到 response,只能假設成功。

⑥ Google Sheet vs Notion API(後端對比)

面向 Google Sheet (GAS) Notion API
後端寫法 寫 Apps Script(JS),部署成 Web App 直接打 REST API(不用寫 server)
認證 token 放前端弱驗證;或 OAuth integration token 必須放後端(不能放前端)
速度 GAS cold start 1-3s,慢 API 200-500ms
免費額度 每日 90 分鐘執行 / 20K URL fetch(個人版) 3 req/s rate limit,沒每日上限
並發寫入 同時多人寫會打架,需 LockService REST 天生並發友好
檢視 / 編輯介面 教練直接打開 Sheet 改一改(人人會用) Notion 介面美,可加 view/filter/calendar
欄位型別 弱型別,全部當文字(0912 變 912 那種雷) 強型別(select/date/number/relation)
關聯(join) 要在 JS 裡自己對 key 原生 relation/rollup(但 API 撈相對麻煩)
長文/圖片 儲存可以,呈現很醜 每筆是一個 page,可放長文/檔案/圖
前端直連 CORS 友善(GAS 設好就行) 官方不允許前端直連,要 proxy / CF Worker
備份/匯出 Google 自動版本歷史 官方匯出 zip 含中文檔名問題
適合場景 表單收集、報名繳費、輕量 CRUD(用戶看不到後台) 知識管理、內容站、需要關聯查詢與檢視介面
給你的建議:snowfishb 用 Sheet 是對的選擇——教練要在手機上隨時看誰報名了、改成團狀態,Sheet 比 Notion 直觀。
你自己 subscription-tracker 用 Notion 也對——你要的是內容資料庫+多 view。
分水嶺:資料消費者是人還是程式?人要看 → Notion;程式要算 → Sheet 或 SQLite/D1。

⑦ 其他值得注意的東西

⚠️
弱驗證的隱憂
SF_TOKEN = 'sf_9Kx7mBpQ4rL2nWv' 寫在前端 → 任何人都能拿去打 GAS,可惡意刷單或洗 Sheet
改進方案:GAS 端加 rate limit(同 IP 60s 內最多 N 筆),或用 Cloudflare Worker 當 proxy 套 Turnstile。
🔒
並發寫入隱患
週末晚上同時 5 個人按「確認預約」,GAS 沒加 LockService.getScriptLock()同列覆寫
也沒做「滿團就拒絕」server-side 檢查,全靠前端 isFull。
📨
缺通知
學生報名 / 繳費後沒收到 Email / LINE 確認,只能去查詢頁自己看。
GAS 可內建 MailApp.sendEmail(),或接 LINE Notify webhook(這你最熟)。
🎯
許願 → 排課 的設計很聰明
不是直接讓學生開團,而是收集需求,由教練彙整後排出有勝算的場次。
避免「一堆冷門時段都開了沒人來」的浪費,這個 pattern 你做 MF 課程也可以用。
🎨
UI 風格統一
所有頁面共用 6 個 CSS variable(--bg --surface --border --text --muted --accent),加上紫色漸層 shimmer 動畫當品牌記憶點。
但 CSS 沒抽出來複用,每頁 inline 一份。
📦
build-less 的代價
group.html 已經 104KB(inline JS 47KB)。
再大下去就要拆 module,或乾脆上 Vite/Astro。
你目前所有專案還沒到這個量,繼續 vanilla 沒問題。