Save workspace changes
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\EarlyGrowth;
|
||||
|
||||
use App\Models\Artwork;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* 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');
|
||||
$randomExpr = $this->dailyRandomExpression('artworks.id', $seed);
|
||||
|
||||
// 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 + {$randomExpr} * 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');
|
||||
$randomExpr = $this->dailyRandomExpression('artworks.id', $seed);
|
||||
|
||||
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 + {$randomExpr} * 0.3 DESC")
|
||||
->limit($limit)
|
||||
->get()
|
||||
->values();
|
||||
}
|
||||
|
||||
private function dailyRandomExpression(string $idColumn, int $seed): string
|
||||
{
|
||||
if (DB::connection()->getDriverName() === 'sqlite') {
|
||||
return "(ABS((({$idColumn} * 1103515245) + {$seed}) % 2147483647) / 2147483647.0)";
|
||||
}
|
||||
|
||||
return "RAND({$seed} + {$idColumn})";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user