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:
176
app/Models/Document.php
Normal file
176
app/Models/Document.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?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';
|
||||
}
|
||||
}
|
||||
56
app/Models/DownloadLog.php
Normal file
56
app/Models/DownloadLog.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class DownloadLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
/**
|
||||
* 表示模型不使用 created_at 和 updated_at 时间戳
|
||||
* 因为我们使用自定义的 downloaded_at 字段
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* 可批量赋值的属性
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'document_id',
|
||||
'user_id',
|
||||
'downloaded_at',
|
||||
'ip_address',
|
||||
];
|
||||
|
||||
/**
|
||||
* 应该被转换为日期的属性
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $casts = [
|
||||
'downloaded_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取下载日志关联的文档
|
||||
*/
|
||||
public function document(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Document::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载日志关联的用户
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
53
app/Models/Group.php
Normal file
53
app/Models/Group.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Group extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
/**
|
||||
* 可批量赋值的属性
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
];
|
||||
|
||||
/**
|
||||
* 模型的启动方法
|
||||
* 注册模型事件监听器
|
||||
*/
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
// 监听分组删除事件
|
||||
static::deleting(function (Group $group) {
|
||||
// 将该分组的所有专用文档的 group_id 设置为 null(孤立状态)
|
||||
$group->documents()->update(['group_id' => null]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分组的所有用户
|
||||
*/
|
||||
public function users(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分组的所有文档
|
||||
*/
|
||||
public function documents(): HasMany
|
||||
{
|
||||
return $this->hasMany(Document::class);
|
||||
}
|
||||
}
|
||||
74
app/Models/User.php
Normal file
74
app/Models/User.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户所属的所有分组
|
||||
*/
|
||||
public function groups(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Group::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户上传的所有文档
|
||||
*/
|
||||
public function uploadedDocuments(): HasMany
|
||||
{
|
||||
return $this->hasMany(Document::class, 'uploaded_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的所有下载日志
|
||||
*/
|
||||
public function downloadLogs(): HasMany
|
||||
{
|
||||
return $this->hasMany(DownloadLog::class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user