- 实现基于 Laravel 11 和 Filament 3.X 的文档管理系统 - 添加用户认证和分组管理功能 - 实现文档上传、分类和权限控制 - 集成 Word 文档自动转换为 Markdown - 集成 Meilisearch 全文搜索引擎 - 实现文档在线预览功能 - 添加安全日志和审计功能 - 完整的简体中文界面 - 包含完整的项目文档和部署指南 技术栈: - Laravel 11.x - Filament 3.X - Meilisearch 1.5+ - Pandoc 文档转换 - Redis 队列系统 - Pest PHP 测试框架
853 lines
15 KiB
Markdown
853 lines
15 KiB
Markdown
# API 参考文档
|
||
|
||
本文档描述知识库系统的核心服务类和方法。
|
||
|
||
## 目录
|
||
|
||
1. [DocumentService](#documentservice)
|
||
2. [DocumentConversionService](#documentconversionservice)
|
||
3. [DocumentSearchService](#documentsearchservice)
|
||
4. [MarkdownRenderService](#markdownrenderservice)
|
||
5. [SecurityLogger](#securitylogger)
|
||
|
||
---
|
||
|
||
## DocumentService
|
||
|
||
文档管理服务,处理文档上传、下载和权限验证。
|
||
|
||
**位置**: `app/Services/DocumentService.php`
|
||
|
||
### 方法
|
||
|
||
#### uploadDocument()
|
||
|
||
上传并保存文档。
|
||
|
||
```php
|
||
public function uploadDocument(
|
||
UploadedFile $file,
|
||
string $title,
|
||
string $type,
|
||
?int $groupId,
|
||
int $uploaderId,
|
||
?string $description = null
|
||
): Document
|
||
```
|
||
|
||
**参数**:
|
||
- `$file` - 上传的文件对象
|
||
- `$title` - 文档标题
|
||
- `$type` - 文档类型('global' 或 'dedicated')
|
||
- `$groupId` - 分组 ID(专用文档必填)
|
||
- `$uploaderId` - 上传者用户 ID
|
||
- `$description` - 文档描述(可选)
|
||
|
||
**返回**: `Document` - 创建的文档模型实例
|
||
|
||
**异常**:
|
||
- `ValidationException` - 文件格式无效或专用文档未指定分组
|
||
- `Exception` - 文件存储失败
|
||
|
||
**示例**:
|
||
```php
|
||
use App\Services\DocumentService;
|
||
use Illuminate\Http\UploadedFile;
|
||
|
||
$service = app(DocumentService::class);
|
||
|
||
$document = $service->uploadDocument(
|
||
file: $request->file('document'),
|
||
title: '技术文档',
|
||
type: 'global',
|
||
groupId: null,
|
||
uploaderId: auth()->id(),
|
||
description: '系统架构说明'
|
||
);
|
||
```
|
||
|
||
#### validateDocumentAccess()
|
||
|
||
验证用户是否有权访问文档。
|
||
|
||
```php
|
||
public function validateDocumentAccess(Document $document, User $user): bool
|
||
```
|
||
|
||
**参数**:
|
||
- `$document` - 文档实例
|
||
- `$user` - 用户实例
|
||
|
||
**返回**: `bool` - 是否有权访问
|
||
|
||
**示例**:
|
||
```php
|
||
$hasAccess = $service->validateDocumentAccess($document, auth()->user());
|
||
|
||
if (!$hasAccess) {
|
||
abort(403, '您没有权限访问此文档');
|
||
}
|
||
```
|
||
|
||
#### downloadDocument()
|
||
|
||
下载文档并记录日志。
|
||
|
||
```php
|
||
public function downloadDocument(Document $document, User $user): StreamedResponse
|
||
```
|
||
|
||
**参数**:
|
||
- `$document` - 文档实例
|
||
- `$user` - 用户实例
|
||
|
||
**返回**: `StreamedResponse` - 文件流式响应
|
||
|
||
**异常**:
|
||
- `AuthorizationException` - 用户无权下载
|
||
- `FileNotFoundException` - 文件不存在
|
||
|
||
**示例**:
|
||
```php
|
||
return $service->downloadDocument($document, auth()->user());
|
||
```
|
||
|
||
#### logDownload()
|
||
|
||
记录文档下载日志。
|
||
|
||
```php
|
||
public function logDownload(Document $document, User $user): void
|
||
```
|
||
|
||
**参数**:
|
||
- `$document` - 文档实例
|
||
- `$user` - 用户实例
|
||
|
||
**示例**:
|
||
```php
|
||
$service->logDownload($document, auth()->user());
|
||
```
|
||
|
||
---
|
||
|
||
## DocumentConversionService
|
||
|
||
文档格式转换服务,将 Word 文档转换为 Markdown。
|
||
|
||
**位置**: `app/Services/DocumentConversionService.php`
|
||
|
||
### 方法
|
||
|
||
#### convertToMarkdown()
|
||
|
||
将 Word 文档转换为 Markdown 格式。
|
||
|
||
```php
|
||
public function convertToMarkdown(Document $document): string
|
||
```
|
||
|
||
**参数**:
|
||
- `$document` - 文档实例
|
||
|
||
**返回**: `string` - Markdown 内容
|
||
|
||
**异常**:
|
||
- `ConversionException` - 转换失败
|
||
|
||
**示例**:
|
||
```php
|
||
use App/Services/DocumentConversionService;
|
||
|
||
$service = app(DocumentConversionService::class);
|
||
$markdown = $service->convertToMarkdown($document);
|
||
```
|
||
|
||
#### queueConversion()
|
||
|
||
将文档转换任务加入队列。
|
||
|
||
```php
|
||
public function queueConversion(Document $document): void
|
||
```
|
||
|
||
**参数**:
|
||
- `$document` - 文档实例
|
||
|
||
**示例**:
|
||
```php
|
||
$service->queueConversion($document);
|
||
```
|
||
|
||
#### saveMarkdownToFile()
|
||
|
||
保存 Markdown 内容到文件。
|
||
|
||
```php
|
||
public function saveMarkdownToFile(Document $document, string $markdown): string
|
||
```
|
||
|
||
**参数**:
|
||
- `$document` - 文档实例
|
||
- `$markdown` - Markdown 内容
|
||
|
||
**返回**: `string` - 文件存储路径
|
||
|
||
**示例**:
|
||
```php
|
||
$path = $service->saveMarkdownToFile($document, $markdown);
|
||
```
|
||
|
||
#### updateDocumentMarkdown()
|
||
|
||
更新文档的 Markdown 相关字段。
|
||
|
||
```php
|
||
public function updateDocumentMarkdown(Document $document, string $markdownPath): void
|
||
```
|
||
|
||
**参数**:
|
||
- `$document` - 文档实例
|
||
- `$markdownPath` - Markdown 文件路径
|
||
|
||
**示例**:
|
||
```php
|
||
$service->updateDocumentMarkdown($document, $markdownPath);
|
||
```
|
||
|
||
#### handleConversionFailure()
|
||
|
||
处理转换失败的情况。
|
||
|
||
```php
|
||
public function handleConversionFailure(Document $document, Exception $e): void
|
||
```
|
||
|
||
**参数**:
|
||
- `$document` - 文档实例
|
||
- `$e` - 异常对象
|
||
|
||
**示例**:
|
||
```php
|
||
try {
|
||
$markdown = $service->convertToMarkdown($document);
|
||
} catch (Exception $e) {
|
||
$service->handleConversionFailure($document, $e);
|
||
}
|
||
```
|
||
|
||
#### getMarkdownPreview()
|
||
|
||
获取 Markdown 内容摘要。
|
||
|
||
```php
|
||
public function getMarkdownPreview(string $markdown, int $length = 500): string
|
||
```
|
||
|
||
**参数**:
|
||
- `$markdown` - Markdown 内容
|
||
- `$length` - 摘要长度(默认 500 字符)
|
||
|
||
**返回**: `string` - Markdown 摘要
|
||
|
||
**示例**:
|
||
```php
|
||
$preview = $service->getMarkdownPreview($markdown, 200);
|
||
```
|
||
|
||
---
|
||
|
||
## DocumentSearchService
|
||
|
||
文档搜索服务,提供全文搜索和权限过滤。
|
||
|
||
**位置**: `app/Services/DocumentSearchService.php`
|
||
|
||
### 方法
|
||
|
||
#### search()
|
||
|
||
搜索文档。
|
||
|
||
```php
|
||
public function search(string $query, User $user, array $filters = []): Collection
|
||
```
|
||
|
||
**参数**:
|
||
- `$query` - 搜索关键词
|
||
- `$user` - 用户实例
|
||
- `$filters` - 筛选条件数组(可选)
|
||
- `type`: 文档类型('global' 或 'dedicated')
|
||
- `group_id`: 分组 ID
|
||
|
||
**返回**: `Collection` - 搜索结果集合
|
||
|
||
**示例**:
|
||
```php
|
||
use App\Services\DocumentSearchService;
|
||
|
||
$service = app(DocumentSearchService::class);
|
||
|
||
$results = $service->search(
|
||
query: '技术文档',
|
||
user: auth()->user(),
|
||
filters: [
|
||
'type' => 'global',
|
||
]
|
||
);
|
||
```
|
||
|
||
#### indexDocument()
|
||
|
||
将文档索引到 Meilisearch。
|
||
|
||
```php
|
||
public function indexDocument(Document $document): void
|
||
```
|
||
|
||
**参数**:
|
||
- `$document` - 文档实例
|
||
|
||
**示例**:
|
||
```php
|
||
$service->indexDocument($document);
|
||
```
|
||
|
||
#### updateDocumentIndex()
|
||
|
||
更新文档索引。
|
||
|
||
```php
|
||
public function updateDocumentIndex(Document $document): void
|
||
```
|
||
|
||
**参数**:
|
||
- `$document` - 文档实例
|
||
|
||
**示例**:
|
||
```php
|
||
$service->updateDocumentIndex($document);
|
||
```
|
||
|
||
#### removeDocumentFromIndex()
|
||
|
||
从索引中删除文档。
|
||
|
||
```php
|
||
public function removeDocumentFromIndex(Document $document): void
|
||
```
|
||
|
||
**参数**:
|
||
- `$document` - 文档实例
|
||
|
||
**示例**:
|
||
```php
|
||
$service->removeDocumentFromIndex($document);
|
||
```
|
||
|
||
#### filterByUserPermissions()
|
||
|
||
根据用户权限过滤搜索结果。
|
||
|
||
```php
|
||
public function filterByUserPermissions(Collection $results, User $user): Collection
|
||
```
|
||
|
||
**参数**:
|
||
- `$results` - 搜索结果集合
|
||
- `$user` - 用户实例
|
||
|
||
**返回**: `Collection` - 过滤后的结果集合
|
||
|
||
**示例**:
|
||
```php
|
||
$filteredResults = $service->filterByUserPermissions($results, auth()->user());
|
||
```
|
||
|
||
#### prepareSearchableData()
|
||
|
||
准备文档的可搜索数据。
|
||
|
||
```php
|
||
public function prepareSearchableData(Document $document): array
|
||
```
|
||
|
||
**参数**:
|
||
- `$document` - 文档实例
|
||
|
||
**返回**: `array` - 可搜索数据数组
|
||
|
||
**示例**:
|
||
```php
|
||
$searchableData = $service->prepareSearchableData($document);
|
||
```
|
||
|
||
---
|
||
|
||
## MarkdownRenderService
|
||
|
||
Markdown 渲染服务,将 Markdown 转换为 HTML。
|
||
|
||
**位置**: `app/Services/MarkdownRenderService.php`
|
||
|
||
### 方法
|
||
|
||
#### render()
|
||
|
||
将 Markdown 渲染为 HTML。
|
||
|
||
```php
|
||
public function render(string $markdown): string
|
||
```
|
||
|
||
**参数**:
|
||
- `$markdown` - Markdown 内容
|
||
|
||
**返回**: `string` - 渲染后的 HTML
|
||
|
||
**示例**:
|
||
```php
|
||
use App\Services\MarkdownRenderService;
|
||
|
||
$service = app(MarkdownRenderService::class);
|
||
$html = $service->render($markdown);
|
||
```
|
||
|
||
#### sanitize()
|
||
|
||
清理 HTML 内容,防止 XSS 攻击。
|
||
|
||
```php
|
||
public function sanitize(string $html): string
|
||
```
|
||
|
||
**参数**:
|
||
- `$html` - HTML 内容
|
||
|
||
**返回**: `string` - 清理后的 HTML
|
||
|
||
**示例**:
|
||
```php
|
||
$safeHtml = $service->sanitize($html);
|
||
```
|
||
|
||
#### extractPreview()
|
||
|
||
从 Markdown 中提取摘要。
|
||
|
||
```php
|
||
public function extractPreview(string $markdown, int $length = 200): string
|
||
```
|
||
|
||
**参数**:
|
||
- `$markdown` - Markdown 内容
|
||
- `$length` - 摘要长度(默认 200 字符)
|
||
|
||
**返回**: `string` - 摘要文本
|
||
|
||
**示例**:
|
||
```php
|
||
$preview = $service->extractPreview($markdown, 150);
|
||
```
|
||
|
||
---
|
||
|
||
## SecurityLogger
|
||
|
||
安全日志记录服务。
|
||
|
||
**位置**: `app/Services/SecurityLogger.php`
|
||
|
||
### 方法
|
||
|
||
#### logUnauthorizedAccess()
|
||
|
||
记录未授权访问尝试。
|
||
|
||
```php
|
||
public function logUnauthorizedAccess(
|
||
User $user,
|
||
Document $document,
|
||
string $action
|
||
): void
|
||
```
|
||
|
||
**参数**:
|
||
- `$user` - 用户实例
|
||
- `$document` - 文档实例
|
||
- `$action` - 尝试的操作(如 'view', 'download')
|
||
|
||
**示例**:
|
||
```php
|
||
use App\Services\SecurityLogger;
|
||
|
||
$logger = app(SecurityLogger::class);
|
||
$logger->logUnauthorizedAccess(
|
||
user: auth()->user(),
|
||
document: $document,
|
||
action: 'download'
|
||
);
|
||
```
|
||
|
||
#### logDocumentAccess()
|
||
|
||
记录文档访问。
|
||
|
||
```php
|
||
public function logDocumentAccess(
|
||
User $user,
|
||
Document $document,
|
||
string $action
|
||
): void
|
||
```
|
||
|
||
**参数**:
|
||
- `$user` - 用户实例
|
||
- `$document` - 文档实例
|
||
- `$action` - 操作类型
|
||
|
||
**示例**:
|
||
```php
|
||
$logger->logDocumentAccess(
|
||
user: auth()->user(),
|
||
document: $document,
|
||
action: 'view'
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## 模型查询作用域
|
||
|
||
### Document 模型
|
||
|
||
#### accessibleBy()
|
||
|
||
获取用户可访问的文档。
|
||
|
||
```php
|
||
Document::accessibleBy($user)->get();
|
||
```
|
||
|
||
**参数**:
|
||
- `$user` - 用户实例
|
||
|
||
**返回**: 查询构建器
|
||
|
||
**示例**:
|
||
```php
|
||
// 获取用户可访问的所有文档
|
||
$documents = Document::accessibleBy(auth()->user())->get();
|
||
|
||
// 结合其他查询条件
|
||
$recentDocuments = Document::accessibleBy(auth()->user())
|
||
->where('created_at', '>=', now()->subDays(7))
|
||
->orderBy('created_at', 'desc')
|
||
->get();
|
||
```
|
||
|
||
#### global()
|
||
|
||
获取全局文档。
|
||
|
||
```php
|
||
Document::global()->get();
|
||
```
|
||
|
||
**返回**: 查询构建器
|
||
|
||
**示例**:
|
||
```php
|
||
$globalDocuments = Document::global()->get();
|
||
```
|
||
|
||
#### dedicated()
|
||
|
||
获取专用文档。
|
||
|
||
```php
|
||
Document::dedicated()->get();
|
||
```
|
||
|
||
**返回**: 查询构建器
|
||
|
||
**示例**:
|
||
```php
|
||
$dedicatedDocuments = Document::dedicated()
|
||
->where('group_id', $groupId)
|
||
->get();
|
||
```
|
||
|
||
---
|
||
|
||
## 策略方法
|
||
|
||
### DocumentPolicy
|
||
|
||
#### view()
|
||
|
||
检查用户是否可以查看文档。
|
||
|
||
```php
|
||
Gate::allows('view', $document);
|
||
```
|
||
|
||
**参数**:
|
||
- `$document` - 文档实例
|
||
|
||
**返回**: `bool`
|
||
|
||
**示例**:
|
||
```php
|
||
if (Gate::allows('view', $document)) {
|
||
// 用户可以查看文档
|
||
}
|
||
|
||
// 或使用 authorize 方法
|
||
$this->authorize('view', $document);
|
||
```
|
||
|
||
#### download()
|
||
|
||
检查用户是否可以下载文档。
|
||
|
||
```php
|
||
Gate::allows('download', $document);
|
||
```
|
||
|
||
**参数**:
|
||
- `$document` - 文档实例
|
||
|
||
**返回**: `bool`
|
||
|
||
**示例**:
|
||
```php
|
||
if (Gate::denies('download', $document)) {
|
||
abort(403, '您没有权限下载此文档');
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 队列任务
|
||
|
||
### ConvertDocumentToMarkdown
|
||
|
||
文档转换队列任务。
|
||
|
||
**位置**: `app/Jobs/ConvertDocumentToMarkdown.php`
|
||
|
||
**分发任务**:
|
||
```php
|
||
use App\Jobs\ConvertDocumentToMarkdown;
|
||
|
||
ConvertDocumentToMarkdown::dispatch($document);
|
||
```
|
||
|
||
**延迟分发**:
|
||
```php
|
||
ConvertDocumentToMarkdown::dispatch($document)
|
||
->delay(now()->addMinutes(5));
|
||
```
|
||
|
||
**指定队列**:
|
||
```php
|
||
ConvertDocumentToMarkdown::dispatch($document)
|
||
->onQueue('documents');
|
||
```
|
||
|
||
---
|
||
|
||
## 事件和监听器
|
||
|
||
### 文档事件
|
||
|
||
系统会在文档生命周期的关键点触发事件:
|
||
|
||
#### 文档创建后
|
||
```php
|
||
// 自动触发转换和索引
|
||
Document::created(function ($document) {
|
||
// 队列转换任务
|
||
// 索引到 Meilisearch
|
||
});
|
||
```
|
||
|
||
#### 文档更新后
|
||
```php
|
||
Document::updated(function ($document) {
|
||
// 如果文件变更,重新转换
|
||
// 更新 Meilisearch 索引
|
||
});
|
||
```
|
||
|
||
#### 文档删除后
|
||
```php
|
||
Document::deleted(function ($document) {
|
||
// 删除文件
|
||
// 从 Meilisearch 移除索引
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 配置选项
|
||
|
||
### 文档转换配置
|
||
|
||
**文件**: `config/documents.php`
|
||
|
||
```php
|
||
return [
|
||
'conversion' => [
|
||
'driver' => env('DOCUMENT_CONVERSION_DRIVER', 'pandoc'),
|
||
'pandoc_path' => env('PANDOC_PATH', '/usr/bin/pandoc'),
|
||
'timeout' => env('CONVERSION_TIMEOUT', 300),
|
||
'queue' => 'documents',
|
||
],
|
||
'markdown' => [
|
||
'renderer' => 'commonmark',
|
||
'sanitize' => true,
|
||
],
|
||
];
|
||
```
|
||
|
||
### Scout 配置
|
||
|
||
**文件**: `config/scout.php`
|
||
|
||
```php
|
||
'meilisearch' => [
|
||
'host' => env('MEILISEARCH_HOST', 'http://127.0.0.1:7700'),
|
||
'key' => env('MEILISEARCH_KEY'),
|
||
'index-settings' => [
|
||
'documents' => [
|
||
'filterableAttributes' => ['type', 'group_id', 'uploaded_by'],
|
||
'sortableAttributes' => ['created_at', 'title'],
|
||
'searchableAttributes' => ['title', 'description', 'markdown_content'],
|
||
],
|
||
],
|
||
],
|
||
```
|
||
|
||
---
|
||
|
||
## 错误处理
|
||
|
||
### 常见异常
|
||
|
||
#### ValidationException
|
||
文件格式验证失败或必填字段缺失。
|
||
|
||
```php
|
||
try {
|
||
$document = $service->uploadDocument(...);
|
||
} catch (ValidationException $e) {
|
||
return back()->withErrors($e->errors());
|
||
}
|
||
```
|
||
|
||
#### AuthorizationException
|
||
用户无权执行操作。
|
||
|
||
```php
|
||
try {
|
||
$this->authorize('download', $document);
|
||
} catch (AuthorizationException $e) {
|
||
abort(403, '您没有权限下载此文档');
|
||
}
|
||
```
|
||
|
||
#### FileNotFoundException
|
||
文件不存在。
|
||
|
||
```php
|
||
try {
|
||
return $service->downloadDocument($document, $user);
|
||
} catch (FileNotFoundException $e) {
|
||
abort(404, '文件不存在');
|
||
}
|
||
```
|
||
|
||
#### ConversionException
|
||
文档转换失败。
|
||
|
||
```php
|
||
try {
|
||
$markdown = $service->convertToMarkdown($document);
|
||
} catch (ConversionException $e) {
|
||
Log::error('文档转换失败', [
|
||
'document_id' => $document->id,
|
||
'error' => $e->getMessage(),
|
||
]);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 最佳实践
|
||
|
||
### 1. 使用依赖注入
|
||
|
||
```php
|
||
class DocumentController extends Controller
|
||
{
|
||
public function __construct(
|
||
private DocumentService $documentService,
|
||
private MarkdownRenderService $markdownService
|
||
) {}
|
||
|
||
public function preview(Document $document)
|
||
{
|
||
$this->authorize('view', $document);
|
||
|
||
$markdown = $document->getMarkdownContent();
|
||
$html = $this->markdownService->render($markdown);
|
||
|
||
return view('documents.preview', compact('html', 'document'));
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 使用事务处理
|
||
|
||
```php
|
||
DB::transaction(function () use ($file, $data) {
|
||
$document = $this->documentService->uploadDocument(
|
||
file: $file,
|
||
title: $data['title'],
|
||
type: $data['type'],
|
||
groupId: $data['group_id'] ?? null,
|
||
uploaderId: auth()->id()
|
||
);
|
||
|
||
// 其他相关操作
|
||
});
|
||
```
|
||
|
||
### 3. 使用查询作用域
|
||
|
||
```php
|
||
// 好的做法
|
||
$documents = Document::accessibleBy(auth()->user())
|
||
->where('type', 'global')
|
||
->latest()
|
||
->paginate(20);
|
||
|
||
// 避免手动实现权限过滤
|
||
```
|
||
|
||
### 4. 异步处理耗时操作
|
||
|
||
```php
|
||
// 文档转换应该异步处理
|
||
ConvertDocumentToMarkdown::dispatch($document);
|
||
|
||
// 而不是同步执行
|
||
// $service->convertToMarkdown($document); // 避免
|
||
```
|
||
|
||
---
|
||
|
||
**最后更新**:2025-12-05
|