Bladeren bron

fix(pdf): unify naming metadata across generation upload and persistence

yemeishu 3 weken geleden
bovenliggende
commit
7449794066

+ 48 - 11
app/Http/Controllers/ExamPdfController.php

@@ -3,6 +3,7 @@
 namespace App\Http\Controllers;
 
 use App\Models\Paper;
+use App\Support\PaperNaming;
 use App\Services\QuestionBankService;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Cache;
@@ -474,6 +475,33 @@ class ExamPdfController extends Controller
         ];
     }
 
+    private function buildPdfMeta(object $paper, string $fallbackPaperId, ?array $studentInfo = null): array
+    {
+        $rawPaperId = (string) ($paper->paper_id ?? $fallbackPaperId);
+        $assembleType = isset($paper->paper_type) && $paper->paper_type !== '' && $paper->paper_type !== null
+            ? (int) $paper->paper_type
+            : null;
+        $examCode = PaperNaming::extractExamCode($rawPaperId);
+
+        $studentName = $studentInfo['name'] ?? ($paper->student_id ?? '________');
+        try {
+            $assembleTypeLabel = $assembleType !== null ? PaperNaming::assembleTypeLabel($assembleType) : '未知类型';
+        } catch (\Throwable $e) {
+            $assembleTypeLabel = '未知类型';
+        }
+        $headerTitle = $examCode;
+
+        return [
+            'student_name' => $studentName,
+            'exam_code' => $examCode,
+            'assemble_type_label' => $assembleTypeLabel,
+            'header_title' => $headerTitle,
+            'exam_pdf_title' => "试卷_{$headerTitle}",
+            'grading_pdf_title' => "判卷_{$headerTitle}",
+            'knowledge_pdf_title' => "知识点讲解_{$headerTitle}",
+        ];
+    }
+
     public function show(Request $request, $paper_id)
     {
         // 获取是否显示答案的参数,默认为true
@@ -497,6 +525,7 @@ class ExamPdfController extends Controller
                     'paper_name' => $cached['paper_name'] ?? 'Demo Paper',
                     'student_id' => $cached['student_id'] ?? null,
                     'teacher_id' => $cached['teacher_id'] ?? null,
+                    'paper_type' => $cached['assemble_type'] ?? null,
                 ];
 
                 // 对于 demo 试卷,需要检查题目数量并限制为用户要求的数量
@@ -765,13 +794,17 @@ class ExamPdfController extends Controller
 
         // 渲染视图
         $viewName = $includeAnswer ? 'pdf.exam-grading' : 'pdf.exam-paper';
+        $studentInfo = $this->getStudentInfo($paper->student_id);
+        $teacherInfo = $this->getTeacherInfo($paper->teacher_id);
+        $pdfMeta = $this->buildPdfMeta($paper, (string) $paper_id, $studentInfo);
 
         return view($viewName, [
             'paper' => $paper,
             'questions' => $questions,
-            'student' => $this->getStudentInfo($paper->student_id),
-            'teacher' => $this->getTeacherInfo($paper->teacher_id),
+            'student' => $studentInfo,
+            'teacher' => $teacherInfo,
             'includeAnswer' => $includeAnswer,
+            'pdfMeta' => $pdfMeta,
         ]);
     }
 
@@ -800,6 +833,7 @@ class ExamPdfController extends Controller
                 'paper_name' => $cached['paper_name'] ?? 'Demo Paper',
                 'student_id' => $cached['student_id'] ?? null,
                 'teacher_id' => $cached['teacher_id'] ?? null,
+                'paper_type' => $cached['assemble_type'] ?? null,
             ];
             $questionsData = $cached['questions'] ?? [];
             $totalQuestions = $cached['total_questions'] ?? count($questionsData);
@@ -977,12 +1011,17 @@ class ExamPdfController extends Controller
             }
         }
 
+        $studentInfo = $this->getStudentInfo($paper->student_id);
+        $teacherInfo = $this->getTeacherInfo($paper->teacher_id);
+        $pdfMeta = $this->buildPdfMeta($paper, (string) $paper_id, $studentInfo);
+
         return view('pdf.exam-grading', [
             'paper' => $paper,
             'questions' => $questions,
-            'student' => $this->getStudentInfo($paper->student_id),
-            'teacher' => $this->getTeacherInfo($paper->teacher_id),
+            'student' => $studentInfo,
+            'teacher' => $teacherInfo,
             'includeAnswer' => true,
+            'pdfMeta' => $pdfMeta,
         ]);
     }
 
