ReproduceErrorCommand.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. <?php
  2. namespace App\Console\Commands;
  3. use GuzzleHttp\Client;
  4. use Illuminate\Console\Command;
  5. use UCore\Model\RequestLog;
  6. use Illuminate\Support\Facades\Log;
  7. /**
  8. * 错误复现命令
  9. *
  10. * 通过 sys_request_logs 表的记录来复现请求,用于调试和错误排查
  11. */
  12. class ReproduceErrorCommand extends Command
  13. {
  14. /**
  15. * 命令签名
  16. *
  17. * @var string
  18. */
  19. protected $signature = 'debug:reproduce-error
  20. {identifier : 请求标识符(可以是id、request_unid或run_unid)}
  21. {--type=auto : 标识符类型(id|request_unid|run_unid|auto),auto为自动检测}
  22. {--timeout=30 : 请求超时时间(秒)}';
  23. /**
  24. * 命令描述
  25. *
  26. * @var string
  27. */
  28. protected $description = '通过请求日志记录复现错误请求,用于调试和问题排查';
  29. /**
  30. * HTTP 客户端
  31. *
  32. * @var Client
  33. */
  34. protected Client $client;
  35. /**
  36. * 基础URL
  37. *
  38. * @var string
  39. */
  40. protected string $baseUrl;
  41. /**
  42. * 执行命令
  43. */
  44. public function handle()
  45. {
  46. $identifier = $this->argument('identifier');
  47. $type = $this->option('type');
  48. $timeout = (int) $this->option('timeout');
  49. $this->info("开始查找请求记录...");
  50. $this->info("标识符: {$identifier}");
  51. $this->info("类型: {$type}");
  52. // 查找请求记录
  53. $requestLog = $this->findRequestLog($identifier, $type);
  54. if (!$requestLog) {
  55. $this->error("未找到匹配的请求记录");
  56. return 1;
  57. }
  58. $this->info("找到请求记录:");
  59. $this->line(" ID: {$requestLog->id}");
  60. $this->line(" Request UNID: {$requestLog->request_unid}");
  61. $this->line(" Run UNID: {$requestLog->run_unid}");
  62. $this->line(" 路径: {$requestLog->path}");
  63. $this->line(" 方法: {$requestLog->method}");
  64. $this->line(" 创建时间: {$requestLog->created_at}");
  65. // 检查必要的数据
  66. if (empty($requestLog->protobuf_json)) {
  67. $this->error("请求记录中缺少 protobuf_json 数据");
  68. return 1;
  69. }
  70. // 解析 headers 获取 token
  71. $token = $this->extractToken($requestLog->headers);
  72. if (!$token) {
  73. $this->warn("未找到 token,将不携带 token 发起请求");
  74. } else {
  75. $this->info("提取到 token: " . substr($token, 0, 10) . "...");
  76. }
  77. // 初始化 HTTP 客户端
  78. $this->initializeHttpClient($timeout);
  79. // 发起请求
  80. $this->info("开始发起请求...");
  81. $response = $this->makeRequest($requestLog->protobuf_json, $token);
  82. if ($response === null) {
  83. return 1;
  84. }
  85. // 输出结果
  86. $this->info("请求完成,响应结果:");
  87. $this->line("状态码: " . $response['status_code']);
  88. $this->line("响应头:");
  89. foreach ($response['headers'] as $name => $values) {
  90. $this->line(" {$name}: " . implode(', ', $values));
  91. }
  92. $this->line("响应内容:");
  93. $this->line($response['body']);
  94. return 0;
  95. }
  96. /**
  97. * 查找请求记录
  98. *
  99. * @param string $identifier 标识符
  100. * @param string $type 类型
  101. * @return RequestLog|null
  102. */
  103. protected function findRequestLog(string $identifier, string $type): ?RequestLog
  104. {
  105. $query = RequestLog::query();
  106. if ($type === 'auto') {
  107. // 自动检测类型
  108. if (is_numeric($identifier)) {
  109. // 纯数字,优先按 ID 查找
  110. $requestLog = $query->where('id', $identifier)->first();
  111. if ($requestLog) {
  112. return $requestLog;
  113. }
  114. }
  115. // 按 request_unid 查找
  116. $requestLog = RequestLog::query()->where('request_unid', $identifier)->first();
  117. if ($requestLog) {
  118. return $requestLog;
  119. }
  120. // 按 run_unid 查找
  121. $requestLog = RequestLog::query()->where('run_unid', $identifier)->first();
  122. if ($requestLog) {
  123. return $requestLog;
  124. }
  125. return null;
  126. }
  127. // 指定类型查找
  128. switch ($type) {
  129. case 'id':
  130. return $query->where('id', $identifier)->first();
  131. case 'request_unid':
  132. return $query->where('request_unid', $identifier)->first();
  133. case 'run_unid':
  134. return $query->where('run_unid', $identifier)->first();
  135. default:
  136. $this->error("不支持的类型: {$type}");
  137. return null;
  138. }
  139. }
  140. /**
  141. * 从 headers JSON 中提取 token
  142. *
  143. * @param string|null $headersJson
  144. * @return string|null
  145. */
  146. protected function extractToken(?string $headersJson): ?string
  147. {
  148. if (empty($headersJson)) {
  149. return null;
  150. }
  151. try {
  152. $headers = json_decode($headersJson, true);
  153. if (!is_array($headers)) {
  154. return null;
  155. }
  156. // 查找 token 字段(可能在不同的键名下)
  157. $tokenKeys = ['token', 'Token', 'authorization', 'Authorization'];
  158. foreach ($tokenKeys as $key) {
  159. if (isset($headers[$key])) {
  160. $tokenValue = $headers[$key];
  161. // headers 中的值可能是数组
  162. if (is_array($tokenValue)) {
  163. return $tokenValue[0] ?? null;
  164. }
  165. return $tokenValue;
  166. }
  167. }
  168. return null;
  169. } catch (\Exception $e) {
  170. $this->warn("解析 headers 失败: " . $e->getMessage());
  171. return null;
  172. }
  173. }
  174. /**
  175. * 初始化 HTTP 客户端
  176. *
  177. * @param int $timeout
  178. */
  179. protected function initializeHttpClient(int $timeout): void
  180. {
  181. $this->baseUrl = env('UNITTEST_URL', 'http://localhost:8000');
  182. $this->info("目标地址: {$this->baseUrl}");
  183. $this->client = new Client([
  184. 'base_uri' => $this->baseUrl,
  185. 'timeout' => $timeout,
  186. 'http_errors' => false,
  187. 'verify' => false, // 禁用 SSL 验证
  188. ]);
  189. }
  190. /**
  191. * 发起请求
  192. *
  193. * @param string $protobufJson
  194. * @param string|null $token
  195. * @return array|null
  196. */
  197. protected function makeRequest(string $protobufJson, ?string $token): ?array
  198. {
  199. try {
  200. $headers = [
  201. 'Content-Type' => 'application/json',
  202. 'Accept' => 'application/json'
  203. ];
  204. if ($token) {
  205. $headers['token'] = $token;
  206. }
  207. Log::info('复现请求开始', [
  208. 'url' => $this->baseUrl . '/gameapi',
  209. 'headers' => $headers,
  210. 'body_length' => strlen($protobufJson)
  211. ]);
  212. $response = $this->client->post('/gameapi', [
  213. 'body' => $protobufJson,
  214. 'headers' => $headers
  215. ]);
  216. $statusCode = $response->getStatusCode();
  217. $responseHeaders = $response->getHeaders();
  218. $responseBody = $response->getBody()->getContents();
  219. Log::info('复现请求完成', [
  220. 'status_code' => $statusCode,
  221. 'response_length' => strlen($responseBody)
  222. ]);
  223. return [
  224. 'status_code' => $statusCode,
  225. 'headers' => $responseHeaders,
  226. 'body' => $responseBody
  227. ];
  228. } catch (\Exception $e) {
  229. $this->error("请求失败: " . $e->getMessage());
  230. Log::error('复现请求失败', [
  231. 'error' => $e->getMessage(),
  232. 'trace' => $e->getTraceAsString()
  233. ]);
  234. return null;
  235. }
  236. }
  237. }