Procházet zdrojové kódy

新增错误复现命令:通过请求日志记录复现错误请求用于调试

- 创建 ReproduceErrorCommand 命令类,支持通过 id/request_unid/run_unid 查找请求记录
- 参考 ProtoJsonRequestTest 实现,使用 GuzzleHttp 向 UNITTEST_URL 发起请求
- 自动提取请求记录中的 protobuf_json 和 headers.token 信息
- 支持自动检测标识符类型和自定义超时时间
- 完善的错误处理和日志记录功能
- 添加详细的使用文档和示例
notfff před 7 měsíci
rodič
revize
632ba4b5d9

+ 151 - 0
app/Console/Commands/README_ReproduceError.md

@@ -0,0 +1,151 @@
+# 错误复现命令 (ReproduceErrorCommand)
+
+## 概述
+
+`debug:reproduce-error` 命令用于通过 `sys_request_logs` 表中的请求记录来复现错误请求,帮助开发者进行调试和问题排查。
+
+## 功能特性
+
+- 支持通过 `id`、`request_unid` 或 `run_unid` 查找请求记录
+- 自动提取请求记录中的 `protobuf_json` 和 `token` 信息
+- 参考 `ProtoJsonRequestTest` 的实现方式发起 HTTP 请求
+- 使用环境变量 `UNITTEST_URL` 作为目标地址
+- 详细的日志记录和错误处理
+- 支持自定义请求超时时间
+
+## 命令语法
+
+```bash
+php artisan debug:reproduce-error <identifier> [options]
+```
+
+### 参数
+
+- `identifier`: 请求标识符,可以是以下任意一种:
+  - 数据库记录的 `id`(数字)
+  - `request_unid`(字符串)
+  - `run_unid`(字符串)
+
+### 选项
+
+- `--type=TYPE`: 指定标识符类型,可选值:
+  - `auto`(默认):自动检测类型
+  - `id`:按数据库 ID 查找
+  - `request_unid`:按请求唯一标识符查找
+  - `run_unid`:按运行唯一标识符查找
+
+- `--timeout=SECONDS`: 设置请求超时时间(秒),默认为 30 秒
+
+## 使用示例
+
+### 1. 通过数据库 ID 复现请求
+
+```bash
+php artisan debug:reproduce-error 68973973
+```
+
+### 2. 通过 request_unid 复现请求
+
+```bash
+php artisan debug:reproduce-error 6840202392627
+```
+
+### 3. 指定标识符类型
+
+```bash
+php artisan debug:reproduce-error 68973973 --type=id
+php artisan debug:reproduce-error 6840202392627 --type=request_unid
+```
+
+### 4. 设置自定义超时时间
+
+```bash
+php artisan debug:reproduce-error 68973973 --timeout=60
+```
+
+## 输出示例
+
+```
+开始查找请求记录...
+标识符: 68973973
+类型: auto
+找到请求记录:
+  ID: 68973973
+  Request UNID: 6840202392627
+  Run UNID: 
+  路径: gameapi
+  方法: POST
+  创建时间: 2025-06-04 18:29:55
+提取到 token: f4b3c51a58...
+目标地址: http://kku_laravel.local.gd/
+开始发起请求...
+请求完成,响应结果:
+状态码: 200
+响应头:
+  Server: nginx/1.27.4
+  Date: Wed, 04 Jun 2025 12:59:52 GMT
+  Content-Type: application/json
+  Transfer-Encoding: chunked
+  Connection: keep-alive
+  X-Powered-By: PHP/8.3.21
+  Cache-Control: no-cache, private
+  Vary: Origin
+响应内容:
+{"runUnid":"68404347a6a07","runMs":"6","code":"OK","callpath":"Shop-Query","lastData":[],"shopQuery":{"lastTimes":"1748943300"}}
+```
+
+## 环境配置
+
+确保在 `.env` 文件中配置了正确的 `UNITTEST_URL`:
+
+```env
+UNITTEST_URL=http://kku_laravel.local.gd/
+```
+
+## 错误处理
+
+命令会处理以下错误情况:
+
+1. **未找到请求记录**:当指定的标识符在数据库中不存在时
+2. **缺少 protobuf_json 数据**:当请求记录中没有必要的请求数据时
+3. **网络请求失败**:当向目标服务器发起请求失败时
+4. **JSON 解析错误**:当解析 headers 或响应数据失败时
+
+## 日志记录
+
+命令会在 Laravel 日志中记录详细的执行信息:
+
+- 请求开始和完成的时间戳
+- 请求和响应的数据长度
+- 错误信息和堆栈跟踪
+
+## 技术实现
+
+### 核心组件
+
+1. **RequestLog 模型**:用于查询 `sys_request_logs` 表
+2. **GuzzleHttp 客户端**:用于发起 HTTP 请求
+3. **JSON 解析**:提取 headers 中的 token 信息
+4. **错误处理**:完善的异常捕获和用户友好的错误提示
+
+### 请求流程
+
+1. 根据标识符和类型查找请求记录
+2. 验证请求记录的完整性
+3. 从 headers 中提取 token
+4. 初始化 HTTP 客户端
+5. 发起 POST 请求到 `/gameapi` 端点
+6. 解析和展示响应结果
+
+## 注意事项
+
+- 命令会禁用 SSL 验证以支持本地开发环境
+- Token 在输出时会被截断显示以保护敏感信息
+- 请求超时时间可以根据实际需要调整
+- 确保目标服务器能够正常处理复现的请求
+
+## 相关文件
+
+- 命令文件:`app/Console/Commands/ReproduceErrorCommand.php`
+- 模型文件:`UCore/Model/RequestLog.php`
+- 测试参考:`tests/Unit/ProtoJsonRequestTest.php`

+ 276 - 0
app/Console/Commands/ReproduceErrorCommand.php

@@ -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;
+        }
+    }
+}