iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
Vibe Coding

神隊友.swift:30 天 Vibe Coding 打造育兒 iOS App 系列 第 4

[Day 4] 三票規則由誰把關,GPT-6 Astra 零素材做出 3D 小孩房

  • 分享至 

  • xImage
  •  

Day 4

文章同步發表在我的個人 Blog

投票頁實作:三票規則由誰把關,GPT-6 Astra 零素材做出 3D 小孩房

一個只收一週的投票頁,架構要開到多細?

昨天提到兩個 AI Agent 怎麼分工,今天講實作:頁面怎麼接後端、資料怎麼存、三票規則由誰驗證、3D 和手機用了什麼。原本規劃的架構今天被我改掉一半,改的原因也寫在裡面。

1. 整體架構

投票頁架構

一個 Worker 同時送網頁和收資料,後面接 D1。

用什麼 做什麼
前端 3D 小孩房(Vite 打包的靜態檔) 走到白板前投票,三票可以調配、可取消
API Cloudflare Worker /api/* 由 Worker 處理,其餘路徑傳回靜態檔
資料庫 D1 選項、投票、自己寫的便利貼

原本規劃用 Cloudflare Pages 加 Pages Functions,實際做的時候改成單一 Worker 搭配靜態資源。差別在於部署與設定只有一份,本機也能用一個指令把網頁和 API 一起跑起來,不用另外開兩個服務。

2. 為什麼不是「送出一次」

原本的設計是:投票過程都只改瀏覽器裡的狀態,按下送出才打一次 API。這樣中途如果更改主意不會留下冗餘資料,請求數也最少。

投票規則後來加了一條:已經貼上的圓點可以點掉,退回的票可以改投別張。 一旦票可以取消,三票上限就不能只在前端算,否則改一下請求內容就能投超過三票。

所以改成每個動作即時送給伺服器:貼一票是 POST /api/votes,點掉一票是 DELETE /api/votes/:id,每次都由伺服器重算你還剩幾票。

前端負責畫面,規則一律以伺服器的回應為準。

端點 做什麼
GET /api/state 一次拿選項、自己的票、自己寫的便利貼
POST /api/votes 投一票
DELETE /api/votes/:id 取消自己的那一票
POST /api/responses 新增「你們家的」補充情境
GET /api/results 投滿三票之後才拿得到的統計

連點會不會投超過三票。 會,如果寫成「先查目前幾票,再寫入」。兩個請求同時進來時,兩邊都會查到「還剩 1 票」,然後各寫一筆。

所以票數上限直接寫在同一句 SQL 裡,查詢和寫入是同一個動作:

INSERT INTO votes(id, option_id, session_id)
SELECT ?, id, ? FROM survey_options
WHERE id = ? AND active = 1
  AND (SELECT COUNT(*) FROM votes WHERE session_id = ?) < 3
RETURNING id;

寫不進去就代表選項無效或票已用完,直接回錯誤。我用同一個 session 同時送 20 票實測:3 票成立,17 票被拒

身分怎麼來。 不做登入。第一次進來時由伺服器發一組隨機的 session id,放在 HttpOnly 的 cookie 裡,正式環境加上 Secure。前端送什麼 session_id 過來都不採用,伺服器只看 cookie,不然改一下請求就能刪別人的票。

3. 四張資料表

放什麼
sessions 伺服器發出去的匿名 session
survey_options 五個正式選項:分類、情境文字、排序、是否啟用
votes 一票一列:選項、session、時間
responses 自己寫的補充情境,附審核狀態

正式選項和自由回答分開存。 五個正式選項是拿來比較的固定題目,自由回答是開放式的補充,兩者混在一起,之後算比例時就得一直排除自訂項目。所以分開:正式選項才進 votes,補充情境不參與投票、不消耗票數。

一票一列,而不是「選項 + 票數」一列。因為三票可以自由分配,一個人投了什麼要能還原成一組;取消時也只要刪掉那一列。

responses 的狀態一律由伺服器寫成 pending。前端送 status: approved 過來會被忽略,不然任何人都能讓自己的回答直接公開。

4. 結果什麼時候才看得到

投完三票才看得到結果

投票前先看到別人怎麼選,其實會影響自己的第一選擇。所以結果是投完三票之後,再按一次「看看其他家庭怎麼選」才顯示。

這條規則伺服器也擋:GET /api/results 會先確認這個 session 剛好有三票,不然回 403。只靠前端隱藏,直接打 API 就看得到了。

百分比的定義寫清楚比較重要:這裡算的是「所有投出的圓點中,各選項占幾成」,不是「多少家庭選了這個」。因為一個人可以把三票都投給同一張。

另外,四捨五入之後五個數字加起來常常不是 100%。所以改用最大餘數法分配,讓總和剛好 100。代價是同樣是三分之一的兩個選項,可能一個顯示 34%、一個顯示 33%。

5. 3D 用了什麼

3D 這部分交給 Codex(GPT-6 Astra)實作,用到的東西比想像中少:

  • Three.js:3D 渲染
  • React Three Fiber:把 Three.js 包成 React 元件,每一幀更新畫面
  • RoundedBoxGeometry:Three.js 內建的圓角方塊,家具和角色都靠它組出來
  • Vite、TypeScript:開發與建置

房間、家具和角色都是程式用基本幾何體組出來的,沒有模型檔,也不需要 Blender。

3D 那部分用了什麼

動畫也是程式算的,不是動畫檔:

  • 角色轉向、鏡頭跟著玩家:每一幀用 lerp(線性插值)往目標靠近一點
  • 按 E 之後鏡頭推近白板:用 THREE.MathUtils.damp(阻尼)做平滑過渡,不會瞬間跳過去
  • 走路時碰到家具會停下:自己寫的碰撞判斷,沒有用物理引擎

好處是零素材、載入快,手機打開不用等模型下載,缺點是質感比較簡單。我另外做過一版質感測試,換成 Kenney 的 CC0 家具與角色模型,質感明顯好很多,要不要換等上線後再決定。

為什麼不用 Unreal:那是桌面等級的渲染,網頁跑不動,投票頁要在手機上三秒內載完。

6. 手機不是把桌機畫面縮小

手機橫向的白板

手機版今天也一起做了:直向提示把手機轉過來,橫向才進房間,左下角是自己寫的虛擬搖桿(沒有用套件),走到白板前右下角出現「查看白板」。搖桿和鍵盤共用同一套移動與碰撞邏輯,所以手機不會比電腦走得快。

白板在手機上是另一套排法:桌機三欄,手機改成 3 + 2,五張卡一次全部看得到。因為這題是「比較五個情境再分配三票」,得先看得到全部,才有辦法比較。中間試過左右滑動的版本,結果是不滑到底根本不知道有哪些選項,所以退掉了。

真正花時間的是高度。iPhone 橫向時,Safari 的網址列和分頁列會吃掉一大塊,實際可用高度只剩 300 px 上下,不能假設拿得到整個螢幕。所以高度全部改用 dvh(動態視窗高度),版面跟著實際可用空間算。

我也查過能不能讓網頁全螢幕,把 Safari 的介面藏起來,結論是iPhone 沒有這個能力:Fullscreen API 在 iOS 只有 iPad 支援。唯一能全螢幕的是「加入主畫面」,但沒有人會為了投一次票去安裝一個網頁。所以方向反過來:承認網址列會一直在,直接照 300 px 的高度設計。

7. 做完才發現的三個問題

功能都跑起來之後,我請 Codex 做一次唯讀的安全審查,自己也拿 curl 打了一輪 API。抓到三個問題:

問題 後果
補充情境沒有數量上限 同一個人連送 12 則全部寫入,審核佇列會被灌爆
連打不存在的網址也會建立 session 20 個 404 就多出 20 筆資料
沒有過濾控制字元 含 NUL 和文字方向反轉字元的內容會原樣存進資料庫

三個都改掉了:補充情境改成每個瀏覽器最多三則、確認網址有效才建立 session、存進資料庫前把控制字元清掉。改完重跑 34 項測試和一輪 API 實測。

還有一個沒修:清掉 cookie 就能再投三票。這要真人驗證或 IP 限流才擋得住,這一關刻意不做。所以畫面上也不會寫「每個人只能投一次」——技術上保證不了的事,就不要寫在介面上。

8. 交給 AI 的 prompt 長什麼樣

一份可用的工單大致有四段:

## 需求
場景大小、家具清單、操作方式、驗收行為

## 這一關禁止實作
投票、資料庫、手機控制、音效……(明列)

## 這些坑是前幾版踩過的,請避開
1. 不要用 React state 每幀更新,手機會閃
2. 不要用某個相機元件,之前沒生效導致整個畫面空白
3. 不要直接用 crypto.randomUUID(),手機走 http 區網連線時不存在,整頁會當掉
4. 套件安裝有 peer 衝突,要加 --legacy-peer-deps

## 完成前自行檢查
build、typecheck、自己加測試、dev server 沒有錯誤

第三段是最值錢的。 每踩一個坑就補一條進去,下一次它就不會再犯同樣的錯。上面那四條都是我這幾天真的遇到的。

今天還多加了一條:票數上限不准用「先查再寫」兩個步驟。 這種競態問題不寫進工單,AI 幾乎都會寫成先查後寫。

另一個小經驗:Codex 不會讀 CLAUDE.md,它讀的是 AGENTS.md 昨天提到的三份文件裡,CLAUDE.md 的協作規則只有 Claude Code 看得到。要讓 Codex 也遵守,得把規則寫進 AGENTS.md,或像上面這樣直接放進工單。我這次是用工單帶過去的。

9. 還沒做的

這幾項規格裡有,但今天還沒動,架構圖上也標成「之後要做」:

  • 防灌票:Turnstile 真人驗證、IP 每日換 salt 的 hash 限流(只留 24 小時)。現在清掉 cookie 就能再投三票。
  • 審核後台:用 Cloudflare Access 保護,只有我進得去,新增的答案先審再公開。
  • 結果分析:資料都在 D1 裡,收完之後要怎麼撈出來看再決定。
  • 正式環境的資料庫:目前只跑在本機,正式 D1 還沒建,設定檔裡還是佔位值。

免費方案的額度,一週的投票量應該綽綽有餘。

明天

Day 5 把上面那三項補起來,先做防灌票的三層,再處理審核後台。


上一篇
[Day 3] Multi-agent 架構:問卷投票頁的 AI 分工與驗收流程
系列文
神隊友.swift:30 天 Vibe Coding 打造育兒 iOS App 4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言