|
|
@@ -100,18 +100,14 @@ class ReproduceErrorCommand extends Command
|
|
|
$this->line(" 方法: {$requestLog->method}");
|
|
|
$this->line(" 创建时间: {$requestLog->created_at}");
|
|
|
|
|
|
- // 解析 headers 获取 Content-Type 和 token
|
|
|
+ // 解析并显示请求头信息
|
|
|
$headers = $this->parseHeaders($requestLog->headers);
|
|
|
+ $this->displayRequestHeaders($headers);
|
|
|
+
|
|
|
+ // 提取关键信息
|
|
|
$contentType = $this->extractContentType($headers);
|
|
|
$token = $this->extractToken($requestLog->headers);
|
|
|
-
|
|
|
- $this->info("Content-Type: " . ($contentType ?: '未检测到'));
|
|
|
-
|
|
|
- if (!$token) {
|
|
|
- $this->warn("未找到 token,将不携带 token 发起请求");
|
|
|
- } else {
|
|
|
- $this->info("提取到 token: " . substr($token, 0, 10) . "...");
|
|
|
- }
|
|
|
+ $headerAnalysis = $this->analyzeRequestHeaders($headers);
|
|
|
|
|
|
// 设置用户会话
|
|
|
if ($requestLog->user_id) {
|
|
|
@@ -119,21 +115,13 @@ class ReproduceErrorCommand extends Command
|
|
|
$this->info("token 设置用户: {$requestLog->user_id} ");
|
|
|
}
|
|
|
|
|
|
- // 准备请求数据
|
|
|
- $requestData = $this->prepareRequestData($requestLog, $dataSource, $contentType);
|
|
|
+ // 准备请求数据(使用头部分析结果)
|
|
|
+ $requestData = $this->prepareRequestData($requestLog, $dataSource, $contentType, $headerAnalysis);
|
|
|
if ($requestData === null) {
|
|
|
return 1;
|
|
|
}
|
|
|
|
|
|
- $this->info("使用数据源: {$requestData['source']}");
|
|
|
- $this->info("检测到数据类型: {$requestData['type']}");
|
|
|
- $this->info("请求数据预览:");
|
|
|
- if ($requestData['type'] === 'json') {
|
|
|
- dump(json_decode($requestData['data'], true));
|
|
|
- } else {
|
|
|
- $this->line(" 二进制数据长度: " . strlen($requestData['data']) . " 字节");
|
|
|
- $this->line(" Base64预览: " . substr(base64_encode($requestData['data']), 0, 100) . "...");
|
|
|
- }
|
|
|
+ $this->displayRequestDataInfo($requestData);
|
|
|
|
|
|
// 初始化 HTTP 客户端
|
|
|
$this->initializeHttpClient($timeout);
|
|
|
@@ -204,6 +192,164 @@ class ReproduceErrorCommand extends Command
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 显示请求头信息
|
|
|
+ *
|
|
|
+ * @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 字符串
|
|
|
*
|
|
|
@@ -283,82 +429,286 @@ class ReproduceErrorCommand extends Command
|
|
|
* @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
|
|
|
+ protected function prepareRequestData(RequestLog $requestLog, string $dataSource, ?string $contentType, array $headerAnalysis = []): ?array
|
|
|
{
|
|
|
- // 根据数据源选择策略
|
|
|
+ // 根据数据源选择策略,结合头部分析结果
|
|
|
if ($dataSource === 'auto') {
|
|
|
- // 自动选择:优先使用 post 数据,如果没有则使用 protobuf_json
|
|
|
- if (!empty($requestLog->post)) {
|
|
|
- return $this->preparePostData($requestLog->post, $contentType);
|
|
|
- } elseif (!empty($requestLog->protobuf_json)) {
|
|
|
- return $this->prepareProtobufJsonData($requestLog->protobuf_json);
|
|
|
+ // 智能选择:根据头部分析结果决定最佳数据源
|
|
|
+ $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 {
|
|
|
- $this->error("请求记录中既没有 post 数据也没有 protobuf_json 数据");
|
|
|
- return null;
|
|
|
+ // 回退到原有逻辑
|
|
|
+ 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);
|
|
|
+ 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);
|
|
|
+ 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
|
|
|
+ protected function preparePostData(string $postData, ?string $contentType, array $headerAnalysis = []): array
|
|
|
{
|
|
|
// 解码 base64 数据
|
|
|
$rawData = base64_decode($postData);
|
|
|
|
|
|
- // 根据 Content-Type 判断数据类型
|
|
|
- if ($contentType && stripos($contentType, 'json') !== false) {
|
|
|
- // JSON 格式数据
|
|
|
- return [
|
|
|
- 'source' => 'post',
|
|
|
- 'type' => 'json',
|
|
|
- 'data' => $rawData
|
|
|
- ];
|
|
|
- } else {
|
|
|
- // 默认为 protobuf 二进制格式
|
|
|
- return [
|
|
|
- 'source' => 'post',
|
|
|
- 'type' => 'protobuf',
|
|
|
- 'data' => $rawData
|
|
|
+ // 根据请求头分析和 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
|
|
|
+ protected function prepareProtobufJsonData(string $protobufJson, array $headerAnalysis = []): array
|
|
|
{
|
|
|
- return [
|
|
|
+ $result = [
|
|
|
'source' => 'protobuf_json',
|
|
|
'type' => 'json',
|
|
|
- 'data' => $protobufJson
|
|
|
+ '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}...");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|