# Laravel Cashier (Stripe) - [简介](#introduction) - [升级 Cashier](#upgrading-cashier) - [安装](#installation) - [数据库迁移](#database-migrations) - [配置信息](#configuration) - [计费模型](#billable-model) - [API 密钥](#api-keys) - [货币配置](#currency-configuration) - [税务配置](#tax-configuration) - [日志](#logging) - [使用自定义模型](#using-custom-models) - [消费者](#customers) - [获取消费者](#retrieving-customers) - [创建消费者](#creating-customers) - [更新消费者](#updating-customers) - [余额](#balances) - [税号](#tax-ids) - [使用 Stripe 同步客户数据](#syncing-customer-data-with-stripe) - [计费门户](#billing-portal) - [支付方式](#payment-methods) - [存储支付方式](#storing-payment-methods) - [检索支付方式](#retrieving-payment-methods) - [判断用户是否有支付方式](#check-for-a-payment-method) - [更新默认支付方式](#updating-the-default-payment-method) - [添加支付方式](#adding-payment-methods) - [删除支付方式](#deleting-payment-methods) - [订阅内容](#subscriptions) - [创建订阅](#creating-subscriptions) - [检查订阅状态](#checking-subscription-status) - [修改价格](#changing-prices) - [订阅数量](#subscription-quantity) - [多方案订阅计划](#multiprice-subscriptions) - [计量计费](#metered-billing) - [订阅税](#subscription-taxes) - [订阅锚定日期](#subscription-anchor-date) - [取消订阅](#cancelling-subscriptions) - [恢复订阅](#resuming-subscriptions) - [订阅试用](#subscription-trials) - [预先使用付款方式](#with-payment-method-up-front) - [没有预先付款方式](#without-payment-method-up-front) - [延长试用期](#extending-trials) - [处理 Stripe Webhooks](#handling-stripe-webhooks) - [定义 Webhook 事件处理器](#defining-webhook-event-handlers) - [验证 Webhook 签名](#verifying-webhook-signatures) - [单次收费](#single-charges) - [基本使用](#simple-charge) - [带发票的支付](#charge-with-invoice) - [退款](#refunding-charges) - [结账](#checkout) - [产品结账](#product-checkouts) - [单次支付结账](#single-charge-checkouts) - [订阅结账](#subscription-checkouts) - [收集税号](#collecting-tax-ids) - [发票](#invoices) - [获取发票](#retrieving-invoices) - [即将发布的发票](#upcoming-invoices) - [预览订阅发票](#previewing-subscription-invoices) - [生成发票 PDF](#generating-invoice-pdfs) - [处理支付失败](#handling-failed-payments) - [强大的客户身份验证 (SCA)](#strong-customer-authentication) - [需要额外确认的支付](#payments-requiring-additional-confirmation) - [非会话支付通知](#off-session-payment-notifications) - [Stripe SDK](#stripe-sdk) - [测试](#testing) ## 简介 [Laravel Cashier Stripe](https://github.com/laravel/cashier-stripe) 为 [Stripe](https://stripe.com) 的订阅计费服务提供了一个富有表现力、流畅的接口。它处理了几乎所有你害怕编写的订阅计费样板代码。除了基本的订阅管理,Cashier 还可以处理优惠券、交换订阅、订阅 「数量」、取消宽限期,甚至生成发票 PDF。 ## 升级 Cashier 升级到新版本的 Cashier 时,请务必仔细阅读 [升级指南](https://github.com/laravel/cashier-stripe/blob/master/UPGRADE.md)。 > 注意:为了防止破坏性变更,Cashier 使用固定的 Stripe API 版本。 Cashier 13 使用 Stripe API 版本 `2020-08-27` 。Stripe API 版本将在次要版本上更新,以利用新的 Stripe 功能和改进。 ## 安装 首先,使用 Composer 为 Stripe 安装 Cashier 扩展包: ```shell composer require laravel/cashier ``` > 注意:为确保 Cashier 正确处理所有 Stripe 事件,请记得 [设置 Cashier 的 webhook](#handling-stripe-webhooks)。 ### 数据库迁移 Cashier 的服务提供器注册了自己的数据库迁移目录,因此请记住在安装此包后迁移数据库。Cashier 迁移将向 `users` 表中添加多个列,并创建一个新的 `subscriptions` 表来保存客户的所有订阅: ```shell php artisan migrate ``` 如果需要覆盖 Cashier 附带的迁移,可以使用 `vendor:publish` Artisan 命令发布它们: ```shell php artisan vendor:publish --tag="cashier-migrations" ``` 如果你想阻止 Cashier 的迁移完全运行,可以使用 Cashier 提供的`ignoreMigrations` 方法。通常应在 `AppServiceProvider` 类的 `register` 方法中调用此方法: use Laravel\Cashier\Cashier; /** * Register any application services. * * @return void */ public function register() { Cashier::ignoreMigrations(); } > 注意:Stripe 建议用于存储 Stripe 标识符的任何列都应区分大小写。因此,在使用 MySQL 时,应该确保将 `stripe_id` 列排序规则设置为 `utf8_bin` 。更多关于这方面的信息可以在 [Stripe 文档](https://stripe.com/docs/upgrades#what-changes-does-stripe-consider-to-be-backwards-compatible) 中找到。 ## 配置 ### 订单模型 在使用 Cashier 之前,需要将 `Billable` trait 添加到可订单模型定义中。通常会放在 `App\Models\User` 模型中。这个特性提供了多个方法以便执行常用支付任务,如创建订阅、应用优惠券和更新支付方法信息: use Laravel\Cashier\Billable; class User extends Authenticatable { use Billable; } Cashier 默认假设你的 Billable 模型是 Laravel 自带的 `App\Models\User` 类。如果需要修改可以在 `useCustomerModel` 方法定义一个不同的模型。通常此方法在 `AppServiceProvider` 类的`boot`方法中被调用: use App\Models\Cashier\User; use Laravel\Cashier\Cashier; /** * Bootstrap any application services. * * @return void */ public function boot() { Cashier::useCustomerModel(User::class); } > 注意:如果你使用的不是 Laravel 自带的 `App\Models\User` 模型,需要发布并修改默认的 [Cashier 迁移](#installation) 文件以匹配你使用模型对应的表名。 ### API 秘钥 接下来需要在 `.env` 文件中配置 Stripe 秘钥,可以在 Stripe 后台控制面板中获取Stripe API 秘钥: ```ini STRIPE_KEY=your-stripe-key STRIPE_SECRET=your-stripe-secret ``` ### 货币配置 Cashier 默认货币是美元 (USD),可以在 `.env` 中设置 `CASHIER_CURRENCY` 环境变量来修改默认的货币配置: ```ini CASHIER_CURRENCY=eur ``` 除了配置 Cashier 的货币之外,还可以在格式化用于显示在发票上的金额时指定本地化配置。在底层,Cashier 使用了 [PHP 的 `NumberFormatter` 类](https://www.php.net/manual/en/class.numberformatter.php) 来设置本地货币: ```ini CASHIER_CURRENCY_LOCALE=nl_BE ``` > 注意:为了使用本地化配置而不是 `en`,需要确保安装了 PHP `ext-intl` PHP 扩展并在服务器上启用配置。 ### 税务配置 感谢[Stripe 税务](https://stripe.com/tax),可以自动计算 Stripe 生成的所有发票的税费。 可以通过应用程序的 `App\Providers\AppServiceProvider`类的 `boot` 方法中调用 `calculateTaxes` 来启用自动税务计算: use Laravel\Cashier\Cashier; /** * Bootstrap any application services. * * @return void */ public function boot() { Cashier::calculateTaxes(); } 启动税务计算后,任何新订阅和生成的一次性发票都会进行自动税务计算。 为了使这个功能正常使用,客户的账单明细中例如客户姓名、住址、发票 ID 需要同步到 Stripe。你可以使用 Cashier 提供的 [客户数据同步](#syncing-customer-data-with-stripe) 和 [Tax ID](#tax-ids) 方法来完成此操作。 > 注意:遗憾的是,目前不支持计算 [单笔交易](#single-charges) 或 [单笔交易支付](#single-charge-checkouts)。此外 Stripe Tax 目在测试期间仅限“受邀”使用。你可以通过 [Stripe Tax 网站](https://stripe.com/tax#request-access)请求访问 Stripe 税务。 ### 日志 Cashier 允许你指定日志通道来记录所有与 Stripe 相关的异常。可以通过在 `.env` 中配置 `CASHIER_LOGGER` 来指定: ```ini CASHIER_LOGGER=stack ``` 对 Stripe 的 API 调用生成的异常将通过应用程序的默认日志通道记录。 ### 使用自定义模型 你可以通过定义自己的模型并扩展相应的 `Cashier` 模型来自由扩展 Cashier 内部的模型,增加一些方法: use Laravel\Cashier\Subscription as CashierSubscription; class Subscription extends CashierSubscription { // ... } 定义模型后,可以通过 `Laravel\Cashier\Cashier` 类配置 Cashier 使用自定义的模型。通常还需要在 `App\Providers\AppServiceProvider` 类的 `boot` 中注册一下: use App\Models\Cashier\Subscription; use App\Models\Cashier\SubscriptionItem; /** * Bootstrap any application services. * * @return void */ public function boot() { Cashier::useSubscriptionModel(Subscription::class); Cashier::useSubscriptionItemModel(SubscriptionItem::class); } ## 消费者 ### 查询消费者 你可以使用 `Cashier::findBillable` 方法通过 Stripe ID 查询消费者信息。该方法返回的是一个 billable 模型实例: use Laravel\Cashier\Cashier; $user = Cashier::findBillable($stripeId); ### 创建消费者 有时候,你可能希望在不开始订阅的情况下创建一个 Stripe 消费者。这可以通过 `createAsStripeCustomer` 方法来实现: $stripeCustomer = $user->createAsStripeCustomer(); 消费者在 Stripe 中创建后,可以过一段时间再开始订阅。还可以使用可选的 `$options` 数组传入所有 [Stripe API 支持的创建消费者参数](https://stripe.com/docs/api/customers/create) 额外支持的参数: $stripeCustomer = $user->createAsStripeCustomer($options); 如果你要返回消费者对象,你可以使用 `asStripeCustomer` 方法: $stripeCustomer = $user->asStripeCustomer(); 此外,可以使用 `createOrGetStripeCustomer` 方法来获取不确定查询的 Stripe 消费者在 Stripe 中是否已经存在。如果不存在,这个方法会创建一个消费者: $stripeCustomer = $user->createOrGetStripeCustomer(); ### 更新消费者 有时候,你可能想要使用额外的信息直接更新 Stripe 顾客信息,可以使用 `updateStripeCustomer` 方法来完成。这个方法接受 [Stripe API](https://stripe.com/docs/api/customers/update) 数组: $stripeCustomer = $user->updateStripeCustomer($options); ### 余额 Stripe 允许你贷记或借记客户的「余额」。稍后,此余额将在新发票上贷记或借记。要检查客户的总余额,你可以使用`balance`可用于计费模型的方法。该`balance`方法将返回以客户货币表示的余额的格式化字符串表示: $balance = $user->balance(); 要记入客户的余额,可以为该`creditBalance`方法提供一个值。如果你愿意,还可以提供描述: ``` $user->creditBalance(500, 'Premium customer top-up.'); ``` 为该方法提供一个值debitBalance将从客户的余额中扣除: ``` $user->debitBalance(300, 'Bad usage penalty.'); ``` `applyBalance` 方法会创建一条客户余额流水记录。可以通过调用 `balanceTransactions` 方法获取余额交易记录,这有助于提供借记或贷记记录给客户查看: ``` // 检索所有交易... $transactions = $user->balanceTransactions(); foreach ($transactions as $transaction) { // Transaction amount... $amount = $transaction->amount(); // $2.31 // Retrieve the related invoice when available... $invoice = $transaction->invoice(); } ``` ### 税号 Cashier 提供了一种管理客户税号的简便方法。`taxIds` 例如,`taxIds` 方法可用于检索作为集合分配给客户的所有 [税号](https://stripe.com/docs/api/customer_tax_ids/object): $taxIds = $user->taxIds(); 您还可以通过标识符检索客户的特定税号: $taxId = $user->findTaxId('txi_belgium'); 您可以通过向 `createTaxId` 方法提供有效的 [type](https://stripe.com/docs/api/customer_tax_ids/object#tax_id_object-type) 和值来创建新的税号: $taxId = $user->createTaxId('eu_vat', 'BE0123456789'); `createTaxId` 方法将立即将增值税 ID 添加到客户的帐户中。 [增值税 ID 的验证也由 Stripe 完成](https://stripe.com/docs/invoicing/customer/tax-ids#validation); 然而,这是一个异步的过程。 您可以通过订阅 `customer.tax_id.updated` webhook 事件并检查 [增值税 ID `verification` 参数](https://stripe.com/docs/api/customer_tax_ids/object#tax_id_object- 确认)。 有关处理 webhook 的更多信息,请参阅 [有关定义 webhook 处理程序的文档](#handling-stripe-webhooks)。 您可以使用 `deleteTaxId` 方法删除税号: $user->deleteTaxId('txi_belgium'); ### 使用 Stripe 同步客户数据 通常,当您的应用程序的用户更新他们的姓名、电子邮件地址或其他也由 Stripe 存储的信息时,您应该通知 Stripe 更新。 这样一来,Stripe 的信息副本将与您的应用程序同步。 要自动执行此操作,您可以在计费模型上定义一个事件侦听器,以响应模型的「更新」事件。然后,在您的事件监听器中,您可以在模型上调用 `syncStripeCustomerDetails` 方法: use function Illuminate\Events\queueable; /** * 模型的「引导」方法。 * * @return void */ protected static function booted() { static::updated(queueable(function ($customer) { if ($customer->hasStripeId()) { $customer->syncStripeCustomerDetails(); } })); } 现在,每次更新您的客户模型时,其信息都会与 Stripe 同步。 为方便起见,Cashier 会在初始创建客户时自动将您客户的信息与 Stripe 同步。 您可以通过覆盖 Cashier 提供的各种方法来自定义用于将客户信息同步到 Stripe 的列。 例如,当 Cashier 将客户信息同步到 Stripe 时,您可以重写 `stripeName` 方法来自定义应该被视为客户「姓名」的属性: /** * 获取应同步到 Stripe 的客户名称。 * * @return string|null */ public function stripeName() { return $this->company_name; } 同样,您可以复写 `stripeEmail`、`stripePhone` 和 `stripeAddress` 方法。 当[更新 Stripe 客户对象](https://stripe.com/docs/api/customers/update) 时,这些方法会将信息同步到其相应的客户参数。 如果您希望完全控制客户信息同步过程,您可以复写 `syncStripeCustomerDetails` 方法。 ### 订单入口 Stripe 提供了一个简单的方式来[设置订单入口](https://stripe.com/docs/billing/subscriptions/customer-portal)以便用户可以管理订阅、支付方法、以及查看历史账单。你可以在控制器或路由中使用 `redirectToBillingPortal` 方法将用户重定向到账单入口: use Illuminate\Http\Request; Route::get('/billing-portal', function (Request $request) { return $request->user()->redirectToBillingPortal(); }); 默认情况下,当用户完成对订阅的管理后,会将能够通过 Stripe 计费门户中的链接返回到应用的 home 路由,你可以通过传递 URL 作为 `redirectToBillingPortal` 方法的参数来自定义用户返回的 URL: use Illuminate\Http\Request; Route::get('/billing-portal', function (Request $request) { return $request->user()->redirectToBillingPortal(route('billing')); }); 如果你只想要生成订单入口的 URL,可以使用 `billingPortalUrl` 方法: $url = $request->user()->billingPortalUrl(route('billing')); ## 支付方式 ### 存储支付方式 为了使用 Stripe 创建订阅或者进行「一次性」支付,你需要存储支付方法并从 `Stripe` 中获取对应的标识符。这种方式可用于实现你是否计划使用这个支付方法进行订阅还是单次收费,下面我们分别来介绍这两种方法。 #### 用于订阅的支付方法 当我们为消费者存储信用卡支付方式以便将来使用时,必须使用 Stripe Setup Intents API 来安全地收集顾客的支付方式细节,比如回调错误信息 。「Setup Intents」用于告知 Stripe 使用顾客的支付方法进行收费的意图。Cashier 的 `Billable` Trait 包含了 `createSetupIntent` 方法来创建新的「Setup Intent」,你需要在渲染收集顾客支付方法细节表单的路由或控制器方法中调用这个方法: return view('update-payment-method', [ 'intent' => $user->createSetupIntent() ]); 创建完 Setup Intent 并将其传递给视图之后,你需要在收集支付方法的元素中添加它的 secret。例如,参考下面这个「更新支付方法」表单: ```html
``` 接下来,会通过 Stripe.js 库添加一个 [Stripe 元素](https://stripe.com/docs/stripe-js)到表单,并安全地收集顾客的支付细节: ```html ``` 然后,使用 [Stripe 的 handleCardSetup 方法](https://stripe.com/docs/stripe-js/reference#stripe-handle-card-setup)验证信用卡并从 Stripe 获取一个安全的「支付方法标识符」: ```js const cardHolderName = document.getElementById('card-holder-name'); const cardButton = document.getElementById('card-button'); const clientSecret = cardButton.dataset.secret; cardButton.addEventListener('click', async (e) => { const { setupIntent, error } = await stripe.confirmCardSetup( clientSecret, { payment_method: { card: cardElement, billing_details: { name: cardHolderName.value } } } ); if (error) { // 显示错误信息给用户... } else { // 信用卡验证成功... } }); ``` 在 Stripe 验证卡后,您可以将生成的 `setupIntent.payment_method` 标识符传递给您的 Laravel 应用程序,并在其中将其附加到客户。支付方式可以是[添加为新的支付方式](#adding-payment-methods) 或[用于更新默认支付方式](#updating-the-default-payment-method)。您还可以立即使用付款方式标识符来[创建新订阅](#creating-subscriptions)。 > 技巧:如果你想要了解更多关于 Setup Intents 以及获取顾客支付细节的信息,可以参考 [Stripe 官方文档](https://stripe.com/docs/payments/save-and-reuse#php)。 #### 用于单次付费的支付方法 当然,如果消费者支付方法使用的是单次付费,我们只需要使用支付方法标识符一次即可。由于 Stripe 本身的限制,你不可以使用存储的默认顾客支付方法进行单次付费,必须允许顾客通过 Stripe.js 库进入他们的支付方法细节。例如,参考下面这个表单: ```html ``` 接下来跟上面文档相似,通过 Stripe.js 库添加 [Stripe 元素](https://stripe.com/docs/stripe-js) 到这个表单,并安全地收集顾客的支付细节: ```html ``` 然后,使用 [Stripe 的 createPaymentMethod 方法](https://stripe.com/docs/stripe-js/reference#stripe-create-payment-method)验证信用卡并获取一个安全的「支付方法标识符」: ```js const cardHolderName = document.getElementById('card-holder-name'); const cardButton = document.getElementById('card-button'); cardButton.addEventListener('click', async (e) => { const { paymentMethod, error } = await stripe.createPaymentMethod( 'card', cardElement, { billing_details: { name: cardHolderName.value } } ); if (error) { // Display "error.message" to the user... } else { // The card has been verified successfully... } }); ``` 如果信用卡验证成功,就可以传递 `paymentMethod.id` 到你的 Laravel 应用并处理[一次性付费](https://learnku.com/docs/laravel/8.5/billing/10421#Simple-Charge)。 ### 获取支付方法 Billable 模型实例上的 `paymentMethods` 方法会返回 `Laravel\Cashier\PaymentMethod` 实例集合: $paymentMethods = $user->paymentMethods(); 默认情况下,此方法将返回 `card` 类型的付款方式。 要检索不同类型的付款方式,您可以将 `type` 作为参数传递给该方法: $paymentMethods = $user->paymentMethods('sepa_debit'); 要获取消费者默认的支付方法,可以使用 `defaultPaymentMethod` 方法: $paymentMethod = $user->defaultPaymentMethod(); 还可以使用 `findPaymentMethod` 方法通过 Billable 模型获取指定支付方法: $paymentMethod = $user->findPaymentMethod($paymentMethodId); ### 判断消费者是否拥有支付方法 要判断某个 `Billable` 模型对应账户是否有默认的支付方法,可以使用 `hasDefaultPaymentMethod` 方法: if ($user->hasDefaultPaymentMethod()) { // } 要判断某个 `Billable` 模型对应账户是否有支付方法,可以使用 `hasPaymentMethod` 方法: if ($user->hasPaymentMethod()) { // } 此方法将确定计费模型是否具有 `card` 类型的付款方式。 要确定模型是否存在另一种类型的付款方式,您可以将 `type` 作为参数传递给该方法: if ($user->hasPaymentMethod('sepa_debit')) { // } ### 更新默认支付方式 更新顾客的默认支付方式信息 可以用 `updateDefaultPaymentMethod` 方法 ,该方法接收一个 Stripe 支付方法标识符并将新的支付方法分配为默认的支付方法: $user->updateDefaultPaymentMethod($paymentMethod); 要同步应用的默认支付方法信息到 Stripe 顾客的默认支付方式信息,可以使用 `updateDefaultPaymentMethodFromStripe` 方法: $user->updateDefaultPaymentMethodFromStripe(); > 注意:消费者的默认支付方式只能用于发票和创建新的订阅,由于 Stripe 的限制,不能将其用于单次付费。 ### 添加支付方式 要添加新的支付方式,可以调用 Billable 用户的 `addPaymentMethod` 方法,并传递支付方法标识符: $user->addPaymentMethod($paymentMethod); > 技巧:要了解如何获取支付方法标识符,请参考[支付方式存储文档](https://learnku.com/docs/laravel/8.5/billing/10421#storing-payment-methods). ### 删除支付方式 删除一个支付方法,你可以调用要删除的 `Laravel\Cashier\PaymentMethod` 实例上的 `delete` 方法: $paymentMethod->delete(); 删除指定 Billable 模型上的所有支付方法信息可以使用 `deletePaymentMethods` 方法: $user->deletePaymentMethod('pm_visa'); `deletePaymentMethods` 方法将删除计费模型的所有付款方式信息: $user->deletePaymentMethods(); 默认情况下,此方法将删除 `card` 类型的付款方式。 要删除不同类型的付款方式,您可以将 `type` 作为参数传递给该方法: $user->deletePaymentMethods('sepa_debit'); > 注意:如果用户有活动订阅,您的应用程序不应允许他们删除其默认付款方式。 ## 订阅内容 订阅提供了一种为消费者设置定期付款的方式。由收银员管理的 Stripe 订阅 提供对多个订阅计划、订阅数量、试用等的支持。 ### 创建订阅 创建一个订阅,首先要获取一个账单模型的实例,通常是 `App\Models\User` 的实例。获取到该模型实例之后,可以使用 `newSubscription` 方法来创建该模型的订阅: use Illuminate\Http\Request; Route::post('/user/subscribe', function (Request $request) { $request->user()->newSubscription( 'default', 'price_monthly' )->create($request->paymentMethodId); // ... }); `newSubscription` 方法的第一个参数是该订阅的名字,如果应用只有一个订阅,可以将其称作 `default` 或 `primary`,第二个参数用于指定用户订阅的计划,这个值对应 `Stripe` 中相应计划的标识符。 `create` 方法接收 [Stripe 支付方法标识符](https://laravelacademy.org/post/22032#toc-13)或者 Stripe `PaymentMethod` 对象的 `create` 方法会自动创建这个 Stripe 订阅,同时更新数据库中 Stripe 的消费者 ID(即 `users` 表中的 `stripe_id`)和其它相关的账单信息。 > 注意:直接传递支付方法标识符到 create () 订阅方法还会自动将其添加到用户存储的支付方法中。 #### 通过发票电子邮件收集定期付款 您可以指示 Stripe 在每次定期付款到期时通过电子邮件将发票发送给客户,而不是自动收取客户的定期付款。 然后,客户可以在收到发票后手动支付发票。 通过发票收取定期付款时,客户无需预先提供付款方式: $user->newSubscription('default', 'price_monthly')->createAndSendInvoice(); 客户在取消订阅之前必须支付发票的时间取决于您在 [Stripe 仪表板](https://dashboard.stripe.com/settings/billing/automatic) 中的订阅和发票设置。 #### 数量 如果你想要在创建订阅时设置计划的具体数量,可以使用 `quantity` 方法: $user->newSubscription('default', 'price_monthly') ->quantity(5) ->create($paymentMethod); #### 其它细节 如果你想要指定其它[消费者](https://stripe.com/docs/api/customers/create)或者[订阅细节](https://stripe.com/docs/api/subscriptions/create),你可以将其作为第二个参数传递给 `create` 方法: $user->newSubscription('default', 'price_monthly')->create($paymentMethod, [ 'email' => $email, ], [ 'metadata' => ['note' => 'Some extra information.'], ]); #### 优惠券 如果你想在创建订阅的时候使用优惠券,你可以使用 `withCoupon` 方法: $user->newSubscription('default', 'price_monthly') ->withCoupon('code') ->create($paymentMethod); 或者,如果你想使用 [Stripe 推广代码](https://stripe.com/docs/billing/subscriptions/discounts/codes),可以使用 `withPromotionCode` 方法: $user->newSubscription('default', 'price_monthly') ->withPromotionCode('promo_code') ->create($paymentMethod); #### 添加订阅 如果你想要为已经有默认支付方式的消费者添加订阅,可以在使用 `newSubscription` 方法时使用 `add` 方法: use App\Models\User; $user = User::find(1); $user->newSubscription('default', 'price_monthly')->add(); #### 从 Stripe 面板中创建订阅 你也可以从 Stripe 面板中创建订阅。在面板中创建订阅时候,收银员将同步新添加的订阅,并为它们分配一个名称为 `default` 的订阅。要自定义分配给仪表板创建订阅的订阅名,[扩展’ WebhookController ‘](https://learnku.com/docs/laravel/8.5/billing/10421#defining-webhook-event-handlers)并覆盖 `newSubscriptionName` 方法。 此外,只能通过 Stripe 面板中创建一种订阅类型。如果应用程序提供使用不同名称的多个订阅,则只能通过 Stripe 面板添加一种订阅类型。 最后,应该始终确保对应用程序提供的每种订阅类型只添加一个活动订阅。如果消费者有两个 `default` 订阅,那么收银员只会使用最近添加的订阅,即使这两个订阅都将与应用程序的数据库同步。 ### 检查订阅状态 客户订阅您的应用程序后,您可以使用各种便捷的方法检查他们的订阅状态。首先,如果客户有活动订阅,则 `subscribed` 方法会返回 `true`,即使订阅当前处于试用期也是如此。`subscribed` 方法接受订阅的名称作为它的第一个参数: if ($user->subscribed('default')) { // } `subscribed` 方法也可以用于[路由中间件](https://learnku.com/docs/laravel/8.5/middleware) , 基于消费者订阅状态允许你对路由和控制器的访问进行过滤: user() && ! $request->user()->subscribed('default')) { // 该用户不是付费用户... return redirect('billing'); } return $next($request); } } 如果你想要判断一个消费者是否还在试用期,可以使用 `onTrial` 方法,该方法对于还处于试用期的用户显示警告信息很有用: if ($user->subscription('default')->onTrial()) { // } `subscribedToProduct` 方法可用于根据给定 Stripe 产品的标识符确定用户是否订阅了给定产品。 在 Stripe 中,产品是价格的集合。 在此示例中,我们将确定用户的 `默认` 订阅是否主动订阅了应用程序的 `高级` 产品。 给定的 Stripe 产品标识符应与您在 Stripe 仪表板中的产品标识符之一相对应: if ($user->subscribedToProduct('prod_premium', 'default')) { // } if ($user->subscribedToProduct(['prod_basic', 'prod_premium'], 'default')) { // } `subscribedToPrice` 方法可用于确定客户的订阅是否对应于给定的价格 ID: if ($user->subscribedToPrice('price_basic_monthly', 'default')) { // } `recurring` 方法可用于确定用户当前是否已订阅并且不再处于试用期内: if ($user->subscription('default')->recurring()) { // } > 注意:如果消费者有两个具有相同名称的订阅,则 subscription 方法将始终返回最近的订阅。例如,消费者可能有两条名为 default 的订阅记录;但是,其中一个订阅可能是旧的、过期的订阅,而另一个是当前的、活动的订阅。最新的订阅将始终返回,而旧的订阅将保留在数据库中以进行历史回顾。 #### 已取消的订阅状态 要判断消费者是否曾经是有效的订阅者,但现在取消了订阅,可以使用 `cancelled` 方法: if ($user->subscription('default')->canceled()) { // } 还可以判断消费者是否曾经取消过订阅,但现在仍然在「宽限期」直到完全失效。例如,如果一个消费者在 3 月 5 号取消了一个实际有效期到 3 月 10 号的订阅,该消费者处于「宽限期」直到 3 月 10 号。注意 `subscribed` 方法在此期间仍然返回 `true`。 if ($user->subscription('default')->onGracePeriod()) { // } 要去定消费者已经取消订阅并且不在「宽限期」内,可以使用 `ended` 方法: if ($user->subscription('default')->ended()) { // } #### 未完成和过期状态 如果某个订阅要求创建完订阅后进行二次付款操作,将被标记为 `incomplete`。订阅状态被存储在 `Cashier subscriptions` 数据表的 `stripe_status` 字段。 类似的,如果在切换订阅计划时也需要进行二次付款操作,对应的订阅会被标记为 `past_due`。当你的订阅处于这种状态时,只有等到消费者确认支付后它们才会被激活。我们可以使用 `Billable` 模型或者订阅实例的 `hasIncompletePayment` 方法来检查某个订阅是否存在未完成支付: if ($user->hasIncompletePayment('default')) { // } if ($user->subscription('default')->hasIncompletePayment()) { // } 如果某个订阅存在未完成支付,你需要引导消费者到 Cashier 的支付确认页面,并传递 `latestPayment` 标识符。你可以使用订阅实例上的 `latestPayment` 方法来获取这个标识符: ```html Please confirm your payment. ``` 如果你想要订阅在 `past_due` 状态下依然有效,可以使用 Cashier 提供的 `keepPastDueSubscriptionsActive` 方法,通常,该方法需要在 `AppServiceProvider` 的 `boot` 方法中调用: use Laravel\Cashier\Cashier; /** * Register any application services. * * @return void */ public function register() { Cashier::keepPastDueSubscriptionsActive(); } > 注意:当某个订阅处于 `incomplete` 状态,只有等到支付被确认后才能进行修改。因此,当订阅处于 `incomplete` 状态时,执行 `swap` 和 `updateQuantity` 方法会抛出异常。 #### 订阅范围 大多数订阅状态也可用作查询范围,以便你可以轻松地查询数据库中处于给定状态的订阅: // 获取所有有效订阅... $subscriptions = Subscription::query()->active()->get(); // Get all of the canceled subscriptions for a user... $subscriptions = $user->subscriptions()->canceled()->get(); 所有内置的订阅查询范围列表如下: Subscription::query()->active(); Subscription::query()->canceled(); Subscription::query()->ended(); Subscription::query()->incomplete(); Subscription::query()->notCanceled(); Subscription::query()->notOnGracePeriod(); Subscription::query()->notOnTrial(); Subscription::query()->onGracePeriod(); Subscription::query()->onTrial(); Subscription::query()->pastDue(); Subscription::query()->recurring(); ### 修改计划 消费者订阅应用后,有时候想要改变到新的订阅计划,需要将消费者切换到新的订阅,传递计划标识符到 swap 方法: 在交换价格时,假设用户想要重新激活他们的订阅(如果之前取消订阅)。 给定的价格标识符应对应于 Stripe 仪表板中可用的 Stripe 价格标识符: use App\Models\User; $user = App\Models\User::find(1); $user->subscription('default')->swap('price_yearly'); 如果消费者在试用,试用期将会被维护。还有,如果订阅存在多个,数量也可以被维护。 如果你想要切换计划并取消消费者所在的所有试用期,可以使用 skipTrial 方法: $user->subscription('default') ->skipTrial() ->swap('price_yearly'); 如果你想要切换计划并立即为消费者开具发票,而不是等到下一个结算周期,可以使用 `swapAndInvoice` 方法: $user = User::find(1); $user->subscription('default')->swapAndInvoice('price_yearly'); #### 按比例分配 默认情况下,Stripe 会在订阅计划间切换时按比例进行分配,`noProrate` 可用于在修改订阅计划时不使用按比例分配机制: $user->subscription('default')->noProrate()->swap('price_yearly'); 更多关于订阅计划按比例分配的细节,请参考 [Stripe 官方文档](https://stripe.com/docs/billing/subscriptions/prorations)。 > 注意:在 `wapAndInvoice` 方法之前执行 `noProrate` 方法不会影响按比例分配。发票始终都会开具。 ### 订阅数量 有时候订阅也会被数量影响,例如,应用中每个账户每月需要付费 $10,要简单增加或减少订阅数量,使用 `incrementQuantity` 和 `decrementQuantity` 方法: use App\Models\User; $user = User::find(1); $user->subscription('default')->incrementQuantity(); // 向订阅的当前数量添加五个... $user->subscription('default')->incrementQuantity(5); $user->subscription('default')->decrementQuantity(); // 从订阅的当前数量中减去五... $user->subscription('default')->decrementQuantity(5); 你也可以使用 `updateQuantity` 方法指定具体的数量: $user->subscription('default')->updateQuantity(10); `noProrate` 方法可用于更新订阅数量而无需对收费进行评级: $user->subscription('default')->noProrate()->updateQuantity(10); 想要了解更多订阅数量信息,可以参考 [Stripe 官方文档](https://stripe.com/docs/subscriptions/quantities). #### 多方案订阅数量 如果你的订阅是[多方案订阅计划](https://learnku.com/docs/laravel/8.5/billing/10421#multiplan-subscriptions) , 你应该将其数量要递增或递减的计划的名称作为第二个参数传递给递增 / 递减方法 $user->subscription('default')->incrementQuantity(1, 'price_chat'); ### 多方案订阅计划 [多方案订阅计划](https://stripe.com/docs/billing/subscriptions/multiplan) 可以分配多个付费方案给单个订阅计划。例如,假设你正在构建一个顾客咨询服务应用,其中包含了一个基础的¥10 / 月的订阅方案,但是如果提供附加的实时聊天服务,需要支付¥15 / 月,订阅信息存储在收银员的 `Subscription_items` 数据库表中: 可以通过将计划数组作为第二个参数传递给 `newSubscription` 方法,为给定的订阅指定多个计划: use Illuminate\Http\Request; Route::post('/user/subscribe', function (Request $request) { $request->user()->newSubscription('default', [ 'price_monthly', 'price_chat', ])->create($request->paymentMethodId); // ... }); 在上面的示例代码中,消费者的 `default` 订阅添加了两个订阅计划。这两个计划都将按各自的计费间隔收费。如果需要,可以使用 `Quantity` 方法来表示每个计划的具体数量: $user = User::find(1); $user->newSubscription('default', ['price_monthly', 'price_chat']) ->quantity(5, 'price_chat') ->create($paymentMethod); 如果您想为现有订阅添加其他价格,可以调用订阅的 `addPrice` 方法: $user = User::find(1); $user->subscription('default')->addPrice('price_chat'); 上面的示例代码会添加新方案并且顾客会在下个支付周期为其支付。如果你想要让顾客立即支付,可以使用 `addPlanAndInvoice` 方法: $user->subscription('default')->addPriceAndInvoice('price_chat'); 如果你想要添加指定数量的方案,可以将数量值作为第二个参数传递给 `addPlan` 或者 `addPlanAndInvoice` 方法: $user = User::find(1); $user->subscription('default')->addPrice('price_chat', 5); 你也可以使用 `removePlan` 方法从订阅计划中移除这些方案: $user->subscription('default')->removePrice('price_chat'); > 注意:不能移除订阅计划中最后一个方案(至少需要保留一个), 相反,需要通过取消订阅的方式移除该方案。 #### 多方案订阅切换 您还可以更改多价订阅的附加价格。 例如,假设客户订阅了带有 `price_chat` 附加价格的 `price_basic`,并且您想将客户从 `price_basic` 升级到 `price_pro` 价格: use App\Models\User; $user = User::find(1); $user->subscription('default')->swap(['price_pro', 'price_chat']); 执行上面的示例时,带有 `price_basic` 的底层订阅项目被删除,而带有`price_chat` 的订阅项目被保留。 此外,还会为 `price_pro` 创建一个新的订阅项目。 您还可以通过将一组键/值对传递给“swap”方法来指定订阅项目选项。 例如,您可能需要指定订阅价格的数量: $user = User::find(1); $user->subscription('default')->swap([ 'price_pro' => ['quantity' => 5], 'price_chat' ]); 如果您想交换订阅的单一价格,您可以使用订阅项目本身的“swap”方法来实现。 如果您想保留订阅的其他价格上的所有现有元数据,此方法特别有用: $user = User::find(1); $user->subscription('default') ->findItemOrFail('price_basic') ->swap('price_pro'); #### 按比例分配 默认情况下,在多价订阅中添加或删除价格时,Stripe 将按比例收取费用。 如果您想在没有按比例分配的情况下进行价格调整,您应该将 `noProrate` 方法链接到您的价格操作: $user->subscription('default')->noProrate()->removePrice('price_chat'); #### 数量 如果您想更新单个订阅价格的数量,您可以使用 [现有数量方法](#subscription-quantity) 将价格名称作为附加参数传递给该方法: $user = User::find(1); $user->subscription('default')->incrementQuantity(5, 'price_chat'); $user->subscription('default')->decrementQuantity(3, 'price_chat'); $user->subscription('default')->updateQuantity(10, 'price_chat'); > 注意:当订阅有多个价格时,“Subscription”模型上的“stripe_price”和“quantity”属性将为“null”。 要访问单个价格属性,您应该使用 `Subscription` 模型上可用的 `items` 关系。 #### 订阅项目 当订阅有多个价格时,它将有多个订阅“项目”存储在数据库的 `subscription_items` 表中。 您可以通过订阅上的 `items` 关系访问这些: use App\Models\User; $user = User::find(1); $subscriptionItem = $user->subscription('default')->items->first(); // 检索特定商品的 Stripe 价格和数量... $stripePrice = $subscriptionItem->stripe_price; $quantity = $subscriptionItem->quantity; 您还可以使用 `findItemOrFail` 方法检索特定价格: $user = User::find(1); $subscriptionItem = $user->subscription('default')->findItemOrFail('price_chat'); ### 计量计费 [计量计费](https://stripe.com/docs/billing/subscriptions/metered-billing) 允许您根据客户在计费周期内的产品使用情况向他们收费。 例如,您可以根据客户每月发送的短信或电子邮件的数量向他们收费。 要开始使用计量计费,您首先需要在 Stripe 仪表板中创建一个具有计量价格的新产品。 然后,使用 `meteredPrice` 将计量价格 ID 添加到客户订阅中: use Illuminate\Http\Request; Route::post('/user/subscribe', function (Request $request) { $request->user()->newSubscription('default') ->meteredPrice('price_metered') ->create($request->paymentMethodId); // ... }); 您也可以通过 [Stripe Checkout](#checkout) 开始计量订阅: $checkout = Auth::user() ->newSubscription('default', []) ->meteredPrice('price_metered') ->checkout(); return view('your-checkout-view', [ 'checkout' => $checkout, ]); #### 报告使用情况 当您的客户使用您的应用程序时,您将向 Stripe 报告他们的使用情况,以便准确计费。 要增加计量订阅的使用量,您可以使用 `reportUsage` 方法: $user = User::find(1); $user->subscription('default')->reportUsage(); 默认情况下,计费周期会添加 1 的“使用量”。 或者,您可以传递特定数量的“使用量”以添加到客户在计费期间的使用量: $user = User::find(1); $user->subscription('default')->reportUsage(15); 如果您的应用程序在单个订阅中提供多个价格,您将需要使用 `reportUsageFor` 方法来指定要报告使用情况的计量价格: $user = User::find(1); $user->subscription('default')->reportUsageFor('price_metered', 15); 有时,您可能需要更新之前报告的使用情况。 为此,您可以将时间戳或“DateTimeInterface”实例作为第二个参数传递给“reportUsage”。 这样做时,Stripe 将更新在给定时间报告的使用情况。 您可以继续更新以前的使用记录,因为给定的日期和时间仍在当前计费周期内: $user = User::find(1); $user->subscription('default')->reportUsage(5, $timestamp); #### 检索使用记录 要检索客户过去的使用情况,您可以使用订阅实例的 `usageRecords` 方法: $user = User::find(1); $usageRecords = $user->subscription('default')->usageRecords(); 如果您的应用程序在单个订阅中提供多个价格,您可以使用 `usageRecordsFor` 方法指定您希望检索使用记录的计量价格: $user = User::find(1); $usageRecords = $user->subscription('default')->usageRecordsFor('price_metered'); `usageRecords` 和 `usageRecordsFor` 方法返回一个包含使用记录关联数组的 Collection 实例。 您可以遍历此数组以显示客户的总使用量: @foreach ($usageRecords as $usageRecord) - Period Starting: {{ $usageRecord['period']['start'] }} - Period Ending: {{ $usageRecord['period']['end'] }} - Total Usage: {{ $usageRecord['total_usage'] }} @endforeach 有关返回的所有使用数据以及如何使用 Stripe 的基于光标的分页的完整参考,请参阅 [官方 Stripe API 文档](https://stripe.com/docs/api/usage_records/subscription_item_summary_list)。 ### 订阅 Taxes > 注意:您可以[使用 StripeTax 自动计算税金](#tax-configuration),而不是手动计算税率 要指定用户为订阅支付的税率,您应该在计费模型上实现 `taxRates` 方法并返回一个包含 Stripe 税率 ID 的数组。 您可以在 [您的 Stripe 仪表板](https://dashboard.stripe.com/test/tax-rates) 中定义这些税率: /** * 应适用于客户订阅的税率。 * * @return array */ public function taxRates() { return ['txr_id']; } `taxRates` 方法使您可以逐个客户应用税率,这可能有助于跨越多个国家和税率的用户群。 如果您提供多价订阅,您可以通过在计费模型上实现 `priceTaxRates` 方法为每个价格定义不同的税率: /** * The tax rates that should apply to the customer's subscriptions. * * @return array */ public function priceTaxRates() { return [ 'price_monthly' => ['txr_id'], ]; } > 注意:`taxRates` 方法仅适用于订阅费用。 如果您使用 Cashier 进行“一次性”收费,您将需要手动指定当时的税率。 #### 同步税率 更改 `taxRates` 方法返回的硬编码税率 ID 时,用户现有订阅的税费设置将保持不变。 如果您希望使用新的 `taxRates` 值更新现有订阅的税值,则应在用户的订阅实例上调用 `syncTaxRates` 方法: $user->subscription('default')->syncTaxRates(); 这还将同步任何多价订阅项目税率。 如果您的应用程序提供多价订阅,您应该确保您的计费模型实现了 `priceTaxRates` 方法 [如上所述](#subscription-taxes)。 #### 免税 Cashier 还提供了 `isNotTaxExempt`、`isTaxExempt` 和 `reverseChargeApplies` 方法来确定客户是否免税。 这些方法将调用 Stripe API 来确定客户的免税状态: use App\Models\User; $user = User::find(1); $user->isTaxExempt(); $user->isNotTaxExempt(); $user->reverseChargeApplies(); > 注意:这些方法也适用于任何 `Laravel\Cashier\Invoice` 对象。 但是,当在“发票”对象上调用时,这些方法将在创建发票时确定豁免状态。 ### 订阅锚定日期 默认情况下,计费周期锚点是订阅的创建日期,或者,如果使用试用期,则为试用结束的日期。 如果您想修改计费锚定日期,可以使用 `anchorBillingCycleOn` 方法: use Illuminate\Http\Request; Route::post('/user/subscribe', function (Request $request) { $anchor = Carbon::parse('first day of next month'); $request->user()->newSubscription('default', 'price_monthly') ->anchorBillingCycleOn($anchor->startOfDay()) ->create($request->paymentMethodId); // ... }); 有关管理订阅计费周期的更多信息,请参阅 [Stripe 计费周期文档](https://stripe.com/docs/billing/subscriptions/billing-cycle) ### 取消订阅 要取消订阅,请在用户订阅上调用 `cancel` 方法: $user->subscription('default')->cancel(); 取消订阅后,Cashier 会自动在您的 `subscriptions` 数据库表中设置 `ends_at` 列。 此列用于了解 `subscribed` 方法应该何时开始返回 `false`。 例如,如果客户在 3 月 1 日取消订阅,但订阅计划到 3 月 5 日才结束,则 `subscribed` 方法将继续返回 `true` 直到 3 月 5 日。 这样做是因为通常允许用户继续使用应用程序,直到他们的计费周期结束。 您可以使用 `onGracePeriod` 方法确定用户是否已取消订阅但仍处于“宽限期”: if ($user->subscription('default')->onGracePeriod()) { // } 如果您想立即取消订阅,请在用户订阅上调用 `cancelNow` 方法: $user->subscription('default')->cancelNow(); 如果您希望立即取消订阅并为任何剩余的未开票计量使用或新的/待定的按比例发票项目开具发票,请在用户的订阅上调用 `cancelNowAndInvoice` 方法: $user->subscription('default')->cancelNowAndInvoice(); 您也可以选择在特定时间取消订阅: $user->subscription('default')->cancelAt( now()->addDays(10) ); ### 恢复订阅 如果客户取消了他们的订阅并且您希望恢复它,您可以在订阅上调用 `resume` 方法。 客户必须仍在其“宽限期”内才能恢复订阅: $user->subscription('default')->resume(); 如果客户取消订阅,然后在订阅完全过期之前恢复订阅,则不会立即向客户收费。 相反,他们的订阅将被重新激活,并将按原始计费周期计费。 ## 订阅 Trials ### 预先使用付款方式 如果您想为您的客户提供试用期,同时仍然预先收集付款方式信息,您应该在创建订阅时使用 `trialDays` 方法: use Illuminate\Http\Request; Route::post('/user/subscribe', function (Request $request) { $request->user()->newSubscription('default', 'price_monthly') ->trialDays(10) ->create($request->paymentMethodId); // ... }); 此方法将在数据库中的订阅记录上设置试用期结束日期,并指示 Stripe 在此日期之后才开始向客户收费。 当使用 `trialDays` 方法时,Cashier 将覆盖为 Stripe 中的价格配置的任何默认试用期。 > 注意:如果客户的订阅没有在试用期结束日期之前取消,他们将在试用期结束后立即收费,因此您应该确保通知您的用户他们的试用期结束日期。 `trialUntil` 方法允许你提供一个 `DateTime` 实例来指定试用期的结束时间: use Carbon\Carbon; $user->newSubscription('default', 'price_monthly') ->trialUntil(Carbon::now()->addDays(10)) ->create($paymentMethod); 您可以使用用户实例的“onTrial”方法或订阅实例的“onTrial”方法来确定用户是否在试用期内。 下面的两个例子是等价的: if ($user->onTrial('default')) { // } if ($user->subscription('default')->onTrial()) { // } 您可以使用 `endTrial` 方法立即结束订阅试用: $user->subscription('default')->endTrial(); #### 在 Stripe 中定义试用天数 / Cashier 您可以选择在 Stripe 仪表板中定义您的价格收到多少试用天,或者始终使用 Cashier 明确传递它们。 如果您选择在 Stripe 中定义价格的试用天数,您应该知道新订阅(包括过去订阅过的客户的新订阅)将始终收到试用期,除非您明确调用 `skipTrial()` 方法. ### 没有预付款方式 如果您想提供试用期而不预先收集用户的付款方式信息,您可以将用户记录上的“trial_ends_at”列设置为您想要的试用结束日期。 这通常在用户注册期间完成: use App\Models\User; $user = User::create([ // ... 'trial_ends_at' => now()->addDays(10), ]); > 注意:请务必在计费模型的类定义中为 `trial_ends_at` 属性添加 [日期转换](/docs/laravel/9.x/eloquent-mutators##date-casting)。 Cashier 将这种类型的试用称为“通用试用”,因为它不附加到任何现有订阅。 如果当前日期未超过 `trial_ends_at` 的值,可计费模型实例上的 `onTrial` 方法将返回 `true`: if ($user->onTrial()) { // 用户在试用期内... } 一旦你准备好为用户创建一个实际的订阅,你可以像往常一样使用`newSubscription`方法: $user = User::find(1); $user->newSubscription('default', 'price_monthly')->create($paymentMethod); 要检索用户的试用结束日期,您可以使用 `trialEndsAt` 方法。 如果用户正在试用,此方法将返回一个 Carbon 日期实例,如果不是,则返回“null”。 如果您想获取除默认订阅之外的特定订阅的试用结束日期,您还可以传递可选订阅名称参数: if ($user->onTrial()) { $trialEndsAt = $user->trialEndsAt('main'); } 如果您想明确知道用户处于 “generic” 试用期并且尚未创建实际订阅,您也可以使用 `onGenericTrial` 方法: if ($user->onGenericTrial()) { // 用户在他们的 “generic” 试用期内... } ### 延长试用 `extendTrial` 方法允许您在创建订阅后延长订阅的试用期。 如果试用期已经过期并且客户已经被收取订阅费用,您仍然可以为他们提供延长试用期。 试用期内花费的时间将从客户的下一张发票中扣除: use App\Models\User; $subscription = User::find(1)->subscription('default'); // 从现在起 7 天后结束试用... $subscription->extendTrial( now()->addDays(7) ); // 试用期再增加 5 天... $subscription->extendTrial( $subscription->trial_ends_at->addDays(5) ); ## 处理 Stripe webhook > 技巧:您可以使用 [the Stripe CLI](https://stripe.com/docs/stripe-cli) 在本地开发期间帮助测试 webhook。 Stripe 可以通过 webhook 通知您的应用程序各种事件。 默认情况下,指向 Cashier 的 webhook 控制器的路由由 Cashier 服务提供者自动注册。 此控制器将处理所有传入的 webhook 请求。 默认情况下,Cashier webhook 控制器将自动处理取消订阅失败的次数过多(由您的 Stripe 设置定义)、客户更新、客户删除、订阅更新和付款方式更改; 然而,我们很快就会发现,你可以扩展这个控制器来处理你喜欢的任何 Stripe webhook 事件。 为确保您的应用程序可以处理 Stripe webhook,请务必在 Stripe 控制面板中配置 webhook URL。 默认情况下,Cashier 的 webhook 控制器响应 `/stripe/webhook` URL 路径。您应该在 Stripe 控制面板中启用的所有 webhook 的完整列表是: - `customer.subscription.created` - `customer.subscription.updated` - `customer.subscription.deleted` - `customer.updated` - `customer.deleted` - `invoice.payment_action_required` 为方便起见,Cashier 包含一个 `cashier:webhook` Artisan 命令。 此命令将在 Stripe 中创建一个 webhook,用于监听 Cashier 所需的所有事件: ```shell php artisan cashier:webhook ``` 默认情况下,创建的 webhook 将指向由 `APP_URL` 环境变量和 Cashier 包含的 `cashier.webhook` 路由定义的 URL。 如果您想使用不同的 URL,可以在调用命令时提供 `--url` 选项: ```shell php artisan cashier:webhook --url "https://example.com/stripe/webhook" ``` 创建的 webhook 将使用与您的 Cashier 版本兼容的 Stripe API 版本。 如果你想使用不同的 Stripe 版本,你可以提供 `--api-version` 选项: ```shell php artisan cashier:webhook --api-version="2019-12-03" ``` 创建后,webhook 将立即激活。 如果您希望创建 webhook 但在准备好之前将其禁用,您可以在调用命令时提供 `--disabled` 选项: ```shell php artisan cashier:webhook --disabled ``` > 注意:确保使用 Cashier 包含的 [webhook 签名验证](#verifying-webhook-signatures) 中间件保护传入的 Stripe webhook 请求。 #### Webhook 和 CSRF 保护 由于 Stripe webhook 需要绕过 Laravel 的 [CSRF 保护](/docs/laravel/9.x/csrf),因此请务必在应用程序的 `App\Http\Middleware\VerifyCsrfToken` 中间件中将 URI 列为异常或列出路由在 `web` 中间件组之外: protected $except = [ 'stripe/*', ]; ### 定义 webhook 事件处理程序 Cashier 自动处理因收费失败和其他常见的 Stripe webhook 事件而取消的订阅。 但是,如果您有其他想要处理的 webhook 事件,您可以通过收听 Cashier 调度的以下事件来实现: - `Laravel\Cashier\Events\WebhookReceived` - `Laravel\Cashier\Events\WebhookHandled` 这两个事件都包含 Stripe webhook 的完整负载。 例如,如果你想处理 `invoice.payment_succeeded` webhook,你可以注册一个 [listener](/docs/laravel/9.x/events#defining-listeners) 来处理事件: payload['type'] === 'invoice.payment_succeeded') { // 处理传入事件... } } } 一旦你的监听器被定义,你可以在你的应用程序的`EventServiceProvider`中注册它: [ StripeEventListener::class, ], ]; } ### 验证 Webhook 签名 为了保护您的 webhook,您可以使用 [Stripe 的 webhook 签名](https://stripe.com/docs/webhooks/signatures)。 为方便起见,Cashier 自动包含一个中间件,用于验证传入的 Stripe webhook 请求是否有效。 要启用 webhook 验证,请确保在应用程序的 .env 文件中设置了 `STRIPE_WEBHOOK_SECRET` 环境变量。 可以从您的 Stripe 帐户仪表板中检索 webhook `secret`。 ## 单次 Charges ### 简单 Charge > 注意:`charge` 方法接受您希望以应用程序使用的货币的最低分母收取的金额。例如,使用美元时,金额应以便士表示。 如果您想向客户一次性收费,您可以在可计费模型实例上使用 `charge` 方法。您需要[提供付款方式标识符](#单笔费用的付款方式)作为 `charge` 方法的第二个参数: use Illuminate\Http\Request; Route::post('/purchase', function (Request $request) { $stripeCharge = $request->user()->charge( 100, $request->paymentMethodId ); // ... }); `charge` 方法接受一个数组作为它的第三个参数,允许您将任何您希望的选项传递给底层的 Stripe 电荷创建。 有关创建费用时可用选项的更多信息,请参阅 [Stripe 文档](https://stripe.com/docs/api/charges/create): $user->charge(100, $paymentMethod, [ 'custom_option' => $value, ]); 您也可以在没有潜在客户或用户的情况下使用“收费”方法。 为此,请在应用程序的计费模型的新实例上调用 `charge` 方法: use App\Models\User; $stripeCharge = (new User)->charge(100, $paymentMethod); 如果充电失败,`charge` 方法会抛出异常。 如果收费成功,方法会返回一个 `Laravel\Cashier\Payment` 的实例: try { $payment = $user->charge(100, $paymentMethod); } catch (Exception $e) { // } ### 用发票收费 有时您可能需要一次性收费并向客户提供 PDF 收据。 `invoicePrice` 方法可以让你做到这一点。 例如,让我们为客户开具五件新衬衫的发票: $user->invoicePrice('price_tshirt', 5); 发票将立即根据用户的默认付款方式收取。 `invoicePrice` 方法也接受一个数组作为它的第三个参数。 此数组包含发票项目的计费选项。 该方法接受的第四个参数也是一个数组,其中应包含发票本身的计费选项: $user->invoicePrice('price_tshirt', 5, [ 'discounts' => [ ['coupon' => 'SUMMER21SALE'] ], ], [ 'default_tax_rates' => ['txr_id'], ]); 或者,您可以使用 `invoiceFor` 方法对客户的默认付款方式进行“一次性”收费: $user->invoiceFor('One Time Fee', 500); 虽然 `invoiceFor` 方法可供您使用,但建议您使用具有预定义价格的 `invoicePrice` 方法。 通过这样做,您将可以在您的 Stripe 仪表板中访问更好的分析和数据,以了解您在每个产品的基础上的销售情况。 > 注意:`invoicePrice` 和 `invoiceFor` 方法将创建一个 Stripe 发票,该发票将重试失败的计费尝试。 如果您不希望发票重试失败的费用,则需要在第一次失败的费用后使用 Stripe API 关闭它们。 ### 退款费用 如果您需要退还 Stripe 费用,您可以使用 `refund` 方法。 此方法接受 Stripe [payment intent ID](#payment-methods-for-single-charges) 作为其第一个参数: $payment = $user->charge(100, $paymentMethodId); $user->refund($payment->id); ## 发票 ### 检索发票 您可以使用 `invoices` 方法轻松检索可计费模型的发票数组。 `invoices` 方法返回 `Laravel\Cashier\Invoice` 实例的集合: $invoices = $user->invoices(); 如果您想在结果中包含待处理的发票,您可以使用 `invoicesIncludingPending` 方法: $invoices = $user->invoicesIncludingPending(); 您可以使用 `findInvoice` 方法通过其 ID 检索特定发票: $invoice = $user->findInvoice($invoiceId); #### 显示发票信息 在为客户列出发票时,您可以使用发票的方法显示相关的发票信息。 例如,您可能希望在表格中列出每张发票,以便用户轻松下载其中任何一张:{{ $invoice->date()->toFormattedDateString() }} | {{ $invoice->total() }} | Download |