Skip to content

Laravel Cashier (Paddle)

介绍

Laravel Cashier Paddle 提供了一个对 Paddle 订阅计费服务的表达性、流畅的接口。它处理了几乎所有你不想编写的订阅计费样板代码。除了基本的订阅管理,Cashier 还可以处理:优惠券、交换订阅、订阅“数量”、取消宽限期等。

在使用 Cashier 时,我们建议你也查看 Paddle 的用户指南API 文档

升级 Cashier

在升级到新版本的 Cashier 时,务必仔细查看升级指南

安装

首先,使用 Composer 包管理器安装 Paddle 的 Cashier 包:

shell
composer require laravel/cashier-paddle
exclamation

为确保 Cashier 正确处理所有 Paddle 事件,请记得设置 Cashier 的 webhook 处理

Paddle 沙盒

在本地和暂存开发期间,你应该注册一个 Paddle 沙盒账户。此账户将为你提供一个沙盒环境,以便在不进行实际支付的情况下测试和开发应用程序。你可以使用 Paddle 的测试卡号来模拟各种支付场景。

在使用 Paddle 沙盒环境时,你应该在应用程序的 .env 文件中将 PADDLE_SANDBOX 环境变量设置为 true

ini
PADDLE_SANDBOX=true

在完成应用程序开发后,你可以申请一个 Paddle 供应商账户。在将应用程序投入生产之前,Paddle 需要批准你的应用程序域名。

数据库迁移

Cashier 服务提供者注册了自己的数据库迁移目录,因此在安装包后记得迁移数据库。Cashier 迁移将创建一个新的 customers 表。此外,还将创建一个新的 subscriptions 表来存储所有客户的订阅。最后,将创建一个新的 receipts 表来存储所有应用程序的收据信息:

shell
php artisan migrate

如果需要覆盖 Cashier 附带的迁移,可以使用 vendor:publish Artisan 命令发布它们:

shell
php artisan vendor:publish --tag="cashier-migrations"

如果你想完全阻止 Cashier 的迁移运行,可以使用 Cashier 提供的 ignoreMigrations。通常,此方法应在 AppServiceProviderregister 方法中调用:

php
use Laravel\Paddle\Cashier;

/**
 * 注册任何应用程序服务。
 *
 * @return void
 */
public function register()
{
    Cashier::ignoreMigrations();
}

配置

可计费模型

在使用 Cashier 之前,必须在用户模型定义中添加 Billable trait。此 trait 提供了各种方法,允许你执行常见的计费任务,例如创建订阅、应用优惠券和更新支付方式信息:

php
use Laravel\Paddle\Billable;

class User extends Authenticatable
{
    use Billable;
}

如果你有不是用户的可计费实体,也可以将 trait 添加到这些类中:

php
use Illuminate\Database\Eloquent\Model;
use Laravel\Paddle\Billable;

class Team extends Model
{
    use Billable;
}

API 密钥

接下来,你应该在应用程序的 .env 文件中配置你的 Paddle 密钥。你可以从 Paddle 控制面板中检索你的 Paddle API 密钥:

ini
PADDLE_VENDOR_ID=your-paddle-vendor-id
PADDLE_VENDOR_AUTH_CODE=your-paddle-vendor-auth-code
PADDLE_PUBLIC_KEY="your-paddle-public-key"
PADDLE_SANDBOX=true

在使用 Paddle 的沙盒环境时,PADDLE_SANDBOX 环境变量应设置为 true。如果你将应用程序部署到生产环境并使用 Paddle 的实时供应商环境,则应将 PADDLE_SANDBOX 变量设置为 false

Paddle JS

Paddle 依赖于其自己的 JavaScript 库来启动 Paddle 结账小部件。你可以通过在应用程序布局的关闭 </head> 标签之前放置 @paddleJS Blade 指令来加载 JavaScript 库:

blade
<head>
    ...

    @paddleJS
</head>

货币配置

默认的 Cashier 货币是美元 (USD)。你可以通过在应用程序的 .env 文件中定义 CASHIER_CURRENCY 环境变量来更改默认货币:

ini
CASHIER_CURRENCY=EUR

除了配置 Cashier 的货币外,你还可以指定一个用于在发票上显示货币值时使用的区域设置。内部,Cashier 使用 PHP 的 NumberFormatter 来设置货币区域设置:

ini
CASHIER_CURRENCY_LOCALE=nl_BE
exclamation

为了使用 en 以外的区域设置,请确保在服务器上安装并配置了 ext-intl PHP 扩展。

覆盖默认模型

你可以通过定义自己的模型并扩展相应的 Cashier 模型来自由扩展 Cashier 内部使用的模型:

php
use Laravel\Paddle\Subscription as CashierSubscription;

class Subscription extends CashierSubscription
{
    // ...
}

在定义模型后,可以通过 Laravel\Paddle\Cashier 类指示 Cashier 使用自定义模型。通常,你应该在应用程序的 App\Providers\AppServiceProvider 类的 boot 方法中通知 Cashier 你的自定义模型:

php
use App\Models\Cashier\Receipt;
use App\Models\Cashier\Subscription;

/**
 * 启动任何应用程序服务。
 *
 * @return void
 */
public function boot()
{
    Cashier::useReceiptModel(Receipt::class);
    Cashier::useSubscriptionModel(Subscription::class);
}

核心概念

支付链接

