PaymentSucceeded 触发后如何清空 Cart 和 Session?
结论
监听器的写法没有明显问题。需要先确认的是,PaymentSucceeded 是否在带有用户 Session 的同一个 HTTP 请求中同步触发。
Cart::destroy() 和 Session::forget() 只能操作当前请求对应的 Session。如果支付成功事件来自支付平台的 webhook、队列任务或其他后台请求,该请求通常不包含用户浏览器的 Session Cookie。即使监听器正常执行,也无法清除用户原有的 Cart 和 coupon。
同一请求内触发事件
如果 PaymentSucceeded 在用户支付回跳请求中同步触发,可以使用下面的监听器:
<?php
namespace App\Listeners;
use App\Events\PaymentSucceeded;
use Gloudemans\Shoppingcart\Facades\Cart;
class Destroy
{
public function handle(PaymentSucceeded $event): void
{
Cart::destroy();
session()->forget('coupon');
}
}
即使对应的键不存在,forget() 也不会报错,所以不必先调用 has()。
如果项目使用的购物车扩展不是 Gloudemans\Shoppingcart,请改用该扩展实际提供的 Facade。例如,部分扩展使用:
use Darryldecode\Cart\Facades\CartFacade as Cart;
还要确认 EventServiceProvider 引用的是正确的事件类和监听器类:
<?php
namespace App\Providers;
use App\Events\PaymentSucceeded;
use App\Listeners\Destroy;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
PaymentSucceeded::class => [
Destroy::class,
],
];
}
分发事件时必须使用完全相同的类:
event(new PaymentSucceeded($order));
如果事件或配置曾被缓存,可以在部署环境中重新生成缓存:
php artisan optimize:clear
php artisan event:cache
也可以先检查事件和监听器是否已经正确注册:
php artisan event:list
支付成功来自 webhook 时
如果事件由支付平台的 webhook 触发,就不应直接在 webhook 监听器中清除浏览器 Session:
用户浏览器请求 -> Session A
支付平台 webhook -> 没有 Session A
这种情况下,可以把处理流程拆开:
- webhook 验证支付结果,并将订单标记为已支付;
- 用户返回支付成功页面后,根据订单状态再次向服务端确认;
- 确认订单已经支付,再在用户自己的请求中清空 Cart 和 coupon。
例如:
public function success(Request $request, Order $order)
{
abort_unless(
$order->user_id === $request->user()->id,
403
);
abort_unless($order->is_paid, 409, 'Payment is not confirmed.');
Cart::destroy();
$request->session()->forget('coupon');
return view('checkout.success', compact('order'));
}
不要只根据支付平台回跳地址中的查询参数判断支付是否成功。收到回跳请求后,应通过订单号重新查询本地订单状态,或者向支付平台的服务端验证支付结果。
如果用户可能不会返回成功页面,可以由 webhook 将类似 cart_should_be_cleared 的标记写入数据库:
$order->update([
'is_paid' => true,
'cart_should_be_cleared' => true,
]);
等用户下次进入站点时,再在带有其 Session 的请求中执行清理:
if ($order->cart_should_be_cleared) {
Cart::destroy();
session()->forget('coupon');
$order->update([
'cart_should_be_cleared' => false,
]);
}
还需要检查的事项
Destroy监听器不能实现ShouldQueue,否则执行时通常无法可靠地访问原请求的 Session。- 确认支付代码实际分发的是
App\Events\PaymentSucceeded,而不是第三方包中的同名事件。 - 确认
Cart使用 Session 存储,并且清理的是正确的购物车实例。如果使用了命名实例,需要先切换实例:
Cart::instance('checkout')->destroy();
- 如果购物车按用户保存在数据库中,应根据用户或订单标识删除购物车记录,不要依赖当前 Session。
- 不建议在 webhook 中强行切换到客户端的 Session ID,这可能带来并发、Session 固定和身份边界方面的问题。
如果支付事件由 webhook 触发,更稳妥的做法是让 webhook 只负责确认并记录支付状态,等用户下一次携带原 Session 发起请求时,再清理 Cart 和 coupon。
备注:内容仅供参考。
