> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Laravel Cashier (Stripe)

> 使用 Laravel Cashier (Stripe) 实现订阅计费、单次支付、发票和 Webhook 的基本流程。

## 概述

Laravel Cashier (Stripe) 是用于在 Laravel 中使用 Stripe 计费功能的官方扩展包。你可以通过统一的 API 完成订阅的创建、状态检查、取消、单次支付、下载发票以及处理 Webhook。

## 安装与配置

首先安装 Cashier，并创建所需的数据库表。

```shell theme={null}
composer require laravel/cashier
php artisan vendor:publish --tag="cashier-migrations"
php artisan migrate
```

然后在 `App\Models\User` 等计费对象模型上添加 `Billable` trait。

```php theme={null}
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
}
```

在 `.env` 中配置 Stripe 密钥。

```ini theme={null}
STRIPE_KEY=your-stripe-key
STRIPE_SECRET=your-stripe-secret
STRIPE_WEBHOOK_SECRET=your-stripe-webhook-secret
```

## 顾客管理

如果 Stripe Customer 可能还未创建，可以使用 `createOrGetStripeCustomer()`。

```php theme={null}
$stripeCustomer = $user->createOrGetStripeCustomer();
```

如果要显式创建，则使用 `createAsStripeCustomer()`。

```php theme={null}
$stripeCustomer = $user->createAsStripeCustomer();
```

## 订阅

### 新建订阅

通过 `newSubscription()` 和 `create()` 开始订阅。
`$paymentMethodId` 传入从 Stripe.js 等获取到的 Payment Method ID。

```php theme={null}
$user->newSubscription('default', 'price_monthly')
    ->create($paymentMethodId);
```

`price_monthly` 只是示例。实际使用时请传入你在 Stripe 后台创建的 Price ID。

### 状态检查

使用 `subscribed()` 可以判断订阅是否有效。

```php theme={null}
if ($user->subscribed('default')) {
    // Active subscription...
}
```

### 取消与恢复

```php theme={null}
$user->subscription('default')->cancel();

if ($user->subscription('default')->onGracePeriod()) {
    // The user is on the grace period...
}

$user->subscription('default')->resume();
```

```mermaid theme={null}
stateDiagram-v2
    [*] --> Active
    Active --> GracePeriod: cancel()
    GracePeriod --> Active: resume()
    GracePeriod --> Canceled: grace period ends
    Active --> Canceled: cancelNow()
```

## 单次支付（Charge）

`charge()` 可以进行一次性收费。金额以货币的最小单位传入（例如 USD 时，`100` 代表 `$1.00`）。
这里 `$paymentMethodId` 同样使用 Stripe 上创建的 Payment Method ID。

```php theme={null}
$payment = $user->charge(100, $paymentMethodId);
```

如果支付失败，`charge()` 会抛出异常。

## Payment Element