Paddle 缺乏一个广泛的 CRUD API 来执行订阅状态更改。因此,大多数与 Paddle 的交互都是通过其结账小部件完成的。在我们可以显示结账小部件之前,必须使用 Cashier 生成一个“支付链接”。“支付链接”将通知结账小部件我们希望执行的计费操作:

php
use App\Models\User;
use Illuminate\Http\Request;

Route::get('/user/subscribe', function (Request $request) {
```php
$payLink = $request->user()->newSubscription('default', $premium = 34567)
    ->returnTo(route('home'))
    ->create();
return view('billing', ['payLink' => $payLink]);

});



Cashier 包含一个 `paddle-button` [Blade 组件](/blade#components)。我们可以将支付链接 URL 作为“prop”传递给此组件。当单击此按钮时,将显示 Paddle 的结账小部件:

```html
<x-paddle-button :url="$payLink" class="px-8 py-4">
    Subscribe
</x-paddle-button>

默认情况下,这将显示一个带有标准 Paddle 样式的按钮。你可以通过向组件添加 data-theme="none" 属性来删除所有 Paddle 样式:

html
<x-paddle-button :url="$payLink" class="px-8 py-4" data-theme="none">
    Subscribe
</x-paddle-button>

Paddle 结账小部件是异步的。一旦用户在小部件中创建或更新订阅,Paddle 将向你的应用程序发送 webhooks,以便你可以正确更新我们自己数据库中的订阅状态。因此,重要的是你要正确设置 webhooks以适应来自 Paddle 的状态更改。

有关支付链接的更多信息,你可以查看 Paddle API 文档关于支付链接生成

exclamation

在订阅状态更改后,接收相应 webhook 的延迟通常很小,但你应该在应用程序中考虑到这一点,因为用户的订阅可能在完成结账后不会立即可用。

手动渲染支付链接

你也可以在不使用 Laravel 内置 Blade 组件的情况下手动渲染支付链接。首先,按照前面的示例生成支付链接 URL:

$payLink = $request->user()->newSubscription('default', $premium = 34567)
    ->returnTo(route('home'))
    ->create();

接下来,只需将支付链接 URL 附加到 HTML 中的 a 元素:

php
<a href="#!" class="ml-4 paddle_button" data-override="{{ $payLink }}">
    Paddle Checkout
</a>

需要额外确认的付款

有时需要额外的验证才能确认和处理付款。当这种情况发生时,Paddle 将显示一个付款确认屏幕。Paddle 或 Cashier 提供的付款确认屏幕可以根据特定银行或卡发行商的付款流程进行定制,并可能包括额外的卡确认、临时小额收费、单独的设备认证或其他形式的验证。

内嵌结账

如果你不想使用 Paddle 的“覆盖”样式结账小部件,Paddle 还提供了内嵌显示小部件的选项。虽然这种方法不允许你调整结账的任何 HTML 字段,但它允许你将小部件嵌入到应用程序中。

为了让你更容易开始使用内嵌结账,Cashier 包含一个 paddle-checkout Blade 组件。要开始,你应该生成一个支付链接并将支付链接传递给组件的 override 属性:

blade
<x-paddle-checkout :override="$payLink" class="w-full" />

要调整内嵌结账组件的高度,可以将 height 属性传递给 Blade 组件:

blade
<x-paddle-checkout :override="$payLink" class="w-full" height="500" />

无支付链接的内嵌结账

或者,你可以使用自定义选项而不是支付链接来定制小部件:

blade
@php
$options = [
    'product' => $productId,
    'title' => 'Product Title',
];
@endphp

<x-paddle-checkout :options="$options" class="w-full" />

请查阅 Paddle 的内嵌结账指南以及他们的参数参考以获取有关内嵌结账可用选项的更多详细信息。

exclamation

如果你想在指定自定义选项时也使用 passthrough 选项,你应该提供一个键/值数组作为其值。Cashier 将自动处理将数组转换为 JSON 字符串。此外,customer_id passthrough 选项是为 Cashier 内部使用保留的。

手动渲染内嵌结账

你也可以在不使用 Laravel 内置 Blade 组件的情况下手动渲染内嵌结账。首先,按照前面的示例生成支付链接 URL

接下来,你可以使用 Paddle.js 初始化结账。为了简化此示例,我们将使用 Alpine.js 演示;但是,你可以自由地将此示例转换为自己的前端堆栈:

<div class="paddle-checkout" x-data="{}" x-init="
    Paddle.Checkout.open({
        override: {{ $payLink }},
        method: 'inline',
        frameTarget: 'paddle-checkout',
        frameInitialHeight: 366,
        frameStyle: 'width: 100%; background-color: transparent; border: none;'
    });
">
</div>

用户识别

与 Stripe 相比,Paddle 用户在所有 Paddle 中都是唯一的,而不是每个 Paddle 账户唯一。因此,Paddle 的 API 目前不提供更新用户详细信息(如电子邮件地址)的方法。在生成支付链接时,Paddle 使用 customer_email 参数识别用户。在创建订阅时,Paddle 将尝试将用户提供的电子邮件与现有的 Paddle 用户匹配。

鉴于这种行为,在使用 Cashier 和 Paddle 时需要注意一些重要事项。首先,你应该意识到,即使 Cashier 中的订阅与同一应用程序用户相关联,它们也可能与 Paddle 内部系统中的不同用户相关联。其次,每个订阅都有其自己的连接支付方式信息,并且在 Paddle 内部系统中也可能有不同的电子邮件地址(取决于创建订阅时分配给用户的电子邮件)。

因此,在显示订阅时,你应该始终告知用户与订阅相关的电子邮件地址或支付方式信息。可以通过 Laravel\Paddle\Subscription 模型提供的以下方法检索此信息:

php
$subscription = $user->subscription('default');

$subscription->paddleEmail();
$subscription->paymentMethod();
$subscription->cardBrand();
$subscription->cardLastFour();
$subscription->cardExpirationDate();

目前无法通过 Paddle API 修改用户的电子邮件地址。当用户想要在 Paddle 中更新他们的电子邮件地址时,唯一的方法是联系 Paddle 客户支持。在与 Paddle 沟通时,他们需要提供订阅的 paddleEmail 值以协助 Paddle 更新正确的用户。

价格

Paddle 允许你根据货币自定义价格,基本上允许你为不同国家/地区配置不同的价格。Cashier Paddle 允许你使用 productPrices 方法检索给定产品的所有价格。此方法接受你希望检索价格的产品 ID:

php
use Laravel\Paddle\Cashier;

$prices = Cashier::productPrices([123, 456]);

货币将根据请求的 IP 地址确定;但是,你可以选择提供特定国家/地区以检索价格:

php
use Laravel\Paddle\Cashier;

$prices = Cashier::productPrices([123, 456], ['customer_country' => 'BE']);

在检索价格后,你可以随意显示它们:

blade
<ul>
    @foreach ($prices as $price)
        <li>{{ $price->product_title }} - {{ $price->price()->gross() }}</li>
    @endforeach
</ul>

你还可以显示净价(不含税)并单独显示税额:

blade
<ul>
    @foreach ($prices as $price)
        <li>{{ $price->product_title }} - {{ $price->price()->net() }} (+ {{ $price->price()->tax() }} tax)</li>
    @endforeach
</ul>

如果你检索了订阅计划的价格,可以分别显示其初始价格和经常性价格:

blade
<ul>
    @foreach ($prices as $price)
        <li>{{ $price->product_title }} - Initial: {{ $price->initialPrice()->gross() }} - Recurring: {{ $price->recurringPrice()->gross() }}</li>
    @endforeach
</ul>

有关更多信息,请查看 Paddle 的 API 文档关于价格

客户

如果用户已经是客户,并且你想显示适用于该客户的价格,可以通过从客户实例直接检索价格来实现:

php
use App\Models\User;

$prices = User::find(1)->productPrices([123, 456]);

在内部,Cashier 将使用用户的 paddleCountry 方法 以其货币检索价格。因此,例如,居住在美国的用户将看到美元价格,而比利时的用户将看到欧元价格。如果找不到匹配的货币,将使用产品的默认货币。你可以在 Paddle 控制面板中自定义产品或订阅计划的所有价格。

优惠券

你还可以选择在优惠券减少后显示价格。在调用 productPrices 方法时,可以将优惠券作为逗号分隔的字符串传递:

php
use Laravel\Paddle\Cashier;

$prices = Cashier::productPrices([123, 456], [
    'coupons' => 'SUMMERSALE,20PERCENTOFF'
]);

然后,使用 price 方法显示计算后的价格:

blade
<ul>
    @foreach ($prices as $price)
        <li>{{ $price->product_title }} - {{ $price->price()->gross() }}</li>
    @endforeach
</ul>

你可以使用 listPrice 方法显示原始列出的价格(不含优惠券折扣):

blade
<ul>
    @foreach ($prices as $price)
        <li>{{ $price->product_title }} - {{ $price->listPrice()->gross() }}</li>
    @endforeach
</ul>
exclamation

在使用价格 API 时,Paddle 仅允许将优惠券应用于一次性购买产品,而不适用于订阅计划。

客户

客户默认值

Cashier 允许你在创建支付链接时为客户定义一些有用的默认值。设置这些默认值可以让你预先填写客户的电子邮件地址、国家/地区和邮政编码,以便他们可以立即进入结账小部件的支付部分。你可以通过覆盖可计费模型上的以下方法来设置这些默认值:

php
/**
 * 获取要与 Paddle 关联的客户电子邮件地址。
 *
 * @return string|null
 */
public function paddleEmail()
{
    return $this->email;
}

/**
 * 获取要与 Paddle 关联的客户国家/地区。
 *
 * 这需要是一个 2 位代码。请参阅下面的链接以获取支持的国家/地区。
 *
 * @return string|null
 * @link https://developer.paddle.com/reference/platform-parameters/supported-countries
 */
public function paddleCountry()
{
    //
}

/**
 * 获取要与 Paddle 关联的客户邮政编码。
 *
 * 请参阅下面的链接以获取需要此信息的国家/地区。
 *
 * @return string|null
 * @link https://developer.paddle.com/reference/platform-parameters/supported-countries#countries-requiring-postcode
 */
public function paddlePostcode()
{
    //
}

这些默认值将用于 Cashier 中生成的每个支付链接操作。

订阅

创建订阅

要创建订阅,首先从数据库中检索可计费模型的实例,通常是 App\Models\User 的实例。一旦检索到模型实例,可以使用 newSubscription 方法创建模型的订阅支付链接:

php
use Illuminate\Http\Request;

Route::get('/user/subscribe', function (Request $request) {
    $payLink = $request->user()->newSubscription('default', $premium = 12345)
        ->returnTo(route('home'))
        ->create();

    return view('billing', ['payLink' => $payLink]);
});

传递给 newSubscription 方法的第一个参数应该是订阅的内部名称。如果你的应用程序只提供一个订阅,可以将其命名为 defaultprimary。此订阅名称仅供内部应用程序使用,不应显示给用户。此外,它不应包含空格,并且在创建订阅后不应更改。传递给 newSubscription 方法的第二个参数是用户订阅的特定计划。此值应对应于 Paddle 中计划的标识符。returnTo 方法接受一个 URL,用户在成功完成结账后将被重定向到该 URL。

create 方法将创建一个支付链接,你可以使用它生成一个支付按钮。支付按钮可以使用 Cashier Paddle 附带的 paddle-button Blade 组件生成:

blade
<x-paddle-button :url="$payLink" class="px-8 py-4">
    Subscribe
</x-paddle-button>

在用户完成结账后,Paddle 将发送一个 subscription_created webhook。Cashier 将接收此 webhook 并为你的客户设置订阅。为了确保所有 webhooks 都能被你的应用程序正确接收和处理,请确保你已正确设置 webhook 处理

额外细节

如果你想指定额外的客户或订阅详细信息,可以通过将它们作为键/值对数组传递给 create 方法来实现。要了解 Paddle 支持的其他字段,请查看 Paddle 关于生成支付链接的文档:

php
$payLink = $user->newSubscription('default', $monthly = 12345)
    ->returnTo(route('home'))
    ->create([
        'vat_number' => $vatNumber,
    ]);

优惠券

如果你想在创建订阅时应用优惠券,可以使用 withCoupon 方法:

php
$payLink = $user->newSubscription('default', $monthly = 12345)
    ->returnTo(route('home'))
    ->withCoupon('code')
    ->create();

元数据

你还可以使用 withMetadata 方法传递一个元数据数组:

php
$payLink = $user->newSubscription('default', $monthly = 12345)
    ->returnTo(route('home'))
    ->withMetadata(['key' => 'value'])
    ->create();
exclamation

在提供元数据时,请避免使用 subscription_name 作为元数据键。此键是为 Cashier 内部使用保留的。

检查订阅状态

一旦用户订阅了你的应用程序,可以使用各种方便的方法检查他们的订阅状态。首先,subscribed 方法返回 true,如果用户有一个活动的订阅,即使订阅当前处于试用期:

php
if ($user->subscribed('default')) {
    //
}

subscribed 方法也是一个很好的路由中间件候选者,允许你根据用户的订阅状态过滤对路由和控制器的访问:

php
<?php

namespace App\Http\Middleware;

use Closure;

class EnsureUserIsSubscribed
{
    /**
     * 处理传入请求。
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if ($request->user() && ! $request->user()->subscribed('default')) {
            // 此用户不是付费客户...
            return redirect('billing');
        }

        return $next($request);
    }
}

如果你想确定用户是否仍在试用期内,可以使用 onTrial 方法。此方法可用于确定是否应向用户显示警告,告知他们仍在试用期内:

php
if ($user->subscription('default')->onTrial()) {
    //
}

subscribedToPlan 方法可用于确定用户是否订阅了基于给定 Paddle 计划 ID 的给定计划。在此示例中,我们将确定用户的 default 订阅是否积极订阅了月度计划:

php
if ($user->subscribedToPlan($monthly = 12345, 'default')) {
    //
}

通过将数组传递给 subscribedToPlan 方法,可以确定用户的 default 订阅是否积极订阅了月度或年度计划:

php
if ($user->subscribedToPlan([$monthly = 12345, $yearly = 54321], 'default')) {
    //
}

recurring 方法可用于确定用户当前是否订阅并且不再处于试用期:

php
if ($user->subscription('default')->recurring()) {
    //
}

取消的订阅状态

要确定用户是否曾经是活跃订阅者但已取消订阅,可以使用 cancelled 方法:

php
if ($user->subscription('default')->cancelled()) {
    //
}

你还可以确定用户是否已取消订阅,但仍处于“宽限期”内,直到订阅完全过期。例如,如果用户在 3 月 5 日取消订阅,而订阅原定于 3 月 10 日到期,则用户在 3 月 10 日之前处于“宽限期”。请注意,在此期间,subscribed 方法仍然返回 true

php
if ($user->subscription('default')->onGracePeriod()) {
    //
}

要确定用户是否已取消订阅并且不再处于“宽限期”,可以使用 ended 方法:

php
if ($user->subscription('default')->ended()) {
    //
}

逾期状态

如果订阅的付款失败,将被标记为 past_due。当你的订阅处于此状态时,它将不会激活,直到客户更新其支付信息。可以使用订阅实例上的 pastDue 方法确定订阅是否逾期:

php
if ($user->subscription('default')->pastDue()) {
    //
}

当订阅逾期时,你应该指示用户更新其支付信息。你可以在 Paddle 订阅设置中配置如何处理逾期订阅。

如果你希望订阅在 past_due 时仍被视为活跃,可以使用 Cashier 提供的 keepPastDueSubscriptionsActive 方法。通常,此方法应在 AppServiceProviderregister 方法中调用:

php
use Laravel\Paddle\Cashier;

/**
 * 注册任何应用程序服务。
 *
 * @return void
 */
public function register()
{
    Cashier::keepPastDueSubscriptionsActive();
}
exclamation

当订阅处于 past_due 状态时,无法更改,直到支付信息已更新。因此,当订阅处于 past_due 状态时,swapupdateQuantity 方法将抛出异常。

订阅范围

大多数订阅状态也可用作查询范围,以便你可以轻松查询数据库中处于给定状态的订阅:

php
// 获取所有活跃的订阅...
$subscriptions = Subscription::query()->active()->get();

// 获取用户的所有已取消订阅...
$subscriptions = $user->subscriptions()->cancelled()->get();

下面是可用范围的完整列表:

php
Subscription::query()->active();
Subscription::query()->onTrial();
Subscription::query()->notOnTrial();
Subscription::query()->pastDue();
Subscription::query()->recurring();
Subscription::query()->ended();
Subscription::query()->paused();
Subscription::query()->notPaused();
Subscription::query()->onPausedGracePeriod();
Subscription::query()->notOnPausedGracePeriod();
Subscription::query()->cancelled();
Subscription::query()->notCancelled();
Subscription::query()->onGracePeriod();
Subscription::query()->notOnGracePeriod();

订阅单次收费

订阅单次收费允许你对订阅者进行一次性收费:

php
$response = $user->subscription('default')->charge(12.99, 'Support Add-on');

单次收费不同,此方法将立即对订阅的客户存储支付方式进行收费。收费金额应始终以订阅的货币定义。

更新支付信息

Paddle 始终为每个订阅保存一个支付方式。如果你想更新订阅的默认支付方式,首先应该使用订阅模型上的 updateUrl 方法生成一个订阅“更新 URL”:

php
use App\Models\User;

$user = User::find(1);

$updateUrl = $user->subscription('default')->updateUrl();

然后,你可以使用生成的 URL 结合 Cashier 提供的 paddle-button Blade 组件,允许用户启动 Paddle 小部件并更新其支付信息:

html
<x-paddle-button :url="$updateUrl" class="px-8 py-4">
    Update Card
</x-paddle-button>

当用户完成更新其信息后,Paddle 将发送一个 subscription_updated webhook,并在应用程序的数据库中更新订阅详细信息。

更改计划

在用户订阅你的应用程序后,他们可能偶尔想要更改为新的订阅计划。要更新用户的订阅计划,应将 Paddle 计划的标识符传递给订阅的 swap 方法:

php
use App\Models\User;

$user = User::find(1);

$user->subscription('default')->swap($premium = 34567);

如果你想交换计划并立即向用户开具发票,而不是等待他们的下一个计费周期,可以使用 swapAndInvoice 方法:

php
$user = User::find(1);

$user->subscription('default')->swapAndInvoice($premium = 34567);
exclamation

当试用期处于活动状态时,无法交换计划。有关此限制的更多信息,请参阅 Paddle 文档

按比例分配

默认情况下,Paddle 在计划之间交换时按比例分配费用。noProrate 方法可用于在不按比例分配费用的情况下更新订阅:

php
$user->subscription('default')->noProrate()->swap($premium = 34567);

订阅数量

有时订阅会受到“数量”的影响。例如,项目管理应用程序可能每月每个项目收费 10 美元。要轻松增加或减少订阅的数量,请使用 incrementQuantitydecrementQuantity 方法:

php
$user = User::find(1);

$user->subscription('default')->incrementQuantity();

// 在订阅的当前数量上增加五个...
$user->subscription('default')->incrementQuantity(5);

$user->subscription('default')->decrementQuantity();

// 在订阅的当前数量上减少五个...
$user->subscription('default')->decrementQuantity(5);

或者,可以使用 updateQuantity 方法设置特定数量:

php
$user->subscription('default')->updateQuantity(10);

noProrate 方法可用于在不按比例分配费用的情况下更新订阅的数量:

php
$user->subscription('default')->noProrate()->updateQuantity(10);

订阅修饰符

订阅修饰符允许你实现计量计费或使用附加组件扩展订阅。

例如,你可能想要为标准订阅提供“高级支持”附加组件。你可以这样创建此修饰符:

php
$modifier = $user->subscription('default')->newModifier(12.99)->create();

上面的示例将向订阅添加一个 12.99 美元的附加组件。默认情况下,此费用将在你为订阅配置的每个间隔中重复。如果你愿意,可以使用修饰符的 description 方法为修饰符添加可读的描述:

php
$modifier = $user->subscription('default')->newModifier(12.99)
    ->description('Premium Support')
    ->create();

为了说明如何使用修饰符实现计量计费,假设你的应用程序按用户发送的每条短信收费。首先,你应该在 Paddle 仪表板中创建一个 0 美元的计划。一旦用户订阅了此计划,可以向订阅添加表示每个单独收费的修饰符:

php
$modifier = $user->subscription('default')->newModifier(0.99)
    ->description('New text message')
    ->oneTime()
    ->create();

如你所见,我们在创建此修饰符时调用了 oneTime 方法。此方法将确保修饰符仅收费一次,并且不会在每个计费间隔中重复。

检索修饰符

可以通过 modifiers 方法检索订阅的所有修饰符列表:

php
$modifiers = $user->subscription('default')->modifiers();

foreach ($modifiers as $modifier) {
    $modifier->amount(); // $0.99
    $modifier->description; // New text message.
}

删除修饰符

可以通过在 Laravel\Paddle\Modifier 实例上调用 delete 方法来删除修饰符:

php
$modifier->delete();

多重订阅

Paddle 允许你的客户同时拥有多个订阅。例如,你可能经营一个提供游泳订阅和举重订阅的健身房,每个订阅可能有不同的定价。当然,客户应该能够订阅任一或两个计划。

当你的应用程序创建订阅时,可以将订阅的名称提供给 newSubscription 方法。名称可以是表示用户正在启动的订阅类型的任何字符串:

php
use Illuminate\Http\Request;

Route::post('/swimming/subscribe', function (Request $request) {
    $request->user()
        ->newSubscription('swimming', $swimmingMonthly = 12345)
        ->create($request->paymentMethodId);

    // ...
});

在此示例中,我们为客户启动了一个月度游泳订阅。但是,他们可能希望在以后切换到年度订阅。在调整客户的订阅时,我们可以简单地交换 swimming 订阅的价格:

php
$user->subscription('swimming')->swap($swimmingYearly = 34567);

当然,你也可以完全取消订阅:

php
$user->subscription('swimming')->cancel();

暂停订阅

要暂停订阅,请在用户的订阅上调用 pause 方法:

php
$user->subscription('default')->pause();

当订阅暂停时,Cashier 将自动在数据库中设置 paused_from 列。此列用于知道何时 paused 方法应开始返回 true。例如,如果客户在 3 月 1 日暂停订阅,但订阅原定于 3 月 5 日重复,则 paused 方法将继续返回 false,直到 3 月 5 日。这是因为用户通常被允许在其计费周期结束之前继续使用应用程序。

可以使用 onPausedGracePeriod 方法确定用户是否暂停了订阅但仍处于“宽限期”:

php
if ($user->subscription('default')->onPausedGracePeriod()) {
    //
}

要恢复暂停的订阅,可以在用户的订阅上调用 unpause 方法:

php
$user->subscription('default')->unpause();
exclamation

暂停的订阅无法修改。如果你想交换到不同的计划或更新数量,必须先恢复订阅。

取消订阅

要取消订阅,请在用户的订阅上调用 cancel 方法:

php
$user->subscription('default')->cancel();

当订阅被取消时,Cashier 将自动在数据库中设置 ends_at 列。此列用于知道何时 subscribed 方法应开始返回 false。例如,如果客户在 3 月 1 日取消订阅,但订阅原定于 3 月 5 日结束,则 subscribed 方法将继续返回 true,直到 3 月 5 日。这是因为用户通常被允许在其计费周期结束之前继续使用应用程序。

可以使用 onGracePeriod 方法确定用户是否已取消订阅但仍处于“宽限期”:

php
if ($user->subscription('default')->onGracePeriod()) {
    //
}

如果你希望立即取消订阅,可以在用户的订阅上调用 cancelNow 方法:

php
$user->subscription('default')->cancelNow();
exclamation

Paddle 的订阅在取消后无法恢复。如果你的客户希望恢复其订阅,他们将不得不订阅新的订阅。

订阅试用

提前支付方式

exclamation

在试用和收集支付方式详细信息时,Paddle 阻止任何订阅更改,例如交换计划或更新数量。如果你想在试用期间允许客户交换计划,必须取消并重新创建订阅。

如果你想在仍然收集支付方式信息的同时向客户提供试用期,应在创建订阅支付链接时使用 trialDays 方法:

php
use Illuminate\Http\Request;

Route::get('/user/subscribe', function (Request $request) {
    $payLink = $request->user()->newSubscription('default', $monthly = 12345)
                ->returnTo(route('home'))
                ->trialDays(10)
                ->create();

    return view('billing', ['payLink' => $payLink]);
});

此方法将在应用程序的数据库中设置订阅记录的试用期结束日期,并指示 Paddle 在此日期之后才开始向客户收费。

exclamation

如果客户的订阅在试用期结束日期之前未取消,他们将在试用期到期后立即被收费,因此你应该确保通知用户其试用期结束日期。

可以使用用户实例的 onTrial 方法或订阅实例的 onTrial 方法确定用户是否在试用期内。以下两个示例是等效的:

php
if ($user->onTrial('default')) {
    //
}

if ($user->subscription('default')->onTrial()) {
    //
}

要确定现有试用期是否已过期,可以使用 hasExpiredTrial 方法:

php
if ($user->hasExpiredTrial('default')) {
    //
}

if ($user->subscription('default')->hasExpiredTrial()) {
    //
}

在 Paddle / Cashier 中定义试用天数

您可以选择在 Paddle 仪表板中定义计划的试用天数,或者始终使用 Cashier 显式传递它们。如果您选择在 Paddle 中定义计划的试用天数,您应该注意到新的订阅,包括过去曾有过订阅的客户的新订阅,将始终获得试用期,除非您显式调用 trialDays(0) 方法。

无需提前提供付款方式

如果您希望在不收集用户付款方式信息的情况下提供试用期,您可以在附加到用户的客户记录上设置 trial_ends_at 列为您期望的试用结束日期。这通常在用户注册期间完成:

php
use App\Models\User;

$user = User::create([
    // ...
]);

$user->createAsCustomer([
    'trial_ends_at' => now()->addDays(10)
]);

Cashier 将这种类型的试用称为“通用试用”,因为它不附加到任何现有订阅上。如果当前日期未超过 trial_ends_at 的值,User 实例上的 onTrial 方法将返回 true

php
if ($user->onTrial()) {
    // 用户在试用期内...
}

一旦您准备好为用户创建实际订阅,您可以像往常一样使用 newSubscription 方法:

php
use Illuminate\Http\Request;

Route::get('/user/subscribe', function (Request $request) {
    $payLink = $user->newSubscription('default', $monthly = 12345)
        ->returnTo(route('home'))
        ->create();

    return view('billing', ['payLink' => $payLink]);
});

要检索用户的试用结束日期,您可以使用 trialEndsAt 方法。如果用户在试用期内,该方法将返回一个 Carbon 日期实例;如果不是,则返回 null。如果您想获取特定订阅(而非默认订阅)的试用结束日期,您还可以传递一个可选的订阅名称参数:

php
if ($user->onTrial()) {
    $trialEndsAt = $user->trialEndsAt('main');
}

如果您希望具体知道用户在其“通用”试用期内且尚未创建实际订阅,您可以使用 onGenericTrial 方法:

php
if ($user->onGenericTrial()) {
    // 用户在其“通用”试用期内...
}
exclamation

在创建 Paddle 订阅后,无法延长或修改试用期。

处理 Paddle Webhooks

Paddle 可以通过 webhooks 通知您的应用程序各种事件。默认情况下,Cashier 服务提供者会注册一个指向 Cashier 的 webhook 控制器的路由。此控制器将处理所有传入的 webhook 请求。

默认情况下,此控制器将自动处理因过多失败的收费(由您的 Paddle dunning 设置定义)而取消的订阅、订阅更新和付款方式更改;然而,正如我们将很快发现的,您可以扩展此控制器以处理您喜欢的任何 Paddle webhook 事件。

为了确保您的应用程序可以处理 Paddle webhooks,请确保在 Paddle 控制面板中配置 webhook URL。默认情况下,Cashier 的 webhook 控制器响应 /paddle/webhook URL 路径。您应该在 Paddle 控制面板中启用的所有 webhooks 的完整列表是:

  • 订阅已创建
  • 订阅已更新
  • 订阅已取消
  • 付款成功
  • 订阅付款成功
exclamation

确保使用 Cashier 的webhook 签名验证中间件保护传入请求。

Webhooks 与 CSRF 保护

由于 Paddle webhooks 需要绕过 Laravel 的 CSRF 保护,请确保在 App\Http\Middleware\VerifyCsrfToken 中间件中将 URI 列为例外,或将路由列在 web 中间件组之外:

php
protected $except = [
    'paddle/*',
];

Webhooks 与本地开发

为了让 Paddle 能够在本地开发期间向您的应用程序发送 webhooks,您需要通过站点共享服务(如 NgrokExpose)公开您的应用程序。如果您使用 Laravel Sail 在本地开发应用程序,您可以使用 Sail 的站点共享命令

定义 Webhook 事件处理程序

Cashier 自动处理因失败收费而取消的订阅和其他常见的 Paddle webhooks。然而,如果您有其他想要处理的 webhook 事件,您可以通过监听 Cashier 调度的以下事件来实现:

  • Laravel\Paddle\Events\WebhookReceived
  • Laravel\Paddle\Events\WebhookHandled

这两个事件都包含 Paddle webhook 的完整负载。例如,如果您希望处理 invoice.payment_succeeded webhook,您可以注册一个监听器来处理该事件:

php
<?php

namespace App\Listeners;

use Laravel\Paddle\Events\WebhookReceived;

class PaddleEventListener
{
    /**
     * 处理接收到的 Paddle webhooks。
     *
     * @param  \Laravel\Paddle\Events\WebhookReceived  $event
     * @return void
     */
    public function handle(WebhookReceived $event)
    {
        if ($event->payload['alert_name'] === 'payment_succeeded') {
            // 处理传入的事件...
        }
    }
}

定义监听器后,您可以在应用程序的 EventServiceProvider 中注册它:

php
<?php

namespace App\Providers;

use App\Listeners\PaddleEventListener;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Laravel\Paddle\Events\WebhookReceived;

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        WebhookReceived::class => [
            PaddleEventListener::class,
        ],
    ];
}

Cashier 还会发出专用于接收到的 webhook 类型的事件。除了来自 Paddle 的完整负载外,它们还包含用于处理 webhook 的相关模型,例如计费模型、订阅或收据:

  • Laravel\Paddle\Events\PaymentSucceeded
  • Laravel\Paddle\Events\SubscriptionPaymentSucceeded
  • Laravel\Paddle\Events\SubscriptionCreated
  • Laravel\Paddle\Events\SubscriptionUpdated
  • Laravel\Paddle\Events\SubscriptionCancelled

您还可以通过在应用程序的 .env 文件中定义 CASHIER_WEBHOOK 环境变量来覆盖默认的内置 webhook 路由。此值应为您的 webhook 路由的完整 URL,并需要与您在 Paddle 控制面板中设置的 URL 匹配:

ini
CASHIER_WEBHOOK=https://example.com/my-paddle-webhook-url

验证 Webhook 签名

为了保护您的 webhooks,您可以使用 Paddle 的 webhook 签名。为了方便起见,Cashier 自动包含一个中间件,该中间件验证传入的 Paddle webhook 请求是否有效。

要启用 webhook 验证,请确保在应用程序的 .env 文件中定义 PADDLE_PUBLIC_KEY 环境变量。可以从您的 Paddle 账户仪表板中检索公钥。

单次收费

简单收费

如果您想对客户进行一次性收费,您可以在计费模型实例上使用 charge 方法来生成收费的支付链接。charge 方法接受收费金额(浮点数)作为其第一个参数,收费描述作为其第二个参数:

php
use Illuminate\Http\Request;

Route::get('/store', function (Request $request) {
    return view('store', [
        'payLink' => $user->charge(12.99, 'Action Figure')
    ]);
});

生成支付链接后,您可以使用 Cashier 提供的 paddle-button Blade 组件来允许用户启动 Paddle 小部件并完成收费:

blade
<x-paddle-button :url="$payLink" class="px-8 py-4">
    购买
</x-paddle-button>

charge 方法接受一个数组作为其第三个参数,允许您将任何选项传递给底层的 Paddle 支付链接创建。请查阅 Paddle 文档以了解创建收费时可用的选项:

php
$payLink = $user->charge(12.99, 'Action Figure', [
    'custom_option' => $value,
]);

收费以 cashier.currency 配置选项中指定的货币进行。默认情况下,这设置为 USD。您可以通过在应用程序的 .env 文件中定义 CASHIER_CURRENCY 环境变量来覆盖默认货币:

ini
CASHIER_CURRENCY=EUR

您还可以使用 Paddle 的动态定价匹配系统覆盖每种货币的价格。为此,请传递一个价格数组而不是固定金额:

php
$payLink = $user->charge([
    'USD:19.99',
    'EUR:15.99',
], 'Action Figure');

收费产品

如果您想对 Paddle 中配置的特定产品进行一次性收费,您可以在计费模型实例上使用 chargeProduct 方法来生成支付链接:

php
use Illuminate\Http\Request;

Route::get('/store', function (Request $request) {
    return view('store', [
        'payLink' => $request->user()->chargeProduct($productId = 123)
    ]);
});

然后,您可以将支付链接提供给 paddle-button 组件,以允许用户初始化 Paddle 小部件:

blade
<x-paddle-button :url="$payLink" class="px-8 py-4">
    购买
</x-paddle-button>

chargeProduct 方法接受一个数组作为其第二个参数,允许您将任何选项传递给底层的 Paddle 支付链接创建。请查阅 Paddle 文档以了解创建收费时可用的选项:

php
$payLink = $user->chargeProduct($productId, [
    'custom_option' => $value,
]);

退款订单

如果您需要退款 Paddle 订单,您可以使用 refund 方法。此方法接受 Paddle 订单 ID 作为其第一个参数。您可以使用 receipts 方法检索给定计费模型的收据:

php
use App\Models\User;

$user = User::find(1);

$receipt = $user->receipts()->first();

$refundRequestId = $user->refund($receipt->order_id);

您可以选择指定特定的退款金额以及退款原因:

php
$receipt = $user->receipts()->first();

$refundRequestId = $user->refund(
    $receipt->order_id, 5.00, '未使用的产品时间'
);
lightbulb

在联系 Paddle 支持时,您可以使用 $refundRequestId 作为退款的参考。

收据

您可以通过 receipts 属性轻松检索计费模型的收据数组:

php
use App\Models\User;

$user = User::find(1);

$receipts = $user->receipts;

在为客户列出收据时,您可以使用收据实例的方法来显示相关的收据信息。例如,您可能希望在表格中列出每个收据,允许用户轻松下载任何收据:

html
<table>
    @foreach ($receipts as $receipt)
        <tr>
            <td>{{ $receipt->paid_at->toFormattedDateString() }}</td>
            <td>{{ $receipt->amount() }}</td>
            <td><a href="{{ $receipt->receipt_url }}" target="_blank">下载</a></td>
        </tr>
    @endforeach
</table>

过去和即将到来的付款

您可以使用 lastPaymentnextPayment 方法来检索和显示客户的过去或即将到来的定期订阅付款:

php
use App\Models\User;

$user = User::find(1);

$subscription = $user->subscription('default');

$lastPayment = $subscription->lastPayment();
$nextPayment = $subscription->nextPayment();

这两个方法都将返回一个 Laravel\Paddle\Payment 实例;然而,当计费周期结束时(例如订阅已取消),nextPayment 将返回 null

blade
下次付款:{{ $nextPayment->amount() }} 到期日 {{ $nextPayment->date()->format('d/m/Y') }}

处理失败的付款

订阅付款因各种原因而失败,例如卡过期或卡上资金不足。当这种情况发生时,我们建议您让 Paddle 为您处理付款失败。具体来说,您可以在 Paddle 仪表板中设置 Paddle 的自动账单电子邮件

或者,您可以通过监听 Cashier 调度的 WebhookReceived 事件来对 subscription_payment_failed Paddle 事件进行更精确的自定义。您还应确保在 Paddle 仪表板的 Webhook 设置中启用“订阅付款失败”选项:

php
<?php

namespace App\Listeners;

use Laravel\Paddle\Events\WebhookReceived;

class PaddleEventListener
{
    /**
     * 处理接收到的 Paddle webhooks。
     *
     * @param  \Laravel\Paddle\Events\WebhookReceived  $event
     * @return void
     */
    public function handle(WebhookReceived $event)
    {
        if ($event->payload['alert_name'] === 'subscription_payment_failed') {
            // 处理失败的订阅付款...
        }
    }
}

定义监听器后,您应在应用程序的 EventServiceProvider 中注册它:

php
<?php

namespace App\Providers;

use App\Listeners\PaddleEventListener;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Laravel\Paddle\Events\WebhookReceived;

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        WebhookReceived::class => [
            PaddleEventListener::class,
        ],
    ];
}

测试

在测试时,您应该手动测试您的计费流程,以确保您的集成按预期工作。

对于自动化测试,包括在 CI 环境中执行的测试,您可以使用 Laravel 的 HTTP 客户端来模拟对 Paddle 的 HTTP 调用。虽然这不会测试来自 Paddle 的实际响应,但它确实提供了一种在不实际调用 Paddle API 的情况下测试您的应用程序的方法。