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