Procházet zdrojové kódy

卷子生成pdf效果制作

yemeishu před 2 týdny
rodič
revize
f2dc91d748
1 změnil soubory, kde provedl 168 přidání a 62 odebrání
  1. 168 62
      app/Services/ExamPdfExportService.php

+ 168 - 62
app/Services/ExamPdfExportService.php

@@ -4,9 +4,12 @@ namespace App\Services;
 
 use App\Http\Controllers\ExamPdfController;
 use Illuminate\Http\Request;
+use Illuminate\Support\Facades\File;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Storage;
 use Illuminate\Support\Facades\URL;
+use Symfony\Component\Process\Exception\ProcessSignaledException;
+use Symfony\Component\Process\Exception\ProcessTimedOutException;
 use Symfony\Component\Process\Process;
 
 class ExamPdfExportService
@@ -40,9 +43,19 @@ class ExamPdfExportService
         string $suffix,
         bool $useGradingView = false
     ): ?string {
+        // 放宽脚本执行时间,避免长耗时渲染被 PHP 全局超时打断
+        if (function_exists('set_time_limit')) {
+            @set_time_limit(240);
+        }
+
         try {
             $html = $this->renderHtml($paperId, $includeAnswer, $useGradingView);
             if (!$html) {
+                Log::error('ExamPdfExportService: 渲染 HTML 为空', [
+                    'paper_id' => $paperId,
+                    'include_answer' => $includeAnswer,
+                    'use_grading_view' => $useGradingView,
+                ]);
                 return null;
             }
 
@@ -59,7 +72,9 @@ class ExamPdfExportService
             Log::error('ExamPdfExportService: 生成 PDF 失败', [
                 'paper_id' => $paperId,
                 'suffix' => $suffix,
-                'error' => $e->getMessage()
+                'error' => $e->getMessage(),
+                'exception' => get_class($e),
+                'trace' => $e->getTraceAsString(),
             ]);
             return null;
         }
@@ -88,24 +103,20 @@ class ExamPdfExportService
     private function buildPdf(string $html): ?string
     {
         $tmpHtml = tempnam(sys_get_temp_dir(), 'exam_html_') . '.html';
-        file_put_contents($tmpHtml, $this->ensureUtf8Html($html));
+        $utf8Html = $this->ensureUtf8Html($html);
+        file_put_contents($tmpHtml, $utf8Html);
 
-        // 先尝试 Chrome
+        // 仅使用 Chrome 渲染,去掉 wkhtmltopdf 兜底以暴露真实问题
         $chromePdf = $this->renderWithChrome($tmpHtml);
-        if ($chromePdf !== null) {
-            @unlink($tmpHtml);
-            return $chromePdf;
-        }
-
-        // Chrome 失败则降级 wkhtmltopdf,尽量保证有输出
-        $wkPdf = $this->renderWithWkhtml($tmpHtml);
         @unlink($tmpHtml);
-        return $wkPdf;
+        return $chromePdf;
     }
 
     private function renderWithChrome(string $htmlPath): ?string
     {
         $tmpPdf = tempnam(sys_get_temp_dir(), 'exam_pdf_') . '.pdf';
+        // 固定用户目录,减少 Chrome 首次初始化开销;允许多进程并发时可按需加锁
+        $userDataDir = sys_get_temp_dir() . '/chrome-pdf-profile';
 
         $chromeBinary = env('PDF_CHROME_BINARY');
         if (!$chromeBinary) {
@@ -125,7 +136,11 @@ class ExamPdfExportService
         }
 
         if (!$chromeBinary) {
-            Log::warning('ExamPdfExportService: 未找到可用的 Chrome/Chromium,可尝试 wkhtmltopdf');
+            Log::error('ExamPdfExportService: 未找到可用的 Chrome/Chromium,已停止导出', [
+                'html_path' => $htmlPath,
+                'path_env' => env('PATH'),
+                'candidates_checked' => $candidates ?? [],
+            ]);
             return null;
         }
 
@@ -137,79 +152,170 @@ class ExamPdfExportService
             '--disable-setuid-sandbox',
             '--disable-dev-shm-usage',
             '--no-zygote',
-            '--single-process',
             '--disable-features=VizDisplayCompositor',
-            '--user-data-dir=' . sys_get_temp_dir() . '/chrome-user-data',
+            '--disable-software-rasterizer',
+            '--disable-extensions',
+            '--disable-background-networking',
+            '--disable-component-update',
+            '--disable-client-side-phishing-detection',
+            '--disable-default-apps',
+            '--disable-domain-reliability',
+            '--disable-sync',
+            '--safebrowsing-disable-auto-update',
+            '--no-first-run',
+            '--no-default-browser-check',
+            '--disable-crash-reporter',
+            '--disable-print-preview',
+            '--user-data-dir=' . $userDataDir,
             '--print-to-pdf=' . $tmpPdf,
             '--print-to-pdf-no-header',
             '--allow-file-access-from-files',
             'file://' . $htmlPath,
         ]);
