feat: 初始化知识库系统项目

- 实现基于 Laravel 11 和 Filament 3.X 的文档管理系统
- 添加用户认证和分组管理功能
- 实现文档上传、分类和权限控制
- 集成 Word 文档自动转换为 Markdown
- 集成 Meilisearch 全文搜索引擎
- 实现文档在线预览功能
- 添加安全日志和审计功能
- 完整的简体中文界面
- 包含完整的项目文档和部署指南

技术栈:
- Laravel 11.x
- Filament 3.X
- Meilisearch 1.5+
- Pandoc 文档转换
- Redis 队列系统
- Pest PHP 测试框架
This commit is contained in:
Knowledge Base System
2025-12-05 14:44:44 +08:00
commit acf549c43c
165 changed files with 32838 additions and 0 deletions

View File

@@ -0,0 +1,177 @@
<?php
namespace App\Jobs;
use App\Models\Document;
use App\Services\DocumentConversionService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
/**
* 文档转换为 Markdown 的队列任务
*/
class ConvertDocumentToMarkdown implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* 任务最大尝试次数
*
* @var int
*/
public $tries;
/**
* 任务超时时间(秒)
*
* @var int
*/
public $timeout;
/**
* 重试延迟(秒)
*
* @var int
*/
public $backoff;
/**
* 文档实例
*
* @var Document
*/
protected Document $document;
/**
* 创建新的任务实例
*
* @param Document $document
*/
public function __construct(Document $document)
{
$this->document = $document;
$this->tries = config('documents.conversion.retry_times', 3);
$this->timeout = config('documents.conversion.timeout', 300);
$this->backoff = config('documents.conversion.retry_delay', 60);
}
/**
* 执行任务
*
* @param DocumentConversionService $conversionService
* @return void
*/
public function handle(DocumentConversionService $conversionService): void
{
try {
Log::info('开始转换文档', [
'document_id' => $this->document->id,
'document_title' => $this->document->title,
'attempt' => $this->attempts(),
]);
// 转换文档为 Markdown
$result = $conversionService->convertToMarkdown($this->document);
$markdown = $result['markdown'];
$mediaDir = $result['mediaDir'] ?? null;
$tempDir = $result['tempDir'];
try {
// 保存 Markdown 文件和媒体文件
$markdownPath = $conversionService->saveMarkdownToFile($this->document, $markdown, $mediaDir);
// 更新文档的 Markdown 信息
$conversionService->updateDocumentMarkdown($this->document, $markdownPath);
} finally {
// 清理临时目录
if (isset($tempDir) && file_exists($tempDir)) {
$this->deleteDirectory($tempDir);
}
}
Log::info('文档转换成功', [
'document_id' => $this->document->id,
'document_title' => $this->document->title,
'markdown_path' => $markdownPath,
]);
// 转换成功后,触发索引(如果需要)
// 这将在后续任务中实现
// $this->document->searchable();
} catch (\Exception $e) {
Log::error('文档转换失败', [
'document_id' => $this->document->id,
'document_title' => $this->document->title,
'attempt' => $this->attempts(),
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
// 如果已达到最大重试次数,标记为失败
if ($this->attempts() >= $this->tries) {
$conversionService->handleConversionFailure($this->document, $e);
}
// 重新抛出异常以触发重试
throw $e;
}
}
/**
* 任务失败时的处理
*
* @param \Throwable $exception
* @return void
*/
public function failed(\Throwable $exception): void
{
Log::error('文档转换任务最终失败', [
'document_id' => $this->document->id,
'document_title' => $this->document->title,
'error' => $exception->getMessage(),
]);
// 确保文档状态被标记为失败
$conversionService = app(DocumentConversionService::class);
$conversionService->handleConversionFailure(
$this->document,
$exception instanceof \Exception ? $exception : new \Exception($exception->getMessage())
);
}
/**
* 递归删除目录
*
* @param string $dir 目录路径
* @return void
*/
protected function deleteDirectory(string $dir): void
{
if (!file_exists($dir)) {
return;
}
if (!is_dir($dir)) {
unlink($dir);
return;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . '/' . $file;
if (is_dir($path)) {
$this->deleteDirectory($path);
} else {
unlink($path);
}
}
rmdir($dir);
}
}