'https://api.paypal.com/',
1 => 'https://www.sandbox.paypal.com/',
];
// 订单信息存储缓存key
private $pay_data_cache_key = 'payment_paypal_pay_data_';
// 返回地址存储缓存key
private $respond_url_cache_key = 'payment_paypal_respond_url_cache_key_';
/**
* 构造方法
* @author Devil
* @blog http://gong.gg/
* @version 1.0.0
* @date 2021-02-07
* @desc description
* @param [array] $params [输入参数(支付配置参数)]
*/
public function __construct($params = [])
{
$this->config = $params;
}
/**
* 配置信息
* @author Devil
* @blog http://gong.gg/
* @version 1.0.0
* @date 2018-09-19
* @desc description
*/
public function Config()
{
// 基础信息
$base = [
'name' => 'PayPal', // 插件名称
'version' => '1.0.0', // 插件版本
'apply_version' => '不限', // 适用系统版本描述
'apply_terminal'=> ['pc', 'h5', 'ios', 'android'], // 适用终端 默认全部 ['pc', 'h5', 'ios', 'android', 'alipay', 'weixin', 'baidu', 'toutiao']
'desc' => '适配PC+H5+APP、国际主流支付方式,即时到帐支付方式,买家的交易资金直接打入卖家账户,快速回笼交易资金。 立即申请', // 插件描述(支持html)
'author' => 'Devil', // 开发者
'author_url' => 'http://shopxo.net/', // 开发者主页
];
// 配置信息
$element = [
[
'element' => 'input',
'type' => 'text',
'default' => '',
'name' => 'client_id',
'placeholder' => 'ClientID',
'title' => 'ClientID',
'is_required' => 0,
'message' => '请填写ClientID',
],
[
'element' => 'input',
'type' => 'text',
'default' => '',
'name' => 'client_secret',
'placeholder' => 'ClientSecret',
'title' => 'ClientSecret',
'is_required' => 0,
'message' => '请填写ClientSecret',
],
[
'element' => 'input',
'type' => 'text',
'default' => '',
'name' => 'webhook_id',
'placeholder' => '订单WebhookID',
'title' => '订单WebhookID',
'is_required' => 0,
'message' => '请填写订单WebhookID、配置异步通知地址后得到的id',
],
[
'element' => 'input',
'type' => 'text',
'default' => '',
'name' => 'wallet_webhook_id',
'placeholder' => '钱包充值WebhookID',
'title' => '钱包充值WebhookID',
'is_required' => 0,
'message' => '请填写钱包充值WebhookID、配置异步通知地址后得到的id',
],
[
'element' => 'input',
'type' => 'text',
'default' => '',
'name' => 'membershiplevelvip_webhook_id',
'placeholder' => '会员购买WebhookID',
'title' => '会员购买WebhookID',
'is_required' => 0,
'message' => '请填写会员购买WebhookID、配置异步通知地址后得到的id',
],
[
'element' => 'input',
'type' => 'text',
'default' => '',
'name' => 'scanpay_webhook_id',
'placeholder' => '扫码收款WebhookID',
'title' => '扫码收款WebhookID',
'is_required' => 0,
'message' => '请填写扫码收款WebhookID、配置异步通知地址后得到的id',
],
[
'element' => 'select',
'title' => '货币',
'message' => '请选择货币',
'name' => 'currency_code',
'is_multiple' => 0,
'element_data' => [
['value'=>'', 'name'=>'Auto Match'],
['value'=>'CNY', 'name'=>'Chinese Renmenbi( CNY )'],
['value'=>'USD', 'name'=>'U.S. Dollar( USD )'],
['value'=>'AUD', 'name'=>'Australian Dollar( AUD )'],
['value'=>'BRL', 'name'=>'Brazilian Real( BRL )'],
['value'=>'CAD', 'name'=>'Canadian Dollar( CAD )'],
['value'=>'CZK', 'name'=>'Czech Koruna( CZK )'],
['value'=>'DKK', 'name'=>'Danish Krone( DKK )'],
['value'=>'EUR', 'name'=>'Euro( EUR )'],
['value'=>'HKD', 'name'=>'Hong Kong Dollar( HKD )'],
['value'=>'HUF', 'name'=>'Hungarian Forint( HUF )'],
['value'=>'ILS', 'name'=>'Israeli New Sheqel( ILS )'],
['value'=>'JPY', 'name'=>'Japanese Yen( JPY )'],
['value'=>'MYR', 'name'=>'Malaysian Ringgit( MYR )'],
['value'=>'MXN', 'name'=>'Mexican Peso( MXN )'],
['value'=>'NOK', 'name'=>'Norwegian Krone( NOK )'],
['value'=>'NZD', 'name'=>'New Zealand Dollar( NZD )'],
['value'=>'PHP', 'name'=>'Philippine Peso( PHP )'],
['value'=>'PLN', 'name'=>'Polish Zloty( PLN )'],
['value'=>'GBP', 'name'=>'Pound Sterling( GBP )'],
['value'=>'RUB', 'name'=>'Russian Ruble( RUB )'],
['value'=>'SGD', 'name'=>'Singapore Dollar( SGD )'],
['value'=>'SEK', 'name'=>'Swedish Krona( SEK )'],
['value'=>'CHF', 'name'=>'Swiss Franc( CHF )'],
['value'=>'TWD', 'name'=>'Taiwan New Dollar( TWD )'],
['value'=>'THB', 'name'=>'Thai Baht( THB )'],
],
],
[
'element' => 'select',
'title' => '是否沙盒环境',
'message' => '请选择是否沙盒环境',
'name' => 'is_dev_env',
'is_multiple' => 0,
'element_data' => [
['value'=>0, 'name'=>'否'],
['value'=>1, 'name'=>'是'],
],
],
[
'element' => 'message',
'message' => '1. 订单异步通知地址,将该地址配置到支付后台异步通知
'.__MY_URL__.'payment_default_order_'.strtolower(str_replace(['payment', '\\'], '', get_class($this))).'_notify.php
2. 钱包充值异步通知地址,将该地址配置到支付后台异步通知
'.__MY_URL__.'payment_default_wallet_'.strtolower(str_replace(['payment', '\\'], '', get_class($this))).'_notify.php
3. 会员等级购买异步通知地址,将该地址配置到支付后台异步通知
'.__MY_URL__.'payment_default_membershiplevelvip_'.strtolower(str_replace(['payment', '\\'], '', get_class($this))).'_notify.php
4. 扫码收款异步通知地址,将该地址配置到支付后台异步通知
'.__MY_URL__.'payment_default_scanpay_'.strtolower(str_replace(['payment', '\\'], '', get_class($this))).'_notify.php
异步通知类型勾选【Payments & Payouts 下面的 Payment capture completed】即可',
],
];
return [
'base' => $base,
'element' => $element,
];
}
/**
* 支付入口
* @author Devil
* @blog http://gong.gg/
* @version 1.0.0
* @date 2018-09-19
* @desc description
* @param [array] $params [输入参数]
*/
public function Pay($params = [])
{
// 参数
if(empty($params))
{
return DataReturn('参数不能为空', -1);
}
// 配置信息
if(empty($this->config) || empty($this->config['client_id']) || empty($this->config['client_secret']))
{
return DataReturn('支付缺少配置', -1);
}
// token
$token = $this->HttpRequest('v1/oauth2/token', 'grant_type=client_credentials');
if($token['code'] != 0)
{
return $token;
}
// 设置请求token
$this->access_token = $token['data']['access_token'];
// 创建订单
$parameter = [
'intent' => 'CAPTURE',
'purchase_units' => [
[
'amount' => [
'currency_code' => $this->CurrencyCode(),
'value' => PriceNumberFormat($params['total_price']),
],
'description' => $params['name'],
'custom_id' => $params['order_no'],
],
],
'application_context'=> [
'cancel_url' => $params['call_back_url'],
'return_url' => $params['call_back_url'],
],
];
// 支付请求记录
PayLogService::PayLogRequestRecord($params['order_no'], ['request_params'=>$parameter]);
// 请求接口
$result = $this->HttpRequest('v2/checkout/orders', $parameter);
if($result['code'] != 0)
{
return $result;
}
// 是否存在返回地址
$respond_url = (!empty($params['params']) && !empty($params['params']['respond_url'])) ? base64_decode(urldecode($params['params']['respond_url'])) : null;
MyCache($this->respond_url_cache_key.$params['order_no'], $respond_url, 3600);
// 订单信息存储缓存
$key = $this->pay_data_cache_key.$result['data']['id'];
MyCache($key, [
'params' => $params,
'token' => $token['data'],
'respond' => $result['data'],
'client_type' => APPLICATION_CLIENT_TYPE,
], 3600);
// APP仅返回id
if(in_array(APPLICATION_CLIENT_TYPE, ['ios', 'android']))
{
// 是否沙盒模式
$is_dev_env = isset($this->config['is_dev_env']) ? $this->config['is_dev_env'] : 0;
// 拼接回调捕获接口
$call_back_url = $params['call_back_url'].(stripos($params['call_back_url'], '?') == false ? '?' : '&').'payment_order_id='.$result['data']['id'];
// 返回支付数据
return DataReturn('success', 0, [
'pay_data' => [
'clientId' => $this->config['client_id'],
'orderId' => $result['data']['id'],
'userAction' => 'paynow',
'currency' => $this->CurrencyCode(),
'environment' => ($is_dev_env == 1) ? 'sandbox' : 'live',
],
'call_back_url' => $call_back_url,
]);
}
// 直接返回支付url地址
return DataReturn('success', 0, $result['data']['links'][1]['href']);
}
/**
* 同步回调处理
* @author Devil
* @blog http://gong.gg/
* @version 1.0.0
* @date 2018-09-19
* @desc description
* @param [array] $params [输入参数]
*/
public function Respond($params = [])
{
// 回调处理
$ret = $this->RespondHandle($params);
if($ret['code'] == 0)
{
// 是否自定义返回地址
$respond_url = MyCache($this->respond_url_cache_key.$ret['data']['out_trade_no']);
if(!empty($respond_url))
{
MyCache($this->respond_url_cache_key.$ret['data']['out_trade_no'], null);
MyRedirect($respond_url, true);
}
}
// 移除多余的数据
if(!empty($ret['data']) && is_array($ret['data']) && isset($ret['data']['pay_data']))
{
unset($ret['data']['pay_data']);
}
return $ret;
}
/**
* 货币code
* @author Devil
* @blog http://gong.gg/
* @version 1.0.0
* @date 2024-07-11
* @desc description
*/
public function CurrencyCode($params = [])
{
if(empty($this->config['currency_code']))
{
// 指定货币
if(!empty($params['currency_data']) && !empty($params['currency_data']['currency_code']))
{
return $params['currency_data']['currency_code'];
}
// 当前默认货币
$res = ResourcesService::CurrencyData();
return (empty($res) || empty($res['currency_code'])) ? 'CNY' : $res['currency_code'];
}
return $this->config['currency_code'];
}
/**
* 回调数据处理
* @author Devil
* @blog http://gong.gg/
* @version 1.0.0
* @date 2022-07-16
* @desc description
* @param [array] $params [输入参数]
*/
public function RespondHandle($params = [])
{
// 回调token
$order_id = empty($params['payment_order_id']) ? (empty($params['token']) ? '' : $params['token']) : $params['payment_order_id'];
if(empty($order_id))
{
return DataReturn('回调token为空', -1);
}
$pay_data = MyCache($this->pay_data_cache_key.$order_id);
if(empty($pay_data))
{
return DataReturn('支付数据失效、请重新发起支付', -1);
}
// 设置请求token
$this->access_token = $pay_data['token']['access_token'];
// 捕获订单
$result = $this->HttpRequest($pay_data['respond']['links'][3]['href'], '');
if($result['code'] != 0)
{
$result['data'] = ['pay_data'=>$pay_data];
return $result;
}
// 返回数据固定基础参数
$data = $result['data'];
$captures = $data['purchase_units'][0]['payments']['captures'][0];
$gross_amount = $captures['seller_receivable_breakdown']['gross_amount'];
$data['trade_no'] = $captures['id']; // 支付平台 - 订单号
$data['buyer_user'] = $data['payer']['email_address']; // 支付平台 - 用户
$data['out_trade_no'] = $captures['custom_id']; // 本系统发起支付的 - 订单号
$data['subject'] = $data['id']; // 本系统发起支付的 - 商品名称
$data['pay_price'] = $gross_amount['value']; // 本系统发起支付的 - 总价
// 当前支付插件需要返回的数据
$data['pay_data'] = $pay_data;
// 默认返回固定格式
return DataReturn('支付成功', 0, $data);
}
/**
* 异步回调处理
* @author Devil
* @blog http://gong.gg/
* @version 1.0.0
* @date 2018-09-19
* @desc description
* @param [array] $params [输入参数]
*/
public function Notify($params = [])
{
if(empty($params['event_type']))
{
return DataReturn('事件类型为空', -1);
}
if(!in_array($params['event_type'], ['PAYMENT.CAPTURE.COMPLETED']))
{
return DataReturn('其他事件('.$params['event_type'].':'.$params['summary'].')', -1);
}
// 判断webhookid、默认订单
$pluginsname = MyInput('pluginsname');
$arr = ['wallet'=>'wallet_webhook_id', 'membershiplevelvip'=>'membershiplevelvip_webhook_id', 'scanpay'=>'scanpay_webhook_id'];
$webhook_field = empty($arr[$pluginsname]) ? 'webhook_id' : $arr[$pluginsname];
// 签名验证
if(empty($this->config[$webhook_field]))
{
return DataReturn('未配置webhookid', -1);
}
$crc32 = crc32(file_get_contents('php://input'));
if (empty($_SERVER['HTTP_PAYPAL_TRANSMISSION_ID']) || empty($_SERVER['HTTP_PAYPAL_TRANSMISSION_TIME']) || empty($crc32))
{
return DataReturn('签名数据为空', -1);
}
$sign_string = $_SERVER['HTTP_PAYPAL_TRANSMISSION_ID'].'|'.$_SERVER['HTTP_PAYPAL_TRANSMISSION_TIME'].'|'.$this->config[$webhook_field].'|'.$crc32;
// 通过PAYPAL-CERT-URL头信息去拿公钥
$public_key = openssl_pkey_get_public($this->HttpCurlCert($_SERVER['HTTP_PAYPAL_CERT_URL']));
$details = openssl_pkey_get_details($public_key);
$verify = openssl_verify($sign_string, base64_decode($_SERVER['HTTP_PAYPAL_TRANSMISSION_SIG']), $details['key'], 'SHA256');
if($verify != 1)
{
return DataReturn('签名验证失败', -1);
}
// 返回数据固定基础参数
$resource = $params['resource'];
$params['trade_no'] = $resource['id']; // 支付平台 - 订单号
$params['buyer_user'] = $params['id']; // 支付平台 - 用户
$params['out_trade_no'] = $resource['custom_id']; // 本系统发起支付的 - 订单号
$params['subject'] = $params['summary']; // 本系统发起支付的 - 商品名称
$params['pay_price'] = $resource['amount']['value']; // 本系统发起支付的 - 总价
return DataReturn('支付成功', 0, $params);
}
/**
* 退款处理
* @author Devil
* @blog http://gong.gg/
* @version 1.0.0
* @date 2019-05-28
* @desc description
* @param [array] $params [输入参数]
*/
public function Refund($params = [])
{
// 参数
$p = [
[
'checked_type' => 'empty',
'key_name' => 'order_no',
'error_msg' => '订单号不能为空',
],
[
'checked_type' => 'empty',
'key_name' => 'trade_no',
'error_msg' => '交易平台订单号不能为空',
],
[
'checked_type' => 'empty',
'key_name' => 'refund_price',
'error_msg' => '退款金额不能为空',
],
];
$ret = ParamsChecked($params, $p);
if($ret !== true)
{
return DataReturn($ret, -1);
}
// token
$token = $this->HttpRequest('v1/oauth2/token', 'grant_type=client_credentials');
if($token['code'] != 0)
{
return $token;
}
// 设置请求token
$this->access_token = $token['data']['access_token'];
// 退款原因
$refund_reason = empty($params['refund_reason']) ? $params['order_no'].'订单退款'.$params['refund_price'].'元' : $params['refund_reason'];
// 退款操作
$parameter = [
'amount' => [
'currency' => $this->CurrencyCode($params),
'total' => $params['refund_price'],
],
'description' => $refund_reason,
];
$result = $this->HttpRequest('v1/payments/sale/'.$params['trade_no'].'/refund', $parameter);
if($result['code'] != 0)
{
// 是否已完成退款
if($result['data'] == 'TRANSACTION_ALREADY_REFUNDED')
{
// 统一返回格式
$data = [
'out_trade_no' => $params['order_no'],
'trade_no' => '',
'buyer_user' => '',
'refund_price' => $params['refund_price'],
'return_params' => $result['data'],
'request_params' => $parameter,
];
return DataReturn('退款成功', 0, $data);
}
return $result;
}
if(isset($result['data']['state']) && $result['data']['state'] == 'completed')
{
// 统一返回格式
$data = [
'out_trade_no' => $params['order_no'],
'trade_no' => $result['data']['id'],
'buyer_user' => $result['data']['sale_id'],
'refund_price' => $result['data']['total_refunded_amount']['value'],
'return_params' => $result['data'],
'request_params' => $parameter,
];
return DataReturn('退款成功', 0, $data);
}
}
/**
* 证书下载
* @author Devil
* @blog http://gong.gg/
* @version 1.0.0
* @date 2022-07-13
* @desc description
* @param [string] $url [url地址]
*/
public static function HttpCurlCert($url)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
curl_close($ch);
return $output;
}
/**
* 网络请求
* @author Devil
* @blog http://gong.gg/
* @version 1.0.0
* @datetime 2017-09-25T09:10:46+0800
* @param [string] $query [请求参数]
* @param [array|string] $data [发送数据]
* @param [int] $second [超时]
* @return [mixed] [请求返回数据]
*/
private function HttpRequest($query, $data, $second = 30)
{
// 是否完整的url地址
if(in_array(substr($query, 0, 6), ['https:', 'http:/']))
{
$url = $query;
} else {
// 是否测试环境
$is_dev_env = isset($this->config['is_dev_env']) ? $this->config['is_dev_env'] : 0;
$url = $this->url[$is_dev_env].$query;
}
// 头信息
$header = [
'Accept: application/json',
'Content-Type: application/json',
];
// 是否存在access_token
if(!empty($this->access_token))
{
$header[] = 'Authorization: Bearer '.$this->access_token;
}
// 数据为数组则转json
$data = is_array($data) ? json_encode($data) : $data;
// 初始化curl
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_TIMEOUT, $second);
// 获取token
if($data == 'grant_type=client_credentials')
{
curl_setopt($ch, CURLOPT_USERPWD, $this->config['client_id'].':'.$this->config['client_secret']);
}
$result = curl_exec($ch);
//返回结果
if($result)
{
curl_close($ch);
$result = json_decode($result, true);
if(empty($result))
{
return DataReturn('返回数据有误:'.$result, -1);
}
if(!empty($result['error_description']))
{
return DataReturn('返回失败:'.$result['error_description'].'('.$result['error'].')', -1, $result['error']);
}
if(!empty($result['message']))
{
return DataReturn('异常错误:'.$result['message'].'('.$result['name'].')', -1, $result['name']);
}
return DataReturn('success', 0, $result);
} else {
$error = curl_errno($ch);
curl_close($ch);
return DataReturn('curl出错,错误码:'.$error, -1);
}
}
}
?>