| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164 |
- <?php
- namespace App\Services;
- use App\Http\Controllers\ExamPdfController;
- use Illuminate\Http\Request;
- use Illuminate\Support\Facades\Log;
- use Illuminate\Support\Facades\Storage;
- use Illuminate\Support\Facades\URL;
- use Symfony\Component\Process\Process;
- class ExamPdfExportService
- {
- private ExamPdfController $controller;
- public function __construct(ExamPdfController $controller)
- {
- $this->controller = $controller;
- }
- /**
- 生成试卷 PDF(不含答案)
- */
- public function generateExamPdf(string $paperId): ?string
- {
- return $this->renderAndStore($paperId, includeAnswer: false, suffix: 'exam');
- }
- /**
- 生成判卷 PDF(含答案与解析)
- */
- public function generateGradingPdf(string $paperId): ?string
- {
- return $this->renderAndStore($paperId, includeAnswer: true, suffix: 'grading', useGradingView: true);
- }
- private function renderAndStore(
- string $paperId,
- bool $includeAnswer,
- string $suffix,
- bool $useGradingView = false
- ): ?string {
- try {
- $html = $this->renderHtml($paperId, $includeAnswer, $useGradingView);
- if (!$html) {
- return null;
- }
- $pdfBinary = $this->buildPdf($html);
- if (!$pdfBinary) {
- return null;
- }
- $path = "exams/{$paperId}_{$suffix}.pdf";
- Storage::disk('public')->put($path, $pdfBinary);
- return URL::to(Storage::url($path));
- } catch (\Throwable $e) {
- Log::error('ExamPdfExportService: 生成 PDF 失败', [
- 'paper_id' => $paperId,
- 'suffix' => $suffix,
- 'error' => $e->getMessage()
- ]);
- return null;
- }
- }
- private function renderHtml(string $paperId, bool $includeAnswer, bool $useGradingView): ?string
- {
- // 复用已有控制器的渲染逻辑,保证版式一致
- $request = Request::create(
- '/admin/intelligent-exam/' . ($useGradingView ? 'grading' : 'pdf') . '/' . $paperId,
- 'GET',
- ['answer' => $includeAnswer ? 'true' : 'false']
- );
- $view = $useGradingView
- ? $this->controller->showGrading($request, $paperId)
- : $this->controller->show($request, $paperId);
- if (is_object($view) && method_exists($view, 'render')) {
- return $this->ensureUtf8Html($view->render());
- }
- return null;
- }
- private function buildPdf(string $html): ?string
- {
- // 使用无头 Chrome 渲染 HTML,保留前端样式并彻底解决大量空白页问题
- $tmpHtml = tempnam(sys_get_temp_dir(), 'exam_html_') . '.html';
- $tmpPdf = tempnam(sys_get_temp_dir(), 'exam_pdf_') . '.pdf';
- file_put_contents($tmpHtml, $this->ensureUtf8Html($html));
- $chromeBinary = env('PDF_CHROME_BINARY');
- if (!$chromeBinary) {
- // 默认优先 Mac,本地开发;不存在则尝试常见 Linux 路径
- $candidates = [
- '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
- '/usr/bin/chromium-browser',
- '/usr/bin/chromium',
- '/usr/bin/google-chrome',
- ];
- foreach ($candidates as $path) {
- if (is_file($path) && is_executable($path)) {
- $chromeBinary = $path;
- break;
- }
- }
- }
- if (!$chromeBinary) {
- Log::error('ExamPdfExportService: 未找到可用的 Chrome/Chromium 可执行文件');
- return null;
- }
- $process = new Process([
- $chromeBinary,
- '--headless',
- '--disable-gpu',
- '--no-sandbox',
- '--disable-setuid-sandbox',
- '--disable-dev-shm-usage', // 避免容器内 /dev/shm 过小导致崩溃
- '--no-zygote',
- '--single-process',
- '--disable-features=VizDisplayCompositor',
- '--user-data-dir=' . sys_get_temp_dir() . '/chrome-user-data',
- '--print-to-pdf=' . $tmpPdf,
- '--print-to-pdf-no-header',
- '--allow-file-access-from-files',
- 'file://' . $tmpHtml,
- ]);
- $process->setTimeout(30);
- $process->run();
- 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(),
- ]);
- @unlink($tmpHtml);
- @unlink($tmpPdf);
- return null;
- }
- $pdfBinary = file_get_contents($tmpPdf);
- @unlink($tmpHtml);
- @unlink($tmpPdf);
- return $pdfBinary ?: null;
- }
- private function ensureUtf8Html(string $html): string
- {
- $meta = '<meta charset="UTF-8">';
- if (stripos($html, '<head>') !== false) {
- return preg_replace('/<head>/i', "<head>{$meta}", $html, 1);
- }
- return $meta . $html;
- }
- }
|