audit_log表created_at必须用DATETIME(3)+UTC,因PHP time()与MySQL NOW()时区行为不一致,需避免TIMESTAMP自动时区转换、禁用DEFAULT CURRENT_TIMESTAMP、强制PHP写入前设UTC并生成毫秒字符串,确保时间线可验证回溯。

时间戳本身不能直接支撑可靠的时间线回溯,必须配合时区统一、原始值捕获、不可变事件存储三者才成立。
audit_log 表的 created_at 字段为什么必须用 DATETIME(3) + UTC?
因为 PHP 的 time() 和 MySQL 的 NOW() 默认行为不一致:前者返回秒级 Unix 时间戳(无时区),后者依赖系统时区设置。一旦数据库和应用时区不同步,日志时间就会漂移,跨服务比对时间线就失效。
- 用
DATETIME(3)而不是TIMESTAMP:避免 MySQL 自动转换时区带来的隐式修改 - PHP 写入前必须强制设为 UTC:
date_default_timezone_set('UTC'),再用date('Y-m-d H:i:s.u')或DateTimeImmutable::createFromFormat()生成带毫秒的字符串 - 绝对不要依赖
created_at DEFAULT CURRENT_TIMESTAMP—— 它会按 MySQL server 时区写入,而你无法控制部署环境的时区配置
为什么 getOriginal() 比 getAttribute() 更适合审计日志?
因为审计要记录「数据被改之前的真实 DB 值」,而不是 ORM 转换后的语义值。比如字段 status 是 TINYINT(1),getAttribute('status') 返回 true/false,但 getOriginal('status') 返回 1/0 —— 后者才是可回溯、可跨语言解析的原始依据。
- 在 Laravel 中,
$model->getOriginal()拿到的是从数据库查出时的原始数组,未经过casts处理 - 如果模型有
protected $casts = ['is_active' => 'boolean'],直接json_encode($model->getAttributes())会把0变成false,导致 diff 失真 - 软删除字段如
deleted_at也得过滤掉:它可能被自动设为NULL或当前时间,不属于业务变更
strtotime('+1 day') 这类写法为什么在审计场景里危险?
因为 strtotime() 是模糊解析器,不保证可逆性。比如 strtotime('2026-03-28 +1 day') 在夏令时切换日可能跳过或重复一小时,导致时间线错位;更严重的是,它默认使用本地时区,而审计要求所有时间点都锚定在 UTC。
立即学习“PHP免费学习笔记(深入)”;
- 替代方案用
DateTimeImmutable:(new DateTimeImmutable('2026-03-28', new DateTimeZone('UTC')))->add(new DateInterval('P1D')) - 所有时间计算必须显式传入
DateTimeZone('UTC'),哪怕只是格式化输出 - 避免用
mktime():参数顺序反直觉(month/day/year),且 PHP 8+ 已废弃 null 参数,容易埋下兼容隐患
事件溯源模式下,时间戳该存在 event 对象里还是外部元数据中?
必须存在 event 对象内部,且类型为 DateTimeImmutable,不能只存 Unix 时间戳整数。
- Unix 时间戳丢失精度(秒级)和时区上下文,无法支持毫秒级操作审计或跨时区回放
- event 类的
getTimestamp(): DateTimeImmutable方法能确保每次调用都返回不可变副本,避免被意外修改 - 存储时序列化用
$event->getTimestamp()->format('Y-m-d\TH:i:s.v\Z'),固定 UTC + 毫秒,不依赖任何运行时环境
真正难的不是记下时间,而是让每个时间点都可验证、可重建、不随环境漂移。UTC 是底线,原始值是基础,不可变对象是保障 —— 少一个,时间线就断一截。



















