From 63f2827cc9d9100f78d936abfd5210d92174ce29 Mon Sep 17 00:00:00 2001 From: lizhuoran <625237490@qq.com> Date: Tue, 19 May 2026 08:44:35 +0800 Subject: [PATCH] fix: use pdf previews for documents --- Dockerfile | 6 +- app/Filament/Resources/DocumentResource.php | 2 +- .../DocumentResource/Pages/EditDocument.php | 1 + .../DocumentResource/Pages/ViewDocument.php | 3 +- app/Http/Controllers/DocumentController.php | 55 +++-- app/Observers/DocumentObserver.php | 2 + app/Services/DocumentPdfPreviewService.php | 190 ++++++++++++++++++ config/filesystems.php | 8 + resources/views/documents/preview.blade.php | 179 ++++------------- .../pages/document-preview-modal.blade.php | 90 ++------- .../resources/document/preview.blade.php | 71 ++----- routes/web.php | 2 + tests/Feature/DocumentPreviewServiceTest.php | 84 +++++--- tests/Feature/DocumentPreviewTest.php | 51 ++++- 14 files changed, 399 insertions(+), 345 deletions(-) create mode 100644 app/Services/DocumentPdfPreviewService.php diff --git a/Dockerfile b/Dockerfile index cfd8bfd..f09ca3a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,10 @@ RUN apk add --no-cache \ oniguruma-dev \ # Pandoc文档转换工具 pandoc \ + # LibreOffice用于生成高保真PDF预览,Noto CJK用于中文字体渲染 + libreoffice \ + font-noto-cjk \ + ttf-dejavu \ # Node.js和npm (使用较小的版本) nodejs \ npm \ @@ -131,4 +135,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD /usr/local/bin/swoole-health-check.sh || exit 1 # 使用supervisor启动多个服务 -CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] \ No newline at end of file +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] diff --git a/app/Filament/Resources/DocumentResource.php b/app/Filament/Resources/DocumentResource.php index 364c731..f435f17 100644 --- a/app/Filament/Resources/DocumentResource.php +++ b/app/Filament/Resources/DocumentResource.php @@ -248,7 +248,7 @@ class DocumentResource extends Resource ->modalSubmitAction(false) ->modalCancelActionLabel('关闭'), Tables\Actions\Action::make('preview') - ->label('预览 Markdown') + ->label('预览 PDF') ->icon('heroicon-o-eye') ->color('info') ->visible(fn(Document $record): bool => $record->conversion_status === 'completed') diff --git a/app/Filament/Resources/DocumentResource/Pages/EditDocument.php b/app/Filament/Resources/DocumentResource/Pages/EditDocument.php index 5f0c6e9..38cbe97 100644 --- a/app/Filament/Resources/DocumentResource/Pages/EditDocument.php +++ b/app/Filament/Resources/DocumentResource/Pages/EditDocument.php @@ -49,6 +49,7 @@ class EditDocument extends EditRecord if ($this->record->markdown_path && Storage::disk('markdown')->exists($this->record->markdown_path)) { Storage::disk('markdown')->delete($this->record->markdown_path); } + app(\App\Services\DocumentPdfPreviewService::class)->clearCachedPreview($this->record); $data['file_path'] = $currentFile; $data['file_name'] = $data['file_name'] ?? basename($currentFile); diff --git a/app/Filament/Resources/DocumentResource/Pages/ViewDocument.php b/app/Filament/Resources/DocumentResource/Pages/ViewDocument.php index e13f485..96e8e67 100644 --- a/app/Filament/Resources/DocumentResource/Pages/ViewDocument.php +++ b/app/Filament/Resources/DocumentResource/Pages/ViewDocument.php @@ -3,7 +3,6 @@ namespace App\Filament\Resources\DocumentResource\Pages; use App\Filament\Resources\DocumentResource; -use App\Services\DocumentPreviewService; use App\Services\DocumentService; use Filament\Actions; use Filament\Infolists\Components\Section; @@ -56,7 +55,7 @@ class ViewDocument extends ViewRecord } }), Actions\Action::make('preview') - ->label('预览 Markdown') + ->label('预览 PDF') ->icon('heroicon-o-eye') ->color('info') ->visible(fn (): bool => $this->record->conversion_status === 'completed') diff --git a/app/Http/Controllers/DocumentController.php b/app/Http/Controllers/DocumentController.php index 38c9f2f..316bd9c 100644 --- a/app/Http/Controllers/DocumentController.php +++ b/app/Http/Controllers/DocumentController.php @@ -3,27 +3,25 @@ namespace App\Http\Controllers; use App\Models\Document; +use App\Services\DocumentPdfPreviewService; use App\Services\DocumentService; -use App\Services\MarkdownRenderService; -use Illuminate\Http\Request; use Illuminate\Support\Facades\Gate; -use Illuminate\Support\Facades\Storage; class DocumentController extends Controller { protected DocumentService $documentService; - protected MarkdownRenderService $markdownRenderService; + protected DocumentPdfPreviewService $pdfPreviewService; public function __construct( DocumentService $documentService, - MarkdownRenderService $markdownRenderService + DocumentPdfPreviewService $pdfPreviewService ) { $this->documentService = $documentService; - $this->markdownRenderService = $markdownRenderService; + $this->pdfPreviewService = $pdfPreviewService; } /** - * 预览文档的 Markdown 内容(支持图片显示) + * 预览文档的 PDF 内容 * 需求:11.1, 11.3, 11.4 * * @param Document $document @@ -31,42 +29,37 @@ class DocumentController extends Controller */ public function preview(Document $document) { - // 验证用户权限(使用 DocumentPolicy) - // 需求:11.3 if (!Gate::allows('view', $document)) { abort(403, '您没有权限预览此文档'); } - // 检查文档是否已完成转换 - if ($document->conversion_status !== 'completed') { - return view('documents.preview', [ - 'document' => $document, - 'markdownHtml' => null, - ]); + return view('documents.preview', [ + 'document' => $document, + 'canPreviewPdf' => $this->pdfPreviewService->canPreview($document), + 'previewPdfUrl' => $this->pdfPreviewService->previewUrl($document), + ]); + } + + public function previewPdf(Document $document) + { + if (! Gate::allows('view', $document)) { + abort(403, '您没有权限预览此文档'); } - $markdownHtml = null; - try { - // 使用 DocumentPreviewService 的 Markdown 预览方法 - // 这会修复图片路径并渲染 Markdown - // 需求:11.1 - $previewService = app(\App\Services\DocumentPreviewService::class); - $markdownHtml = $previewService->convertMarkdownToHtml($document); - } catch (\Exception $e) { - // 记录错误但不中断流程 - \Log::error('Markdown 预览失败', [ + $path = $this->pdfPreviewService->getPreviewPath($document); + } catch (\Throwable $e) { + \Log::error('PDF 预览生成失败', [ 'document_id' => $document->id, 'error' => $e->getMessage(), ]); + + abort(500, 'PDF 预览生成失败:' . $e->getMessage()); } - // 处理内容为空的情况 - // 需求:11.4 - // 返回渲染后的 HTML 视图 - return view('documents.preview', [ - 'document' => $document, - 'markdownHtml' => $markdownHtml, + return response()->file($path, [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => 'inline; filename="document-' . $document->getKey() . '.pdf"', ]); } diff --git a/app/Observers/DocumentObserver.php b/app/Observers/DocumentObserver.php index 84890b0..52715cd 100644 --- a/app/Observers/DocumentObserver.php +++ b/app/Observers/DocumentObserver.php @@ -124,6 +124,8 @@ class DocumentObserver ]); } } + + app(\App\Services\DocumentPdfPreviewService::class)->clearCachedPreview($document); } catch (\Exception $e) { \Log::error('清理文档文件失败', [ 'document_id' => $document->id, diff --git a/app/Services/DocumentPdfPreviewService.php b/app/Services/DocumentPdfPreviewService.php new file mode 100644 index 0000000..6515def --- /dev/null +++ b/app/Services/DocumentPdfPreviewService.php @@ -0,0 +1,190 @@ +conversion_status === 'completed' + && ! empty($document->file_path) + && Storage::disk('local')->exists($document->file_path); + } + + public function getPreviewPath(Document $document): string + { + if (! $this->canPreview($document)) { + throw new \RuntimeException('文档尚未完成转换或原文件不存在'); + } + + if ($this->isPdf($document)) { + return Storage::disk('local')->path($document->file_path); + } + + $previewPath = $this->cachedPreviewPath($document); + + if (! $this->cachedPreviewIsFresh($document, $previewPath)) { + $this->generatePdfPreview($document, $previewPath); + } + + return Storage::disk('previews')->path($previewPath); + } + + public function previewUrl(Document $document): string + { + return route('documents.preview-pdf', $document); + } + + public function clearCachedPreview(Document $document): void + { + Storage::disk('previews')->deleteDirectory((string) $document->getKey()); + } + + protected function isPdf(Document $document): bool + { + $extension = strtolower(pathinfo($document->display_file_name ?: $document->file_path, PATHINFO_EXTENSION)); + + return $document->mime_type === 'application/pdf' || $extension === 'pdf'; + } + + protected function cachedPreviewPath(Document $document): string + { + return $document->getKey() . '/preview-libreoffice.pdf'; + } + + protected function cachedPreviewIsFresh(Document $document, string $previewPath): bool + { + if (! Storage::disk('previews')->exists($previewPath)) { + return false; + } + + $sourceMtime = Storage::disk('local')->lastModified($document->file_path); + $previewMtime = Storage::disk('previews')->lastModified($previewPath); + + return $previewMtime >= $sourceMtime; + } + + protected function generatePdfPreview(Document $document, string $previewPath): void + { + $sourcePath = Storage::disk('local')->path($document->file_path); + $absolutePreviewPath = Storage::disk('previews')->path($previewPath); + $previewDirectory = dirname($absolutePreviewPath); + + if (! is_dir($previewDirectory) && ! mkdir($previewDirectory, 0775, true) && ! is_dir($previewDirectory)) { + throw new \RuntimeException('无法创建 PDF 预览目录'); + } + + $this->convertWithLibreOffice($sourcePath, $absolutePreviewPath, $previewDirectory); + + if (! file_exists($absolutePreviewPath) || filesize($absolutePreviewPath) === 0) { + throw new \RuntimeException('PDF 预览生成失败'); + } + } + + protected function convertWithLibreOffice(string $sourcePath, string $targetPath, string $workingDirectory): void + { + $binary = $this->resolveLibreOfficeBinary(); + + if ($binary === null) { + throw new \RuntimeException('无法生成准确的 PDF 预览:服务器未安装 LibreOffice/soffice。请安装 LibreOffice 和中文字体后重试。'); + } + + $tmpDirectory = $workingDirectory . '/tmp-' . bin2hex(random_bytes(8)); + $profileDirectory = $tmpDirectory . '/profile'; + + if (! mkdir($profileDirectory, 0775, true) && ! is_dir($profileDirectory)) { + throw new \RuntimeException('无法创建 LibreOffice 临时目录'); + } + + $process = new Process([ + $binary, + '--headless', + '--nologo', + '--nofirststartwizard', + '--norestore', + '-env:UserInstallation=file://' . $profileDirectory, + '--convert-to', + 'pdf', + '--outdir', + $tmpDirectory, + $sourcePath, + ]); + $process->setTimeout((int) config('documents.conversion.timeout', 300)); + + try { + $process->mustRun(); + $convertedPath = $this->findConvertedPdf($tmpDirectory, $sourcePath); + + if ($convertedPath === null) { + throw new \RuntimeException(trim($process->getOutput() . "\n" . $process->getErrorOutput()) ?: 'LibreOffice 未输出 PDF 文件'); + } + + if (! rename($convertedPath, $targetPath)) { + throw new \RuntimeException('无法保存 LibreOffice 生成的 PDF 预览'); + } + } catch (ProcessFailedException $e) { + throw new \RuntimeException('LibreOffice 转换 PDF 失败:' . trim($e->getProcess()->getErrorOutput() ?: $e->getProcess()->getOutput()), 0, $e); + } finally { + $this->deleteDirectory($tmpDirectory); + } + } + + protected function resolveLibreOfficeBinary(): ?string + { + $configured = env('LIBREOFFICE_BINARY'); + $candidates = array_filter([ + is_string($configured) && $configured !== '' ? $configured : null, + '/opt/homebrew/bin/soffice', + '/opt/homebrew/bin/libreoffice', + '/usr/bin/libreoffice', + '/usr/bin/soffice', + '/usr/local/bin/libreoffice', + '/usr/local/bin/soffice', + '/Applications/LibreOffice.app/Contents/MacOS/soffice', + ]); + + foreach ($candidates as $candidate) { + if (is_executable($candidate)) { + return $candidate; + } + } + + return null; + } + + protected function findConvertedPdf(string $directory, string $sourcePath): ?string + { + $expectedPath = $directory . '/' . pathinfo($sourcePath, PATHINFO_FILENAME) . '.pdf'; + + if (is_file($expectedPath)) { + return $expectedPath; + } + + $pdfFiles = glob($directory . '/*.pdf') ?: []; + + return $pdfFiles[0] ?? null; + } + + protected function deleteDirectory(string $directory): void + { + if (! is_dir($directory)) { + return; + } + + $items = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($items as $item) { + $item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname()); + } + + rmdir($directory); + } +} diff --git a/config/filesystems.php b/config/filesystems.php index 2523623..97d94cb 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -63,6 +63,14 @@ return [ 'report' => false, ], + 'previews' => [ + 'driver' => 'local', + 'root' => storage_path('app/private/previews'), + 'visibility' => 'private', + 'throw' => false, + 'report' => false, + ], + 's3' => [ 'driver' => 's3', 'key' => env('AWS_ACCESS_KEY_ID'), diff --git a/resources/views/documents/preview.blade.php b/resources/views/documents/preview.blade.php index fb8e9f0..69a651e 100644 --- a/resources/views/documents/preview.blade.php +++ b/resources/views/documents/preview.blade.php @@ -5,162 +5,65 @@ - {{ $document->title }} - Markdown 预览 + {{ $document->title }} - PDF 预览 - @vite(['resources/css/app.css']) - - - -
- -
-
-
-

