ThinkPHP 8事件系统实战:拆解订单状态变更的一地鸡毛

2026-08-05 0 389

做电商后端的朋友应该都体会过:一个订单创建,接下来要扣库存、发优惠券、记积分、推送小程序模板消息、给运营发邮件通知。如果这些代码全都写在Controller或者Service里,不出三个月,你会发现自己连改个订单状态都手抖,生怕动了一处带着另一处崩。

ThinkPHP 8把完整的事件系统摆上了台面,算是一件正经事。事件的好处说白了就是“谁关心,谁自己去监听”。订单服务只管把订单状态改完,然后广播一声“订单已创建”。谁想响应,谁就去监听这个事件,互不打扰。这么说可能有点抽象,咱们直接上代码,看它是怎么帮我把一个烂摊子理顺的。

一、我遇到的真实情况

上个月领导让我在现有订单流程里加一个“订单完成后送抽奖次数”的功能。原本的代码是这样的:

public function paid($orderId)
{
    $order = Order::find($orderId);
    $order->status = 2;
    $order->save();

    // 通知用户
    SmsService::send($order->user_id, '您的订单已付款...');

    // 加积分
    UserScore::add($order->user_id, $order->amount);

    // 给运营发邮件
    MailService::send('admin@example.com', '新订单...');
}

看着不复杂,可你要知道,这个paid方法被调用的地方至少有六七处,分散在支付回调、手动确认、后台补单等等。今天要加抽奖,我必须去把每一处都复制粘贴一遍。要是忘了其中一处,线上就会出现“这单付款了没给抽奖”的投诉。

用事件系统改完以后,paid方法里只留下改订单状态这一件事。其他乱七八糟的通知全部变成听众,什么时候想加新动作,就再写一个监听类,不用动原有代码。

二、事件系统的基础构件

TP8的事件系统主要有两个角色:事件类和监听器(也叫订阅者)。你可以把事件类理解为一个“消息信封”,里面装着订单ID等数据;监听器就是打开这个信封并干活的人。

先看看怎么定义一个订单支付成功的事件类。我在app/event/目录下建了一个OrderPaid.php:

namespace appevent;

class OrderPaid
{
    public $order;
    public $extra;

    public function __construct($order, array $extra = [])
    {
        $this->order = $order;
        $this->extra = $extra;
    }
}

再看监听器。我想把“送积分”单独做成一个监听器,在app/listener/下建一个UpdateUserScore.php:

namespace applistener;

use appeventOrderPaid;
use thinkfacadeLog;

class UpdateUserScore
{
    public function handle(OrderPaid $event)
    {
        $order = $event->order;
        // 积分比例:1元=1分
        UserScore::add($order->user_id, intval($order->amount));
        Log::info("订单 {$order->id} 积分已加");
    }
}

然后去注册监听关系。ThinkPHP 8在app/event.php文件里这样弄:

use appeventOrderPaid;
use applistenerUpdateUserScore;
use applistenerSendSmsNotification;
use applistenerAddLotteryChance;

return [
    'bind' => [
        // 如果你希望用字符串名字触发事件,可以在这里绑定
    ],
    'listen' => [
        OrderPaid::class => [
            UpdateUserScore::class,
            SendSmsNotification::class,
            AddLotteryChance::class,
        ],
    ],
];

注意listen数组里,一个事件类对应一个数组,里面可以挂多个监听器。它们会按照你挂的顺序依次执行。目前我有三个:加积分、发短信、送抽奖次数。

三、订单控制器里怎么触发

触发事件有两种办法,你习惯用哪个就用哪个。第一个是事件门面(thinkfacadeEvent):

use thinkfacadeEvent;

public function paid($orderId)
{
    $order = Order::find($orderId);
    $order->status = 2;
    $order->save();

    // 广播事件,把订单对象传过去
    Event::trigger(new appeventOrderPaid($order));
}

第二种是用助手函数event():

event(new appeventOrderPaid($order));

两种都一样。我现在更习惯用event(),因为它短。而且就算后期把构造参数改成多个,也不用改触发地方。

触发这一行代码,后面三个监听器就屁颠屁颠跑了。加积分的、发短信的、加抽奖的,全都自己做自己的事,互不相干。订单业务的主流程只改一次状态,干净利索。

四、带参数和返回值的事怎么处理

有时候监听器里需要执行完以后给个反馈,比如某个外部系统调用失败是不是要终止流程?TP8事件系统也支持返回值,但默认多个监听器之间不会互相影响。

你可以在监听器的handle方法里返回false,就能阻止后续监听器执行。举例:我写了一个风险控制监听器,如果发现订单有异常,就返回false,后面加积分、送抽奖的动作就不执行了。

namespace applistener;

class RiskCheck
{
    public function handle($event)
    {
        if ($event->order->user_id == 10086) {
            return false; // 拦截
        }
        return true;
    }
}

