Files
ECShopX/todo/paypal-server-sdk-migration.md
2025-12-29 22:14:41 +08:00

13 KiB
Raw Permalink Blame History

PayPal SDK 迁移指南

本文档详细说明了从旧版 PayPal REST API SDK (paypal/rest-api-sdk-php) 迁移到新版 PayPal Server SDK (paypal/paypal-server-sdk:1.1.0) 的步骤和变更。

变更概述

  1. SDK 包变更

    • 旧版:paypal/rest-api-sdk-php
    • 新版:paypal/paypal-server-sdk:1.1.0
  2. API 架构变更

    • 旧版:使用 ApiContext 和各种模型类直接调用 API
    • 新版:使用控制器模式,通过 OrdersControllerPaymentsController 等控制器调用 API
  3. 认证方式变更

    • 旧版:使用 OAuthTokenCredentialApiContext
    • 新版:使用 ClientCredentialsAuthCredentialsBuilderPaypalServerSdkClientBuilder
  4. Webhook 验证变更

    • 旧版:使用 VerifyWebhookSignature
    • 新版:不再直接支持 Webhook 验证,需要手动实现或使用 cURL 调用 API

迁移步骤

1. 安装新版 SDK

composer require paypal/paypal-server-sdk:1.1.0

2. 更新 PaypalManager 类

2.1 导入新的命名空间

use PaypalServerSdkLib\PaypalServerSdkClient;
use PaypalServerSdkLib\PaypalServerSdkClientBuilder;
use PaypalServerSdkLib\Authentication\ClientCredentialsAuthCredentialsBuilder;
use PaypalServerSdkLib\Environment;
use Psr\Log\LogLevel;
use PaypalServerSdkLib\Logging\LoggingConfigurationBuilder;
use PaypalServerSdkLib\Logging\RequestLoggingConfigurationBuilder;
use PaypalServerSdkLib\Logging\ResponseLoggingConfigurationBuilder;

2.2 更新客户端初始化

旧版:

public function getApiContext()
{
    if ($this->apiContext !== null) {
        return $this->apiContext;
    }

    $this->apiContext = new ApiContext(
        new OAuthTokenCredential(
            $this->clientId,
            $this->clientSecret
        )
    );

    $this->apiContext->setConfig([
        'mode' => $this->sandbox ? 'sandbox' : 'live',
        'log.LogEnabled' => true,
        'log.FileName' => storage_path('logs/paypal.log'),
        'log.LogLevel' => 'INFO'
    ]);

    return $this->apiContext;
}

新版:

public function getClient()
{
    if ($this->client !== null) {
        return $this->client;
    }

    // 创建客户端
    $this->client = PaypalServerSdkClientBuilder::init()
        ->clientCredentialsAuthCredentials(
            ClientCredentialsAuthCredentialsBuilder::init(
                $this->clientId,
                $this->clientSecret
            )
        )
        ->environment($this->sandbox ? Environment::SANDBOX : Environment::PRODUCTION)
        ->loggingConfiguration(
            LoggingConfigurationBuilder::init()
                ->level(LogLevel::INFO)
                ->requestConfiguration(RequestLoggingConfigurationBuilder::init()->body(true))
                ->responseConfiguration(ResponseLoggingConfigurationBuilder::init()->headers(true))
        )
        ->build();

    return $this->client;
}

2.3 更新 Webhook 验证

由于新版 SDK 不再直接支持 Webhook 验证,我们需要手动实现:

public function verifyWebhook($headers, $body, $webhookId)
{
    // 获取必要的头信息
    $authAlgo = $headers['PAYPAL-AUTH-ALGO'] ?? $headers['paypal-auth-algo'] ?? '';
    $certUrl = $headers['PAYPAL-CERT-URL'] ?? $headers['paypal-cert-url'] ?? '';
    $transmissionId = $headers['PAYPAL-TRANSMISSION-ID'] ?? $headers['paypal-transmission-id'] ?? '';
    $transmissionSig = $headers['PAYPAL-TRANSMISSION-SIG'] ?? $headers['paypal-transmission-sig'] ?? '';
    $transmissionTime = $headers['PAYPAL-TRANSMISSION-TIME'] ?? $headers['paypal-transmission-time'] ?? '';

    // 准备验证数据
    $verificationData = [
        'auth_algo' => $authAlgo,
        'cert_url' => $certUrl,
        'transmission_id' => $transmissionId,
        'transmission_sig' => $transmissionSig,
        'transmission_time' => $transmissionTime,
        'webhook_id' => $webhookId,
        'webhook_event' => json_decode($body, true)
    ];

    // 获取访问令牌
    $accessToken = $this->getAccessToken();
    if (!$accessToken) {
        app('log')->error('PayPal 获取访问令牌失败');
        return [
            'verified' => false,
            'error' => '获取访问令牌失败'
        ];
    }

    // 使用 cURL 调用 PayPal API 验证 Webhook
    $ch = curl_init();
    $apiUrl = $this->sandbox
        ? 'https://api-m.sandbox.paypal.com/v1/notifications/verify-webhook-signature'
        : 'https://api-m.paypal.com/v1/notifications/verify-webhook-signature';

    curl_setopt($ch, CURLOPT_URL, $apiUrl);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($verificationData));
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $accessToken
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode !== 200) {
        app('log')->error('PayPal Webhook验证失败: HTTP状态码 ' . $httpCode);
        return [
            'verified' => false,
            'error' => 'HTTP状态码 ' . $httpCode
        ];
    }

    $result = json_decode($response, true);
    $isVerified = isset($result['verification_status']) && $result['verification_status'] === 'SUCCESS';

    return [
        'verified' => $isVerified,
        'event' => json_decode($body, true),
        'response' => $result
    ];
}

/**
 * 获取 PayPal 访问令牌
 */
private function getAccessToken()
{
    $ch = curl_init();
    $apiUrl = $this->sandbox
        ? 'https://api-m.sandbox.paypal.com/v1/oauth2/token'
        : 'https://api-m.paypal.com/v1/oauth2/token';

    curl_setopt($ch, CURLOPT_URL, $apiUrl);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
    curl_setopt($ch, CURLOPT_USERPWD, $this->clientId . ':' . $this->clientSecret);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: application/x-www-form-urlencoded'
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode !== 200) {
        return null;
    }

    $result = json_decode($response, true);
    return $result['access_token'] ?? null;
}

2.4 更新支付相关方法

创建支付

旧版:

public function createPayment($paymentData)
{
    try {
        $payment = new \PayPal\Api\Payment();
        $payment->fromArray($paymentData);
        $payment->create($this->getApiContext());
        return $payment;
    } catch (\Exception $e) {
        app('log')->error('PayPal 创建支付失败: ' . $e->getMessage());
        throw $e;
    }
}

新版:

public function createPayment($paymentData)
{
    try {
        $client = $this->getClient();
        $ordersController = $client->getOrdersController();
        
        $collect = [
            'body' => $paymentData,
            'prefer' => 'return=representation'
        ];
        
        $response = $ordersController->createOrder($collect);
        
        if ($response->isSuccess()) {
            return $response->getResult();
        } else {
            app('log')->error('PayPal 创建支付失败: ' . json_encode($response->getBody()));
            throw new BadRequestHttpException('PayPal 创建支付失败');
        }
    } catch (\Exception $e) {
        app('log')->error('PayPal 创建支付失败: ' . $e->getMessage());
        throw $e;
    }
}
执行支付

旧版:

public function executePayment($paymentId, $payerId)
{
    try {
        $payment = \PayPal\Api\Payment::get($paymentId, $this->getApiContext());
        
        $execution = new \PayPal\Api\PaymentExecution();
        $execution->setPayerId($payerId);
        
        $result = $payment->execute($execution, $this->getApiContext());
        return $result;
    } catch (\Exception $e) {
        app('log')->error('PayPal 执行支付失败: ' . $e->getMessage());
        throw $e;
    }
}

新版:

public function executePayment($paymentId, $payerId)
{
    try {
        $client = $this->getClient();
        $ordersController = $client->getOrdersController();
        
        $collect = [
            'id' => $paymentId,
            'prefer' => 'return=representation'
        ];
        
        $response = $ordersController->captureOrder($collect);
        
        if ($response->isSuccess()) {
            return $response->getResult();
        } else {
            app('log')->error('PayPal 执行支付失败: ' . json_encode($response->getBody()));
            throw new BadRequestHttpException('PayPal 执行支付失败');
        }
    } catch (\Exception $e) {
        app('log')->error('PayPal 执行支付失败: ' . $e->getMessage());
        throw $e;
    }
}
退款

