HttpClient.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. <?php
  2. namespace common\components\delivery\helpers;
  3. use GuzzleHttp\Client;
  4. use GuzzleHttp\Exception\RequestException;
  5. use GuzzleHttp\Pool;
  6. use GuzzleHttp\Psr7\Request;
  7. use Yii;
  8. class HttpClient
  9. {
  10. /**
  11. * 发送 POST 请求(支持 JSON 自动解析 + 重试)
  12. */
  13. public static function post(string $url, array $data = [], array $headers = [], int $retry = 1)
  14. {
  15. $client = new Client([
  16. 'timeout' => 10.0,
  17. 'verify' => false, // 禁用 SSL 证书验证(测试环境可用)
  18. ]);
  19. // $headers = array_merge([
  20. // 'Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8',
  21. // ], $headers);
  22. $headers = array_merge([
  23. 'Content-Type' => 'application/json;charset=utf-8',
  24. ], $headers);
  25. // 根据 Content-Type 决定请求体格式
  26. if (strpos($headers['Content-Type'], 'application/json') !== false) {
  27. $body = json_encode($data);
  28. } else {
  29. $body = http_build_query($data);
  30. }
  31. for ($i = 0; $i <= $retry; $i++) {
  32. try {
  33. $response = $client->post($url, [
  34. 'headers' => $headers,
  35. 'body' => $body
  36. ]);
  37. $status = $response->getStatusCode();
  38. $respBody = (string) $response->getBody();
  39. //Yii::info("[HttpClient] POST {$url} {$status} {$respBody}");
  40. $json = json_decode($respBody, true);
  41. return $json ?: ['code' => $status, 'body' => $respBody];
  42. } catch (RequestException $e) {
  43. $message = $e->getMessage();
  44. Yii::error("[HttpClient] POST {$url} failed: {$message}");
  45. if ($i < $retry) {
  46. sleep(1); // 重试间隔
  47. continue;
  48. }
  49. return ['code' => 500, 'error' => $message];
  50. }
  51. }
  52. }
  53. /**
  54. * 发送 GET 请求
  55. */
  56. public static function get(string $url, array $params = [], array $headers = [], int $retry = 1)
  57. {
  58. $client = new Client([
  59. 'timeout' => 8.0,
  60. 'verify' => false,
  61. ]);
  62. $headers = array_merge([
  63. 'Accept' => 'application/json',
  64. ], $headers);
  65. for ($i = 0; $i <= $retry; $i++) {
  66. try {
  67. $response = $client->get($url, [
  68. 'headers' => $headers,
  69. 'query' => $params,
  70. ]);
  71. $status = $response->getStatusCode();
  72. $respBody = (string) $response->getBody();
  73. Yii::info("[HttpClient] GET {$url} {$status} {$respBody}");
  74. $json = json_decode($respBody, true);
  75. return $json ?: ['code' => $status, 'body' => $respBody];
  76. } catch (RequestException $e) {
  77. Yii::error("[HttpClient] GET {$url} failed: {$e->getMessage()}");
  78. if ($i < $retry) {
  79. sleep(1);
  80. continue;
  81. }
  82. return ['code' => 500, 'error' => $e->getMessage()];
  83. }
  84. }
  85. }
  86. /**
  87. * 并发发送多个 POST 请求(支持自定义超时和错误处理)
  88. *
  89. * 使用 Guzzle Pool 实现真正的并发请求,每个请求支持独立的超时配置。
  90. * 若某个请求失败或超时,不会影响其他请求的执行。
  91. *
  92. * @param array $requests 请求配置数组,结构:
  93. * [
  94. * 'platform_name' => [
  95. * 'url' => 'http://api.example.com/quote',
  96. * 'data' => [...],
  97. * 'headers' => ['Content-Type' => 'application/json'],
  98. * 'timeout' => 5.0, // 单位:秒
  99. * ],
  100. * ...
  101. * ]
  102. * @param int $maxConcurrent 最大并发数(默认3)
  103. * @return array 返回成功和失败的请求结果,结构:
  104. * [
  105. * 'success' => [
  106. * 'platform_name' => [响应数据],
  107. * ...
  108. * ],
  109. * 'failed' => [
  110. * 'platform_name' => 'error message',
  111. * ...
  112. * ],
  113. * ]
  114. *
  115. * @example
  116. * ```php
  117. * $requests = [
  118. * 'huolala' => [
  119. * 'url' => 'https://openapi.huolala.cn/v1/order/quote',
  120. * 'data' => ['city_id' => 1001, ...],
  121. * 'headers' => ['Content-Type' => 'application/json'],
  122. * 'timeout' => 5.0,
  123. * ],
  124. * 'fengniao' => [
  125. * 'url' => 'https://open-anubis.ele.me/anubis-webapi/v3/invoke/trace',
  126. * 'data' => [...],
  127. * 'headers' => ['Content-Type' => 'application/json'],
  128. * 'timeout' => 5.0,
  129. * ],
  130. * ];
  131. * $results = HttpClient::postConcurrent($requests, 2);
  132. * // 结果:
  133. * // [
  134. * // 'success' => ['huolala' => [...], 'fengniao' => [...]],
  135. * // 'failed' => [],
  136. * // ]
  137. * ```
  138. */
  139. public static function postConcurrent(array $requests, int $maxConcurrent = 3)
  140. {
  141. $results = [
  142. 'success' => [],
  143. 'failed' => [],
  144. ];
  145. // 构建 Guzzle 请求配置
  146. $requestConfigs = [];
  147. $platformNames = [];
  148. foreach ($requests as $platform => $config) {
  149. $url = $config['url'] ?? '';
  150. $data = $config['data'] ?? [];
  151. $headers = $config['headers'] ?? [];
  152. $timeout = $config['timeout'] ?? 8.0;
  153. if (!$url) {
  154. $results['failed'][$platform] = 'URL not provided';
  155. continue;
  156. }
  157. //$headers = array_merge([
  158. //'Content-Type' => 'application/x-www-form-urlencoded;charset=utf-8',
  159. //], $headers);
  160. $headers = array_merge([
  161. 'Content-Type' => 'application/json;charset=utf-8',
  162. ], $headers);
  163. // 根据 Content-Type 决定请求体格式
  164. if (strpos($headers['Content-Type'], 'application/json') !== false) {
  165. $body = json_encode($data);
  166. } else {
  167. $body = http_build_query($data);
  168. }
  169. $requestConfigs[] = [
  170. 'method' => 'POST',
  171. 'uri' => $url,
  172. 'headers' => $headers,
  173. 'body' => $body,
  174. 'timeout' => $timeout,
  175. ];
  176. $platformNames[] = $platform;
  177. }
  178. if (empty($requestConfigs)) {
  179. return $results;
  180. }
  181. $client = new Client([
  182. 'verify' => false,
  183. ]);
  184. // 使用生成器生成请求和配置对
  185. // Guzzle Pool 期望每个生成的值是:
  186. // 1. 返回 Promise 的 callable
  187. // 2. 或直接是 RequestInterface
  188. $requestGenerator = function () use ($requestConfigs, $client) {
  189. foreach ($requestConfigs as $index => $config) {
  190. $request = new Request(
  191. $config['method'],
  192. $config['uri'],
  193. $config['headers'],
  194. $config['body']
  195. );
  196. // 返回 callable,返回 Promise 对象
  197. yield function () use ($client, $request, $config) {
  198. return $client->sendAsync($request, [
  199. 'timeout' => $config['timeout'],
  200. ]);
  201. };
  202. }
  203. };
  204. // 使用 Pool 实现并发请求
  205. $pool = new Pool($client, $requestGenerator(), [
  206. 'concurrency' => $maxConcurrent,
  207. 'fulfilled' => function ($response, $index) use (&$results, $platformNames) {
  208. $platformName = $platformNames[$index] ?? "platform_{$index}";
  209. $status = $response->getStatusCode();
  210. $respBody = (string) $response->getBody();
  211. //Yii::info("[HttpClient:Concurrent] {$platformName} completed with status {$status}");
  212. $json = json_decode($respBody, true);
  213. $results['success'][$platformName] = $json ?: ['code' => $status, 'body' => $respBody];
  214. },
  215. 'rejected' => function ($reason, $index) use (&$results, $platformNames) {
  216. $platformName = $platformNames[$index] ?? "platform_{$index}";
  217. $message = $reason->getMessage();
  218. Yii::error("[HttpClient:Concurrent] {$platformName} failed: {$message}");
  219. $results['failed'][$platformName] = $message;
  220. },
  221. ]);
  222. $promise = $pool->promise();
  223. $promise->wait();
  224. return $results;
  225. }
  226. }