ReproduceErrorCommand.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  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. * 默认使用 post 字段的原始请求数据,根据 headers 中的 Content-Type 自动检测数据类型
  15. *
  16. * 使用示例:
  17. * php artisan debug:reproduce-error 68981433 # 使用ID查找,自动选择数据源
  18. * php artisan debug:reproduce-error request_1749626545371 # 使用request_unid查找
  19. * php artisan debug:reproduce-error 68973982 --type=request_unid # 明确指定查找类型
  20. * php artisan debug:reproduce-error 68973982 --no-clear-logs # 不清空日志文件
  21. * php artisan debug:reproduce-error 68973982 --data-source=post # 强制使用post数据
  22. * php artisan debug:reproduce-error 68973982 --data-source=protobuf_json # 强制使用protobuf_json数据
  23. *
  24. */
  25. class ReproduceErrorCommand extends Command
  26. {
  27. /**
  28. * 命令签名
  29. *
  30. * @var string
  31. */
  32. 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 : 运行时不前清空当前日志文件} {--data-source=auto : 数据源(post|protobuf_json|auto),auto为自动选择,优先使用post}';
  33. /**
  34. * 命令描述
  35. *
  36. * @var string
  37. */
  38. protected $description = '通过请求日志记录复现错误请求,用于调试和问题排查。支持运行前清空日志文件,默认使用post字段数据并根据headers自动检测数据类型';
  39. /**
  40. * HTTP 客户端
  41. *
  42. * @var Client
  43. */
  44. protected Client $client;
  45. /**
  46. * 基础URL
  47. *
  48. * @var string
  49. */
  50. protected string $baseUrl;
  51. /**
  52. * 执行命令
  53. */
  54. public function handle()
  55. {
  56. $identifier = $this->argument('identifier');
  57. $type = $this->option('type');
  58. $timeout = (int)$this->option('timeout');
  59. $noclearLogs = $this->option('no-clear-logs');
  60. $dataSource = $this->option('data-source');
  61. // 如果开启了清空日志选项,则清空日志文件
  62. if ($noclearLogs) {
  63. } else {
  64. $this->clearLogFiles();
  65. }
  66. $this->info("开始查找请求记录...");
  67. $this->info("标识符: {$identifier}");
  68. $this->info("类型: {$type}");
  69. $this->info("数据源: {$dataSource}");
  70. // 查找请求记录
  71. $requestLog = $this->findRequestLog($identifier, $type);
  72. if (!$requestLog) {
  73. $this->error("未找到匹配的请求记录");
  74. return 1;
  75. }
  76. $this->info("找到请求记录:");
  77. $this->line(" ID: {$requestLog->id}");
  78. $this->line(" Request UNID: {$requestLog->request_unid}");
  79. $this->line(" Run UNID: {$requestLog->run_unid}");
  80. $this->line(" UserId : {$requestLog->user_id}");
  81. $this->line(" 路径: {$requestLog->path}");
  82. $this->line(" 方法: {$requestLog->method}");
  83. $this->line(" 创建时间: {$requestLog->created_at}");
  84. // 解析 headers 获取 Content-Type 和 token
  85. $headers = $this->parseHeaders($requestLog->headers);
  86. $contentType = $this->extractContentType($headers);
  87. $token = $this->extractToken($requestLog->headers);
  88. $this->info("Content-Type: " . ($contentType ?: '未检测到'));
  89. if (!$token) {
  90. $this->warn("未找到 token,将不携带 token 发起请求");
  91. } else {
  92. $this->info("提取到 token: " . substr($token, 0, 10) . "...");
  93. }
  94. // 设置用户会话
  95. if ($requestLog->user_id) {
  96. SessionHelper::sessionLogin($token, $requestLog->user_id);
  97. $this->info("token 设置用户: {$requestLog->user_id} ");
  98. }
  99. // 准备请求数据
  100. $requestData = $this->prepareRequestData($requestLog, $dataSource, $contentType);
  101. if ($requestData === null) {
  102. return 1;
  103. }
  104. $this->info("使用数据源: {$requestData['source']}");
  105. $this->info("检测到数据类型: {$requestData['type']}");
  106. $this->info("请求数据预览:");
  107. if ($requestData['type'] === 'json') {
  108. dump(json_decode($requestData['data'], true));
  109. } else {
  110. $this->line(" 二进制数据长度: " . strlen($requestData['data']) . " 字节");
  111. $this->line(" Base64预览: " . substr(base64_encode($requestData['data']), 0, 100) . "...");
  112. }
  113. // 初始化 HTTP 客户端
  114. $this->initializeHttpClient($timeout);
  115. // 发起请求
  116. $this->info("开始发起请求...");
  117. $response = $this->makeRequest($requestData['data'], $token, $requestData['type']);
  118. if ($response === null) {
  119. return 1;
  120. }
  121. // 输出结果
  122. $this->line("响应内容:");
  123. $this->displayResponse($response, $requestData['type']);
  124. return 0;
  125. }
  126. /**
  127. * 查找请求记录
  128. *
  129. * @param string $identifier 标识符
  130. * @param string $type 类型
  131. * @return RequestLog|null
  132. */
  133. protected function findRequestLog(string $identifier, string $type): ?RequestLog
  134. {
  135. $query = RequestLog::query();
  136. if ($type === 'auto') {
  137. // 自动检测类型
  138. if (is_numeric($identifier)) {
  139. // 纯数字,优先按 ID 查找
  140. $requestLog = $query->where('id', $identifier)->first();
  141. if ($requestLog) {
  142. return $requestLog;
  143. }
  144. }
  145. // 按 request_unid 查找
  146. $requestLog = RequestLog::query()->where('request_unid', $identifier)->first();
  147. if ($requestLog) {
  148. return $requestLog;
  149. }
  150. // 按 run_unid 查找
  151. $requestLog = RequestLog::query()->where('run_unid', $identifier)->first();
  152. if ($requestLog) {
  153. return $requestLog;
  154. }
  155. return null;
  156. }
  157. // 指定类型查找
  158. switch ($type) {
  159. case 'id':
  160. return $query->where('id', $identifier)->first();
  161. case 'request_unid':
  162. return $query->where('request_unid', $identifier)->first();
  163. case 'run_unid':
  164. return $query->where('run_unid', $identifier)->first();
  165. default:
  166. $this->error("不支持的类型: {$type}");
  167. return null;
  168. }
  169. }
  170. /**
  171. * 解析 headers JSON 字符串
  172. *
  173. * @param string|null $headersJson
  174. * @return array
  175. */
  176. protected function parseHeaders(?string $headersJson): array
  177. {
  178. if (empty($headersJson)) {
  179. return [];
  180. }
  181. try {
  182. $headers = json_decode($headersJson, true);
  183. return is_array($headers) ? $headers : [];
  184. } catch (\Exception $e) {
  185. $this->warn("解析 headers 失败: " . $e->getMessage());
  186. return [];
  187. }
  188. }
  189. /**
  190. * 从 headers 中提取 Content-Type
  191. *
  192. * @param array $headers
  193. * @return string|null
  194. */
  195. protected function extractContentType(array $headers): ?string
  196. {
  197. $contentTypeKeys = ['content-type', 'Content-Type', 'CONTENT-TYPE'];
  198. foreach ($contentTypeKeys as $key) {
  199. if (isset($headers[$key])) {
  200. $contentType = $headers[$key];
  201. // headers 中的值可能是数组
  202. if (is_array($contentType)) {
  203. return $contentType[0] ?? null;
  204. }
  205. return $contentType;
  206. }
  207. }
  208. return null;
  209. }
  210. /**
  211. * 从 headers JSON 中提取 token
  212. *
  213. * @param string|null $headersJson
  214. * @return string|null
  215. */
  216. protected function extractToken(?string $headersJson): ?string
  217. {
  218. $headers = $this->parseHeaders($headersJson);
  219. // 查找 token 字段(可能在不同的键名下)
  220. $tokenKeys = [ 'token', 'Token', 'authorization', 'Authorization' ];
  221. foreach ($tokenKeys as $key) {
  222. if (isset($headers[$key])) {
  223. $tokenValue = $headers[$key];
  224. // headers 中的值可能是数组
  225. if (is_array($tokenValue)) {
  226. return $tokenValue[0] ?? null;
  227. }
  228. return $tokenValue;
  229. }
  230. }
  231. return null;
  232. }
  233. /**
  234. * 准备请求数据
  235. *
  236. * @param RequestLog $requestLog
  237. * @param string $dataSource
  238. * @param string|null $contentType
  239. * @return array|null
  240. */
  241. protected function prepareRequestData(RequestLog $requestLog, string $dataSource, ?string $contentType): ?array
  242. {
  243. // 根据数据源选择策略
  244. if ($dataSource === 'auto') {
  245. // 自动选择:优先使用 post 数据,如果没有则使用 protobuf_json
  246. if (!empty($requestLog->post)) {
  247. return $this->preparePostData($requestLog->post, $contentType);
  248. } elseif (!empty($requestLog->protobuf_json)) {
  249. return $this->prepareProtobufJsonData($requestLog->protobuf_json);
  250. } else {
  251. $this->error("请求记录中既没有 post 数据也没有 protobuf_json 数据");
  252. return null;
  253. }
  254. } elseif ($dataSource === 'post') {
  255. if (empty($requestLog->post)) {
  256. $this->error("请求记录中缺少 post 数据");
  257. return null;
  258. }
  259. return $this->preparePostData($requestLog->post, $contentType);
  260. } elseif ($dataSource === 'protobuf_json') {
  261. if (empty($requestLog->protobuf_json)) {
  262. $this->error("请求记录中缺少 protobuf_json 数据");
  263. return null;
  264. }
  265. return $this->prepareProtobufJsonData($requestLog->protobuf_json);
  266. } else {
  267. $this->error("不支持的数据源: {$dataSource}");
  268. return null;
  269. }
  270. }
  271. /**
  272. * 准备 post 数据
  273. *
  274. * @param string $postData base64 编码的原始请求数据
  275. * @param string|null $contentType
  276. * @return array
  277. */
  278. protected function preparePostData(string $postData, ?string $contentType): array
  279. {
  280. // 解码 base64 数据
  281. $rawData = base64_decode($postData);
  282. // 根据 Content-Type 判断数据类型
  283. if ($contentType && stripos($contentType, 'json') !== false) {
  284. // JSON 格式数据
  285. return [
  286. 'source' => 'post',
  287. 'type' => 'json',
  288. 'data' => $rawData
  289. ];
  290. } else {
  291. // 默认为 protobuf 二进制格式
  292. return [
  293. 'source' => 'post',
  294. 'type' => 'protobuf',
  295. 'data' => $rawData
  296. ];
  297. }
  298. }
  299. /**
  300. * 准备 protobuf_json 数据
  301. *
  302. * @param string $protobufJson
  303. * @return array
  304. */
  305. protected function prepareProtobufJsonData(string $protobufJson): array
  306. {
  307. return [
  308. 'source' => 'protobuf_json',
  309. 'type' => 'json',
  310. 'data' => $protobufJson
  311. ];
  312. }
  313. /**
  314. * 初始化 HTTP 客户端
  315. *
  316. * @param int $timeout
  317. */
  318. protected function initializeHttpClient(int $timeout): void
  319. {
  320. $this->baseUrl = env('UNITTEST_URL', 'http://localhost:8000');
  321. $this->info("目标地址: {$this->baseUrl}");
  322. $this->client = new Client([
  323. 'base_uri' => $this->baseUrl,
  324. 'timeout' => $timeout,
  325. 'http_errors' => false,
  326. 'verify' => false, // 禁用 SSL 验证
  327. ]);
  328. }
  329. /**
  330. * 发起请求
  331. *
  332. * @param string $requestData 请求数据
  333. * @param string|null $token 认证token
  334. * @param string $dataType 数据类型 (json|protobuf)
  335. * @return array|null
  336. */
  337. protected function makeRequest(string $requestData, ?string $token, string $dataType): ?array
  338. {
  339. try {
  340. // 根据数据类型设置不同的 headers
  341. if ($dataType === 'json') {
  342. $headers = [
  343. 'Content-Type' => 'application/json',
  344. 'Accept' => 'application/json'
  345. ];
  346. } else {
  347. // protobuf 二进制格式
  348. $headers = [
  349. 'Content-Type' => 'application/x-protobuf',
  350. 'Accept' => 'application/x-protobuf',
  351. 'Force-Json'=>'1'
  352. ];
  353. }
  354. if ($token) {
  355. $headers['token'] = $token;
  356. }
  357. Log::info('复现请求开始', [
  358. 'url' => $this->baseUrl . '/gameapi',
  359. 'headers' => $headers,
  360. 'body_length' => strlen($requestData),
  361. 'data_type' => $dataType
  362. ]);
  363. $response = $this->client->post('/gameapi', [
  364. 'body' => $requestData,
  365. 'headers' => $headers
  366. ]);
  367. $statusCode = $response->getStatusCode();
  368. $responseHeaders = $response->getHeaders();
  369. $responseBody = $response->getBody()->getContents();
  370. Log::info('复现请求完成', [
  371. 'status_code' => $statusCode,
  372. 'response_length' => strlen($responseBody),
  373. 'data_type' => $dataType
  374. ]);
  375. return [
  376. 'status_code' => $statusCode,
  377. 'headers' => $responseHeaders,
  378. 'body' => $responseBody
  379. ];
  380. } catch (\Exception $e) {
  381. $this->error("请求失败: " . $e->getMessage());
  382. Log::error('复现请求失败', [
  383. 'error' => $e->getMessage(),
  384. 'trace' => $e->getTraceAsString(),
  385. 'data_type' => $dataType
  386. ]);
  387. return null;
  388. }
  389. }
  390. /**
  391. * 显示响应内容
  392. *
  393. * @param array $response 响应数据
  394. * @param string $requestType 请求类型
  395. */
  396. protected function displayResponse(array $response, string $requestType): void
  397. {
  398. $this->line("状态码: " . $response['status_code']);
  399. if ($requestType === 'json') {
  400. // JSON 响应,尝试解析
  401. $jsonData = json_decode($response['body'], true);
  402. if ($jsonData !== null) {
  403. dump($jsonData);
  404. } else {
  405. $this->warn("响应不是有效的 JSON 格式");
  406. $this->line("原始响应: " . substr($response['body'], 0, 500) . "...");
  407. }
  408. } else {
  409. // Protobuf 二进制响应
  410. $this->line("二进制响应长度: " . strlen($response['body']) . " 字节");
  411. $this->line("Base64 编码: " . substr(base64_encode($response['body']), 0, 200) . "...");
  412. // 尝试解析为 JSON(某些情况下服务器可能返回 JSON)
  413. $jsonData = json_decode($response['body'], true);
  414. if ($jsonData !== null) {
  415. $this->line("响应可解析为 JSON:");
  416. dump($jsonData);
  417. } else {
  418. $this->line("响应为二进制 Protobuf 数据,无法直接显示");
  419. }
  420. }
  421. }
  422. /**
  423. * 清空当前日志文件
  424. */
  425. protected function clearLogFiles(): void
  426. {
  427. Logger::clear_log();
  428. Logger::debug('旧的日志已经清理');
  429. }
  430. }