diff --git a/.gitignore b/.gitignore index 196d49e68..99b9dd57e 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,8 @@ src/cache src/temp src/tmp src/attachments + +# 数据库 +mysql_db_data +redis_data +xdebug_logs diff --git a/docker-compose.yml b/docker-compose.yml index b050124fb..db73c3dfd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -119,9 +119,9 @@ services: - "com.docker.compose.project.working_dir=${PROJECT_NAME}" volumes: - mysql_db_data: - redis_data: - xdebug_logs: + mysql_db_data: ./mysql_db_data + redis_data: ./redis_data + xdebug_logs: ./xdebug_logs networks: sass-platform-net: diff --git a/src/addon/huaweipay/data/sdk/HuaweiPayClient.php b/src/addon/huaweipay/data/sdk/HuaweiPayClient.php index 6e4bf3e9f..34c4be610 100644 --- a/src/addon/huaweipay/data/sdk/HuaweiPayClient.php +++ b/src/addon/huaweipay/data/sdk/HuaweiPayClient.php @@ -6,6 +6,7 @@ namespace addon\huaweipay\data\sdk; +use app\exception\ApiException; use think\facade\Log; class HuaweiPayClient @@ -18,6 +19,18 @@ class HuaweiPayClient // 签名算法 private $signType = 'RSA2'; + + // 商户应用私有证书实例 + private $private_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; /** * 构造函数 @@ -26,6 +39,46 @@ class HuaweiPayClient public function __construct($config) { $this->config = $config; + + // 证书基础路径 + $cert_base_path = app()->getRootPath(); + + // 加载商户应用私有证书 + $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); + $this->private_key_certificate_instance = openssl_pkey_get_private($private_key_content); + if (!$this->private_key_certificate_instance) { + throw new \Exception('加载商户应用私有证书失败'); + } + + // 加载华为平台支付证书 + $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); + $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'])) { + $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; + } + // 根据配置设置网关地址 if (isset($config['sandbox']) && $config['sandbox']) { @@ -57,17 +110,20 @@ class HuaweiPayClient } $stringToSign = rtrim($stringToSign, '&'); + // 签名结果 + $sign = ''; + // 根据签名类型生成签名 switch ($this->signType) { case 'RSA2': - $privateKey = $this->config['private_key']; - $privateKey = "-----BEGIN PRIVATE KEY-----\n" . wordwrap($privateKey, 64, "\n", true) . "\n-----END PRIVATE KEY-----"; - openssl_sign($stringToSign, $sign, $privateKey, OPENSSL_ALGO_SHA256); + if (!openssl_sign($stringToSign, $sign, $this->private_key_certificate_instance, OPENSSL_ALGO_SHA256)) { + throw new \Exception('RSA2签名生成失败'); + } break; case 'RSA': - $privateKey = $this->config['private_key']; - $privateKey = "-----BEGIN PRIVATE KEY-----\n" . wordwrap($privateKey, 64, "\n", true) . "\n-----END PRIVATE KEY-----"; - openssl_sign($stringToSign, $sign, $privateKey, OPENSSL_ALGO_SHA1); + if (!openssl_sign($stringToSign, $sign, $this->private_key_certificate_instance, OPENSSL_ALGO_SHA1)) { + throw new \Exception('RSA签名生成失败'); + } break; default: throw new \Exception('不支持的签名类型'); @@ -85,6 +141,10 @@ class HuaweiPayClient { // 保存签名 $sign = $params['sign'] ?? ''; + if (empty($sign)) { + return false; + } + unset($params['sign']); unset($params['sign_type']); @@ -104,15 +164,14 @@ class HuaweiPayClient $stringToSign = rtrim($stringToSign, '&'); // 验证签名 - $huaweiPublicKey = $this->config['huawei_public_key']; - $huaweiPublicKey = "-----BEGIN PUBLIC KEY-----\n" . wordwrap($huaweiPublicKey, 64, "\n", true) . "\n-----END PUBLIC KEY-----"; + $result = 0; switch ($this->signType) { case 'RSA2': - $result = openssl_verify($stringToSign, base64_decode($sign), $huaweiPublicKey, OPENSSL_ALGO_SHA256); + $result = openssl_verify($stringToSign, base64_decode($sign), $this->huawei_public_key_certificate_instance, OPENSSL_ALGO_SHA256); break; case 'RSA': - $result = openssl_verify($stringToSign, base64_decode($sign), $huaweiPublicKey, OPENSSL_ALGO_SHA1); + $result = openssl_verify($stringToSign, base64_decode($sign), $this->huawei_public_key_certificate_instance, OPENSSL_ALGO_SHA1); break; default: throw new \Exception('不支持的签名类型'); @@ -131,14 +190,20 @@ class HuaweiPayClient private function httpRequest($url, $params, $method = 'POST') { $ch = curl_init(); - - // 设置请求头 - $headers = [ - 'Content-Type: application/x-www-form-urlencoded; charset=utf-8', - ]; - + // 生成签名 - $params['app_id'] = $this->config['app_id']; + // --- 必须参数 --- + $params['appId'] = $this->config['app_id']; + $params['mercNo'] = $this->config['merc_no']; + + // --- 验证以下参数必须存在 --- + $requiredParams = ['appId', 'mercNo', 'mercOrderNo', 'tradeSummary', 'totalAmount', 'callbackUrl']; + foreach ($requiredParams as $param) { + if (!isset($params[$param]) || empty($params[$param])) { + throw new \Exception('缺少必要参数: ' . $param); + } + } + $params['sign_type'] = $this->signType; $params['sign'] = $this->generateSign($params); @@ -148,6 +213,27 @@ class HuaweiPayClient } else { $url .= '?' . http_build_query($params); } + + // 参考:https://developer.huawei.com/consumer/cn/doc/HMSCore-References/api-data-model-0000001538219104#section11744172016145 + $pay_merc_auth_part = [ + 'callerId' => $this->config['merc_no'], // 商户号 + 'traceId' => $params['traceId'] ?? uniqid(), // 交易ID或跟踪ID + 'time' => date('YmdHis'), // 时间戳 + 'authId' => $this->config['auth_id'], // 商户证书编号 + 'bodySign' => $this->generateSign($params), // 对请求Body参数签名 + ]; + + $pay_merc_auth = [ + 'headerSign' => $this->generateSign($pay_merc_auth_part), // 对PayMercAuthPart参数签名 + // 其他参数直接使用PayMercAuthPart中的值,不进行修改,能用解构赋值 + ...$pay_merc_auth_part, + ]; + + // 设置请求头 + $headers = [ + 'Content-Type: application/x-www-form-urlencoded; charset=utf-8', + 'PayMercAuth: ' . json_encode($pay_merc_auth), + ]; curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); @@ -164,7 +250,14 @@ class HuaweiPayClient curl_close($ch); // 解析响应 - parse_str($response, $result); + $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; } @@ -184,16 +277,31 @@ class HuaweiPayClient 'timestamp' => date('Y-m-d H:i:s'), 'out_trade_no' => $params['out_trade_no'], 'subject' => $params['subject'], - 'total_amount' => $params['total_amount'], 'body' => $params['body'], + 'total_amount' => $params['total_amount'], + 'scene' => 'h5', 'return_url' => $returnUrl, 'notify_url' => $notifyUrl, - 'scene' => 'h5', ]; + // 可选参数,根据文档添加 + 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'] = $params['attach']; + } + return $this->httpRequest($this->gatewayUrl, $requestParams); } + /** * APP支付 * @param array $params 支付参数 @@ -208,11 +316,22 @@ class HuaweiPayClient 'timestamp' => date('Y-m-d H:i:s'), 'out_trade_no' => $params['out_trade_no'], 'subject' => $params['subject'], - 'total_amount' => $params['total_amount'], 'body' => $params['body'], + 'total_amount' => $params['total_amount'], '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']; + } + return $this->httpRequest($this->gatewayUrl, $requestParams); } diff --git a/src/addon/huaweipay/event/Pay.php b/src/addon/huaweipay/event/Pay.php index f409ede05..f3b41ee50 100644 --- a/src/addon/huaweipay/event/Pay.php +++ b/src/addon/huaweipay/event/Pay.php @@ -18,7 +18,7 @@ class Pay public function handle($param) { if ($param[ "pay_type" ] == "huaweipay") { - if (in_array($param[ "app_type" ], [ "h5", "app", "pc", "hwapp" ])) { + if (in_array($param[ "app_type" ], [ "h5", "app", "pc", "hwapp", 'weapp'])) { $pay_model = new PayModel($param[ 'site_id' ]); $res = $pay_model->pay($param); return $res; diff --git a/src/addon/huaweipay/model/Pay.php b/src/addon/huaweipay/model/Pay.php index f90c69a2a..d7f44b7f3 100644 --- a/src/addon/huaweipay/model/Pay.php +++ b/src/addon/huaweipay/model/Pay.php @@ -6,6 +6,7 @@ namespace addon\huaweipay\model; use addon\huaweipay\data\sdk\HuaweiPayClient; +use app\exception\ApiException; use app\model\BaseModel; use app\model\system\Cron; use app\model\system\Pay as PayCommon; @@ -17,7 +18,23 @@ use think\facade\Log; */ class Pay extends BaseModel { - public $hwpay_client; + /** + * 支付接口实例 + * @var + */ + private $hwpay_client; + + /** + * 支付配置 + * @var array|mixed + */ + private $config = []; + + /** + * 站点id + * @var + */ + private $site_id; /** * 构造函数 @@ -25,17 +42,16 @@ class Pay extends BaseModel */ function __construct($site_id) { - try { - // 获取华为支付参数 - $config_info = (new Config())->getPayConfig($site_id)['data']['value']; - - if (!empty($config_info)) { - // 初始化华为支付客户端 - $this->hwpay_client = new HuaweiPayClient($config_info); - } - } catch (\Exception $e) { - return $this->error('', '华为支付配置错误: ' . $e->getMessage()); - } + $this->site_id = $site_id; + + // 获取华为支付参数 + $config_info = (new Config())->getPayConfig($this->site_id)['data']['value']; + $this->config = $config_info; + if (empty($this->config)) throw new ApiException(-1, "平台未配置华为支付"); + $this->config['site_id'] = $this->site_id; + + // 初始化华为支付客户端 + $this->hwpay_client = new HuaweiPayClient($this->config); } /** diff --git a/src/addon/huaweipay/shop/controller/Pay.php b/src/addon/huaweipay/shop/controller/Pay.php index 697f54c6d..8a121ef40 100644 --- a/src/addon/huaweipay/shop/controller/Pay.php +++ b/src/addon/huaweipay/shop/controller/Pay.php @@ -23,7 +23,7 @@ class Pay extends BaseShop $mch_id = input("mch_id", "");//商户号, // PETALPAY.MERC_NO, 商户号 $private_key = input("private_key", "");//商户应用私钥, // PETALPAY.MERC_PRIVATE_KEY, 商户应用私钥 $mch_auth_id = input("mch_auth_id", "");//商户证书id, // PETALPAY.MERC_AUTH_ID, 商户证书id - $sign_type = input("sign_type", "RSA");//商户公私钥签名类型, // PETALPAY.SIGN_TYPE, 签名类型, 默认使用RSA + $sign_type = input("sign_type", "RSA2");//商户公私钥签名类型, // PETALPAY.SIGN_TYPE, 签名类型, 默认使用RSA $huawei_public_key = input("huawei_public_key", ""); //华为公钥, // PETALPAY.HW_PAY_PUBLIC_KEY_FOR_CALLBACK, 华为公钥 $huawei_public_key_for_sessionkey = input("huawei_public_key_for_sessionkey", ""); //华为公钥, // PETALPAY.HW_PAY_PUBLIC_KEY_FOR_SESSIONKEY, 华为公钥 $enable_app_types = input("enable_app_types", '');//支持端口 如web app,app // PETALPAY.ENABLE_APP_TYPES, 支持的支付端口 @@ -68,7 +68,7 @@ class Pay extends BaseShop */ public function uploadHuaweiCrt() { - $site_id = request()->siteid(); + $site_id = $this->site_id; $upload_model = new Upload($site_id); $name = input("name", "");