Browse Source

优化合并 pdf 功能

yemeishu 5 giờ trước cách đây
mục cha
commit
0e910d1bd9

+ 6 - 2
app/Jobs/GenerateExamPdfJob.php

@@ -110,8 +110,12 @@ class GenerateExamPdfJob implements ShouldQueue
 
             $taskManager->updateTaskProgress($this->taskId, 70, '判卷PDF生成完成,开始合并PDF...');
 
-            // 生成合并PDF(试卷 + 判卷)
-            $mergedPdfUrl = $pdfExportService->generateMergedPdf($this->paperId);
+            // 【优化】生成合并PDF(试卷 + 判卷) - 使用快速合并模式
+            $mergedPdfUrl = $pdfExportService->generateMergedPdf($this->paperId, function($percentage, $message) use ($taskManager) {
+                // 进度更新:70% 开始,最高到 95%
+                $progress = 70 + ($percentage / 100) * 25;
+                $taskManager->updateTaskProgress($this->taskId, round($progress, 0), $message);
+            });
 
             // 构建完整的试卷内容
             $examContent = $paperPayloadService->buildExamContent($paperModel);

+ 35 - 3
app/Services/ExamPdfExportService.php

@@ -68,11 +68,16 @@ class ExamPdfExportService
     /**
      * 生成合并PDF(试卷 + 判卷)
      * 先分别生成两个PDF,然后合并
+     * 【优化】添加进度回调支持和快速合并模式
      */
-    public function generateMergedPdf(string $paperId): ?string
+    public function generateMergedPdf(string $paperId, ?callable $progressCallback = null): ?string
     {
         Log::info('generateMergedPdf 开始:', ['paper_id' => $paperId]);
 
+        if ($progressCallback) {
+            $progressCallback(0, '准备合并PDF...');
+        }
+
         $tempDir = storage_path("app/temp");
         if (!is_dir($tempDir)) {
             mkdir($tempDir, 0755, true);
@@ -84,6 +89,9 @@ class ExamPdfExportService
 
         try {
             // 先生成试卷PDF
+            if ($progressCallback) {
+                $progressCallback(10, '生成试卷PDF...');
+            }
             $examPdfUrl = $this->generateExamPdf($paperId);
             if (!$examPdfUrl) {
                 Log::error('ExamPdfExportService: 生成试卷PDF失败', ['paper_id' => $paperId]);
@@ -91,6 +99,9 @@ class ExamPdfExportService
             }
 
             // 再生成判卷PDF
+            if ($progressCallback) {
+                $progressCallback(30, '生成判卷PDF...');
+            }
             $gradingPdfUrl = $this->generateGradingPdf($paperId);
             if (!$gradingPdfUrl) {
                 Log::error('ExamPdfExportService: 生成判卷PDF失败', ['paper_id' => $paperId]);
@@ -103,6 +114,10 @@ class ExamPdfExportService
                 'grading_url' => $gradingPdfUrl
             ]);
 
+            if ($progressCallback) {
+                $progressCallback(40, '下载试卷PDF...');
+            }
+
             $examPdfPath = $tempDir . "/{$paperId}_exam.pdf";
             $gradingPdfPath = $tempDir . "/{$paperId}_grading.pdf";
 
@@ -114,6 +129,10 @@ class ExamPdfExportService
             }
             file_put_contents($examPdfPath, $examContent);
 
+            if ($progressCallback) {
+                $progressCallback(50, '下载判卷PDF...');
+            }
+
             // 下载判卷PDF
             $gradingContent = Http::get($gradingPdfUrl)->body();
             if (empty($gradingContent)) {
@@ -127,9 +146,13 @@ class ExamPdfExportService
                 'grading_size' => filesize($gradingPdfPath)
             ]);
 
-            // 合并PDF文件
+            if ($progressCallback) {
+                $progressCallback(60, '开始合并PDF文件...');
+            }
+
+            // 【优化】合并PDF文件 - 使用快速合并模式
             $mergedPdfPath = $tempDir . "/{$paperId}_merged.pdf";
