PHP项目Laravel Cashier订阅支付集成

wen PHP项目 6

本文目录导读:

PHP项目Laravel Cashier订阅支付集成

  1. 安装和配置
  2. 模型准备
  3. 创建订阅计划
  4. 订阅控制器
  5. 视图文件
  6. 支付方式管理
  7. Webhook处理
  8. 中间件和路由配置
  9. 测试用例
  10. 最佳实践建议

我来为您详细介绍如何在Laravel项目中集成Cashier进行订阅支付,这是一个完整的指南,涵盖从安装到实际使用的全过程。

安装和配置

安装Cashier

# 对于Stripe支付
composer require laravel/cashier
# 对于Paddle支付(如果使用)
composer require laravel/cashier-paddle

数据库迁移

# 发布迁移文件
php artisan vendor:publish --tag="cashier-migrations"
# 执行迁移
php artisan migrate

Stripe配置

.env文件中添加:

STRIPE_KEY=your_stripe_publishable_key
STRIPE_SECRET=your_stripe_secret_key
STRIPE_WEBHOOK_SECRET=your_stripe_webhook_secret
STRIPE_CURRENCY=usd

服务提供商配置

// config/services.php
'stripe' => [
    'model' => App\Models\User::class,
    'key' => env('STRIPE_KEY'),
    'secret' => env('STRIPE_SECRET'),
    'webhook' => [
        'secret' => env('STRIPE_WEBHOOK_SECRET'),
        'tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300),
    ],
    'currency' => env('STRIPE_CURRENCY', 'usd'),
],

模型准备

更新User模型

<?php
namespace App\Models;
use Laravel\Cashier\Billable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
    use Billable;
    // 其他代码...
}

创建订阅计划

定义套餐

// app/Services/SubscriptionService.php
<?php
namespace App\Services;
class SubscriptionService
{
    /**
     * 获取所有可用的订阅套餐
     */
    public static function getPlans(): array
    {
        return [
            'basic' => [
                'name' => '基础版',
                'description' => '适合个人使用',
                'monthly_price' => 9.99,
                'yearly_price' => 99.99,
                'features' => [
                    '10个项目',
                    '基础支持',
                    '1GB存储空间'
                ]
            ],
            'pro' => [
                'name' => '专业版',
                'description' => '适合团队使用',
                'monthly_price' => 29.99,
                'yearly_price' => 299.99,
                'features' => [
                    '无限项目',
                    '优先支持',
                    '100GB存储空间',
                    '高级分析'
                ]
            ],
            'enterprise' => [
                'name' => '企业版',
                'description' => '适合大型企业',
                'monthly_price' => 99.99,
                'yearly_price' => 999.99,
                'features' => [
                    '所有功能',
                    '专属客户经理',
                    '无限存储',
                    'API访问'
                ]
            ]
        ];
    }
}

订阅控制器

创建订阅控制器

