Files
shop-platform/src/addon/huaweipay/data/sdk/HuaweiPayClient.php

871 lines
35 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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及以下版本使用数字值18OpenSSL内部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
* 2RSA2使用SHA256WithRSA算法RSA使用SHA1WithRSA算法
* 3SM2使用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);
}
}