# 如何在 PayPal 实现循环扣款(订阅)功能? **Published by:** [hwen](https://paragraph.com/@hwen/) **Published on:** 2025-01-16 **URL:** https://paragraph.com/@hwen/paypal ## Content 起因由于业务需求需要集成 PayPal,实现循环扣款功能,经过多次在 百度 和 Google 搜索,除了官方网站外,并未找到相关的开发教程。于是我花了两天时间深入了解并成功集成。本文将总结如何使用 PayPal 的支付接口。PayPal 现有的接口方式:Braintree 接口:通过 Braintree 实现 Express Checkout,后面会详细讲解。REST API 接口:当前主流的接口方式。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 IDClient SecretWebhook API(必须是 HTTPS 开头且使用 443 端口,本地调试可结合 ngrok)Return URL(确保符合要求)接口分类在了解了 REST API 后,理解接口分类对于完成业务需求非常重要。以下是一些常见的接口分类:Payments:一次性支付接口,不支持循环扣款。主要支持 PayPal 支付、信用卡支付等。Payouts:本项目中未使用。Authorization and Capture:支持通过 PayPal 账号登录并获取相关信息。Billing Plan & Agreements:用于实现 订阅 功能,支持循环扣款,这是本文的重点。Vault:用于存储信用卡信息。Notifications:处理 Webhook 信息。Invoice:用于票据处理。如何实现循环扣款实现循环扣款功能需要四个步骤:创建并激活升级计划;创建订阅(Agreement),并跳转到 PayPal 网站等待用户同意;用户同意后,执行订阅;获取扣款账单。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(); } 需要考虑的问题实现功能后,仍有以下注意事项:国内使用 Sandbox 测试时连接较慢,需考虑用户关闭页面的情况。必须实现 Webhook,否则无法接收到用户取消订阅的通知。用户在切换订阅计划时,必须取消之前的订阅。订阅过程应当作为原子操作,以确保长时间的操作不会影响用户体验。☞ WildCard | 一分钟注册,轻松订阅海外线上服务 使用门槛极低,微信支付宝均可开通使用。支持开通各类海外平台:ChatGPT、Claude、Google Play、Apple Store、OpenAI、X、Patreon、MidJourney、Amazon、POE、Microsoft、Facebook、GitHub、Telegram、PayPal等各类海淘订阅平台。使用邀请码:ACCPAY,立享消费0手续费,减免开卡费用。 ## Publication Information - [hwen](https://paragraph.com/@hwen/): Publication homepage - [All Posts](https://paragraph.com/@hwen/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@hwen): Subscribe to updates