PayPal 是备受用户追捧的国际贸易支付工具,即时支付,即时到账,在本地化方面也做的不错,支持中文,就是手续费费率有点高。本文主要介绍,如何使用 PHP 在网站中接入 PayPal 支付,使网站拥有支付能力。
首先要去 PayPal 官网 申请一个 PayPal 账号。
申请完成后登录,进入到 PayPal 开发者平台 的 Sandbox 选项下即可看到申请账号自动分配的两个测试账号,账号类型分别是 BUSINESS
和 PERSONAL
。PayPal 给开发者提供了沙盒测试环境,所有流程和 API 接口(测试环境根域名:api.sandbox.paypal.com
;线上环境根域名:api.paypal.com
)调用都和真实环境一样,唯一不同的区别是,仅供测试使用,货币都是无效的,所以你可以充值任意金额。事实上这两个账号的状态一直是 processing 状态,无法使用,所以我们需要点击 sandbox 里的创建账户,另外建立两个账号:一个是商家账号(BUSINESS,用于收款和付款),一个是买家账号(PERSONAL,用于付款和转账收款)。
点击对应账号下的 Profile 就能看到账号相关的信息,比如账号、密码、余额等等。
在 My Apps & Credentials 选项下创建一个 APP,然后选择一个测试账号绑定到该 APP 上。Client ID
和 Secret
用于 API 调用的身份认证,我们在后面会用到。
PayPal 官方提供了基于 RESTful 风格实现的 API 接口,供 PHP 开发者使用。
项目地址:https://github.com/paypal/PayPal-PHP-SDK
我们用 composer 即可方便安装:
composer require paypal/rest-api-sdk-php:*
接口请求和返回的数据都是 JSON 格式,SDK 为我们封装了常用的接口,根据文档说明调用对应方法即可。
由于支付是异步进行的,所以支付结果无法在支付流程结束后立即返回,需要在支付操作完成后再去 PayPal 查询相应订单信息,以获取支付最终的支付结果来判断是否继续支付成功后的业务流程。即时支付一般在用户支付后几秒内订单就会更新状态;而转账往往需要十几秒到数十秒不等,所以如何有效的查询订单支付状态,这个比较重要。常见的是,在即将支付时,原页面显示支付成功或支付遇到问题的按钮,然后新开一个页面跳转到第三方支付平台,用户支付成功后点击支付成功按钮来查询支付结果。
由于每次请求 API 时都需要初始化必要的参数,所以我们把这部分代码抽离出来放到一个单独文件中,在后续使用时引入该文件即可。
//file: start.php
require 'vendor/autoload.php'; // 引入 composer 自动加载
use PayPal\Rest\ApiContext;
use PayPal\Auth\OAuthTokenCredential;
$apiContext = new ApiContext(new OAuthTokenCredential(YOUR_CLIENT_ID, YOUR_CLIENT_SECRET));
//file: create.php
require 'start.php';
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;
$payer = new Payer(); // 实例化支付者
$payer->setPaymentMethod('paypal'); // 使用 PayPal 进行支付
$item = new Item(); // 实例化一个物品
$item->setName('product_name') // 名称
->setCurrency('USD') // 币种
->setQuantity(1) // 数量
->setSku('product_123456') // 编号
->setPrice(12.00); // 价格
$itemList = new ItemList(); // 实例化一个物品列表,即支付清单
$itemList->setItems([$item]); // 将物品加入到列表中
$details = new Details(); // 支付详情
$details->setShipping(2) // 运输费
->setTax(2.5) // 税费
->setSubtotal(12); // 合计,即上面的 物品单价 * 物品数量
$amount = new Amount(); // 实例化总计
$amount->setCurrency('USD')
->setTotal(16.5) // 总金额 = 运输费 + 税费 + 合计
->setDetails($details);
$transaction = new Transaction(); // 实例化交易
$transaction->setAmount($amount)
->setItemList($itemList)
->setDescription('payment description') // 交易描述
->setInvoiceNumber(uniqid());
$baseUrl = 'https://example.com'; // 支付成功或者取消的回调地址
$redirectUrls = new RedirectUrls();
$redirectUrls->setReturnUrl($baseUrl . '/execute.php?success=true')
->setCancelUrl($baseUrl . '/execute.php?success=false');
$payment = new Payment(); // 实例化支付
$payment->setIntent('sale')
->setPayer($payer)
->setRedirectUrls($redirectUrls)
->setTransactions([$transaction]);
$request = clone $payment;
try {
$payment->create($apiContext); // 创建支付,相当于微信、支付宝里的统一下单
} catch (Exception $e) {
echo $e->getMessage();
}
提交的信息会在 PayPal 结算界面看到:
在创建支付之前,$payment
是什么呢,或者说我们做了什么?其实就是拼凑了一个 JSON 格式的请求体,调用 toJSON 方法可以得到下面的信息:
{
"intent": "sale",
"payer": {
"payment_method": "paypal"
},
"redirect_urls": {
"return_url": "https://example.com/execute.php?success=true",
"cancel_url": "https://example.com/execute.php?success=false"
},
"transactions": [
{
"amount": {
"currency": "USD",
"total": "16.50",
"details": {
"shipping": "2",
"tax": "2.50",
"subtotal": "12"
}
},
"item_list": {
"items": [
{
"name": "product_name",
"currency": "USD",
"quantity": "1",
"sku": "PRODUCT_bb42cd6e00065b6a611c9466fc84a2e7",
"price": "12"
}
]
},
"description": "payment description",
"invoice_number": "5a7c69645b2e1"
}
]
}
后续执行 create 方法就是在请求 API 接口,成功后返回下单后的数据。再次调用 toJSON 方法,新增的数据如下:
{
"id": "PAY-AAAAAAAA",
"state": "created",
"create_time": "2018-02-07T03:04:09Z",
"links": [
{
"href": "https://api.sandbox.paypal.com/v1/payments/payment/PAY-AAAAA",
"rel": "self",
"method": "GET"
},
{
"href": "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=XXXXX",
"rel": "approval_url",
"method": "REDIRECT"
},
{
"href": "https://api.sandbox.paypal.com/v1/payments/payment/PAY-AAAAA/execute",
"rel": "execute",
"method": "POST"
}
}
approval_url
即用户登录确认支付订单的地址,若确认支付,跳转到 return_url
;若不同意或取消,则跳转到 cancel_url
。这里需要注意的是页面跳转时,可能会由于用户手动关闭或其他特殊原因而失败,此时就需要根据支付订单 id 去获取用户确认状态。
用户确认支付订单后,跳转到支付页面,paymentId
、PayerID
会以 GET 参数的形式传过去,根据 paymentID
获取支付订单,调用 execute 方法进行支付。
//file: execute.php
require 'start.php';
use PayPal\Api\Payment;
use PayPal\Api\PaymentExecution;
if (isset($_GET['success']) && $_GET['success'] == 'true') {
$paymentId = $_GET['paymentId']; // 接收传过来的支付 id
$payment = Payment::get($paymentId, $apiContext); // 根据支付 id 获取支付对象
$execution = new PaymentExecution();
$execution->setPayerId($_GET['PayerID']);
try {
$result = $payment->execute($execution, $apiContext); // 进行支付
try {
$payment = Payment::get($paymentId, $apiContext); // 获取支付后的支付状态
} catch (Exception $e) {
echo $e->getMessage();
}
} catch (Exception $e) {
echo $e->getMessage();
}
}
如果成功进行了支付,支付状态 status
会由原来的 created
修改为 approved
。即时支付的手续费费率参考 Paypal 官网。
转账演示里面,官方文档里面提供了同步通知的创建单个转账接口[Create Single Payout (Synchronously)],但由于同步模式下存在问题,将不再被支持,所以转账需要使用异步或者批量转账接口。同时生产环境下转账接口默认是不开放的,需要联系 Paypal 客服手动开启。
同上述 4.1 。
//file: payouts.php
require 'start.php';
use PayPal\PayPal\Api\PayoutSenderBatchHeader;
use PayPal\Api\PayoutItem;
use PayPal\Api\Currency;
use PayPal\Api\Payout;
$senderBatchHeader = new PayoutSenderBatchHeader();
$senderBatchHeader->setSenderBatchId(uniqid()) // senderBatchId 标识订单的
->setEmailSubject("You have a payment"); // 转账主题
$senderItem1 = new PayoutItem();
$senderItem1->setRecipientType('Email') // Email 方式收款
->setNote('Thanks you') // 转账说明
->setReceiver('email@example.com') // 对应的收款账号
->setSenderItemId('item1_' . uniqid()) // 物品编号
->setAmount(new Currency('{"value:"0.99","currency":"USD"}')); // 转账金额及币种
$payouts = new Payouts(); // 实例化转账对象
$payouts->setSenderBatchHeader($senderBatchHeader)
->addItem($senderItem1);
$request = clone $payouts;
try {
$payouts->create(null, $apiContext); // 进行转账
} catch (Exception $e) {
echo $e->getMessage();
}
转账和支付不一样,不需要用户进行支付确认,直接进行转账。
在创建转账之前,$payouts 是什么呢?同样只是拼凑了一个 JSON 格式的请求体,我们调用 toJSON 方法可以得到下面的信息:
{
"sender_batch_header": {
"sender_batch_id": "5abb54011de20",
"email_subject": "You have a payment"
},
"items": [
{
"recipient_type": "Email",
"note": "Thanks you",
"receiver": "email@example.com",
"sender_item_id": "item_15abb54011de20",
"amount": {
"value": "0.99",
"currency": "USD"
}
}
]
}
后续执行 create 方法就是在请求 API 接口,成功后返回下单后的数据。再次调用 toJSON 方法,新增的数据如下:
{
"batch_header": {
"payout_batch_id": "9CRKTWPDJT9U8",
"batch_status": "PENDING",
"time_created": "2018-03-28T08:28:46Z",
"time_completed": "2018-03-28T08:29:02Z",
"sender_batch_header": {
"email_subject": "You have a payment",
"sender_batch_id": "5abb54011de20"
},
"amount": {
"currency": "USD",
"value": "0.99"
},
"fees": {
"currency": "USD",
"value": "0.25"
}
},
"items": [
{
"payout_item_id": "4ESXTXACP455W",
"transaction_id": "20W42029061726645",
"transaction_status": "PENDING",
"payout_item_fee": {
"currency": "USD",
"value": "0.25"
},
"payout_batch_id": "9CRKTWPDJT9U8",
"payout_item": {
"amount": {
"currency": "USD",
"value": "0.99"
},
"note": "Thanks you.",
"receiver": "1829964321@qq.com",
"recipient_type": "EMAIL",
"sender_item_id": "item_15abb5237c3a78"
},
"time_processed": "2018-03-28T08:28:56Z",
"links": [
{
"href": "https://api.sandbox.paypal.com/v1/payments/payouts-item/4ESXTXACP455W",
"rel": "item",
"method": "GET"
}
]
}
],
"links": [
{
"href": "https://api.sandbox.paypal.com/v1/payments/payouts/9CRKTWPDJT9U8",
"rel": "self",
"method": "GET"
}
]
}
这里要注意,查询订单信息使用的是 payout_batch_id
,sender_batch_id
只是转账这边用于标识订单的,30 天内同一 sender_batch_id
的订单只会被处理一次。转账成功后,订单状态由 PENDING
变为 SUCCESS
。手续费好像是固定的每笔 0.25 美元。
我们常用的就是收付款了,其他还有通知、发票等可以参考官方 API,用法都差不多。