'array', 'is_sticky' => 'boolean', 'close_comment' => 'boolean', 'published_at' => 'datetime', ]; public function category(): BelongsTo { return $this->belongsTo(Category::class); } public function author(): BelongsTo { return $this->belongsTo(User::class, 'user_id'); } public function coverMedia(): BelongsTo { return $this->belongsTo(MediaLibrary::class, 'cover_media_id'); } public function coverImageUrl(?string $conversion = 'card'): ?string { $libraryUrl = $this->coverMedia?->url; if (filled($libraryUrl)) { return (string) $libraryUrl; } $cover = $this->getFirstMedia('cover'); if ($cover === null) { return null; } return $cover->getUrl($conversion) ?: $cover->getUrl(); } public function comments(): HasMany { return $this->hasMany(Comment::class); } public function scopePublished(Builder $query): Builder { return $query->where('status', 'published') ->whereNotNull('published_at') ->where('published_at', '<=', now()); } public function scopeSearch(Builder $query, ?string $keyword): Builder { if (! $keyword) { return $query; } return $query->where(function (Builder $q) use ($keyword) { $q->where('title', 'like', "%{$keyword}%") ->orWhere('content', 'like', "%{$keyword}%") ->orWhere('excerpt', 'like', "%{$keyword}%") ->orWhere('keywords', 'like', "%{$keyword}%"); }); } public function registerMediaCollections(): void { $this->addMediaCollection('attachments')->useDisk(\App\Support\MediaDisk::name()); $this->addMediaCollection('cover')->useDisk(\App\Support\MediaDisk::name()) ->singleFile() ->registerMediaConversions(function () { $this->addMediaConversion('card')->width(1200)->height(630)->sharpen(1); }); } public function registerMediaConversions(?Media $media = null): void { [$tw, $th] = array_pad(explode('x', strtolower((string) blog_setting('attachments_thumbs_size', '500x500'))), 2, '500'); $this->addMediaConversion('thumb') ->width((int) $tw) ->height((int) $th) ->performOnCollections('attachments'); } public function getUrlAttribute(): string { return route('posts.show', $this->id); } /** * 作为可购买实体(单篇解锁)的展示约定。 */ public function payableLabel(): string { return '文章:'.$this->title; } public function payableUrl(): ?string { return route('filament.admin.resources.posts.edit', $this); } public function getExcerptOrFallbackAttribute(): string { if ($this->excerpt) { return $this->excerpt; } // 会员专享文章不落入内容摘要,避免泄漏到 paywall/SEO meta if (! empty(($this->meta ?? [])['members_only'])) { return ''; } $text = strip_tags($this->content); // 付费块不进入摘要,避免付费内容泄漏到列表页/SEO meta $text = preg_replace('/\[paid\].*?\[\/paid\]/s', '', $text) ?? $text; $text = preg_replace('/!\[([^\]]*)\]\((?:media|attach):\d+\)/i', '$1', $text) ?? $text; $text = preg_replace('/\[([^\]]*)\]\((?:media|attach):\d+\)/i', '$1', $text) ?? $text; $text = preg_replace('/\[[^\]]*\]/', '', $text) ?? $text; return mb_substr($text, 0, 200); } }