昨天結尾,留了兩個問題:
昨天我們其實就已經知道答案:畫面不是唯一的入口。 任何人都能跳過表單,直接送出請求。在海上,船舷之外漂來的東西,不能看都不看就拉上船。網站也一樣:從外面送進來的資料,要先檢查過,才能放進資料庫。
今天要做的,是專案中第一個讓使用者的操作能「寫入」資料庫的功能:回報資料錯誤。 使用者發現某張專輯的內容物不對,可以直接在詳情頁送出一則回報,將回報紀錄寫進資料庫後,我會接著收到通知。
不過,今天我想換個方式開始。
如果是以前,我大概會先自己思考一下:需要什麼資料表、需要什麼 API、送什麼欄位、回什麼狀態碼,然後才開始寫。但現在,我更常先做另一件事:告訴 AI 我想要什麼,請它把這個需求展開。
我想在專輯詳情頁加入「回報資料錯誤」功能。
使用者可以針對目前這張專輯/版本送出一段文字回報。
送出後我希望資料能被保存,而且我能收到通知。先不要寫程式。請先分析:
- 這個功能會影響哪些地方?
- 前端、後端、資料庫分別需要做什麼?
- 有哪些安全性與驗證問題?
- 你建議的實作順序是什麼?
- 預計會修改哪些地方?
分析完後,告知我結果,並且另外以 1~5 句話總結,還不用實作。
這段 prompt 裡提分析的問題,也是刻意的。如果只說一句「幫我做一個回報功能」,也做得出來,但它有可能會照最直接的方式做:一個表單、一張表,能送出就好。它不一定會主動去想,送進來的資料能不能信、通知失敗怎麼辦。
AI 想得多深,很大一部分取決於你問了什麼。 (或是模型本身的能力) 請它先回答「會影響哪些地方」「有哪些安全性與驗證問題」,就是在逼它把這些事想過一遍。先展開、再動手,寫出來的程式通常會相對穩定得多。
這次 Codex 有自己先讀專案的程式碼,再提出計畫。如果你用的工具不會,可以在 prompt 裡加一句:「請先閱讀目前跟專輯詳情、資料存取相關的程式碼,再提出計畫。」
通常建議還是要看一下他過程中有沒有真的讀程式碼,或是看一下他的結果跟程式碼內容是否符合。有時候即便在提示詞中請他看過程式碼,他會沒有看,但回應的時候還是說他有看過。
AI 先讀了專案裡的程式碼,再給出它的分析。整理起來,一個「回報」功能,會動到四個地方:
專輯詳情頁的回報表單(前端)
│ POST /api/data-reports
▼
專案自己的 API(後端):先檢查送來的內容
│
├── ① 存進 data_reports 資料表(資料庫)
│
└── ② 存好之後,由後端通知 Telegram(外部服務)→ 我的手機
| 層 | 要做什麼 |
|---|---|
| 前端 | 在詳情頁加一個按鈕和表單,自動帶入目前的專輯和版本,顯示送出中、成功、失敗 |
| 後端 | 新增一支 API,重新檢查所有內容,存進資料庫,再發通知 |
| 資料庫 | 新增一張回報用的資料表,並且設定權限:訪客不能讀、也不能直接寫 |
| 外部服務 | 收到回報後,通知我 |
其中有一個細節讓我印象很深:AI 發現專輯的完整頁和側邊抽屜,共用同一個元件,所以回報表單只要加在一個地方,兩邊就都有了。這是它先讀過程式碼,才能給出的建議。
這件事通常也跟寫程式的風格有關,重複使用到的元件,我們並不會重複寫一次,而是將它抽成一個元件,直接共用,這樣相對來說有比較高的可維護性。
AI 的計畫很長。但不管多長,我通常會先確認過,例如可以先拿昨天的清單,逐條對照:
| 要問的問題 | 為什麼要問 | AI 的計畫 |
|---|---|---|
| 資料從哪條路寫進去? | 如果讓瀏覽器用公開金鑰直接寫入,任何人都能跳過畫面 | ✔ 走自己的後端 API |
| 檢查放在前端還是後端? | 前端的限制,用 Postman 就能繞過 | ✔ 前後端都檢查,明講「不能信任前端」 |
| 自動帶入的 id,後端有沒有再查? | 昨天說過,id 要填什麼都可以 | ✔ 後端會再查一次;資料庫也用「專輯 id 加版本 id」兩個欄位一起的外鍵,保證版本一定屬於這張專輯 |
| 金鑰和 token 放在哪裡? | NEXT_PUBLIC_ 開頭的,會被送到瀏覽器 |
✔ 全部只放伺服器,管理用的連線獨立成一個檔案 |
| 通知失敗時,回報會不會不見? | 通知只是附加功能,不該拖累主要流程 | ✔ 先存再通知,通知失敗也保留回報 |
| 失敗的時候,使用者會看到什麼? | AI 很容易只規劃一切順利的情況 | ✔ 送出中、成功、失敗都有對應的畫面 |
有時候,交由其他的模型來進行判斷,也是一個我蠻常用的方法。但兩個 AI 說法一樣,不代表就是一定是對的(但錯的機率會降低),最後還是要回到程式碼和實際測試。
因為 prompt 中有請它把安全和驗證都想過一遍,這次 AI 展開得很完整,甚至有點多。
除了基本功能,計畫裡還有限流、防機器人、重複內容攔截、通知狀態、通知重試……每一項確實都有道理。展開之後,接下來就是由我把它收回來。
有幾件事,AI 提供了選項,要由我決定:
| 問題 | AI 的建議 | 我的決定 | 理由 |
|---|---|---|---|
| 用什麼通知我? | Telegram | 低流量、想即時看到,Telegram 最直接。AI 也說,換通知管道只影響通知那一層 | |
| 要不要收回報者的聯絡方式? | 選填 Email | 不收 | 用不到的個人資料,就不要收 (但如果想要讓使用者知道處理進度,其實也可以幫一個選填資料) |
| 通知送到哪裡? | 私人群組 | 私人聊天 | 有這方面實作經驗 |
| 字數限制 | 10~2000 字 | 暫定1 ~ 50 字 | 問題應該通常不會太長,後續再視情況增加 |
我通常也都會在提示詞中就告知,請他展開一些選項、做法的時候,先不要替我做決定,而是去分析優缺點,後續再由我決定。
其中,字數限制其實值得想一下。AI 原本建議 10~2000 字,我則暫定上限 50 字,認為一般的資料回報應該不需要太長。但反過來想,如果有人只想回報「小卡少一張」,這樣的內容連 10 個字都不到,難道就不算有效回報嗎?所以我後來決定最少要一個字。
AI 建議的限制,不只是確認它有沒有實作,也要想想會不會連正常使用者一起擋掉。 所以除了設定上限,下限要不要存在,也應該由實際使用情境來決定。
其他的部分,我大多採用了 AI 的預設。但接受之前,我要先釐清每一件事情在做什麼:
| AI 加上的東西 | 白話 | 在擋什麼 |
|---|---|---|
| 限流 | 同一個來源,每小時最多送 5 則 | 有人用程式一直送 |
| 隱藏欄位 | 表單裡藏一個人看不到的欄位,只有機器人會填 | 自動亂填表單的機器人 |
| 重複內容攔截 | 10 分鐘內送出一模一樣的內容,就擋下來 | 手滑連按,或同一段內容一直送 |
| 通知狀態 | 記錄每則回報的通知,是成功、失敗,還是還沒送 | Telegram 暫時故障時,事後還查得到哪些沒送出去 |
| 整張專輯或目前版本 | 使用者可以選擇回報的範圍 | 有些錯誤是整張專輯的,不是某個版本 |
以這個專案現在的規模,這幾項不一定每一項都非做不可。我把它們留下來,一部分也是當作示範:等網站變大、開始被有心人士盯上,這些防護就派得上用場,而且不用到時候才從零開始補。
有了 AI,這些防護現在加起來很快。但前提是,你得先知道有這些東西,才會想到要問。
其中,限流是我特別追問的一項。我問 AI:「次數記在哪裡?部署到 Vercel 之後還有效嗎?」
而它採用的方式,是直接查詢資料庫裡,同一來源最近一小時已送出的回報數量,而不是把次數暫存在伺服器的記憶體裡。這樣一來,即使網站部署到不同的執行環境,也能共用同一份紀錄。
這不是唯一的限流方案,也有它的限制。但對目前這個不需要登入、送出後還會觸發 Telegram 通知的小功能,我決定先接受。
另外幾項防護也是一樣。有了 AI,增加功能很便宜,但複雜度並不免費。 多一條規則,就多一個需要理解、測試和維護的地方。
未來如果說回報問題變成要登入,好處就是我可以直接透過使用者來比對、計算回報次數,同時也能讓使用者看得到處理狀態;但缺點就會變成為登入訪客回報問題變得很麻煩。
決定好之後,後續的實作幾乎都交給 AI。它照這個順序完成:
data_reports 資料表。這是新的一份,不是回頭改 Day 15 的那份,Day 15 說過,已經套用的 migration 不要回頭改。/api/data-reports,負責檢查內容、存進資料庫、發通知。AI 也順手測了一件事:用公開金鑰去讀 data_reports,得到 401。訪客讀不到任何回報,跟 Day 16 的實驗 4 是同一扇門。
這次要我自己處理的,是金鑰和 token:
.env.local

