ThinkPHP 事件监听里那个 `observer` 和 `Event::trigger`,让我把"自动发券"做成了"随机发券"

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
站长杂谈 126 浏览 0 回复

上周搞了个订单支付后发优惠券的需求,产品经理说"简单,支付成功自动触发就行"。我心想这活儿我熟啊,OrderObserver 一挂,afterUpdate 里判断状态变更,完事儿。

结果上线第二天,客服群里炸了:有人没支付也收到了券,有人支付了三笔只收到一笔,还有人凌晨两点莫名其妙被塞了八张。

我盯着 OrderObserver 的代码看了十分钟,逻辑没毛病啊——if ($order->status == 1 && $order->getOriginal('status') == 0),严格判断从待支付变已支付。问题出在哪?

直到我在 afterUpdate 里加了行 Log::info('observer hit: ' . $order->order_sn . ' | original_status: ' . $order->getOriginal('status') . ' | current_status: ' . $order->status);,才发现这玩意儿被触发了四次。四次!

追查下去,订单支付流程里有个"同步回调更新状态、异步回调再更新一次、定时任务兜底又更新一次、最后财务对账还批量更新一次"。每次 save() 都走 afterUpdate,而我的 getOriginal 在第二次之后全是 1,判断自然失效。

这教训让我重新梳理了 TP 里几种钩子的适用场景,现在项目里是这么分的:

Model 事件(Observer):只用来维护模型自身的数据一致性,比如自动填充时间戳、软删除联动清理。绝不放业务逻辑,尤其别放"钱相关的"。

Event::trigger 显式触发:支付成功这种关键节点,我在 Service 层支付确认成功后手动 Event::trigger('OrderPaid', $order),监听者里再做发券、通知、分账。好处是调用栈清晰,日志里能看到是谁触发的;坏处是容易漏,所以我在支付确认函数顶部写了个注释:"此处以下三行顺序不可调换,新增事件需同步更新文档"。

中间件/行为(Behavior):全局性的、跟具体业务无关的,比如记录操作日志、接口限流。这个挂 app_init 或路由中间件,不跟模型层搅在一起。

调试这块我也换了套路。以前事件监听里出 bug,日志打了一坨找不到北。现在每个监听类都实现一个 getListenerName() 返回类名,触发时先打 [Event] 触发: OrderPaid | 监听: SendCouponListener | 开始,结束打 完成 | 耗时: 12ms。配合 debugbar 的事件面板,一眼能看到哪个监听挂了、哪个慢。

还有个坑:TP 的 Event::listen 支持通配符 Order.*,我图省事写了个 Order.PaidOrder.PaidFailed 共用同一个通配监听。结果后来加了个 Order.PaidRetry,这监听也被抓进去了,重试时发了两遍券。通配符现在被我列为"技术债",能不用就不用。

最后说个冷门的:如果你用 Queue 把事件异步化,注意 Event::trigger 是同步返回的,不会等你队列执行完。我踩过的坑是单元测试里触发完事件立刻查数据库,断言失败——队列 job 还没跑呢。现在测试环境强制 QUEUE_DRIVER=sync,生产环境才用 Redis。

你们项目里事件和钩子是怎么组织的?有没有被 Observer 的"静默执行"坑过?

评论0
回复 · 0
还没有回复
微信客服 微信客服