| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055 |
- <?php
- namespace App\Console\Commands;
- use App\Module\AppGame\SessionApp;
- use App\Module\AppGame\SessionHelper;
- use GuzzleHttp\Client;
- use Illuminate\Console\Command;
- use UCore\Model\RequestLog;
- use Illuminate\Support\Facades\Log;
- use UCore\Helper\Logger;
- /**
- * 错误复现命令
- *
- * 通过 sys_request_logs 表的记录来复现请求,用于调试和错误排查
- * 默认使用 post 字段的原始请求数据,根据 headers 中的 Content-Type 自动检测数据类型
- * 根据响应的 Content-Type 头智能显示响应内容(JSON、HTML、Protobuf、文本等)
- *
- * 使用示例:
- * php artisan debug:reproduce-error 68981433 # 使用ID查找,自动选择数据源
- * php artisan debug:reproduce-error request_1749626545371 # 使用request_unid查找
- * php artisan debug:reproduce-error 68973982 --type=request_unid # 明确指定查找类型
- * php artisan debug:reproduce-error 68973982 --no-clear-logs # 不清空日志文件
- * php artisan debug:reproduce-error 68973982 --data-source=post # 强制使用post数据
- * php artisan debug:reproduce-error 68973982 --data-source=protobuf_json # 强制使用protobuf_json数据
- *
- */
- 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 : 请求超时时间(秒)} {--no-clear-logs : 运行时不前清空当前日志文件} {--data-source=auto : 数据源(post|protobuf_json|auto),auto为自动选择,优先使用post}';
- /**
- * 命令描述
- *
- * @var string
- */
- protected $description = '通过请求日志记录复现错误请求,用于调试和问题排查。支持运行前清空日志文件,默认使用post字段数据并根据headers自动检测数据类型,根据响应Content-Type智能显示内容';
- /**
- * 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');
- $noclearLogs = $this->option('no-clear-logs');
- $dataSource = $this->option('data-source');
- // 如果开启了清空日志选项,则清空日志文件
- if ($noclearLogs) {
- } else {
- $this->clearLogFiles();
- }
- Log::info("正在使用 debug:reproduce-error 命令重放请求,当前执行命令: 'php artisan debug:reproduce-error {$identifier}' ");
- $this->info("开始查找请求记录...");
- $this->info("标识符: {$identifier}");
- $this->info("类型: {$type}");
- $this->info("数据源: {$dataSource}");
- // 查找请求记录
- $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(" UserId : {$requestLog->user_id}");
- $this->line(" 路径: {$requestLog->path}");
- $this->line(" 方法: {$requestLog->method}");
- $this->line(" 创建时间: {$requestLog->created_at}");
- // 解析并显示请求头信息
- $headers = $this->parseHeaders($requestLog->headers);
- $this->displayRequestHeaders($headers);
- // 提取关键信息
- $contentType = $this->extractContentType($headers);
- $token = $this->extractToken($requestLog->headers);
- $headerAnalysis = $this->analyzeRequestHeaders($headers);
- // 设置用户会话
- if ($requestLog->user_id) {
- SessionHelper::sessionLogin($token, $requestLog->user_id);
- $this->info("token 设置用户: {$requestLog->user_id} ");
- }
- // 准备请求数据(使用头部分析结果)
- $requestData = $this->prepareRequestData($requestLog, $dataSource, $contentType, $headerAnalysis);
- if ($requestData === null) {
- return 1;
- }
- $this->displayRequestDataInfo($requestData);
- // 初始化 HTTP 客户端
- $this->initializeHttpClient($timeout);
- // 发起请求
- $this->info("开始发起请求...");
- $response = $this->makeRequest($requestData['data'], $token, $requestData['type']);
- if ($response === null) {
- return 1;
- }
- // 输出结果
- $this->line("响应内容:");
- $this->displayResponse($response);
- 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;
- }
- }
- /**
- * 显示请求头信息
- *
- * @param array $headers 解析后的请求头数组
- */
- protected function displayRequestHeaders(array $headers): void
- {
- if (empty($headers)) {
- $this->warn("未找到请求头信息");
- return;
- }
- $this->info("请求头信息:");
- foreach ($headers as $name => $values) {
- $value = is_array($values) ? implode(', ', $values) : $values;
- // 对敏感信息进行脱敏处理
- if (in_array(strtolower($name), ['token', 'authorization', 'cookie'])) {
- if (strlen($value) > 10) {
- $value = substr($value, 0, 10) . '...(已脱敏)';
- }
- }
- $this->line(" {$name}: {$value}");
- }
- }
- /**
- * 分析请求头信息
- *
- * @param array $headers 解析后的请求头数组
- * @return array 分析结果
- */
- protected function analyzeRequestHeaders(array $headers): array
- {
- $analysis = [
- 'content_type' => null,
- 'content_length' => null,
- 'user_agent' => null,
- 'accept' => null,
- 'encoding' => null,
- 'auth_type' => null,
- 'is_ajax' => false,
- 'is_mobile' => false,
- 'has_custom_headers' => false,
- 'security_headers' => [],
- 'custom_headers' => []
- ];
- foreach ($headers as $name => $values) {
- $value = is_array($values) ? ($values[0] ?? '') : $values;
- $lowerName = strtolower($name);
- switch ($lowerName) {
- case 'content-type':
- $analysis['content_type'] = $value;
- if (str_contains($value, 'charset=')) {
- $analysis['encoding'] = trim(explode('charset=', $value)[1]);
- }
- break;
- case 'content-length':
- $analysis['content_length'] = (int)$value;
- break;
- case 'user-agent':
- $analysis['user_agent'] = $value;
- $analysis['is_mobile'] = $this->isMobileUserAgent($value);
- break;
- case 'accept':
- $analysis['accept'] = $value;
- break;
- case 'authorization':
- $analysis['auth_type'] = 'Bearer/Basic';
- break;
- case 'token':
- $analysis['auth_type'] = 'Token';
- break;
- case 'x-requested-with':
- if (strtolower($value) === 'xmlhttprequest') {
- $analysis['is_ajax'] = true;
- }
- break;
- default:
- // 检测自定义头部
- if (str_starts_with($lowerName, 'x-') ||
- !in_array($lowerName, ['host', 'connection', 'cache-control', 'pragma', 'accept-encoding', 'accept-language'])) {
- $analysis['has_custom_headers'] = true;
- $analysis['custom_headers'][$name] = $value;
- }
- // 检测安全相关头部
- if (in_array($lowerName, ['x-csrf-token', 'x-xsrf-token', 'x-frame-options', 'x-content-type-options'])) {
- $analysis['security_headers'][$name] = $value;
- }
- break;
- }
- }
- $this->displayHeaderAnalysis($analysis);
- return $analysis;
- }
- /**
- * 显示请求头分析结果
- *
- * @param array $analysis 分析结果
- */
- protected function displayHeaderAnalysis(array $analysis): void
- {
- $this->info("请求头分析:");
- if ($analysis['content_type']) {
- $this->line(" Content-Type: {$analysis['content_type']}");
- }
- if ($analysis['content_length']) {
- $this->line(" Content-Length: {$analysis['content_length']} 字节");
- }
- if ($analysis['auth_type']) {
- $this->line(" 认证方式: {$analysis['auth_type']}");
- }
- if ($analysis['is_ajax']) {
- $this->line(" 请求类型: AJAX请求");
- }
- if ($analysis['is_mobile']) {
- $this->line(" 客户端: 移动设备");
- }
- if ($analysis['has_custom_headers']) {
- $this->line(" 自定义头部: " . count($analysis['custom_headers']) . " 个");
- }
- if (!empty($analysis['security_headers'])) {
- $this->line(" 安全头部: " . implode(', ', array_keys($analysis['security_headers'])));
- }
- }
- /**
- * 检测是否为移动设备User-Agent
- *
- * @param string $userAgent
- * @return bool
- */
- protected function isMobileUserAgent(string $userAgent): bool
- {
- $mobileKeywords = ['Mobile', 'Android', 'iPhone', 'iPad', 'Windows Phone', 'BlackBerry'];
- foreach ($mobileKeywords as $keyword) {
- if (str_contains($userAgent, $keyword)) {
- return true;
- }
- }
- return false;
- }
- /**
- * 解析 headers JSON 字符串
- *
- * @param string|null $headersJson
- * @return array
- */
- protected function parseHeaders(?string $headersJson): array
- {
- if (empty($headersJson)) {
- return [];
- }
- try {
- $headers = json_decode($headersJson, true);
- return is_array($headers) ? $headers : [];
- } catch (\Exception $e) {
- $this->warn("解析 headers 失败: " . $e->getMessage());
- return [];
- }
- }
- /**
- * 从 headers 中提取 Content-Type
- *
- * @param array $headers
- * @return string|null
- */
- protected function extractContentType(array $headers): ?string
- {
- $contentTypeKeys = ['content-type', 'Content-Type', 'CONTENT-TYPE'];
- foreach ($contentTypeKeys as $key) {
- if (isset($headers[$key])) {
- $contentType = $headers[$key];
- // headers 中的值可能是数组
- if (is_array($contentType)) {
- return $contentType[0] ?? null;
- }
- return $contentType;
- }
- }
- return null;
- }
- /**
- * 从 headers JSON 中提取 token
- *
- * @param string|null $headersJson
- * @return string|null
- */
- protected function extractToken(?string $headersJson): ?string
- {
- $headers = $this->parseHeaders($headersJson);
- // 查找 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;
- }
- /**
- * 准备请求数据
- *
- * @param RequestLog $requestLog
- * @param string $dataSource
- * @param string|null $contentType
- * @param array $headerAnalysis
- * @return array|null
- */
- protected function prepareRequestData(RequestLog $requestLog, string $dataSource, ?string $contentType, array $headerAnalysis = []): ?array
- {
- // 根据数据源选择策略,结合头部分析结果
- if ($dataSource === 'auto') {
- // 智能选择:根据头部分析结果决定最佳数据源
- $bestSource = $this->determineBestDataSource($requestLog, $headerAnalysis);
- $this->info("根据请求头分析,推荐使用数据源: {$bestSource}");
- if ($bestSource === 'post' && !empty($requestLog->post)) {
- return $this->preparePostData($requestLog->post, $contentType, $headerAnalysis);
- } elseif ($bestSource === 'protobuf_json' && !empty($requestLog->protobuf_json)) {
- return $this->prepareProtobufJsonData($requestLog->protobuf_json, $headerAnalysis);
- } else {
- // 回退到原有逻辑
- if (!empty($requestLog->post)) {
- return $this->preparePostData($requestLog->post, $contentType, $headerAnalysis);
- } elseif (!empty($requestLog->protobuf_json)) {
- return $this->prepareProtobufJsonData($requestLog->protobuf_json, $headerAnalysis);
- } else {
- $this->error("请求记录中既没有 post 数据也没有 protobuf_json 数据");
- return null;
- }
- }
- } elseif ($dataSource === 'post') {
- if (empty($requestLog->post)) {
- $this->error("请求记录中缺少 post 数据");
- return null;
- }
- return $this->preparePostData($requestLog->post, $contentType, $headerAnalysis);
- } elseif ($dataSource === 'protobuf_json') {
- if (empty($requestLog->protobuf_json)) {
- $this->error("请求记录中缺少 protobuf_json 数据");
- return null;
- }
- return $this->prepareProtobufJsonData($requestLog->protobuf_json, $headerAnalysis);
- } else {
- $this->error("不支持的数据源: {$dataSource}");
- return null;
- }
- }
- /**
- * 根据请求头分析确定最佳数据源
- *
- * @param RequestLog $requestLog
- * @param array $headerAnalysis
- * @return string
- */
- protected function determineBestDataSource(RequestLog $requestLog, array $headerAnalysis): string
- {
- // 如果Content-Type明确指示JSON,优先使用protobuf_json
- if ($headerAnalysis['content_type'] &&
- str_contains(strtolower($headerAnalysis['content_type']), 'json')) {
- return 'protobuf_json';
- }
- // 如果Content-Type指示protobuf或二进制,优先使用post
- if ($headerAnalysis['content_type'] &&
- (str_contains(strtolower($headerAnalysis['content_type']), 'protobuf') ||
- str_contains(strtolower($headerAnalysis['content_type']), 'octet-stream'))) {
- return 'post';
- }
- // 如果是AJAX请求,通常是JSON格式
- if ($headerAnalysis['is_ajax']) {
- return 'protobuf_json';
- }
- // 默认优先使用post数据
- return 'post';
- }
- /**
- * 准备 post 数据
- *
- * @param string $postData base64 编码的原始请求数据
- * @param string|null $contentType
- * @param array $headerAnalysis
- * @return array
- */
- protected function preparePostData(string $postData, ?string $contentType, array $headerAnalysis = []): array
- {
- // 解码 base64 数据
- $rawData = base64_decode($postData);
- // 根据请求头分析和 Content-Type 判断数据类型
- $dataType = $this->determineDataType($contentType, $headerAnalysis, $rawData);
- $result = [
- 'source' => 'post',
- 'type' => $dataType,
- 'data' => $rawData,
- 'analysis' => [
- 'original_size' => strlen($postData),
- 'decoded_size' => strlen($rawData),
- 'content_type' => $contentType,
- 'detected_type' => $dataType
- ]
- ];
- // 如果有头部分析信息,添加到结果中
- if (!empty($headerAnalysis)) {
- $result['header_context'] = [
- 'is_ajax' => $headerAnalysis['is_ajax'] ?? false,
- 'is_mobile' => $headerAnalysis['is_mobile'] ?? false,
- 'auth_type' => $headerAnalysis['auth_type'] ?? null,
- 'content_length' => $headerAnalysis['content_length'] ?? null
- ];
- }
- return $result;
- }
- /**
- * 确定数据类型
- *
- * @param string|null $contentType
- * @param array $headerAnalysis
- * @param string $rawData
- * @return string
- */
- protected function determineDataType(?string $contentType, array $headerAnalysis, string $rawData): string
- {
- // 优先根据Content-Type判断
- if ($contentType) {
- if (stripos($contentType, 'json') !== false) {
- return 'json';
- }
- if (stripos($contentType, 'protobuf') !== false ||
- stripos($contentType, 'octet-stream') !== false) {
- return 'protobuf';
- }
- }
- // 根据请求头分析判断
- if (!empty($headerAnalysis['is_ajax'])) {
- return 'json';
- }
- // 尝试解析数据内容判断
- if ($this->looksLikeJson($rawData)) {
- return 'json';
- }
- // 默认为protobuf
- return 'protobuf';
- }
- /**
- * 检查数据是否看起来像JSON
- *
- * @param string $data
- * @return bool
- */
- protected function looksLikeJson(string $data): bool
- {
- // 检查是否以JSON常见字符开始
- $trimmed = trim($data);
- if (empty($trimmed)) {
- return false;
- }
- $firstChar = $trimmed[0];
- if (in_array($firstChar, ['{', '[', '"'])) {
- // 尝试解析JSON
- json_decode($trimmed);
- return json_last_error() === JSON_ERROR_NONE;
- }
- return false;
- }
- /**
- * 准备 protobuf_json 数据
- *
- * @param string $protobufJson
- * @param array $headerAnalysis
- * @return array
- */
- protected function prepareProtobufJsonData(string $protobufJson, array $headerAnalysis = []): array
- {
- $result = [
- 'source' => 'protobuf_json',
- 'type' => 'json',
- 'data' => $protobufJson,
- 'analysis' => [
- 'data_size' => strlen($protobufJson),
- 'detected_type' => 'json'
- ]
- ];
- // 如果有头部分析信息,添加到结果中
- if (!empty($headerAnalysis)) {
- $result['header_context'] = [
- 'is_ajax' => $headerAnalysis['is_ajax'] ?? false,
- 'is_mobile' => $headerAnalysis['is_mobile'] ?? false,
- 'auth_type' => $headerAnalysis['auth_type'] ?? null
- ];
- }
- return $result;
- }
- /**
- * 显示请求数据信息
- *
- * @param array $requestData
- */
- protected function displayRequestDataInfo(array $requestData): void
- {
- $this->info("请求数据信息:");
- $this->line(" 数据源: {$requestData['source']}");
- $this->line(" 数据类型: {$requestData['type']}");
- // 显示分析信息
- if (isset($requestData['analysis'])) {
- $analysis = $requestData['analysis'];
- $this->line(" 数据分析:");
- if (isset($analysis['decoded_size'])) {
- $this->line(" 原始大小: {$analysis['original_size']} 字节");
- $this->line(" 解码后大小: {$analysis['decoded_size']} 字节");
- } elseif (isset($analysis['data_size'])) {
- $this->line(" 数据大小: {$analysis['data_size']} 字节");
- }
- if (isset($analysis['content_type'])) {
- $this->line(" Content-Type: {$analysis['content_type']}");
- }
- if (isset($analysis['detected_type'])) {
- $this->line(" 检测类型: {$analysis['detected_type']}");
- }
- }
- // 显示头部上下文信息
- if (isset($requestData['header_context'])) {
- $context = $requestData['header_context'];
- $contextInfo = [];
- if ($context['is_ajax']) {
- $contextInfo[] = 'AJAX请求';
- }
- if ($context['is_mobile']) {
- $contextInfo[] = '移动设备';
- }
- if ($context['auth_type']) {
- $contextInfo[] = "认证: {$context['auth_type']}";
- }
- if (!empty($contextInfo)) {
- $this->line(" 请求上下文: " . implode(', ', $contextInfo));
- }
- }
- // 显示数据预览
- $this->info("请求数据预览:");
- if ($requestData['type'] === 'json') {
- $jsonData = json_decode($requestData['data'], true);
- if ($jsonData !== null) {
- dump($jsonData);
- } else {
- $this->warn("数据类型标记为JSON但解析失败");
- $this->line("原始数据: " . substr($requestData['data'], 0, 200) . "...");
- }
- } else {
- $this->line(" 二进制数据长度: " . strlen($requestData['data']) . " 字节");
- $this->line(" Base64预览: " . substr(base64_encode($requestData['data']), 0, 100) . "...");
- // 尝试检测是否包含可读文本
- if (mb_check_encoding($requestData['data'], 'UTF-8')) {
- $preview = substr($requestData['data'], 0, 100);
- if (ctype_print($preview)) {
- $this->line(" 文本预览: {$preview}...");
- }
- }
- }
- }
- /**
- * 初始化 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 $requestData 请求数据
- * @param string|null $token 认证token
- * @param string $dataType 数据类型 (json|protobuf)
- * @return array|null
- */
- protected function makeRequest(string $requestData, ?string $token, string $dataType): ?array
- {
- try {
- // 根据数据类型设置不同的 headers
- if ($dataType === 'json') {
- $headers = [
- 'Content-Type' => 'application/json',
- 'Accept' => 'application/json'
- ];
- } else {
- // protobuf 二进制格式
- $headers = [
- 'Content-Type' => 'application/x-protobuf',
- 'Accept' => 'application/x-protobuf',
- 'Force-Json'=>'1'
- ];
- }
- if ($token) {
- $headers['token'] = $token;
- }
- Log::info('复现请求开始', [
- 'url' => $this->baseUrl . '/gameapi',
- 'headers' => $headers,
- 'body_length' => strlen($requestData),
- 'data_type' => $dataType
- ]);
- $response = $this->client->post('/gameapi', [
- 'body' => $requestData,
- 'headers' => $headers
- ]);
- $statusCode = $response->getStatusCode();
- $responseHeaders = $response->getHeaders();
- $responseBody = $response->getBody()->getContents();
- Log::info('复现请求完成', [
- 'status_code' => $statusCode,
- 'response_length' => strlen($responseBody),
- 'data_type' => $dataType
- ]);
- return [
- 'status_code' => $statusCode,
- 'headers' => $responseHeaders,
- 'body' => $responseBody
- ];
- } catch (\Exception $e) {
- $this->error("请求失败: " . $e->getMessage());
- Log::error('复现请求失败', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString(),
- 'data_type' => $dataType
- ]);
- return null;
- }
- }
- /**
- * 显示响应内容
- *
- * @param array $response 响应数据(包含status_code、headers、body)
- */
- protected function displayResponse(array $response): void
- {
- $this->line("状态码: " . $response['status_code']);
- // 显示响应头信息
- $this->line("响应头:");
- foreach ($response['headers'] as $name => $values) {
- $value = is_array($values) ? implode(', ', $values) : $values;
- $this->line(" {$name}: {$value}");
- }
- // 从响应头中提取Content-Type
- $responseContentType = $this->extractResponseContentType($response['headers']);
- $this->line("检测到响应Content-Type: " . ($responseContentType ?: '未知'));
- // 根据响应Content-Type决定显示方式
- $responseBody = $response['body'];
- $bodyLength = strlen($responseBody);
- $this->line("响应体长度: {$bodyLength} 字节");
- if ($this->isJsonContentType($responseContentType)) {
- // JSON 响应
- $this->line("按JSON格式解析响应:");
- $jsonData = json_decode($responseBody, true);
- if ($jsonData !== null) {
- dump($jsonData);
- } else {
- $this->warn("响应Content-Type为JSON但解析失败");
- $this->displayRawContent($responseBody);
- }
- } elseif ($this->isProtobufContentType($responseContentType)) {
- // Protobuf 二进制响应
- $this->line("按Protobuf格式处理响应:");
- $this->line("Base64 编码: " . substr(base64_encode($responseBody), 0, 200) . "...");
- // 尝试解析为 JSON(某些情况下服务器可能返回 JSON)
- $jsonData = json_decode($responseBody, true);
- if ($jsonData !== null) {
- $this->line("响应可解析为 JSON:");
- dump($jsonData);
- } else {
- $this->line("响应为二进制 Protobuf 数据,无法直接显示");
- }
- } elseif ($this->isHtmlContentType($responseContentType)) {
- // HTML 响应
- $this->line("按HTML格式处理响应:");
- $this->displayHtmlContent($responseBody);
- } elseif ($this->isTextContentType($responseContentType)) {
- // 纯文本响应
- $this->line("按文本格式显示响应:");
- $this->line($responseBody);
- } else {
- // 未知类型,尝试智能检测
- $this->line("未知Content-Type,尝试智能检测:");
- $this->smartDisplayContent($responseBody);
- }
- }
- /**
- * 从响应头中提取Content-Type
- *
- * @param array $headers 响应头数组
- * @return string|null
- */
- protected function extractResponseContentType(array $headers): ?string
- {
- // 尝试不同的Content-Type键名(大小写不敏感)
- $contentTypeKeys = ['Content-Type', 'content-type', 'Content-type', 'CONTENT-TYPE'];
- foreach ($contentTypeKeys as $key) {
- if (isset($headers[$key])) {
- $value = $headers[$key];
- // 如果是数组,取第一个值
- if (is_array($value)) {
- $value = $value[0] ?? '';
- }
- // 只取分号前的部分(去掉charset等参数)
- return trim(explode(';', $value)[0]);
- }
- }
- return null;
- }
- /**
- * 判断是否为JSON内容类型
- *
- * @param string|null $contentType
- * @return bool
- */
- protected function isJsonContentType(?string $contentType): bool
- {
- if (!$contentType) {
- return false;
- }
- $jsonTypes = [
- 'application/json',
- 'text/json',
- 'application/ld+json'
- ];
- return in_array(strtolower($contentType), $jsonTypes);
- }
- /**
- * 判断是否为Protobuf内容类型
- *
- * @param string|null $contentType
- * @return bool
- */
- protected function isProtobufContentType(?string $contentType): bool
- {
- if (!$contentType) {
- return false;
- }
- $protobufTypes = [
- 'application/x-protobuf',
- 'application/protobuf',
- 'application/octet-stream'
- ];
- return in_array(strtolower($contentType), $protobufTypes);
- }
- /**
- * 判断是否为HTML内容类型
- *
- * @param string|null $contentType
- * @return bool
- */
- protected function isHtmlContentType(?string $contentType): bool
- {
- if (!$contentType) {
- return false;
- }
- $htmlTypes = [
- 'text/html',
- 'application/xhtml+xml'
- ];
- return in_array(strtolower($contentType), $htmlTypes);
- }
- /**
- * 判断是否为文本内容类型
- *
- * @param string|null $contentType
- * @return bool
- */
- protected function isTextContentType(?string $contentType): bool
- {
- if (!$contentType) {
- return false;
- }
- return str_starts_with(strtolower($contentType), 'text/');
- }
- /**
- * 显示HTML内容
- *
- * @param string $content
- */
- protected function displayHtmlContent(string $content): void
- {
- // 检查是否包含错误页面标识
- if (str_contains($content, '<title>') && str_contains($content, '</title>')) {
- preg_match('/<title>(.*?)<\/title>/i', $content, $matches);
- $title = $matches[1] ?? '未知';
- $this->line("HTML页面标题: {$title}");
- }
- // 显示部分内容
- $preview = substr(strip_tags($content), 0, 500);
- $this->line("HTML内容预览: " . $preview . "...");
- // 检查是否为错误页面
- if (str_contains($content, 'error') || str_contains($content, 'Error') || str_contains($content, 'exception')) {
- $this->warn("检测到可能的错误页面");
- }
- }
- /**
- * 显示原始内容
- *
- * @param string $content
- */
- protected function displayRawContent(string $content): void
- {
- $this->line("原始响应内容:");
- if (strlen($content) > 1000) {
- $this->line(substr($content, 0, 1000) . "...");
- $this->line("(内容过长,已截断显示)");
- } else {
- $this->line($content);
- }
- }
- /**
- * 智能检测并显示内容
- *
- * @param string $content
- */
- protected function smartDisplayContent(string $content): void
- {
- // 尝试JSON解析
- $jsonData = json_decode($content, true);
- if ($jsonData !== null) {
- $this->line("内容可解析为JSON:");
- dump($jsonData);
- return;
- }
- // 检查是否为HTML
- if (str_contains($content, '<html') || str_contains($content, '<!DOCTYPE')) {
- $this->line("检测到HTML内容:");
- $this->displayHtmlContent($content);
- return;
- }
- // 检查是否为二进制数据
- if (!mb_check_encoding($content, 'UTF-8')) {
- $this->line("检测到二进制数据:");
- $this->line("数据长度: " . strlen($content) . " 字节");
- $this->line("Base64 编码: " . substr(base64_encode($content), 0, 200) . "...");
- return;
- }
- // 默认按文本显示
- $this->line("按文本格式显示:");
- $this->displayRawContent($content);
- }
- /**
- * 清空当前日志文件
- */
- protected function clearLogFiles(): void
- {
- Logger::clear_log();
- Logger::debug('旧的日志已经清理');
- }
- }
|