如何修改Laravel Cashier实现订阅特定套餐后添加用户账户积分
针对你用Laravel Cashier搭配Stripe管理订阅,想要在用户成功订阅特定套餐后添加对应积分的需求,我整理了几种可靠的实现方式,按推荐程度排序:
方法一:利用Stripe Webhook(最可靠)
这种方式通过监听Stripe的订阅状态变更事件,确保只有当订阅真正变为活跃状态(付款成功)时才发放积分,避免了同步处理可能出现的付款失败、3D验证延迟等问题。
步骤1:配置Stripe Webhook
在Stripe后台的「Webhooks」页面,添加你的应用端点(比如https://your-domain.com/stripe/webhook),勾选customer.subscription.updated事件,并复制Webhook签名密钥到.env文件:
STRIPE_WEBHOOK_SECRET=你的签名密钥
步骤2:生成并修改Webhook控制器
先生成Cashier自带的Webhook控制器:
php artisan cashier:webhook
打开app/Http/Controllers/StripeWebhookController.php,添加订阅更新的处理逻辑:
namespace App\Http\Controllers; use Laravel\Cashier\Http\Controllers\WebhookController as CashierController; use Symfony\Component\HttpFoundation\Response; class StripeWebhookController extends CashierController { /** * 处理订阅状态更新事件 */ public function handleCustomerSubscriptionUpdated($payload) { // 从Stripe payload中获取订阅记录 $subscription = $this->getSubscriptionFromPayload($payload); if (!$subscription) { return response()->json(['message' => '订阅记录不存在'], Response::HTTP_NOT_FOUND); } $user = $subscription->user; $subscriptionStatus = $payload['data']['object']['status']; $stripePlanId = $payload['data']['object']['items']['data'][0]['price']['id']; // 仅当订阅变为活跃状态时处理积分奖励 if ($subscriptionStatus !== 'active') { return response()->json(['message' => '订阅未激活,跳过积分奖励']); } // 检查用户是否已获得过该套餐的积分(避免重复发放) $hasReceivedReward = \App\Models\UserSubscriptionReward::where([ 'user_id' => $user->id, 'stripe_plan_id' => $stripePlanId ])->exists(); if ($hasReceivedReward) { return response()->json(['message' => '用户已获得过该套餐积分']); } // 定义套餐对应的积分规则 $planPointsMap = [ 'price_123Bronze' => 300, // 替换成你Stripe后台的Bronze套餐价格ID 'price_456Silver' => 600, 'price_789Gold' => 1000 ]; if (isset($planPointsMap[$stripePlanId])) { // 给用户添加积分(假设用户模型有points字段) $user->increment('points', $planPointsMap[$stripePlanId]); // 记录奖励历史,防止重复发放 \App\Models\UserSubscriptionReward::create([ 'user_id' => $user->id, 'stripe_plan_id' => $stripePlanId, 'points' => $planPointsMap[$stripePlanId], 'reason' => '订阅'.$stripePlanId.'套餐奖励' ]); } return response()->json(['message' => '积分奖励发放成功']); } }
步骤3:创建奖励历史模型(可选但推荐)
生成UserSubscriptionReward模型和迁移,用来记录用户的积分奖励历史:
php artisan make:model UserSubscriptionReward -m
在迁移文件中添加字段:
public function up() { Schema::create('user_subscription_rewards', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->string('stripe_plan_id'); $table->integer('points'); $table->text('reason')->nullable(); $table->timestamps(); }); }
运行迁移:
php artisan migrate
方法二:监听Laravel Cashier事件(优雅且便捷)
如果你的业务场景不需要严格依赖Stripe的异步回调,可以通过监听Cashier自带的订阅事件来处理积分发放。
步骤1:生成事件监听器
php artisan make:listener AwardPointsOnSubscription
步骤2:注册事件监听
在app/Providers/EventServiceProvider.php的$listen数组中添加事件与监听器的绑定:
protected $listen = [ \Laravel\Cashier\Events\SubscriptionUpdated::class => [ \App\Listeners\AwardPointsOnSubscription::class, ], ];
步骤3:编写监听器逻辑
打开app/Listeners/AwardPointsOnSubscription.php:
namespace App\Listeners; use Laravel\Cashier\Events\SubscriptionUpdated; class AwardPointsOnSubscription { public function handle(SubscriptionUpdated $event) { $subscription = $event->subscription; $user = $subscription->user; // 仅处理活跃状态的订阅 if (!$subscription->active()) { return; } $stripePlanId = $subscription->stripe_price; $planPointsMap = [ 'price_123Bronze' => 300, 'price_456Silver' => 600, 'price_789Gold' => 1000 ]; // 检查是否已发放过积分 $hasReceivedReward = \App\Models\UserSubscriptionReward::where([ 'user_id' => $user->id, 'stripe_plan_id' => $stripePlanId ])->exists(); if (!$hasReceivedReward && isset($planPointsMap[$stripePlanId])) { $user->increment('points', $planPointsMap[$stripePlanId]); \App\Models\UserSubscriptionReward::create([ 'user_id' => $user->id, 'stripe_plan_id' => $stripePlanId, 'points' => $planPointsMap[$stripePlanId], 'reason' => '订阅套餐奖励' ]); } } }
方法三:同步处理(简单但有风险)
如果你的订阅流程是完全同步且能确保付款100%成功,可以直接在订阅控制器中添加积分逻辑,但不推荐用于生产环境(比如遇到3D验证时,订阅会处于pending状态,同步代码不会触发):
public function subscribe(Request $request) { $user = auth()->user(); try { $subscription = $user->newSubscription('default', 'price_123Bronze') ->create($request->paymentMethodId); if ($subscription->active()) { $user->increment('points', 300); } return redirect()->route('dashboard')->with('success', '订阅成功,已获得300积分!'); } catch (\Exception $e) { return back()->withErrors(['message' => '订阅失败:'.$e->getMessage()]); } }
- 必须验证订阅状态为
active,避免给付款失败的用户发放积分。 - 加入奖励历史记录逻辑,防止用户重复订阅同一套餐时重复获得积分(如果业务允许重复发放可忽略)。
- 本地开发测试Webhook时,需要用ngrok等工具暴露本地服务,确保Stripe能正常回调。
内容的提问来源于stack exchange,提问作者user7747472

