随着电商业务日益复杂,订单模块往往夹杂着库存扣减、优惠券核销、物流通知、数据分析等大量耦合逻辑。一旦某个环节调整,整套流程可能面临回归风险。ThinkPHP 8 对事件系统进行了全面升级,支持事件订阅、通配符监听以及队列事件,为业务解耦提供了优雅的原生方案。本文将通过一个完整的订单状态机案例,演示如何利用 ThinkPHP 8 的事件驱动能力,将订单生命周期中的副作用剥离为独立的监听器,从而构建出高可用、易维护的订单系统。
1. 案例背景与目标
我们模拟一个简化版电商订单流程,订单状态依次为:待支付 → 已支付 → 已发货 → 已完成,同时允许已支付状态下取消订单进入已取消。
在状态变迁时,需要触发以下业务动作:
- 支付成功时:冻结库存、发送支付成功通知、记录资金流水。
- 发货时:生成物流单号、向用户推送发货提醒。
- 订单完成时:解冻库存(若冻结模式)、发放积分、触发结算统计。
- 取消订单时:释放库存、退款处理、发送取消通知。
传统做法往往将这些逻辑直接写在模型方法或控制器中,导致“上帝类”膨胀。通过事件驱动架构,订单模型只负责状态流转本身,所有附加动作由监听器异步或同步响应,核心流程清晰且极易扩展。
2. 环境准备与项目初始化
确保已安装 PHP 8.0+ 与 Composer,创建 ThinkPHP 8 项目:
composer create-project topthink/think tp8-order-state
cd tp8-order-state
修改 .env 文件配置数据库连接(示例使用 MySQL),并创建订单相关数据表:
CREATE TABLE `order` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`order_no` VARCHAR(32) NOT NULL COMMENT '订单号',
`user_id` INT UNSIGNED NOT NULL,
`amount` DECIMAL(10,2) NOT NULL COMMENT '订单金额',
`status` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '状态:0待支付,1已支付,2已发货,3已完成,4已取消',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
准备一些辅助表(库存、日志等)可根据实际需求创建,本文主要聚焦事件机制,这些表操作将在监听器中以伪代码体现。
3. 订单模型与状态流转封装
首先创建订单模型 app/model/Order.php,并在内部封装状态变更方法。每个状态变更方法只负责更新数据库中的状态字段,不包含任何额外业务逻辑。
<?php
namespace appmodel;
use thinkModel;
use appeventOrderPaid;
use appeventOrderShipped;
use appeventOrderCompleted;
use appeventOrderCancelled;
class Order extends Model
{
protected $name = 'order';
// 状态常量
const STATUS_PENDING = 0;
const STATUS_PAID = 1;
const STATUS_SHIPPED = 2;
const STATUS_COMPLETED = 3;
const STATUS_CANCELLED = 4;
/**
* 标记订单为已支付
*/
public function markPaid(): bool
{
if ($this->status != self::STATUS_PENDING) {
return false;
}
$this->status = self::STATUS_PAID;
$result = $this->save();
if ($result) {
// 触发支付成功事件
event(new OrderPaid($this));
}
return $result;
}
/**
* 标记已发货
*/
public function markShipped(string $logisticsNo): bool
{
if ($this->status != self::STATUS_PAID) {
return false;
}
$this->status = self::STATUS_SHIPPED;
$this->save();
// 触发发货事件,并携带物流单号
event(new OrderShipped($this, $logisticsNo));
return true;
}
/**
* 标记已完成
*/
public function markCompleted(): bool
{
if ($this->status != self::STATUS_SHIPPED) {
return false;
}
$this->status = self::STATUS_COMPLETED;
$this->save();
event(new OrderCompleted($this));
return true;
}
/**
* 取消订单 (仅允许待支付或已支付状态取消)
*/
public function cancel(): bool
{
if (!in_array($this->status, [self::STATUS_PENDING, self::STATUS_PAID])) {
return false;
}
$previousStatus = $this->status;
$this->status = self::STATUS_CANCELLED;
$this->save();
event(new OrderCancelled($this, $previousStatus));
return true;
}
}
4. 创建事件类
ThinkPHP 8 的事件类无需继承特定基类,只需定义一个普通类并传递必要数据。在 app/event 目录下分别创建以下事件类:
OrderPaid 事件
<?php
namespace appevent;
use appmodelOrder;
class OrderPaid
{
public function __construct(public Order $order)
{}
}
OrderShipped 事件
<?php
namespace appevent;
use appmodelOrder;
class OrderShipped
{
public function __construct(public Order $order, public string $logisticsNo)
{}
}
OrderCompleted 事件
<?php
namespace appevent;
use appmodelOrder;
class OrderCompleted
{
public function __construct(public Order $order)
{}
}
OrderCancelled 事件
<?php
namespace appevent;
use appmodelOrder;
class OrderCancelled
{
public function __construct(public Order $order, public int $previousStatus)
{}
}
5. 注册事件监听器
监听器可以放在 app/listener 目录下,并通过事件服务提供者或监听配置进行注册。推荐使用订阅者(Subscriber)模式,将同一业务领域的不同监听器集中管理。以下创建一个订单事件订阅者 app/subscribe/OrderEventSubscriber.php:
<?php
namespace appsubscribe;
use appeventOrderPaid;
use appeventOrderShipped;
use appeventOrderCompleted;
use appeventOrderCancelled;
class OrderEventSubscriber
{
/**
* 处理支付成功事件
*/
public function onOrderPaid(OrderPaid $event): void
{
$order = $event->order;
// 1. 冻结库存(实际应调用库存服务)
// InventoryService::freeze($order->id);
// 2. 发送支付成功通知(邮件/短信/站内信)
// NotificationService::sendPaidNotice($order->user_id, $order->order_no);
// 3. 记录资金流水
// FundLog::record($order->user_id, $order->amount, '支付');
// 日志记录,便于调试
trace("订单 {$order->order_no} 支付成功,已触发后续处理", 'info');
}
/**
* 处理发货事件
*/
public function onOrderShipped(OrderShipped $event): void
{
$order = $event->order;
$logisticsNo = $event->logisticsNo;
// 生成物流记录
// Logistics::create(['order_id' => $order->id, 'no' => $logisticsNo]);
// 推送发货提醒
// UserNotice::send($order->user_id, "您的订单 {$order->order_no} 已发货,单号:{$logisticsNo}");
trace("订单 {$order->order_no} 已发货,物流单号:{$logisticsNo}", 'info');
}
/**
* 处理订单完成事件
*/
public function onOrderCompleted(OrderCompleted $event): void
{
$order = $event->order;
// 解冻库存(若之前采用冻结模式)并扣减实际库存
// InventoryService::confirmAndReduce($order->id);
// 发放用户积分
// PointService::award($order->user_id, $order->amount);
// 触发数据统计(如销售额累计)
// StatsService::report($order->amount);
trace("订单 {$order->order_no} 已完成,积分与统计已处理", 'info');
}
/**
* 处理取消订单事件
*/
public function onOrderCancelled(OrderCancelled $event): void
{
$order = $event->order;
$prevStatus = $event->previousStatus;
// 根据取消前的状态,执行不同策略
if ($prevStatus === Order::STATUS_PAID) {
// 已支付订单取消需要退款
// RefundService::create($order->id, $order->amount);
trace("订单 {$order->order_no} 已支付取消,发起退款", 'info');
} elseif ($prevStatus === Order::STATUS_PENDING) {
// 待支付订单取消仅需释放预占资源
trace("订单 {$order->order_no} 待支付取消,释放预占", 'info');
}
// 释放库存(无论哪种状态取消,都需要释放可能占用的库存)
// InventoryService::release($order->id);
// 发送取消通知
// NotificationService::sendCancelNotice($order->user_id, $order->order_no);
}
}
接下来在事件服务中注册该订阅者。编辑 app/event.php(若不存在则创建),添加订阅:
<?php
// 事件定义文件
return [
'bind' => [],
'listen' => [
// 可在此以“事件类名 => 监听器数组”方式注册,但订阅者更推荐下方方式
],
'subscribe' => [
appsubscribeOrderEventSubscriber::class,
],
];
ThinkPHP 8 会自动扫描 subscribe 中定义的类,并将类中以 on 开头的公共方法注册为对应事件的监听器。例如 onOrderPaid 方法会监听 OrderPaid 事件(类名去除命名空间后的匹配规则),无需手动逐个绑定。
6. 控制器调用与完整流程演示
创建订单控制器 app/controller/Order.php(也可以使用多级目录),模拟订单状态流转:
<?php
namespace appcontroller;
use appmodelOrder as OrderModel;
use thinkfacadeLog;
class Order
{
/**
* 模拟支付回调,将订单标记为已支付
*/
public function pay($id)
{
$order = OrderModel::find($id);
if (!$order) {
return '订单不存在';
}
if ($order->markPaid()) {
return "订单 {$order->order_no} 支付成功,事件已触发";
}
return '状态变更失败,当前状态不允许支付';
}
/**
* 模拟发货操作
*/
public function ship($id)
{
$order = OrderModel::find($id);
$logisticsNo = 'SF' . date('YmdHis') . rand(1000, 9999);
if ($order->markShipped($logisticsNo)) {
return "订单 {$order->order_no} 已发货,单号:{$logisticsNo}";
}
return '只有已支付订单才能发货';
}
/**
* 模拟确认收货完成订单
*/
public function complete($id)
{
$order = OrderModel::find($id);
if ($order->markCompleted()) {
return "订单 {$order->order_no} 已完成";
}
return '当前状态无法完成';
}
/**
* 取消订单
*/
public function cancel($id)
{
$order = OrderModel::find($id);
if ($order->cancel()) {
return "订单 {$order->order_no} 已取消";
}
return '取消失败,当前状态不允许取消';
}
}
定义路由方便测试,在 route/app.php 中添加:
use thinkfacadeRoute;
Route::get('order/pay/:id', 'appcontrollerOrder@pay');
Route::get('order/ship/:id', 'appcontrollerOrder@ship');
Route::get('order/complete/:id', 'appcontrollerOrder@complete');
Route::get('order/cancel/:id', 'appcontrollerOrder@cancel');
启动内置服务器 php think run,访问 http://localhost:8000/order/pay/1 即可触发支付事件,同时日志中能看到监听器执行记录。至此,一个完全解耦的订单状态机已搭建完成。
7. 进阶优化:将部分监听器放入队列
对于耗时较长的监听任务(如发送通知、数据统计),可以将其配置为队列事件,避免阻塞主流程。ThinkPHP 8 内置了队列支持,只需在监听器类中实现 ShouldQueue 接口即可。
以订单支付通知为例,新建一个队列监听器 app/listener/SendPaidNotification.php:
<?php
namespace applistener;
use appeventOrderPaid;
use thinkqueueShouldQueue;
class SendPaidNotification implements ShouldQueue
{
/**
* 队列连接
*/
public $connection = 'redis';
/**
* 队列名称
*/
public $queue = 'order_notify';
/**
* 延迟执行秒数(可选)
*/
public $delay = 10;
public function handle(OrderPaid $event): void
{
$order = $event->order;
// 执行真实的通知发送逻辑
// SmsService::send($order->user_id, '您的订单已支付成功');
trace("队列异步发送支付通知:订单号{$order->order_no}", 'info');
}
}
然后在 app/event.php 中除了订阅者外,单独将该监听器绑定到事件:
'listen' => [
appeventOrderPaid::class => [
applistenerSendPaidNotification::class,
],
],
这样 OrderPaid 事件触发时,订阅者中的 onOrderPaid 会同步执行(如库存冻结),而 SendPaidNotification 监听器则会推送到 Redis 队列异步处理。记得启动队列处理器:php think queue:listen --queue order_notify。
8. 测试与验证
可以通过简单的单元测试或手动访问路由验证整个事件驱动链条:
- 创建一条待支付订单(直接在数据库中插入一条记录,status=0)。
- 访问
/order/pay/订单ID,观察状态更新为1,同时日志中输出订阅者和队列监听器的处理信息。 - 依次测试发货、完成、取消等操作,确认所有附加逻辑在监听器中正确执行,但订单模型代码保持不变。
9. 总结与思考
通过本案例,我们完整实践了 ThinkPHP 8 的事件驱动架构:
- 将订单状态机作为核心领域模型,仅负责状态持久化与守卫条件。
- 利用事件类解耦状态变迁的副作用,任何新增业务只需添加新的监听器或订阅者方法。
- 通过队列事件平衡同步与异步需求,保障核心流程的响应速度和高可用性。
- 订阅者模式统一管理关联监听器,降低配置复杂度。
这种架构尤其适合中大型电商、交易系统等业务多变且对稳定性要求高的场景。您可以在此基础上继续扩展,例如引入事件溯源、Saga 模式处理分布式事务等,ThinkPHP 8 的柔性与高性能为复杂业务提供了坚实的基础。
完整的项目代码已按模块展示,欢迎动手实践并根据业务需求调整监听器逻辑。事件驱动不仅是一种技术选择,更是一种让系统“呼吸”的设计哲学。

