- 实现基于 Laravel 11 和 Filament 3.X 的文档管理系统 - 添加用户认证和分组管理功能 - 实现文档上传、分类和权限控制 - 集成 Word 文档自动转换为 Markdown - 集成 Meilisearch 全文搜索引擎 - 实现文档在线预览功能 - 添加安全日志和审计功能 - 完整的简体中文界面 - 包含完整的项目文档和部署指南 技术栈: - Laravel 11.x - Filament 3.X - Meilisearch 1.5+ - Pandoc 文档转换 - Redis 队列系统 - Pest PHP 测试框架
146 lines
4.7 KiB
PHP
146 lines
4.7 KiB
PHP
<?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(),
|
||
]);
|
||
}
|
||
}
|