From 348de5cb19427f73a4068edb6f3cd0fb8ba500de Mon Sep 17 00:00:00 2001 From: JING Date: Mon, 15 Jun 2026 16:35:40 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=B8=8A=E4=BC=A0=E7=9A=84?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/factory/object/onelevel/UploadRequest.php | 33 +---- src/http/impl/HFPayHttpClient.php | 136 +++++++++++++----- 2 files changed, 99 insertions(+), 70 deletions(-) diff --git a/src/factory/object/onelevel/UploadRequest.php b/src/factory/object/onelevel/UploadRequest.php index 3a82480..cc87528 100644 --- a/src/factory/object/onelevel/UploadRequest.php +++ b/src/factory/object/onelevel/UploadRequest.php @@ -66,37 +66,6 @@ class UploadRequest if(empty($this->data['handle_type'])) { throw new ParamsException('上传类型不能为空', 'handle_type'); } - - // 构建 multipart 数据结构 - $multipartData = [ - [ - 'name' => 'attach_file', // 这里的 'file' 是接口定义的文件字段名 - 'contents' => fopen($this->data['attach_file'], 'r'), // 使用 fopen 打开文件流 - ], - // 可以同时上传其他文本字段 - [ - 'name' => 'attach_no', - 'contents' => $this->data['attach_no'], - ], - [ - 'name' => 'trans_type', - 'contents' => $this->data['trans_type'], - ], - [ - 'name' => 'attach_type', - 'contents' => $this->data['attach_type'], - ], - [ - 'name' => 'attach_desc', - 'contents' => $this->data['attach_desc'], - ], - [ - 'name' => 'handle_type', - 'contents' => $this->data['handle_type'], - ], - ]; - - - return $multipartData; + return $this->data; } } \ No newline at end of file diff --git a/src/http/impl/HFPayHttpClient.php b/src/http/impl/HFPayHttpClient.php index 8286b23..53bebee 100644 --- a/src/http/impl/HFPayHttpClient.php +++ b/src/http/impl/HFPayHttpClient.php @@ -114,7 +114,61 @@ class HFPayHttpClient implements IHttpClient */ private ISigner $signClient; + /** + * 准备 Multipart 数据 + * 核心原则:文件流只进 multipart,不进 signData;文本字段两者都进 + */ + private function _prepareMultipartData(array $body): array + { + $multipart = []; + $signData = []; + + foreach ($body as $key => $value) { + // 1. 资源类型(文件流):仅放入 multipart,绝不参与签名 + if (is_resource($value)) { + $multipart[] = [ + 'name' => $key, + 'contents' => $value, + ]; + continue; + } + + // 2. 已是 Guzzle Multipart 格式的数组 + if (is_array($value) && isset($value['contents'])) { + $multipart[] = $value; + // 仅当 contents 非资源时才参与签名 + if (!is_resource($value['contents'])) { + $signData[$key] = (string)$value['contents']; + } + continue; + } + + // 3. 普通数组:转 JSON 字符串 + if (is_array($value)) { + $jsonStr = json_encode($value, JSON_UNESCAPED_UNICODE); + $multipart[] = ['name' => $key, 'contents' => $jsonStr]; + $signData[$key] = $jsonStr; + continue; + } + // 4. NULL 值:转为空字符串(汇付通常要求空值也参与签名) + if ($value === null) { + $multipart[] = ['name' => $key, 'contents' => '']; + $signData[$key] = ''; + continue; + } + + // 5. 标量类型:强制转为字符串 + $strVal = (string)$value; + $multipart[] = ['name' => $key, 'contents' => $strVal]; + $signData[$key] = $strVal; + } + + return [ + 'multipart' => $multipart, + 'sign_data' => $signData, + ]; + } /** * @param string $method * @param string $uri @@ -131,64 +185,70 @@ class HFPayHttpClient implements IHttpClient { $options = $this->getRequestOptions(); - if(!empty($query)) { + if (!empty($query)) { $options['query'] = array_filter($query, fn($v) => $v !== null); } - //添加固定的参数 - //版本号 - if(empty($body['version'])) { - $body['version'] = $this->config['version'] ?? ''; + + // ✅ 关键修正:固定参数必须在签名之前注入到 $body 中 + // 这样无论是 form/json 还是 multipart,都能保证这些字段参与签名 + if (empty($body['version'])) { + $body['version'] = $this->config['version'] ?? self::API_VERSION; } - if(empty($body['mer_cust_id'])) { + if (empty($body['mer_cust_id'])) { $body['mer_cust_id'] = $this->config['mid'] ?? ''; } - if(empty($body['bg_ret_url']) && !empty($this->config['callback'])) { + if (empty($body['bg_ret_url']) && !empty($this->config['callback'])) { $body['bg_ret_url'] = $this->config['callback']; } - //将内容生成签名 - $sign = $this->signClient->sign($body); - $body['check_value'] = $sign; + if ($format === 'multipart') { + // 1. 分离文件流和文本参数 + $processed = $this->_prepareMultipartData($body); -// echo '请求参数 #####'; -// print_r($body); -// echo '请求参数 end'; - if(empty($format)) { - $format = 'form'; - } - switch ($format) { - case 'json': - $options['json'] = array_filter($body, fn($v) => $v !== null); - break; - case 'form': - $options['form_params'] = array_filter($body, fn($v) => $v !== null); - break; - case 'body': - $options['body'] = array_filter($body, fn($v) => $v !== null); - break; - // 新增的分支 - case 'multipart': - $options['multipart'] = array_filter($body, fn($v) => $v !== null); - break; - } + // 2. 使用纯文本参数生成签名(文件流已被排除) + $sign = $this->signClient->sign($processed['sign_data']); + + // 3. 将签名结果作为普通文本字段追加到 multipart + $processed['multipart'][] = [ + 'name' => 'check_value', + 'contents' => $sign, + ]; + + $options['multipart'] = $processed['multipart']; + } else { + // 非 multipart 逻辑保持不变 + $sign = $this->signClient->sign($body); + $body['check_value'] = $sign; + switch ($format) { + case 'json': + $options['json'] = array_filter($body, fn($v) => $v !== null); + break; + case 'form': + $options['form_params'] = array_filter($body, fn($v) => $v !== null); + break; + case 'body': + $options['body'] = json_encode(array_filter($body, fn($v) => $v !== null), JSON_UNESCAPED_UNICODE); + break; + } + } try { $resp = $this->client->request($method, $uri, $options); } catch (GuzzleException $e) { throw new ApiException($e->getMessage()); } - $response = Response::make($resp); - if(!$response->isSuccess()) { + $response = Response::make($resp); + if (!$response->isSuccess()) { throw new ApiException('汇付反馈状态不正确', $resp->getStatusCode()); } - //验证数据 - $body = $response->getBody(); - if(!$this->signClient->verify($body)) { + + $respBody = $response->getBody(); + if (!$this->signClient->verify($respBody)) { throw new ApiException('签名验证未通过'); } - //解码 - $decodeBody = $this->signClient->decode($body['check_value']); + + $decodeBody = $this->signClient->decode($respBody['check_value']); ErrCodeHelper::parseError($decodeBody['resp_code'], $decodeBody['resp_desc']); return Response::tempInit(200, $decodeBody); }