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); } }