跳至主要内容
💬 回報這一頁

稽核與回復

問題

「這個欄位上週不是這個值,誰改的?」

大多數系統的稽核記錄只能回答一半:有時間、有操作者、有「更新了單位 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 更新了資料」。這種記錄唯一的功能是讓稽核報告有東西可以印。