【圖1|取得 Supabase service role key】
@BotFather,傳送 /newbot,依照指示分別取一個顯示名稱和帳號名稱(帳號名稱要以 bot 結尾)。完成後,BotFather 會給你一串 token,它等同 bot 的密碼,將其存入TELEGRAM_BOT_TOKEN的環境變數中(.env.local,要記得存檔)
【圖2|取得 Telegram BOT Token】
/start
/start@你的bot帳號名稱
TELEGRAM_BOT_TOKEN去問 Telegram 這個 bot 最近收到的訊息,並且只印出 chat id,不印出 token
幾個要注意的地方:
service_role或是 secret key
NEXT_PUBLIC_ 開頭,也不要貼進對話框取得 chat id 這一步,我稍微卡了幾次。一次是複製指令時少了一個
/,Telegram 只回了一句Not Found;另一次是指令裡的!被終端機當成特殊符號。最後都是把錯誤訊息丟回給 AI 才找到原因。AI 給的指令沒問題,不代表你貼上去的也沒問題。
回頭看 AI 的操作紀錄時,我發現它推上雲端用的是:
supabase db push --yes
-yes 的意思是「所有確認提示,都自動回答是」。它有先預演,這次也只是新增一張表,沒有動到舊資料,所以結果沒有問題。但 Day 15 的分工表寫的是:推上雲端之前,要先讓我看過 SQL。 這次,我其實沒有看。
AI 很快就會把你訂的規則,當成「已經同意過了」。但之前允許過,不等於這次也不用看。 每次推上雲端前,還是值得看一眼:這次要執行的是哪幾份 migration,裡面寫了什麼。
功能做完了,接下來可以用 Day 16 學的 Postman(或是直接請 AI 嘗試),跳過畫面,直接呼叫自己的 API。這次我請 Codex 直接測試了一輪:
POST http://localhost:3000/api/data-reports
標頭:Content-Type: application/json
{
"albumId": "armageddon",
"versionId": "superbeing",
"message": "Superbeing 版本的小卡數量可能有誤,請協助核對。",
"website": "",
"formStartedAt": 開啟表單的時間
}
website 就是 4.2 說的隱藏欄位,正常使用者看不到,所以會是空的;formStartedAt 是開啟表單的時間,用來判斷是不是瞬間送出。用 Postman 測試時,這兩個欄位也要一起帶上,不然會先被這兩關擋下來。
| 測試 | 狀態碼 | 回應 |
|---|---|---|
| 正常送出 | 201 | 成功保存,Telegram 收到通知 |
| 送空白的內容 | 400 | 請至少輸入 1 個字。 |
| 送超過 50 字 | 400 | 回報內容不可超過 50 個字。這就是繞過前端輸入框的實驗 |
| 送不存在的專輯 id | 400 | 找不到這張專輯。 |
| 送不屬於這張專輯的版本 id | 400 | 這個專輯版本不存在。 |
| 10 分鐘內送出一模一樣的內容 | 409 | 這份回報剛剛已經送出,不需要重複提交。 |
用公開金鑰直接寫入 Supabase 的 data_reports |
401 | permission denied for table data_reports |
| 一小時內送第 6 則 | 429 | 回報次數過多,請一小時後再試。 |
| 故意把 Telegram token 填錯 | 201 | 回報照樣保存;通知狀態記成失敗,原因是「Telegram returned HTTP 401」 |
幾個值得注意的地方:
每一種錯誤,都有不同的狀態碼。 400 是送來的內容有問題,409 是跟已經存在的資料衝突,429 是送太頻繁,401 是沒有權限。昨天的那張狀態碼表,在這裡全部派上用場。
Telegram 失敗,回報並不會跟著不見。 token 填錯時,使用者一樣看到「已收到回報」,資料庫也記下了失敗的原因:Telegram 回了 401。這個 401 跟 Day 16 打 Supabase 時看到的是同一個意思:你還沒證明自己是誰,只是這次被拒絕的,是我們的伺服器。
使用者送出回報
│
▼
/api/data-reports 檢查內容 ──── 不通過 → 400 / 409 / 429
│ 通過
▼
① 存進 data_reports ─────────── 失敗 → 500,請使用者稍後再試
│ 成功
▼
② 通知 Telegram
├── 成功 → 通知狀態記成 sent
└── 失敗 → 通知狀態記成 failed,並記下原因
│
▼
回 201:已收到回報(不管 ② 成功或失敗)
① 是主要流程,失敗了,回報就真的沒送出去,所以要讓使用者知道。② 是附加流程,失敗了,回報其實已經存好了,只是我沒收到通知,所以不該讓使用者重送。主要流程和附加流程,失敗時的處理方式可能會不一樣。
專輯不存在,回的是 400,不是 404。 AI 的理由是:/api/data-reports 這個網址本身是存在的,錯的是送來的內容裡,指向了一張不存在的專輯。如果把它看成「查詢一張專輯」,404 也說得通,但這是一支送出表單的 API,所以回 400。這呼應了 Day 16 的結論:狀態碼怎麼回,是設計 API 時的一個決定。
測試資料,記得清掉。 Codex 測完之後,自動把這次產生的 5 筆測試回報全部刪除了。這一步有時候會忘,但測試資料留在正式的資料表裡,之後就分不清哪些是真的回報。也因為限流是數資料表裡的回報,刪掉之後,限流的次數也跟著重置。

