跳转到主内容
websoft网络软件专家 - 深耕网络技术,打造实用软件!

ThinkPHP怎么处理模型字段自动状态变更日志_ThinkPHP记录每次状态跳转详情【说明】

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()
封装方法里,统一控制事务边界。 立即学习 “ PHP免费学习笔记(深入) ”; 不要在事件回调里单独开事务(
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,别等出问题了再后悔没留 事情说清了就结束。状态日志最难的不是写代码,是想清楚“谁在什么时候、基于什么依据、改了哪个字段的什么值”——这个链条断一环,日志就变成干扰项。

相关文章