isProduction = app()->environment('production'); $this->mergeTool = $this->detectMergeTool(); } /** * 检测系统中可用的PDF合并工具 */ private function detectMergeTool(): string { // 生产环境优先使用pdfunite if ($this->isProduction) { if ($this->commandExists('pdfunite')) { return 'pdfunite'; } } // 本地开发环境使用qpdf if ($this->commandExists('qpdf')) { return 'qpdf'; } // 备选:尝试pdfunite if ($this->commandExists('pdfunite')) { return 'pdfunite'; } throw new \Exception('未找到可用的PDF合并工具(pdfunite或qpdf)'); } /** * 检查命令是否存在 */ private function commandExists(string $command): bool { // 【修复】异步环境中PATH可能不完整,尝试绝对路径 $fullPaths = [ "/opt/homebrew/bin/{$command}", "/usr/bin/{$command}", "/usr/local/bin/{$command}", ]; Log::debug("检查命令是否存在: {$command}", [ 'checking_absolute_paths' => $fullPaths ]); // 首先尝试绝对路径 foreach ($fullPaths as $path) { if (file_exists($path) && is_executable($path)) { Log::debug("找到命令(绝对路径): {$command} -> {$path}"); return true; } } // 如果绝对路径都不行,再尝试which命令 Log::debug("绝对路径未找到,尝试which命令: {$command}"); $output = Process::run("which {$command}"); $result = $output->successful(); Log::debug("which命令结果", [ 'command' => $command, 'successful' => $result, 'output' => $output->output(), 'error' => $output->errorOutput() ]); return $result; } /** * 合并多个PDF文件 * * @param array $pdfPaths PDF文件路径数组 * @param string $outputPath 输出文件路径 * @return bool 合并是否成功 * @throws \Exception */ public function merge(array $pdfPaths, string $outputPath): bool { // 验证输入文件 foreach ($pdfPaths as $path) { if (!file_exists($path)) { throw new \Exception("PDF文件不存在: {$path}"); } } // 确保输出目录存在 $outputDir = dirname($outputPath); if (!is_dir($outputDir)) { mkdir($outputDir, 0755, true); } Log::info('开始合并PDF', [ 'tool' => $this->mergeTool, 'input_count' => count($pdfPaths), 'output_path' => $outputPath ]); try { switch ($this->mergeTool) { case 'pdfunite': return $this->mergeWithPdfunite($pdfPaths, $outputPath); case 'qpdf': return $this->mergeWithQpdf($pdfPaths, $outputPath); default: throw new \Exception("不支持的合并工具: {$this->mergeTool}"); } } catch (\Exception $e) { Log::error('PDF合并失败', [ 'tool' => $this->mergeTool, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); throw $e; } } /** * 使用pdfunite合并PDF * pdfunite file1.pdf file2.pdf ... output.pdf */ private function mergeWithPdfunite(array $pdfPaths, string $outputPath): bool { $command = 'pdfunite ' . implode(' ', array_map('escapeshellarg', $pdfPaths)) . ' ' . escapeshellarg($outputPath); Log::debug('执行pdfunite命令', ['command' => $command]); $output = Process::run($command); if ($output->successful()) { Log::info('pdfunite合并成功', ['output_path' => $outputPath]); return true; } Log::error('pdfunite合并失败', [ 'exit_code' => $output->exitCode(), 'output' => $output->output(), 'error' => $output->errorOutput() ]); return false; } /** * 使用qpdf合并PDF * qpdf --empty --pages file1.pdf file2.pdf -- -- output.pdf */ private function mergeWithQpdf(array $pdfPaths, string $outputPath): bool { // 构建qpdf命令 $pagesArg = implode(' ', array_map('escapeshellarg', $pdfPaths)); $command = "qpdf --empty --pages {$pagesArg} -- -- " . escapeshellarg($outputPath); Log::debug('执行qpdf命令', ['command' => $command]); $output = Process::run($command); if ($output->successful()) { Log::info('qpdf合并成功', ['output_path' => $outputPath]); return true; } Log::error('qpdf合并失败', [ 'exit_code' => $output->exitCode(), 'output' => $output->output(), 'error' => $output->errorOutput() ]); return false; } /** * 获取当前使用的合并工具 */ public function getMergeTool(): string { return $this->mergeTool; } /** * 检查环境是否支持PDF合并 */ public function isSupported(): bool { return in_array($this->mergeTool, ['pdfunite', 'qpdf']); } }