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,145 @@
<?php
namespace App\Services;
use App\Models\Document;
use App\Models\DownloadLog;
use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
class DocumentService
{
/**
* 上传文档
*
* @param UploadedFile $file 上传的文件
* @param string $title 文档标题
* @param string $type 文档类型 ('global' 'dedicated')
* @param int|null $groupId 分组 ID (专用文档必填)
* @param int $uploaderId 上传者用户 ID
* @return Document
* @throws \Exception
*/
public function uploadDocument(
UploadedFile $file,
string $title,
string $type,
?int $groupId,
int $uploaderId
): Document {
// 验证文件格式
$extension = strtolower($file->getClientOriginalExtension());
if (!in_array($extension, ['doc', 'docx'])) {
throw new \InvalidArgumentException('文件格式不支持,请上传 Word 文档(.doc 或 .docx');
}
// 验证专用文档必须有分组
if ($type === 'dedicated' && empty($groupId)) {
throw new \InvalidArgumentException('专用知识库文档必须指定所属分组');
}
// 使用事务确保一致性
return DB::transaction(function () use ($file, $title, $type, $groupId, $uploaderId) {
// 获取原始文件名
$originalFileName = $file->getClientOriginalName();
// 生成文件存储路径,使用原始文件名
$directory = 'documents/' . date('Y/m/d');
$filePath = $file->storeAs($directory, $originalFileName, 'local');
// 创建数据库记录,设置初始转换状态为 pending
$document = Document::create([
'title' => $title,
'file_path' => $filePath,
'file_name' => $originalFileName,
'file_size' => $file->getSize(),
'mime_type' => $file->getMimeType(),
'type' => $type,
'group_id' => $groupId,
'uploaded_by' => $uploaderId,
'description' => '',
'conversion_status' => 'pending',
]);
// 文档保存成功后,触发异步转换
$conversionService = app(DocumentConversionService::class);
$conversionService->queueConversion($document);
return $document;
});
}
/**
* 验证用户是否有权访问指定文档
*
* @param Document $document 要访问的文档
* @param User $user 用户
* @return bool
*/
public function validateDocumentAccess(Document $document, User $user): bool
{
// 如果是全局文档,所有用户都可以访问
if ($document->type === 'global') {
return true;
}
// 如果是专用文档,检查用户是否属于该文档的分组
if ($document->type === 'dedicated') {
// 获取用户所属的所有分组 ID
$userGroupIds = $user->groups()->pluck('groups.id')->toArray();
// 检查文档的分组 ID 是否在用户的分组列表中
return in_array($document->group_id, $userGroupIds);
}
return false;
}
/**
* 下载文档
*
* @param Document $document 要下载的文档
* @param User $user 用户
* @return StreamedResponse
* @throws \Exception
*/
public function downloadDocument(Document $document, User $user): StreamedResponse
{
// 验证用户权限
if (!$this->validateDocumentAccess($document, $user)) {
throw new \Exception('您没有权限访问此文档');
}
// 检查文件是否存在
if (!Storage::disk('local')->exists($document->file_path)) {
throw new \Exception('文档不存在或已被删除');
}
// 返回文件流式响应,使用原始文件名
return Storage::disk('local')->download(
$document->file_path,
$document->file_name
);
}
/**
* 记录文档下载日志
*
* @param Document $document 被下载的文档
* @param User $user 下载的用户
* @param string|null $ipAddress IP 地址
* @return DownloadLog
*/
public function logDownload(Document $document, User $user, ?string $ipAddress = null): DownloadLog
{
return DownloadLog::create([
'document_id' => $document->id,
'user_id' => $user->id,
'downloaded_at' => now(),
'ip_address' => $ipAddress ?? request()->ip(),
]);
}
}