Save workspace changes

This commit is contained in:
2026-04-18 17:02:56 +02:00
parent f02ea9a711
commit 87d60af5a9
4220 changed files with 1388603 additions and 1554 deletions

View File

@@ -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})";
}
}