生效日版本化設定
問題
系統參數改了,歷史資料要不要跟著變?
假設運費費率從 3% 調成 3.5%。如果報表直接讀「目前的費率」重算, 那上個月已經結案的數字會突然變了。使用者拿去對帳的那份報表,明天就對不起來了。
這個 bug 有兩個特徵:發生時很安靜,發現時很難查。
做法
參數不是一個值,是一串帶生效日的版本。
type Versioned<T> = { effectiveFrom: string; value: T }[];
// 取某個期間該用哪一版:「生效日 ≤ 該期間」的最後一版
function resolve<T>(versions: Versioned<T>, period: string, fallback: T): T {
const hit = versions.filter((v) => v.effectiveFrom <= period).at(-1);
return hit?.value ?? fallback;
}
版本清單 2024-01 起 3.0%
2024-07 起 3.5%
2024-03 的報表 → 解析到 3.0% ← 歷史不被污染
2024-08 的報表 → 解析到 3.5%
三條實作規則
1. 計算層不吃版本清單
✅ calculate(input, { feeRate: 0.03 }) ← 已解析的值
🚫 calculate(input, { feeRateVersions }) ← 讓計算層自己去挑
計算層保持純函數、只吃已解析的值。這樣它可以被單獨測試, 也不會因為版本邏輯改了就要重測所有計算。
2. 所有消費端一律依期間解析
報表、試算、匯出、圖表——每一個地方都走同一個 resolve()。
只要有一處直接讀「最新值」,就會出現「這張報表跟那張對不起來」。
3. 就地編輯要有守衛
✅ 這樣做
要改參數 → 新增一個生效版本,生效日必須晚於最後一個已鎖定期間。
🚫 不要這樣
不要直接改最新版本的值,如果它的生效區間已經涵蓋了已鎖定的期間。 那等於偷偷改寫歷史。
UI 要主動導向正確做法:偵測到使用者想改一個「已被歷史引用」的版本時, 不是擋下來就算了,而是引導他去「新增生效版本」。
取捨
代價:概念變複雜。 使用者要理解「這個設定有版本」。
所以不是所有設定都值得版本化。判斷標準:
| 設定 | 要版本化? |
|---|---|
| 會進入歷史計算結果的(費率、稅率、係數) | ✅ 必須 |
| 只影響當下顯示的(頁面大小、預設排序) | ❌ 不必 |
| 影響未來行為但不回溯的(通知規則) | ❌ 一般不必 |
版本化錯了東西,只是讓使用者多按幾次;不版本化該版本化的東西,是資料正確性問題。
反例
✅ 這樣做
提供「相容鏡像」:settings.feeRate 永遠等於最新版本的值,
讓不在意歷史的地方(例如新增畫面的預設值)可以直接用。
🚫 不要這樣
不要為了「乾淨」而強迫所有讀取都走 resolve()。 90% 的地方只需要最新值,多繞一層只會讓程式碼更難讀。