|
@@ -170,9 +170,11 @@ class ExamPdfExportService
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
|
|
+ $totalStartedAt = microtime(true);
|
|
|
// 步骤0:获取知识点讲解HTML(如需要)
|
|
// 步骤0:获取知识点讲解HTML(如需要)
|
|
|
$kpExplainHtml = null;
|
|
$kpExplainHtml = null;
|
|
|
if ($shouldIncludeKpExplain) {
|
|
if ($shouldIncludeKpExplain) {
|
|
|
|
|
+ $kpStartedAt = microtime(true);
|
|
|
Log::info('generateUnifiedPdf: 开始获取知识点讲解HTML', ['paper_id' => $paperId]);
|
|
Log::info('generateUnifiedPdf: 开始获取知识点讲解HTML', ['paper_id' => $paperId]);
|
|
|
$kpExplainHtml = $this->fetchKnowledgeExplanationHtml($paperId);
|
|
$kpExplainHtml = $this->fetchKnowledgeExplanationHtml($paperId);
|
|
|
$mark('kp_explain_html_ms');
|
|
$mark('kp_explain_html_ms');
|
|
@@ -182,6 +184,7 @@ class ExamPdfExportService
|
|
|
Log::info('generateUnifiedPdf: 知识点讲解HTML获取并处理成功', [
|
|
Log::info('generateUnifiedPdf: 知识点讲解HTML获取并处理成功', [
|
|
|
'paper_id' => $paperId,
|
|
'paper_id' => $paperId,
|
|
|
'length' => strlen($kpExplainHtml),
|
|
'length' => strlen($kpExplainHtml),
|
|
|
|
|
+ 'elapsed_ms' => (int) round((microtime(true) - $kpStartedAt) * 1000),
|
|
|
]);
|
|
]);
|
|
|
} else {
|
|
} else {
|
|
|
Log::warning('generateUnifiedPdf: 知识点讲解HTML获取失败,将跳过', ['paper_id' => $paperId]);
|
|
Log::warning('generateUnifiedPdf: 知识点讲解HTML获取失败,将跳过', ['paper_id' => $paperId]);
|
|
@@ -191,6 +194,7 @@ class ExamPdfExportService
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 步骤1:同时渲染两个页面的HTML
|
|
// 步骤1:同时渲染两个页面的HTML
|
|
|
|
|
+ $examRenderStartedAt = microtime(true);
|
|
|
Log::info('generateUnifiedPdf: 开始渲染试卷HTML', ['paper_id' => $paperId]);
|
|
Log::info('generateUnifiedPdf: 开始渲染试卷HTML', ['paper_id' => $paperId]);
|
|
|
$examHtml = $this->renderExamHtml($paperId, includeAnswer: false, useGradingView: false);
|
|
$examHtml = $this->renderExamHtml($paperId, includeAnswer: false, useGradingView: false);
|
|
|
$mark('exam_html_ms');
|
|
$mark('exam_html_ms');
|
|
@@ -199,8 +203,13 @@ class ExamPdfExportService
|
|
|
|
|
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
- Log::info('generateUnifiedPdf: 试卷HTML渲染完成', ['paper_id' => $paperId, 'length' => strlen($examHtml)]);
|
|
|
|
|
|
|
+ Log::info('generateUnifiedPdf: 试卷HTML渲染完成', [
|
|
|
|
|
+ 'paper_id' => $paperId,
|
|
|
|
|
+ 'length' => strlen($examHtml),
|
|
|
|
|
+ 'elapsed_ms' => (int) round((microtime(true) - $examRenderStartedAt) * 1000),
|
|
|
|
|
+ ]);
|
|
|
|
|
|
|
|
|
|
+ $gradingRenderStartedAt = microtime(true);
|
|
|
Log::info('generateUnifiedPdf: 开始渲染判卷HTML', ['paper_id' => $paperId]);
|
|
Log::info('generateUnifiedPdf: 开始渲染判卷HTML', ['paper_id' => $paperId]);
|
|
|
$gradingHtml = $this->renderExamHtml($paperId, includeAnswer: true, useGradingView: true);
|
|
$gradingHtml = $this->renderExamHtml($paperId, includeAnswer: true, useGradingView: true);
|
|
|
$mark('grading_html_ms');
|
|
$mark('grading_html_ms');
|
|
@@ -209,9 +218,14 @@ class ExamPdfExportService
|
|
|
|
|
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
- Log::info('generateUnifiedPdf: 判卷HTML渲染完成', ['paper_id' => $paperId, 'length' => strlen($gradingHtml)]);
|
|
|
|
|
|
|
+ Log::info('generateUnifiedPdf: 判卷HTML渲染完成', [
|
|
|
|
|
+ 'paper_id' => $paperId,
|
|
|
|
|
+ 'length' => strlen($gradingHtml),
|
|
|
|
|
+ 'elapsed_ms' => (int) round((microtime(true) - $gradingRenderStartedAt) * 1000),
|
|
|
|
|
+ ]);
|
|
|
|
|
|
|
|
// 步骤2:插入分页符,合并HTML
|
|
// 步骤2:插入分页符,合并HTML
|
|
|
|
|
+ $mergeStartedAt = microtime(true);
|
|
|
Log::info('generateUnifiedPdf: 开始合并HTML(保留原始样式)', ['paper_id' => $paperId]);
|
|
Log::info('generateUnifiedPdf: 开始合并HTML(保留原始样式)', ['paper_id' => $paperId]);
|
|
|
$unifiedHtml = $this->mergeHtmlWithPageBreak($examHtml, $gradingHtml, $kpExplainHtml);
|
|
$unifiedHtml = $this->mergeHtmlWithPageBreak($examHtml, $gradingHtml, $kpExplainHtml);
|
|
|
$mark('merge_html_ms');
|
|
$mark('merge_html_ms');
|
|
@@ -224,9 +238,11 @@ class ExamPdfExportService
|
|
|
'paper_id' => $paperId,
|
|
'paper_id' => $paperId,
|
|
|
'length' => strlen($unifiedHtml),
|
|
'length' => strlen($unifiedHtml),
|
|
|
'has_kp_explain' => ! empty($kpExplainHtml),
|
|
'has_kp_explain' => ! empty($kpExplainHtml),
|
|
|
|
|
+ 'elapsed_ms' => (int) round((microtime(true) - $mergeStartedAt) * 1000),
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
// 步骤3:一次性生成PDF(只需20-25秒,比原来节省10-25秒)
|
|
// 步骤3:一次性生成PDF(只需20-25秒,比原来节省10-25秒)
|
|
|
|
|
+ $pdfRenderStartedAt = microtime(true);
|
|
|
Log::info('generateUnifiedPdf: 开始使用buildPdf直接生成PDF(不使用pdfunite)', ['paper_id' => $paperId]);
|
|
Log::info('generateUnifiedPdf: 开始使用buildPdf直接生成PDF(不使用pdfunite)', ['paper_id' => $paperId]);
|
|
|
$this->lastDebugHtmlPath = null;
|
|
$this->lastDebugHtmlPath = null;
|
|
|
$pdfBinary = $this->buildPdf($unifiedHtml, true, true);
|
|
$pdfBinary = $this->buildPdf($unifiedHtml, true, true);
|
|
@@ -236,7 +252,11 @@ class ExamPdfExportService
|
|
|
|
|
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
- Log::info('generateUnifiedPdf: PDF生成完成', ['paper_id' => $paperId, 'pdf_size' => strlen($pdfBinary)]);
|
|
|
|
|
|
|
+ Log::info('generateUnifiedPdf: PDF生成完成', [
|
|
|
|
|
+ 'paper_id' => $paperId,
|
|
|
|
|
+ 'pdf_size' => strlen($pdfBinary),
|
|
|
|
|
+ 'elapsed_ms' => (int) round((microtime(true) - $pdfRenderStartedAt) * 1000),
|
|
|
|
|
+ ]);
|
|
|
|
|
|
|
|
// 步骤4:保存PDF
|
|
// 步骤4:保存PDF
|
|
|
$paper = Paper::where('paper_id', $paperId)->first();
|
|
$paper = Paper::where('paper_id', $paperId)->first();
|
|
@@ -247,6 +267,7 @@ class ExamPdfExportService
|
|
|
}
|
|
}
|
|
|
$allPdfName = $this->buildPdfFileName($paper);
|
|
$allPdfName = $this->buildPdfFileName($paper);
|
|
|
$path = "exams/{$allPdfName}";
|
|
$path = "exams/{$allPdfName}";
|
|
|
|
|
+ $storageStartedAt = microtime(true);
|
|
|
Log::info('generateUnifiedPdf: 开始保存PDF到云存储', ['paper_id' => $paperId, 'path' => $path]);
|
|
Log::info('generateUnifiedPdf: 开始保存PDF到云存储', ['paper_id' => $paperId, 'path' => $path]);
|
|
|
$url = $this->pdfStorageService->put($path, $pdfBinary);
|
|
$url = $this->pdfStorageService->put($path, $pdfBinary);
|
|
|
$mark('upload_pdf_ms');
|
|
$mark('upload_pdf_ms');
|
|
@@ -255,7 +276,11 @@ class ExamPdfExportService
|
|
|
|
|
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
- Log::info('generateUnifiedPdf: PDF保存完成', ['paper_id' => $paperId, 'url' => $url]);
|
|
|
|
|
|
|
+ Log::info('generateUnifiedPdf: PDF保存完成', [
|
|
|
|
|
+ 'paper_id' => $paperId,
|
|
|
|
|
+ 'url' => $url,
|
|
|
|
|
+ 'elapsed_ms' => (int) round((microtime(true) - $storageStartedAt) * 1000),
|
|
|
|
|
+ ]);
|
|
|
|
|
|
|
|
// 步骤5:保存URL到数据库(存储到all_pdf_url字段)
|
|
// 步骤5:保存URL到数据库(存储到all_pdf_url字段)
|
|
|
Log::info('generateUnifiedPdf: 开始保存URL到数据库', ['paper_id' => $paperId, 'field' => 'all_pdf_url']);
|
|
Log::info('generateUnifiedPdf: 开始保存URL到数据库', ['paper_id' => $paperId, 'field' => 'all_pdf_url']);
|
|
@@ -282,6 +307,7 @@ class ExamPdfExportService
|
|
|
'url' => $url,
|
|
'url' => $url,
|
|
|
'pdf_size' => strlen($pdfBinary),
|
|
'pdf_size' => strlen($pdfBinary),
|
|
|
'method' => 'direct HTML merge to PDF (no pdfunite)',
|
|
'method' => 'direct HTML merge to PDF (no pdfunite)',
|
|
|
|
|
+ 'elapsed_ms' => (int) round((microtime(true) - $totalStartedAt) * 1000),
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
return $url;
|
|
return $url;
|
|
@@ -738,6 +764,7 @@ class ExamPdfExportService
|
|
|
if ($ca === $cb) {
|
|
if ($ca === $cb) {
|
|
|
return strcmp((string) ($a['code'] ?? ''), (string) ($b['code'] ?? ''));
|
|
return strcmp((string) ($a['code'] ?? ''), (string) ($b['code'] ?? ''));
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
return $cb <=> $ca;
|
|
return $cb <=> $ca;
|
|
|
});
|
|
});
|
|
|
$radarChildrenByModule[$moduleCode] = $items;
|
|
$radarChildrenByModule[$moduleCode] = $items;
|
|
@@ -811,6 +838,7 @@ class ExamPdfExportService
|
|
|
$keep = array_values(array_filter($highToLow, fn ($i) => ($this->toPcMasteryPercent($i['mastery_level']) ?? -1) >= 85));
|
|
$keep = array_values(array_filter($highToLow, fn ($i) => ($this->toPcMasteryPercent($i['mastery_level']) ?? -1) >= 85));
|
|
|
$boost = array_values(array_filter($lowToHigh, function ($i) {
|
|
$boost = array_values(array_filter($lowToHigh, function ($i) {
|
|
|
$percent = $this->toPcMasteryPercent($i['mastery_level']) ?? -1;
|
|
$percent = $this->toPcMasteryPercent($i['mastery_level']) ?? -1;
|
|
|
|
|
+
|
|
|
return $percent >= 60 && $percent < 85;
|
|
return $percent >= 60 && $percent < 85;
|
|
|
}));
|
|
}));
|
|
|
$key = array_values(array_filter($lowToHigh, fn ($i) => ($this->toPcMasteryPercent($i['mastery_level']) ?? 101) < 60));
|
|
$key = array_values(array_filter($lowToHigh, fn ($i) => ($this->toPcMasteryPercent($i['mastery_level']) ?? 101) < 60));
|
|
@@ -1517,6 +1545,7 @@ class ExamPdfExportService
|
|
|
if ($code !== $rootCode) {
|
|
if ($code !== $rootCode) {
|
|
|
$leaves[] = $code;
|
|
$leaves[] = $code;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1839,8 +1868,8 @@ class ExamPdfExportService
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
$url = route('filament.admin.auth.intelligent-exam.knowledge-explanation', ['paper_id' => $paperId]);
|
|
$url = route('filament.admin.auth.intelligent-exam.knowledge-explanation', ['paper_id' => $paperId]);
|
|
|
-
|
|
|
|
|
- $response = Http::timeout(2)->get($url);
|
|
|
|
|
|
|
+ $timeout = max(1, (int) config('pdf.kp_explain_fetch_timeout_seconds', 2));
|
|
|
|
|
+ $response = Http::timeout($timeout)->get($url);
|
|
|
if ($response->successful()) {
|
|
if ($response->successful()) {
|
|
|
$html = $response->body();
|
|
$html = $response->body();
|
|
|
if (! empty(trim($html))) {
|
|
if (! empty(trim($html))) {
|
|
@@ -1859,6 +1888,7 @@ class ExamPdfExportService
|
|
|
Log::warning('ExamPdfExportService: 获取知识点讲解HTML失败', [
|
|
Log::warning('ExamPdfExportService: 获取知识点讲解HTML失败', [
|
|
|
'paper_id' => $paperId,
|
|
'paper_id' => $paperId,
|
|
|
'url' => $url,
|
|
'url' => $url,
|
|
|
|
|
+ 'timeout_seconds' => $timeout,
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
return null;
|
|
return null;
|
|
@@ -1867,6 +1897,7 @@ class ExamPdfExportService
|
|
|
Log::warning('ExamPdfExportService: 获取知识点讲解HTML异常', [
|
|
Log::warning('ExamPdfExportService: 获取知识点讲解HTML异常', [
|
|
|
'paper_id' => $paperId,
|
|
'paper_id' => $paperId,
|
|
|
'error' => $e->getMessage(),
|
|
'error' => $e->getMessage(),
|
|
|
|
|
+ 'timeout_seconds' => config('pdf.kp_explain_fetch_timeout_seconds', 2),
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
return null;
|
|
return null;
|
|
@@ -1915,18 +1946,16 @@ class ExamPdfExportService
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 渲染试卷HTML(优先直接渲染视图;失败再回退HTTP)
|
|
|
|
|
- */
|
|
|
|
|
private function renderExamHtml(string $paperId, bool $includeAnswer, bool $useGradingView): ?string
|
|
private function renderExamHtml(string $paperId, bool $includeAnswer, bool $useGradingView): ?string
|
|
|
{
|
|
{
|
|
|
- // 阶段A:优先本地直渲,降低 HTTP 自调用开销。
|
|
|
|
|
|
|
+ // PDF worker 已经运行在 Laravel 进程内,优先直接渲染 Blade,避免 HTTP 自调用开销。
|
|
|
$html = $this->renderExamHtmlFromView($paperId, $includeAnswer, $useGradingView);
|
|
$html = $this->renderExamHtmlFromView($paperId, $includeAnswer, $useGradingView);
|
|
|
- if (! empty($html)) {
|
|
|
|
|
|
|
+ if ($html !== null) {
|
|
|
return $html;
|
|
return $html;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
|
|
+ // 兜底:保留原 HTTP 路由渲染路径,避免特殊页面上下文下直接视图失败。
|
|
|
$routeName = $useGradingView
|
|
$routeName = $useGradingView
|
|
|
? 'filament.admin.auth.intelligent-exam.grading'
|
|
? 'filament.admin.auth.intelligent-exam.grading'
|
|
|
: 'filament.admin.auth.intelligent-exam.pdf';
|
|
: 'filament.admin.auth.intelligent-exam.pdf';
|
|
@@ -2322,6 +2351,7 @@ class ExamPdfExportService
|
|
|
$masteryData = array_map(function ($item) {
|
|
$masteryData = array_map(function ($item) {
|
|
|
if (is_object($item)) {
|
|
if (is_object($item)) {
|
|
|
$kpCode = $item->kp_code ?? null;
|
|
$kpCode = $item->kp_code ?? null;
|
|
|
|
|
+
|
|
|
return [
|
|
return [
|
|
|
'kp_code' => $kpCode,
|
|
'kp_code' => $kpCode,
|
|
|
'kp_name' => $item->kp_name ?? null,
|
|
'kp_name' => $item->kp_name ?? null,
|
|
@@ -2752,7 +2782,9 @@ class ExamPdfExportService
|
|
|
*/
|
|
*/
|
|
|
private function buildPdf(string $html, bool $applyWideImageSizing = false, bool $scopeToExamPart = false): ?string
|
|
private function buildPdf(string $html, bool $applyWideImageSizing = false, bool $scopeToExamPart = false): ?string
|
|
|
{
|
|
{
|
|
|
|
|
+ $startedAt = microtime(true);
|
|
|
$tmpHtml = tempnam(sys_get_temp_dir(), 'exam_html_').'.html';
|
|
$tmpHtml = tempnam(sys_get_temp_dir(), 'exam_html_').'.html';
|
|
|
|
|
+ $prepareStartedAt = microtime(true);
|
|
|
$utf8Html = $this->ensureUtf8Html($html);
|
|
$utf8Html = $this->ensureUtf8Html($html);
|
|
|
if ($applyWideImageSizing) {
|
|
if ($applyWideImageSizing) {
|
|
|
$utf8Html = $scopeToExamPart
|
|
$utf8Html = $scopeToExamPart
|
|
@@ -2761,10 +2793,11 @@ class ExamPdfExportService
|
|
|
}
|
|
}
|
|
|
$written = file_put_contents($tmpHtml, $utf8Html);
|
|
$written = file_put_contents($tmpHtml, $utf8Html);
|
|
|
|
|
|
|
|
- Log::debug('ExamPdfExportService: HTML文件已创建', [
|
|
|
|
|
|
|
+ Log::info('ExamPdfExportService: PDF HTML准备完成', [
|
|
|
'path' => $tmpHtml,
|
|
'path' => $tmpHtml,
|
|
|
'html_length' => strlen($utf8Html),
|
|
'html_length' => strlen($utf8Html),
|
|
|
'written_bytes' => $written,
|
|
'written_bytes' => $written,
|
|
|
|
|
+ 'elapsed_ms' => (int) round((microtime(true) - $prepareStartedAt) * 1000),
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
// 【调试】如果启用了HTML保存调试,复制HTML到storage用于分析
|
|
// 【调试】如果启用了HTML保存调试,复制HTML到storage用于分析
|
|
@@ -2775,11 +2808,152 @@ class ExamPdfExportService
|
|
|
Log::debug('ExamPdfExportService: [DEBUG] HTML副本已保存', ['path' => $debugPath]);
|
|
Log::debug('ExamPdfExportService: [DEBUG] HTML副本已保存', ['path' => $debugPath]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 仅使用Chrome渲染
|
|
|
|
|
- $chromePdf = $this->renderWithChrome($tmpHtml);
|
|
|
|
|
|
|
+ $pdf = $this->renderWithConfiguredBackend($tmpHtml);
|
|
|
@unlink($tmpHtml);
|
|
@unlink($tmpHtml);
|
|
|
|
|
|
|
|
- return $chromePdf;
|
|
|
|
|
|
|
+ Log::info('ExamPdfExportService: buildPdf完成', [
|
|
|
|
|
+ 'success' => $pdf !== null,
|
|
|
|
|
+ 'pdf_size' => $pdf !== null ? strlen($pdf) : null,
|
|
|
|
|
+ 'elapsed_ms' => (int) round((microtime(true) - $startedAt) * 1000),
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return $pdf;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 根据配置选择 PDF 渲染后端。
|
|
|
|
|
+ */
|
|
|
|
|
+ private function renderWithConfiguredBackend(string $htmlPath): ?string
|
|
|
|
|
+ {
|
|
|
|
|
+ $renderer = strtolower(trim((string) config('pdf.renderer', 'gotenberg')));
|
|
|
|
|
+
|
|
|
|
|
+ if ($renderer === 'gotenberg') {
|
|
|
|
|
+ $pdf = $this->renderWithGotenberg($htmlPath);
|
|
|
|
|
+ if ($pdf !== null) {
|
|
|
|
|
+ return $pdf;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if ($this->shouldFallbackToChrome()) {
|
|
|
|
|
+ Log::warning('ExamPdfExportService: Gotenberg 渲染失败,回退 Chrome CLI', [
|
|
|
|
|
+ 'html_path' => $htmlPath,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return $this->renderWithChrome($htmlPath);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Log::error('ExamPdfExportService: Gotenberg 渲染失败,配置禁止回退 Chrome', [
|
|
|
|
|
+ 'html_path' => $htmlPath,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if ($renderer === 'chrome') {
|
|
|
|
|
+ return $this->renderWithChrome($htmlPath);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Log::error('ExamPdfExportService: 未识别的 PDF_RENDERER,拒绝渲染', [
|
|
|
|
|
+ 'renderer' => $renderer,
|
|
|
|
|
+ 'html_path' => $htmlPath,
|
|
|
|
|
+ 'supported_renderers' => ['gotenberg', 'chrome'],
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 通过 Gotenberg 常驻服务生成 PDF。
|
|
|
|
|
+ */
|
|
|
|
|
+ private function renderWithGotenberg(string $htmlPath): ?string
|
|
|
|
|
+ {
|
|
|
|
|
+ $startedAt = microtime(true);
|
|
|
|
|
+ $baseUrl = rtrim((string) config('pdf.gotenberg_url', 'http://gotenberg:3000'), '/');
|
|
|
|
|
+ $connectTimeout = max(1, (int) config('pdf.gotenberg_connect_timeout_seconds', 3));
|
|
|
|
|
+ $timeout = max(5, (int) config('pdf.gotenberg_timeout_seconds', 60));
|
|
|
|
|
+ $trace = 'exam-pdf-'.basename($htmlPath).'-'.str_replace('.', '', uniqid('', true));
|
|
|
|
|
+
|
|
|
|
|
+ if ($baseUrl === '') {
|
|
|
|
|
+ Log::error('ExamPdfExportService: Gotenberg URL 为空', [
|
|
|
|
|
+ 'html_path' => $htmlPath,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (! is_file($htmlPath)) {
|
|
|
|
|
+ Log::error('ExamPdfExportService: Gotenberg 渲染失败,HTML文件不存在', [
|
|
|
|
|
+ 'html_path' => $htmlPath,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ $htmlContent = file_get_contents($htmlPath);
|
|
|
|
|
+ if ($htmlContent === false) {
|
|
|
|
|
+ Log::error('ExamPdfExportService: Gotenberg 渲染失败,HTML文件读取失败', [
|
|
|
|
|
+ 'trace' => $trace,
|
|
|
|
|
+ 'html_path' => $htmlPath,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $response = Http::timeout($timeout)
|
|
|
|
|
+ ->connectTimeout($connectTimeout)
|
|
|
|
|
+ ->withHeaders([
|
|
|
|
|
+ 'Gotenberg-Trace' => $trace,
|
|
|
|
|
+ 'Gotenberg-Output-Filename' => 'exam.pdf',
|
|
|
|
|
+ ])
|
|
|
|
|
+ ->attach('files', $htmlContent, 'index.html')
|
|
|
|
|
+ ->post($baseUrl.'/forms/chromium/convert/html', [
|
|
|
|
|
+ 'preferCssPageSize' => 'true',
|
|
|
|
|
+ 'printBackground' => 'true',
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ $body = $response->body();
|
|
|
|
|
+ $contentType = strtolower((string) $response->header('Content-Type', ''));
|
|
|
|
|
+ $isPdfContent = str_contains($contentType, 'application/pdf')
|
|
|
|
|
+ || str_starts_with($body, '%PDF-');
|
|
|
|
|
+
|
|
|
|
|
+ if ($response->successful() && $isPdfContent && $body !== '') {
|
|
|
|
|
+ Log::info('ExamPdfExportService: Gotenberg 渲染成功', [
|
|
|
|
|
+ 'trace' => $trace,
|
|
|
|
|
+ 'status' => $response->status(),
|
|
|
|
|
+ 'pdf_size' => strlen($body),
|
|
|
|
|
+ 'content_type' => $contentType,
|
|
|
|
|
+ 'elapsed_ms' => (int) round((microtime(true) - $startedAt) * 1000),
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return $body;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Log::error('ExamPdfExportService: Gotenberg 渲染失败', [
|
|
|
|
|
+ 'trace' => $trace,
|
|
|
|
|
+ 'status' => $response->status(),
|
|
|
|
|
+ 'body_size' => strlen($body),
|
|
|
|
|
+ 'content_type' => $contentType,
|
|
|
|
|
+ 'is_pdf_content' => $isPdfContent,
|
|
|
|
|
+ 'body_preview' => mb_substr($body, 0, 500),
|
|
|
|
|
+ 'elapsed_ms' => (int) round((microtime(true) - $startedAt) * 1000),
|
|
|
|
|
+ ]);
|
|
|
|
|
+ } catch (\Throwable $e) {
|
|
|
|
|
+ Log::error('ExamPdfExportService: Gotenberg 请求异常', [
|
|
|
|
|
+ 'trace' => $trace,
|
|
|
|
|
+ 'url' => $baseUrl,
|
|
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
|
|
+ 'connect_timeout_seconds' => $connectTimeout,
|
|
|
|
|
+ 'timeout_seconds' => $timeout,
|
|
|
|
|
+ 'elapsed_ms' => (int) round((microtime(true) - $startedAt) * 1000),
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function shouldFallbackToChrome(): bool
|
|
|
|
|
+ {
|
|
|
|
|
+ return (bool) config('pdf.fallback_to_chrome', true);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -3131,6 +3305,7 @@ class ExamPdfExportService
|
|
|
if ($result !== null) {
|
|
if ($result !== null) {
|
|
|
return $result;
|
|
return $result;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
return $this->renderWithChromeMinimal($chromeBinary, $htmlPath);
|
|
return $this->renderWithChromeMinimal($chromeBinary, $htmlPath);
|
|
|
} catch (\Throwable $e) {
|
|
} catch (\Throwable $e) {
|
|
|
if ($process->isRunning()) {
|
|
if ($process->isRunning()) {
|