PHP 项目集成 PayPal 支付完整指南
·
1. 开发者账号准备
企业版paypal账号审核进度太慢,可以先注册自己的个人paypal账号然后创建沙箱应用,创建两个沙箱账号一个卖家(默认就有)一个买家。直接去测试支付流程。支付后的订单默认就到卖家沙箱账号中了。可以登录卖家的沙箱账号到订单查看
- 注册 PayPal 开发者账号
- 创建沙盒(Sandbox)测试账号(包括商家和买家账号)
- 获取 API 凭证(Client ID 和 Secret),用于测试和生产环境
2、安装paypal依赖
尝试很多依赖发现后续引用都会出现报错 后面示例是直接写的
composer require paypal/paypal-checkout-sdk:1.0.0
3、示例
示例是fastadmin框架,根据实际使用的框架 安排配置文件和初始化文件的位置。代码中可以引用到就行
shop-project/ # 项目根目录
├─ app/
│ ├─ api/
│ │ └─ controller
| | └─ Paypal.php # 接口控制器(创建订单)
| └─—— common/
| └─library
| └─ PayPalClient.php #初始化paypal客户端
└── extra/ # 扩展文件
└─ paypal.php # paypal配置信息文件
1、paypal配置信息文件(paypal.php)
<?php
return [
'client_id' => 'AWe1Ni_ArcgSLVQv3rsvTJ7VdauJiYnmHYOsBpWSRKTAyoXXjVUD_LF0rbmyKR3y7sZa7bNLtffxt_-p',
'client_secret' => 'EO2bg_zKEgNl3fKT6V1r0By69sW-GjoGLNa7xIsGgdKu4CcQVHbTCQJGjqo_YVu27uZf43PxrEv4wA9d',
'success_url' => 'http://你的域名/回调地址', // 支付成功回调地址
'cancel_url' => 'http://你的域名/回调地址', // 支付取消回调地址
];
2、初始化paypal客户端(PayPalCilent.php)
这里是直接写的,安装很多的依赖都用不上(以后发现好办法了会更新)
沙箱环境 需要切换( $this->baseUrl = self::SANDBOX_URL;)
生产环境 需要切换( $this->baseUrl = self::LIVE_URL;)
<?php
namespace app\common\library;
class PayPalClient
{
// PayPal API基础地址(沙盒环境)
const SANDBOX_URL = 'https://api-m.sandbox.paypal.com';
// 生产环境地址
const LIVE_URL = 'https://api-m.paypal.com';
private $clientId;
private $clientSecret;
private $baseUrl;
private $accessToken;
public function __construct()
{
// 从配置获取凭证
$this->clientId = config('paypal.client_id');
$this->clientSecret = config('paypal.client_secret');
// 验证配置
if (empty($this->clientId) || empty($this->clientSecret)) {
throw new \Exception("请配置PayPal的client_id和client_secret");
}
// 默认使用沙盒环境
$this->baseUrl = self::LIVE_URL;
// 获取访问令牌
$this->accessToken = $this->getAccessToken();
}
/**
* 获取访问令牌
*/
private function getAccessToken()
{
$url = $this->baseUrl . '/v1/oauth2/token';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
curl_setopt($ch, CURLOPT_USERPWD, $this->clientId . ':' . $this->clientSecret);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// 关键修复:处理SSL证书问题
// 生产环境建议开启验证,仅测试环境临时关闭
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 关闭证书验证
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); // 关闭主机验证
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// 捕获CURL错误
if(curl_errno($ch)) {
throw new \Exception("CURL错误:" . curl_error($ch));
}
curl_close($ch);
$result = json_decode($response, true);
if ($httpCode != 200 || empty($result['access_token'])) {
throw new \Exception("获取访问令牌失败:" . json_encode($result));
}
return $result['access_token'];
}
/**
* 创建PayPal订单
*/
public function createOrder($amount, $currency, $returnUrl, $cancelUrl, $referenceId)
{
$url = $this->baseUrl . '/v2/checkout/orders';
$data = [
'intent' => 'CAPTURE',
'purchase_units' => [[
'reference_id' => $referenceId,
'amount' => [
'currency_code' => $currency,
'value' => number_format($amount, 2)
]
]],
'application_context' => [
'return_url' => $returnUrl,
'cancel_url' => $cancelUrl
]
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->accessToken
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// 同样添加SSL证书处理
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if(curl_errno($ch)) {
throw new \Exception("CURL错误:" . curl_error($ch));
}
curl_close($ch);
$result = json_decode($response, true);
if ($httpCode != 201 || empty($result['id'])) {
throw new \Exception("创建订单失败:" . json_encode($result));
}
return $result;
}
/**
* 捕获支付
*/
public function capturePayment($orderId)
{
$url = $this->baseUrl . '/v2/checkout/orders/' . $orderId . '/capture';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->accessToken
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// SSL证书处理
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if(curl_errno($ch)) {
throw new \Exception("CURL错误:" . curl_error($ch));
}
curl_close($ch);
$result = json_decode($response, true);
if ($httpCode != 201 || $result['status'] != 'COMPLETED') {
throw new \Exception("捕获支付失败:" . json_encode($result));
}
return $result;
}
/**
* 获取PayPal订单状态(新增方法)
* 用于验证订单是否真的支付成功,避免前端伪造数据
*/
public function getOrderStatus($orderId)
{
$url = $this->baseUrl . '/v2/checkout/orders/' . $orderId;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPGET, true); // GET请求
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->accessToken
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// 测试环境关闭SSL验证(生产环境需开启)
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
throw new \Exception("获取订单状态CURL错误:" . curl_error($ch));
}
curl_close($ch);
$result = json_decode($response, true);
if ($httpCode != 200) {
throw new \Exception("获取订单状态失败:" . json_encode($result));
}
return $result;
}
}
3、接口控制器(创建订单)
<?php
namespace app\api\controller;
use app\common\library\PayPalClient;
use app\common\controller\Api;
use think\Log;
class Paypal extends Api
{
protected $noNeedLogin = ['*'];
protected $noNeedRight = ['*'];
public function createOrder()
{
// 生成唯一错误追踪ID(与返回给用户的一致)
$errorTrackId = uniqid();
// 写入初始日志
Log::error("===== 开始创建订单 [追踪ID: {$errorTrackId}] =====");
try {
$orderId = $this->request->param('order_id');
Log::error("接收的本地订单ID: {$orderId}");
$order = \app\admin\model\shop\Order::find($orderId);
Log::error("查询到的订单数据: " . json_encode($order ? $order->toArray() : '无'));
if (!$order || $order['paystate'] == 1) {
$msg = "订单不存在或已支付";
Log::error($msg);
Log::error("===== 结束创建订单 [追踪ID: {$errorTrackId}] =====");
// 使用json助手函数返回响应
return json([
'code' => 1,
'msg' => $msg,
'data' => null,
'time' => time()
]);
}
$paypal = new PayPalClient();
Log::error("PayPal客户端初始化完成");
// 准备API参数
$successUrl = config('paypal.success_url') . '?order_id=' . $orderId;
$cancelUrl = config('paypal.cancel_url') . '?order_id=' . $orderId;
$params = [
'amount' => $order['amount'],
'currency' => 'USD',
'success_url' => $successUrl,
'cancel_url' => $cancelUrl,
'order_sn' => $order['order_sn']
];
Log::error("准备调用API的参数: " . json_encode($params));
// 调用PayPal API
$result = $paypal->createOrder(
$order['amount'],
'USD',
$successUrl,
$cancelUrl,
$order['order_sn']
);
Log::error("PayPal API返回结果: " . json_encode($result));
// 验证返回结果
if (!is_array($result)) {
throw new \Exception("API返回格式错误,不是数组");
}
if (empty($result['id'])) {
throw new \Exception("API返回缺少订单ID字段");
}
// 保存到本地
$order->payinfo = json_encode(['paypal_order_id' => $result['id'], 'track_id' => $errorTrackId]);
$saveResult = $order->save();
Log::error("本地订单保存结果: " . ($saveResult ? '成功' : '失败') . json_encode($order->getError()));
if (!$saveResult) {
throw new \Exception("本地订单保存失败");
}
Log::error("===== 订单创建成功 [追踪ID: {$errorTrackId}] =====");
// 直接返回JSON响应,定义明确的结构
// 使用json助手函数返回成功响应
return json([
'code' => 1,
'msg' => '订单创建成功',
'data' => [
'paypal_order_id' => $result['id'],
'local_order_id' => $order->id,
'track_id' => $errorTrackId
],
'time' => time()
]);
} catch (\Exception $e) {
// 记录完整异常信息
$errorInfo = [
'message' => $e->getMessage() ?: '无错误消息',
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
];
Log::error("捕获异常: " . json_encode($errorInfo));
Log::error("===== 订单创建失败 [追踪ID: {$errorTrackId}] =====");
// 使用json助手函数返回错误响应
return json([
'code' => 0,
'msg' => "创建订单失败,请稍后重试(错误ID: {$errorTrackId})",
'data' => null,
'time' => time()
]);
}
}
/**
* 确认支付结果
*/
public function confirm()
{
$paypalOrderId = $this->request->param('order_id');
$localOrderId = $this->request->param('local_order_id');
$tradeNo = $this->request->param('trade_no'); // 接收前端传递的交易号
try {
$order = \app\admin\model\shop\Order::find($localOrderId);
if (!$order) {
return json(['code' => 0, 'msg' => '订单不存在']);
}
// 2. 防止重复处理(如果已支付,直接返回成功)
if ($order['paystate'] == 1) {
return json([
'code' => 1,
'msg' => '订单已支付',
]);
}
$paypal = new PayPalClient();
$orderStatus = $paypal->getOrderStatus($paypalOrderId);
if ($orderStatus['status'] != 'COMPLETED') {
throw new \Exception("PayPal订单未完成支付,状态:{$orderStatus['status']}");
}
// 5. 更新本地订单状态(使用前端传递的交易号)
$order->paystate = 1;
$order->paytime = time();
$order->trade_no = $tradeNo;
$order->payinfo = json_encode([
'paypal_order_id' => $paypalOrderId,
'trade_no' => $tradeNo,
'paypal_status' => $orderStatus['status']
]);
$order->save();
// 触发支付成功事件
// event('OrderPaySuccess', $order);
return json([
'code' => 1,
'msg' => '支付成功',
'data' => ['redirect' => 'order/detail']
]);
} catch (\Exception $e) {
return json(['code' => 0, 'msg' => '支付确认失败:' . $e->getMessage()]);
}
}
}
后端到这里就算结束了。下方记录一下相对应的前端页面调用
实际生产环境 订单号和应付金额以及订单id 都需要通过 createOrder接口获取 示例是写了一个默认的订单号
注意:这里也要区分生产环境和沙箱环境
生产环境用 script.src = `https://www.sandbox.paypal.com/sdk/js?client-id=${paypalClientId}¤cy=USD`; 测试环境用 script.src = `https://www.sandbox.paypal.com/sdk/js?client-id=${paypalClientId}¤cy=USD`;
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>paypal支付测试页</title>
<link rel="shortcut icon" href="__CDN__/assets/img/favicon.ico"/>
<link href="__CDN__/assets/css/index.css" rel="stylesheet">
</head>
<body>
<div class="pay-container" style="max-width: 600px; margin: 50px auto; padding: 0 20px;">
<!-- 订单基本信息 -->
<div class="order-info" style="border-bottom: 1px solid #eee; padding-bottom: 20px; margin-bottom: 30px;">
<h3 style="margin-bottom: 15px;">订单支付 <small class="text-gray">订单号:20250828042007000041831752</small></h3>
<p style="font-size: 16px; color: #e64340;">应付金额:<span style="font-size: 24px;">114.00</span> 元</p>
</div>
<!-- PayPal 按钮容器(默认隐藏,加载成功后显示) -->
<div id="paypal-button-container" style="display: none; margin: 30px 0;"></div>
<!-- 加载状态提示(默认显示) -->
<div id="loading-tip" style="text-align: center; padding: 50px 0; color: #666;">
<i class="fa fa-spinner fa-spin" style="font-size: 24px; margin-bottom: 10px;"></i>
<p>正在加载 PayPal 支付组件...</p>
</div>
<!-- 错误提示(默认隐藏) -->
<div id="error-tip" style="display: none; text-align: center; padding: 50px 0; color: #e64340;">
<i class="fa fa-exclamation-circle" style="font-size: 24px; margin-bottom: 10px;"></i>
<p id="error-message">支付组件加载失败,请刷新页面重试</p>
</div>
<!-- 隐藏的订单ID(供JS使用) -->
<input type="hidden" id="local-order-id" value="191">
<input type="hidden" id="paypal-client-id" value="{$Think.config.paypal.client_id}">
</div>
<!-- 直接在页面内写JS,避免外部文件依赖 -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script>
$(function() {
// 1. 获取页面基础参数
const localOrderId = $('#local-order-id').val(); // 本地订单ID
const paypalClientId = $('#paypal-client-id').val(); // PayPal客户端ID
let paypalOrderId = ''; // PayPal创建的订单ID
// 2. 第一步:调用后端接口创建 PayPal 订单
$.ajax({
url: '/api/paypal/createOrder', // 对应第三步的创建订单接口
type: 'GET',
data: { order_id: localOrderId },
dataType: 'json',
success: function(res) {
console.log(res)
if (res.code === 1) {
paypalOrderId = res.data.paypal_order_id; // 拿到PayPal订单ID
loadPayPalSdk(); // 加载PayPal SDK
} else {
// 订单创建失败,显示错误
showError(res.msg || '创建支付订单失败,请联系客服');
}
},
error: function(xhr, status, error) {
// AJAX请求失败(如接口不存在、网络问题)
showError('调用支付接口失败,错误信息:' + error);
}
});
// 3. 第二步:加载 PayPal SDK(加载成功后渲染按钮)
function loadPayPalSdk() {
// 动态创建SDK脚本标签
const script = document.createElement('script');
// SDK地址带客户端ID,确保正确识别商户
script.src = `https://www.sandbox.paypal.com/sdk/js?client-id=${paypalClientId}¤cy=USD`;
// 加载成功回调
script.onload = function() {
renderPayPalButton(); // 渲染支付按钮
};
// 加载失败回调
script.onerror = function() {
showError('PayPal支付组件加载失败,请检查网络或稍后重试');
};
// 插入到页面中
document.body.appendChild(script);
}
// 4. 第三步:渲染 PayPal 支付按钮
function renderPayPalButton() {
// 检查PayPal对象是否存在(防止SDK加载异常)
if (typeof paypal === 'undefined') {
showError('PayPal组件初始化失败,请刷新页面重试');
return;
}
// 渲染按钮
paypal.Buttons({
// 按钮样式(可选,适配页面风格)
style: {
layout: 'vertical', // 垂直布局(vertical)或水平布局(horizontal)
color: 'blue', // 按钮颜色(blue/black/silver/gold)
shape: 'rect', // 按钮形状(rect/pill)
label: 'pay' // 按钮文字(pay/checkout/buy-now/pay-with-paypal)
},
// 【核心】确认订单(直接使用已创建的PayPal订单ID)
createOrder: function(data, actions) {
return paypalOrderId; // 无需重新创建,直接返回已有的订单ID
},
// 【核心】用户支付成功后的回调
onApprove: function(data, actions) {
// 捕获支付结果(确认资金已到账)
return actions.order.capture().then(function(details) {
// 支付成功,跳转到后端确认接口(更新订单状态)
window.location.href = `/api/paypal/confirm?order_id=${data.orderID}&trade_no=${details.purchase_units[0].payments.captures[0].id}&local_order_id=${localOrderId}`;
});
},
// 支付过程中出错的回调
onError: function(err) {
console.error('PayPal支付错误:', err); // 控制台打印错误(调试用)
showError('支付过程中发生错误:' + (err.message || '未知错误'));
},
// 用户取消支付的回调
onCancel: function(data) {
// 跳转到支付取消页面(可自定义)
window.location.href = `/order/paypal/cancel?order_id=${localOrderId}`;
}
}).render('#paypal-button-container') // 渲染到指定容器
.then(function() {
// 按钮渲染成功,显示按钮、隐藏加载提示
$('#loading-tip').hide();
$('#paypal-button-container').show();
})
.catch(function(err) {
// 按钮渲染失败
showError('支付按钮渲染失败:' + err.message);
});
}
// 5. 通用错误提示方法(统一控制显示状态)
function showError(message) {
$('#loading-tip').hide();
$('#paypal-button-container').hide();
$('#error-message').text(message);
$('#error-tip').show();
}
});
</script>
</body>
</html>
更多推荐

所有评论(0)