<?php
namespace App\Http\Controllers;
use App\Models\Plan;
use App\Models\User;
use Illuminate\Http\Request;
use Stripe\Checkout\Session;
use Stripe\Customer;
use Stripe\Exception\ApiErrorException;
use Stripe\PaymentMethod;
class SubscriptionController extends Controller
{
    /**
     * 显示订阅页面
     */
    public function show()
    {
        $user = auth()->user();
        // 检查用户是否已订阅
        $subscription = $user->subscriptions()->active()->first();
        $plans = [
            [
                'id' => 'basic',
                'name' => '基础版',
                'monthly_price' => 9.99,
                'yearly_price' => 99.99,
            ],
            [
                'id' => 'pro',
                'name' => '专业版',
                'monthly_price' => 29.99,
                'yearly_price' => 299.99,
            ],
            // 更多套餐...
        ];
        return view('subscription.plans', [
            'user' => $user,
            'subscription' => $subscription,
            'plans' => $plans
        ]);
    }
    /**
     * 处理订阅
     */
    public function subscribe(Request $request)
    {
        $user = $request->user();
        $validated = $request->validate([
            'plan' => 'required|string|in:basic,pro,enterprise',
            'billing_cycle' => 'required|in:monthly,yearly',
            'payment_method' => 'required|string'
        ]);
        try {
            // 设置支付方式
            $user->updateDefaultPaymentMethod($validated['payment_method']);
            // 创建订阅
            $subscription = $user->newSubscription(
                'default',
                $this->getPriceId($validated['plan'], $validated['billing_cycle'])
            )->create($validated['payment_method']);
            // 记录订阅信息
            $user->subscription_details = [
                'plan' => $validated['plan'],
                'billing_cycle' => $validated['billing_cycle'],
            ];
            $user->save();
            return response()->json([
                'success' => true,
                'message' => '订阅成功!',
                'subscription' => $subscription
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => '订阅失败:' . $e->getMessage()
            ], 422);
        }
    }
    /**
     * 取消订阅
     */
    public function cancel(Request $request)
    {
        $user = $request->user();
        $subscription = $user->subscription('default');
        if ($subscription && $subscription->active()) {
            $subscription->cancel();
            return response()->json([
                'success' => true,
                'message' => '订阅已取消,将在当前周期结束后生效'
            ]);
        }
        return response()->json([
            'success' => false,
            'message' => '没有找到活动订阅'
        ], 404);
    }
    /**
     * 恢复订阅
     */
    public function resume(Request $request)
    {
        $user = $request->user();
        $subscription = $user->subscription('default');
        if ($subscription && $subscription->cancelled()) {
            $subscription->resume();
            return response()->json([
                'success' => true,
                'message' => '订阅已恢复'
            ]);
        }
        return response()->json([
            'success' => false,
            'message' => '无法恢复订阅'
        ], 404);
    }
    /**
     * 更换订阅套餐
     */
    public function swap(Request $request)
    {
        $user = $request->user();
        $validated = $request->validate([
            'new_plan' => 'required|string|in:basic,pro,enterprise',
            'billing_cycle' => 'required|in:monthly,yearly'
        ]);
        try {
            $user->subscription('default')->swap(
                $this->getPriceId($validated['new_plan'], $validated['billing_cycle'])
            );
            return response()->json([
                'success' => true,
                'message' => '订阅套餐已更新'
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => '更新失败:' . $e->getMessage()
            ], 422);
        }
    }
    /**
     * 获取价格ID
     */
    private function getPriceId($plan, $billingCycle)
    {
        return config("stripe.plans.{$plan}.{$billingCycle}_price_id");
    }
}

视图文件

订阅套餐选择视图

{{-- resources/views/subscription/plans.blade.php --}}
@extends('layouts.app')
@section('content')
<div class="container">
    <h1 class="mb-4">选择您的订阅套餐</h1>
    @if(session('error'))
        <div class="alert alert-danger">{{ session('error') }}</div>
    @endif
    <div class="row">
        @foreach($plans as $plan)
        <div class="col-md-4 mb-4">
            <div class="card h-100">
                <div class="card-header text-center">
                    <h3>{{ $plan['name'] }}</h3>
                </div>
                <div class="card-body">
                    <div class="text-center mb-3">
                        <span class="display-4">${{ $plan['monthly_price'] }}</span>
                        <span class="text-muted">/月</span>
                    </div>
                    <ul class="list-unstyled">
                        @foreach($plan['features'] ?? [] as $feature)
                        <li class="mb-2">
                            <i class="fas fa-check text-success"></i> {{ $feature }}
                        </li>
                        @endforeach
                    </ul>
                    <form method="POST" action="{{ route('subscription.subscribe') }}">
                        @csrf
                        <input type="hidden" name="plan" value="{{ $plan['id'] }}">
                        <input type="hidden" name="billing_cycle" value="monthly">
                        <div class="form-group">
                            <select name="payment_method" id="payment-method-{{ $plan['id'] }}" 
                                    class="form-control mb-3" style="display: none;">
                            </select>
                        </div>
                        <button type="submit" class="btn btn-primary btn-block">
                            @if($subscription && $subscription->active())
                                升级到{{ $plan['name'] }}
                            @else
                                订阅{{ $plan['name'] }}
                            @endif
                        </button>
                    </form>
                </div>
            </div>
        </div>
        @endforeach
    </div>
