PAYATHON 2026

Laravel Billing Package 能否集成其他支付平台?

支付小周

结论

能否接入 Rave payment,取决于你使用的 Laravel Billing Package 有没有提供支付网关适配器接口。

如果这个 package 只封装了 Stripe、Braintree 等指定平台,仅修改配置无法切换到 Rave。更合适的处理方式是:

  1. 继续用现有 package 管理套餐和订阅状态等业务;
  2. 单独实现 Rave 支付网关适配器;
  3. 如果 package 与特定支付平台强耦合,不建议勉强扩展。可以自行维护订阅逻辑,或者改用明确支持自定义 driver 的计费 package。

Rave 的产品名称和 API 可能已经发生变化。接口地址、签名算法和字段应以你当前使用的 Flutterwave/Rave 官方文档为准。

为什么通常不能直接集成

计费 package 通常不只是调用支付 API,还依赖特定平台的数据模型和功能,例如:

  • customer、subscription、plan 等远程对象;
  • 支付方式 token;
  • 自动续费和扣款失败处理;
  • 退款、取消及订阅变更接口;
  • webhook 事件名称和签名验证;
  • 试用期、按比例计费和账单周期计算。

所以,即使 Rave 可以完成单次支付,也不等于它能直接替换 package 原有的支付平台。只有 package 提供 Gateway、Driver、Provider 等扩展接口时,才适合加入自定义实现。

推荐的实现方式

先定义一个与支付平台无关的接口,让业务代码只依赖这个接口:

<?php

namespace App\Contracts;

interface PaymentGateway
{
    public function initializePayment(array $payment): array;

    public function verifyPayment(string $transactionReference): array;

    public function refund(
        string $transactionReference,
        int $amount
    ): array;
}

再实现 Rave 网关。下面的代码只展示结构,不代表 Rave 当前版本的真实参数或 API 路径:

<?php

namespace App\Payments;

use App\Contracts\PaymentGateway;
use Illuminate\Support\Facades\Http;
use RuntimeException;

class RavePaymentGateway implements PaymentGateway
{
    public function __construct(
        private readonly string $baseUrl,
        private readonly string $secretKey,
    ) {
    }

    public function initializePayment(array $payment): array
    {
        $response = Http::withToken($this->secretKey)
            ->acceptJson()
            ->post($this->baseUrl . '/payments', $payment)
            ->throw();

        return $response->json();
    }

    public function verifyPayment(string $transactionReference): array
    {
        $response = Http::withToken($this->secretKey)
            ->acceptJson()
            ->get(
                $this->baseUrl . '/transactions/' .
                rawurlencode($transactionReference) .
                '/verify'
            )
            ->throw();

        $result = $response->json();

        if (!is_array($result)) {
            throw new RuntimeException('Invalid payment verification response.');
        }

        return $result;
    }

    public function refund(
        string $transactionReference,
        int $amount
    ): array {
        $response = Http::withToken($this->secretKey)
            ->acceptJson()
            ->post($this->baseUrl . '/refunds', [
                'transaction_reference' => $transactionReference,
                'amount' => $amount,
            ])
            ->throw();

        return $response->json();
    }
}

在服务容器中绑定具体实现:

<?php

namespace App\Providers;

use App\Contracts\PaymentGateway;
use App\Payments\RavePaymentGateway;
use Illuminate\Support\ServiceProvider;

class PaymentServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(PaymentGateway::class, function () {
            return new RavePaymentGateway(
                baseUrl: (string) config('services.rave.base_url'),
                secretKey: (string) config('services.rave.secret_key'),
            );
        });
    }
}

相关配置可以放在 config/services.php 中:

'rave' => [
    'base_url' => env('RAVE_BASE_URL'),
    'secret_key' => env('RAVE_SECRET_KEY'),
    'webhook_secret' => env('RAVE_WEBHOOK_SECRET'),
],

业务层统一通过接口调用支付网关:

<?php

namespace App\Services;

use App\Contracts\PaymentGateway;

class SubscriptionService
{
    public function __construct(
        private readonly PaymentGateway $gateway,
    ) {
    }

    public function startCheckout(
        int $userId,
        int $amount,
        string $currency
    ): array {
        $reference = 'sub_' . $userId . '_' . bin2hex(random_bytes(8));

        return $this->gateway->initializePayment([
            'tx_ref' => $reference,
            'amount' => $amount / 100,
            'currency' => $currency,
            'redirect_url' => route('billing.callback'),
        ]);
    }
}

tx_ref 等字段只是示例,必须按照实际 API 文档调整。

支付成功后的处理

不要仅凭浏览器跳转结果开通订阅。用户可能伪造回调参数,也可能已经完成付款,却没有返回网站。

通常可以按以下流程处理:

  1. 创建本地待支付记录,并生成唯一交易编号;
  2. 跳转到支付平台完成付款;
  3. 接收 webhook;
  4. 验证 webhook 签名;
  5. 主动调用交易查询接口进行复核;
  6. 核对金额、币种、商户编号和交易状态;
  7. 在数据库事务中更新付款和订阅状态;
  8. 对同一笔交易进行幂等处理。

示例结构如下:

public function webhook(Request $request, PaymentGateway $gateway)
{
    // 此处必须先按照支付平台文档验证 webhook 签名。
    // 不要直接信任请求正文中的 status。

    $reference = (string) $request->input('tx_ref');
    $transaction = $gateway->verifyPayment($reference);

    DB::transaction(function () use ($reference, $transaction) {
        $payment = Payment::query()
            ->where('reference', $reference)
            ->lockForUpdate()
            ->firstOrFail();

        if ($payment->status === 'paid') {
            return;
        }

        // 这里还应核对交易状态、金额、币种和收款账户。
        $payment->update([
            'status' => 'paid',
            'gateway_payload' => $transaction,
        ]);

        $payment->subscription()->update([
            'status' => 'active',
        ]);
    });

    return response()->json(['received' => true]);
}

如何选择其他 package

选择替代方案时,不能只看 package 是否标注了 “billing” 或 “subscription”,还要确认它是否具备以下能力:

  • 明确支持自定义 payment driver;
  • 允许替换 customer、charge、refund 和 webhook 实现;
  • 将订阅业务与支付平台 SDK 解耦;
  • 支持你使用的 Laravel 和 PHP 版本;
  • 仍在维护;
  • 能覆盖自动续费、退款、失败重试和取消订阅等需求。

如果没有成熟的 package 明确支持 Rave,自建一层轻量的支付适配器,通常比修改一个与 Stripe 或 Braintree 强绑定的 package 更容易维护。套餐、订阅周期、权限和账单记录可以保存在本地数据库中,支付平台只负责收款、退款和查询交易。

注意事项

  • 金额建议以最小货币单位的整数保存在本地,例如分,以免产生浮点数误差。
  • API 密钥只能保存在服务端,不能写入 JavaScript 或公开仓库。
  • webhook 必须验证签名,并支持重复调用。
  • 确认支付成功前,需要再次核对金额、币种和交易编号。
  • 网络超时不代表支付失败,应通过查询接口确认最终状态。
  • 不要直接修改第三方 package 的 vendor 源码。需要扩展时,应使用接口、事件或独立适配器。
  • 能否自动续费,取决于 Rave 当前 API 是否支持 tokenization、授权扣款或订阅能力,Laravel package 无法单方面实现。

备注:内容仅供参考。