iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0

Day 9

Day 8 處理的是餘額:當一個數字會持續被交易改變,就不能只保存最後結果。

菜單也有類似問題,但麻煩的地方不太一樣。

今天看到的「雞排飯 100 元」,下個月可能變成 105 元;某個品項可能停售,也可能多出半份、加量等變體。店家名稱還可能經過整理,歷史資料裡又保留著舊代碼。

這裡的餐點名稱、價格與 Vendor A 範例都是為了說明版本衝突而簡化的示意,不代表某一筆實際 Production 訂單。

如果系統永遠只維護一份最新版菜單,更新很簡單。但一旦有人回頭看舊訂單,就會碰到一個很實際的問題:

現在的菜單可以被整理,過去那筆訂單當時看到的菜單不能跟著被改寫。

Menu History 的責任,是讓店家、餐點、價格、variant 與生效時間組成可追溯的版本,而不只是多存幾張菜單圖片。


Day 5 已經把菜單變成資料,Day 9 要處理的是「資料會變」

單一店家時,菜單很容易被當成一份固定設定。

Day 5 已經談過,當店家增加、價格改變、開團與截止條件開始不同,原本藏在環境裡的常數就會變成 Domain Data。

但把菜單存進資料庫,只解決了一半。

例如某個品項今天是:

Vendor A
A01
招牌便當
100

下週改成 105 元時,最直覺的做法是直接把 price 更新成 105。

對「現在要顯示什麼」來說,這完全合理。

對歷史訂單來說卻不一定合理。因為兩週前下單的人,當時看到的是 100 元。如果舊訂單畫面重新去查現在的 menu item,就可能把過去重新解釋成 105 元。

問題已經不只在 UI 顯示,現在狀態開始污染歷史語意。


一份菜單有三種不同責任

整理這段資料後,我比較傾向把菜單拆成三個問題看。

層次 回答的問題
Current Menu 現在管理者看到、可以維護的是什麼?
Effective Menu 某個指定日期,當時有效的菜單是什麼?
Historical Evidence 過去訂單與舊資料當時實際代表什麼?

這三者看起來很像,但不能完全共用同一種更新語意。

Current Menu 可以接受名稱整理、未來價格調整、品項啟停。

Effective Menu 要根據日期解析當時應該套用哪一版。

Historical Evidence 則更保守:只要已經成為過去交易的一部分,就不應因為今天做了 canonicalization,回頭把舊訂單的意思一起換掉。

這也是這套系統後來把 menu change 做成 append-only history 的原因。新的變更往後加,而不是把舊紀錄改成「現在看起來比較乾淨」的版本。


只靠 item_code,後來也不夠用了

一開始很容易把 item_code 當成餐點身份。

例如:

H1 = 某個餐點
H2 = 另一個餐點

但真實菜單會慢慢出現更多維度。

同一個代碼可能需要區分不同店家;同一個餐點也可能存在 BASE、HALF、PLUS 等不同 variant。這時候只看 item_code,就無法完整回答「這到底是哪一個品項」。

目前 normalized menu 的 identity 會同時考慮:

vendor
+ item_code
+ variant_key

餐點身份因此不能只看一個孤立代碼,必須放在店家與變體脈絡裡判斷。

這個差異很重要。

假設兩家店剛好都有 A01,如果 resolver 只用 item_code 尋找最新版,就可能把 Vendor A 的變更套到 Vendor B。

Vendor isolation 同時影響畫面分類與資料身份。


effective_date 解決「哪一天開始算」,revision order 解決「同一天誰比較新」

只有 identity 還是不夠。

菜單版本還需要回答另一個維度:

這個變更從哪一天開始有效?

每筆 menu change 都有 effective_date。Resolver 查某一天的菜單時,會找出對那個日期已經生效的版本,而不是直接拿資料庫裡最後一筆紀錄。

這讓「今天的菜單」和「下週即將生效的菜單」可以同時存在。

但還有更細的情況:同一個 effective date 可能發生不只一次修正。

如果只靠日期排序,同一天的兩筆 revision 並沒有穩定先後。後來又需要 append-only sequence,讓同日 revision 也有 deterministic winner。

最後一個 menu identity 的解析,實際上比較接近:

Vendor
  ↓
Item Code
  ↓
Variant
  ↓
Effective Date
  ↓
Revision Order
  ↓
Resolved Menu Item

Day 9 Menu History|Vendor / Item Code / Variant / Effective Date / Revision → Current / Historical Evidence

每多一層看起來都增加了一點複雜度,但每一層都在回答不同問題:誰的餐點、哪個變體、從哪天生效、同一天哪次修正最後成立。


v1 不需要假裝自己從一開始就是 v2

