config =$payConfigService->getConfig('Apcopay'); // 确保base_uri不重复包含/merchanttools $baseUrl = rtrim($this->config['api_url'], '/'); // 如果baseUrl已经包含merchanttools,则移除它 if (strpos($baseUrl, '/merchanttools') !== false) { $baseUrl = str_replace('/merchanttools', '', $baseUrl); } $this->client = new Client([ 'base_uri' => $baseUrl, 'timeout' => 30, 'verify' => false // 开发环境可能需要关闭SSL验证 ]); // 记录初始化信息 Log::info('Apcopay Client initialized', [ 'base_uri' => $baseUrl, 'config' => $this->config ]); } /** * 生成Basic认证头 */ protected function getAuthorizationHeader(): string { $credentials = base64_encode($this->config['client_id'] . ':' . $this->config['secret_key']); return 'Basic ' . $credentials; } /** * 生成签名 */ protected function generateSignature(array $data): string { // 记录签名前的数据 Log::info('Apcopay Signature Data Before', [ 'data' => $data ]); // 使用品牌密钥 - 直接使用,不做解码 $brandKey = $this->config['brand_key']; // 1. 扁平化并按字母顺序排序字段 $fields = $this->flattenAndSortFields($data); // 记录扁平化后的字段 Log::info('Apcopay Flattened Fields', [ 'fields' => $fields ]); // 2. 拼接字段 $fieldsString = $this->concatenateFields($fields); // 记录拼接后的字符串 Log::info('Apcopay Concatenated Fields', [ 'fieldsString' => $fieldsString ]); // 3. 转为小写 $lowercaseString = strtolower($fieldsString); Log::info('Apcopay Lowercase String', [ 'lowercaseString' => $lowercaseString ]); // 4. 使用HMAC SHA256计算签名 Log::info('Apcopay Brand Key Length', [ 'length' => strlen($brandKey) ]); // 生成签名 $signature = base64_encode(hash_hmac('sha256', $lowercaseString, $brandKey, true)); // 记录生成的签名 Log::info('Apcopay Generated Signature', [ 'signature' => $signature ]); return $signature; } /** * 扁平化并排序字段 - 完全按照官方文档实现 */ protected function flattenAndSortFields(array $data, string $prefix = ''): array { $fields = []; foreach ($data as $key => $value) { $fieldKey = $prefix ? "{$prefix}.{$key}" : $key; if (is_null($value)) { $fields[$fieldKey] = ''; } elseif (is_array($value)) { // 检查是否为索引数组 if (array_keys($value) === range(0, count($value) - 1)) { // 索引数组处理方式 foreach ($value as $index => $item) { $arrayKey = $fieldKey . "[" . $index . "]"; if (is_array($item)) { $fields = array_merge($fields, $this->flattenAndSortFields($item, $arrayKey)); } else { $fields[$arrayKey] = $this->formatValue($item); } } } else { // 关联数组处理方式 $fields = array_merge($fields, $this->flattenAndSortFields($value, $fieldKey)); } } else { $fields[$fieldKey] = $this->formatValue($value); } } ksort($fields, SORT_STRING | SORT_FLAG_CASE); return $fields; } /** * 处理布尔值转字符串 - 确保与官方文档一致 */ protected function formatValue($value) { if (is_bool($value)) { return $value ? 'true' : 'false'; } elseif (is_null($value)) { return ''; } return (string)$value; } /** * 拼接字段 */ protected function concatenateFields(array $fields): string { $pairs = []; foreach ($fields as $key => $value) { $pairs[] = $key . '=' . $value; } return implode('&', $pairs); } /** * 发送请求 */ protected function request(string $method, string $uri, array $data = [], array $headers = []): array { try { // 记录请求信息 Log::info('Apcopay API Request', [ 'method' => $method, 'uri' => $uri, 'data' => $data ]); // 生成签名 $signature = $this->generateSignature($data); // 1. 准备请求头 $headers = array_merge([ 'Authorization' => $this->getAuthorizationHeader(), 'User-Agent' => 'PHP/ApcopayAPI', 'Content-Type' => 'application/json', 'Accept' => 'application/json', 'Signature' => $signature // 添加签名到请求头 ], $headers); // 2. 不再将签名添加到请求体中,按照文档仅添加到请求头 // 记录完整请求头和请求体 Log::info('Apcopay API Request Details', [ 'headers' => array_merge( $headers, ['Authorization' => 'Basic **REDACTED**'] // 不记录完整的Authorization头 ), 'data' => $data ]); // 3. 发送请求 $options = [ 'headers' => $headers, 'http_errors' => false // 不自动抛出HTTP错误,我们自己处理 ]; if (!empty($data)) { $options['json'] = $data; } $response = $this->client->request($method, $uri, $options); // 3. 解析响应 $statusCode = $response->getStatusCode(); $body = $response->getBody()->getContents(); Log::info('Apcopay API Raw Response', [ 'uri' => $uri, 'status_code' => $statusCode, 'body' => $body ]); $result = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE) { Log::error('Apcopay API Invalid JSON', [ 'uri' => $uri, 'body' => $body, 'json_error' => json_last_error_msg() ]); throw new \RuntimeException('Invalid JSON response: ' . $body); } // 4. 检查HTTP状态码 if ($statusCode >= 400) { Log::error('Apcopay API HTTP Error', [ 'uri' => $uri, 'status_code' => $statusCode, 'result' => $result ]); // 返回错误信息而不是抛出异常,让调用者决定如何处理 return [ 'isSuccess' => false, 'errorMessage' => $result['errorMessage'] ?? "HTTP Error: {$statusCode}", 'statusCode' => $statusCode, 'rawResponse' => $result ]; } // 记录响应信息 Log::info('Apcopay API Response', [ 'uri' => $uri, 'result' => $result ]); return $result; } catch (GuzzleException $e) { Log::error('Apcopay API Error', [ 'method' => $method, 'uri' => $uri, 'data' => $data, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return [ 'isSuccess' => false, 'errorMessage' => 'Apcopay API request failed: ' . $e->getMessage(), 'exception' => get_class($e) ]; } catch (\Exception $e) { Log::error('Apcopay General Error', [ 'method' => $method, 'uri' => $uri, 'data' => $data, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return [ 'isSuccess' => false, 'errorMessage' => 'Error processing Apcopay request: ' . $e->getMessage(), 'exception' => get_class($e) ]; } } /** * GET请求 */ protected function get(string $uri, array $query = [], array $headers = []): array { return $this->request('GET', $uri . '?' . http_build_query($query), [], $headers); } /** * POST请求 */ protected function post(string $uri, array $data = [], array $headers = []): array { return $this->request('POST', $uri, $data, $headers); } /** * PUT请求 */ protected function put(string $uri, array $data = [], array $headers = []): array { return $this->request('PUT', $uri, $data, $headers); } /** * DELETE请求 */ protected function delete(string $uri, array $headers = []): array { return $this->request('DELETE', $uri, [], $headers); } }