本文目录导读:

我来为您详细介绍如何在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后台设置相应的订阅产品。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播