</div>
@section('scripts')
<script src="https://js.stripe.com/v3/"></script>
<script>
    // Stripe支付集成
    const stripe = Stripe('{{ config('services.stripe.key') }}');
    // 处理支付方式选择
    document.querySelectorAll('form').forEach(form => {
        form.addEventListener('submit', async (e) => {
            e.preventDefault();
            const paymentMethodInput = form.querySelector('select[name="payment_method"]');
            // 创建支付方式
            const { error, paymentMethod } = await stripe.createPaymentMethod({
                type: 'card',
                card: elements.getElement('card'),
            });
            if (error) {
                alert(error.message);
                return;
            }
            paymentMethodInput.value = paymentMethod.id;
            form.submit();
        });
    });
    // 创建卡片元素
    const elements = stripe.elements();
    const cardElement = elements.create('card');
    cardElement.mount('#card-element');
</script>
@endsection
@endsection

支付方式管理

支付方式管理控制器

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PaymentMethodController extends Controller
{
    /**
     * 添加支付方式
     */
    public function add(Request $request)
    {
        $user = $request->user();
        $validated = $request->validate([
            'payment_method_id' => 'required|string'
        ]);
        try {
            $paymentMethod = $user->addPaymentMethod($validated['payment_method_id']);
            // 设为默认支付方式
            $user->updateDefaultPaymentMethod($validated['payment_method_id']);
            return response()->json([
                'success' => true,
                'message' => '支付方式添加成功',
                'payment_method' => $paymentMethod
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => '添加失败:' . $e->getMessage()
            ], 422);
        }
    }
    /**
     * 获取支付方式列表
     */
    public function list(Request $request)
    {
        $user = $request->user();
        try {
            $paymentMethods = $user->paymentMethods();
            return response()->json([
                'success' => true,
                'payment_methods' => $paymentMethods->map(function ($method) {
                    return [
                        'id' => $method->id,
                        'brand' => $method->card->brand,
                        'last4' => $method->card->last4,
                        'exp_month' => $method->card->exp_month,
                        'exp_year' => $method->card->exp_year,
                        'is_default' => $method->id === $user->defaultPaymentMethod()?->id
                    ];
                })
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => '获取失败:' . $e->getMessage()
            ], 422);
        }
    }
    /**
     * 删除支付方式
     */
    public function remove(Request $request, $paymentMethodId)
    {
        $user = $request->user();
        try {
            $paymentMethod = $user->findPaymentMethod($paymentMethodId);
            $paymentMethod->delete();
            return response()->json([
                'success' => true,
                'message' => '支付方式删除成功'
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => '删除失败:' . $e->getMessage()
            ], 422);
        }
    }
    /**
     * 设置默认支付方式
     */
    public function setDefault(Request $request, $paymentMethodId)
    {
        $user = $request->user();
        try {
            $user->updateDefaultPaymentMethod($paymentMethodId);
            return response()->json([
                'success' => true,
                'message' => '默认支付方式已更新'
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => '更新失败:' . $e->getMessage()
            ], 422);
        }
    }
}

Webhook处理

创建Webhook控制器

