最近,我需要在项目中集成 PayPal,以实现自动循环扣款的功能。尽管通过 百度 和 Google 搜索了一番,但除了 PayPal 官网外,几乎没有找到相关开发教程。最终,我花了两天时间在 PayPal 官网上查阅文档,成功完成了集成。接下来,我将总结如何使用 PayPal 的支付接口,特别是如何实现循环扣款功能。
PayPal 目前提供了多套支付接口:
Braintree:支持 Express Checkout;
REST API:当前主流的接口方式;
NVP/SOAP API:旧接口,不推荐使用。
Braintree 是 PayPal 收购的一家公司,它不仅支持 PayPal 支付,还提供信用卡管理、客户信息管理等全套解决方案。Braintree 的 Dashboard 功能比 PayPal REST API 更强大,适合需要全面管理的场景。然而,Braintree 在国内不支持,因此只能放弃。
REST API 是当前主流的支付接口,基于 OAuth 2.0 认证,支持现代 API 调用方式。如果你熟悉 OAuth 2.0 和 REST API,那么使用这套接口会非常顺手。
除非 REST API 无法满足你的需求(例如政策限制),否则不推荐使用旧接口。当前全球都在向 OAuth 2.0 和 REST API 迁移,建议与时俱进。
PayPal 官方提供了详细的 API 文档:PayPal REST API 文档。然而,直接调用这些 API 可能会比较繁琐。为了快速实现业务需求,建议直接使用 PayPal 官方提供的 PayPal-PHP-SDK,并以其 Wiki 为起点进行开发。
在开始之前,请确保你已准备好以下配置:
Client ID 和 Client Secret;
Webhook API(必须是 https 开头且使用 443 端口,本地调试建议结合 ngrok 反向代理);
Return URL(同上,必须是 https 且使用 443 端口)。
理解 PayPal 的接口分类有助于更好地实现业务需求。以下是主要接口类别的简要说明:
Payments:一次性支付接口,不支持循环扣款;
Authorization and Capture:支持通过 PayPal 账户登录并获取信息;
Billing Plan & Agreements:实现订阅功能,支持循环扣款;
Vault:存储信用卡信息;
Notifications:处理 Webhook 信息;
Identity:实现 OAuth 2.0 登录,获取 token。
实现自动循环扣款功能分为以下四个步骤:
创建并激活升级计划(Billing Plan);
创建订阅(Agreement),并跳转到 PayPal 网站等待用户同意;
用户同意后,执行订阅;
获取扣款账单。
升级计划对应 Plan 类。创建时需要注意以下几点:
计划创建后,默认状态为 CREATED,需手动激活为 ACTIVE 后才能使用;
计划中的 PaymentDefinition 和 MerchantPreferences 对象不能为空;
若要创建试用计划(TRIAL),必须附带一个 REGULAR 支付定义;
setSetupFee 方法设置了首次扣款的费用,而 Agreement 对象设置的是第二次开始的扣款金额。
以下是一个创建标准升级计划的代码示例:
php
$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(array('value' => $param->amount, 'currency' => $param->currency)));
$chargeModel = new ChargeModel();
$chargeModel->setType('TAX')
->setAmount(new Currency(array('value' => 0, 'currency' => $param->currency)));
$merchantPreferences = new MerchantPreferences();
$merchantPreferences->setReturnUrl(config('payment.returnurl') . "?success=true")
->setCancelUrl(config('payment.returnurl') . "?success=false")
->setAutoBillAmount("yes")
->setInitialFailAmountAction("CONTINUE")
->setMaxFailAttempts("0")
->setSetupFee(new Currency(array('value' => $param->amount, 'currency' => 'USD')));
$plan->setPaymentDefinitions(array($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;
}
创建 Agreement 对象后,需跳转到 PayPal 网站等待用户同意。注意事项:
setSetupFee 设置的是首次扣款费用,Agreement 设置的是第二次开始的扣款金额;
setStartDate 设置的是第二次扣款时间,需按月加 1,并使用 ISO8601 格式;
通过 getApprovalLink 方法获取跳转链接,提取其中的 token 作为唯一标识。
以下是一个创建订阅的代码示例:
php
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 "create payment failed, please retry or contact the merchant.";
}
return $approvalUrl;
}
用户同意后,需执行 Agreement 的 execute 方法完成订阅。注意:
订阅完成后,扣款可能会延迟几分钟;
如果首次扣款金额为 0,则需等到循环扣款时间到达时才会生成订单。
代码示例如下:
php
public function onPay($request) {
$apiContext = $this->getApiContext();
if ($request->has('success') && $request->success == 'true') {
$token = $request->token;
$agreement = new \PayPal\Api\Agreement();
try {
$agreement->execute($token, $apiContext);
} catch(\Exception $e) {
return null;
}
return $agreement;
}
return null;
}
订阅完成后,需获取交易记录。注意:
start_date 和 end_date 不能为空;
交易记录可能不会立即生成,需稍后重试。
代码示例如下:
php
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("get transactions failed" . $e->getMessage());
return null;
}
return $result->getAgreementTransactionList();
}
在实现自动循环扣款功能时,需注意以下几点:
网络延迟:在国内使用 Sandbox 测试时,连接较慢且容易超时,需考虑用户中断操作的情况;
Webhook 实现:必须实现 Webhook,以便在用户取消订阅时及时通知;
计划切换:用户订阅一个计划后,切换计划需先取消旧计划;
原子操作:订阅、取消、修改用户信息等操作应作为原子操作执行,并放入队列中处理。