Paypal 国际版REST接口的开发
最近在开发一个项目用到了国际板的paypal的支付功能,在网上找了很多资料,基本上都是老版本接口开发教程。按照教程开发了一套,但是同时也用到了退款接口,可是这个退款接口查阅了大量的国内外网站,也没找到相应的资料。无奈之下只能自己去研究官方文档,看看有没有什么收获。Paypal的官方开发者中心地址:https://developer.paypal.com ,发现现在Paypal主推的是REST方式接口。经过一番阅读,大致了解了这个接口的流程,于是我又找到了官方的SDK下载:https://developer.paypal.com/docs/classic/lifecycle/sdks/ ,通过这个SDK可以仔细的研究一下这个接口的开发。
与我们平时开发的国内的支付接口不同的是,Paypal REST接口是一个单向的请求接口,提交的时候,只有同步回调地址,没有异步的回调通知地址。这个问题当时令我很困惑,因为在我的常识当中,如果网络不稳定的话,这个单向请求同步回调是极为不妥的,有可能用户支付成功了,但是由于网络等原因可能无法回调回来,我们就无法判断他是否真的支付成功了。又是一番查找和阅读,发现和REST对应的所谓的异步通知是由另一种东西实现的,Paypal这里是webhooks,经过国外的一些搜索,这种“钩子”的模式,在国外还是很流行的。
那么,webhooks和通常意义上的notify url通知有什么不同呢。webhooks可以通过开发平台设置哪些接口可以使用webhooks来通知,而且它还可以主动创建和生成。这样一来在向支付接口发起请求的时候,可以不带着异步回调地址,也减少了一些异步检查不严格而产生的风险。当然这只是其中一部分的好处,至于更多的优势,鉴于我开发的这个功能比较简单,就是支付和退款。所以,也没多做深究。这里值得一提的是,webhook的地址必须是https开头的,必须要有SSL证书才行,所以,选用哪个接口还是需要根据时期的服务器环境来甄选。
说了这么多,就来看看这些接口是怎么开发的吧。
支付接口:
<?php /** * Paypal_REST支付接口 * 以下内容是从我项目中的类里摘出来的, * 为了方便阅读,我给过程化了。可以封装到一个类里。 */ require_once 'paypalrest/vendor/autoload.php'; // 引入SDK类库 $mode = 'sandbox'; //应用模式 /* * 这两个参数可以在开发者中心的控制台获取到, * 需要在控制台新建应用,然后会自动分配得到这两个参数 * 同时会生成两套,一套LIVE正式应用,另一套SandBox沙盒应用 */ $config = array( 'clientId' => '您的clientId', 'clientSecret' => '您的Secret', ); $apiContext = getApiContext($config['clientId'], $config['clientSecret']); $payment = array( 'subject' => '测试支付标题', 'total_fee' => '1.00', 'out_trade_no' => 'PAYPAL201602040001', ); echo getPayInfo($payment); /** * 获取ApiContext */ function getApiContext($clientId, $clientSecret){ $apiContext = new \PayPal\Rest\ApiContext( new \PayPal\Auth\OAuthTokenCredential( $clientId, $clientSecret ) ); $apiContext->setConfig( array( 'mode' => $mode, 'log.LogEnabled' => true, 'log.FileName' => dirname(__FILE__).'/paypal_log.txt', 'log.LogLevel' => 'DEBUG', // PLEASE USE `FINE` LEVEL FOR LOGGING IN LIVE ENVIRONMENTS 'cache.enabled' => true, // 'http.CURLOPT_CONNECTTIMEOUT' => 30 // 'http.headers.PayPal-Partner-Attribution-Id' => '123123123' ) ); return $apiContext; } /** * 获取支付URL * 以下是比较传统的获取支付链接的方式,还有信用卡支付等接口,大同小异。 */ function getPayInfo($payment){ $payer = new \PayPal\Api\Payer(); // 创建支付 $payer->setPaymentMethod('paypal'); // 设置支付方式 $item = new \PayPal\Api\Item(); // 创建支付项目 $item->setName($payment['subject']) // 设置项目名称、币种、数量、总价等参数,这些在开发文档里面都有描述。 ->setCurrency('USD') ->setQuantity(1) ->setPrice($payment['total_fee']); $itemList = new \PayPal\Api\ItemList(); // 创建项目列表 $itemList->setItems([$item]); // 设置把项目放到项目列表里面,此处可以传入多个项目 $details = new \PayPal\Api\Details(); // 创建明细 $details->setSubtotal($payment['total_fee']); // 把总价传入明细中 $amount = new \PayPal\Api\Amount(); // 创建总金额(支付的总金额以此为准) $amount->setCurrency('USD') // 设置币种、总金额、和价格明细等参数 ->setTotal($payment['total_fee']) ->setDetails($details); $transaction = new \PayPal\Api\Transaction(); // 创建交易 $transaction->setAmount($amount) // 传入总金额、项目列表、支付描述、订单(发票)号 ->setItemList($itemList) ->setDescription($payment['subject']) ->setInvoiceNumber($payment['out_trade_no']); $returnUrl = 'http://xxx.xxx.xxx/paypal_return.php'; // 同步回调地址 $redirectUrls = new \PayPal\Api\RedirectUrls(); // 创建回调对象 $redirectUrls->setReturnUrl($returnUrl) // 设置同步回调地址和取消回调地址 ->setCancelUrl($cancelUrl); $payment = new \PayPal\Api\Payment(); // 创建支付 $payment->setIntent('sale') // 传入接口方式,这里填写的是sale,还有其它接口,例如信用卡支付接口等 ->setPayer($payer) // 传入payer对象、回调、交易等 ->setRedirectUrls($redirectUrls) ->setTransactions([$transaction]); try { $payment->create($this->apiContext); } catch (Exception $e) { print_r($e->getMessage()); print_r(CJSON::decode($e->getData())); die(); } $approvalUrl = $payment->getApprovalLink(); return $approvalUrl; }
以上内容仅作说明和讲解,并未对代码的错误进行验证,但是已经能说明问题。
paypal_return.php同步回调页面
<?php /** * Paypal_REST支付接口 * paypal_return.php * 以下内容是从我项目中的类里摘出来的,(并不对安全做过多的处理) * 为了方便阅读,我给过程化了。可以封装到一个类里。 */ require_once 'paypalrest/vendor/autoload.php'; // 引入SDK类库 $mode = 'sandbox'; //应用模式 /* * 这两个参数可以在开发者中心的控制台获取到, * 需要在控制台新建应用,然后会自动分配得到这两个参数 * 同时会生成两套,一套LIVE正式应用,另一套SandBox沙盒应用 */ $config = array( 'clientId' => '您的clientId', 'clientSecret' => '您的Secret', ); $apiContext = getApiContext($config['clientId'], $config['clientSecret']); if($payInfo = return_check()){ // 支付成功做逻辑操作,最好把saleId存入你的数据库以便后期其它接口使用。 // .... exit('支付成功'); } else { exit('支付失败'); } function return_check(){ $payStatus = $_POST['pay_status']; $apiPaymentId = $_POST['paymentId']; $payerID = $_POST['PayerID']; if((!$payStatus) || (!$apiPaymentId) || (!$payerID)){ return false; } if($payStatus === 'fail'){ return false; } $payment = \PayPal\Api\Payment::get($apiPaymentId, $this->apiContext); $execute = new \PayPal\Api\PaymentExecution(); $execute->setPayerId($payerID); try{ $payment->execute($execute, $this->apiContext); /* * 实际上到这里就可以判断是否支付成功了。 * 但是Paypal并没有给我们返回saleId,而这个saleId是我们后续退款接口用到的必须参数 */ $transactions = $payment->getTransactions(); $relatedResources = $transactions[0]->getRelatedResources(); $sale = $relatedResources[0]->getSale(); $saleId = $sale->getId(); // 获取到saleId $result = array(); $result['sale_id'] = $saleId; return $result; }catch(Exception $e){ return false; } }
接下来就是刚刚一直都在提到的webhook的开发,其实和异步通知区别不大,代码如下:
<?php /** * Paypal_REST支付接口 * paypal_webhook.php * 以下内容是从我项目中的类里摘出来的,(并不对安全做过多的处理) * 为了方便阅读,我给过程化了。可以封装到一个类里。 */ require_once 'paypalrest/vendor/autoload.php'; // 引入SDK类库 $mode = 'sandbox'; //应用模式 /* * 这两个参数可以在开发者中心的控制台获取到, * 需要在控制台新建应用,然后会自动分配得到这两个参数 * 同时会生成两套,一套LIVE正式应用,另一套SandBox沙盒应用 */ $config = array( 'clientId' => '您的clientId', 'clientSecret' => '您的Secret', ); $apiContext = getApiContext($config['clientId'], $config['clientSecret']); if(webhook()){ // 支付完成做业务逻辑处理 // ... } function webhook(){ $bodyReceived = file_get_contents('php://input'); // 获取通知的全部内容 $output = ''; try { $output = \PayPal\Api\WebhookEvent::validateAndGetReceivedEvent($bodyReceived, $this->apiContext); } catch (\InvalidArgumentException $ex) { // This catch is based on the bug fix required for proper validation for PHP. Please read the note below for more details. // If you receive an InvalidArgumentException, please return back with HTTP 503, to resend the webhooks. Returning HTTP Status code [is shown here](http://php.net/manual/en/function.http-response-code.php). However, for most application, the below code should work just fine. http_response_code(503); } catch (Exception $ex) { exit(1); } if($output){ $result = array(); $callbackArr = json_decode($bodyReceived, true); switch($callbackArr['event_type']){ // 这里做switch处理是因为我的项目中有其它接口也用到了这个webhook,所以,可以根据你的项目处理 case 'PAYMENT.SALE.COMPLETED': // 判断是否支付完成 $result = eventPaymentSaleComoleted($callbackArr); // 获取支付完成的信息 break; } return $result; } else { exit; } } function eventPaymentSaleComoleted($callbackArr){ $paymentId = $callbackArr['resource']['parent_payment']; try { $payment = \PayPal\Api\Payment::get($paymentId, $this->apiContext); } catch (Exception $ex) { exit(1); } $transactions = $payment->getTransactions(); $relatedResources = $transactions[0]->getRelatedResources(); $sale = $relatedResources[0]->getSale(); $saleId = $sale->getId(); $invoiceNumber = $transactions[0]->invoice_number; if($this->logEnabled){ $file_error = fopen(dirname(__FILE__).'/saleR_'.$this->logFileName, 'w'); $txt = $invoiceNumber.','.$callbackArr['resource']['amount']['total'].','.$saleId; fwrite($file_error, $txt); fclose($file_error); } $result = array( 'out_trade_no' => $invoiceNumber, 'total_fee' => $callbackArr['resource']['amount']['total'], 'api_trade_no' => $saleId, ); return $result; }
退款接口也是一样的,用SDK提供的类,来对接接口即可,退款接口需要用到curl来获取结果信息,在沙盒的开发过程中遇到了SSL connect error的错误,这个在我的另一篇博文中提到了,如果你也遇到了,可以参考一下那篇文章。