82 lines
2.8 KiB
PHP
82 lines
2.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\NovaCards;
|
|
|
|
use App\Models\NovaCard;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* Surfaces "rising" cards — recently published cards that are gaining
|
|
* engagement faster than average, weighted to balance novelty creators.
|
|
*/
|
|
class NovaCardRisingService
|
|
{
|
|
/** Number of hours a card is eligible for "rising" feed. */
|
|
private const WINDOW_HOURS = 96;
|
|
|
|
/** Cache TTL in seconds. */
|
|
private const CACHE_TTL = 300;
|
|
|
|
public function risingCards(int $limit = 18, bool $cached = true): Collection
|
|
{
|
|
if ($cached) {
|
|
return Cache::remember(
|
|
'nova_cards.rising.' . $limit,
|
|
self::CACHE_TTL,
|
|
fn () => $this->queryRising($limit),
|
|
);
|
|
}
|
|
|
|
return $this->queryRising($limit);
|
|
}
|
|
|
|
public function invalidateCache(): void
|
|
{
|
|
foreach ([6, 18, 24, 36] as $limit) {
|
|
Cache::forget('nova_cards.rising.' . $limit);
|
|
}
|
|
}
|
|
|
|
private function queryRising(int $limit): Collection
|
|
{
|
|
$cutoff = Carbon::now()->subHours(self::WINDOW_HOURS);
|
|
$isSqlite = DB::connection()->getDriverName() === 'sqlite';
|
|
$ageHoursExpression = $isSqlite
|
|
? "CASE WHEN ((julianday('now') - julianday(published_at)) * 24.0) < 1 THEN 1 ELSE ((julianday('now') - julianday(published_at)) * 24.0) END"
|
|
: 'GREATEST(1, TIMESTAMPDIFF(HOUR, published_at, NOW()))';
|
|
$decayExpression = $isSqlite
|
|
? $ageHoursExpression
|
|
: 'POWER(' . $ageHoursExpression . ', 0.7)';
|
|
$risingMomentumExpression = '(
|
|
(saves_count * 5.0 + remixes_count * 6.0 + likes_count * 4.0 + favorites_count * 2.5 + comments_count * 2.0 + challenge_entries_count * 4.0)
|
|
/ ' . $decayExpression . '
|
|
) AS rising_momentum';
|
|
|
|
return NovaCard::query()
|
|
->publiclyVisible()
|
|
->where('published_at', '>=', $cutoff)
|
|
// Must have at least one meaningful engagement signal.
|
|
->where(function (Builder $q): void {
|
|
$q->where('saves_count', '>', 0)
|
|
->orWhere('remixes_count', '>', 0)
|
|
->orWhere('likes_count', '>', 1);
|
|
})
|
|
->select([
|
|
'nova_cards.*',
|
|
// Rising score: weight recent engagement, penalise by sqrt(age hours) to let novelty show
|
|
DB::raw($risingMomentumExpression),
|
|
])
|
|
->orderByDesc('rising_momentum')
|
|
->orderByDesc('published_at')
|
|
->limit($limit)
|
|
->with(['user.profile', 'category', 'template', 'backgroundImage', 'tags'])
|
|
->get();
|
|
}
|
|
}
|