{{ $document->title }}

- -
-
- - - - {{ $document->type === 'global' ? '全局知识库' : '专用知识库' }} -
- - @if($document->group) -
- - - - {{ $document->group->name }} -
- @endif - -
- - - - {{ $document->uploader->name }} -
- -
- - - - {{ $document->created_at->format('Y年m月d日 H:i') }} -
+ +
+
+
+
+

{{ $document->title }}

+
+ {{ $document->display_file_name }} + {{ $document->uploader->name }} + {{ $document->created_at->format('Y年m月d日 H:i') }}
@if($document->description) -

{{ $document->description }}

+

{{ $document->description }}

@endif
- -
+
- -
- @if($markdownHtml) - {!! $markdownHtml !!} +
+ @if($canPreviewPdf) + @else -
-
📄
-

Markdown 内容为空

-

该文档的 Markdown 内容尚未生成或为空

- - - - - 下载原始文档 - -
+
+

PDF 预览暂不可用

+

该文档尚未完成转换或原文件不存在。

+ + 下载原始文档 + +
@endif -
+
diff --git a/resources/views/filament/pages/document-preview-modal.blade.php b/resources/views/filament/pages/document-preview-modal.blade.php index 953c073..cdeadec 100644 --- a/resources/views/filament/pages/document-preview-modal.blade.php +++ b/resources/views/filament/pages/document-preview-modal.blade.php @@ -1,19 +1,13 @@ @php - use App\Services\DocumentPreviewService; - - $previewService = app(DocumentPreviewService::class); - $htmlContent = null; - $error = null; - - try { - $htmlContent = $previewService->convertToHtml($document); - } catch (\Exception $e) { - $error = $e->getMessage(); - } + use App\Services\DocumentPdfPreviewService; + + $previewService = app(DocumentPdfPreviewService::class); + $canPreview = $previewService->canPreview($document); + $previewUrl = $canPreview ? $previewService->previewUrl($document) : null; @endphp
- @if ($error) + @if (! $canPreview)
@@ -21,81 +15,29 @@