旧版:

public function refund($saleId, $amount, $currency = 'USD')
{
    try {
        $sale = new \PayPal\Api\Sale();
        $sale->setId($saleId);
        
        $refundRequest = new \PayPal\Api\RefundRequest();
        $amountObj = new \PayPal\Api\Amount();
        $amountObj->setCurrency($currency)
            ->setTotal($amount);
        
        $refundRequest->setAmount($amountObj);
        
        return $sale->refund($refundRequest, $this->getApiContext());
    } catch (\Exception $e) {
        app('log')->error('PayPal 退款失败: ' . $e->getMessage());
        throw $e;
    }
}

新版:

public function refund($captureId, $amount, $currency = 'USD')
{
    try {
        $client = $this->getClient();
        $paymentsController = $client->getPaymentsController();
        
        $collect = [
            'captureId' => $captureId,
            'prefer' => 'return=representation',
            'body' => [
                'amount' => [
                    'value' => $amount,
                    'currency_code' => $currency
                ]
            ]
        ];
        
        $response = $paymentsController->refundCapturedPayment($collect);
        
        if ($response->isSuccess()) {
            return $response->getResult();
        } else {
            app('log')->error('PayPal 退款失败: ' . json_encode($response->getBody()));
            throw new BadRequestHttpException('PayPal 退款失败');
        }
    } catch (\Exception $e) {
        app('log')->error('PayPal 退款失败: ' . $e->getMessage());
        throw $e;
    }
}

3. 更新 PaypalService 类

由于 PaypalService 类主要是通过 PaypalManager 类调用 PayPal API所以只需要确保 PaypalService 类中调用 PaypalManager 类的方法名称和参数正确即可。

4. 更新配置文件

确保 config/paypal.php 文件包含所有必要的配置项:

<?php

return [
    // PayPal API 凭证
    'client_id' => env('PAYPAL_CLIENT_ID', ''),
    'client_secret' => env('PAYPAL_SECRET', ''),
    
    // 环境设置
    'sandbox' => env('PAYPAL_SANDBOX', true),
    
    // Webhook 配置
    'webhook_id' => env('PAYPAL_WEBHOOK_ID', ''),
    
    // 货币设置
    'currency' => env('PAYPAL_CURRENCY', 'USD'),
    
    // 回调 URL
    'return_url' => env('PAYPAL_RETURN_URL', '/payment/paypal/success'),
    'cancel_url' => env('PAYPAL_CANCEL_URL', '/payment/paypal/cancel'),
    'webhook_url' => env('PAYPAL_WEBHOOK_URL', '/payment/paypal/webhook'),
    
    // 前端跳转 URL
    'success_url' => env('PAYPAL_SUCCESS_URL', '/payment/success'),
    'fail_url' => env('PAYPAL_FAIL_URL', '/payment/failed'),
    'cancelled_url' => env('PAYPAL_CANCELLED_URL', '/payment/cancelled'),
    
    // 日志设置
    'log_enabled' => env('PAYPAL_LOG_ENABLED', true),
    'log_level' => env('PAYPAL_LOG_LEVEL', 'INFO'),
    'log_file' => storage_path('logs/paypal.log'),
];

注意事项

  1. API 结构变化:新版 SDK 使用了不同的 API 结构,需要通过控制器访问 API。

  2. 支付流程变化

    • 旧版:创建 Payment -> 执行 Payment
    • 新版:创建 Order -> 捕获 Order
  3. 退款流程变化

    • 旧版:通过 Sale ID 退款
    • 新版:通过 Capture ID 退款
  4. Webhook 验证:新版 SDK 不再直接支持 Webhook 验证,需要手动实现。

  5. 错误处理:新版 SDK 使用 isSuccess() 方法检查响应是否成功,使用 getResult() 获取结果,使用 getBody() 获取原始响应。

  6. 参数格式:新版 SDK 的参数格式有所变化,需要使用 collect 数组传递参数。

测试步骤

  1. 确保已安装新版 SDKcomposer require paypal/paypal-server-sdk:1.1.0
  2. 更新 PaypalManager 类
  3. 测试基本功能:
    • 创建支付
    • 执行支付
    • 查询支付
    • 退款
    • Webhook 验证
  4. 检查日志,确保没有错误