PAYATHON 2026

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

这种情况下,可以把处理流程拆开:

  1. webhook 验证支付结果,并将订单标记为已支付;
  2. 用户返回支付成功页面后,根据订单状态再次向服务端确认;
  3. 确认订单已经支付,再在用户自己的请求中清空 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。

备注:内容仅供参考。