attributes, $value); } public function attributesToArray(): array { $attributes = parent::attributesToArray(); // Laravel's snake-case mutator discovery misses the legacy camelCase column. if (array_key_exists('sThumb', $attributes)) { $attributes['sThumb'] = $this->getAttribute('sThumb'); } return $attributes; } protected $appends = ['s_created_at_diff']; use Searchable; protected function sCreatedAtDiff(): Attribute { return Attribute::get(function () { // 1. 别直接用 $this->created_at,防止它还没被实例化成 Carbon 对象 $oDate = \Carbon\Carbon::parse($this->attributes['sDisplayPublishedAt'] ?? $this->created_at); $oNow = \Carbon\Carbon::now(); // 2. 计算小时差(强制转整数) $iHours = (int)$oDate->diffInHours($oNow); if ($iHours < 1) { // 不足 1 小时,看看是不是不足 1 分钟 $iMinutes = (int)$oDate->diffInMinutes($oNow); return $iMinutes < 1 ? '刚刚' : $iMinutes . '分钟前'; } if ($iHours < 24) { return $iHours . '小时前'; } // 3. 计算天数差(强制转整数) $iDays = (int)$oDate->diffInDays($oNow); if ($iDays < 30) { return $iDays . '天前'; } // 超过一个月直接显示日期 return $oDate->format('Y-m-d'); }); } public function shouldBeSearchable(): bool { return \App\Services\Seo::public($this); } public function searchableAs(): string { return config('editorial.search_index') ?: 'articles'; } public function toSearchableArray(): array { // 药水哥原则:字段要精准,类型要死磕 return [ 'id' => (int) $this->id, 'iCategoryId' => (int) $this->iCategoryId, 'sCategoryName' => (string) ($this->oCategory->sName ?? 'none'), 'sCategorySlug' => (string) ($this->oCategory->sSlug ?? 'none'), 'sTitle' => (string) $this->sTitle, 'sTitleSub' => (string) $this->sTitleSub, 'sSlug' => (string) $this->sSlug, 'sContent' => (string) mb_substr(strip_tags($this->sContent), 0, 500), 'iStatus' => (int) $this->iStatus, 'iIsTop' => (int) $this->iIsTop, 'updated_at' => (int) $this->updated_at->timestamp, ]; } /** * 判断当前文章是否超过分类设置的 SEO 暴露天数 * @return bool */ public function bIsSeoExpired(): bool { return !\App\Services\Seo::indexable($this); } public function scopePublicVisible($query) { return $query->where('articles.iStatus', 1)->where('articles.created_at', '<=', now()) ->whereHas('oCategory', fn($c) => $c->where('iStatus', 1)->where(fn($p) => $p ->where('iParentId', 0)->orWhereHas('oParent', fn($parent) => $parent->where('iStatus', 1)))); } protected static function booted() { static::saving(function ($article) { // Historical test fixtures and migration tooling may use the pre-SEO schema. if (!\Illuminate\Support\Facades\Schema::hasColumn('articles', 'sContentUpdatedAt')) return; if (!$article->exists || $article->isDirty(['sTitle','sTitleSub','sContent','sThumb','iCategoryId','iStatus','bPageThumb'])) { $article->sContentUpdatedAt = now(); } if (!$article->exists && $article->oCategory?->sSlug === 'freenode') $article->sSeoPolicy = 'archive'; }); static::saved(function ($article) { if ($article->wasRecentlyCreated || $article->wasChanged(['sTitle','sTitleSub','sContent','sThumb','iCategoryId','iStatus'])) { app(\App\Services\SeoNotifications::class)->changed($article); } ResponseCache::forget('/'); // 要改成具体文章 }); static::deleted(function ($article) { app(\App\Services\SeoNotifications::class)->changed($article,true); ResponseCache::forget('/'); // 要改成具体文章 }); } public function oTags(): BelongsToMany { return $this->belongsToMany( Tags::class, 'article_tag', 'iArticleId', 'iTagId' ); } public function oCategory(): BelongsTo { return $this->belongsTo(Categories::class, 'iCategoryId', 'id'); } public function oUser() { return $this->belongsTo(User::class, 'iUserId', 'id'); } }