必须在事务提交后触发异步通知,因模型事件在commit前执行导致队列查不到新数据;正确做法是用DbTransactionCommitted事件监听器或ResponseSent钩子确保数据已落库。

在 ThinkPHP 8.0 中,模型数据变更后需立即触发短信、邮件或站内信等异步通知,但若在模型 save() 成功后直接 dispatch 队列任务,常因事务未提交导致队列消费时查不到最新数据——必须把事件触发点卡在事务真正落库之后、响应返回之前。
模型事件中不能直接 dispatch 的原因
ThinkPHP 模型的 afterWrite、afterUpdate 等事件在事务 commit 前触发,此时数据库尚未写入,队列任务一旦被消费,查询不到刚保存的数据,造成通知内容为空或失败。
Db::transaction() 内部的模型操作会延迟到 commit 后才真正落库,但模型事件回调早已执行完毕,【此时 dispatch 出去的任务必然读不到刚写入的记录】。
这一步操作起来很简单,直接把 dispatch 写进 afterWrite 里就能跑通,但线上一定会出问题。
正确触发时机:用事务后置监听器
第一步:在 app/event.php 中绑定事务完成事件
'bind' => ['DbTransactionCommitted' => 'app\event\DbTransactionCommitted'],
第二步:创建事件类 app/event/DbTransactionCommitted.php
namespace app\event; use think\Event; class DbTransactionCommitted extends Event { public $model; public $data; }
第三步:在模型 save() 后手动触发该事件(仅限事务内)
Db::transaction(function () use ($user) { $user->save(); Event::trigger(new DbTransactionCommitted(['model' => User::class, 'data' => $user->toArray()])); });
第四步:编写监听器 app/listener/NotifyUserChange.php,handle 方法接收 $model 和 $data 参数,内部调用 Queue::push() 发送异步任务。
更轻量的替代方案:HTTP 请求级兜底
方法一:在控制器响应前触发事件
用户注册成功 → 调用 User::create() → 事务自动提交 → controller 返回 JSON 前执行 Event::trigger(new UserRegistered($user))。
此方式不依赖事务监听,适用于单表操作或已确认事务已 commit 的场景。
方法二:利用 Response::send() 前的钩子
在 app/middleware/ResponseHook.php 中监听 ResponseSent 事件,在响应头发出前触发通知事件。该时机确保所有数据库写入已完成,且不会阻塞主流程。
【注意:此钩子需在 config/middleware.php 中置于最后,否则可能被提前中断】


