-        $process->setTimeout(40);
-        $process->run();
+        $process->setTimeout(60);
 
-        if (!$process->isSuccessful() || !file_exists($tmpPdf)) {
-            Log::error('ExamPdfExportService: Chrome 渲染失败', [
-                'cmd' => implode(' ', (array) $process->getCommandLine()),
-                'exit_code' => $process->getExitCode(),
-                'error' => $process->getErrorOutput(),
-                'output' => $process->getOutput(),
+        try {
+            $startedAt = microtime(true);
+            Log::info('ExamPdfExportService: Chrome 渲染启动', [
+                'cmd' => $process->getCommandLine(),
+                'html_path' => $htmlPath,
+                'tmp_pdf' => $tmpPdf,
+                'user_data_dir' => $userDataDir,
+                'html_exists' => file_exists($htmlPath),
+                'html_size' => file_exists($htmlPath) ? filesize($htmlPath) : null,
+                'cwd' => $process->getWorkingDirectory(),
             ]);
-            @unlink($tmpPdf);
-            return null;
-        }
+            $process->start();
+            $pdfGenerated = false;
 
-        $pdfBinary = file_get_contents($tmpPdf);
-        @unlink($tmpPdf);
-        return $pdfBinary ?: null;
-    }
-
-    private function renderWithWkhtml(string $htmlPath): ?string
-    {
-        $tmpPdf = tempnam(sys_get_temp_dir(), 'exam_pdf_wk_') . '.pdf';
-
-        $wkBinary = env('PDF_WKHTML_BINARY');
-        if (!$wkBinary) {
-            $candidates = [
-                '/usr/bin/wkhtmltopdf',
-                '/usr/local/bin/wkhtmltopdf',
-            ];
-            foreach ($candidates as $path) {
-                if (is_file($path) && is_executable($path)) {
-                    $wkBinary = $path;
+            // 轮询检测 PDF 是否生成,尽快返回,避免等待 Chrome 完整退出
+            $pollStart = microtime(true);
+            $maxPollSeconds = 45;
+            while ($process->isRunning() && (microtime(true) - $pollStart) < $maxPollSeconds) {
+                if (file_exists($tmpPdf) && filesize($tmpPdf) > 0) {
+                    $pdfGenerated = true;
+                    Log::info('ExamPdfExportService: 发现 PDF 已生成,提前结束 Chrome', [
+                        'duration_sec' => round(microtime(true) - $startedAt, 3),
+                        'tmp_pdf_size' => filesize($tmpPdf),
+                    ]);
+                    $process->stop(5, SIGKILL);
                     break;
                 }
+                usleep(200_000); // 200ms
             }
-        }
 
-        if (!$wkBinary) {
-            Log::error('ExamPdfExportService: 未找到可用的 Chrome,且 wkhtmltopdf 未安装,无法导出');
-            return null;
-        }
-
-        $process = new Process([
-            $wkBinary,
-            '--disable-smart-shrinking',
-            '--encoding', 'utf-8',
-            $htmlPath,
-            $tmpPdf,
-        ]);
-        $process->setTimeout(40);
-        $process->run();
+            // 如果仍在运行且超过轮询窗口,则强制结束
+            if ($process->isRunning()) {
+                Log::warning('ExamPdfExportService: Chrome 轮询超时,强制结束', [
+                    'duration_sec' => round(microtime(true) - $startedAt, 3),
+                ]);
+                $process->stop(5, SIGKILL);
+            }
 
-        if (!$process->isSuccessful() || !file_exists($tmpPdf)) {
-            Log::error('ExamPdfExportService: wkhtmltopdf 渲染失败', [
-                'cmd' => implode(' ', (array) $process->getCommandLine()),
+            $process->wait();
+            Log::info('ExamPdfExportService: Chrome 渲染完成', [
+                'duration_sec' => round(microtime(true) - $startedAt, 3),
                 'exit_code' => $process->getExitCode(),
+                'tmp_pdf_exists' => file_exists($tmpPdf),
+                'tmp_pdf_size' => file_exists($tmpPdf) ? filesize($tmpPdf) : null,
+                'stderr' => $process->getErrorOutput(),
+                'stdout' => $process->getOutput(),
+                'pdf_generated_during_poll' => $pdfGenerated,
+            ]);
+        } catch (ProcessTimedOutException|ProcessSignaledException $e) {
+            Log::error('ExamPdfExportService: Chrome 进程异常', [
+                'cmd' => $process->getCommandLine(),
+                'signal' => method_exists($process, 'getTermSignal') ? $process->getTermSignal() : null,
                 'error' => $process->getErrorOutput(),
                 'output' => $process->getOutput(),
+                'exit_code' => $process->getExitCode(),
+                'exception' => $e->getMessage(),
+                'trace' => $e->getTraceAsString(),
+            ]);
+            if ($process->isRunning()) {
+                $process->stop(5, SIGKILL);
+            }
+            $pdfExists = file_exists($tmpPdf);
+            $pdfSize = $pdfExists ? filesize($tmpPdf) : null;
+            if ($pdfExists && $pdfSize > 0) {
+                Log::warning('ExamPdfExportService: Chrome 异常但产生了 PDF,尝试继续返回', [
+                    'tmp_pdf_exists' => $pdfExists,
+                    'tmp_pdf_size' => $pdfSize,
+                    'duration_sec' => isset($startedAt) ? round(microtime(true) - $startedAt, 3) : null,
+                ]);
+                $pdfBinary = file_get_contents($tmpPdf);
+                @unlink($tmpPdf);
+                File::deleteDirectory($userDataDir);
+                return $pdfBinary ?: null;
+            }
+            @unlink($tmpPdf);
+            File::deleteDirectory($userDataDir);
+            return null;
+        } catch (\Throwable $e) {
+            Log::error('ExamPdfExportService: Chrome 调用异常', [
+                'cmd' => $process->getCommandLine(),
+                'error' => $e->getMessage(),
+                'exit_code' => $process->getExitCode(),
+                'stderr' => $process->getErrorOutput(),
+                'stdout' => $process->getOutput(),
+                'trace' => $e->getTraceAsString(),
             ]);
+            if ($process->isRunning()) {
+                $process->stop(5, SIGKILL);
+            }
+            $pdfExists = file_exists($tmpPdf);
+            $pdfSize = $pdfExists ? filesize($tmpPdf) : null;
+            if ($pdfExists && $pdfSize > 0) {
+                Log::warning('ExamPdfExportService: Chrome 调用异常但产生了 PDF,尝试继续返回', [
+                    'tmp_pdf_exists' => $pdfExists,
+                    'tmp_pdf_size' => $pdfSize,
+                    'duration_sec' => isset($startedAt) ? round(microtime(true) - $startedAt, 3) : null,
+                ]);
+                $pdfBinary = file_get_contents($tmpPdf);
+                @unlink($tmpPdf);
+                File::deleteDirectory($userDataDir);
+                return $pdfBinary ?: null;
+            }
             @unlink($tmpPdf);
+            File::deleteDirectory($userDataDir);
             return null;
         }
 
-        $pdfBinary = file_get_contents($tmpPdf);
+        $pdfExists = file_exists($tmpPdf);
+        $pdfSize = $pdfExists ? filesize($tmpPdf) : null;
+
+        if (!$process->isSuccessful()) {
+            if ($pdfExists && $pdfSize > 0) {
+                Log::warning('ExamPdfExportService: Chrome 进程异常但生成了 PDF,继续使用', [
+                    'cmd' => implode(' ', (array) $process->getCommandLine()),
+                    'exit_code' => $process->getExitCode(),
+                    'error' => $process->getErrorOutput(),
+                    'output' => $process->getOutput(),
+                    'tmp_pdf_exists' => $pdfExists,
+                    'tmp_pdf_size' => $pdfSize,
+                    'html_path' => $htmlPath,
+                    'user_data_dir' => $userDataDir,
+                ]);
+            } else {
+                Log::error('ExamPdfExportService: Chrome 渲染失败', [
+                    'cmd' => implode(' ', (array) $process->getCommandLine()),
+                    'exit_code' => $process->getExitCode(),
+                    'error' => $process->getErrorOutput(),
+                    'output' => $process->getOutput(),
+                    'tmp_pdf_exists' => $pdfExists,
+                    'tmp_pdf_size' => $pdfSize,
+                    'html_path' => $htmlPath,
+                    'user_data_dir' => $userDataDir,
+                ]);
+                @unlink($tmpPdf);
+                File::deleteDirectory($userDataDir);
+                return null;
+            }
+        }
+
+        $pdfBinary = $pdfExists ? file_get_contents($tmpPdf) : null;
         @unlink($tmpPdf);
+        File::deleteDirectory($userDataDir);
         return $pdfBinary ?: null;
     }