ReproduceErrorCommand.php 9.2 KB

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