PayPal 支付:Checkout 收银台与 Subscription 订阅计划流程解析
今天我们将深入探讨 PayPal 支付的完整流程,从请求生命周期开始,逐步实现 Checkout 收银台和 Subscription 订阅功能。
一、支付生命周期
1. Checkout - 收银台支付
流程示意图如下(与支付宝的收银台类似):
流程详解:
- 本地应用组装参数并请求 Checkout 接口,获取支付 URL。
- 本地应用重定向至支付 URL,用户在 PayPal 登陆并确认支付,支付后跳转至本地应用地址。
- 本地应用请求 PayPal 执行付款接口发起扣款。
- PayPal 向本地应用发送异步通知,本地接收数据包并进行验签。
- 验签成功后执行支付完成后的操作(如修改订单状态、增加销量、发送邮件等)。
2. Subscription - 订阅支付
流程示意图:
流程详解:
- 创建一个订阅计划。
- 激活该计划。
- 用激活的计划创建订阅申请。
- 本地跳转至订阅申请链接,获取用户授权并完成首期付款,用户支付后跳转至本地应用地址。
- 回跳后,发起执行订阅请求。
- 接收到订阅授权异步回调结果,验证支付,成功后进行后续操作。
二、具体实现
理解了流程后,我们接下来进行编码实现。GitHub 上有许多 SDK,这里我们使用官方提供的 SDK。
Checkout 实现
1. 安装 PayPal 扩展
bash
$ composer require paypal/rest-api-sdk-php:* // 选择最新版本
2. 创建 PayPal 配置文件
bash
$ touch config/paypal.php
配置内容如下(沙箱和生产环境两套配置):
php
<?php
return [
// PayPal 沙箱配置
'sandbox' => [
'client_id' => env('PAYPAL_SANDBOX_CLIENT_ID', ''),
'secret' => env('PAYPAL_SANDBOX_SECRET', ''),
'notify_web_hook_id' => env('PAYPAL_SANDBOX_NOTIFY_WEB_HOOK_ID', ''),
'checkout_notify_web_hook_id' => env('PAYPAL_SANDBOX_CHECKOUT_NOTIFY_WEB_HOOK_ID', ''),
'subscription_notify_web_hook_id' => env('PAYPAL_SANDBOX_SUBSCRIPTION_NOTIFY_WEB_HOOK_ID', ''),
],
// PayPal 生产配置
'live' => [
'client_id' => env('PAYPAL_CLIENT_ID', ''),
'secret' => env('PAYPAL_SECRET', ''),
'notify_web_hook_id' => env('PAYPAL_NOTIFY_WEB_HOOK_ID', ''),
'checkout_notify_web_hook_id' => env('PAYPAL_CHECKOUT_NOTIFY_WEB_HOOK_ID', ''),
'subscription_notify_web_hook_id' => env('PAYPAL_SUBSCRIPTION_NOTIFY_WEB_HOOK_ID', ''),
],
];
3. 创建 PayPal 服务类
bash
$ mkdir -p app/Services && touch app/Services/PayPalService.php
4. 编写 Checkout 方法
可以参考官方提供的 DEMO。
php
<?php
namespace App\Services;
use App\Models\Order;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use PayPal\Api\Currency;
use PayPal\Auth\OAuthTokenCredential;
use PayPal\Rest\ApiContext;
use PayPal\Api\Amount;
use PayPal\Api\Details;
use PayPal\Api\Item;
use PayPal\Api\ItemList;
use PayPal\Api\Payer;
use PayPal\Api\Payment;
use PayPal\Api\RedirectUrls;
use PayPal\Api\Transaction;
use PayPal\Api\PaymentExecution;
use Symfony\Component\HttpKernel\Exception\HttpException;
class PayPalService
{
protected $config;
protected $notifyWebHookId;
public $apiContext;
public function __construct($config)
{
$this->config = $config;
$this->notifyWebHookId = $this->config['web_hook_id'];
$this->apiContext = new ApiContext(
new OAuthTokenCredential(
$this->config['client_id'],
$this->config['secret']
)
);
$this->apiContext->setConfig([
'mode' => $this->config['mode'],
'log.LogEnabled' => true,
'log.FileName' => storage_path('logs/PayPal.log'),
'log.LogLevel' => 'DEBUG',
'cache.enabled' => true,
]);
}
/**
* @Des 收银台支付
* @Author Mars
* @param Order $order
* @return string|null
*/
public function checkout(Order $order)
{
try {
$payer = new Payer();
$payer->setPaymentMethod('paypal');
$item = new Item();
$item->setName($order->product->title)
->setDescription($order->no)
->setCurrency($order->product->currency)
->setQuantity(1)
->setPrice($order->total_amount);
$itemList = new ItemList();
$itemList->setItems([$item]);
$details = new Details();
$details->setShipping(0)
->setSubtotal($order->total_amount);
$amount = new Amount();
$amount->setCurrency($order->product->currency)
->setTotal($order->total_amount)
->setDetails($details);
$transaction = new Transaction();
$transaction->setAmount($amount)
->setItemList($itemList)
->setDescription($order->no)
->setInvoiceNumber(uniqid());
$redirectUrls = new RedirectUrls();
$redirectUrls->setReturnUrl(route('payment.paypal.return', ['success' => 'true', 'no' => $order->no]))
->setCancelUrl(route('payment.paypal.return', ['success' => 'false', 'no' => $order->no]));
$payment = new Payment();
$payment->setIntent('sale')
->setPayer($payer)
->setRedirectUrls($redirectUrls)
->setTransactions([$transaction]);
$payment->create($this->apiContext);
return $payment->getApprovalLink();
} catch (HttpException $e) {
Log::error('PayPal Checkout Create Failed', [
'msg' => $e->getMessage(),
'code' => $e->getStatusCode(),
'data' => ['order' => ['no' => $order->no]]
]);
return null;
}
}
/**
* @Des 执行付款
* @Author Mars
* @param Payment $payment
* @return bool|Payment
*/
public function executePayment($paymentId)
{
try {
$payment = Payment::get($paymentId, $this->apiContext);
$execution = new PaymentExecution();
$execution->setPayerId($payment->getPayer()->getPayerInfo()->getPayerId());
$payment->execute($execution, $this->apiContext);
return Payment::get($payment->getId(), $this->apiContext);
} catch (HttpException $e) {
return false;
}
}
}
5. 注册 PayPal 服务类到容器中
打开文件 app/Providers/AppServiceProvider.php
,添加如下代码:
php
<?php
namespace App\Providers;
use App\Services\PayPalService;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton('paypal', function () {
if (app()->environment() !== 'production') {
$config = [
'mode' => 'sandbox',
'client_id' => config('paypal.sandbox.client_id'),
'secret' => config('paypal.sandbox.secret'),
'web_hook_id' => config('paypal.sandbox.notify_web_hook_id'),
];
} else {
$config = [
'mode' => 'live',
'client_id' => config('paypal.live.client_id'),
'secret' => config('paypal.live.secret'),
'web_hook_id' => config('paypal.live.notify_web_hook_id'),
];
}
return new PayPalService($config);
});
}
}
6. 创建控制器
bash
$ php artisan make:controller PaymentsController
php
<?php
namespace App\Http\Controllers;
use App\Models\Order;
class PaymentsController extends Controller
{
/*
* @Des PayPal-Checkout
* @Author Mars
* @param Order $order
/
public function payByPayPalCheckout(Order $order)
{
if ($order->paid_at || $order->closed) {
return json_encode(['code' => 422, 'msg' => 'Order Status Error.', 'url' => '']);
}
$approvalUrl = app('paypal')->checkout($order);
if (!$approvalUrl) {
return json_encode(['code' => 500, 'msg' => 'Interval Error.', 'url' => '']);
}
return json_encode(['code' => 201, 'msg' => 'Success.', 'url' => $approvalUrl]);
}
}
7. 回调方法
php
// app/Http/Controllers/PaymentController.php
use Illuminate\Http\Request;
class PaymentController extends Controller
{
/*
* @Des 付款回调入口
* @Author Mars
* @param Request $request
/
public function payPalReturn(Request $request)
{
if ($request->has('success') && $request->success == 'true') {
$payment = app('paypal')->executePayment($request->paymentId);
// TODO: 这里编写支付后的具体业务(如跳转到订单详情页)
} else {
// TODO: 处理支付失败的情况
}
}
}
8. 验签方法
在 PayPalService 中添加验签方法:
php
namespace App\Services;
use PayPal\Api\VerifyWebhookSignature;
class PayPalService
{
// ... 其他方法
/**
* @des 回调验签
* @author Mars
* @param Request $request
* @param $webHookId
* @return VerifyWebhookSignature|bool
*/
public function verify(Request $request, $webHookId = null)
{
try {
$headers = $request->header();
$headers = array_change_key_case($headers, CASE_UPPER);
$content = $request->getContent();
$signatureVerification = new VerifyWebhookSignature();
$signatureVerification->setAuthAlgo($headers['PAYPAL-AUTH-ALGO'][0]);
$signatureVerification->setTransmissionId($headers['PAYPAL-TRANSMISSION-ID'][0]);
$signatureVerification->setCertUrl($headers['PAYPAL-CERT-URL'][0]);
$signatureVerification->setWebhookId($webHookId ?: $this->notifyWebHookId);
$signatureVerification->setTransmissionSig($headers['PAYPAL-TRANSMISSION-SIG'][0]);
$signatureVerification->setTransmissionTime($headers['PAYPAL-TRANSMISSION-TIME'][0]);
$signatureVerification->setRequestBody($content);
$result = clone $signatureVerification;
$output = $signatureVerification->post($this->apiContext);
if ($output->getVerificationStatus() == "SUCCESS") {
return $result;
}
throw new HttpException(400, 'Verify Failed.');
} catch (HttpException $e) {
Log::error('PayPal Notification Verify Failed', ['msg' => $e->getMessage(), 'code' => $e->getStatusCode()]);
return false;
}
}
}
9. 异步回调
在支付控制器内添加异步回调处理方法:
php
// app/Http/Controllers/PaymentController.php
class PaymentController extends Controller
{
/*
* @des PayPal-Checkout-Notify
* @author Mars
* @param Request $request
* @return string
/
public function payPalNotify(Request $request)
{
Log::info('PayPal Checkout Notification', ['request' => ['header' => $request->header(), 'body' => $request->getContent()]]);
$response = app('paypal')->verify($request, config('paypal.live.checkout_notify_web_hook_id'));
if (!$response) {
return 'fail';
}
// 处理回调数据
$data = json_decode($response->request_body, true);
if ($data['event_type'] == 'PAYMENT.SALE.COMPLETED') {
// TODO: 这里编写支付完成后的业务
return 'success';
}
return 'fail';
}
}
10. 创建路由
php
// routes/web.php
// PayPal-Checkout
Route::get('payment/{order}/paypal', 'PaymentsController@payByPayPalCheckout')->name('payment.paypal_checkout');
// PayPal-Checkout-Return
Route::get('payment/paypal/return', 'PaymentController@payPalReturn')->name('payment.paypal.return');
// PayPal-Checkout-Notify
Route::post('payment/paypal/notify', 'PaymentController@payPalNotify')->name('payment.paypal.notify');
注意,由于异步回调是 POST 请求,需将其路由添加到 CSRF 白名单中,以便 PayPal 访问。
php
// app/Http/Middleware/VerifyCsrfToken.php
protected $except = [
'payment/paypal/notify',
];
11. 设置 PayPal Webhook Event
访问 PayPal开发者中心 进行配置。
以沙箱环境为例,创建 Webhook 的回调地址为 https://yoursite.com/payment/paypal/notify
,并勾选 Payments payment created
和 Payment sale completed
事件。
PayPal 的回调地址只支持 HTTPS 协议。
12. 测试 Checkout 支付
复制链接在浏览器中访问,登录后进行支付(沙箱环境可能会比较慢)。
注意:在开发者中心的沙箱环境中可以一键创建测试账号。
13. 请求头与验签
php
// app/Http/Controllers/PaymentController.php
public function payPalNotify(Request $request)
{
$response = app('paypal')->verify($request, config('paypal.sandbox.checkout_notify_web_hook_id'));
dd($response); // 打印验签结果
}
二、Subscription 订阅支付
下载并查看官方 DEMO 来获取更多详细信息。
创建计划并激活计划:
php
// app/Services/PayPalService.php
public function createPlan(Order $order)
{
try {
// 创建并激活计划的逻辑
// ...
return $result;
} catch (HttpException $e) {
Log::error('PayPal Create Plan Failed', ['msg' => $e->getMessage()]);
return false;
}
}
其他代码类似 Checkout 部分,具体方法如创建订阅申请、执行订阅以及控制器逻辑。此外,确保在路由中添加相应的处理映射。
至此,PayPal 的 Checkout 和 Subscription 支付流已经完成,希望能对您的开发工作有所帮助。如有不当之处请多多指正。