Leo Code

主页 > PHP > Paypal 国际版REST接口的开发

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