# 如何在 PayPal 实现循环扣款（订阅）功能？

By [hwen](https://paragraph.com/@hwen) · 2025-01-16

---

起因
--

由于业务需求需要集成 PayPal，实现循环扣款功能，经过多次在 **百度** 和 **Google** 搜索，除了官方网站外，并未找到相关的开发教程。于是我花了两天时间深入了解并成功集成。本文将总结如何使用 PayPal 的支付接口。

### PayPal 现有的接口方式：

1.  **Braintree 接口**：通过 **Braintree** 实现 Express Checkout，后面会详细讲解。
    
2.  **REST API 接口**：当前主流的接口方式。
    
3.  **NVP/SOAP API 接口**：较旧的接口方式，已不推荐使用。
    

Braintree 接口
------------

**Braintree** 是 PayPal 收购的一家公司，除了支持 PayPal 支付外，它还提供了升级计划、信用卡和客户信息等全套管理功能。虽然 **PayPal** 的 **REST API** 接口也支持大部分功能，但 **Braintree** 可以直接通过后台管理这些信息，而 **PayPal** 的 **Dashboard** 并不能。因此，个人推荐使用 **Braintree**，特别是我使用的后端框架 **Laravel**，其 **Cashier** 解决方案默认支持 **Braintree**，这使得集成更加方便。

然而，问题也随之而来：**Braintree** 在国内并不支持，这让我只能转向其他接口。

REST API 接口
-----------

**REST API** 是顺应时代发展的接口，如果你熟悉 **OAuth 2.0** 和 **REST API**，使用起来应该不会有什么困惑。

旧接口
---

除非 **REST API** 无法满足特定需求（例如政策限制），否则不推荐使用旧的 **NVP/SOAP API**。全世界都在向 **OAuth 2.0** 和 **REST API** 迁移，使用最新的接口将带来更好的体验和支持。

REST API 介绍
-----------

PayPal 提供了官方的 API 参考文档，详尽地介绍了 API 的使用方式。不过，如果直接使用这些 API 会比较繁琐，我们更希望快速完成业务需求。建议直接安装 **PayPal-PHP-SDK**，并通过其 Wiki 作为起点进行集成。

在完成示例之前，请确保已配置好以下内容：

*   **Client ID**
    
*   **Client Secret**
    
*   **Webhook API**（必须是 HTTPS 开头且使用 443 端口，本地调试可结合 **ngrok**）
    
*   **Return URL**（确保符合要求）
    

### 接口分类

在了解了 **REST API** 后，理解接口分类对于完成业务需求非常重要。以下是一些常见的接口分类：

*   **Payments**：一次性支付接口，不支持循环扣款。主要支持 PayPal 支付、信用卡支付等。
    
*   **Payouts**：本项目中未使用。
    
*   **Authorization and Capture**：支持通过 PayPal 账号登录并获取相关信息。
    
*   **Billing Plan & Agreements**：用于实现 **订阅** 功能，支持循环扣款，这是本文的重点。
    
*   **Vault**：用于存储信用卡信息。
    
*   **Notifications**：处理 **Webhook** 信息。
    
*   **Invoice**：用于票据处理。
    

如何实现循环扣款
--------

实现循环扣款功能需要四个步骤：

1.  创建并激活升级计划；
    
2.  创建订阅（Agreement），并跳转到 PayPal 网站等待用户同意；
    
3.  用户同意后，执行订阅；
    
4.  获取扣款账单。
    

### 1\. 创建升级计划

升级计划对应 **Plan** 类。在创建时需要注意以下几点：

*   **Plan** 创建后默认为 **CREATED** 状态，必须修改为 **ACTIVE** 才能正常使用。
    
*   **PaymentDefinition** 和 **MerchantPreferences** 两个对象是必填项。
    
*   如果计划为 **TRIAL** 类型，必须有配套的 **REGULAR** 支付定义，否则会报错。
    

以下是一个 **Standard** 计划的示例：

    $param = [
        "name" => "standard_monthly",
        "display_name" => "Standard Plan",
        "desc" => "Standard Plan for one month",
        "type" => "REGULAR",
        "frequency" => "MONTH",
        "frequency_interval" => 1,
        "cycles" => 0,
        "amount" => 20,
        "currency" => "USD"
    ];
    

创建并激活计划的代码：

    public function createPlan($param)
    {
        $apiContext = $this->getApiContext();
        $plan = new Plan();
    
        $plan->setName($param->name)
            ->setDescription($param->desc)
            ->setType('INFINITE'); // 设置为无限循环
    
        $paymentDefinition = new PaymentDefinition();
        $paymentDefinition->setName($param->name)
            ->setType($param->type)
            ->setFrequency($param->frequency)
            ->setFrequencyInterval((string)$param->frequency_interval)
            ->setCycles((string)$param->cycles)
            ->setAmount(new Currency(['value' => $param->amount, 'currency' => $param->currency]));
    
        $chargeModel = new ChargeModel();
        $chargeModel->setType('TAX')
            ->setAmount(new Currency(['value' => 0, 'currency' => $param->currency]));
    
        $returnUrl = config('payment.returnurl');
        $merchantPreferences = new MerchantPreferences();
        $merchantPreferences->setReturnUrl("$returnUrl?success=true")
            ->setCancelUrl("$returnUrl?success=false")
            ->setAutoBillAmount("yes")
            ->setInitialFailAmountAction("CONTINUE")
            ->setMaxFailAttempts("0")
            ->setSetupFee(new Currency(['value' => $param->amount, 'currency' => 'USD']));
    
        $plan->setPaymentDefinitions([$paymentDefinition]);
        $plan->setMerchantPreferences($merchantPreferences);
    
        try {
            $output = $plan->create($apiContext);
        } catch (Exception $ex) {
            return false;
        }
    
        $patch = new Patch();
        $value = new PayPalModel('{"state":"ACTIVE"}');
        $patch->setOp('replace')
            ->setPath('/')
            ->setValue($value);
    
        $patchRequest = new PatchRequest();
        $patchRequest->addPatch($patch);
    
        $output->update($patchRequest, $apiContext);
    
        return $output;
    }
    

### 2\. 创建订阅（Agreement）

创建订阅的过程如下：

    public function createPayment($param)
    {
        $apiContext = $this->getApiContext();
        $agreement = new Agreement();
    
        $agreement->setName($param['name'])
            ->setDescription($param['desc'])
            ->setStartDate(Carbon::now()->addMonths(1)->toIso8601String());
    
        $plan = new Plan();
        $plan->setId($param['id']);
        $agreement->setPlan($plan);
    
        $payer = new Payer();
        $payer->setPaymentMethod('paypal');
        $agreement->setPayer($payer);
    
        try {
            $agreement = $agreement->create($apiContext);
            $approvalUrl = $agreement->getApprovalLink();
        } catch (Exception $ex) {
            return "创建支付失败，请重试或联系商家。";
        }
    
        return $approvalUrl; // 跳转到 PayPal 网站
    }
    

### 3\. 用户同意后，执行订阅

用户同意后，必须调用 **Agreement** 的 **execute** 方法才能完成订阅。

### 4\. 获取交易记录

订阅后，可能不会立刻产生交易记录，若为空可稍后再次尝试。获取交易记录的代码如下：

    public function transactions($id)
    {
        $apiContext = $this->getApiContext();
        $params = ['start_date' => date('Y-m-d', strtotime('-15 years')), 'end_date' => date('Y-m-d', strtotime('+5 days'))];
    
        try {
            $result = Agreement::searchTransactions($id, $params, $apiContext);
        } catch (\Exception $e) {
            Log::error("获取交易记录失败：" . $e->getMessage());
            return null;
        }
    
        return $result->getAgreementTransactionList();
    }
    

需要考虑的问题
-------

实现功能后，仍有以下注意事项：

1.  国内使用 **Sandbox** 测试时连接较慢，需考虑用户关闭页面的情况。
    
2.  必须实现 **Webhook**，否则无法接收到用户取消订阅的通知。
    
3.  用户在切换订阅计划时，必须取消之前的订阅。
    
4.  订阅过程应当作为原子操作，以确保长时间的操作不会影响用户体验。
    

☞ [WildCard | 一分钟注册，轻松订阅海外线上服务](https://bit.ly/bewildcard)

使用门槛极低，微信支付宝均可开通使用。支持开通各类海外平台：ChatGPT、Claude、Google Play、Apple Store、OpenAI、X、Patreon、MidJourney、Amazon、POE、Microsoft、Facebook、GitHub、Telegram、PayPal等各类海淘订阅平台。使用邀请码：**ACCPAY**，立享消费0手续费，减免开卡费用。

---

*Originally published on [hwen](https://paragraph.com/@hwen/paypal)*
