10.0, 'verify' => false, // 禁用 SSL 证书验证(测试环境可用) ]); // $headers = array_merge([ // 'Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8', // ], $headers); $headers = array_merge([ 'Content-Type' => 'application/json;charset=utf-8', ], $headers); // 根据 Content-Type 决定请求体格式 if (strpos($headers['Content-Type'], 'application/json') !== false) { $body = json_encode($data); } else { $body = http_build_query($data); } for ($i = 0; $i <= $retry; $i++) { try { $response = $client->post($url, [ 'headers' => $headers, 'body' => $body ]); $status = $response->getStatusCode(); $respBody = (string) $response->getBody(); //Yii::info("[HttpClient] POST {$url} {$status} {$respBody}"); $json = json_decode($respBody, true); return $json ?: ['code' => $status, 'body' => $respBody]; } catch (RequestException $e) { $message = $e->getMessage(); Yii::error("[HttpClient] POST {$url} failed: {$message}"); if ($i < $retry) { sleep(1); // 重试间隔 continue; } return ['code' => 500, 'error' => $message]; } } } /** * 发送 GET 请求 */ public static function get(string $url, array $params = [], array $headers = [], int $retry = 1) { $client = new Client([ 'timeout' => 8.0, 'verify' => false, ]); $headers = array_merge([ 'Accept' => 'application/json', ], $headers); for ($i = 0; $i <= $retry; $i++) { try { $response = $client->get($url, [ 'headers' => $headers, 'query' => $params, ]); $status = $response->getStatusCode(); $respBody = (string) $response->getBody(); Yii::info("[HttpClient] GET {$url} {$status} {$respBody}"); $json = json_decode($respBody, true); return $json ?: ['code' => $status, 'body' => $respBody]; } catch (RequestException $e) { Yii::error("[HttpClient] GET {$url} failed: {$e->getMessage()}"); if ($i < $retry) { sleep(1); continue; } return ['code' => 500, 'error' => $e->getMessage()]; } } } /** * 并发发送多个 POST 请求(支持自定义超时和错误处理) * * 使用 Guzzle Pool 实现真正的并发请求,每个请求支持独立的超时配置。 * 若某个请求失败或超时,不会影响其他请求的执行。 * * @param array $requests 请求配置数组,结构: * [ * 'platform_name' => [ * 'url' => 'http://api.example.com/quote', * 'data' => [...], * 'headers' => ['Content-Type' => 'application/json'], * 'timeout' => 5.0, // 单位:秒 * ], * ... * ] * @param int $maxConcurrent 最大并发数(默认3) * @return array 返回成功和失败的请求结果,结构: * [ * 'success' => [ * 'platform_name' => [响应数据], * ... * ], * 'failed' => [ * 'platform_name' => 'error message', * ... * ], * ] * * @example * ```php * $requests = [ * 'huolala' => [ * 'url' => 'https://openapi.huolala.cn/v1/order/quote', * 'data' => ['city_id' => 1001, ...], * 'headers' => ['Content-Type' => 'application/json'], * 'timeout' => 5.0, * ], * 'fengniao' => [ * 'url' => 'https://open-anubis.ele.me/anubis-webapi/v3/invoke/trace', * 'data' => [...], * 'headers' => ['Content-Type' => 'application/json'], * 'timeout' => 5.0, * ], * ]; * $results = HttpClient::postConcurrent($requests, 2); * // 结果: * // [ * // 'success' => ['huolala' => [...], 'fengniao' => [...]], * // 'failed' => [], * // ] * ``` */ public static function postConcurrent(array $requests, int $maxConcurrent = 3) { $results = [ 'success' => [], 'failed' => [], ]; // 构建 Guzzle 请求配置 $requestConfigs = []; $platformNames = []; foreach ($requests as $platform => $config) { $url = $config['url'] ?? ''; $data = $config['data'] ?? []; $headers = $config['headers'] ?? []; $timeout = $config['timeout'] ?? 8.0; if (!$url) { $results['failed'][$platform] = 'URL not provided'; continue; } //$headers = array_merge([ //'Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8', //], $headers); $headers = array_merge([ 'Content-Type' => 'application/json;charset=utf-8', ], $headers); // 根据 Content-Type 决定请求体格式 if (strpos($headers['Content-Type'], 'application/json') !== false) { $body = json_encode($data); } else { $body = http_build_query($data); } $requestConfigs[] = [ 'method' => 'POST', 'uri' => $url, 'headers' => $headers, 'body' => $body, 'timeout' => $timeout, ]; $platformNames[] = $platform; } if (empty($requestConfigs)) { return $results; } $client = new Client([ 'verify' => false, ]); // 使用生成器生成请求和配置对 // Guzzle Pool 期望每个生成的值是: // 1. 返回 Promise 的 callable // 2. 或直接是 RequestInterface $requestGenerator = function () use ($requestConfigs, $client) { foreach ($requestConfigs as $index => $config) { $request = new Request( $config['method'], $config['uri'], $config['headers'], $config['body'] ); // 返回 callable,返回 Promise 对象 yield function () use ($client, $request, $config) { return $client->sendAsync($request, [ 'timeout' => $config['timeout'], ]); }; } }; // 使用 Pool 实现并发请求 $pool = new Pool($client, $requestGenerator(), [ 'concurrency' => $maxConcurrent, 'fulfilled' => function ($response, $index) use (&$results, $platformNames) { $platformName = $platformNames[$index] ?? "platform_{$index}"; $status = $response->getStatusCode(); $respBody = (string) $response->getBody(); //Yii::info("[HttpClient:Concurrent] {$platformName} completed with status {$status}"); $json = json_decode($respBody, true); $results['success'][$platformName] = $json ?: ['code' => $status, 'body' => $respBody]; }, 'rejected' => function ($reason, $index) use (&$results, $platformNames) { $platformName = $platformNames[$index] ?? "platform_{$index}"; $message = $reason->getMessage(); Yii::error("[HttpClient:Concurrent] {$platformName} failed: {$message}"); $results['failed'][$platformName] = $message; }, ]); $promise = $pool->promise(); $promise->wait(); return $results; } }