然后在event.php里把它排在第一个:

OrderPaid::class => [
    RiskCheck::class,
    UpdateUserScore::class,
    SendSmsNotification::class,
    AddLotteryChance::class,
],

这样风险检查一旦返回false,后面所有监听器都不会执行。这相当于给订单支付流程加了一道闸门,非常实用。

五、监听器里能用依赖注入吗

这是我最喜欢的一点。TP8的监听器是支持依赖注入的。也就是说你可以在监听器的构造函数里提前把要用到的服务传进来,而不是在handle里写一堆new。

namespace applistener;

use appserviceSmsService;

class SendSmsNotification
{
    protected $sms;

    public function __construct(SmsService $sms)
    {
        $this->sms = $sms;
    }

    public function handle($event)
    {
        $this->sms->send($event->order->user_id, '您有一个新订单已付款');
    }
}

以前写业务代码时,总是到处new_service,代码里全是耦合。现在通过构造函数注入,整个监听器就像一个小型API,清爽不少。

注意:监听器里做业务必须保证不抛异常影响主流程。所以我在每个监听器里都try/catch包住,异常记录到日志,但是不允许它中断事件循环。除非你确实想让某个监听器失败后阻断后面的监听器,那你就在监听器里不捕获异常,让TP8自己处理。我一般是捕获后写日志,然后正常继续。

六、队列事件:把短信和邮件扔进后台

事件默认是同步执行的。也就是说,监听器里跑了2秒,你的订单接口就得等2秒。虽然订单状态已经改了,但用户点完支付看到转圈转2秒,体验肯定差。

ThinkPHP 8里做了队列事件的集成。你不需要在监听器里自己写异步逻辑,直接定义一个“事件类”,让它实现一个接口就行。我用的是thinkcontractQueueShouldQueue

namespace appevent;

class OrderPaid implements thinkcontractQueueShouldQueue
{
    public $order;

    public function __construct($order)
    {
        $this->order = $order;
    }
}

然后在config/queue.php里配置好队列驱动(比如Redis或者数据库)。当触发OrderPaid事件时,如果你在event.php里监听器没有特别设置,TP8会自动把事件丢进队列里,异步执行监听器。同步的监听器还是同步执行,需要异步的监听器单独处理。

不过这里有个坑,监听器类里的依赖注入在队列模式下不会自动完成,因为那是另一个进程,容器是重新初始化的。所以我的做法是:把短信、邮件这类耗时的监听器在handle方法里使用thinkfacadeQueue或容器手动创建服务,而一些轻量的逻辑(比如写日志、改内存缓存)保留同步。

我实际项目中的配置是这样的:

OrderPaid::class => [
    RiskCheck::class,                    // 同步,立即拦截
    UpdateUserScore::class,              // 同步,写本地数据
    SendSmsNotification::class,          // 异步,丢队列
    AddLotteryChance::class,             // 异步,丢队列
],

那么问题来了:一个事件,怎么让部分监听器同步,部分异步?当事件类实现了QueueShouldQueue时,所有监听器都会进队列。要让某个监听器同步,可以在监听器里加一个属性:

class SendSmsNotification
{
    // 这个属性设为false,表示不异步,直接执行
    public $async = false;
}

TP8的队列事件会去检查监听器对象上有没有$async属性,如果为false就同步执行。所以上面RiskCheck没有$async属性,默认被队列包裹;实际上我更想让它同步,所以我在代码里给RiskCheck类加了public $async = false;这样它就不进队列。

这个细节官方文档里没有写得很清楚,反正我是试出来的。如果你有什么更好的办法,欢迎告诉我。

七、从折腾到真香

后来我们又加了“订单金额满100元赠运费险”的需求,我只需要新建一个监听器,然后在event.php里加一行,完事。当时同事都有点惊讶,因为我只花了十分钟。

用事件系统最大的改变不是代码量少了多少,而是你不再需要去理解“一笔订单支付后到底要经历什么”这种全局逻辑。你只需要关心自己负责的那个监听器,接收数据,处理数据。真正做到了各自负责,互不踩踏。

如果你是刚开始接触ThinkPHP 8,我建议你从简单的日志监听开始。比如用户登录成功事件,写个监听器记一下登录时间和IP。等熟悉了这套模式,再把订单、支付这种核心流程慢慢拆开。你会发现,原来乱成一团的业务逻辑,也能被整理得服服帖帖。

代码写久了,越来越觉得优雅的架构不是靠设计出来的,而是靠一次次解耦拆出来。事件系统就是那把拆分的手术刀。

ThinkPHP 8事件系统实战:拆解订单状态变更的一地鸡毛
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 thinkphp ThinkPHP 8事件系统实战:拆解订单状态变更的一地鸡毛 https://www.taomawang.com/server/thinkphp/2490.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务