|
|
@@ -35,6 +35,7 @@ class ExamPdfExportService
|
|
|
* @var array<string, array{w:int,h:int}|null>
|
|
|
*/
|
|
|
private array $pdfImageDimensionCache = [];
|
|
|
+ private ?bool $hasPdfImageMetricsTable = null;
|
|
|
|
|
|
public function __construct(
|
|
|
private readonly LearningAnalyticsService $learningAnalyticsService,
|
|
|
@@ -95,7 +96,7 @@ class ExamPdfExportService
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- $pdfBinary = $this->buildPdf($html);
|
|
|
+ $pdfBinary = $this->buildPdf($html, ! $includeAnswer && ! $useGradingView);
|
|
|
if ($pdfBinary === null || $pdfBinary === '') {
|
|
|
Log::error('renderAndStoreExamPdf: buildPdf 失败', [
|
|
|
'paper_id' => $paperId,
|
|
|
@@ -206,7 +207,7 @@ class ExamPdfExportService
|
|
|
|
|
|
// 步骤3:一次性生成PDF(只需20-25秒,比原来节省10-25秒)
|
|
|
Log::info('generateUnifiedPdf: 开始使用buildPdf直接生成PDF(不使用pdfunite)', ['paper_id' => $paperId]);
|
|
|
- $pdfBinary = $this->buildPdf($unifiedHtml);
|
|
|
+ $pdfBinary = $this->buildPdf($unifiedHtml, true, true);
|
|
|
if (! $pdfBinary) {
|
|
|
Log::error('ExamPdfExportService: 生成统一PDF失败', ['paper_id' => $paperId]);
|
|
|
|
|
|
@@ -1394,11 +1395,15 @@ class ExamPdfExportService
|
|
|
/**
|
|
|
* 构建PDF
|
|
|
*/
|
|
|
- private function buildPdf(string $html): ?string
|
|
|
+ private function buildPdf(string $html, bool $applyWideImageSizing = false, bool $scopeToExamPart = false): ?string
|
|
|
{
|
|
|
$tmpHtml = tempnam(sys_get_temp_dir(), 'exam_html_').'.html';
|
|
|
$utf8Html = $this->ensureUtf8Html($html);
|
|
|
- $utf8Html = $this->applyAdaptiveWideImageSizing($utf8Html);
|
|
|
+ if ($applyWideImageSizing) {
|
|
|
+ $utf8Html = $scopeToExamPart
|
|
|
+ ? $this->applyAdaptiveWideImageSizingToExamPart($utf8Html)
|
|
|
+ : $this->applyAdaptiveWideImageSizing($utf8Html);
|
|
|
+ }
|
|
|
$written = file_put_contents($tmpHtml, $utf8Html);
|
|
|
|
|
|
Log::debug('ExamPdfExportService: HTML文件已创建', [
|
|
|
@@ -1479,6 +1484,30 @@ class ExamPdfExportService
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 仅在 unified HTML 的试卷容器中应用扁图策略,避免影响判卷/知识点讲解部分。
|
|
|
+ */
|
|
|
+ private function applyAdaptiveWideImageSizingToExamPart(string $html): string
|
|
|
+ {
|
|
|
+ $startMarker = '<!-- EXAM_PART_START -->';
|
|
|
+ $endMarker = '<!-- EXAM_PART_END -->';
|
|
|
+ $startPos = strpos($html, $startMarker);
|
|
|
+ $endPos = strpos($html, $endMarker);
|
|
|
+ if ($startPos === false || $endPos === false || $endPos <= $startPos) {
|
|
|
+ return $html;
|
|
|
+ }
|
|
|
+
|
|
|
+ $contentStart = $startPos + strlen($startMarker);
|
|
|
+ $examContent = substr($html, $contentStart, $endPos - $contentStart);
|
|
|
+ if ($examContent === false || $examContent === '') {
|
|
|
+ return $html;
|
|
|
+ }
|
|
|
+
|
|
|
+ $processedExamContent = $this->applyAdaptiveWideImageSizing($examContent);
|
|
|
+
|
|
|
+ return substr($html, 0, $contentStart).$processedExamContent.substr($html, $endPos);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* @return array{w:int,h:int}|null
|
|
|
*/
|
|
|
@@ -1489,6 +1518,13 @@ class ExamPdfExportService
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
+ $persisted = $this->getPersistedPdfImageMetrics($src);
|
|
|
+ if ($persisted !== null) {
|
|
|
+ $this->pdfImageDimensionCache[$src] = $persisted;
|
|
|
+
|
|
|
+ return $persisted;
|
|
|
+ }
|
|
|
+
|
|
|
if (! str_starts_with($src, 'http://') && ! str_starts_with($src, 'https://')) {
|
|
|
$this->pdfImageDimensionCache[$src] = null;
|
|
|
|
|
|
@@ -1498,6 +1534,7 @@ class ExamPdfExportService
|
|
|
$size = @getimagesize($src);
|
|
|
if (is_array($size) && count($size) >= 2) {
|
|
|
$data = ['w' => (int) $size[0], 'h' => (int) $size[1]];
|
|
|
+ $this->persistPdfImageMetrics($src, $data);
|
|
|
$this->pdfImageDimensionCache[$src] = $data;
|
|
|
|
|
|
return $data;
|
|
|
@@ -1514,6 +1551,70 @@ class ExamPdfExportService
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * @return array{w:int,h:int}|null
|
|
|
+ */
|
|
|
+ private function getPersistedPdfImageMetrics(string $src): ?array
|
|
|
+ {
|
|
|
+ if (! $this->isPdfImageMetricsTableReady()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $row = DB::table('pdf_image_metrics')
|
|
|
+ ->where('src', $src)
|
|
|
+ ->first(['width', 'height']);
|
|
|
+
|
|
|
+ if (! $row) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $w = (int) ($row->width ?? 0);
|
|
|
+ $h = (int) ($row->height ?? 0);
|
|
|
+ if ($w <= 0 || $h <= 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return ['w' => $w, 'h' => $h];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param array{w:int,h:int} $data
|
|
|
+ */
|
|
|
+ private function persistPdfImageMetrics(string $src, array $data): void
|
|
|
+ {
|
|
|
+ if (! $this->isPdfImageMetricsTableReady()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $w = (int) ($data['w'] ?? 0);
|
|
|
+ $h = (int) ($data['h'] ?? 0);
|
|
|
+ if ($w <= 0 || $h <= 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ DB::table('pdf_image_metrics')->upsert([
|
|
|
+ [
|
|
|
+ 'src' => $src,
|
|
|
+ 'width' => $w,
|
|
|
+ 'height' => $h,
|
|
|
+ 'ratio' => round($w / max(1, $h), 4),
|
|
|
+ 'updated_at' => now(),
|
|
|
+ 'created_at' => now(),
|
|
|
+ ],
|
|
|
+ ], ['src'], ['width', 'height', 'ratio', 'updated_at']);
|
|
|
+ }
|
|
|
+
|
|
|
+ private function isPdfImageMetricsTableReady(): bool
|
|
|
+ {
|
|
|
+ if ($this->hasPdfImageMetricsTable !== null) {
|
|
|
+ return $this->hasPdfImageMetricsTable;
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->hasPdfImageMetricsTable = Schema::hasTable('pdf_image_metrics');
|
|
|
+
|
|
|
+ return $this->hasPdfImageMetricsTable;
|
|
|
+ }
|
|
|
+
|
|
|
private function shouldApplyAdaptiveSizingToSrc(string $src): bool
|
|
|
{
|
|
|
$parts = parse_url($src);
|
|
|
@@ -2282,10 +2383,12 @@ class ExamPdfExportService
|
|
|
|
|
|
// 添加试卷部分
|
|
|
$bodyContent .= '
|
|
|
+ <!-- EXAM_PART_START -->
|
|
|
<!-- 试卷部分 - 连续显示 -->
|
|
|
<div class="exam-part">
|
|
|
'.$examBody.'
|
|
|
</div>
|
|
|
+ <!-- EXAM_PART_END -->
|
|
|
';
|
|
|
|
|
|
// 添加判卷部分
|
|
|
@@ -3353,6 +3456,16 @@ class ExamPdfExportService
|
|
|
private function normalizeAnswerFieldForPdf(object $question): object
|
|
|
{
|
|
|
$normalizedQuestion = clone $question;
|
|
|
+ // 以 paper_questions.question_text 为标准题干字段,兼容旧链路 content。
|
|
|
+ $questionText = trim((string) ($normalizedQuestion->question_text ?? ''));
|
|
|
+ if ($questionText === '') {
|
|
|
+ $questionText = trim((string) ($normalizedQuestion->content ?? ''));
|
|
|
+ }
|
|
|
+ if ($questionText !== '') {
|
|
|
+ $normalizedQuestion->question_text = $questionText;
|
|
|
+ $normalizedQuestion->content = $questionText;
|
|
|
+ }
|
|
|
+
|
|
|
$answerText = trim((string) ($normalizedQuestion->answer ?? ''));
|
|
|
if ($answerText !== '') {
|
|
|
return $normalizedQuestion;
|