You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel Cashier(Stripe)订阅如何关联带有效期的用户Rank等级

解决方案

1. 核心逻辑依赖Stripe Webhook实现自动同步

不要依赖checkout成功页面修改用户等级,该逻辑不可靠(用户提前关闭页面、网络异常都会导致执行失败)。Laravel Cashier已内置Stripe Webhook的事件解析,直接监听对应事件处理等级升降即可。

配置步骤:

  • 首先在Stripe后台配置Webhook端点,指向你站点的/stripe/webhook路由(Cashier默认提供),并开启以下事件:
    • checkout.session.completed(支付/订阅开通成功)
    • customer.subscription.updated(订阅更新,比如升降级、续费成功)
    • customer.subscription.deleted(订阅到期/取消)
    • customer.subscription.paused(订阅暂停)
    • invoice.payment_failed(续费支付失败)
  • 在app/Providers/EventServiceProvider.php中注册事件监听:
protected $listen = [
    \Laravel\Cashier\Events\WebhookReceived::class => [
        \App\Listeners\HandleStripeWebhook::class,
    ],
];

2. 实现Webhook监听器

创建app/Listeners/HandleStripeWebhook.php,根据不同事件处理用户等级:

<?php

namespace App\Listeners;

use Laravel\Cashier\Events\WebhookReceived;
use App\Models\User;
use App\Models\Rank;

class HandleStripeWebhook
{
    public function handle(WebhookReceived $event)
    {
        $payload = $event->payload;
        $eventType = $payload['type'];
        
        // 提取关联用户
        $customerId = $payload['data']['object']['customer'] ?? null;
        if (!$customerId) return;
        $user = User::where('stripe_id', $customerId)->first();
        if (!$user) return;
        
        switch ($eventType) {
            // 支付/订阅开通成功
            case 'checkout.session.completed':
                $session = $payload['data']['object'];
                // 订阅模式
                if ($session['mode'] === 'subscription') {
                    $priceId = $session['subscription']['items']['data'][0]['price']['id'] ?? null;
                } 
                // 一次性支付(对应永久premium+)
                else if ($session['mode'] === 'payment') {
                    $priceId = $session['line_items']['data'][0]['price']['id'] ?? null;
                }
                if ($priceId) {
                    $rank = Rank::where('stripe_id', $priceId)->first();
                    // 优先升级到更高等级的角色
                    if ($rank && $rank->strength > $user->rank->strength) {
                        $user->update(['rank_id' => $rank->id]);
                    }
                }
                break;
                
            // 订阅到期/取消/暂停/支付失败
            case 'customer.subscription.deleted':
            case 'customer.subscription.paused':
            case 'invoice.payment_failed':
                // 先判断用户是否还有其他有效的付费订阅
                if (!$user->subscribed('default')) {
                    // 降级为普通用户,替换成你实际普通用户的rank_id
                    $defaultRank = Rank::where('title', '普通用户')->first();
                    // 只有当前角色不是staff/永久premium+的时候才降级
                    if (!$user->rank->staff && $user->rank->title !== 'premium+') {
                        $user->update(['rank_id' => $defaultRank->id]);
                    }
                }
                break;
                
            // 订阅更新(升降级、续费)
            case 'customer.subscription.updated':
                $subscription = $payload['data']['object'];
                if ($subscription['status'] === 'active') {
                    $priceId = $subscription['items']['data'][0]['price']['id'];
                    $rank = Rank::where('stripe_id', $priceId)->first();
                    if ($rank) {
                        $user->update(['rank_id' => $rank->id]);
                    }
                }
                break;
        }
    }
}

3. 兜底定时任务(可选但推荐)

为避免Webhook推送丢失导致等级不同步,添加每日执行的定时任务做兜底校验:

  • 在app/Console/Kernel.php中注册任务:
protected function schedule(Schedule $schedule)
{
    $schedule->call(function () {
        $defaultRank = Rank::where('title', '普通用户')->first();
        // 遍历所有非staff、非永久premium+的付费等级用户
        Rank::where('staff', 0)
            ->where('title', '!=', 'premium+')
            ->whereNotNull('price')
            ->each(function ($rank) use ($defaultRank) {
                $rank->users()->each(function ($user) use ($defaultRank) {
                    if (!$user->subscribed('default')) {
                        $user->update(['rank_id' => $defaultRank->id]);
                    }
                });
            });
    })->daily();
}

4. 原有逻辑优化

  • 你的Rank表的stripe_id字段可以直接存储对应付费等级的Stripe Price ID,不需要硬写RankHelper::getIdByName('Premium'),逻辑更灵活。
  • Checkout成功页面的等级更新逻辑可以移除,或者仅做降级兼容,核心逻辑以Webhook为准。

内容的提问来源于stack exchange,提问作者Kylian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.07 05:39:00