预览失败

-

{{ $error }}

+

该文档尚未完成转换或原文件不存在

- @elseif ($htmlContent) + @elseif ($previewUrl)

- 文档内容预览 + PDF 内容预览

{{ $document->display_file_name }}
- -
-
- {!! $htmlContent !!} -
-
- -
-

- 提示:这是文档的预览版本,可能与原始格式略有差异。如需查看完整格式,请下载文档。 -

-
-
- @else -
-
- - - - -

正在加载文档预览...

-
+ +
@endif
- - diff --git a/resources/views/filament/resources/document/preview.blade.php b/resources/views/filament/resources/document/preview.blade.php index 893b8f6..5e02db2 100644 --- a/resources/views/filament/resources/document/preview.blade.php +++ b/resources/views/filament/resources/document/preview.blade.php @@ -1,18 +1,9 @@ @php - use App\Services\DocumentPreviewService; + use App\Services\DocumentPdfPreviewService; - $previewService = app(DocumentPreviewService::class); + $previewService = app(DocumentPdfPreviewService::class); $canPreview = $previewService->canPreview($document); - $htmlContent = null; - $error = null; - - if ($canPreview) { - try { - $htmlContent = $previewService->convertToHtml($document); - } catch (\Exception $e) { - $error = $e->getMessage(); - } - } + $previewUrl = $canPreview ? $previewService->previewUrl($document) : null; @endphp
@@ -37,7 +28,7 @@

