审计日志必须与业务操作原子性一致,需共用同一数据库连接、显式指定$connection、用db::transaction包裹写入、观察者中用getchanges()记录差异,并监听restored事件。

审计日志必须与业务操作原子性一致,否则“写了日志但数据没改”或“数据改了但日志丢了”都会破坏审计可信度。
audit_log 表必须和业务表共用同一数据库连接
很多团队把 AuditLog 模型放在默认 mysql 连接,但业务模型用了 tenant 或 reporting 连接,结果事务提交时只刷了业务表,审计日志被丢在另一个连接里——它根本不在事务作用域内。
- 检查
AuditLog模型是否显式声明了$connection = 'mysql'(或与业务表一致的连接名) - 不要依赖
config/database.php的默认设置,每个模型都应明确指定 - 若用多租户,
AuditLog通常需写入主库(非租户库),此时必须确保该连接支持事务(比如不能是 SQLite 内存库或只读从库)
用 DB::transaction() 包裹业务 + 日志写入
别依赖模型事件自动触发日志——updated 回调发生在事务中间,但日志写入若失败,事务不会自动回滚;而 DB::transaction() 能强制二者绑定。
- 手写事务块:先
$user->update($data),再AuditLog::create([...]),最后DB::commit() - 用闭包形式更安全:
DB::transaction(function () use ($user, $changes) { $user->update($data); AuditLog::create([...]); }); - 避免在事务块内调用
dispatch(new SomeJob()),队列任务会脱离当前事务上下文
logMessage() 不是万能的,队列任务脱敏要分两层做
队列任务失败时,Laravel 会把整个 $job->data 序列化进 failed_jobs 表和日志文件。仅重写 logMessage() 只影响 artisan queue:work 控制台输出,不影响数据库存储内容。
- 第一层:在
failed()方法里手动清理$this->data敏感字段,再调用parent::failed($exception) - 第二层:重写
logMessage()控制控制台日志显示,返回脱敏字符串,例如sprintf('UpdateProfileJob for user #%d', $this->userId) - 禁止在
logMessage()中访问未序列化的属性(如$this->user->email),反序列化后可能为null或抛出SerializationException
observer 里记录变更必须用 getChanges(),别信 $model->toArray()
用户改了一条记录,但 $model->toArray() 返回的是当前最新值,不是“这次改了什么”。审计日志的核心价值在于差异,不是快照。
- 在
updated观察者方法中,优先用$model->getChanges()获取真实变动字段 - 配合
$model->getOriginal('email')拿旧值,$model->email拿新值,组成['old' => 'a@b.c', 'new' => 'x@y.z'] - 如果模型启用了
casts(如'status' => 'boolean'),getChanges()返回的是转换后的值,无需额外处理 - 软删除恢复(
restored)事件也要监听,否则“删掉又找回”的操作会漏记
最常被忽略的点是:事务提交后、响应返回前,还有一次机会写日志——比如异步通知、缓存刷新。这些操作若失败,审计日志却已落库,就会造成事实偏差。真要保原子性,就得把它们也拉进事务,或者换用最终一致性方案(如发事件+补偿任务),不能假装它们不存在。











