現在老闆打進這個 App 的每一筆訂房,都是餵給廣告引擎的真實轉換事件。也就是說,只要資料錯了、或不見了,下游的優化就是在對著垃圾做最佳化。
這篇跟〈當輸入訂房資料的人已經 70 歲〉講的是同一個專案,但切入點不同。那篇講的是讓老闆願意每天記帳的 UX 決定。這篇講的是那層 UX「底下」必須先成立的所有東西——在它產出的資料能被信任之前,得有備份、安全部署、測試隔離,還有一套不讓一筆打錯的訂房污染整個共用日曆的完整性規則。
資料一夜之間從「有就好」變成「不能倒」
這個記錄工具大半輩子裡,弄丟一筆訂房紀錄只是煩人。老闆可以從 LINE 對話或紙本筆記重建。但當這些紀錄變成餵給廣告流程的轉換訊號——當「這筆訂房成立了」變成廣告要去最佳化的目標——弄丟或弄錯一筆紀錄,就不再只是煩人,而是悄悄污染了下游每一個決策。
這個重新定義,帶出了一串表面上看起來很不起眼的水電工程。沒有一項是老闆看得見的功能。但每一項都關乎一件事:你敢不敢拿一筆廣告預算押在這份資料上。
兩層備份,因為只有一層不算備份
我先加的是每日備份流程:一個排程的 GitHub Action,把四張核心表——訂房、房源、業者、成員——直接從正式環境的 Supabase REST API 拉出來,上傳成保留 90 天的 artifact。
90 天的 artifact 對付「昨天有人手滑刪掉一筆」很夠用。但對付「三月的資料長什麼樣」就完全沒用。Artifact 會過期。所以每個月一號,同一個流程會把一份快照 commit 到專門的 backups 分支——如果分支還不存在就用 orphan 方式建立——這樣就有了永久、可以 diff、永遠留在 git 裡的每月檢查點。
這個切分很重要,因為兩種出錯方式是不同的。每日 artifact 防的是近期的操作失誤。每月的 git 快照防的是慢性污染——一個悄悄亂改紀錄好幾週的 bug,那種你只有在某個數字看起來不對、需要知道從哪天開始壞的時候才會發現的問題。一層蓋的是「把昨晚還原回來」,另一層蓋的是「這到底從什麼時候開始壞的」。兩者誰都取代不了誰。
operator_members 這張表是後來才補進備份清單的——一張平常幾乎是空的表很容易忘掉,直到哪天你需要還原「誰當初有哪些存取權限」。
一個拒絕逞英雄的部署腳本
現在正式環境的部署是一套五步流程:動任何東西之前先備份、跑 migration、部署程式、最後驗證——任何一步失敗就立刻停下,說明原因。前一步沒確認乾淨,下一步絕不往下走。
這裡真正有意思的出錯點不是部署本身,是 migration 那一步。最早的版本會掃整個 supabase/ 目錄底下所有的 .sql 檔,全部都跑。但那個目錄裡也放了 schema.sql 跟一個 dev 環境的建置腳本——那是用來把一個空資料庫長出全套表的完整定義,不是拿來對著有真實資料的正式環境跑的。把一個盲目「全部 SQL 都跑一遍」的迴圈指向那個目錄,就是你不小心拿 schema 建置腳本去重跑一張已經有資料的正式表的方式。
修法是只從 supabase/migrations/ 讀 migration——這個目錄裡放的剛好就是那些增量、可重複執行的變更,不含任何重建結構的東西。意圖上是一行的改動,但影響範圍上是有份量的:部署工具現在實體上就碰不到那些有破壞性的建置腳本。
Dev 環境一直在寫正式日曆
每一筆訂房都會在老闆共用的 Google 日曆上建一個事件——這就是一個不懂技術的老闆實際「看見」自己訂房狀況的方式,就在他手機上本來就會看的那個 App 裡。這在我架起一個獨立的 dev 環境時就出問題了:本機的測試訂房,把測試事件寫進了正式的家族日曆。
我用兩層解掉。第一層是一個 SKIP_GOOGLE_CALENDAR 旗標,只設在不進 git 的本機環境檔裡,在三支訂房 API——新增、編輯、刪除——都把日曆寫入短路掉。正式環境永遠不設它,行為完全不變。
然後我再修細一點。完全跳過日曆寫入,意味著我在 dev 根本沒辦法測到日曆那段程式。所以我又加了兩個覆寫:一個讓 dev 指向一個獨立命名的測試日曆,一個強制 dev 事件用石墨灰色。現在 dev 會完整跑過真正的 Google Calendar API,對著一個隔離的日曆,而且每個測試事件在視覺上都標成「這不是真的」。這就是「dev 把日曆程式關掉」跟「dev 完整測過日曆程式但完全不碰正式環境」的差別——而你要的是後者。
那條資料庫表達不出來的完整性規則
業務規則很單純:同一間房源,一筆包棟不能跟另一筆包棟在日期上重疊。同樣那幾晚,你不能把整間賣兩次。
這條規則寫在新增跟編輯兩條路徑的應用程式碼裡,用一段重疊查詢——checkin < newCheckout AND checkout > newCheckin——衝突時回 409,錯誤訊息裡直接把衝突的日期寫清楚。編輯路徑會把正在編輯的那筆排除在自己的衝突檢查之外,這是你不處理就一定會撞到的明顯 bug。
讓這件事超出單純檢查的地方在於:編輯表單現在讓老闆可以把一筆訂房換到「不同的」房源——拿來修「把訂房記錯房間」這個常見的失誤。這代表編輯時的衝突檢查得對「目標」房源跑,不是訂房目前所在的房源。一筆訂房在原本位置上沒衝突,搬過去的那一刻可能就撞上了。
搭著這個,我也不再讓 UI 把 API 錯誤吞掉。舊的失敗路徑不管實際發生什麼,都固定顯示「儲存失敗,請再試一次」。現在前端會讀 body.message ?? body.error,把真正的原因攤出來——所以一個 409 衝突會直接告訴老闆是哪一筆現有訂房擋路、日期是哪天,而不是給一個籠統的聳肩。
為什麼這類客戶能拿到這些
這些沒一樣是高難度工程。一個備份流程、一個有護欄的部署腳本、一個環境旗標、一段重疊查詢。值得一提的是這筆帳:一家單獨的民宿業者,永遠不會為了一個訂房 App,去找外包做兩層備份、部署驗證流程、加 dev 環境隔離策略。光是工時就會把價值整個壓垮。
這些之所以存在,是因為同一套加固只寫一次、就套用到這整套系統裡的每一間房源;也因為 AI 把這些水電工程的成本壓到一個程度,讓「把它做對」不再是只有預算充足的客戶才享受得起的奢侈。老闆完全看不見這些。但拿來砸在這份資料上的廣告預算,能有多可信,就只跟那層沒人看見的東西一樣可信。
整個迴圈撐不撐得住,最後還是看一個人類習慣:老闆每天把每一筆訂房記下來。水電工程修不了這件事——它只能保證,當這個習慣撐住的時候,這份資料配得上那份信任。