871 lines
35 KiB
PHP
871 lines
35 KiB
PHP
<?php
|
||
/**
|
||
* 华为支付客户端类
|
||
* 封装华为支付API调用
|
||
*/
|
||
|
||
namespace addon\huaweipay\data\sdk;
|
||
|
||
use phpseclib3\Crypt\PublicKeyLoader;
|
||
use phpseclib3\Crypt\RSA;
|
||
|
||
use app\exception\ApiException;
|
||
use think\facade\Log;
|
||
|
||
// 引入工具类
|
||
use addon\huaweipay\data\sdk\Utils;
|
||
|
||
|
||
// 定义常量
|
||
// 使用SM2算法验证签名,使用SM3哈希算法
|
||
// 注意:OPENSSL_ALGO_SM3常量在PHP 8.0.0及以上版本可用
|
||
// 对于PHP 7.4及以下版本,使用数字值18(OpenSSL内部SM3算法编号)
|
||
define('OPENSSL_ALGO_SM3', 18);
|
||
|
||
class HuaweiPayClient
|
||
{
|
||
// 华为支付网关地址
|
||
private $gatewayUrl = 'https://petalpay.cloud.huawei.com.cn';
|
||
|
||
// 华为支付配置
|
||
private $config = [];
|
||
|
||
// 签名算法
|
||
private $signType = 'RSA2';
|
||
|
||
// 商户应用私有证书实例
|
||
private $private_key_certificate_instance;
|
||
|
||
// 商户应用公钥证书实例
|
||
private $public_key_certificate_instance;
|
||
|
||
// 华为平台支付证书实例
|
||
private $huawei_public_key_certificate_instance;
|
||
|
||
// 华为平台支付服务加密证书实例
|
||
private $huawei_public_key_certificate_instance_encrypt;
|
||
|
||
// 是否加载了华为平台支付服务加密证书
|
||
private $has_huawei_public_key_certificate_instance_encrypt = false;
|
||
|
||
// 是否显示详细的curl请求信息
|
||
private $show_curl_detail = false;
|
||
|
||
/**
|
||
* 构造函数
|
||
* @param array $config 华为支付配置
|
||
* @param string $cert_root_path 证书根路径
|
||
*/
|
||
public function __construct($config, $cert_root_path = '')
|
||
{
|
||
$this->config = $config;
|
||
|
||
// 验证必要配置项
|
||
if (empty($this->config['app_id'])) {
|
||
throw new \Exception('缺少必要配置:app_id');
|
||
}
|
||
if (empty($this->config['merc_no'])) {
|
||
throw new \Exception('缺少必要配置:merc_no');
|
||
}
|
||
if (empty($this->config['site_id'])) {
|
||
throw new \Exception('缺少必要配置:site_id');
|
||
}
|
||
if (empty($this->config['mch_auth_id'])) {
|
||
throw new \Exception('缺少必要配置:mch_auth_id');
|
||
}
|
||
if (empty($this->config['private_key_text']) && empty($this->config['private_key'])) {
|
||
throw new \Exception('缺少必要配置:private_key_text或private_key');
|
||
}
|
||
if (empty($this->config['huawei_public_key_text']) && empty($this->config['huawei_public_key'])) {
|
||
throw new \Exception('缺少必要配置:huawei_public_key_text或huawei_public_key');
|
||
}
|
||
|
||
// 证书基础路径
|
||
$cert_base_path = $cert_root_path;
|
||
if (empty($cert_base_path)) {
|
||
// 没有指定证书根路径时,使用默认路径
|
||
try {
|
||
$cert_base_path = app()->getRootPath();
|
||
} catch (\Exception $e) {
|
||
// 捕获异常,使用默认路径
|
||
}
|
||
}
|
||
|
||
// 加载证书
|
||
try {
|
||
$this->loadCertificates($cert_base_path);
|
||
} catch (\Exception $e) {
|
||
// 释放已加载的证书资源
|
||
$this->__destruct();
|
||
throw $e;
|
||
}
|
||
|
||
// 根据配置设置网关地址
|
||
if (isset($config['sandbox']) && $config['sandbox']) {
|
||
$this->gatewayUrl = 'https://petalpay-developer.cloud.huawei.com.cn';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载证书
|
||
* @param string $cert_base_path 证书基础路径
|
||
* @throws \Exception
|
||
*/
|
||
private function loadCertificates($cert_base_path)
|
||
{
|
||
// 加载商户应用私有证书
|
||
$private_key_content = '';
|
||
if (!empty($this->config['private_key_text'])) {
|
||
// 文本模式,需要格式化
|
||
$private_key_content = Utils::formatCertificateContent($this->config['private_key_text'], 'private_key');
|
||
} elseif (!empty($this->config['private_key'])) {
|
||
// 文件模式
|
||
$private_key_path = realpath($cert_base_path . $this->config['private_key']);
|
||
if (!$private_key_path) {
|
||
throw new \Exception('商户应用私有证书文件不存在: ' . $this->config['private_key']);
|
||
}
|
||
$private_key_content = file_get_contents($private_key_path);
|
||
} else {
|
||
throw new \Exception('缺少必要配置:private_key或private_key_text');
|
||
}
|
||
|
||
$this->private_key_certificate_instance = openssl_pkey_get_private($private_key_content);
|
||
if (!$this->private_key_certificate_instance) {
|
||
// 输出详细的openssl错误信息
|
||
$error = '';
|
||
while ($err = openssl_error_string()) {
|
||
$error .= $err . ' ';
|
||
}
|
||
throw new \Exception('加载商户应用私有证书失败,请检查证书格式是否正确。OpenSSL错误: ' . $error . ' 证书内容开头: ' . substr($private_key_content, 0, 100));
|
||
}
|
||
|
||
// 加载商户应用公钥证书
|
||
$public_key_content = '';
|
||
if (!empty($this->config['public_key_text'])) {
|
||
// 文本模式,需要格式化
|
||
$public_key_content = Utils::formatCertificateContent($this->config['public_key_text'], 'public_key');
|
||
} elseif (!empty($this->config['public_key'])) {
|
||
// 文件模式
|
||
$public_key_path = realpath($cert_base_path . $this->config['public_key']);
|
||
if (!$public_key_path) {
|
||
throw new \Exception('商户应用公钥证书文件不存在: ' . $this->config['public_key']);
|
||
}
|
||
$public_key_content = file_get_contents($public_key_path);
|
||
}
|
||
|
||
if (!empty($public_key_content)) {
|
||
$this->public_key_certificate_instance = openssl_pkey_get_public($public_key_content);
|
||
if (!$this->public_key_certificate_instance) {
|
||
throw new \Exception('加载商户应用公钥证书失败,请检查证书格式是否正确');
|
||
}
|
||
}
|
||
|
||
// 加载华为平台支付证书
|
||
$huawei_public_key_content = '';
|
||
if (!empty($this->config['huawei_public_key_text'])) {
|
||
// 文本模式,需要格式化
|
||
$huawei_public_key_content = Utils::formatCertificateContent($this->config['huawei_public_key_text'], 'public_key');
|
||
} elseif (!empty($this->config['huawei_public_key'])) {
|
||
// 文件模式
|
||
$huawei_public_key_path = realpath($cert_base_path . $this->config['huawei_public_key']);
|
||
if (!$huawei_public_key_path) {
|
||
throw new \Exception('华为平台支付证书文件不存在: ' . $this->config['huawei_public_key']);
|
||
}
|
||
$huawei_public_key_content = file_get_contents($huawei_public_key_path);
|
||
} else {
|
||
throw new \Exception('缺少必要配置:huawei_public_key或huawei_public_key_text');
|
||
}
|
||
|
||
$this->huawei_public_key_certificate_instance = openssl_pkey_get_public($huawei_public_key_content);
|
||
if (!$this->huawei_public_key_certificate_instance) {
|
||
throw new \Exception('加载华为平台支付证书失败,请检查证书格式是否正确');
|
||
}
|
||
|
||
// 加载华为平台支付服务加密证书(可选)
|
||
if (!empty($this->config['huawei_public_key_for_sessionkey_text'])) {
|
||
// 文本模式,需要格式化
|
||
$huawei_public_key_encrypt_content = Utils::formatCertificateContent($this->config['huawei_public_key_for_sessionkey_text'], 'public_key');
|
||
$this->huawei_public_key_certificate_instance_encrypt = openssl_pkey_get_public($huawei_public_key_encrypt_content);
|
||
if (!$this->huawei_public_key_certificate_instance_encrypt) {
|
||
throw new \Exception('加载华为平台支付服务加密证书失败');
|
||
}
|
||
$this->has_huawei_public_key_certificate_instance_encrypt = true;
|
||
} elseif (!empty($this->config['huawei_public_key_for_sessionkey'])) {
|
||
// 文件模式
|
||
$huawei_public_key_encrypt_path = realpath($cert_base_path . $this->config['huawei_public_key_for_sessionkey']);
|
||
if (!$huawei_public_key_encrypt_path) {
|
||
throw new \Exception('华为平台支付服务加密证书文件不存在: ' . $this->config['huawei_public_key_for_sessionkey']);
|
||
}
|
||
$huawei_public_key_encrypt_content = file_get_contents($huawei_public_key_encrypt_path);
|
||
$this->huawei_public_key_certificate_instance_encrypt = openssl_pkey_get_public($huawei_public_key_encrypt_content);
|
||
if (!$this->huawei_public_key_certificate_instance_encrypt) {
|
||
throw new \Exception('加载华为平台支付服务加密证书失败');
|
||
}
|
||
$this->has_huawei_public_key_certificate_instance_encrypt = true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 析构函数,释放证书资源
|
||
*/
|
||
public function __destruct()
|
||
{
|
||
// 释放证书资源
|
||
if ($this->private_key_certificate_instance) {
|
||
if (is_resource($this->private_key_certificate_instance)) {
|
||
openssl_free_key($this->private_key_certificate_instance);
|
||
}
|
||
}
|
||
if ($this->huawei_public_key_certificate_instance) {
|
||
if (is_resource($this->huawei_public_key_certificate_instance)) {
|
||
openssl_free_key($this->huawei_public_key_certificate_instance);
|
||
}
|
||
}
|
||
if ($this->huawei_public_key_certificate_instance_encrypt) {
|
||
if (is_resource($this->huawei_public_key_certificate_instance_encrypt)) {
|
||
openssl_free_key($this->huawei_public_key_certificate_instance_encrypt);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 通用生成签名
|
||
* @param array $params 请求参数
|
||
* @param resource $private_key_certificate_instance 商户应用私钥证书实例
|
||
* @param array $excludeParams 要排除的参数键名数组
|
||
* @param string $signType 签名类型,默认RSA2
|
||
*
|
||
* @return string 签名结果
|
||
* @notes
|
||
* (1)签名类型根据$signType参数确定,默认RSA2
|
||
* (2)RSA2使用SHA256WithRSA算法,RSA使用SHA1WithRSA算法
|
||
* (3)SM2使用SM3WithSM2算法
|
||
*
|
||
* 签名使用的私钥是商户私有,商户公钥已经上传到华为支付平台
|
||
*/
|
||
private function generateSign($params, $private_key_certificate_instance, $excludeParams = [], $signType = 'RSA2')
|
||
{
|
||
// 深拷贝参数,避免修改原参数
|
||
$signParams = $params;
|
||
|
||
// 移除空值和签名参数
|
||
$signParams = array_filter($signParams, function ($value) {
|
||
return $value !== null && $value !== '';
|
||
});
|
||
|
||
// 移除指定的参数
|
||
foreach ($excludeParams as $key) {
|
||
if (array_key_exists($key, $signParams)) {
|
||
unset($signParams[$key]);
|
||
}
|
||
}
|
||
|
||
// 按键名排序
|
||
ksort($signParams);
|
||
|
||
// 拼接参数,华为支付文档要求的格式
|
||
$stringToSign = '';
|
||
foreach ($signParams as $key => $value) {
|
||
// 确保值为字符串类型
|
||
$value = (string) $value;
|
||
$stringToSign .= $key . '=' . $value . '&';
|
||
}
|
||
$stringToSign = rtrim($stringToSign, '&');
|
||
|
||
// 签名结果
|
||
$sign = '';
|
||
$signatureAlgo = '';
|
||
|
||
// 根据签名类型生成签名
|
||
switch ($signType) {
|
||
case 'RSA2':
|
||
$signatureAlgo = OPENSSL_ALGO_SHA256;
|
||
break;
|
||
case 'RSA':
|
||
$signatureAlgo = OPENSSL_ALGO_SHA1;
|
||
break;
|
||
case 'SM2':
|
||
$signatureAlgo = OPENSSL_ALGO_SM3;
|
||
break;
|
||
default:
|
||
throw new \Exception('不支持的签名类型: ' . $signType);
|
||
}
|
||
|
||
// 检查私钥证书实例是否有效
|
||
if (!is_resource($private_key_certificate_instance)) {
|
||
throw new \Exception('私钥证书实例无效');
|
||
}
|
||
|
||
// 生成签名
|
||
if (!openssl_sign($stringToSign, $sign, $private_key_certificate_instance, $signatureAlgo)) {
|
||
$error = openssl_error_string();
|
||
throw new \Exception('签名生成失败: ' . $error);
|
||
}
|
||
|
||
// Base64编码并URL编码
|
||
return urlencode(base64_encode($sign));
|
||
}
|
||
|
||
/**
|
||
* 通用验证签名
|
||
* @param array $params 响应参数
|
||
* @param resource $publicKeyInstance 公钥证书实例
|
||
* @param array $excludeParams 要排除的参数键名数组
|
||
* @param string $signType 签名类型,默认RSA2
|
||
* @return bool 验证结果
|
||
*/
|
||
public function verifySign($params, $publicKeyInstance, $excludeParams = ['sign'], $signType = 'RSA2')
|
||
{
|
||
// 保存签名
|
||
$sign = $params['sign'] ?? '';
|
||
if (empty($sign)) {
|
||
return false;
|
||
}
|
||
|
||
// 深拷贝参数,避免修改原参数
|
||
$verifyParams = $params;
|
||
|
||
// 移除指定的参数
|
||
foreach ($excludeParams as $key) {
|
||
if (array_key_exists($key, $verifyParams)) {
|
||
unset($verifyParams[$key]);
|
||
}
|
||
}
|
||
|
||
// 如果有sign_type参数,使用它覆盖默认值
|
||
if (isset($verifyParams['sign_type'])) {
|
||
$signType = $verifyParams['sign_type'];
|
||
}
|
||
|
||
// 移除空值
|
||
$verifyParams = array_filter($verifyParams, function ($value) {
|
||
return $value !== null && $value !== '';
|
||
});
|
||
|
||
// 按键名排序
|
||
ksort($verifyParams);
|
||
|
||
// 拼接参数
|
||
$stringToSign = '';
|
||
foreach ($verifyParams as $key => $value) {
|
||
$stringToSign .= $key . '=' . $value . '&';
|
||
}
|
||
$stringToSign = rtrim($stringToSign, '&');
|
||
|
||
// 验证公钥证书实例是否有效
|
||
if (!is_resource($publicKeyInstance)) {
|
||
throw new \Exception('公钥证书实例无效');
|
||
}
|
||
|
||
// 检查公钥类型是否和签名类型匹配
|
||
$keyDetails = openssl_pkey_get_details($publicKeyInstance);
|
||
if ($signType === 'SM2' && ($keyDetails['type'] !== OPENSSL_KEYTYPE_EC || $keyDetails['ec']['curve_name'] !== 'sm2p256v1')) {
|
||
throw new \Exception('SM2签名类型要求使用SM2公钥');
|
||
} elseif ($signType === 'RSA2' && $keyDetails['type'] !== OPENSSL_KEYTYPE_RSA) {
|
||
throw new \Exception('RSA2签名类型要求使用RSA公钥');
|
||
} elseif ($signType === 'RSA' && $keyDetails['type'] !== OPENSSL_KEYTYPE_RSA) {
|
||
throw new \Exception('RSA签名类型要求使用RSA公钥');
|
||
}
|
||
|
||
// 验证签名
|
||
$result = 0;
|
||
|
||
// 先进行url解码,再进行base64解码(因为generateSign方法中先base64编码再url编码)
|
||
$decoded_sign = base64_decode(urldecode($sign));
|
||
|
||
// 根据签名类型选择不同的验证方法
|
||
switch ($signType) {
|
||
case 'SM2':
|
||
// 使用SM2算法验证签名
|
||
$result = openssl_verify($stringToSign, $decoded_sign, $publicKeyInstance, OPENSSL_ALGO_SM3);
|
||
break;
|
||
case 'RSA2':
|
||
// 使用RSA2算法验证签名
|
||
$result = openssl_verify($stringToSign, $decoded_sign, $publicKeyInstance, OPENSSL_ALGO_SHA256);
|
||
break;
|
||
case 'RSA':
|
||
// 使用RSA算法验证签名
|
||
$result = openssl_verify($stringToSign, $decoded_sign, $publicKeyInstance, OPENSSL_ALGO_SHA1);
|
||
break;
|
||
default:
|
||
// 默认使用RSA2算法
|
||
$result = openssl_verify($stringToSign, $decoded_sign, $publicKeyInstance, OPENSSL_ALGO_SHA256);
|
||
break;
|
||
}
|
||
|
||
return $result === 1;
|
||
}
|
||
|
||
/**
|
||
* 验证华为支付中心服务器返回的签名
|
||
* @param array $params 响应参数
|
||
* @return bool 验证结果
|
||
*/
|
||
private function verifySignFromHuawei($params)
|
||
{
|
||
// 验证签名
|
||
return $this->verifySign($params, $this->huawei_public_key_certificate_instance, ['sign'], $params['sign_type'] ?? 'SM2');
|
||
}
|
||
|
||
/**
|
||
* 发送HTTP请求
|
||
* @param string $url 请求地址
|
||
* @param array $params 请求参数
|
||
* @param $param["out_trade_no"] 订单号,商户订单号
|
||
* @param $param["subject"] 订单标题,商品简称
|
||
* @param $param["total_amount"] 订单金额(单位:分)
|
||
* @param $param["bizType"] 业务类型,默认值:100001
|
||
* @param $param["notify_url"] 回调URL, 异步通知商户服务器的URL,用于接收支付结果通知。
|
||
* @param string $method 请求方法 可选值:POST、GET
|
||
* @return array 响应结果
|
||
* @throws \Exception 异常信息
|
||
* @doc
|
||
* - [API 参考](https://developer.huawei.com/consumer/cn/doc/HMSCore-References/api-android-pre-pay-order-0000001589121249)
|
||
*/
|
||
private function httpRequest($url, $params, $method = 'POST')
|
||
{
|
||
$ch = curl_init();
|
||
|
||
// 根据场景方法验证必要参数
|
||
$scene_method = $params['method'] ?? ''; // 场景方法,如:h5pay.createPayment
|
||
$requiredParams = []; // 场景方法必填参数
|
||
|
||
// 根据不同的场景方法设置不同的必填参数, 同时设置参数映射转义
|
||
switch ($scene_method) {
|
||
case 'h5pay.createPayment':
|
||
case 'quick_app_pay.createPayment':
|
||
case 'fa_pay.createPayment':
|
||
case 'apppay.createPayment':
|
||
// 应用支付必填参数
|
||
$requiredParams = ['out_trade_no', 'subject', 'total_amount', 'notify_url', 'bizType'];
|
||
|
||
// 参数映射转义
|
||
$params['mercOrderNo'] = $params['out_trade_no']; // 订单号,商户订单号
|
||
$params['tradeSummary'] = $params['subject']; // 订单标题,商品简称
|
||
$params['totalAmount'] = $params['total_amount'] * 100; // 订单金额(单位:分)
|
||
$params['callbackUrl'] = $params['notify_url']; // 回调URL
|
||
$params['currency'] = $params['currency'] ?? "CNY"; // 货币类型,默认值:CNY
|
||
$params['bizType'] = $params['bizType'] ?? "100001"; // (100001:虚拟商品购买,100002:实物商品购买,100003:预付类账号充值,100004:航旅交通服务,100005:活动票务订购,100006:商业服务消费,100007:生活服务消费,100008:租金缴纳,100009:会员费缴纳,100011:其他商家消费,100037:公共便民服务)
|
||
|
||
// 可选参数
|
||
$params['payload'] = $params['payload'] ?? ""; // 业务扩展参数,JSON格式字符串,用于传递业务相关信息。
|
||
|
||
break;
|
||
case 'unifiedorder.query':
|
||
// 查询订单只需要out_trade_no或trade_no中的一个
|
||
break;
|
||
case 'unifiedorder.close':
|
||
case 'refund.apply':
|
||
$requiredParams = ['out_trade_no'];
|
||
break;
|
||
}
|
||
|
||
// 验证必填参数
|
||
foreach ($requiredParams as $param) {
|
||
if (!isset($params[$param]) || empty($params[$param])) {
|
||
throw new \Exception('缺少必要参数: ' . $param);
|
||
}
|
||
}
|
||
|
||
// 根据华为支付文档,添加必要的公共参数
|
||
$params['appId'] = $this->config['app_id']; // 应用ID,商户在华为支付平台注册的应用ID
|
||
$params['mercNo'] = $this->config['merc_no']; // 商户号,商户在华为支付平台注册的商户号
|
||
|
||
// 生成签名
|
||
$params['sign_type'] = $this->signType;
|
||
$params['sign'] = $this->generateSign($params, $this->private_key_certificate_instance, ['sign'], $this->signType);
|
||
|
||
// 根据$method参数设置请求方式
|
||
$method = strtoupper($method);
|
||
if ($method == 'POST') {
|
||
curl_setopt($ch, CURLOPT_POST, 1);
|
||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
|
||
} elseif ($method == 'GET') {
|
||
// GET请求时将参数拼接到URL中
|
||
$url = $url . '?' . http_build_query($params);
|
||
curl_setopt($ch, CURLOPT_HTTPGET, 1);
|
||
} else {
|
||
// 支持其他HTTP方法
|
||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
|
||
}
|
||
|
||
// 添加调试信息
|
||
curl_setopt($ch, CURLOPT_VERBOSE, true);
|
||
$verbose = fopen('php://temp', 'rw+');
|
||
curl_setopt($ch, CURLOPT_STDERR, $verbose);
|
||
|
||
// 参考华为支付文档,设置PayMercAuth头
|
||
// 注意:华为支付SDK v2.0及以上版本需要此头部
|
||
// 参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/payment-prepay-V5
|
||
if (isset($this->config['mch_auth_id']) && !empty($this->config['mch_auth_id'])) {
|
||
$traceId = isset($params['traceId']) ? $params['traceId'] : uniqid();
|
||
|
||
$pay_merc_auth_part = [
|
||
'callerId' => $this->config['merc_no'], // 商户号,商户在华为支付平台注册的商户号
|
||
'traceId' => $traceId, // 调用方请求ID,用于跟踪请求,建议使用UUID格式
|
||
'time' => intval(microtime(true) * 1000), // 时间戳,格式:毫秒级时间戳
|
||
'authId' => $this->config['mch_auth_id'], // 商户认证ID,商户在华为支付平台注册的商户认证ID
|
||
'bodySign' => $params['sign'] // 直接使用已生成的签名
|
||
];
|
||
|
||
// 对PayMercAuthPart进行签名
|
||
$pay_merc_auth_part['headerSign'] = $this->generateSign($pay_merc_auth_part, $this->private_key_certificate_instance, ['headerSign'], $this->signType);
|
||
|
||
// 设置请求头
|
||
$headers = [
|
||
'Content-Type: application/x-www-form-urlencoded; charset=utf-8',
|
||
'PayMercAuth: ' . json_encode($pay_merc_auth_part),
|
||
];
|
||
} else {
|
||
// 不使用PayMercAuth头的情况
|
||
$headers = [
|
||
'Content-Type: application/x-www-form-urlencoded; charset=utf-8',
|
||
];
|
||
}
|
||
|
||
// 设置curl选项
|
||
curl_setopt($ch, CURLOPT_URL, $url);
|
||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||
|
||
// 在生产环境中应该启用SSL验证
|
||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||
|
||
$response = curl_exec($ch);
|
||
|
||
if (curl_errno($ch)) {
|
||
throw new \Exception('HTTP请求错误: ' . curl_error($ch));
|
||
}
|
||
|
||
|
||
// 获取调试信息
|
||
$verboseLog = '';
|
||
if ($this->show_curl_detail) {
|
||
rewind($verbose);
|
||
$verboseLog = stream_get_contents($verbose);
|
||
fclose($verbose);
|
||
}
|
||
|
||
curl_close($ch);
|
||
|
||
// 添加调试日志
|
||
if (!empty($verboseLog) && $this->show_curl_detail) {
|
||
error_log('cURL Debug Log: ' . $verboseLog);
|
||
// 也可以将调试日志输出到控制台,方便测试
|
||
echo "\n - cURL Debug Log:\n";
|
||
echo " " . str_replace("\n", "\n ", $verboseLog) . "\n";
|
||
}
|
||
|
||
// 解析响应
|
||
$result = json_decode($response, true);
|
||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||
// 尝试解析为URL编码格式
|
||
parse_str($response, $result);
|
||
if (empty($result)) {
|
||
throw new \Exception('响应解析失败: ' . json_last_error_msg());
|
||
}
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 元服务支付
|
||
* @param array $params 支付参数
|
||
* @param string $notifyUrl 异步回调地址
|
||
* @return array 支付结果
|
||
*/
|
||
public function faPay($params, $notifyUrl)
|
||
{
|
||
// 检查必要参数
|
||
if (empty($params['out_trade_no'])) {
|
||
throw new \Exception('缺少必要参数:out_trade_no');
|
||
}
|
||
if (empty($params['subject'])) {
|
||
throw new \Exception('缺少必要参数:subject');
|
||
}
|
||
if (!isset($params['total_amount']) || $params['total_amount'] <= 0) {
|
||
throw new \Exception('缺少或无效的参数:total_amount');
|
||
}
|
||
if (empty($notifyUrl)) {
|
||
throw new \Exception('缺少必要参数:notify_url');
|
||
}
|
||
|
||
$requestParams = [
|
||
'method' => 'fa_pay.createPayment',
|
||
'version' => '1.0',
|
||
'timestamp' => date('Y-m-d H:i:s'),
|
||
'out_trade_no' => $params['out_trade_no'],
|
||
'subject' => mb_substr($params['subject'], 0, 128, 'UTF-8'), // 限制长度
|
||
'body' => isset($params['body']) ? mb_substr($params['body'], 0, 512, 'UTF-8') : mb_substr($params['subject'], 0, 512, 'UTF-8'),
|
||
'total_amount' => number_format($params['total_amount'], 2, '.', ''), // 确保格式正确
|
||
'scene' => 'quick_app',
|
||
'notify_url' => $notifyUrl,
|
||
];
|
||
|
||
// 可选参数,根据文档添加
|
||
if (isset($params['currency'])) {
|
||
$requestParams['currency'] = $params['currency'];
|
||
}
|
||
if (isset($params['client_ip'])) {
|
||
$requestParams['client_ip'] = $params['client_ip'];
|
||
}
|
||
if (isset($params['timeout_express'])) {
|
||
$requestParams['timeout_express'] = $params['timeout_express'];
|
||
}
|
||
if (isset($params['attach'])) {
|
||
$requestParams['attach'] = mb_substr($params['attach'], 0, 128, 'UTF-8');
|
||
}
|
||
|
||
$url = $this->gatewayUrl . '/api/v2/aggr/preorder/create/fa';
|
||
return $this->httpRequest($url, $requestParams);
|
||
}
|
||
|
||
/**
|
||
* 快应用支付
|
||
* @param array $params 支付参数
|
||
* @param string $notifyUrl 异步回调地址
|
||
* @return array 支付结果
|
||
*/
|
||
public function quickAppPay($params, $notifyUrl)
|
||
{
|
||
// 检查必要参数
|
||
if (empty($params['out_trade_no'])) {
|
||
throw new \Exception('缺少必要参数:out_trade_no');
|
||
}
|
||
if (empty($params['subject'])) {
|
||
throw new \Exception('缺少必要参数:subject');
|
||
}
|
||
if (!isset($params['total_amount']) || $params['total_amount'] <= 0) {
|
||
throw new \Exception('缺少或无效的参数:total_amount');
|
||
}
|
||
if (empty($notifyUrl)) {
|
||
throw new \Exception('缺少必要参数:notify_url');
|
||
}
|
||
|
||
$requestParams = [
|
||
'method' => 'quick_app_pay.createPayment',
|
||
'version' => '1.0',
|
||
'timestamp' => date('Y-m-d H:i:s'),
|
||
'out_trade_no' => $params['out_trade_no'],
|
||
'subject' => mb_substr($params['subject'], 0, 128, 'UTF-8'), // 限制长度
|
||
'body' => isset($params['body']) ? mb_substr($params['body'], 0, 512, 'UTF-8') : mb_substr($params['subject'], 0, 512, 'UTF-8'),
|
||
'total_amount' => number_format($params['total_amount'], 2, '.', ''), // 确保格式正确
|
||
'scene' => 'quick_app',
|
||
'notify_url' => $notifyUrl,
|
||
];
|
||
|
||
// 可选参数,根据文档添加
|
||
if (isset($params['currency'])) {
|
||
$requestParams['currency'] = $params['currency'];
|
||
}
|
||
if (isset($params['client_ip'])) {
|
||
$requestParams['client_ip'] = $params['client_ip'];
|
||
}
|
||
if (isset($params['timeout_express'])) {
|
||
$requestParams['timeout_express'] = $params['timeout_express'];
|
||
}
|
||
if (isset($params['attach'])) {
|
||
$requestParams['attach'] = mb_substr($params['attach'], 0, 128, 'UTF-8');
|
||
}
|
||
|
||
$url = $this->gatewayUrl . '/api/v2/aggr/preorder/create/quick-app';
|
||
return $this->httpRequest($url, $requestParams);
|
||
}
|
||
|
||
/**
|
||
* H5支付
|
||
* @param array $params 支付参数
|
||
* @param string $notifyUrl 异步回调地址,用于华为支付服务器调用,将用户支付成功的消息通知给商户服务器。
|
||
* @param string $returnUrl (可选参数)支付成功后跳转地址,用于用户支付收银台支付完成后重定向的URL地址。
|
||
* @return array 支付结果
|
||
* @doc
|
||
* - [H5支付说明文档](https://developer.huawei.com/consumer/cn/doc/HMSCore-Guides/h5-dev-0000001537334464#section28465232319)
|
||
*/
|
||
public function h5Pay($params, $notifyUrl, $returnUrl = '')
|
||
{
|
||
// 检查必要参数
|
||
if (empty($params['out_trade_no'])) {
|
||
throw new \Exception('缺少必要参数:out_trade_no');
|
||
}
|
||
if (empty($params['subject'])) {
|
||
throw new \Exception('缺少必要参数:subject');
|
||
}
|
||
if (!isset($params['total_amount']) || $params['total_amount'] <= 0) {
|
||
throw new \Exception('缺少或无效的参数:total_amount');
|
||
}
|
||
if (empty($notifyUrl)) {
|
||
throw new \Exception('缺少必要参数:notify_url');
|
||
}
|
||
|
||
$requestParams = [
|
||
'method' => 'h5pay.createPayment',
|
||
'version' => '1.0',
|
||
'timestamp' => date('Y-m-d H:i:s'),
|
||
'out_trade_no' => $params['out_trade_no'],
|
||
'subject' => mb_substr($params['subject'], 0, 128, 'UTF-8'), // 限制长度
|
||
'body' => isset($params['body']) ? mb_substr($params['body'], 0, 512, 'UTF-8') : mb_substr($params['subject'], 0, 512, 'UTF-8'),
|
||
'total_amount' => $params['total_amount'], // 确保格式正确
|
||
'scene' => 'h5',
|
||
'notify_url' => $notifyUrl,
|
||
];
|
||
|
||
// 判断是否有return_url参数
|
||
if (!empty($returnUrl)) {
|
||
$requestParams['return_url'] = $returnUrl;
|
||
}
|
||
|
||
// 可选参数,根据文档添加
|
||
// 参照:https://developer.huawei.com/consumer/cn/doc/HMSCore-References/api-h5-pre-pay-order-0000001538376420
|
||
if (isset($params['currency']) && $params['currency'] !== 'CNY') {
|
||
$requestParams['currency'] = $params['currency'];
|
||
}
|
||
if (isset($params['expireTime'])) {
|
||
$requestParams['expire_time'] = $params['expireTime'];
|
||
}
|
||
|
||
$url = $this->gatewayUrl . '/api/v2/aggr/preorder/create/h5';
|
||
return $this->httpRequest($url, $requestParams);
|
||
}
|
||
|
||
|
||
/**
|
||
* APP支付
|
||
* @param array $params 支付参数
|
||
* @param string $notifyUrl 异步回调地址
|
||
* @return array 支付结果
|
||
*/
|
||
public function appPay($params, $notifyUrl)
|
||
{
|
||
// 检查必要参数
|
||
if (empty($params['out_trade_no'])) {
|
||
throw new \Exception('缺少必要参数:out_trade_no');
|
||
}
|
||
if (empty($params['subject'])) {
|
||
throw new \Exception('缺少必要参数:subject');
|
||
}
|
||
if (!isset($params['total_amount']) || $params['total_amount'] <= 0) {
|
||
throw new \Exception('缺少或无效的参数:total_amount');
|
||
}
|
||
if (empty($notifyUrl)) {
|
||
throw new \Exception('缺少必要参数:notify_url');
|
||
}
|
||
|
||
$requestParams = [
|
||
'method' => 'apppay.createPayment',
|
||
'version' => '1.0',
|
||
'timestamp' => date('Y-m-d H:i:s'),
|
||
'out_trade_no' => $params['out_trade_no'],
|
||
'subject' => mb_substr($params['subject'], 0, 128, 'UTF-8'), // 限制长度
|
||
'body' => isset($params['body']) ? mb_substr($params['body'], 0, 512, 'UTF-8') : mb_substr($params['subject'], 0, 512, 'UTF-8'),
|
||
'total_amount' => number_format($params['total_amount'], 2, '.', ''), // 确保格式正确
|
||
'notify_url' => $notifyUrl,
|
||
];
|
||
|
||
// 可选参数,根据文档添加
|
||
if (isset($params['currency'])) {
|
||
$requestParams['currency'] = $params['currency'];
|
||
}
|
||
if (isset($params['timeout_express'])) {
|
||
$requestParams['timeout_express'] = $params['timeout_express'];
|
||
}
|
||
if (isset($params['attach'])) {
|
||
$requestParams['attach'] = $params['attach'];
|
||
}
|
||
|
||
$url = $this->gatewayUrl . '/api/v2/aggr/preorder/create/app';
|
||
return $this->httpRequest($url, $requestParams);
|
||
}
|
||
|
||
/**
|
||
* 查询订单
|
||
* @param array $params 查询参数
|
||
* @return array 查询结果
|
||
*/
|
||
public function queryOrder($params)
|
||
{
|
||
$requestParams = [
|
||
'method' => 'unifiedorder.query',
|
||
'version' => '1.0',
|
||
'timestamp' => date('Y-m-d H:i:s'),
|
||
];
|
||
|
||
// 支持通过商户订单号或华为交易号查询
|
||
if (isset($params['out_trade_no'])) {
|
||
$requestParams['out_trade_no'] = $params['out_trade_no'];
|
||
} elseif (isset($params['trade_no'])) {
|
||
$requestParams['trade_no'] = $params['trade_no'];
|
||
} else {
|
||
throw new \Exception('查询参数错误,必须提供out_trade_no或trade_no');
|
||
}
|
||
|
||
return $this->httpRequest($this->gatewayUrl, $requestParams);
|
||
}
|
||
|
||
/**
|
||
* 关闭订单
|
||
* @param array $params 关闭参数
|
||
* @return array 关闭结果
|
||
*/
|
||
public function closeOrder($params)
|
||
{
|
||
$requestParams = [
|
||
'method' => 'unifiedorder.close',
|
||
'version' => '1.0',
|
||
'timestamp' => date('Y-m-d H:i:s'),
|
||
'out_trade_no' => $params['out_trade_no'],
|
||
];
|
||
|
||
return $this->httpRequest($this->gatewayUrl, $requestParams);
|
||
}
|
||
|
||
/**
|
||
* 退款
|
||
* @param array $params 退款参数
|
||
* @return array 退款结果
|
||
*/
|
||
public function refund($params)
|
||
{
|
||
// 检查必要参数
|
||
if (!isset($params['out_trade_no']) && !isset($params['trade_no'])) {
|
||
throw new \Exception('退款请求必须提供out_trade_no或trade_no');
|
||
}
|
||
if (!isset($params['out_request_no'])) {
|
||
throw new \Exception('退款请求必须提供out_request_no');
|
||
}
|
||
if (!isset($params['refund_amount']) || $params['refund_amount'] <= 0) {
|
||
throw new \Exception('退款金额必须大于0');
|
||
}
|
||
|
||
$requestParams = [
|
||
'method' => 'refund.apply',
|
||
'version' => '1.0',
|
||
'timestamp' => date('Y-m-d H:i:s'),
|
||
'refund_amount' => $params['refund_amount'],
|
||
'out_request_no' => $params['out_request_no'],
|
||
'refund_reason' => $params['refund_reason'] ?? '',
|
||
];
|
||
|
||
// 添加订单标识(二选一)
|
||
if (isset($params['out_trade_no'])) {
|
||
$requestParams['out_trade_no'] = $params['out_trade_no'];
|
||
}
|
||
if (isset($params['trade_no'])) {
|
||
$requestParams['trade_no'] = $params['trade_no'];
|
||
}
|
||
|
||
return $this->httpRequest($this->gatewayUrl, $requestParams);
|
||
}
|
||
|
||
/**
|
||
* 验证回调签名
|
||
* @param array $params 回调参数
|
||
* @return bool 验证结果
|
||
*/
|
||
public function verifyNotify($params)
|
||
{
|
||
return $this->verifySignFromHuawei($params);
|
||
}
|
||
} |