@@ -998,12 +1037,9 @@ class ExamPdfController extends Controller
             abort(404, '试卷未找到');
         }
 
-        // 提取15位paper_id数字部分作为学案编号
-        $rawPaperId = $paper->paper_id ?? $paper_id;
-        preg_match('/paper_(\d{15})/', (string) $rawPaperId, $matches);
-        $examCode = $matches[1] ?? preg_replace('/[^0-9]/', '', (string) $rawPaperId);
-
-        $studentName = $this->getStudentInfo($paper->student_id)['name'] ?? ($paper->student_id ?? '________');
+        $studentInfo = $this->getStudentInfo($paper->student_id);
+        $pdfMeta = $this->buildPdfMeta($paper, (string) $paper_id, $studentInfo);
+        $studentName = $pdfMeta['student_name'];
         // 生成时间(格式:2026年01月30日 15:04:05)
         $generateDateTime = now()->format('Y年m月d日 H:i:s');
 
@@ -1050,10 +1086,11 @@ class ExamPdfController extends Controller
         $knowledgePoints = $pdfService->buildExplanations($kpCodes);
         return view('pdf.exam-knowledge-explanation', [
             'paperId' => $paper_id,
-            'examCode' => $examCode ?: $paper_id,
+            'examCode' => $pdfMeta['exam_code'],
             'studentName' => $studentName,
             'generateDateTime' => $generateDateTime,
             'knowledgePoints' => $knowledgePoints,
+            'pdfMeta' => $pdfMeta,
         ]);
     }
 

+ 1 - 0
app/Models/Paper.php

@@ -50,6 +50,7 @@ class Paper extends Model
         'completed_at' => 'datetime',
         'exam_pdf_url' => 'string',
         'grading_pdf_url' => 'string',
