本文目录导读:

我来为您详细介绍如何在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订阅支付集成方案包括:
- 基础安装和配置
- 模型准备
- 订阅计划管理
- 控制器和视图
- 支付方式管理
- Webhook处理
- 中间件和安全
- 测试用例
使用这个方案,您可以快速实现功能完善的订阅支付系统,记得在实际部署时配置好Stripe的Webhook,并在Stripe后台设置相应的订阅产品。