文档等待转换中...

- @elseif ($error) + @elseif (! $canPreview)
@@ -45,16 +36,16 @@

预览加载失败

-

{{ $error }}

+

该文档尚未完成转换或原文件不存在

- @elseif ($htmlContent) + @elseif ($previewUrl)

- 文档内容预览 + PDF 内容预览

{{ $document->display_file_name }} @@ -62,48 +53,12 @@
-
-
- {!! $htmlContent !!} -
-
+
@endif
- - diff --git a/routes/web.php b/routes/web.php index b74a81b..269bd3e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -93,6 +93,8 @@ Route::get('/guides/pages/{page}', function (\App\Models\GuidePage $page) { Route::middleware(['auth'])->group(function () { Route::get('/documents/{document}/preview', [DocumentController::class, 'preview']) ->name('documents.preview'); + Route::get('/documents/{document}/preview-pdf', [DocumentController::class, 'previewPdf']) + ->name('documents.preview-pdf'); Route::get('/documents/{document}/download', [DocumentController::class, 'download']) ->name('documents.download'); }); diff --git a/tests/Feature/DocumentPreviewServiceTest.php b/tests/Feature/DocumentPreviewServiceTest.php index fcb453e..33a8df0 100644 --- a/tests/Feature/DocumentPreviewServiceTest.php +++ b/tests/Feature/DocumentPreviewServiceTest.php @@ -4,9 +4,8 @@ namespace Tests\Feature; use App\Models\Document; use App\Models\User; -use App\Services\DocumentPreviewService; +use App\Services\DocumentPdfPreviewService; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; use Tests\TestCase; @@ -14,56 +13,79 @@ class DocumentPreviewServiceTest extends TestCase { use RefreshDatabase; - protected DocumentPreviewService $previewService; + protected DocumentPdfPreviewService $previewService; protected function setUp(): void { parent::setUp(); - $this->previewService = new DocumentPreviewService(); + $this->previewService = new DocumentPdfPreviewService(); Storage::fake('local'); + Storage::fake('previews'); + config(['scout.driver' => 'null']); } - public function test_可以检查文档是否支持预览(): void + public function test_可以检查文档是否支持_pdf_预览(): void { $user = User::factory()->create(); - - // 创建一个 .docx 文档 + $document = Document::factory()->create([ - 'file_name' => 'test.docx', - 'uploaded_by' => $user->id, - ]); - - $this->assertTrue($this->previewService->canPreview($document)); - - // 创建一个 .doc 文档 - $document2 = Document::factory()->create([ - 'file_name' => 'test.doc', - 'uploaded_by' => $user->id, - ]); - - $this->assertTrue($this->previewService->canPreview($document2)); - - // 创建一个不支持的格式 - $document3 = Document::factory()->create([ + 'conversion_status' => 'completed', + 'file_path' => 'documents/test.pdf', 'file_name' => 'test.pdf', + 'mime_type' => 'application/pdf', 'uploaded_by' => $user->id, ]); - - $this->assertFalse($this->previewService->canPreview($document3)); + + Storage::disk('local')->put($document->file_path, '%PDF-1.4 test'); + + $this->assertTrue($this->previewService->canPreview($document)); + + $document2 = Document::factory()->create([ + 'conversion_status' => 'pending', + 'file_path' => 'documents/pending.pdf', + 'file_name' => 'pending.pdf', + 'mime_type' => 'application/pdf', + 'uploaded_by' => $user->id, + ]); + + Storage::disk('local')->put($document2->file_path, '%PDF-1.4 test'); + + $this->assertFalse($this->previewService->canPreview($document2)); } public function test_文档不存在时抛出异常(): void { $user = User::factory()->create(); $document = Document::factory()->create([ - 'file_path' => 'documents/2024/01/01/nonexistent.docx', - 'file_name' => 'nonexistent.docx', + 'conversion_status' => 'completed', + 'file_path' => 'documents/2024/01/01/nonexistent.pdf', + 'file_name' => 'nonexistent.pdf', + 'mime_type' => 'application/pdf', 'uploaded_by' => $user->id, ]); - $this->expectException(\Exception::class); - $this->expectExceptionMessage('文档文件不存在'); - - $this->previewService->convertToHtml($document); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('文档尚未完成转换或原文件不存在'); + + $this->previewService->getPreviewPath($document); + } + + public function test_非_pdf_文件在缺少_libreoffice_时给出明确错误(): void + { + $user = User::factory()->create(); + $document = Document::factory()->create([ + 'conversion_status' => 'completed', + 'file_path' => 'documents/test.docx', + 'file_name' => 'test.docx', + 'mime_type' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'uploaded_by' => $user->id, + ]); + + Storage::disk('local')->put($document->file_path, 'fake-docx-content'); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('LibreOffice'); + + $this->previewService->getPreviewPath($document); } } diff --git a/tests/Feature/DocumentPreviewTest.php b/tests/Feature/DocumentPreviewTest.php index 71cbccc..495b1c0 100644 --- a/tests/Feature/DocumentPreviewTest.php +++ b/tests/Feature/DocumentPreviewTest.php @@ -4,51 +4,66 @@ use App\Models\Document; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Storage; +use Spatie\Permission\Models\Permission; uses(RefreshDatabase::class); beforeEach(function () { Storage::fake('local'); + Storage::fake('previews'); + config(['scout.driver' => 'null']); + + Permission::findOrCreate('document.view', 'web'); }); -test('用户可以预览已转换的文档', function () { +test('用户可以打开已转换文档的 PDF 预览页', function () { $user = User::factory()->create(); + $user->givePermissionTo('document.view'); $document = Document::factory()->create([ 'conversion_status' => 'completed', - 'markdown_path' => 'markdown/test.md', + 'file_path' => 'documents/test.pdf', + 'file_name' => 'test.pdf', + 'mime_type' => 'application/pdf', ]); - Storage::disk('local')->put($document->markdown_path, '# 测试标题\n\n这是测试内容。'); + Storage::disk('local')->put($document->file_path, '%PDF-1.4 test'); $response = $this->actingAs($user)->get(route('documents.preview', $document)); $response->assertStatus(200); $response->assertSee($document->title); - $response->assertSee('测试标题'); + $response->assertSee(route('documents.preview-pdf', $document)); + $response->assertSee('PDF 预览', false); }); -test('预览页面正确处理 Markdown 内容为空的情况', function () { +test('预览页面正确处理 PDF 预览不可用的情况', function () { $user = User::factory()->create(); + $user->givePermissionTo('document.view'); $document = Document::factory()->create([ 'conversion_status' => 'completed', - 'markdown_path' => null, + 'file_path' => 'documents/missing.pdf', + 'file_name' => 'missing.pdf', + 'mime_type' => 'application/pdf', ]); $response = $this->actingAs($user)->get(route('documents.preview', $document)); $response->assertStatus(200); - $response->assertSee('Markdown 内容为空'); + $response->assertSee('PDF 预览暂不可用'); $response->assertSee('下载原始文档'); }); test('预览页面显示下载按钮', function () { $user = User::factory()->create(); + $user->givePermissionTo('document.view'); $document = Document::factory()->create([ 'conversion_status' => 'completed', - 'markdown_path' => 'markdown/test.md', + 'file_path' => 'documents/test.pdf', + 'file_name' => 'test.pdf', + 'mime_type' => 'application/pdf', ]); - Storage::disk('local')->put($document->markdown_path, '# 测试'); + Storage::disk('local')->put($document->file_path, '%PDF-1.4 test'); $response = $this->actingAs($user)->get(route('documents.preview', $document)); @@ -56,3 +71,21 @@ test('预览页面显示下载按钮', function () { $response->assertSee('下载原文档'); $response->assertSee(route('documents.download', $document)); }); + +test('PDF 原文件直接以内联 PDF 响应预览', function () { + $user = User::factory()->create(); + $user->givePermissionTo('document.view'); + $document = Document::factory()->create([ + 'conversion_status' => 'completed', + 'file_path' => 'documents/test.pdf', + 'file_name' => 'test.pdf', + 'mime_type' => 'application/pdf', + ]); + + Storage::disk('local')->put($document->file_path, '%PDF-1.4 test'); + + $response = $this->actingAs($user)->get(route('documents.preview-pdf', $document)); + + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/pdf'); +});