+        'all_pdf_url' => 'string',
     ];
 
     /**

+ 104 - 4
app/Services/ExamPdfExportService.php

@@ -6,6 +6,7 @@ use App\DTO\ExamAnalysisDataDto;
 use App\DTO\ReportPayloadDto;
 use App\Models\Paper;
 use App\Models\Student;
+use App\Support\PaperNaming;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\File;
 use Illuminate\Support\Facades\Http;
@@ -13,6 +14,7 @@ use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Schema;
 use Illuminate\Support\Facades\Storage;
 use Illuminate\Support\Facades\URL;
+use Illuminate\Support\Str;
 use Symfony\Component\Process\Exception\ProcessSignaledException;
 use Symfony\Component\Process\Exception\ProcessTimedOutException;
 use Symfony\Component\Process\Process;
@@ -151,7 +153,14 @@ class ExamPdfExportService
             Log::info('generateUnifiedPdf: PDF生成完成', ['paper_id' => $paperId, 'pdf_size' => strlen($pdfBinary)]);
 
             // 步骤4:保存PDF
-            $path = "exams/{$paperId}_all.pdf";
+            $paper = Paper::where('paper_id', $paperId)->first();
+            if (! $paper) {
+                Log::error('ExamPdfExportService: 生成统一PDF失败,未找到试卷', ['paper_id' => $paperId]);
+
+                return null;
+            }
+            $allPdfName = $this->buildPdfFileName($paper);
+            $path = "exams/{$allPdfName}";
             Log::info('generateUnifiedPdf: 开始保存PDF到云存储', ['paper_id' => $paperId, 'path' => $path]);
             $url = $this->pdfStorageService->put($path, $pdfBinary);
             if (! $url) {
@@ -373,7 +382,14 @@ class ExamPdfExportService
 
             // 读取合并后的PDF内容并上传到云存储
             $mergedPdfContent = file_get_contents($mergedPdfPath);
-            $path = "exams/{$paperId}_all.pdf";
+            $paper = Paper::where('paper_id', $paperId)->first();
+            if (! $paper) {
+                Log::error('ExamPdfExportService: 合并PDF失败,未找到试卷', ['paper_id' => $paperId]);
+
+                return null;
+            }
+            $allPdfName = $this->buildPdfFileName($paper);
+            $path = "exams/{$allPdfName}";
             $mergedUrl = $this->pdfStorageService->put($path, $mergedPdfContent);
 
             if (! $mergedUrl) {
@@ -571,6 +587,21 @@ class ExamPdfExportService
             $teacherModel = \App\Models\Teacher::find($paper->teacher_id);
             $student = ['name' => $studentModel->name ?? ($paper->student_id ?? '________'), 'grade' => $studentModel->grade ?? '________'];
             $teacher = ['name' => $teacherModel->name ?? ($paper->teacher_id ?? '________')];
+            $examCode = PaperNaming::extractExamCode((string) $paper->paper_id);
+            try {
+                $assembleTypeLabel = PaperNaming::assembleTypeLabel((int) $paper->paper_type);
+            } catch (\Throwable $e) {
+                $assembleTypeLabel = '未知类型';
+            }
+            $pdfMeta = [
+                'student_name' => $student['name'],
+                'exam_code' => $examCode,
+                'assemble_type_label' => $assembleTypeLabel,
+                'header_title' => $examCode,
+                'exam_pdf_title' => '试卷_'.$examCode,
+                'grading_pdf_title' => '判卷_'.$examCode,
+                'knowledge_pdf_title' => '知识点讲解_'.$examCode,
+            ];
 
             $html = view($viewName, [
                 'paper' => $paper,
@@ -578,6 +609,7 @@ class ExamPdfExportService
                 'includeAnswer' => $includeAnswer,
                 'student' => $student,
                 'teacher' => $teacher,
+                'pdfMeta' => $pdfMeta,
             ])->render();
 
             if (empty(trim($html))) {
@@ -1713,6 +1745,13 @@ class ExamPdfExportService
 
         // 构建HTML内容
         $bodyContent = '';
+        $mergedTitle = '试卷与判卷合并';
+        if (preg_match('/<title>(.*?)<\/title>/is', $examHead, $titleMatches)) {
+            $candidateTitle = trim(strip_tags($titleMatches[1]));
+            if ($candidateTitle !== '') {
+                $mergedTitle = $candidateTitle;
+            }
+        }
 
         // 如果有知识点讲解,添加到最前面
         if ($kpExplainBody) {
@@ -1745,7 +1784,7 @@ class ExamPdfExportService
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>试卷与判卷合并</title>
+    <title>'.$mergedTitle.'</title>
 '.$headContent.'
 </head>
 <body>
@@ -1949,6 +1988,56 @@ class ExamPdfExportService
         };
     }
 
+    private function buildPaperNamePrefix(Paper $paper): string
+    {
+        $assembleType = ($paper->paper_type === null || $paper->paper_type === '')
+            ? null
+            : (int) $paper->paper_type;
+        $studentName = Student::query()
+            ->where('student_id', $paper->student_id)
+            ->value('name') ?? ($paper->student_id ?: '________');
+        try {
+            $assembleTypeLabel = $assembleType !== null ? PaperNaming::assembleTypeLabel($assembleType) : '未知类型';
+        } catch (\Throwable $e) {
+            $assembleTypeLabel = '未知类型';
+        }
+
+        return "{$studentName}_".PaperNaming::extractExamCode((string) $paper->paper_id)."_{$assembleTypeLabel}";
+    }
+
+    private function buildPaperDisplayTitle(Paper $paper): string
+    {
+        return $this->buildPaperNamePrefix($paper).'_'.now()->format('Ymd');
+    }
+
+    private function buildPdfFileName(Paper $paper, ?string $stamp = null): string
+    {
+        $basePrefix = $this->buildPaperNamePrefix($paper);
+        $stamp = $stamp ?: now()->format('YmdHis').strtoupper(Str::random(4));
+        $base = "{$basePrefix}_{$stamp}";
+        $safe = PaperNaming::toSafeFilename($base);
+
+        return "{$safe}.pdf";
+    }
+
+    private function extractUploadStamp(string $url): ?string
+    {
+        $path = parse_url($url, PHP_URL_PATH);
+        if (! is_string($path) || $path === '') {
+            return null;
+        }
+        $stem = pathinfo($path, PATHINFO_FILENAME);
+        if (! is_string($stem) || $stem === '') {
+            return null;
+        }
+
+        if (preg_match('/(\d{14}[A-Za-z0-9]{4})/', $stem, $matches)) {
+            return $matches[1];
+        }
+
+        return null;
+    }
+
     /**
      * 保存PDF URL到数据库
      */
