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

176
app/Models/Document.php Normal file
View 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';
}
}

View 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
View 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
View 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);
}
}