| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326 |
- <?php
- namespace App\Console\Commands;
- use GuzzleHttp\Client;
- use Illuminate\Console\Command;
- use UCore\Model\RequestLog;
- use Illuminate\Support\Facades\Log;
- use UCore\Helper\Logger;
- /**
- * 错误复现命令
- *
- * 通过 sys_request_logs 表的记录来复现请求,用于调试和错误排查
- * php artisan debug:reproduce-error 68973982
- * php artisan debug:reproduce-error 68973982 --type=request_unid
- * php artisan debug:reproduce-error 68973982 --clear-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 : 请求超时时间(秒)} {--clear-logs : 运行前清空当前日志文件}';
- /**
- * 命令描述
- *
- * @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');
- $clearLogs = $this->option('clear-logs');
- // 如果开启了清空日志选项,则清空日志文件
- if ($clearLogs) {
- $this->clearLogFiles();
- }
- $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;
- }
- }
- /**
- * 清空当前日志文件
- */
- protected function clearLogFiles(): void
- {
- $this->info("正在清空日志文件...");
- try {
- // 获取当前日志文件路径
- $logPath = storage_path('logs');
- $currentDate = date('Y-m-d');
- // Laravel 默认日志文件名格式
- $logFiles = [
- $logPath . '/laravel.log',
- $logPath . "/laravel-{$currentDate}.log",
- ];
- $clearedCount = 0;
- foreach ($logFiles as $logFile) {
- if (file_exists($logFile)) {
- // 清空文件内容但保留文件
- file_put_contents($logFile, '');
- $clearedCount++;
- $this->line("已清空: " . basename($logFile));
- }
- }
- if ($clearedCount > 0) {
- $this->info("成功清空 {$clearedCount} 个日志文件");
- } else {
- $this->warn("未找到需要清空的日志文件");
- }
- Logger::debug('旧的日志已经清理');
- } catch (\Exception $e) {
- $this->error("清空日志文件失败: " . $e->getMessage());
- }
- }
- }
|