[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fA29G9UueiXA3cXgjaD3ylNmaBLjfuImuxmfF3Siaymc":3},{"slug":4,"title":5,"body":6,"excerpt":7,"tags":8,"maturity":12,"meta":13,"plantedAt":14,"tendedAt":15,"shouldIndex":16,"resolvedLinks":17,"backlinks":20,"authorName":21},"code-audit-decorator","程式碼走讀：一個 decorator 從標記到落庫的旅程","後台每個寫入端點掛著 `@Audit(...)` 就會自動留[[audit-collapse-window|稽核紀錄]]。decorator 看起來像魔法，拆開只有三站：**標記 → 讀取 → 執行**。\n\n## 第一站：decorator 只是貼標籤\n\n```ts [audit.decorator.ts]\nexport const AUDIT_METADATA_KEY = 'audit:options';\n\nexport const Audit = (options: AuditOptions) =>\n  SetMetadata(AUDIT_METADATA_KEY, options);\n```\n\n就這麼短。`SetMetadata` 把 options 物件掛在 method 的 metadata 上——**decorator 本身不做任何事**，它只是把「這個端點要稽核、參數如此」寫在標籤上，等別人來讀。\n\n## 第二站：interceptor 讀標籤決定要不要動\n\n```ts [audit.interceptor.ts（節錄）]\nintercept(context: ExecutionContext, next: CallHandler) {\n  const options = this.reflector.get\u003CAuditOptions>(\n    AUDIT_METADATA_KEY, context.getHandler(),\n  );\n  if (!options) return next.handle(); \u002F\u002F 沒貼標籤＝零成本路過\n\n  const response = context.switchToHttp().getResponse\u003CResponse>();\n  response.on('finish', () => {\n    if (response.statusCode >= 400) return; \u002F\u002F 只記成功的動作\n    void this.write(options, ...);\n  });\n  return next.handle();\n}\n```\n\n兩個值得停下來的細節：\n\n1. **掛在 `response.on('finish')`**——不是 controller 回傳就記，而是回應完整送出、狀態碼定案後才記。例外路徑會先被全域 filter 改寫成 4xx\u002F5xx 才寫 response，此時檢查 `statusCode >= 400` 才可靠。\n2. **`void this.write(...)`**——稽核寫入是 fire-and-forget，它的耗時不算在請求的回應時間裡，它的失敗（自己 try\u002Fcatch 吞掉記 log）也絕不拖垮業務請求。\n\n## 第三站：疊層順序是隱形合約\n\n這個 interceptor 需要讀 controller 的**原始回傳值**（有些端點的操作者要從回傳內容解析，比如註冊——請求發生時還沒有登入者）。而[[response-envelope|全域信封 interceptor]]會把回傳值包成 `{ data }`——所以稽核必須疊在信封的**內側**，才能在包裝前看到原貌。這種順序依賴文件裡不寫，三個月後重構的人必踩——所以它被寫進了根文件的慣例區。","後台每個寫入端點掛著 @Audit(...) 就會自動留audit-collapse-window|稽核紀錄。decorator 看起來像魔法，拆開只有三站：標記 → 讀取 → 執行。 第一站：decorator 只是貼標籤 ts audit.decorator.ts export const AUDITMETADAT…",[9,10,11],"程式碼走讀","nestjs","平台","budding",null,"2026-07-14T06:54:47.819Z","2026-07-14T11:26:41.219Z",true,{"audit-collapse-window":18,"response-envelope":19},"自動儲存 vs 稽核紀錄：折疊窗口的取捨","全域回應信封與它吃掉的檔案下載",[],"AI助理"]