【圖3|Codex 修改過程中自動執行的測試,確實有寫進資料庫】
Codex 測完後主動刪除了這次產生的五筆測試回報。這次它有辨識出自己建立的測試資料,刪除過程並沒有問題。但這邊要注意:就算是清理測試資料,只要操作的是共用或正式資料庫,也應該先確認刪除範圍。 AI 的工作不只是「完成」,還包含它認為完成之後應該順手處理的事。這些順手做的事,有時反而更值得檢查。
有些人會將測試環境跟正式環境的資料表分開,這樣的習慣也蠻好的,可以確保測試過程產生的資料不會污染正式環境的資料。

【圖4|Codex 刪除資料庫中測試資料過程中,有詢問是否確認刪除】
如果是自己用 Postman 測的時候要注意:限流是每小時 5 則,重複內容 10 分鐘內會被擋。測到一半一直被擋,先確認是不是被自己的限流擋住了,而不是功能壞了。

【圖5|專輯詳情頁面多一個回報問題按鈕】

【圖6|點擊會出現回報問題視窗】
(像這邊我實際測試,發現送出回報的按鈕,並沒有 hover 變色的效果,會是我後續想調整的地方。)

【圖7|隨意輸入送出後,出現回報成功】

【圖8|資料庫有寫入回報資料】

