一段修改狀態的程式,誰能改?什麼狀態能改?哪些入口可能繞過檢查直接改?
假設公司的規則是:商品交付物流前可以取消,交付後不能取消,但符合條件時可以申請退貨。以一張整批出貨的出貨單來看,可以記錄待出貨、已出貨與已取消;退貨另外建立單據,追蹤申請、收貨與驗收進度。
規則很清楚,取消功能寫起來也不難:
shipment.Status = ShipmentStatus.Cancelled;
用Enum列出狀態,能讓選項清楚,卻不會阻止已出貨的單據被改成已取消。規則寫在哪裡、在什麼時候檢查,決定了它能不能真的守住。
先弄清楚每個狀態代表什麼
取消代表停止原本的出貨。商品送出後再申請退貨,則要保留出貨紀錄,繼續追蹤商品是否退回。如果直接把出貨單改成已取消,後續程式就可能把「曾經出貨、正在退回」當成「沒有出貨」。即使另有異動紀錄,出貨單目前的狀態仍然表達錯了。
因此,「已出貨,而且正在退貨」可以同時成立;執行取消的程式,則必須先確認目前仍是待出貨,而不只是接受一個新的狀態值。
畫面擋住了,後端仍要檢查
已出貨時,停用取消按鈕並說明原因,可以讓使用者知道目前不能操作,不必按下去才收到錯誤。
但後端不能把畫面檢查當成保證。使用者開啟畫面時可能還是待出貨,等到按下取消,倉庫已經完成交付。其他程式也可能直接呼叫同一支API,根本沒有經過這個畫面。
後端收到取消要求時,必須依目前資料確認能不能執行;若不能,就拒絕修改並回傳原因,讓畫面更新資訊。前後端看起來都有判斷,卻各有用途:畫面幫助使用者操作,後端確保不能取消的單據不會被取消。看到重複條件時,要先理解用途,不能直接把其中一邊刪掉。
每一條寫入路徑,都要經過同一份規則
後端有了檢查,還要確認所有改狀態的地方都會經過它。搜尋取消方法的呼叫位置,只能找到已經使用它的程式;如果還有舊批次直接更新出貨狀態,就不會出現在這份清單裡。因此也要搜尋狀態欄位與資料表的更新位置,確認是否有相同操作走了另一套處理。
找到不同寫法後,也別急著全部改成共用。取消出貨與申請退貨都會改狀態,但條件與後續處理不同。真正需要一致的是相同的業務規則,不能只因為都在更新狀態,就硬塞進同一個方法。
規則要在寫入的那一刻成立
最後還有時間點的問題。取消與確認出貨幾乎同時執行,兩邊都先讀到「待出貨」,也都通過檢查。如果之後各自直接寫入,就可能一邊寫入已取消、另一邊寫入已出貨,最後還各自回報成功。資料庫只留下後一次寫入的狀態,客服卻可能已經看到取消成功,出貨流程也繼續往下走。
這類問題不需要很大的流量,只要兩個操作剛好重疊就可能發生。因此,檢查不能和寫入分開:例如更新時一併指定「目前狀態仍是待出貨」才寫入;沒有更新到任何資料,就代表狀態已經被別人改掉,取消應該回報失敗。這和庫存預留的道理相同,條件要在寫入的同一個動作裡確認。
狀態規則要守得住,得放在每條寫入路徑都會經過的地方,並在寫入的那一刻確認仍然成立。
兩位室友出門前,各自打開冰箱看了一眼,都發現牛奶喝完了,於是各買一瓶回家。
「我出門前看冰箱沒有牛奶啊。」
「我也是。」
![]()