audit_log表应包含user_id、table_name、record_id、action、old_values、new_values六字段,其中old_values和new_values用JSON类型存储,action需区分soft_delete/restore,created_at用DATETIME(3)并统一UTC时区。

audit_log 表结构怎么设计才够用
直接存 user_id、table_name、record_id、action(insert/update/delete)、old_values 和 new_values 这六个字段,基本覆盖 90% 场景。其中 old_values 和 new_values 必须用 JSON 类型(MySQL 5.7+)或 TEXT(兼容老版本),别用 serialize() —— PHP 反序列化有安全风险,且跨语言难读。
容易踩的坑:
-
record_id别设成自增主键,它得是被审计表的主键值(比如users.id = 123) - 如果业务有软删除,
action建议细分为soft_delete/restore,别全塞进delete - 时间字段用
created_at,类型为DATETIME(3),避免时区混乱(PHP 默认用 UTC 写入,数据库也统一设 UTC)
如何在 Laravel Eloquent 中自动触发审计
别在每个 Model 的 saved、deleted 事件里手写日志逻辑 —— 重复、难维护、漏覆盖。用 Trait + 观察者组合更稳:
定义 Auditable Trait,内含 getDirtyFields() 方法,对比 $this->getOriginal() 和 $this->getAttributes(),过滤掉 updated_at、deleted_at 等系统字段;再在 boot() 中注册观察者:
立即学习“PHP免费学习笔记(深入)”;
protected static function boot()
{
parent::boot();
static::updating(function ($model) {
$model->logAudit('update');
});
static::created(function ($model) {
$model->logAudit('insert');
});
static::deleted(function ($model) {
$model->logAudit('delete');
});
}
关键点:
- 注意
static::restored()要单独监听软删除恢复场景 - 如果模型用了
casts(如'status' => 'boolean'),getOriginal()返回的是原始 DB 值(1/0),而getAttribute('status')是转换后值(true/false),审计日志应记录原始值,否则回溯时语义失真 - 大字段(如
content、description)建议默认不进日志,用白名单控制:protected $auditInclude = ['name', 'email', 'status'];
非 Laravel 项目怎么低成本接入
没有 ORM?那就靠数据库触发器 + PHP 接收钩子。MySQL 触发器能捕获变更,但不能直接调 HTTP 或写文件 —— 所以折中方案是:触发器把变更写进一张轻量中间表(如 audit_queue),再由定时脚本(php audit-consumer.php)轮询处理。
audit_queue 至少包含:id、table_name、row_id、action、raw_sql(可选)、created_at。PHP 脚本每次取 100 条,查出对应行当前数据(用于生成 new_values),再查历史快照(如果有)补 old_values,最后写入正式 audit_log 并删队列。
注意事项:
- 触发器里别做复杂 JSON 拼装 —— MySQL JSON 函数性能差,且易出错;交给 PHP 做更可控
- 脚本必须加
SELECT ... FOR UPDATE或 Redis 分布式锁,防止多实例重复消费 - 如果业务写入峰值高,
audit_queue表要建好索引:(table_name, row_id, action)复合索引
谁改的?—— user_id 怎么拿才可靠
不能依赖 $_SESSION['user_id'] 或 Auth::id() 全局变量,因为 CLI 脚本、队列任务、API 网关透传等场景下它们可能为空或错乱。真正可靠的来源只有一个:操作发起时明确传入的上下文。
推荐做法是,在业务入口(Controller、Command、Job)显式提取用户标识,并一路透传到 Model 层:
// Controller
$user = Auth::user();
$article->update($data, ['auditor_id' => $user->id]);
// Model 的 update() 方法支持第二个参数传审计上下文
public function update(array $attributes, array $context = [])
{
$this->auditContext = $context;
return parent::update($attributes);
}
这样既解耦又可测。容易忽略的细节:
- API 请求带
X-Forwarded-User或Authorizationheader 时,也要在中间件里解析并注入,不能只认 session - 后台任务(如
php artisan import:users)应强制要求命令行指定--as-user=123,否则拒绝执行 - 如果用 JWT,
auditor_id应来自 token payload,而不是 DB 查询结果 —— 防止 token 未过期但用户已被禁用导致日志归属错误
审计不是加个日志就完事,关键字段的来源链必须可追溯、不可伪造、不依赖运行时环境状态。



