-            $merged = $this->pdfMerger->merge([$examPdfPath, $gradingPdfPath], $mergedPdfPath);
+            $merged = $this->pdfMerger->mergeWithProgress([$examPdfPath, $gradingPdfPath], $mergedPdfPath, $progressCallback);
 
             if (!$merged) {
                 Log::error('ExamPdfExportService: PDF文件合并失败', [
@@ -138,6 +161,10 @@ class ExamPdfExportService
                 return null;
             }
 
+            if ($progressCallback) {
+                $progressCallback(90, '上传合并PDF...');
+            }
+
             // 读取合并后的PDF内容并上传到云存储
             $mergedPdfContent = file_get_contents($mergedPdfPath);
             $path = "exams/{$paperId}_all.pdf";
@@ -164,6 +191,11 @@ class ExamPdfExportService
                 'error' => $e->getMessage(),
                 'trace' => $e->getTraceAsString(),
             ]);
+
+            if ($progressCallback) {
+                $progressCallback(-1, '合并PDF失败: ' . $e->getMessage());
+            }
+
             return null;
         } finally {
             // 【修复】清理临时文件

+ 10 - 11
app/Services/KnowledgeMasteryService.php

@@ -162,32 +162,31 @@ class KnowledgeMasteryService
 
     /**
      * 获取单个知识点名称(带缓存)
+     * 【优化】直接从MySQL数据库查询,不再调用知识图谱API
      */
     private function getKnowledgePointName(string $kpCode): ?string
     {
         $cacheKey = "kp_name_{$kpCode}";
 
         return Cache::remember($cacheKey, 3600, function () use ($kpCode) {
+            // 【优化】直接从MySQL数据库查询知识点名称
             try {
-                // 首先尝试从知识图谱服务获取
-                $response = Http::timeout(5)
-                    ->get($this->knowledgeServiceBase . '/knowledge-points/' . $kpCode);
+                $kpName = \DB::table('knowledge_points')
+                    ->where('kp_code', $kpCode)
+                    ->value('kp_name');
 
-                if ($response->successful()) {
-                    $data = $response->json();
-                    $name = $data['cn_name'] ?? $data['en_name'] ?? null;
-                    if ($name) {
-                        return $name;
-                    }
+                if ($kpName) {
+                    Log::debug('从数据库获取知识点名称', ['kp_code' => $kpCode, 'kp_name' => $kpName]);
+                    return $kpName;
                 }
             } catch (\Throwable $e) {
-                Log::debug('Failed to get knowledge point name from service, will use local mapping', [
+                Log::debug('从数据库查询知识点名称失败,将使用本地映射', [
                     'kp_code' => $kpCode,
                     'error' => $e->getMessage(),
                 ]);
             }
 
-            // 如果知识图谱服务不可用,使用本地映射
+            // 如果数据库中没有,使用本地映射
             return $this->getLocalKnowledgePointName($kpCode);
         });
     }

+ 169 - 32
app/Services/PdfMerger.php

@@ -124,9 +124,9 @@ class PdfMerger
         try {
             switch ($this->mergeTool) {
                 case 'pdfunite':
-                    return $this->mergeWithPdfunite($pdfPaths, $outputPath);
+                    return $this->mergeWithPdfunite($pdfPaths, $outputPath, null);
                 case 'qpdf':
-                    return $this->mergeWithQpdf($pdfPaths, $outputPath);
+                    return $this->mergeWithQpdf($pdfPaths, $outputPath, null);
                 default:
                     throw new \Exception("不支持的合并工具: {$this->mergeTool}");
             }
@@ -140,73 +140,210 @@ class PdfMerger
         }
     }
 
+
+    /**
+     * 获取当前使用的合并工具
+     */
+    public function getMergeTool(): string
+    {
+        return $this->mergeTool;
+    }
+
+    /**
+     * 检查环境是否支持PDF合并
+     */
+    public function isSupported(): bool
+    {
+        return in_array($this->mergeTool, ['pdfunite', 'qpdf']);
+    }
+
     /**
-     * 使用pdfunite合并PDF
-     * pdfunite file1.pdf file2.pdf ... output.pdf
+     * 【新增】快速合并模式(带进度回调)
+     *
+     * @param array $pdfPaths PDF文件路径数组
+     * @param string $outputPath 输出文件路径
+     * @param callable|null $progressCallback 进度回调函数 (percentage, message) => void
+     * @return bool 合并是否成功
+     * @throws \Exception
      */
-    private function mergeWithPdfunite(array $pdfPaths, string $outputPath): bool
+    public function mergeWithProgress(array $pdfPaths, string $outputPath, ?callable $progressCallback = null): bool
+    {
+        // 进度回调:开始
+        if ($progressCallback) {
+            $progressCallback(0, '开始合并PDF...');
+        }
+
+        // 验证输入文件
+        foreach ($pdfPaths as $index => $path) {
+            if (!file_exists($path)) {
+                throw new \Exception("PDF文件不存在: {$path}");
+            }
+            // 进度回调:验证文件
+            if ($progressCallback) {
+                $progress = round(($index + 1) / (count($pdfPaths) + 1) * 20, 0); // 前20%用于验证
+                $progressCallback($progress, "验证PDF文件: " . basename($path));
+            }
+        }
+
+        // 确保输出目录存在
+        $outputDir = dirname($outputPath);
+        if (!is_dir($outputDir)) {
+            mkdir($outputDir, 0755, true);
+        }
+
+        Log::info('开始快速合并PDF', [
+            'tool' => $this->mergeTool,
+            'input_count' => count($pdfPaths),
+            'output_path' => $outputPath
+        ]);
+
+        try {
+            // 进度回调:开始合并
+            if ($progressCallback) {
+                $progressCallback(20, "使用 {$this->mergeTool} 开始合并PDF...");
+            }
+
+            switch ($this->mergeTool) {
+                case 'pdfunite':
+                    $result = $this->mergeWithPdfunite($pdfPaths, $outputPath, $progressCallback);
+                    break;
+                case 'qpdf':
+                    $result = $this->mergeWithQpdf($pdfPaths, $outputPath, $progressCallback);
+                    break;
+                default:
+                    throw new \Exception("不支持的合并工具: {$this->mergeTool}");
+            }
+
+            // 进度回调:完成
+            if ($progressCallback) {
+                $progressCallback(100, 'PDF合并完成!');
+            }
+
+            return $result;
+        } catch (\Exception $e) {
+            Log::error('PDF合并失败', [
+                'tool' => $this->mergeTool,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+
+            if ($progressCallback) {
+                $progressCallback(-1, 'PDF合并失败: ' . $e->getMessage());
+            }
+
+            throw $e;
+        }
+    }
+
+    /**
+     * 使用pdfunite合并PDF(带进度回调)
+     * 【优化】添加进度反馈
+     */
+    private function mergeWithPdfunite(array $pdfPaths, string $outputPath, ?callable $progressCallback = null): bool
     {
         $command = 'pdfunite ' . implode(' ', array_map('escapeshellarg', $pdfPaths)) . ' ' . escapeshellarg($outputPath);
 
         Log::debug('执行pdfunite命令', ['command' => $command]);
 
-        $output = Process::run($command);
+        // 【优化】设置超时时间为60秒,避免无限等待
+        $timeout = 60;
+
+        if ($progressCallback) {
+            $progressCallback(30, '执行pdfunite命令...');
+        }
+
+        $startTime = microtime(true);
+        $output = Process::timeout($timeout)->run($command);
+        $duration = round((microtime(true) - $startTime) * 1000, 2);
+
+        if ($progressCallback) {
+            $progressCallback(80, '处理PDF合并结果...');
+        }
 
         if ($output->successful()) {
-            Log::info('pdfunite合并成功', ['output_path' => $outputPath]);
+            Log::info('pdfunite合并成功', [
+                'output_path' => $outputPath,
+                'duration_ms' => $duration,
+                'file_count' => count($pdfPaths)
+            ]);
+
+            if ($progressCallback) {
+                $progressCallback(95, "合并完成!耗时 {$duration}ms");
+            }
+
             return true;
         }
 
         Log::error('pdfunite合并失败', [
             'exit_code' => $output->exitCode(),
             'output' => $output->output(),
-            'error' => $output->errorOutput()
+            'error' => $output->errorOutput(),
+            'duration_ms' => $duration,
+            'timeout_seconds' => $timeout
         ]);
 
         return false;
     }
 
     /**
-     * 使用qpdf合并PDF
-     * qpdf --empty --pages file1.pdf file2.pdf -- -- output.pdf
+     * 使用qpdf合并PDF(带进度回调)
+     * 【修复】qpdf命令格式不正确,缺少页面范围参数
+     * 正确格式:qpdf --empty --pages file1.pdf 1-z,file2.pdf 1-z -- output.pdf
      */
-    private function mergeWithQpdf(array $pdfPaths, string $outputPath): bool
+    private function mergeWithQpdf(array $pdfPaths, string $outputPath, ?callable $progressCallback = null): bool
     {
-        // 构建qpdf命令
-        $pagesArg = implode(' ', array_map('escapeshellarg', $pdfPaths));
+        // 【修复】构建正确的qpdf命令
+        // qpdf --empty --pages file1.pdf 1-z,file2.pdf 1-z -- output.pdf
+        // 每个文件都需要指定页面范围(1-z表示从第一页到最后一页)
+
+        $pagesArgs = [];
+        foreach ($pdfPaths as $pdfPath) {
+            $pagesArgs[] = escapeshellarg($pdfPath) . ' 1-z';
+        }
+        $pagesArg = implode(',', $pagesArgs);
+
         $command = "qpdf --empty --pages {$pagesArg} -- -- " . escapeshellarg($outputPath);
 
         Log::debug('执行qpdf命令', ['command' => $command]);
 
-        $output = Process::run($command);
+        // 【优化】设置超时时间为60秒,避免无限等待
+        $timeout = 60;
+
+        if ($progressCallback) {
+            $progressCallback(30, '执行qpdf命令...');
+        }
+
+        $startTime = microtime(true);
+        $output = Process::timeout($timeout)->run($command);
+        $duration = round((microtime(true) - $startTime) * 1000, 2);
+
+        if ($progressCallback) {
+            $progressCallback(80, '处理PDF合并结果...');
+        }
 
         if ($output->successful()) {
-            Log::info('qpdf合并成功', ['output_path' => $outputPath]);
+            Log::info('qpdf合并成功', [
+                'output_path' => $outputPath,
+                'duration_ms' => $duration,
+                'file_count' => count($pdfPaths)
+            ]);
+
+            if ($progressCallback) {
+                $progressCallback(95, "合并完成!耗时 {$duration}ms");
+            }
+
             return true;
         }
 
         Log::error('qpdf合并失败', [
             'exit_code' => $output->exitCode(),
             'output' => $output->output(),
-            'error' => $output->errorOutput()
+            'error' => $output->errorOutput(),
+            'duration_ms' => $duration,
+            'timeout_seconds' => $timeout,
+            'corrected_command' => $command // 记录修正后的命令用于调试
         ]);
 
         return false;
     }
-
-    /**
-     * 获取当前使用的合并工具
-     */
-    public function getMergeTool(): string
-    {
-        return $this->mergeTool;
-    }
-
-    /**
-     * 检查环境是否支持PDF合并
-     */
-    public function isSupported(): bool
-    {
-        return in_array($this->mergeTool, ['pdfunite', 'qpdf']);
-    }
 }