<?php
namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller;
use Laravel\Cashier\Events\WebhookReceived;
use Laravel\Cashier\Events\WebhookHandled;
use Stripe\Webhook;
use Illuminate\Http\Request;
class StripeWebhookController extends Controller
{
    /**
     * 处理Stripe webhook
     */
    public function handleWebhook(Request $request)
    {
        $payload = $request->getContent();
        $sigHeader = $request->header('Stripe-Signature');
        $endpointSecret = config('services.stripe.webhook.secret');
        try {
            $event = Webhook::constructEvent(
                $payload,
                $sigHeader,
                $endpointSecret
            );
        } catch (\UnexpectedValueException $e) {
            // 无效的 payload
            return response('Invalid payload', 400);
        } catch (\Stripe\Exception\SignatureVerificationException $e) {
            // 无效的签名
            return response('Invalid signature', 400);
        }
        // 处理事件
        event(new WebhookReceived($event));
        try {
            $this->handleEvent($event);
        } catch (\Exception $e) {
            \Log::error('Stripe webhook error: ' . $e->getMessage());
        }
        event(new WebhookHandled($event));
        return response('Webhook handled', 200);
    }
    /**
     * 处理具体事件
     */
    protected function handleEvent($event)
    {
        switch ($event->type) {
            case 'invoice.payment_succeeded':
                $this->handlePaymentSucceeded($event->data->object);
                break;
            case 'invoice.payment_failed':
                $this->handlePaymentFailed($event->data->object);
                break;
            case 'customer.subscription.updated':
                $this->handleSubscriptionUpdated($event->data->object);
                break;
            case 'customer.subscription.deleted':
                $this->handleSubscriptionDeleted($event->data->object);
                break;
            case 'checkout.session.completed':
                $this->handleCheckoutCompleted($event->data->object);
                break;
        }
    }
    /**
     * 处理支付成功
     */
    protected function handlePaymentSucceeded($invoice)
    {
        $user = $this->findUserByStripeCustomerId($invoice->customer);
        if ($user) {
            // 记录支付信息
            PaymentLog::create([
                'user_id' => $user->id,
                'amount' => $invoice->amount_paid / 100,
                'currency' => $invoice->currency,
                'stripe_invoice_id' => $invoice->id,
                'status' => 'success',
            ]);
        }
    }
    /**
     * 处理支付失败
     */
    protected function handlePaymentFailed($invoice)
    {
        $user = $this->findUserByStripeCustomerId($invoice->customer);
        if ($user) {
            // 发送邮件通知
            Mail::to($user->email)->send(new PaymentFailedNotification());
            // 记录失败日志
            PaymentLog::create([
                'user_id' => $user->id,
                'amount' => $invoice->amount_due / 100,
                'currency' => $invoice->currency,
                'stripe_invoice_id' => $invoice->id,
                'status' => 'failed',
            ]);
        }
    }
    /**
     * 根据Stripe客户ID查找用户
     */
    protected function findUserByStripeCustomerId($customerId)
    {
        return User::where('stripe_id', $customerId)->first();
    }
}

中间件和路由配置

创建订阅检查中间件

<?php
namespace App\Http\Middleware;
use Closure;
class EnsureUserHasSubscription
{
    /**
     * 检查用户是否有有效订阅
     */
    public function handle($request, Closure $next, $plan = null)
    {
        $user = $request->user();
        if (!$user) {
            return redirect()->route('login');
        }
        // 检查是否有活动订阅
        if (!$user->subscribed('default')) {
            return redirect()->route('subscription.plans')
                ->with('error', '您需要订阅才能访问此功能');
        }
        // 检查特定套餐
        if ($plan && !$user->subscribedToPlan($plan)) {
            return redirect()->route('subscription.plans')
                ->with('error', '您需要升级到更高套餐才能访问此功能');
        }
        return $next($request);
    }
}

路由配置

