|
@@ -40,6 +40,7 @@ class ExamPdfExportService
|
|
|
private array $pdfImageDimensionCache = [];
|
|
private array $pdfImageDimensionCache = [];
|
|
|
private ?bool $hasPdfImageMetricsTable = null;
|
|
private ?bool $hasPdfImageMetricsTable = null;
|
|
|
private ?array $knowledgePointMetaCache = null;
|
|
private ?array $knowledgePointMetaCache = null;
|
|
|
|
|
+ private ?string $lastDebugHtmlPath = null;
|
|
|
|
|
|
|
|
public function __construct(
|
|
public function __construct(
|
|
|
private readonly LearningAnalyticsService $learningAnalyticsService,
|
|
private readonly LearningAnalyticsService $learningAnalyticsService,
|
|
@@ -147,11 +148,21 @@ class ExamPdfExportService
|
|
|
*/
|
|
*/
|
|
|
public function generateUnifiedPdf(string $paperId): ?string
|
|
public function generateUnifiedPdf(string $paperId): ?string
|
|
|
{
|
|
{
|
|
|
|
|
+ $flowStart = microtime(true);
|
|
|
|
|
+ $lastMark = $flowStart;
|
|
|
|
|
+ $timings = [];
|
|
|
|
|
+ $mark = static function (string $label) use (&$lastMark, &$timings): void {
|
|
|
|
|
+ $now = microtime(true);
|
|
|
|
|
+ $timings[$label] = round(($now - $lastMark) * 1000, 1);
|
|
|
|
|
+ $lastMark = $now;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
// 与组卷规则保持一致:仅知识点组卷类型(paper_type=2)包含知识点讲解。
|
|
// 与组卷规则保持一致:仅知识点组卷类型(paper_type=2)包含知识点讲解。
|
|
|
$paperType = Paper::query()
|
|
$paperType = Paper::query()
|
|
|
->where('paper_id', $paperId)
|
|
->where('paper_id', $paperId)
|
|
|
->value('paper_type');
|
|
->value('paper_type');
|
|
|
$shouldIncludeKpExplain = ((int) $paperType) === 2;
|
|
$shouldIncludeKpExplain = ((int) $paperType) === 2;
|
|
|
|
|
+ $mark('load_paper_type_ms');
|
|
|
|
|
|
|
|
Log::info('generateUnifiedPdf 开始(终极优化版本,直接HTML合并生成PDF):', [
|
|
Log::info('generateUnifiedPdf 开始(终极优化版本,直接HTML合并生成PDF):', [
|
|
|
'paper_id' => $paperId,
|
|
'paper_id' => $paperId,
|
|
@@ -164,6 +175,7 @@ class ExamPdfExportService
|
|
|
if ($shouldIncludeKpExplain) {
|
|
if ($shouldIncludeKpExplain) {
|
|
|
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');
|
|
|
if ($kpExplainHtml) {
|
|
if ($kpExplainHtml) {
|
|
|
// 统一在 mergeHtmlWithPageBreak()->ensureUtf8Html() 阶段处理内联与公式预渲染,
|
|
// 统一在 mergeHtmlWithPageBreak()->ensureUtf8Html() 阶段处理内联与公式预渲染,
|
|
|
// 避免在此处重复处理导致额外耗时。
|
|
// 避免在此处重复处理导致额外耗时。
|
|
@@ -174,11 +186,14 @@ class ExamPdfExportService
|
|
|
} else {
|
|
} else {
|
|
|
Log::warning('generateUnifiedPdf: 知识点讲解HTML获取失败,将跳过', ['paper_id' => $paperId]);
|
|
Log::warning('generateUnifiedPdf: 知识点讲解HTML获取失败,将跳过', ['paper_id' => $paperId]);
|
|
|
}
|
|
}
|
|
|
|
|
+ } else {
|
|
|
|
|
+ $timings['kp_explain_html_ms'] = 0.0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 步骤1:同时渲染两个页面的HTML
|
|
// 步骤1:同时渲染两个页面的HTML
|
|
|
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');
|
|
|
if (! $examHtml) {
|
|
if (! $examHtml) {
|
|
|
Log::error('ExamPdfExportService: 渲染卷子HTML失败', ['paper_id' => $paperId]);
|
|
Log::error('ExamPdfExportService: 渲染卷子HTML失败', ['paper_id' => $paperId]);
|
|
|
|
|
|
|
@@ -188,6 +203,7 @@ class ExamPdfExportService
|
|
|
|
|
|
|
|
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');
|
|
|
if (! $gradingHtml) {
|
|
if (! $gradingHtml) {
|
|
|
Log::error('ExamPdfExportService: 渲染判卷HTML失败', ['paper_id' => $paperId]);
|
|
Log::error('ExamPdfExportService: 渲染判卷HTML失败', ['paper_id' => $paperId]);
|
|
|
|
|
|
|
@@ -198,6 +214,7 @@ class ExamPdfExportService
|
|
|
// 步骤2:插入分页符,合并HTML
|
|
// 步骤2:插入分页符,合并HTML
|
|
|
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');
|
|
|
if (! $unifiedHtml) {
|
|
if (! $unifiedHtml) {
|
|
|
Log::error('ExamPdfExportService: HTML合并失败', ['paper_id' => $paperId]);
|
|
Log::error('ExamPdfExportService: HTML合并失败', ['paper_id' => $paperId]);
|
|
|
|
|
|
|
@@ -211,7 +228,9 @@ class ExamPdfExportService
|
|
|
|
|
|
|
|
// 步骤3:一次性生成PDF(只需20-25秒,比原来节省10-25秒)
|
|
// 步骤3:一次性生成PDF(只需20-25秒,比原来节省10-25秒)
|
|
|
Log::info('generateUnifiedPdf: 开始使用buildPdf直接生成PDF(不使用pdfunite)', ['paper_id' => $paperId]);
|
|
Log::info('generateUnifiedPdf: 开始使用buildPdf直接生成PDF(不使用pdfunite)', ['paper_id' => $paperId]);
|
|
|
|
|
+ $this->lastDebugHtmlPath = null;
|
|
|
$pdfBinary = $this->buildPdf($unifiedHtml, true, true);
|
|
$pdfBinary = $this->buildPdf($unifiedHtml, true, true);
|
|
|
|
|
+ $mark('build_pdf_ms');
|
|
|
if (! $pdfBinary) {
|
|
if (! $pdfBinary) {
|
|
|
Log::error('ExamPdfExportService: 生成统一PDF失败', ['paper_id' => $paperId]);
|
|
Log::error('ExamPdfExportService: 生成统一PDF失败', ['paper_id' => $paperId]);
|
|
|
|
|
|
|
@@ -230,6 +249,7 @@ class ExamPdfExportService
|
|
|
$path = "exams/{$allPdfName}";
|
|
$path = "exams/{$allPdfName}";
|
|
|
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');
|
|
|
if (! $url) {
|
|
if (! $url) {
|
|
|
Log::error('ExamPdfExportService: 保存统一PDF失败', ['path' => $path]);
|
|
Log::error('ExamPdfExportService: 保存统一PDF失败', ['path' => $path]);
|
|
|
|
|
|
|
@@ -240,8 +260,23 @@ class ExamPdfExportService
|
|
|
// 步骤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']);
|
|
|
$this->savePdfUrlToDatabase($paperId, 'all_pdf_url', $url);
|
|
$this->savePdfUrlToDatabase($paperId, 'all_pdf_url', $url);
|
|
|
|
|
+ $mark('save_url_ms');
|
|
|
Log::info('generateUnifiedPdf: URL保存完成', ['paper_id' => $paperId]);
|
|
Log::info('generateUnifiedPdf: URL保存完成', ['paper_id' => $paperId]);
|
|
|
|
|
|
|
|
|
|
+ Log::warning('PDF_GENERATION_TIMING', [
|
|
|
|
|
+ 'paper_id' => $paperId,
|
|
|
|
|
+ 'paper_type' => (int) $paperType,
|
|
|
|
|
+ 'has_kp_explain' => ! empty($kpExplainHtml),
|
|
|
|
|
+ 'exam_html_bytes' => strlen($examHtml),
|
|
|
|
|
+ 'grading_html_bytes' => strlen($gradingHtml),
|
|
|
|
|
+ 'kp_explain_html_bytes' => $kpExplainHtml ? strlen($kpExplainHtml) : 0,
|
|
|
|
|
+ 'unified_html_bytes' => strlen($unifiedHtml),
|
|
|
|
|
+ 'pdf_bytes' => strlen($pdfBinary),
|
|
|
|
|
+ 'timings_ms' => $timings,
|
|
|
|
|
+ 'total_ms' => round((microtime(true) - $flowStart) * 1000, 1),
|
|
|
|
|
+ 'debug_html_path' => $this->lastDebugHtmlPath,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
Log::info('generateUnifiedPdf 全部完成(终极优化:直接HTML合并生成一份PDF)', [
|
|
Log::info('generateUnifiedPdf 全部完成(终极优化:直接HTML合并生成一份PDF)', [
|
|
|
'paper_id' => $paperId,
|
|
'paper_id' => $paperId,
|
|
|
'url' => $url,
|
|
'url' => $url,
|
|
@@ -2736,7 +2771,8 @@ class ExamPdfExportService
|
|
|
if (config('pdf.debug_save_html', false)) {
|
|
if (config('pdf.debug_save_html', false)) {
|
|
|
$debugPath = storage_path('app/debug_pdf_'.date('YmdHis').'.html');
|
|
$debugPath = storage_path('app/debug_pdf_'.date('YmdHis').'.html');
|
|
|
@copy($tmpHtml, $debugPath);
|
|
@copy($tmpHtml, $debugPath);
|
|
|
- Log::warning('ExamPdfExportService: [DEBUG] HTML副本已保存', ['path' => $debugPath]);
|
|
|
|
|
|
|
+ $this->lastDebugHtmlPath = $debugPath;
|
|
|
|
|
+ Log::debug('ExamPdfExportService: [DEBUG] HTML副本已保存', ['path' => $debugPath]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 仅使用Chrome渲染
|
|
// 仅使用Chrome渲染
|
|
@@ -3045,7 +3081,7 @@ class ExamPdfExportService
|
|
|
$process->setTimeout(180); // 复杂学情报告页允许更长渲染时间,降低超时失败率
|
|
$process->setTimeout(180); // 复杂学情报告页允许更长渲染时间,降低超时失败率
|
|
|
$killSignal = \defined('SIGKILL') ? \SIGKILL : 9;
|
|
$killSignal = \defined('SIGKILL') ? \SIGKILL : 9;
|
|
|
|
|
|
|
|
- Log::warning('ExamPdfExportService: [调试] Chrome命令准备执行', [
|
|
|
|
|
|
|
+ Log::debug('ExamPdfExportService: [调试] Chrome命令准备执行', [
|
|
|
'chrome_binary' => $chromeBinary,
|
|
'chrome_binary' => $chromeBinary,
|
|
|
'html_path' => $htmlPath,
|
|
'html_path' => $htmlPath,
|
|
|
'html_exists' => file_exists($htmlPath),
|
|
'html_exists' => file_exists($htmlPath),
|
|
@@ -3253,8 +3289,7 @@ class ExamPdfExportService
|
|
|
$hasKatexCdn = strpos($html, 'cdn.jsdelivr.net/npm/katex') !== false;
|
|
$hasKatexCdn = strpos($html, 'cdn.jsdelivr.net/npm/katex') !== false;
|
|
|
$hasKatexLocal = strpos($html, '/js/katex.min.js') !== false || strpos($html, '/css/katex/katex.min.css') !== false;
|
|
$hasKatexLocal = strpos($html, '/js/katex.min.js') !== false || strpos($html, '/css/katex/katex.min.css') !== false;
|
|
|
|
|
|
|
|
- // 【调试】记录HTML内容信息
|
|
|
|
|
- Log::warning('ExamPdfExportService: inlineExternalResources', [
|
|
|
|
|
|
|
+ Log::debug('ExamPdfExportService: inlineExternalResources', [
|
|
|
'html_length' => strlen($html),
|
|
'html_length' => strlen($html),
|
|
|
'has_katex_cdn' => $hasKatexCdn,
|
|
'has_katex_cdn' => $hasKatexCdn,
|
|
|
'has_katex_local' => $hasKatexLocal,
|
|
'has_katex_local' => $hasKatexLocal,
|
|
@@ -3262,7 +3297,7 @@ class ExamPdfExportService
|
|
|
|
|
|
|
|
// 如果既没有 CDN 也没有本地链接,仍尝试注入 KaTeX 关系符通用修复
|
|
// 如果既没有 CDN 也没有本地链接,仍尝试注入 KaTeX 关系符通用修复
|
|
|
if (! $hasKatexCdn && ! $hasKatexLocal) {
|
|
if (! $hasKatexCdn && ! $hasKatexLocal) {
|
|
|
- Log::warning('ExamPdfExportService: HTML 中没有 KaTeX 资源链接,跳过内联');
|
|
|
|
|
|
|
+ Log::debug('ExamPdfExportService: HTML 中没有 KaTeX 资源链接,跳过内联');
|
|
|
|
|
|
|
|
return $this->applyKatexRelationGlyphFixes($html);
|
|
return $this->applyKatexRelationGlyphFixes($html);
|
|
|
}
|
|
}
|