user()?->isSuperAdmin(), 403, '仅超级管理员可查看系统日志'); $logsDir = storage_path('logs'); $files = $this->listLogFiles($logsDir); $requested = basename((string) $request->query('file', '')); $selected = ''; if ($requested !== '' && $requested !== '.' && $this->isAllowedBasename($requested)) { $candidate = $logsDir.DIRECTORY_SEPARATOR.$requested; if (is_file($candidate)) { $selected = $requested; } } if ($selected === '' && $files !== []) { $selected = $files[0]['name']; } $linesParam = (int) $request->query('lines', self::DEFAULT_LINES); $lines = max(50, min(self::MAX_LINES, $linesParam)); $linesPayload = []; $error = null; if ($selected !== '') { $fullPath = $logsDir.DIRECTORY_SEPARATOR.$selected; if (! is_file($fullPath) || ! is_readable($fullPath)) { $error = '日志文件不可读'; } else { $linesPayload = $this->tailLines($fullPath, $lines); } } return response()->json([ 'files' => $files, 'file' => $selected, 'lines' => $linesPayload, 'lines_requested' => $lines, 'error' => $error, ]); } /** * @return array */ private function listLogFiles(string $logsDir): array { if (! is_dir($logsDir)) { return []; } $pattern = $logsDir.DIRECTORY_SEPARATOR.'laravel*.log'; $paths = glob($pattern) ?: []; $files = []; foreach ($paths as $full) { $base = basename($full); if (! $this->isAllowedBasename($base)) { continue; } $mtime = @filemtime($full); $size = @filesize($full); $files[] = [ 'name' => $base, 'size_bytes' => $size !== false ? $size : 0, 'modified_at' => $mtime ? date('c', $mtime) : '', ]; } usort($files, static function (array $a, array $b): int { return strcmp($b['modified_at'], $a['modified_at']); }); return array_values($files); } /** * @return array */ private function tailLines(string $path, int $maxLines): array { $size = filesize($path); if ($size === false || $size === 0) { return []; } $maxLines = max(1, min(self::MAX_LINES, $maxLines)); $chunk = min($size, 262144); while ($chunk <= $size) { $start = $size - $chunk; $data = file_get_contents($path, false, null, $start, $chunk); if ($data === false) { return []; } if ($start > 0) { $firstNl = strpos($data, "\n"); if ($firstNl !== false) { $data = substr($data, $firstNl + 1); } } $parts = preg_split('/\r\n|\r|\n/', $data); if ($parts === false) { $parts = []; } if (count($parts) >= $maxLines || $chunk >= $size) { return array_values(array_slice($parts, -$maxLines)); } $chunk = min($size, $chunk * 2); } return []; } }