117 lines
4.2 KiB
PHP
117 lines
4.2 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Services\EarlyGrowth;
|
||
|
||
use App\Models\Artwork;
|
||
use Illuminate\Support\Collection;
|
||
use Illuminate\Support\Facades\Cache;
|
||
|
||
/**
|
||
* SpotlightEngine
|
||
*
|
||
* Selects and rotates curated spotlight artworks for use in feed blending,
|
||
* grid filling, and dedicated spotlight sections.
|
||
*
|
||
* Selection is date-seeded so the spotlight rotates daily without DB writes.
|
||
* No artwork timestamps or engagement metrics are modified — this is purely
|
||
* a read-and-present layer.
|
||
*/
|
||
final class SpotlightEngine implements SpotlightEngineInterface
|
||
{
|
||
/**
|
||
* Return spotlight artworks for the current day.
|
||
* Cached for `early_growth.cache_ttl.spotlight` seconds (default 1 hour).
|
||
* Rotates daily via a date-seeded RAND() expression.
|
||
*
|
||
* Returns empty collection when SpotlightEngine is disabled.
|
||
*/
|
||
public function getSpotlight(int $limit = 6): Collection
|
||
{
|
||
if (! EarlyGrowth::spotlightEnabled()) {
|
||
return collect();
|
||
}
|
||
|
||
$ttl = (int) config('early_growth.cache_ttl.spotlight', 3600);
|
||
$cacheKey = 'egs.spotlight.' . now()->format('Y-m-d') . ".{$limit}";
|
||
|
||
return Cache::remember($cacheKey, $ttl, fn (): Collection => $this->selectSpotlight($limit));
|
||
}
|
||
|
||
/**
|
||
* Return high-quality older artworks for feed blending ("curated" pool).
|
||
* Excludes artworks newer than $olderThanDays to keep them out of the
|
||
* "fresh" section yet available for blending.
|
||
*
|
||
* Cached per (limit, olderThanDays) tuple and rotated daily.
|
||
*/
|
||
public function getCurated(int $limit = 12, int $olderThanDays = 7): Collection
|
||
{
|
||
if (! EarlyGrowth::enabled()) {
|
||
return collect();
|
||
}
|
||
|
||
$ttl = (int) config('early_growth.cache_ttl.spotlight', 3600);
|
||
$cacheKey = 'egs.curated.' . now()->format('Y-m-d') . ".{$limit}.{$olderThanDays}";
|
||
|
||
return Cache::remember($cacheKey, $ttl, fn (): Collection => $this->selectCurated($limit, $olderThanDays));
|
||
}
|
||
|
||
// ─── Private selection logic ──────────────────────────────────────────────
|
||
|
||
/**
|
||
* Select spotlight artworks.
|
||
* Uses a date-based seed for deterministic daily rotation.
|
||
* Fetches 3× the needed count and selects the top-ranked subset.
|
||
*/
|
||
private function selectSpotlight(int $limit): Collection
|
||
{
|
||
$seed = (int) now()->format('Ymd');
|
||
|
||
// Artworks published > 7 days ago with meaningful ranking score
|
||
return Artwork::query()
|
||
->public()
|
||
->published()
|
||
->with([
|
||
'user:id,name,username',
|
||
'user.profile:user_id,avatar_hash',
|
||
'categories:id,name,slug,content_type_id,parent_id,sort_order',
|
||
])
|
||
->leftJoin('artwork_stats as _ast', '_ast.artwork_id', '=', 'artworks.id')
|
||
->select('artworks.*')
|
||
->where('artworks.published_at', '<=', now()->subDays(7))
|
||
// Blend ranking quality with daily-seeded randomness so spotlight varies
|
||
->orderByRaw("COALESCE(_ast.ranking_score, 0) * 0.6 + RAND({$seed}) * 0.4 DESC")
|
||
->limit($limit * 3)
|
||
->get()
|
||
->sortByDesc(fn ($a) => optional($a->artworkStats)->ranking_score ?? 0)
|
||
->take($limit)
|
||
->values();
|
||
}
|
||
|
||
/**
|
||
* Select curated older artworks for feed blending.
|
||
*/
|
||
private function selectCurated(int $limit, int $olderThanDays): Collection
|
||
{
|
||
$seed = (int) now()->format('Ymd');
|
||
|
||
return Artwork::query()
|
||
->public()
|
||
->published()
|
||
->with([
|
||
'user:id,name,username',
|
||
'user.profile:user_id,avatar_hash',
|
||
'categories:id,name,slug,content_type_id,parent_id,sort_order',
|
||
])
|
||
->leftJoin('artwork_stats as _ast2', '_ast2.artwork_id', '=', 'artworks.id')
|
||
->select('artworks.*')
|
||
->where('artworks.published_at', '<=', now()->subDays($olderThanDays))
|
||
->orderByRaw("COALESCE(_ast2.ranking_score, 0) * 0.7 + RAND({$seed}) * 0.3 DESC")
|
||
->limit($limit)
|
||
->get()
|
||
->values();
|
||
}
|
||
}
|