ThinkPHP模型修改时应通过getOriginData()获取原始值并与新值逐字段比对,仅记录真实变更字段;日志须复用同一数据库连接并置于主事务中,使用JSON存储变更详情以兼容字段变更,排除时间戳与敏感字段,并确保模型为查询所得而非new创建。

ThinkPHP 模型修改时怎么自动记录字段变更
直接改 save() 前后对比字段就行,别依赖模型事件钩子做全量快照——太重、易漏、性能差。核心是:在数据写入前拿到原始值,和新值逐字段比对,只记真正变过的字段。
常见错误是直接读数据库查旧数据再比对,结果并发时读到的不是本次操作前的状态;或者用 beforeUpdate 事件但没意识到该事件触发时模型已加载了新数据,$this->getData() 和 $this->getOriginData() 差异被掩盖。
- 务必在
beforeWrite或beforeUpdate钩子里用$this->getOriginData()获取原始值(前提是模型已查询过) - 如果走
Db::table()->update()这种原生写法,模型钩子不触发,必须自己手动查旧值 + 对比 - 注意时间字段(如
update_time)默认会被自动更新,要从对比列表里排除,否则每次都会“变” - 敏感字段(如密码、token)建议跳过日志记录,避免泄露
ThinkPHP 日志表设计要兼容字段名变更
别把字段名硬编码进日志表结构里。字段名可能改,但日志表不能跟着频繁改结构。正确做法是用 JSON 存变更详情,主表只留通用字段。
典型错误是建一张固定列的表,比如 old_name / new_name / old_status / new_status……一旦模型加字段或改名,日志逻辑就得同步动,维护成本爆炸。
立即学习“PHP免费学习笔记(深入)”;
- 日志表至少包含:
table_name、record_id、operator_id、changed_data(TEXT类型,存 JSON) -
changed_data格式示例:{"title": {"old": "旧标题", "new": "新标题"}, "status": {"old": 1, "new": 2}} - MySQL 5.7+ 可用
JSON类型,但 PHP 写入前仍需json_encode(),读取后json_decode() - 避免在
changed_data里存大字段(如富文本、base64 图片),会撑爆日志表
ThinkPHP 中怎么安全获取修改前的数据
靠 $model->getOriginData() 最省事,但它有个硬前提:模型实例必须是从数据库查出来的,而不是 new 出来的空模型。否则返回空数组,对比就失效。
常见坑是用户提交表单后,代码里 new 一个模型再赋值保存:$user = new User(); $user->data($post)->save(); —— 这时 getOriginData() 拿不到任何东西,因为没加载过原始数据。
- 优先走「先查后改」流程:
$user = User::find($id); $user->name = $newName; $user->save(); - 如果必须 new 模型,就得手动查一次旧数据:
$origin = User::where('id', $id)->field('name,status')->find();,再和新数据比对 - 注意软删除模型:如果旧记录已被软删,
find()默认查不到,得加withTrashed()才能拿到原始值 - 关联字段(如
category.name)不在模型原始数据里,需要额外处理,别误当普通字段比对
ThinkPHP 字段变更日志的性能和事务边界
日志写入必须和主业务在同一个事务里,否则主操作成功但日志失败,就会丢记录;或者日志写了但主操作回滚,留下脏日志。
最容易被忽略的是:日志写入用了另一个数据库连接,或用了 Db::connect('log')->table(...)->insert() 这种显式连接,导致无法自动加入当前事务。
- 日志写入必须复用当前模型使用的数据库连接,例如:
Db::name('admin_log')->insert($logData)(不指定连接名) - 避免在日志里调用耗时操作,比如远程 API、文件写入、复杂计算——这些应该异步做
- 高频更新场景(如计数器字段)建议加白名单,只记录关键业务字段,否则日志表膨胀极快
- 测试时记得关掉日志,否则单元测试跑着跑着发现日志表多了几百条测试数据
字段级变更日志真正的难点不在技术实现,而在什么时候不该记——比如定时任务批量更新状态,或者后台管理员覆盖式编辑,这些场景的“变更”语义和用户操作完全不同,得按业务意图区分处理,不能一刀切。



















