觸發場景:POS App 的前端為後端事件建立本地追蹤記錄,記錄裡存了事件當下的兩個後端 id(單據 id、明細列 id),後續的取消、追加操作用這兩個 id 回寫。後端執行「合併兩張單據」後,這些操作全部失效。 疑問來源:合併後前端已把記錄「改掛」到新單據——為什麼還是壞? 整理目的:把「跨邊界參照的生命週期」整理成可判準的問題清單:哪些 id 會死、哪個 id 不死、持有端該怎麼設計。 本文邊界:測試層的對策(語意級假後端)另見 T.C5 凍結參照失效被 stub 遮蔽


1. 事故解剖:兩層 id、兩種命運

後端「合併兩張單據」的實際行為(埋 log 實測,非文件推理):

資料合併時的命運對前端凍結參照的意義
單據建新單據、刪舊單據凍結的單據 id 死亡
明細列全部重建(全新 id)凍結的明細 id 死亡
事件記錄保留,只改外鍵事件 id 是唯一跨合併不變的身份

第一版修復只處理了單據層(合併後通知本地記錄改掛新單據 id),明細層的 id 照樣死——因為修復者的心智模型裡「合併」是搬家,而後端的實作是重生。同一個業務動詞,兩端對「什麼保留、什麼重建」的理解不同,這正是跨邊界參照最危險的地方。

2. 判準:每一個跨邊界參照都要回答三個問題

前端(或任何下游系統)持有上游資料的 id 時,這個參照的有效性完全由上游的行為決定。設計時逐一回答:

  1. 對方的哪些操作會讓這個 id 死亡?(合併、拆分、覆蓋式更新、重建式修復——任何「刪舊建新」的實作都是 id 屠宰場)
  2. 有沒有一個跨越這些操作仍不變的身份?(本案是事件記錄 id——後端只改它的外鍵)
  3. 這個「不變」是文件說的、推理出的,還是實測證實的?——只有第三種算數。上游把「更新」實作成「刪除重建」是實作自由,下游的推理攔不住。

第二題的答案決定架構:存在穩定身份 → 以它為錨、其餘參照動態解析;不存在 → 凍結參照只能當一次性用途,跨操作的功能要改走查詢。

3. 模式:穩定身份為錨、活解析為主、凍結值為退路

修復後的形態:

  • 本地記錄仍凍結事件當下的 id(顯示用途夠用,也是最後退路)
  • 需要回寫的操作(取消、追加)不信任凍結值——以穩定身份(事件 id)向當前快照反查「現在有效的單據 id+明細 id」
  • 查無(同步空窗、資料已消失)才退回凍結值,且下游有存在性檢查兜底——id 是 uuid,失效的凍結值只會被擋下,不會誤中別的資料

這個模式的通用名字是「reference by identity + 解引用時機延後」:把「id → 實體」的解析從持有時延後到使用時,讓解析結果永遠反映上游當前的真相。額外的紅利是跨端韌性——即使身份轉移發生在本端不知情的時候(另一台裝置觸發合併),使用時解析照樣找得到新家。

4. 邊界情況:凍結有凍結的正當用途

不是所有凍結參照都該消滅。同一個專案裡,「已結帳品項」的商品資訊就是刻意凍結的快照——結帳當下的名稱與價格,本來就不該隨商品目錄後續的修改而變。判準的分水嶺:

用途正確形態
呈現歷史事實(結帳當下的價格)凍結快照——上游變動不該影響它
回寫當前狀態(取消、追加)活解析——必須命中上游當前的資料

同一筆記錄可以同時包含兩種欄位;出事的專案往往是把「顯示用的凍結快照」順手拿去做「回寫用的定位」。

5. 可複用的判準

  1. 跨邊界持有的每個 id,設計時回答:誰會殺它、什麼不會死、證據是實測還是推理。
  2. 回寫操作以穩定身份使用時解析;凍結值只做顯示與最後退路。
  3. 上游動詞的「保留 vs 重建」語意要實測一次、固化為測試資產(假後端+真實後端驗證),不要留在人的記憶。
  4. 區分快照欄位的用途:呈現歷史 → 凍結正確;定位回寫 → 凍結是地雷。

下一步