- 实现基于 Laravel 11 和 Filament 3.X 的文档管理系统 - 添加用户认证和分组管理功能 - 实现文档上传、分类和权限控制 - 集成 Word 文档自动转换为 Markdown - 集成 Meilisearch 全文搜索引擎 - 实现文档在线预览功能 - 添加安全日志和审计功能 - 完整的简体中文界面 - 包含完整的项目文档和部署指南 技术栈: - Laravel 11.x - Filament 3.X - Meilisearch 1.5+ - Pandoc 文档转换 - Redis 队列系统 - Pest PHP 测试框架
177 lines
4.5 KiB
PHP
177 lines
4.5 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Laravel\Scout\Searchable;
|
|
|
|
class Document extends Model
|
|
{
|
|
use HasFactory, Searchable;
|
|
/**
|
|
* 可批量赋值的属性
|
|
*
|
|
* @var array<int, string>
|
|
*/
|
|
protected $fillable = [
|
|
'title',
|
|
'file_path',
|
|
'file_name',
|
|
'file_size',
|
|
'mime_type',
|
|
'type',
|
|
'group_id',
|
|
'uploaded_by',
|
|
'description',
|
|
'markdown_path',
|
|
'markdown_preview',
|
|
'conversion_status',
|
|
'conversion_error',
|
|
];
|
|
|
|
/**
|
|
* 获取文档所属的分组
|
|
*/
|
|
public function group(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Group::class);
|
|
}
|
|
|
|
/**
|
|
* 获取文档的上传者
|
|
*/
|
|
public function uploader(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'uploaded_by');
|
|
}
|
|
|
|
/**
|
|
* 获取文档的所有下载日志
|
|
*/
|
|
public function downloadLogs(): HasMany
|
|
{
|
|
return $this->hasMany(DownloadLog::class);
|
|
}
|
|
|
|
/**
|
|
* 查询作用域:获取用户可访问的文档
|
|
* 包含全局文档和用户分组的专用文档,排除其他分组的专用文档
|
|
*
|
|
* @param Builder $query
|
|
* @param User $user
|
|
* @return Builder
|
|
*/
|
|
public function scopeAccessibleBy(Builder $query, User $user): Builder
|
|
{
|
|
// 获取用户所属的所有分组 ID
|
|
$userGroupIds = $user->groups()->pluck('groups.id')->toArray();
|
|
|
|
return $query->where(function (Builder $query) use ($userGroupIds) {
|
|
// 包含所有全局文档
|
|
$query->where('type', 'global')
|
|
// 或者包含用户所属分组的专用文档
|
|
->orWhere(function (Builder $query) use ($userGroupIds) {
|
|
$query->where('type', 'dedicated')
|
|
->whereIn('group_id', $userGroupIds);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 查询作用域:仅获取全局文档
|
|
*
|
|
* @param Builder $query
|
|
* @return Builder
|
|
*/
|
|
public function scopeGlobal(Builder $query): Builder
|
|
{
|
|
return $query->where('type', 'global');
|
|
}
|
|
|
|
/**
|
|
* 查询作用域:仅获取专用文档
|
|
*
|
|
* @param Builder $query
|
|
* @return Builder
|
|
*/
|
|
public function scopeDedicated(Builder $query): Builder
|
|
{
|
|
return $query->where('type', 'dedicated');
|
|
}
|
|
|
|
/**
|
|
* 获取可搜索的数组数据
|
|
* 用于 Meilisearch 索引
|
|
*
|
|
* @return array
|
|
*/
|
|
public function toSearchableArray(): array
|
|
{
|
|
return [
|
|
'id' => $this->id,
|
|
'title' => $this->title,
|
|
'file_name' => $this->file_name,
|
|
'description' => $this->description,
|
|
'markdown_content' => $this->getMarkdownContent(),
|
|
'type' => $this->type,
|
|
'group_id' => $this->group_id,
|
|
'uploaded_by' => $this->uploaded_by,
|
|
'created_at' => $this->created_at?->timestamp,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 判断文档是否应该被索引
|
|
* 只有转换完成的文档才会被索引
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function shouldBeSearchable(): bool
|
|
{
|
|
return $this->conversion_status === 'completed';
|
|
}
|
|
|
|
/**
|
|
* 获取完整的 Markdown 内容
|
|
* 从文件系统读取 Markdown 文件
|
|
*
|
|
* @return string|null
|
|
*/
|
|
public function getMarkdownContent(): ?string
|
|
{
|
|
if (!$this->markdown_path) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
if (Storage::disk('markdown')->exists($this->markdown_path)) {
|
|
return Storage::disk('markdown')->get($this->markdown_path);
|
|
}
|
|
} catch (\Exception $e) {
|
|
// 记录错误但不抛出异常
|
|
\Log::warning('Failed to read markdown content', [
|
|
'document_id' => $this->id,
|
|
'markdown_path' => $this->markdown_path,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 检查文档是否已转换为 Markdown
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function hasMarkdown(): bool
|
|
{
|
|
return !empty($this->markdown_path) && $this->conversion_status === 'completed';
|
|
}
|
|
}
|