使用 Stripe 的 [Payment Element](https://stripe.com/docs/payments/payment-element)，可以统一处理卡、Apple Pay、Google Pay、iDEAL 等多种支付方式。

### 在订阅中使用

先创建 Setup Intent 并传给视图。

```php theme={null}
return view('subscribe', [
    'intent' => $user->createSetupIntent()
]);
```

在 Blade 视图中挂载 Payment Element，并传入 Setup Intent 的 `client_secret`。

```html theme={null}
<div id="payment-element"></div>
<button id="submit">Subscribe</button>

<script src="https://js.stripe.com/v3/"></script>
<script>
    const stripe = Stripe('stripe-public-key');

    const elements = stripe.elements({
        clientSecret: '{{ $intent->client_secret }}'
    });

    const paymentElement = elements.create('payment');

    paymentElement.mount('#payment-element');

    document.getElementById('submit').addEventListener('click', async () => {
        const { error } = await stripe.confirmSetup({
            elements,
            confirmParams: {
                return_url: '{{ route("subscription.complete") }}',
            },
        });

        if (error) {
            // 向用户展示错误信息...
        }
    });
</script>
```

Stripe 会在跳转回来的 URL 上附加 `setup_intent` 参数。使用它获取 Payment Method ID，然后创建订阅。

```php theme={null}
use Illuminate\Http\Request;

Route::get('/subscription/complete', function (Request $request) {
    $setupIntent = $request->user()->findSetupIntent(
        $request->setup_intent
    );

    $paymentMethod = $setupIntent->payment_method;

    $request->user()
        ->newSubscription('default', 'price_xxx')
        ->create($paymentMethod);

    return redirect('/dashboard');
})->name('subscription.complete');
```

### 在单次支付中使用

对于单次支付，使用 `pay()` 创建 Payment Intent。Order 模型需要包含 `user_id`、`amount`、`status`、`stripe_payment_intent_id` 字段。

```php theme={null}
use App\Models\Order;
use Illuminate\Http\Request;

Route::post('/pay', function (Request $request) {
    $amount = 1000;

    $payment = $request->user()->pay($amount);

    $order = Order::create([
        'user_id' => $request->user()->id,
        'amount' => $amount,
        'status' => 'pending',
        'stripe_payment_intent_id' => $payment->id,
    ]);

    return view('checkout', [
        'clientSecret' => $payment->client_secret,
        'order' => $order,
    ]);
});
```

挂载 Payment Element 并确认支付。

```html theme={null}
<div id="payment-element"></div>
<button id="submit">Pay Now</button>

<script src="https://js.stripe.com/v3/"></script>
<script>
    const stripe = Stripe('stripe-public-key');

    const elements = stripe.elements({
        clientSecret: '{{ $clientSecret }}'
    });

    const paymentElement = elements.create('payment');

    paymentElement.mount('#payment-element');

    document.getElementById('submit').addEventListener('click', async () => {
        const { error } = await stripe.confirmPayment({
            elements,
            confirmParams: {
                return_url: '{{ route("payment.complete") }}',
            },
        });

        if (error) {
            // 向用户展示错误信息...
        }
    });
</script>
```

跳转回来后，通过 `payment_intent` 参数查找 Order，并在确认 Payment Intent 属于当前已认证用户且状态为 `succeeded` 之后再完成订单。

```php theme={null}
use App\Models\Order;
use Illuminate\Http\Request;

Route::get('/payment/complete', function (Request $request) {
    $order = Order::where('user_id', $request->user()->id)
        ->where('stripe_payment_intent_id', $request->payment_intent)
        ->firstOrFail();

    $paymentIntent = $request->user()
        ->stripe()
        ->paymentIntents
        ->retrieve($request->payment_intent);

    if ($paymentIntent->customer === $request->user()->stripe_id &&
        $paymentIntent->status === 'succeeded') {
        $order->update(['status' => 'paid']);

        // 完成订单的处理...
    }

    return redirect('/dashboard');
})->name('payment.complete');
```

```mermaid theme={null}
sequenceDiagram
    participant User as 用户
    participant App as Laravel<br>应用
    participant Stripe

    User->>App: POST /pay
    App->>Stripe: pay() 创建 PaymentIntent
    Stripe-->>App: client_secret
    App-->>User: 展示 checkout 视图
    User->>Stripe: confirmPayment()
    Stripe-->>User: 重定向到 return_url
    User->>App: GET /payment/complete
    App->>Stripe: 获取并验证 PaymentIntent
    App->>App: order.status = paid
    App-->>User: 重定向到仪表盘
```

## 发票

使用 `invoices()` 可以获取发票列表。

```php theme={null}
$invoices = $user->invoices();
```

下载 PDF 需要先安装 `dompdf/dompdf`，然后使用 `downloadInvoice()`。`$invoiceId` 传入通过 `invoices()` 得到的发票 ID。

```shell theme={null}
composer require dompdf/dompdf
```

```php theme={null}
return $user->downloadInvoice($invoiceId);
```

## Webhook 配置

Cashier 会自动为 Stripe Webhook 注册路由，默认使用 `/stripe/webhook`。请在 Stripe 后台中配置该 URL。

可以通过 `cashier:webhook` 命令创建 Webhook。

```shell theme={null}
php artisan cashier:webhook
```

需要将 `stripe/*` 从 CSRF 保护中排除。

```php theme={null}
->withMiddleware(function (Middleware $middleware): void {
    $middleware->preventRequestForgery(except: [
        'stripe/*',
    ]);
})
```

在 `.env` 中配置 `STRIPE_WEBHOOK_SECRET` 后，Cashier 的签名校验中间件就能对 Webhook 请求进行校验。


## Related topics

- [Laravel 10 升级到 11](/zh/blog/upgrade-10-to-11.md)
- [2026 年 6 月 Laravel 更新](/zh/blog/changelog/202606.md)
- [CSRF 保护](/zh/csrf.md)
- [Laravel 11 之后的应用结构](/zh/advanced/app-structure.md)
- [服务容器](/zh/service-container.md)
