在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 ''。
- 统一用
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 才补上删除日志。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










