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
相关产品推荐
相关产品推荐