@@ -1957,11 +2046,22 @@ class ExamPdfExportService
         try {
             $paper = Paper::where('paper_id', $paperId)->first();
             if ($paper) {
-                $paper->update([$field => $url]);
+                $paperDisplayTitle = $this->buildPaperDisplayTitle($paper);
+                $stamp = $this->extractUploadStamp($url);
+                if ($stamp) {
+                    $paperDisplayTitle = $this->buildPaperNamePrefix($paper).'_'.$stamp;
+                }
+                $updatePayload = [
+                    $field => $url,
+                    'paper_name' => $paperDisplayTitle,
+                ];
+
+                $paper->update($updatePayload);
                 Log::info('ExamPdfExportService: PDF URL已写入数据库', [
                     'paper_id' => $paperId,
                     'field' => $field,
                     'url' => $url,
+                    'paper_name' => $paperDisplayTitle,
                 ]);
             }
         } catch (\Throwable $e) {

+ 3 - 1
app/Services/PdfStorageService.php

@@ -128,7 +128,9 @@ class PdfStorageService
             // 发送POST请求上传内容
             $response = Http::timeout(60)
                 ->attach('file', $binary, $baseName, ['Content-Type' => $mimeType])
-                ->post($uploadUrl);
+                ->post($uploadUrl, [
+                    'fileName' => $baseName,
+                ]);
 
             if (!$response->successful()) {
                 Log::error('PdfStorageService: Chunsun上传失败', [

+ 21 - 4
app/Services/QuestionBankService.php

@@ -4,6 +4,8 @@ namespace App\Services;
 
 use App\Models\Paper;
 use App\Models\Question;
+use App\Models\Student;
+use App\Support\PaperNaming;
 use Illuminate\Support\Facades\Http;
 use Illuminate\Support\Facades\Log;
 
@@ -548,24 +550,39 @@ class QuestionBankService
         try {
             // 使用数据库事务确保数据一致性
             return \Illuminate\Support\Facades\DB::transaction(function () use ($examData) {
+                if (! isset($examData['assembleType'])) {
+                    throw new \InvalidArgumentException('assembleType 缺失,无法保存试卷');
+                }
+
+                $assembleType = (int) $examData['assembleType'];
+
                 // 使用行业标准的Snowflake ID生成12位唯一数字ID
                 $uniqueId = PaperIdGenerator::generate();
                 $paperId = 'paper_'.$uniqueId;
+                $studentId = $examData['student_id'] ?? '';
+                $studentName = '________';
+                if (! empty($studentId)) {
+                    $studentName = Student::query()
+                        ->where('student_id', $studentId)
+                        ->value('name') ?? $studentId;
+                }
+                $paperDisplayName = PaperNaming::buildPaperDisplayTitle((string) $studentName, $paperId, $assembleType);
 
                 Log::info('开始保存试卷到数据库', [
                     'paper_id' => $paperId,
-                    'paper_name' => $examData['paper_name'] ?? '未命名试卷',
+                    'paper_name' => $paperDisplayName,
                     'question_count' => count($examData['questions']),
+                    'assemble_type' => $assembleType,
                 ]);
 
                 // 使用Laravel模型保存到 papers 表
                 // 注意:total_questions将在题目处理完成后再更新,确保与实际插入的题目数量一致
                 $paper = \App\Models\Paper::create([
                     'paper_id' => $paperId,
-                    'student_id' => $examData['student_id'] ?? '',
+                    'student_id' => $studentId,
                     'teacher_id' => $examData['teacher_id'] ?? '',
-                    'paper_name' => $examData['paper_name'] ?? '未命名试卷',
-                    'paper_type' => $examData['assembleType'] ?? 'auto_generated', // 组卷类型: 0 摸底; 1 智能组卷,2. 知识点组卷,3 教材组卷
+                    'paper_name' => $paperDisplayName,
+                    'paper_type' => $assembleType, // assemble_type 唯一来源
                     'diagnostic_chapter_id' => $examData['diagnostic_chapter_id'] ?? null, // 摸底的章节ID(章节摸底时记录)
                     'explanation_kp_codes' => $examData['explanation_kp_codes'] ?? null, // 学案讲解的知识点列表(最多2个)
                     'total_questions' => 0, // 临时设为0,处理完题目后再更新

+ 73 - 0
app/Support/PaperNaming.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace App\Support;
+
+use InvalidArgumentException;
+
+class PaperNaming
+{
+    public static function assembleTypeLabel(int $assembleType): string
+    {
+        return match ($assembleType) {
+            0, 9 => '智能摸底',
+            1, 4, 8 => '智能组卷',
+            2 => '知识点组卷',
+            3 => '教材组卷',
+            5 => '智能追练',
+            default => throw new InvalidArgumentException("不支持的 assemble_type: {$assembleType}"),
+        };
+    }
+
+    public static function extractExamCode(string $paperId): string
+    {
+        preg_match('/paper_(\d{15})/', $paperId, $matches);
+        $examCode = $matches[1] ?? preg_replace('/[^0-9]/', '', $paperId);
+
+        return $examCode !== '' ? $examCode : $paperId;
+    }
+
+    public static function buildPaperDisplayTitle(string $studentName, string $paperId, int $assembleType): string
+    {
+        $prefix = self::buildPaperPrefix($studentName, $paperId, $assembleType);
+        $date = now()->format('Ymd');
+
+        return "{$prefix}_{$date}";
+    }
+
+    public static function buildPaperPrefix(string $studentName, string $paperId, int $assembleType): string
+    {
+        $examCode = self::extractExamCode($paperId);
+        $label = self::assembleTypeLabel($assembleType);
+
+        return "{$studentName}_{$examCode}_{$label}";
+    }
+
+    public static function buildPaperDisplayTitleWithStamp(string $studentName, string $paperId, int $assembleType, string $stamp): string
+    {
+        $prefix = self::buildPaperPrefix($studentName, $paperId, $assembleType);
+
+        return "{$prefix}_{$stamp}";
+    }
+
+    public static function buildPdfTitle(string $studentName, string $paperId, int $assembleType, string $kind): string
+    {
+        return self::buildPaperDisplayTitle($studentName, $paperId, $assembleType)."_{$kind}";
+    }
+
+    public static function toSafeFilename(string $name, int $maxLength = 120): string
+    {
+        $safe = preg_replace('/[\\\\\\/\\:\\*\\?\\\"\\<\\>\\|\\s]+/u', '_', trim($name));
+        $safe = preg_replace('/_+/u', '_', (string) $safe);
+        $safe = trim((string) $safe, '._');
+
+        if ($safe === '') {
+            $safe = 'paper';
+        }
+
+        if (mb_strlen($safe) > $maxLength) {
+            $safe = mb_substr($safe, 0, $maxLength);
+        }
+
+        return $safe;
+    }
+}

+ 11 - 10
resources/views/pdf/exam-grading.blade.php

@@ -1,10 +1,8 @@
 @php
     $grading = true;
-    // 提取15位paper_id数字部分作为学案编号
-    $rawPaperId = $paper->paper_id ?? 'unknown';
-    preg_match('/paper_(\d{15})/', $rawPaperId, $matches);
-    $gradingCode = $matches[1] ?? preg_replace('/[^0-9]/', '', $rawPaperId);
-    $studentName = $student['name'] ?? ($paper->student_id ?? '________');
+    $gradingCode = $pdfMeta['exam_code'] ?? ($paper->paper_id ?? 'unknown');
+    $studentName = $pdfMeta['student_name'] ?? ($student['name'] ?? ($paper->student_id ?? '________'));
+    $paperHeaderTitle = $pdfMeta['header_title'] ?? ($studentName . '|' . $gradingCode . '|未知类型');
     // 生成时间(格式:2026年01月30日 15:04:05)
     $generateDateTime = now()->format('Y年m月d日 H:i:s');
     // 是否在判卷PDF末尾追加扫描判题卡
@@ -14,7 +12,7 @@
 <html lang="zh-CN">
 <head>
     <meta charset="UTF-8">
-    <title>{{ $paper->paper_name ?? '判卷预览' }}</title>
+    <title>{{ $pdfMeta['grading_pdf_title'] ?? ($paper->paper_name ?? '判卷预览') }}</title>
     <link rel="stylesheet" href="/css/katex/katex.min.css">
     <style>
         @page {
@@ -31,8 +29,8 @@
                 color: #666;
             }
             @top-right {
-                content: "{{ $gradingCode }}";
-                font-size: 17px;
+                content: "{{ $paperHeaderTitle }}";
+                font-size: 12px;
                 font-weight: 600;
                 font-family: "Noto Sans", "Liberation Sans", "Nimbus Sans", sans-serif;
                 letter-spacing: 0;
@@ -41,8 +39,8 @@
                 color: #222;
             }
             @bottom-left {
-                content: "{{ $gradingCode }}";
-                font-size: 13px;
+                content: "{{ $paperHeaderTitle }}";
+                font-size: 11px;
                 color: #666;
             }
             @bottom-right {
@@ -360,6 +358,9 @@
         <div style="display:flex;justify-content:space-between;font-size:14px;margin-top:8px;">
             <span>老师:{{ $teacher['name'] ?? '________' }}</span>
             <span>年级:@formatGrade($student['grade'] ?? '________')</span>
+            @if(!empty($pdfMeta['assemble_type_label']) && $pdfMeta['assemble_type_label'] !== '未知类型')
+                <span>类型:{{ $pdfMeta['assemble_type_label'] }}</span>
+            @endif
             <span>姓名:{{ $student['name'] ?? '________' }}</span>
             <span>得分:________</span>
         </div>

+ 11 - 10
resources/views/pdf/exam-paper.blade.php

@@ -2,14 +2,12 @@
 <html lang="zh-CN">
 <head>
     <meta charset="UTF-8">
-    <title>{{ $paper->paper_name ?? '试卷预览' }}</title>
+    <title>{{ $pdfMeta['exam_pdf_title'] ?? ($paper->paper_name ?? '试卷预览') }}</title>
     <link rel="stylesheet" href="/css/katex/katex.min.css">
     @php
-        // 提取15位paper_id数字部分作为学案编号
-        $rawPaperId = $paper->paper_id ?? 'unknown';
-        preg_match('/paper_(\d{15})/', $rawPaperId, $matches);
-        $examCode = $matches[1] ?? preg_replace('/[^0-9]/', '', $rawPaperId);
-        $studentName = $student['name'] ?? ($paper->student_id ?? '________');
+        $examCode = $pdfMeta['exam_code'] ?? ($paper->paper_id ?? 'unknown');
+        $studentName = $pdfMeta['student_name'] ?? ($student['name'] ?? ($paper->student_id ?? '________'));
+        $paperHeaderTitle = $pdfMeta['header_title'] ?? ($studentName . '|' . $examCode . '|未知类型');
         // 生成时间(格式:2026年01月30日 15:04:05)
         $generateDateTime = now()->format('Y年m月d日 H:i:s');
     @endphp
@@ -28,8 +26,8 @@
                 color: #666;
             }
             @top-right {
-                content: "{{ $examCode }}";
-                font-size: 17px;
+                content: "{{ $paperHeaderTitle }}";
+                font-size: 12px;
                 font-weight: 600;
                 font-family: "Noto Sans", "Liberation Sans", "Nimbus Sans", sans-serif;
                 letter-spacing: 0;
@@ -38,8 +36,8 @@
                 color: #222;
             }
             @bottom-left {
-                content: "{{ $examCode }}";
-                font-size: 13px;
+                content: "{{ $paperHeaderTitle }}";
+                font-size: 11px;
                 color: #666;
             }
             @bottom-right {
@@ -453,6 +451,9 @@
         <div class="info-row">
             <span>老师:{{ $teacher['name'] ?? '________' }}</span>
             <span>年级:@formatGrade($student['grade'] ?? '________')</span>
+            @if(!empty($pdfMeta['assemble_type_label']) && $pdfMeta['assemble_type_label'] !== '未知类型')
+                <span>类型:{{ $pdfMeta['assemble_type_label'] }}</span>
+            @endif
             <span>姓名:{{ $student['name'] ?? '________' }}</span>
             <span>得分:________</span>
         </div>

+ 16 - 4
resources/views/pdf/partials/kp-explain-styles.blade.php

@@ -15,8 +15,8 @@
             color: #666;
         }
         @top-right {
-            content: "{{ $examCode }}";
-            font-size: 17px;
+            content: "{{ $pdfMeta['header_title'] ?? ($studentName . '|' . $examCode) }}";
+            font-size: 12px;
             font-weight: 600;
             font-family: "Noto Sans", "Liberation Sans", "Nimbus Sans", sans-serif;
             letter-spacing: 0;
@@ -25,8 +25,8 @@
             color: #222;
         }
         @bottom-left {
-            content: "{{ $examCode }}";
-            font-size: 13px;
+            content: "{{ $pdfMeta['header_title'] ?? ($studentName . '|' . $examCode) }}";
+            font-size: 11px;
             color: #666;
         }
         @bottom-right {
@@ -79,6 +79,12 @@
             font-size: 14px;
             color: #666;
         }
+        .kp-explain-meta {
+            font-size: 14px;
+            margin-bottom: 8px;
+            color: #333;
+            font-weight: 600;
+        }
         .info-row {
             display: flex;
             justify-content: space-between;
@@ -130,6 +136,12 @@
             font-size: 14px;
             color: #666;
         }
+        .kp-explain-meta {
+            font-size: 14px;
+            margin-bottom: 8px;
+            color: #333;
+            font-weight: 600;
+        }
         .info-row {
             display: flex;
             justify-content: space-between;