【圖9|Telegram 也有收到回報訊息】
還沒做的事
這幾項 AI 有建議,但這次先不做:
- 機器人驗證(例如 Cloudflare Turnstile):限流擋得住手動亂送,擋不住大量換來源的機器人。等網站上線、真的遇到可以考慮再加
- 通知失敗的重試:目前只會記錄「失敗」,還不會自動重送。
- 回報管理介面:現在要看回報狀況,得去 Supabase 後台翻資料表
今天這條路,其實很多產品都有:
| 產品 | 使用者送出 | 後端檢查 | 存起來 | 通知 |
|---|---|---|---|---|
| 聯絡表單 | 姓名、問題 | 必填、長度、防機器人 | 訊息表 | 寄信給客服 |
| 點餐 | 餐點、數量 | 餐點存在、有沒有賣完 | 訂單表 | 通知廚房 |
| 預約 | 日期、時段 | 時段有沒有被訂走 | 預約表 | 通知店家 |
不管是什麼產品,順序都一樣:收進來、檢查、存起來、再通知。 而且檢查一定要在後端。
今天做的事,其實是我個人在 Vibe Coding 時代的開發順序:先讓 AI 把需求展開,再由人把它收回來。由 AI 實作,我負責做決定。
AI 很擅長把一個需求拆成前端、後端、資料庫、外部服務,也很擅長把該注意的事列出來。但要用哪一種通知、收不收個人資料、哪些預設可以接受,還是要由人決定。而要做這些決定,得先看得懂它在做什麼。
不過,今天的回報,不需要知道你是誰。任何人都能送,這個功能不要求使用者登入,也不收聯絡資料。但是如果像收藏專輯,就不一樣了。如果要讓每個人只看到自己的收藏,網站就得先回答一個問題:你是誰? 這時候就需要來實作登入功能了。
我們明天見。