操作日志与变更记录
Zenith Admin 的操作日志不仅记录了「谁做了什么」,还支持记录变更前后的实体快照,在日志详情弹窗中以表格 diff 形式高亮展示差异字段。
整体架构
操作请求
│
├──> guard 中间件(before next())
│ 查询当前实体 → setAuditBeforeData(c, beforeRow)
│
├──> 路由 handler
│ 执行数据库写操作,返回 { code: 0, data: afterRow }
│
└──> guard 中间件(after next())
提取响应体中的 data → afterData
将 beforeData + afterData + 其他字段 写入 operation_logs 表| 层 | 文件 | 职责 |
|---|---|---|
| 数据库 | operation_logs.before_data / operation_logs.after_data | 存储 JSON 快照字符串(text 类型) |
| 中间件 | packages/server/src/middleware/guard.ts | 自动提取 afterData;暴露 setAuditBeforeData() |
| 路由 | PUT / DELETE handler | 操作前查询实体,调用 setAuditBeforeData(c, entity) |
| 前端 | OperationLogsPage.tsx 中的 DiffTable 组件 | 解析 JSON、比对字段、高亮变更行(只读,不需维护) |
字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
before_data | text (JSON) | 操作前的实体快照,DELETE 后该字段有值 |
after_data | text (JSON) | 操作后的实体快照,来自响应体 data;DELETE 时通常为 null |
前端 Diff 展示效果
当操作日志记录了 beforeData 或 afterData 时,日志详情弹窗会展示 DiffTable 组件:
- 每行代表一个字段,列出字段名、变更前的值、变更后的值
- 有差异的行高亮显示(黄色背景)
- 仅
beforeData有值时(DELETE 操作),展示被删除前的数据快照 - 两者都有值时(PUT 操作),展示完整的字段变更对比
如何为新路由添加 Diff
前提条件
路由必须使用 guard 中间件(CRUD 路由已默认使用)。
步骤
1. 导入 setAuditBeforeData
typescript
import { guard, setAuditBeforeData } from '../middleware/guard';2. 在写操作前,查询当前实体并注入快照
在 PUT / DELETE handler 中,通过验证后、调用 service 写方法之前执行:
typescript
// 推荐:service 的 ensureXxxExists 同时返回实体交给路由做快照
import { ensureYourRecordExists } from '../services/your.service';
const before = await ensureYourRecordExists(id); // 不存在时抛 HTTPException(404)
// 如有敏感字段(如 password),先排除
const { password: _pw, ...safeBefore } = before as Record<string, unknown>;
setAuditBeforeData(c, safeBefore);若未在 service 中提供
ensureXxxExists,可在路由内临时await db.select().from(yourTable).where(eq(yourTable.id, id)).limit(1)取快照 —— 但仅作为过渡方案,建议后补 service 方法以保持路由薄层。
3. 返回操作后实体(afterData 由中间件自动提取)
handler 正常返回 okBody(updatedEntity) 即可,guard 中间件会自动从响应体的 data 字段提取 afterData:
typescript
import { updateYourRecord } from '../services/your.service';
const updated = await updateYourRecord(id, updateData);
return c.json(okBody(updated, 'success'), 200);DELETE 操作
DELETE 接口的 afterData 通常为 null(响应 data 为 null),这是预期行为。前端 DiffTable 会仅展示删除前的数据快照。
typescript
// DELETE handler 示例
import { ensureYourRecordExists, deleteYourRecord } from '../services/your.service';
const before = await ensureYourRecordExists(id);
setAuditBeforeData(c, before);
await deleteYourRecord(id);
return c.json(okBody(null, 'success'), 200);排除敏感字段
在注入 beforeData 前,应主动将敏感字段从快照中排除:
typescript
// 排除 password 和其他敏感字段
const { password: _pw, secretKey: _sk, ...safeRecord } = record as any;
setAuditBeforeData(c, safeRecord);查询操作日志
操作日志通过 GET /api/operation-logs 接口查询,支持按用户名、模块、操作路径、请求方法、IP 地址、时间范围等多维度筛选。
详见 操作日志 API。