Files
SkinbaseNova/app/Services/NovaCards/NovaCardTrendingService.php
2026-03-28 19:15:39 +01:00

90 lines
3.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\NovaCards;
use App\Models\NovaCard;
use App\Models\NovaCardChallengeEntry;
use App\Models\NovaCardComment;
use App\Models\NovaCardCollectionItem;
use App\Models\NovaCardReaction;
use Carbon\Carbon;
use Carbon\CarbonInterface;
class NovaCardTrendingService
{
public function refreshCard(NovaCard $card): NovaCard
{
$likes = NovaCardReaction::query()->where('card_id', $card->id)->where('type', NovaCardReaction::TYPE_LIKE)->count();
$favorites = NovaCardReaction::query()->where('card_id', $card->id)->where('type', NovaCardReaction::TYPE_FAVORITE)->count();
$saves = NovaCardCollectionItem::query()->where('card_id', $card->id)->count();
$remixes = NovaCard::query()->where('original_card_id', $card->id)->count();
$comments = NovaCardComment::query()->where('card_id', $card->id)->where('status', 'visible')->count();
$challengeEntries = NovaCardChallengeEntry::query()->where('card_id', $card->id)->count();
$lastEngagedAt = $this->lastEngagedAt($card);
$card->forceFill([
'likes_count' => $likes,
'favorites_count' => $favorites,
'saves_count' => $saves,
'remixes_count' => $remixes,
'comments_count' => $comments,
'challenge_entries_count' => $challengeEntries,
'last_engaged_at' => $lastEngagedAt,
'trending_score' => $this->score($card, $likes, $favorites, $saves, $remixes, $comments, $challengeEntries, $lastEngagedAt),
])->save();
return $card->refresh();
}
public function rebuildAll(): void
{
NovaCard::query()->select('id')->orderBy('id')->chunkById(100, function ($cards): void {
foreach ($cards as $card) {
$this->refreshCard(NovaCard::query()->findOrFail($card->id));
}
});
}
private function score(NovaCard $card, int $likes, int $favorites, int $saves, int $remixes, int $comments, int $challengeEntries, ?CarbonInterface $lastEngagedAt): float
{
$base = ($likes * 4.0)
+ ($favorites * 2.5)
+ ($saves * 5.0)
+ ($remixes * 6.0)
+ ($comments * 2.0)
+ ($challengeEntries * 4.0)
+ ($card->shares_count * 3.0)
+ ($card->downloads_count * 2.0)
+ ($card->views_count * 0.25);
$engagedAt = $lastEngagedAt ?? $card->published_at ?? now();
$ageHours = max(1.0, (float) $engagedAt->diffInHours(now()));
$decay = max(0.2, 1 / (1 + ($ageHours / 72)));
return round($base * $decay, 4);
}
private function lastEngagedAt(NovaCard $card): ?CarbonInterface
{
$timestamps = array_filter([
NovaCardReaction::query()->where('card_id', $card->id)->max('created_at'),
NovaCardCollectionItem::query()->where('card_id', $card->id)->max('created_at'),
NovaCard::query()->where('original_card_id', $card->id)->max('created_at'),
NovaCardComment::query()->where('card_id', $card->id)->max('created_at'),
NovaCardChallengeEntry::query()->where('card_id', $card->id)->max('created_at'),
$card->updated_at?->toDateTimeString(),
$card->published_at?->toDateTimeString(),
]);
if ($timestamps === []) {
return null;
}
return collect($timestamps)
->map(fn ($timestamp) => Carbon::parse($timestamp))
->sortDesc()
->first();
}
}