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的错误,这个在我的另一篇博文中提到了,如果你也遇到了,可以参考一下那篇文章。