HttpClient.php 8.3 KB

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