稽核與回復
問題
「這個欄位上週不是這個值,誰改的?」
大多數系統的稽核記錄只能回答一半:有時間、有操作者、有「更新了單位 U-1042」, 但沒有改了什麼,更不用說改回去。
做法
1. 結構化的 before / after
稽核記錄不是一行字串,是結構:
{
action: "update",
target: { kind: "customer", id: "U-1042" },
scenario: "tier-upgrade", // 情境(若走情境化申請單)
reason: "年度累計量達標,依規則升等",
before: { tier: "silver", creditLimit: 1_000_000 },
after: { tier: "gold", creditLimit: 1_500_000 },
restoreKind: "customer", // 有這個才可回復
}
before/after 直接來自變更摘要用的同一份 Change[]——
使用者送出前看到的,就是稽核記錄呈現的。
2. 可回復 vs 不可回復,用資料本身判定
有 before/after + restoreKind → 可回復
沒有 → 不可回復
| 操作類型 | 可回復? | 為什麼 |
|---|---|---|
| 逐欄更新 | ✅ | 有明確的前值 |
| 整份清單更新 | ✅ | 存整份快照即可 |
| 新增 | ❌ | 「回復新增」語意是刪除,那是另一個動作 |
| 刪除 | ❌ | 連鎖影響難以還原(相關單據怎麼辦?) |
| 批次操作 | ❌ | 部分成功時無法定義「還原到哪」 |
| 確認/解鎖 | ❌ | 那是狀態轉換,不是資料變更 |
✅ 這樣做
用資料結構判定能不能回復,讓 UI 自動決定要不要顯示「回復」鈕。
🚫 不要這樣
不要維護一份「哪些動作可回復」的硬編清單。 加了新動作忘記更新清單,就會出現按了沒反應的按鈕。
3. 回復本身也要留痕
回復動作另記一筆稽核,並標註 restoredFrom: <原記錄 id>。
否則資料會神秘地變回去,而記錄裡看不出來。
4. 回復也受鎖定守衛
已鎖定期間的資料不能被回復——硬鎖定沒有例外, 「回復」不是繞過鎖定的後門。
取捨
代價:儲存量。 每次更新都存 before/after,資料量是「只存操作記錄」的數倍。
實務上這個代價很小(欄位級的 diff 本來就不大),而且可以只對敏感實體開啟。 真正的成本是每個 mutation 都要記得寫——所以要把它做進共用的更新函式,不要靠自律。
反例
✅ 這樣做
稽核記錄要能回答問題:誰、何時、改了哪個欄位、從什麼變成什麼、為什麼。
🚫 不要這樣
不要記「使用者 A 更新了資料」。這種記錄唯一的功能是讓稽核報告有東西可以印。