zoukankan      html  css  js  c++  java
  • PayPal 支付Checkout 收银台和 Subscription 订阅计划全过程分享

    废话不多说,我们先从请求的生命周期来分析,逐步实现整个过程.

    一。生命周期
    1. Checkout - 收银台支付
    拆解流程如图所示 (过程类似支付宝的收银台):


    流程详解:

    1. 本地应用组装好参数并请求 Checkout 接口,接口同步返回一个支付 URL;
    2. 本地应用重定向至这个 URL, 登陆 PayPal 账户并确认支付,用户支付后跳转至设置好的本地应用地址;
    3. 本地请求 PayPal 执行付款接口发起扣款;
    4. PayPal 发送异步通知至本地应用,本地拿到数据包后进行验签操作;
    5. 验签成功则进行支付完成后的业务 (修改本地订单状态、增加销量、发送邮件等).

    2. Subscription - 订阅支付

    拆解流程:

    流程详解:

    1. 创建一个计划;
    2. 激活该计划;
    3. 用已经激活的计划去创建一个订阅申请;
    4. 本地跳转至订阅申请链接获取用户授权并完成第一期付款,用户支付后携带 token 跳转至设置好的本地应用地址;
    5. 回跳后请求执行订阅;
    6. 收到订阅授权异步回调结果,收到支付结果的异步回调,验证支付异步回调成功则进行支付完成后的业务.

    二。具体实现


    了解了以上流程,接下来开始 Coding.

    github 上有很多 SDK, 这里使用的是官方的 SDK.

    Checkout
    在项目中安装扩展

    $ composer require paypal/rest-api-sdk-php:* // 这里使用的最新版本

    创建 paypal 配置文件

    $ touch config/paypal.php

    配置内容如下 (沙箱和生产两套配置):

    <?php
    
    return [
        /*
        |--------------------------------------------------------------------------
        | PayPal sandbox config
        |--------------------------------------------------------------------------
        |
        |
        */
    
        'sandbox' => [
            'client_id' => env('PAYPAL_SANDBOX_CLIENT_ID', ''),
            'secret' => env('PAYPAL_SANDBOX_SECRET', ''),
            'notify_web_hook_id' => env('PAYPAL_SANDBOX_NOTIFY_WEB_HOOK_ID', ''), // 全局回调的钩子id(可不填)
            'checkout_notify_web_hook_id' => env('PAYPAL_SANDBOX_CHECKOUT_NOTIFY_WEB_HOOK_ID', ''), // 收银台回调的钩子id
            'subscription_notify_web_hook_id' => env('PAYPAL_SANDBOX_SUBSCRIPTION_NOTIFY_WEB_HOOK_ID', ''), // 订阅回调的钩子id
        ],
    
        /*
        |--------------------------------------------------------------------------
        | PayPal live config
        |--------------------------------------------------------------------------
        |
        |
        */
    
        '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', ''),
        ],
    
    ];


    创建一个 PayPal 服务类

    $ mkdir -p app/Services && touch app/Services/PayPalService.php

    编写 Checkout 的方法
    可以参考官方给的 DEMO

    <?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
    {
        /*
         * array
         */
        protected $config;
    
        /*
         * string
         */
        protected $notifyWebHookId;
    
        /*
         * obj ApiContext
         */
        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', // PLEASE USE `INFO` LEVEL FOR LOGGING IN LIVE ENVIRONMENTS
                '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;
            }
        }


    将 PayPal 服务类注册在容器中
    打开文件 app/Providers/AppServiceProvider.php

    <?php
      namespace App\Providers;
      .
      .
      .
      use App\Services\PayPalService;
    
      class AppServiceProvider extends ServiceProvider
      {
        public function register()
        {
           .
           .
           .
    
            // 注册PayPalService开始
            $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);
            });
          // 注册PayPalService结束
        }


    创建控制器
    由于订单系统要视具体业务需求,在这里就不赘述了。下面直接根据订单去直接请求 checkout 支付

    $ php artisan make:controller PaymentsController
    <?php
    
    namespace App\Http\Controllers;
    
    use App\Models\Order;
    
    class PaymentController 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]);
        }
    }


    支付完的回跳方法
    app/Http/Controllers/PaymentController.php

    <?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: 这里编写失败后的业务
    
          }
      }
    }


    验签方法
    在 PayPalService 中加入验签方法 app/Services/PayPalService.php

    <?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(), 'data' => ['request' => ['header' => $headers, 'body' => $content]]]);
                return false;
            }
        }
    
    }


    异步回调
    app/Http/Controllers/PaymentController.php

    <?php
    .
    .
    .
    use Illuminate\Http\Request;
    use Illuminate\Support\Arr;
    
    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);
          $eventType = Arr::get($data, 'event_type');
          $resourceState = Arr::get($data, 'resource.state');
    
          // 验证回调事件类型和状态
          if ($eventType == 'PAYMENT.SALE.COMPLETED' && strcasecmp($resourceState, 'completed') == 0) {
                $paymentId = Arr::get($data, 'resource.parent_payment');
                if (!$paymentId) {
                    return 'fail';
                }
                // 订单
                $payment = app('paypal')->getPayment($paymentId);
    
                // 包中会有买家的信息
                $payerInfo = $payment->getPayer()->getPayerInfo();
    
                // TODO: 这里写具体的支付完成后的流程(如: 更新订单的付款时间、状态 & 增加商品销量 & 发送邮件业务 等)
                .
                .
                .
    
                return 'success';
          }
            return 'fail';
        }
    }


    创建路由
    route/web.php

    <?php
      .
      .
      .
      // PayPal-Checkout
      Route::get('payment/{order}/paypal', 'PaymentController@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 请求,因为 Laravel 的 CSRF 机制,所以我们需要在相应的中间件中将其路由加入到白名单中才能被 PayPal 访问.

    app/Http/MiddlewareVerifyCsrfToken.php

    <?php
    
    namespace App\Http\Middleware;
    
    use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
    
    class VerifyCsrfToken extends Middleware
    {
       .
       .
       .
    
        /**
         * The URIs that should be excluded from CSRF verification.
         *
         * @var array
         */
        protected $except = [
            // PayPal-Checkout-Notify
            'payment/paypal/notify',
        ];
    }


    设置 PayPal-WebHookEvent
    打开 PayPal 开发者中心进行配置

    以沙箱环境为例,生产一样


    没有账号的新建一个,如果有就点进去,拉至最下面,点击 Add Webhook 创建一个事件,输入回调地址 https://yoursite.com/payment/paypal/notify, 把 Payments payment created 和 Payment sale completed 勾选,然后确认即可.

    PayPal 的回调地址只支持 HTTPS 协议,可以参考下 Nginx 官方给的配置 HTTPS 方法 , 耐心照着步骤一步一步来很好配,这里不做赘述.

    PayPal 提供的事件类型有很多,PayPal-Checkout 只用到了 Payments payment created 和 Payment sale completed.

    配置完记得将 Webhook ID 添加到我们项目的配置中!

    测试 Checkout 支付


    复制链接浏览器访问

    登陆后进行支付. (这里不得不吐槽,沙箱环境真的真的真的很慢很慢很慢...)

    在开发者中心的沙箱环境中可以一键创建测试账号 (支付用个人账号), 这里就不做演示了.

    从线上的日志中拿到数据包进行本地测试

    请求头:

    在控制器中先打印验签结果 app/Http/Controllers/PaymentController.php

    <?php
    
    namespace App\Http\Controllers;
    
    use App\Events\OrderPaid;
    use App\Models\Order;
    use App\Models\User;
    use Carbon\Carbon;
    use Illuminate\Http\Request;
    use Illuminate\Support\Arr;
    use Illuminate\Support\Facades\Log;
    
    class PaymentController extends Controller
    {
        .
        .
        .
    
        public function payPalNotify(Request $request)
        {
            $response = app('paypal')->verify($request, config('paypal.sandbox.checkout_notify_web_hook_id'));
            dd($response);
                .
            .
            .
        }
    
    }


    打印结果如下,接下来就可以编写支付成功后的业务代码了.

    至此,Checkout 流程就结束了.

    Subscription
    创建计划并激活计划
    以下方法均参考官方 DEMO

    app/Services/PayPalService.php

    <?php
    namespace App\Services;
    
    .
    .
    .
    use PayPal\Api\Plan;
    use PayPal\Api\PaymentDefinition;
    use PayPal\Api\ChargeModel;
    use PayPal\Api\MerchantPreferences;
    use PayPal\Api\Patch;
    use PayPal\Common\PayPalModel;
    use PayPal\Api\PatchRequest;
    use PayPal\Api\Agreement;
    
    class PayPalService
    {
        .
        .
        .
    
        /**
         * @des 创建计划并激活计划
         * @author Mars
         * @param Order $order
         * @return Plan|false
         */
        public function createPlan(Order $order)
        {
            try {
                $plan = new Plan();
                $plan->setName($order->no)
                    ->setDescription($order->product->title)
                    ->setType('INFINITE'); // 可选(FIXED | INFINITE)
    
                $paymentDefinition = new PaymentDefinition();
    
                $paymentDefinition->setName('Regular Payments')
                    ->setType('REGULAR')
                    ->setFrequency('MONTH') // 设置频率, 可选(DAY | WEEK | MONTH | YEAR)
                    // ->setFrequency('DAY')
                    ->setFrequencyInterval($order->product->effective_months) // 设置频率间隔
                    ->setCycles(0) // 设置周期(如果Plan的Type为FIXED的, 对应这里填99表示无限期. 或Plan的Type为INFINITE, 这里设置0)
                    ->setAmount(new Currency([
                        'value' => $order->product->price, // 价格
                        'currency' => $order->product->currency // 币种
                    ]));
    
                // Charge Models 这里可设置税和运费等
                $chargeModel = new ChargeModel();
                $chargeModel->setType('TAX')
                    // ->setType('SHIPPING')
                    ->setAmount(new Currency([
                        'value' => $order->product->tax ?? 0,
                        'currency' => $order->product->currency
                    ]));
    
                $paymentDefinition->setChargeModels([$chargeModel]);
    
                $merchantPreferences = new MerchantPreferences();
                            // 这里设置支付成功和失败的回跳URL
                $merchantPreferences->setReturnUrl(route('subscriptions.paypal.return', ['success' => 'true', 'no' => $order->no]))
                    ->setCancelUrl(route('subscriptions.paypal.return', ['success' => 'false', 'no' => $order->no]))
                    ->setAutoBillAmount("yes")
                    ->setInitialFailAmountAction("CONTINUE")
                    ->setMaxFailAttempts("0")
                    ->setSetupFee(new Currency([
                        'value' => $order->product->price, // 设置第一次订阅扣款金额***, 默认0表示不扣款
                        'currency' => $order->product->currency // 币种
                    ]));
    
                $plan->setPaymentDefinitions([$paymentDefinition]);
                $plan->setMerchantPreferences($merchantPreferences);
    
                $output = $plan->create($this->apiContext);
    
                // 激活计划
                $patch = new Patch();
    
                $value = new PayPalModel('{"state":"ACTIVE"}');
    
                $patch->setOp('replace')
                    ->setPath('/')
                    ->setValue($value);
                $patchRequest = new PatchRequest();
                $patchRequest->addPatch($patch);
    
                $output->update($patchRequest, $this->apiContext);
    
                $result = Plan::get($output->getId(), $this->apiContext);
                if (!$result) {
                    throw new HttpException(500, 'PayPal Interval Error.');
                }
    
                return $result;
            } catch (HttpException $e) {
                Log::error('PayPal Create Plan Failed', ['msg' => $e->getMessage(), 'code' => $e->getStatusCode(), 'data' => ['order' => ['no' => $order->no]]]);
    
                return false;
            }
        }


    创建订阅申请
    接上面的代码 ↑

    
       .
       .
       .
    
       /**
         * @des 创建订阅申请
         * @author Mars
         * @param Plan $param
         * @param Order $order
         * @return string|null
         */
        public function createAgreement(Plan $param, Order $order)
        {
            try {
    
                $agreement = new Agreement();
    
                $agreement->setName($param->getName())
                    ->setDescription($param->getDescription())
                    ->setStartDate(Carbon::now()->addMonths($order->product->effective_months)->toIso8601String()); // 设置下次扣款的时间, 测试的时候可以用下面的 ↓, 第二天扣款
                    // ->setStartDate(Carbon::now()->addDays(1)->toIso8601String());
    
                $plan = new Plan();
                $plan->setId($param->getId());
                $agreement->setPlan($plan);
    
                $payer = new Payer();
                $payer->setPaymentMethod('paypal');
                $agreement->setPayer($payer);
    
                // $request = clone $agreement;
    
                // Please note that as the agreement has not yet activated, we wont be receiving the ID just yet.
                $agreement = $agreement->create($this->apiContext);
    
                // ### Get redirect url
                // The API response provides the url that you must redirect
                // the buyer to. Retrieve the url from the $agreement->getApprovalLink()
                // method
                $approvalUrl = $agreement->getApprovalLink();
    
                // 跳转到 $approvalUrl 等待用户同意
                return $approvalUrl;
            } catch (HttpException $e) {
                Log::error('PayPal Create Agreement Failed', ['msg' => $e->getMessage(), 'code' => $e->getStatusCode(), 'data' => ['plan' => $param]]);
                return null;
            }
        }


    执行订阅
    接上面 ↑

       .
       .
       .
    
       /**
         * @Des 执行订阅
         * @Date 2019-10-30
         * @Author Mars
         * @param $token
         * @return Agreement|bool
         */
        public function executeAgreement($token)
        {
            try {
                $agreement = new Agreement();
    
                $agreement->execute($token, $this->apiContext);
    
                return $agreement;
            } catch (HttpException $e) {
                Log::error('PayPal Execute Agreement Failed', ['msg' => $e->getMessage(), 'code' => $e->getStatusCode(), 'data' => ['token' => $token]]);
                return false;
            }
        }


    控制器调用
    这里为了跟 Checkout 区别开来,我们新建一个专门负责订阅的控制器

    $ php artisan make:controller SubscriptionsController
    <?php
    
    namespace App\Http\Controllers;
    
    use App\Models\Order;
    
    class SubscriptionsController extends Controller
    {
       /**
         * @Des PayPal-CreatePlan
         * @Author Mars
         * @param Order $order
         */
        public function createPlan(Order $order)
        {
            if ($order->paid_at || $order->closed) {
                return json_encode(['code' => 422, 'msg' => 'Order Status Error.', 'url' => '']);
            }
    
            // 创建计划并升级计划
            $plan = app('paypal')->createPlan($order);
            if (!$plan) {
                return json_encode(['code' => 500, 'msg' => 'Create Plan Failed.', 'url' => ''])
            }
    
            // 创建订阅申请
            $approvalUrl = app('paypal')->createAgreement($plan, $order);
            if (!$approvalUrl) {
                return json_encode(['code' => 500, 'msg' => 'Create Agreement Failed.', 'url' => '']);
            }
            // 跳转至PayPal授权订阅申请的链接
            return json_encode(['code' => 201, 'msg' => 'success.', 'url' => $approvalUrl]);
        }
    
    }


    支付完的回跳方法
    app/Http/Controllers/SubscriptionsController.php

    <?php
    
    namespace App\Http\Controllers;
    
    .
    .
    .
    use Carbon\Carbon;
    use Illuminate\Http\Request;
    use Symfony\Component\HttpKernel\Exception\HttpException;
    
    class SubscriptionsController extends Controller
    {  
        .
        .
        .
    
       /**
         * @Des 执行订阅
         * @Author Mars
         * @param Request $request
         * @return void|\Illuminate\View\View
         */
        public function executeAgreement(Request $request)
        {
            if ($request->has('success') && $request->success == 'true') {
                $token = $request->token;
                try {
                    // 执行订阅
                    // PayPal\Api\Agreement
                    $agreement = app('paypal')->executeAgreement($token);
    
                    if (!$agreement) {
                        throw new HttpException(500, 'Execute Agreement Failed');
                    }
                    // TODO: 这里写支付后的业务, 比如跳转至订单详情页或订阅成功页等
                    .
                    .
                    .
    
                    // 这里举例
                    $order = Order::where('no', $request->no)->first();
                    return view('orders.show', $order);
                } catch (HttpException $e) {
                    return abort($e->getStatusCode(), $e->getMessage());
                }
            }
                return abort(401, '非法请求');
        }


    异步回调
    订阅过程中的回调事件共有四种,分别是 Billing plan created、Billing plan updated、 Billing subscription created、 Billing subscription updated 和 Payment sale completed, 而我们更新本地订单的业务只需要用到最后一个 (Payment sale completed) 即可,其他的视具体业务而定,所以我们在创建 WebHookEvent 的时候需要跟其他回调业务区分开来.

    app/Http/Controllers/SubscriptionsController.php

    <?php
    
    namespace App\Http\Controllers;
    
    .
    .
    .
    use App\Models\User;
    use Illuminate\Support\Arr;
    use Illuminate\Support\Facades\Log;
    
    class SubscriptionsController extends Controller
    {
        .
        .
        .
    
        /**
         * @Des 订阅的异步回调处理
         * @Author Mars
         * @param Request $request
         * @return string
         */
        public function payPalNotify(Request $request)
        {
            Log::info('PayPal Subscription Notification', ['request' => ['header' => $request->header(), 'body' => $request->getContent()]]);
    
            $response = app('paypal')->verify($request, config('paypal.sanbox.subscription_notify_web_hook_id'));
    
            if (!$response) {
                return 'fail';
            }
    
            $requestBody = json_decode($response->request_body, true);
    
            $eventType = Arr::get($requestBody, 'event_type');
            $resourceState = Arr::get($requestBody, 'resource.state');
    
            if ($eventType == 'PAYMENT.SALE.COMPLETED' && strcasecmp($resourceState, 'completed') == 0) {
                $billingAgreementId = Arr::get($requestBody, 'resource.billing_agreement_id');
                $billingAgreement = app('paypal')->getBillingAgreement($billingAgreementId);
                if (!$billingAgreement) {
                    return 'fail';
                }
    
                // 获取买家信息
                $payerInfo = $billingAgreement->getPayer()->getPayerInfo();
                // 买家地址
                $shippingAddress = $billingAgreement->getShippingAddress();
    
                // 收录买家信息到用户表
                $email = $payerInfo->getEmail();
                $user = User::where('email', $email)->first();
                if (!$user) {
                    $user = User::create([
                        'email' => $email,
                        'name' => $payerInfo->getLastName() . ' ' . $payerInfo->getFirstName(),
                        'password' => bcrypt($payerInfo->getPayerId())
                    ]);
                }
    
                // 获取订单号(因为我在创建计划的时候把本地订单号追加到了description属性里, 大家可以视情况而定)
                $description = $billingAgreement->getDescription();
                $tmp = explode(' - ', $description);
                $orderNo = array_pop($tmp);
                $order = Order::where('no', $orderNo)->first();
    
                if (!$order) {
                    return 'fail';
                }
    
                // 订阅续费订单(如果查到的本地订单已经付过了且包中的'完成周期数`不是0, 则说明是续费订单, 本地可以新建一个订单标记是续费的. 这部分仅供参考, 具体视大家的业务而定)
                if ($order->paid_at && $billingAgreement->getAgreementDetails()->getCyclesCompleted() != 0) {
                    // 产品
                    $sku = $order->product;
    
                    // 新建一个本地订单
                    $order = new Order([
                        'address' => $shippingAddress->toArray(),
                        'paid_at' => Carbon::now(),
                        'payment_method' => 'paypal-subscription',
                        'payment_no' => $billingAgreementId,
                        'total_amount' => $billingAgreement->getAgreementDetails()
                            ->getLastPaymentAmount()
                            ->getValue(),
                        'remark' => '订阅续费订单 - ' . $billingAgreement->getAgreementDetails()->getCyclesCompleted() . '期',
                    ]);
                    // 订单关联到当前用户
                    $order->user()->associate($user);
                    $order->save();
                } else {
                    // 首次付款
                    $order->update([
                        'paid_at' => Carbon::now(),
                        'payment_method' => 'paypal-subscription',
                        'payment_no' => $billingAgreementId,
                        'user_id' => $user->id,
                        'address' => $shippingAddress->toArray(),
                    ]);
    
                    // TODO: 增加销量、发送邮件等业务
                    .
                    .
                    .
                }
                return 'success';
            }
            return 'fail';
        }
    }


    创建路由
    上面的方法中一共需要三个路由,分别是 ' 创建计划 '、' 执行订阅 '、' 订阅付款异步回调'

    routes\web.php

    <?php
    .
    .
    .
    // PayPal-Subscription-CreatePlan
    Route::get('subscriptions/{order}/paypal/plan', 'SubscriptionsController@createPlan')
        ->name('subscriptions.paypal.createPlan');
    // PayPal-Subscription-Return
    Route::get('subscriptions/paypal/return', 'SubscriptionsController@execute')
        ->name('subscriptions.paypal.return');
    // PayPal-Subscription-Notify
    Route::post('subscriptions/paypal/notify', 'SubscriptionsController@payPalNotify')
        ->name('subscriptions.paypal.notify');


    同样的,不要忘记把异步回调路由加入到白名单中

    app/Http/MiddlewareVerifyCsrfToken.php

    <?php
    
    namespace App\Http\Middleware;
    
    use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
    
    class VerifyCsrfToken extends Middleware
    {
       .
       .
       .
    
        /**
         * The URIs that should be excluded from CSRF verification.
         *
         * @var array
         */
        protected $except = [
            .
            .
            .
            // PayPal-Subscription-Notify
            'subscriptions/paypal/notify',
        ];
    }


    设置 PayPal-WebHookEvent
    同上面提到的设置方法,我们这里只将 Payment sale completed 事件勾选即可,具体过程这里不再赘述.

    测试 Subscription


    复制链接到浏览器打开,登陆后如下

    订阅完成.

    本地测试异步回调

    订阅的header和body

    {"header":{"Content-Type":"application\/json","Cf-Request-Id":"08b2795df100003b0436840000000001","Cdn-Loop":"cloudflare","Cf-Connecting-Ip":"173.0.82.126","Correlation-Id":"ee6f93394eb95","User-Agent":"PayPal\/AUHD-214.0-55417034","Paypal-Auth-Algo":"SHA256withRSA","Paypal-Cert-Url":"https:\/\/api.sandbox.paypal.com\/v1\/notifications\/certs\/CERT-360caa42-fca2a594-1d93a270","Paypal-Auth-Version":"v2","Paypal-Transmission-Sig":"G\/3Wixb0SvkQhe116lGpgGxK2OiPoc7McEopYjPYw\/pcPRQTErscZSMHzkDB9GU4kmMtMwBKFM6iT4uI9U4TJ2EBEsBgoeg8dAY1cka\/YQS76olqE2iYe4nXGp4la+Vo\/jLYJXTanIbEtLqGlXmmPWBkK7a4+2wpI\/8Aeg91PIW\/2ZbTdpVW3DSx64868DPfWPI9aitCSJp1OHxLL2a+6M\/kC4be1IVT3+tYOKXXNZ9WqXNdc3ArTuMff+KpSVO38\/atAvw3mQAiivezNBXMpzL+vTbjesNMEngn1m+6z\/y3GwUAYf4NLz5OKNpab7Ysr9B2yvNWl5HXraLB5b2j5Q==","Paypal-Transmission-Time":"2021-03-08T08:07:03Z","Paypal-Transmission-Id":"446f1120-7fe5-11eb-947c-f9005c1a6ad2","Accept":"*\/*","Cf-Visitor":"{\"scheme\":\"https\"}","X-Forwarded-Proto":"https","Content-Length":"1229","Cf-Ray":"62ca91a98f6f3b04-SJC","X-Forwarded-For":"173.0.82.126","Cf-Ipcountry":"US","Accept-Encoding":"gzip","Connection":"Keep-Alive","Host":"api.geekpandashare.com"},"body":"{\"id\":\"WH-77A20588F8391304V-0YJ27391M4465813T\",\"event_version\":\"1.0\",\"create_time\":\"2021-03-08T08:06:33.726Z\",\"resource_type\":\"sale\",\"event_type\":\"PAYMENT.SALE.COMPLETED\",\"summary\":\"Payment completed for $ 39.99 USD\",\"resource\":{\"billing_agreement_id\":\"I-N54WLLUG3DW6\",\"amount\":{\"total\":\"39.99\",\"currency\":\"USD\",\"details\":{\"subtotal\":\"39.99\"}},\"payment_mode\":\"INSTANT_TRANSFER\",\"update_time\":\"2021-03-08T08:06:13Z\",\"create_time\":\"2021-03-08T08:06:13Z\",\"protection_eligibility_type\":\"ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE\",\"transaction_fee\":{\"currency\":\"USD\",\"value\":\"1.46\"},\"protection_eligibility\":\"ELIGIBLE\",\"links\":[{\"method\":\"GET\",\"rel\":\"self\",\"href\":\"https:\/\/api.sandbox.paypal.com\/v1\/payments\/sale\/6Y505934MD224870E\"},{\"method\":\"POST\",\"rel\":\"refund\",\"href\":\"https:\/\/api.sandbox.paypal.com\/v1\/payments\/sale\/6Y505934MD224870E\/refund\"}],\"id\":\"6Y505934MD224870E\",\"state\":\"completed\",\"invoice_number\":\"\"},\"links\":[{\"href\":\"https:\/\/api.sandbox.paypal.com\/v1\/notifications\/webhooks-events\/WH-77A20588F8391304V-0YJ27391M4465813T\",\"rel\":\"self\",\"method\":\"GET\"},{\"href\":\"https:\/\/api.sandbox.paypal.com\/v1\/notifications\/webhooks-events\/WH-77A20588F8391304V-0YJ27391M4465813T\/resend\",\"rel\":\"resend\",\"method\":\"POST\"}]}"}

    异步回调相应数据:

    {"auth_algo": "SHA256withRSA","transmission_id": "446f1120-7fe5-11eb-947c-f9005c1a6ad2","cert_url": "https://api.sandbox.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-1d93a270","webhook_id": "82W63144WX618073T","transmission_sig": "G/3Wixb0SvkQhe116lGpgGxK2OiPoc7McEopYjPYw/pcPRQTErscZSMHzkDB9GU4kmMtMwBKFM6iT4uI9U4TJ2EBEsBgoeg8dAY1cka/YQS76olqE2iYe4nXGp4la+Vo/jLYJXTanIbEtLqGlXmmPWBkK7a4+2wpI/8Aeg91PIW/2ZbTdpVW3DSx64868DPfWPI9aitCSJp1OHxLL2a+6M/kC4be1IVT3+tYOKXXNZ9WqXNdc3ArTuMff+KpSVO38/atAvw3mQAiivezNBXMpzL+vTbjesNMEngn1m+6z/y3GwUAYf4NLz5OKNpab7Ysr9B2yvNWl5HXraLB5b2j5Q==","transmission_time": "2021-03-08T08:07:03Z","webhook_event": {"id":"WH-77A20588F8391304V-0YJ27391M4465813T","event_version":"1.0","create_time":"2021-03-08T08:06:33.726Z","resource_type":"sale","event_type":"PAYMENT.SALE.COMPLETED","summary":"Payment completed for $ 39.99 USD","resource":{"billing_agreement_id":"I-N54WLLUG3DW6","amount":{"total":"39.99","currency":"USD","details":{"subtotal":"39.99"}},"payment_mode":"INSTANT_TRANSFER","update_time":"2021-03-08T08:06:13Z","create_time":"2021-03-08T08:06:13Z","protection_eligibility_type":"ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE","transaction_fee":{"currency":"USD","value":"1.46"},"protection_eligibility":"ELIGIBLE","links":[{"method":"GET","rel":"self","href":"https://api.sandbox.paypal.com/v1/payments/sale/6Y505934MD224870E"},{"method":"POST","rel":"refund","href":"https://api.sandbox.paypal.com/v1/payments/sale/6Y505934MD224870E/refund"}],"id":"6Y505934MD224870E","state":"completed","invoice_number":""},"links":[{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-77A20588F8391304V-0YJ27391M4465813T","rel":"self","method":"GET"},{"href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-77A20588F8391304V-0YJ27391M4465813T/resend","rel":"resend","method":"POST"}]}}


    同上面提到的,这里不再赘述.

    至此,两种支付的整个过程就算完结啦。

    异步支付成功回调

    {
        "auth_algo":"SHA256withRSA",
        "transmission_id":"a3302ab0-80de-11eb-aacd-47b3747d966f",
        "cert_url":"https://api.sandbox.paypal.com/v1/notifications/certs/CERT-360caa42-fca2a594-1d93a270",
        "webhook_id":"82W63144WX618073T",
        "transmission_sig":"gDxFglF2VBz0OycfdbyeGcBInu0vgiroBAVBOD5uD4SymiX4cs++9g4jXWMLiRxbnOxLnRSY/al/HVpZyJhzjco/ot0xj6RuPs8DbVwUBfOjiDPlMqpon8p2YX+nHbwXVNy3t0gsZ8boOmcQRwBBVEbDhW8Qtuyjv0c+M7xub03V4sKJXWHJYTduuNHkcfktVRvxopDFbEyFym0eQea6erPA2FuILCkfEPMkiSzOs54K2yS7Ao32u6ybiy1rbdXwnvMlc2P4C95slldyQgZYO1uV4yrnawfxNT33pzB5LFNSASGHYwXlu1T1xTMq/jpenhZYSiL3HDcIEyY4NJkzYw==",
        "transmission_time":"2021-03-09T13:52:07Z",
        "webhook_event":{
            "id":"WH-9GC90780WS280374S-0J366698HK4127702",
            "event_version":"1.0",
            "create_time":"2021-03-09T13:52:04.050Z",
            "resource_type":"sale",
            "event_type":"PAYMENT.SALE.COMPLETED",
            "summary":"Payment completed for $ 39.99 USD",
            "resource":{
                "billing_agreement_id":"I-NP5WJSMVV814",
                "amount":{
                    "total":"39.99",
                    "currency":"USD",
                    "details":{
                        "subtotal":"39.99"
                    }
                },
                "payment_mode":"INSTANT_TRANSFER",
                "update_time":"2021-03-09T13:51:27Z",
                "create_time":"2021-03-09T13:51:27Z",
                "protection_eligibility_type":"ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE",
                "transaction_fee":{
                    "currency":"USD",
                    "value":"1.46"
                },
                "protection_eligibility":"ELIGIBLE",
                "links":[
                    {
                        "method":"GET",
                        "rel":"self",
                        "href":"https://api.sandbox.paypal.com/v1/payments/sale/61C034852Y123674K"
                    },
                    {
                        "method":"POST",
                        "rel":"refund",
                        "href":"https://api.sandbox.paypal.com/v1/payments/sale/61C034852Y123674K/refund"
                    }
                ],
                "id":"61C034852Y123674K",
                "state":"completed",
                "invoice_number":""
            },
            "links":[
                {
                    "href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-9GC90780WS280374S-0J366698HK4127702",
                    "rel":"self",
                    "method":"GET"
                },
                {
                    "href":"https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-9GC90780WS280374S-0J366698HK4127702/resend",
                    "rel":"resend",
                    "method":"POST"
                }
            ]
        }
    }

    赞赏码

    非学,无以致疑;非问,无以广识

  • 相关阅读:
    Codeforces Round #598 (Div. 3)(全)
    Codeforces Round #597 (Div. 2) F
    Codeforces Round #597 (Div. 2) D Shichikuji and Power Grid (MST)
    ACM bitset
    Codeforces Round #592 (Div. 2) E. Minimizing Difference
    Educational Codeforces Round 74 (Rated for Div. 2)D. AB-string
    Educational Codeforces Round 74 (Rated for Div. 2) tandard Free2play
    2018南昌邀请赛网络赛 单调栈 I
    rmq问题
    poj 3259Wormholes
  • 原文地址:https://www.cnblogs.com/lxwphp/p/15452603.html
Copyright © 2011-2022 走看看