Skip to content

操作日志与变更记录

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_datatext (JSON)操作前的实体快照,DELETE 后该字段有值
after_datatext (JSON)操作后的实体快照,来自响应体 data;DELETE 时通常为 null

前端 Diff 展示效果

当操作日志记录了 beforeDataafterData 时,日志详情弹窗会展示 DiffTable 组件:

  • 每行代表一个字段,列出字段名、变更前的值、变更后的值
  • 有差异的行高亮显示(黄色背景)
  • beforeData 有值时(DELETE 操作),展示被删除前的数据快照
  • 两者都有值时(PUT 操作),展示完整的字段变更对比

如何为新路由添加 Diff

前提条件

路由必须使用 guard 中间件(CRUD 路由已默认使用)。

步骤

1. 导入 setAuditBeforeData

typescript
import { guard, setAuditBeforeData } from '../middleware/guard';

2. 在写操作前,查询当前实体并注入快照

在 PUT / DELETE handler 中,通过验证后、执行数据库写操作之前执行:

typescript
// 查询操作前的实体状态
const [before] = await db
  .select()
  .from(yourTable)
  .where(eq(yourTable.id, id))
  .limit(1);

if (before) {
  // 如有敏感字段(如 password),先排除
  const { password: _pw, ...safeBefore } = before as any;
  setAuditBeforeData(c, safeBefore);
}

3. 返回操作后实体(afterData 由中间件自动提取)

handler 正常返回 { code: 0, data: updatedEntity } 即可,guard 中间件会自动从响应体的 data 字段提取 afterData

typescript
const [updated] = await db
  .update(yourTable)
  .set({ ...updateData, updatedAt: new Date() })
  .where(eq(yourTable.id, id))
  .returning();

return c.json({ code: 0, message: 'success', data: updated });

DELETE 操作

DELETE 接口的 afterData 通常为 null(响应 datanull),这是预期行为。前端 DiffTable 会仅展示删除前的数据快照。

typescript
// DELETE handler 示例
const [before] = await db.select().from(yourTable).where(eq(yourTable.id, id)).limit(1);
if (before) {
  setAuditBeforeData(c, before);
}

await db.delete(yourTable).where(eq(yourTable.id, id));
return c.json({ code: 0, message: 'success', data: null });

排除敏感字段

在注入 beforeData 前,应主动将敏感字段从快照中排除:

typescript
// 排除 password 和其他敏感字段
const { password: _pw, secretKey: _sk, ...safeRecord } = record as any;
setAuditBeforeData(c, safeRecord);

查询操作日志

操作日志通过 GET /api/operation-logs 接口查询,支持按用户名、模块、操作路径、请求方法、IP 地址、时间范围等多维度筛选。

详见 操作日志 API

Built with VitePress for local documentation preview.