|
|
@@ -0,0 +1,276 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace App\Console\Commands;
|
|
|
+
|
|
|
+use GuzzleHttp\Client;
|
|
|
+use Illuminate\Console\Command;
|
|
|
+use UCore\Model\RequestLog;
|
|
|
+use Illuminate\Support\Facades\Log;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 错误复现命令
|
|
|
+ *
|
|
|
+ * 通过 sys_request_logs 表的记录来复现请求,用于调试和错误排查
|
|
|
+ */
|
|
|
+class ReproduceErrorCommand extends Command
|
|
|
+{
|
|
|
+ /**
|
|
|
+ * 命令签名
|
|
|
+ *
|
|
|
+ * @var string
|
|
|
+ */
|
|
|
+ protected $signature = 'debug:reproduce-error
|
|
|
+ {identifier : 请求标识符(可以是id、request_unid或run_unid)}
|
|
|
+ {--type=auto : 标识符类型(id|request_unid|run_unid|auto),auto为自动检测}
|
|
|
+ {--timeout=30 : 请求超时时间(秒)}';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 命令描述
|
|
|
+ *
|
|
|
+ * @var string
|
|
|
+ */
|
|
|
+ protected $description = '通过请求日志记录复现错误请求,用于调试和问题排查';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * HTTP 客户端
|
|
|
+ *
|
|
|
+ * @var Client
|
|
|
+ */
|
|
|
+ protected Client $client;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 基础URL
|
|
|
+ *
|
|
|
+ * @var string
|
|
|
+ */
|
|
|
+ protected string $baseUrl;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 执行命令
|
|
|
+ */
|
|
|
+ public function handle()
|
|
|
+ {
|
|
|
+ $identifier = $this->argument('identifier');
|
|
|
+ $type = $this->option('type');
|
|
|
+ $timeout = (int) $this->option('timeout');
|
|
|
+
|
|
|
+ $this->info("开始查找请求记录...");
|
|
|
+ $this->info("标识符: {$identifier}");
|
|
|
+ $this->info("类型: {$type}");
|
|
|
+
|
|
|
+ // 查找请求记录
|
|
|
+ $requestLog = $this->findRequestLog($identifier, $type);
|
|
|
+
|
|
|
+ if (!$requestLog) {
|
|
|
+ $this->error("未找到匹配的请求记录");
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->info("找到请求记录:");
|
|
|
+ $this->line(" ID: {$requestLog->id}");
|
|
|
+ $this->line(" Request UNID: {$requestLog->request_unid}");
|
|
|
+ $this->line(" Run UNID: {$requestLog->run_unid}");
|
|
|
+ $this->line(" 路径: {$requestLog->path}");
|
|
|
+ $this->line(" 方法: {$requestLog->method}");
|
|
|
+ $this->line(" 创建时间: {$requestLog->created_at}");
|
|
|
+
|
|
|
+ // 检查必要的数据
|
|
|
+ if (empty($requestLog->protobuf_json)) {
|
|
|
+ $this->error("请求记录中缺少 protobuf_json 数据");
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析 headers 获取 token
|
|
|
+ $token = $this->extractToken($requestLog->headers);
|
|
|
+ if (!$token) {
|
|
|
+ $this->warn("未找到 token,将不携带 token 发起请求");
|
|
|
+ } else {
|
|
|
+ $this->info("提取到 token: " . substr($token, 0, 10) . "...");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化 HTTP 客户端
|
|
|
+ $this->initializeHttpClient($timeout);
|
|
|
+
|
|
|
+ // 发起请求
|
|
|
+ $this->info("开始发起请求...");
|
|
|
+ $response = $this->makeRequest($requestLog->protobuf_json, $token);
|
|
|
+
|
|
|
+ if ($response === null) {
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 输出结果
|
|
|
+ $this->info("请求完成,响应结果:");
|
|
|
+ $this->line("状态码: " . $response['status_code']);
|
|
|
+ $this->line("响应头:");
|
|
|
+ foreach ($response['headers'] as $name => $values) {
|
|
|
+ $this->line(" {$name}: " . implode(', ', $values));
|
|
|
+ }
|
|
|
+ $this->line("响应内容:");
|
|
|
+ $this->line($response['body']);
|
|
|
+
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查找请求记录
|
|
|
+ *
|
|
|
+ * @param string $identifier 标识符
|
|
|
+ * @param string $type 类型
|
|
|
+ * @return RequestLog|null
|
|
|
+ */
|
|
|
+ protected function findRequestLog(string $identifier, string $type): ?RequestLog
|
|
|
+ {
|
|
|
+ $query = RequestLog::query();
|
|
|
+
|
|
|
+ if ($type === 'auto') {
|
|
|
+ // 自动检测类型
|
|
|
+ if (is_numeric($identifier)) {
|
|
|
+ // 纯数字,优先按 ID 查找
|
|
|
+ $requestLog = $query->where('id', $identifier)->first();
|
|
|
+ if ($requestLog) {
|
|
|
+ return $requestLog;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按 request_unid 查找
|
|
|
+ $requestLog = RequestLog::query()->where('request_unid', $identifier)->first();
|
|
|
+ if ($requestLog) {
|
|
|
+ return $requestLog;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按 run_unid 查找
|
|
|
+ $requestLog = RequestLog::query()->where('run_unid', $identifier)->first();
|
|
|
+ if ($requestLog) {
|
|
|
+ return $requestLog;
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 指定类型查找
|
|
|
+ switch ($type) {
|
|
|
+ case 'id':
|
|
|
+ return $query->where('id', $identifier)->first();
|
|
|
+ case 'request_unid':
|
|
|
+ return $query->where('request_unid', $identifier)->first();
|
|
|
+ case 'run_unid':
|
|
|
+ return $query->where('run_unid', $identifier)->first();
|
|
|
+ default:
|
|
|
+ $this->error("不支持的类型: {$type}");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从 headers JSON 中提取 token
|
|
|
+ *
|
|
|
+ * @param string|null $headersJson
|
|
|
+ * @return string|null
|
|
|
+ */
|
|
|
+ protected function extractToken(?string $headersJson): ?string
|
|
|
+ {
|
|
|
+ if (empty($headersJson)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ $headers = json_decode($headersJson, true);
|
|
|
+ if (!is_array($headers)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查找 token 字段(可能在不同的键名下)
|
|
|
+ $tokenKeys = ['token', 'Token', 'authorization', 'Authorization'];
|
|
|
+
|
|
|
+ foreach ($tokenKeys as $key) {
|
|
|
+ if (isset($headers[$key])) {
|
|
|
+ $tokenValue = $headers[$key];
|
|
|
+ // headers 中的值可能是数组
|
|
|
+ if (is_array($tokenValue)) {
|
|
|
+ return $tokenValue[0] ?? null;
|
|
|
+ }
|
|
|
+ return $tokenValue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ $this->warn("解析 headers 失败: " . $e->getMessage());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化 HTTP 客户端
|
|
|
+ *
|
|
|
+ * @param int $timeout
|
|
|
+ */
|
|
|
+ protected function initializeHttpClient(int $timeout): void
|
|
|
+ {
|
|
|
+ $this->baseUrl = env('UNITTEST_URL', 'http://localhost:8000');
|
|
|
+ $this->info("目标地址: {$this->baseUrl}");
|
|
|
+
|
|
|
+ $this->client = new Client([
|
|
|
+ 'base_uri' => $this->baseUrl,
|
|
|
+ 'timeout' => $timeout,
|
|
|
+ 'http_errors' => false,
|
|
|
+ 'verify' => false, // 禁用 SSL 验证
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发起请求
|
|
|
+ *
|
|
|
+ * @param string $protobufJson
|
|
|
+ * @param string|null $token
|
|
|
+ * @return array|null
|
|
|
+ */
|
|
|
+ protected function makeRequest(string $protobufJson, ?string $token): ?array
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $headers = [
|
|
|
+ 'Content-Type' => 'application/json',
|
|
|
+ 'Accept' => 'application/json'
|
|
|
+ ];
|
|
|
+
|
|
|
+ if ($token) {
|
|
|
+ $headers['token'] = $token;
|
|
|
+ }
|
|
|
+
|
|
|
+ Log::info('复现请求开始', [
|
|
|
+ 'url' => $this->baseUrl . '/gameapi',
|
|
|
+ 'headers' => $headers,
|
|
|
+ 'body_length' => strlen($protobufJson)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $response = $this->client->post('/gameapi', [
|
|
|
+ 'body' => $protobufJson,
|
|
|
+ 'headers' => $headers
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $statusCode = $response->getStatusCode();
|
|
|
+ $responseHeaders = $response->getHeaders();
|
|
|
+ $responseBody = $response->getBody()->getContents();
|
|
|
+
|
|
|
+ Log::info('复现请求完成', [
|
|
|
+ 'status_code' => $statusCode,
|
|
|
+ 'response_length' => strlen($responseBody)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'status_code' => $statusCode,
|
|
|
+ 'headers' => $responseHeaders,
|
|
|
+ 'body' => $responseBody
|
|
|
+ ];
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ $this->error("请求失败: " . $e->getMessage());
|
|
|
+ Log::error('复现请求失败', [
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ 'trace' => $e->getTraceAsString()
|
|
|
+ ]);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|