thinkphp模型save()时需手动比对旧值记录状态日志:先lock(true)查旧数据,再对比关键状态字段,确保事务一致性和避免并发脏读;日志表须含model_name、record_id、from_value、to_value、field_name、operator_type、operator_id、高精度时间及extra_data。

模型 save() 时怎么自动记录状态变更日志
ThinkPHP 模型本身不内置状态变更日志功能,必须手动在 save() 前后比对字段值。核心思路是:读取原始数据(用 find() 或缓存旧值),再和待更新数据对比关键状态字段(如 status、order_status),有差异才写日志。
常见错误是直接监听 after_update 钩子——此时原始数据已不可见,无法判断“从什么变到什么”。必须在 before_update 或显式查一次旧记录。
- 推荐在模型的
save()调用前主动查旧数据:$old = static::find($id),再和$data中的状态字段比对 - 若用
update()静态方法,务必先find(),不能依赖$this->data—— 它只含本次传入字段,不含完整旧值 - 状态字段名要硬编码比对,避免用循环遍历所有字段,否则容易误判非状态类字段(如
updated_time)
用事件监听实现日志写入但不破坏事务一致性
想解耦逻辑?可以用 before_update 事件,但必须确保日志写入和主更新在同一个数据库事务里,否则会出现“状态已改但日志没写”或反之。
ThinkPHP 6+ 的事件默认不参与事务,需手动处理。最稳的方式不是靠事件,而是把日志逻辑写进模型的 save() 封装方法里,统一控制事务边界。
- 不要在事件回调里单独开事务(
Db::startTrans()),这会嵌套事务,MySQL 可能静默降级为 auto-commit - 如果坚持用事件,得在业务层手动传入事务对象,或通过
Db::transaction()包裹整个操作,包括日志写入 - 日志表建议和主表同库同引擎(如都用 InnoDB),避免跨库事务引发 XA 问题
记录“从 A 到 B”时怎么避免脏读和并发覆盖
两个请求几乎同时修改同一订单状态,可能出现日志记成“待付款→已发货”,实际却是“待付款→已取消→已发货”,中间状态丢失。根本原因是没加行锁或版本校验。
单纯靠 PHP 层比对旧值不够,数据库层面必须保证读取旧值那一刻该记录未被其他事务修改。
- 查旧数据时用
lock(true):例如Order::where('id', $id)->lock(true)->find(),生成SELECT ... FOR UPDATE - 避免用
findOrEmpty()这类非锁查询,它不阻塞并发写入,比对结果可能已过期 - 如果业务允许,可加乐观锁字段(如
version),在where条件中带上旧 version,update 失败即说明已被改过,需重试或告警
日志表结构设计要注意哪些字段才够用
只记 from_status、to_status、operator_id 不够。漏掉关键上下文,后期排查时根本没法还原现场。
尤其要注意时间精度和来源标识——ThinkPHP 默认的 date('Y-m-d H:i:s') 秒级时间,在高频操作下无法区分先后;不带模块/控制器信息,就分不清是后台改的还是 API 接口改的。
- 必加字段:
model_name(如'order')、record_id、from_value、to_value、field_name(如'status')、operator_type('admin'/'api')、operator_id - 时间用
microtime(true)存 float 或DATETIME(6),避免秒级时间冲突 - 加
extra_data字段(text/json),存当时附带的备注、IP、user_agent,别等出问题了再后悔没留
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