<?php
// routes/web.php
use App\Http\Controllers\SubscriptionController;
use App\Http\Controllers\PaymentMethodController;
use App\Http\Controllers\Webhook\StripeWebhookController;
// 订阅相关路由
Route::middleware(['auth'])->group(function () {
    // 订阅页面
    Route::get('/subscription/plans', [SubscriptionController::class, 'show'])
        ->name('subscription.plans');
    // 订阅操作
    Route::post('/subscription/subscribe', [SubscriptionController::class, 'subscribe'])
        ->name('subscription.subscribe');
    Route::post('/subscription/cancel', [SubscriptionController::class, 'cancel'])
        ->name('subscription.cancel');
    Route::post('/subscription/resume', [SubscriptionController::class, 'resume'])
        ->name('subscription.resume');
    Route::post('/subscription/swap', [SubscriptionController::class, 'swap'])
        ->name('subscription.swap');
    // 支付方式管理
    Route::get('/payment-methods', [PaymentMethodController::class, 'list'])
        ->name('payment.methods.list');
    Route::post('/payment-methods/add', [PaymentMethodController::class, 'add'])
        ->name('payment.methods.add');
    Route::delete('/payment-methods/{paymentMethod}', [PaymentMethodController::class, 'remove'])
        ->name('payment.methods.remove');
    Route::post('/payment-methods/{paymentMethod}/default', [PaymentMethodController::class, 'setDefault'])
        ->name('payment.methods.set-default');
});
// Webhook路由(无需CSRF和认证)
Route::post('/stripe/webhook', [StripeWebhookController::class, 'handleWebhook'])
    ->withoutMiddleware(['csrf', 'auth']);

测试用例

编写订阅测试

<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class SubscriptionTest extends TestCase
{
    use RefreshDatabase;
    /**
     * 测试用户订阅
     */
    public function test_user_can_subscribe_to_plan()
    {
        $user = User::factory()->create();
        $this->actingAs($user)
            ->post('/subscription/subscribe', [
                'plan' => 'pro',
                'billing_cycle' => 'monthly',
                'payment_method' => 'pm_test',
            ])
            ->assertStatus(200)
            ->assertJson(['success' => true]);
    }
    /**
     * 测试未登录用户无法订阅
     */
    public function test_unauthenticated_user_cannot_subscribe()
    {
        $this->post('/subscription/subscribe', [
            'plan' => 'pro',
            'billing_cycle' => 'monthly',
            'payment_method' => 'pm_test',
        ])->assertRedirect('/login');
    }
    /**
     * 测试取消订阅
     */
    public function test_user_can_cancel_subscription()
    {
        $user = User::factory()->create();
        // 模拟用户有活动订阅
        $user->newSubscription('default', 'plan_pro')
            ->create('pm_test');
        $this->actingAs($user)
            ->post('/subscription/cancel')
            ->assertStatus(200)
            ->assertJson(['success' => true]);
    }
}

最佳实践建议

缓存配置

// config/cache.php
'default' => env('CACHE_DRIVER', 'redis'),
// 缓存订阅状态
Cache::tags(['user_' . $user->id, 'subscription'])->remember('status', 3600, function () use ($user) {
    return $user->subscribed('default');
});

错误处理优化

// app/Exceptions/SubscriptionException.php
namespace App\Exceptions;
use Exception;
class SubscriptionException extends Exception
{
    public function render($request)
    {
        return response()->json([
            'success' => false,
            'message' => $this->getMessage()
        ], 422);
    }
}

安全检查

// 检查用户是否已被禁止订阅
if ($user->isBanned()) {
    throw new SubscriptionException('您的账号已被禁止进行订阅操作');
}

这个完整的Laravel Cashier订阅支付集成方案包括:

  1. 基础安装和配置
  2. 模型准备
  3. 订阅计划管理
  4. 控制器和视图
  5. 支付方式管理
  6. Webhook处理
  7. 中间件和安全
  8. 测试用例

使用这个方案,您可以快速实现功能完善的订阅支付系统,记得在实际部署时配置好Stripe的Webhook,并在Stripe后台设置相应的订阅产品。

抱歉,评论功能暂时关闭!