Shadow
Thinkphp6接入Paypal支付流程
直接进入主题
这里直接在默认的Index
控制器中编写的测试代码
<?php
/**
* Created by PhpStorm.
* Author: Dcr163
* Date: 2025/10/8
* Time: 14:33
*/
namespace app\controller;
use app\Request;
use think\Exception;
use think\response\Json;
class Index
{
//paypal 沙盒环境
const PAYPAL_URL = 'https://api-m.sandbox.paypal.com';
//paypal 正式环境
// const PAYPAL_URL = 'https://api-m.paypal.com';
//API KEY 沙箱环境https://developer.paypal.com/dashboard/accounts/里的Business账户详情REST API apps 模块下
const CLIENT_ID = '替换成自己的';
//API秘钥
const CLIENT_SECRET = '替换成自己的';
//获取Token 链接 沙盒环境
const GET_TOKEN_URL = self::PAYPAL_URL . '/v1/oauth2/token';
//订单创建URL
const CREATE_ORDER_URL = self::PAYPAL_URL . '/v2/checkout/orders';
//get:显示订单详情 patch:更新状态为“已创建”或“已批准”的订单。无法更新状态为“已完成”的订单 /v2/checkout/orders/{id}
const SHOW_ORDER_URL = self::PAYPAL_URL . '/v2/checkout/orders/';
//paypal auth token缓存key
const TOKEN_KEY = 'paypal:auth:token';
public $paypalToken = '';
public function __construct()
{
$this->paypalToken = $this->getToken();
}
public function index()
{
echo '我是Index';
}
/**
* 向paypal下单
* @return Json
*/
public function createOrder()
{
$ordersn = 'SO' . date('YmdHis');
$customId = 'UE' . mt_rand(10000, 99999);
$orderData = [
'purchase_units' => [
[
'reference_id' => $ordersn,
'description' => 'Helle Demo ' . $ordersn,
'custom_id' => $customId,
'amount' => [
'currency_code' => 'USD',
'value' => '0.01',
//如果需要列表打开下面,总额需要和items里面的合计相等
// 'breakdown' => [
// 'item_total' => [
// 'currency_code' => 'USD',
// 'value' => '1.00',
// ]
// ]
],
//如果需要商品列表打开下面
// 'items' => [
// [
// 'name' => 'goods1',
// 'quantity' => 1,
// 'unit_amount' => [
// 'currency_code' => 'USD',
// 'value' => '0.50',
// ],
// ],
// [
// 'name' => 'goods2',
// 'quantity' => 1,
// 'unit_amount' => [
// 'currency_code' => 'USD',
// 'value' => '0.50',
// ],
// ],
// ],
]
],
'intent' => 'CAPTURE', //即时到账
'payment_source' => [
'paypal' => [
'experience_context' => [
'payment_method_preference' => 'IMMEDIATE_PAYMENT_REQUIRED',
//LOGIN:当客户点击 PayPal Checkout 时,客户将被重定向到一个页面以登录 PayPal 并批准付款 ; GUEST_CHECKOUT:当客户点击 PayPal 结账时,系统会将客户重定向至一个页面,要求输入信用卡或借记卡信息以及其他完成购买所需的相关账单信息。此选项之前也称为“账单”;NO_PREFERENCE:当客户点击 PayPal Checkout 时,客户将被重定向到登录 PayPal 并批准付款的页面,或者输入信用卡或借记卡以及完成购买所需的其他相关账单信息的页面,具体取决于他们之前与 PayPal 的互动。
'landing_page' => 'NO_PREFERENCE',
//GET_FROM_FILE:在 PayPal 网站上获取客户提供的送货地址; NO_SHIPPING:从 API 响应和 Paypal 网站中删除收货地址信息; SET_PROVIDED_ADDRESS:获取商家提供的地址。客户无法在 PayPal 网站上更改此地址。如果商家未提供地址,客户可以在 PayPal 页面上选择地址
"shipping_preference" => "NO_SHIPPING",
"user_action" => "PAY_NOW",
"return_url" => "http://localhost:8000/index/return_url", //批准付款后跳转的商家地址
"cancel_url" => "http://localhost:8000/index/cancel_url" //取消付款后跳转的商家地址
],
]
],
];
//请求需要的格式
$header = [
'Authorization:Bearer ' . $this->paypalToken,
'Content-Type:application/json',
];
$res = $this->request(self::CREATE_ORDER_URL, 'POST', json_encode($orderData), $header);
$content = json_decode($res['content'], true);
if ($res['http_code'] != 200) {
return json($content);
}
//前端需要跳转的支付地址
$redirectUrl = '';
//获取操作支付的地址
foreach ($content['links'] as $link) {
//payer-action 支付的操作
if ($link['rel'] == 'payer-action') {
$redirectUrl = $link['href'];
break;
}
}
//返回前端
$returnData = [
'id' => $content['id'],
'paypal_id' => $content['id'],
'status' => $content['status'],
'redirect_url' => $redirectUrl,
];
return json($returnData);
}
/**
* 查询订单详情
* @param string $paypalOrderId
* @return Json
*/
public function showPaypalOrder(string $paypalOrderId)
{
$res = $this->request(self::SHOW_ORDER_URL . $paypalOrderId, 'get', [], ['Authorization:Bearer ' . $this->paypalToken]);
$content = json_decode($res['content']);
if ($res['http_code'] != 200) {
return json($content);
}
return json($content);
}
/**
* 用户操作支付后跳转的页面
* @param Request $request
* @return Json
*/
public function return_url(Request $request)
{
//paypal的订单id
$orderId = $request->get('token', '', 'trim');
$payerID = $request->get('PayerID', '', 'trim');
// echo printf("已支付回调:订单ID:%s,支付者ID:%s", $orderId, $payerID);
//这里同步操作去捕获订单
return $this->capturePaypalOrder($orderId);
}
/**
* 用户未支付/取消支付
* @param Request $request
* @return void
*/
public function cancel_url(Request $request)
{
$orderId = $request->get('token', '', 'trim');
$payerID = $request->get('PayerID', '', 'trim');
echo printf("未支付:订单ID:%s,支付者ID:%s", $orderId, $payerID);
}
/**
* 捕获支付订单,这里可以修改订单状态,同步capture_id到订单表
* @param string $paypalOrderId paypal的订单ID
* @return Json
*/
public function capturePaypalOrder(string $paypalOrderId)
{
$header = [
'Authorization:Bearer ' . $this->paypalToken,
'Content-Type:application/json',
];
$data = $this->request(self::SHOW_ORDER_URL . $paypalOrderId . '/capture', 'post', [], $header);
$content = json_decode($data['content'], true);
if ($data['http_code'] != 200) {
return json($content);
}
return json($content);
}
/**
* 支付订单退款
* @param string $captureId 捕获订单的ID
* @param string $money 退款的总金额
* @return Json
*/
public function refundPaypalOrder(string $captureId, string $money)
{
$refundUrl = self::PAYPAL_URL . '/v2/payments/captures/' . $captureId . '/refund';
$header = [
'Authorization:Bearer ' . $this->paypalToken,
'Content-Type:application/json',
];
$params = [
'amount' => [
'currency_code' => 'USD',
'value' => $money,
]
];
$data = $this->request($refundUrl, 'post', json_encode($params), $header);
$content = json_decode($data['content'], true);
if ($data['http_code'] != 200) {
return json($content);
}
return json($content);
}
/**
* 获取paypal Token
* @param bool $refresh
* @return mixed|string|null
* @throws Exception
*/
public function getToken(bool $refresh = false)
{
//先读取缓存
$token = redisUser(12)->get(self::TOKEN_KEY);
if (!$token || $refresh) {
$res = $this->request(self::GET_TOKEN_URL, 'POST', http_build_query(['grant_type' => 'client_credentials']), '', 10, '', [self::CLIENT_ID, self::CLIENT_SECRET]);
$data = json_decode($res['content'], true);
if (isset($data['error']) || !isset($data['access_token'])) {
throw new Exception($data);
}
//把token给缓存下来,设置过期时间早与paypal过期时间
redisUser(12)->setex(self::TOKEN_KEY, $data['expires_in'] - 10, $data['access_token']);
$token = $data['access_token'];
}
$this->paypalToken = $token;
return $token;
}
private function curlUrl($url, array $data = [], string $method = 'POST', array $authInfo = [])
{
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 10,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_SSL_VERIFYPEER => false,
));
if (strtoupper($method) == 'GET') {
curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($data));
}
if (strtoupper($method) == 'POST') {
curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($data));
}
if (!empty($authInfo)) {
curl_setopt($curl, CURLOPT_USERPWD, implode(':', $authInfo));
}
$response = curl_exec($curl);
curl_close($curl);
return $response;
}
/**
* 发起请求
* @param string $url
* @param string $method
* @param $data
* @param $header
* @param int $timeout
* @param $cookie
* @param array $authInfo
* @return array
*/
private function request(string $url, string $method = 'get', $data = [], $header = false, int $timeout = 10, $cookie = '', array $authInfo = [])
{
$curl = curl_init($url);
$method = strtoupper($method);
//请求方式
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
//携带参数
if (in_array($method, ['POST', 'PATCH'])) {
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
} elseif ($method == 'GET' && count($data)) {
$url .= '?' . http_build_query($data);
curl_setopt($curl, CURLOPT_URL, $url);
}
//超时时间
curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
//设置header头
if ($header) curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
curl_setopt($curl, CURLOPT_FAILONERROR, false);
//返回抓取数据
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
//输出header头信息
curl_setopt($curl, CURLOPT_HEADER, true);
//TRUE 时追踪句柄的请求字符串,从 PHP 5.1.3 开始可用。这个很关键,就是允许你查看请求header
curl_setopt($curl, CURLINFO_HEADER_OUT, true);
//https请求
if (1 == strpos("$" . $url, "https://")) {
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
}
//设置cookie
if ($cookie != '') curl_setopt($curl, CURLOPT_COOKIE, $cookie);
//设置认证信息
if (!empty($authInfo)) curl_setopt($curl, CURLOPT_USERPWD, implode(':', $authInfo));
$curlError = curl_error($curl);
list($content, $status) = [curl_exec($curl), curl_getinfo($curl), curl_close($curl)];
$content = trim(substr($content, $status['header_size']));
return [
'error' => $curlError,
'http_code' => $status['http_code'],
'content' => $content,
];
}
}
整理了一个简易的流程图
Dcr163的博客
http://dcr163.cn/763.html(转载时请注明本文出处及文章链接)