這套系統還有一個很現實的條件:Menu History 不是第一天就用現在的模型建立。

早期已經存在一批 legacy menu changes。後來才加入 normalized identity,明確區分 legacy/raw 與 normalized schema,並讓新版支援 variant。

這時有兩種做法。

第一種,是把所有舊資料全部改寫成新版格式,讓資料庫看起來非常一致。

第二種,是承認舊資料就是在舊規則下產生的歷史,保留原 identity,再讓新的 resolver 知道該怎麼把它投影到現在。

這套系統採用的是後者。

既有 legacy row 保留原本身份;normalized v2 從新的邊界開始承擔新的 identity 規則。現在的 projection 可以更乾淨,但不需要假裝過去也是用今天的 schema 產生。

對 migration 來說,這是一個很實用的界線:

Schema 可以往前演進,Historical Evidence 不必被重寫成看起來像從第一天就設計正確。


Canonicalization 也有同樣的邊界

店家名稱整理是另一個例子。

假設歷史上曾經出現不同寫法,現在系統決定統一成 canonical vendor。這可以改善 current UI、搜尋與管理,但不代表要把過去資料全部回寫。

比較安全的做法是:

  • current / future projection 使用 canonical identity;
  • resolver 處理 compatibility;
  • 已形成歷史意義的資料保持不動;
  • 只有明確 review 過的 migration / repair 才能修改歷史。

這裡的重點不是把字串整理得更整齊,而是讓 current projection 與 historical evidence 各自保有清楚責任。


訂單不要重新依賴「現在的菜單」

Menu History 最後還會回到 Order。

假設一筆訂單已經成立,它所代表的餐點、價格與 variant,應該依照交易當時保存下來的語意理解。

不能因為今天菜單改名、改價、停售,就重新用 current menu 解釋舊訂單。

這也是為什麼 current menu resolver 和 historical order evidence 要分開。

Current Menu 可以回答:

今天如果要下單,現在有哪些選擇?

Historical Order 則回答:

那一天,這筆交易當時成立的是什麼?

兩個畫面都可能顯示「菜單」,背後承擔的是不同時間語意。


一個可以直接拿走的 Menu History 判斷框架

如果系統裡的菜單、方案、費率或產品設定會持續改變,可以先問五個問題:

  1. Identity 是什麼?
    只靠 item_code,還是必須包含 vendor、variant 或其他維度?

  2. 何時生效?
    變更是立即覆蓋,還是有 effective date?

  3. 同一時間能不能再次修正?
    如果可以,需要穩定的 revision order。

  4. 歷史交易保存什麼?
    舊訂單是否能在不查 current config 的情況下,仍然知道當時買了什麼?

  5. Normalization 會不會改寫歷史?
    現在的 canonical naming、mapping 或 schema upgrade,是只改 projection,還是會動到 historical evidence?

如果這五題沒有先回答,Menu History 很容易最後只剩「一張 changelog table」,但系統仍然不知道某一天到底該相信哪一版。


不是所有菜單都需要版本系統

如果一個網站只展示今天最新菜單,沒有訂單歷史、沒有未來生效日期,也不需要回看過去價格,那麼直接覆蓋 current menu 可能已經足夠。

版本模型有成本。

它增加 schema、resolver、migration、測試與管理介面的複雜度;一旦加入 effective date 與 revision order,debug 時也不能只看「最後一列」。

是否採用 append-only,要先看系統是否需要同時回答兩個問題:

  • 現在應該看到什麼?
  • 過去當時是什麼?

只要兩個答案可能不同,資料模型就不能只剩一份最新版。


Day 9 留下的不是「菜單版控」

Menu History 最後留下的重點,是同一份 Domain Data 會同時存在 current、future 與 historical 三種時間語意。

菜單因此從「現在有哪些餐點」的一張表,變成一條可以沿著日期與 identity 回看的時間線。

當資料開始有時間,系統就不能再只問:

現在是什麼?

還得能回答:

那一天,它是什麼?


本系列實作專案

這不是純概念示範,而是一套持續開發中的便當訂購系統。文章著重「為什麼」,GitHub 保留實際程式碼、文件與演進紀錄。

GitHub:https://github.com/henryfir456/bento-order-app


下一篇

菜單需要 effective date,已經讓「日期」開始進入資料模型。

但能不能訂某一天,還不只看菜單有沒有生效。工作日、截止時間、Mode A / B、自動開團,都會一起決定那一天是否可下單。

下一篇會沿著這條線,繼續看時間怎麼從一個欄位,變成 Domain Logic。


上一篇
Day 8|餘額一進來,系統就不再只是表單
系列文
從 GAS 到 Cloudflare D1:AI Agent 如何打造、接手並重構真實便當系統9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言