跳转到主内容
新派编程网 - 新潮编程理念,全新技术课堂!

ThinkPHP如何做数据变更日志_ThinkPHP操作记录审计功能【技巧】

在ThinkPHP模型中用afterUpdate事件配合getOrigin()对比新旧值记录变更日志最安全,需过滤敏感字段、避开自动填充字段、异步写入日志,并处理JSON字段序列化及软删除场景。 怎么在 ThinkPHP 模型里自动记录数据变更 直接改
save()
或
update()
的行为最省事,但别动框架源码——用模型事件钩子更安全。ThinkPHP 6+ 支持
beforeUpdate
、
afterUpdate
等生命周期事件,变更日志必须在
afterUpdate
里取新旧值对比,否则旧数据可能已被覆盖。 旧值要用
$model->getOrigin()
拿,不是
$model->getData()
,后者返回当前状态 新值用
$model->getData()
,但注意它包含未修改字段,得和旧值做键值比对 别在
beforeUpdate
里记日志:那时事务还没提交,且
getOrigin()
返回的可能是上一次查询缓存,不准确 批量更新(
where()->update()
)不触发模型事件,这种场景必须手动调用日志逻辑 日志字段设计要避开 ThinkPHP 自动填充陷阱
create_time
、
update_time
这类由框架自动写入的字段,如果也塞进变更日志里,会导致日志膨胀且无业务意义。更麻烦的是,如果你在日志表里也配了
auto_write_timestamp
,会和主表时间戳冲突,查日志时分不清是操作时间还是记录入库时间。 日志表的
created_at
字段建议设为数据库默认
CURRENT_TIMESTAMP
,不走 ThinkPHP 时间戳自动填充 变更字段名用
field_name
存字符串,别用 JSON 存整行,否则没法按字段查历史(比如“谁改过 price”) 别把
id
、
status
这类高频变更字段默认全记——加个白名单配置,只记业务关心的字段 敏感字段如
password
、
id_card
必须过滤,连旧值都不能落库,用
isset($diff['password']) && unset($diff['password'])
显式剔除 事务里写日志失败导致主操作回滚怎么办 日志写入失败不能拖垮主业务,尤其当用 MySQL 时,日志表引擎是 MyISAM(不支持事务)或跨库写入,很容易破坏事务一致性。ThinkPHP 默认事务不包含日志写入,但很多人会手写
$db->transaction()
把日志也包进去,这是危险操作。 日志必须异步或至少非事务方式写入:用
Db::connect('log_db')->table('log_table')->insert()
单独连接,不加入当前事务 如果坚持同步写,得捕获
Throwable
,记录失败并告警,但绝不 throw 出去中断主流程 别用
think-queue
做日志队列——延迟写入会导致“操作已成功但日志没生成”,审计时断档 测试时故意让日志表不可写,确认主业务仍能成功提交,这是关键验收点 用 Db::raw() 处理 JSON 字段变更时的坑 ThinkPHP 对 JSON 字段(如 MySQL 的
JSON
类型)做变更检测时,
getOrigin()
返回的是解析后的 PHP 数组,而数据库里存的是字符串。直接用
json_encode()
对比会因键序、空格、类型隐式转换出错,比如
true
vs
1
、
null
vs
''
。 立即学习 “ PHP免费学习笔记(深入) ”; 统一用
json_encode($data, JSON_UNESCAPED_UNICODE | JSON_FORCE_OBJECT)
格式化后再比对 别信
array_diff_assoc()
—— 它对嵌套数组无效,得递归序列化或用
json_encode()
后 md5 MySQL 8.0+ 可用
JSON_CONTAINS_PATH()
在 SQL 层判断是否变更,但 ThinkPHP 不原生支持,需手写
Db::raw()
字段值是 JSON 但业务语义是“结构化配置”,建议拆成独立配置表,比在日志里存大段 JSON 更易查、更省空间 最常被绕开的一点:软删除(
delete_time
)触发的“删除”操作,本质是 update,但多数人只监听
delete
事件,漏掉这部分变更。得在
afterUpdate
里额外判断
$model->delete_time !== null
才补上删除日志。

相关文章