Commit workspace changes
This commit is contained in:
@@ -6,6 +6,7 @@ namespace App\Services;
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ArtworkMetricSnapshotHourly;
|
||||
use App\Models\Group;
|
||||
use App\Models\Leaderboard;
|
||||
use App\Models\Story;
|
||||
use App\Models\StoryLike;
|
||||
@@ -22,6 +23,11 @@ class LeaderboardService
|
||||
private const CREATOR_STORE_LIMIT = 10000;
|
||||
private const ENTITY_STORE_LIMIT = 500;
|
||||
|
||||
public function __construct(
|
||||
private readonly GroupReputationService $groupReputation,
|
||||
) {
|
||||
}
|
||||
|
||||
public function calculateCreatorLeaderboard(string $period): int
|
||||
{
|
||||
$normalizedPeriod = $this->normalizePeriod($period);
|
||||
@@ -52,6 +58,16 @@ class LeaderboardService
|
||||
return $this->persistRows(Leaderboard::TYPE_STORY, $normalizedPeriod, $rows, self::ENTITY_STORE_LIMIT);
|
||||
}
|
||||
|
||||
public function calculateGroupLeaderboard(string $period): int
|
||||
{
|
||||
$normalizedPeriod = $this->normalizePeriod($period);
|
||||
$rows = $normalizedPeriod === Leaderboard::PERIOD_ALL_TIME
|
||||
? $this->allTimeGroupRows()
|
||||
: $this->windowedGroupRows($this->periodStart($normalizedPeriod));
|
||||
|
||||
return $this->persistRows(Leaderboard::TYPE_GROUP, $normalizedPeriod, $rows, self::ENTITY_STORE_LIMIT);
|
||||
}
|
||||
|
||||
public function refreshAll(): array
|
||||
{
|
||||
$results = [];
|
||||
@@ -59,12 +75,14 @@ class LeaderboardService
|
||||
foreach ([
|
||||
Leaderboard::TYPE_CREATOR,
|
||||
Leaderboard::TYPE_ARTWORK,
|
||||
Leaderboard::TYPE_GROUP,
|
||||
Leaderboard::TYPE_STORY,
|
||||
] as $type) {
|
||||
foreach ($this->periods() as $period) {
|
||||
$results[$type][$period] = match ($type) {
|
||||
Leaderboard::TYPE_CREATOR => $this->calculateCreatorLeaderboard($period),
|
||||
Leaderboard::TYPE_ARTWORK => $this->calculateArtworkLeaderboard($period),
|
||||
Leaderboard::TYPE_GROUP => $this->calculateGroupLeaderboard($period),
|
||||
Leaderboard::TYPE_STORY => $this->calculateStoryLeaderboard($period),
|
||||
};
|
||||
}
|
||||
@@ -83,14 +101,12 @@ class LeaderboardService
|
||||
$this->cacheKey($normalizedType, $normalizedPeriod, $limit),
|
||||
self::CACHE_TTL_SECONDS,
|
||||
function () use ($normalizedType, $normalizedPeriod, $limit): array {
|
||||
$items = Leaderboard::query()
|
||||
->where('type', $normalizedType)
|
||||
->where('period', $normalizedPeriod)
|
||||
->orderByDesc('score')
|
||||
->orderBy('entity_id')
|
||||
->limit($limit)
|
||||
->get(['entity_id', 'score'])
|
||||
->values();
|
||||
$items = $this->leaderboardRows($normalizedType, $normalizedPeriod, $limit);
|
||||
|
||||
if ($items->isEmpty()) {
|
||||
$this->generateLeaderboard($normalizedType, $normalizedPeriod);
|
||||
$items = $this->leaderboardRows($normalizedType, $normalizedPeriod, $limit);
|
||||
}
|
||||
|
||||
if ($items->isEmpty()) {
|
||||
return [
|
||||
@@ -103,6 +119,7 @@ class LeaderboardService
|
||||
$entities = match ($normalizedType) {
|
||||
Leaderboard::TYPE_CREATOR => $this->creatorEntities($items->pluck('entity_id')->all()),
|
||||
Leaderboard::TYPE_ARTWORK => $this->artworkEntities($items->pluck('entity_id')->all()),
|
||||
Leaderboard::TYPE_GROUP => $this->groupEntities($items->pluck('entity_id')->all()),
|
||||
Leaderboard::TYPE_STORY => $this->storyEntities($items->pluck('entity_id')->all()),
|
||||
};
|
||||
|
||||
@@ -186,11 +203,34 @@ class LeaderboardService
|
||||
return match (strtolower(trim($type))) {
|
||||
'creator', 'creators' => Leaderboard::TYPE_CREATOR,
|
||||
'artwork', 'artworks' => Leaderboard::TYPE_ARTWORK,
|
||||
'group', 'groups' => Leaderboard::TYPE_GROUP,
|
||||
'story', 'stories' => Leaderboard::TYPE_STORY,
|
||||
default => Leaderboard::TYPE_CREATOR,
|
||||
};
|
||||
}
|
||||
|
||||
private function leaderboardRows(string $type, string $period, int $limit): Collection
|
||||
{
|
||||
return Leaderboard::query()
|
||||
->where('type', $type)
|
||||
->where('period', $period)
|
||||
->orderByDesc('score')
|
||||
->orderBy('entity_id')
|
||||
->limit($limit)
|
||||
->get(['entity_id', 'score'])
|
||||
->values();
|
||||
}
|
||||
|
||||
private function generateLeaderboard(string $type, string $period): void
|
||||
{
|
||||
match ($type) {
|
||||
Leaderboard::TYPE_CREATOR => $this->calculateCreatorLeaderboard($period),
|
||||
Leaderboard::TYPE_ARTWORK => $this->calculateArtworkLeaderboard($period),
|
||||
Leaderboard::TYPE_GROUP => $this->calculateGroupLeaderboard($period),
|
||||
Leaderboard::TYPE_STORY => $this->calculateStoryLeaderboard($period),
|
||||
};
|
||||
}
|
||||
|
||||
private function periodStart(string $period): CarbonImmutable
|
||||
{
|
||||
$now = CarbonImmutable::now();
|
||||
@@ -465,6 +505,194 @@ class LeaderboardService
|
||||
->values();
|
||||
}
|
||||
|
||||
private function allTimeGroupRows(): Collection
|
||||
{
|
||||
$members = DB::table('group_members')
|
||||
->select('group_id', DB::raw('COUNT(*) as members_count'))
|
||||
->where('status', Group::STATUS_ACTIVE)
|
||||
->groupBy('group_id');
|
||||
|
||||
$releases = DB::table('group_releases')
|
||||
->select('group_id', DB::raw('COUNT(*) as releases_count'))
|
||||
->where('visibility', 'public')
|
||||
->where('status', 'released')
|
||||
->groupBy('group_id');
|
||||
|
||||
$projects = DB::table('group_projects')
|
||||
->select('group_id', DB::raw('COUNT(*) as projects_count'))
|
||||
->where('visibility', 'public')
|
||||
->whereIn('status', ['active', 'review', 'released'])
|
||||
->groupBy('group_id');
|
||||
|
||||
$challenges = DB::table('group_challenges')
|
||||
->select('group_id', DB::raw('COUNT(*) as challenges_count'))
|
||||
->where('visibility', 'public')
|
||||
->whereIn('status', ['published', 'active'])
|
||||
->groupBy('group_id');
|
||||
|
||||
$events = DB::table('group_events')
|
||||
->select('group_id', DB::raw('COUNT(*) as events_count'))
|
||||
->where('visibility', 'public')
|
||||
->where('status', 'published')
|
||||
->groupBy('group_id');
|
||||
|
||||
$activity = DB::table('group_activity_items')
|
||||
->select('group_id', DB::raw('COUNT(*) as activity_count'))
|
||||
->where('visibility', 'public')
|
||||
->groupBy('group_id');
|
||||
|
||||
return Group::query()
|
||||
->from('groups')
|
||||
->leftJoinSub($members, 'members', 'members.group_id', '=', 'groups.id')
|
||||
->leftJoinSub($releases, 'releases', 'releases.group_id', '=', 'groups.id')
|
||||
->leftJoinSub($projects, 'projects', 'projects.group_id', '=', 'groups.id')
|
||||
->leftJoinSub($challenges, 'challenges', 'challenges.group_id', '=', 'groups.id')
|
||||
->leftJoinSub($events, 'events', 'events.group_id', '=', 'groups.id')
|
||||
->leftJoinSub($activity, 'activity', 'activity.group_id', '=', 'groups.id')
|
||||
->public()
|
||||
->select([
|
||||
'groups.id',
|
||||
'groups.followers_count',
|
||||
'groups.artworks_count',
|
||||
'groups.collections_count',
|
||||
'groups.is_verified',
|
||||
DB::raw('COALESCE(members.members_count, 0) as members_count'),
|
||||
DB::raw('COALESCE(releases.releases_count, 0) as releases_count'),
|
||||
DB::raw('COALESCE(projects.projects_count, 0) as projects_count'),
|
||||
DB::raw('COALESCE(challenges.challenges_count, 0) as challenges_count'),
|
||||
DB::raw('COALESCE(events.events_count, 0) as events_count'),
|
||||
DB::raw('COALESCE(activity.activity_count, 0) as activity_count'),
|
||||
])
|
||||
->get()
|
||||
->map(function ($row): array {
|
||||
$score = ((int) $row->followers_count * 8)
|
||||
+ ((int) $row->artworks_count * 10)
|
||||
+ ((int) $row->collections_count * 6)
|
||||
+ ((int) $row->members_count * 20)
|
||||
+ ((int) $row->releases_count * 30)
|
||||
+ ((int) $row->projects_count * 24)
|
||||
+ ((int) $row->challenges_count * 18)
|
||||
+ ((int) $row->events_count * 14)
|
||||
+ ((int) $row->activity_count * 4)
|
||||
+ ((bool) $row->is_verified ? 120 : 0);
|
||||
|
||||
return [
|
||||
'entity_id' => (int) $row->id,
|
||||
'score' => $score,
|
||||
];
|
||||
})
|
||||
->filter(fn (array $row): bool => $row['score'] > 0)
|
||||
->values();
|
||||
}
|
||||
|
||||
private function windowedGroupRows(CarbonImmutable $start): Collection
|
||||
{
|
||||
$follows = DB::table('group_follows')
|
||||
->select('group_id', DB::raw('COUNT(*) as follows_count'))
|
||||
->where('created_at', '>=', $start)
|
||||
->groupBy('group_id');
|
||||
|
||||
$artworks = DB::table('artworks')
|
||||
->select('group_id', DB::raw('COUNT(*) as artworks_count'))
|
||||
->whereNotNull('group_id')
|
||||
->where('is_public', true)
|
||||
->where('is_approved', true)
|
||||
->whereNull('deleted_at')
|
||||
->whereNotNull('published_at')
|
||||
->where('published_at', '>=', $start)
|
||||
->groupBy('group_id');
|
||||
|
||||
$releases = DB::table('group_releases')
|
||||
->select('group_id', DB::raw('COUNT(*) as releases_count'))
|
||||
->where('visibility', 'public')
|
||||
->where('status', 'released')
|
||||
->where('released_at', '>=', $start)
|
||||
->groupBy('group_id');
|
||||
|
||||
$projects = DB::table('group_projects')
|
||||
->select('group_id', DB::raw('COUNT(*) as projects_count'))
|
||||
->where('visibility', 'public')
|
||||
->whereIn('status', ['active', 'review', 'released'])
|
||||
->where('updated_at', '>=', $start)
|
||||
->groupBy('group_id');
|
||||
|
||||
$challenges = DB::table('group_challenges')
|
||||
->select('group_id', DB::raw('COUNT(*) as challenges_count'))
|
||||
->where('visibility', 'public')
|
||||
->whereIn('status', ['published', 'active'])
|
||||
->where(function ($query) use ($start): void {
|
||||
$query->where('updated_at', '>=', $start)
|
||||
->orWhere('start_at', '>=', $start)
|
||||
->orWhere('created_at', '>=', $start);
|
||||
})
|
||||
->groupBy('group_id');
|
||||
|
||||
$events = DB::table('group_events')
|
||||
->select('group_id', DB::raw('COUNT(*) as events_count'))
|
||||
->where('visibility', 'public')
|
||||
->where('status', 'published')
|
||||
->where(function ($query) use ($start): void {
|
||||
$query->where('published_at', '>=', $start)
|
||||
->orWhere('start_at', '>=', $start)
|
||||
->orWhere('updated_at', '>=', $start);
|
||||
})
|
||||
->groupBy('group_id');
|
||||
|
||||
$activity = DB::table('group_activity_items')
|
||||
->select('group_id', DB::raw('COUNT(*) as activity_count'))
|
||||
->where('visibility', 'public')
|
||||
->where('occurred_at', '>=', $start)
|
||||
->groupBy('group_id');
|
||||
|
||||
$members = DB::table('group_members')
|
||||
->select('group_id', DB::raw('COUNT(*) as members_count'))
|
||||
->where('status', Group::STATUS_ACTIVE)
|
||||
->groupBy('group_id');
|
||||
|
||||
return Group::query()
|
||||
->from('groups')
|
||||
->leftJoinSub($follows, 'follows', 'follows.group_id', '=', 'groups.id')
|
||||
->leftJoinSub($artworks, 'artworks', 'artworks.group_id', '=', 'groups.id')
|
||||
->leftJoinSub($releases, 'releases', 'releases.group_id', '=', 'groups.id')
|
||||
->leftJoinSub($projects, 'projects', 'projects.group_id', '=', 'groups.id')
|
||||
->leftJoinSub($challenges, 'challenges', 'challenges.group_id', '=', 'groups.id')
|
||||
->leftJoinSub($events, 'events', 'events.group_id', '=', 'groups.id')
|
||||
->leftJoinSub($activity, 'activity', 'activity.group_id', '=', 'groups.id')
|
||||
->leftJoinSub($members, 'members', 'members.group_id', '=', 'groups.id')
|
||||
->public()
|
||||
->select([
|
||||
'groups.id',
|
||||
'groups.is_verified',
|
||||
DB::raw('COALESCE(follows.follows_count, 0) as follows_count'),
|
||||
DB::raw('COALESCE(artworks.artworks_count, 0) as artworks_count'),
|
||||
DB::raw('COALESCE(releases.releases_count, 0) as releases_count'),
|
||||
DB::raw('COALESCE(projects.projects_count, 0) as projects_count'),
|
||||
DB::raw('COALESCE(challenges.challenges_count, 0) as challenges_count'),
|
||||
DB::raw('COALESCE(events.events_count, 0) as events_count'),
|
||||
DB::raw('COALESCE(activity.activity_count, 0) as activity_count'),
|
||||
DB::raw('COALESCE(members.members_count, 0) as members_count'),
|
||||
])
|
||||
->get()
|
||||
->map(function ($row): array {
|
||||
$score = ((int) $row->follows_count * 18)
|
||||
+ ((int) $row->artworks_count * 16)
|
||||
+ ((int) $row->releases_count * 34)
|
||||
+ ((int) $row->projects_count * 22)
|
||||
+ ((int) $row->challenges_count * 20)
|
||||
+ ((int) $row->events_count * 16)
|
||||
+ ((int) $row->activity_count * 6)
|
||||
+ ((int) $row->members_count * 8)
|
||||
+ ((bool) $row->is_verified ? 45 : 0);
|
||||
|
||||
return [
|
||||
'entity_id' => (int) $row->id,
|
||||
'score' => $score,
|
||||
];
|
||||
})
|
||||
->filter(fn (array $row): bool => $row['score'] > 0)
|
||||
->values();
|
||||
}
|
||||
|
||||
private function artworkSnapshotDeltas(CarbonImmutable $start): \Illuminate\Database\Query\Builder
|
||||
{
|
||||
return ArtworkMetricSnapshotHourly::query()
|
||||
@@ -472,15 +700,26 @@ class LeaderboardService
|
||||
->where('snapshots.bucket_hour', '>=', $start)
|
||||
->select([
|
||||
'snapshots.artwork_id',
|
||||
DB::raw('GREATEST(MAX(snapshots.views_count) - MIN(snapshots.views_count), 0) as views_delta'),
|
||||
DB::raw('GREATEST(MAX(snapshots.downloads_count) - MIN(snapshots.downloads_count), 0) as downloads_delta'),
|
||||
DB::raw('GREATEST(MAX(snapshots.favourites_count) - MIN(snapshots.favourites_count), 0) as favourites_delta'),
|
||||
DB::raw('GREATEST(MAX(snapshots.comments_count) - MIN(snapshots.comments_count), 0) as comments_delta'),
|
||||
DB::raw($this->nonNegativeSnapshotDelta('views_count', 'views_delta')),
|
||||
DB::raw($this->nonNegativeSnapshotDelta('downloads_count', 'downloads_delta')),
|
||||
DB::raw($this->nonNegativeSnapshotDelta('favourites_count', 'favourites_delta')),
|
||||
DB::raw($this->nonNegativeSnapshotDelta('comments_count', 'comments_delta')),
|
||||
])
|
||||
->groupBy('snapshots.artwork_id')
|
||||
->toBase();
|
||||
}
|
||||
|
||||
private function nonNegativeSnapshotDelta(string $column, string $alias): string
|
||||
{
|
||||
$delta = sprintf('MAX(snapshots.%1$s) - MIN(snapshots.%1$s)', $column);
|
||||
|
||||
if (DB::connection()->getDriverName() === 'sqlite') {
|
||||
return sprintf('CASE WHEN %1$s > 0 THEN %1$s ELSE 0 END as %2$s', $delta, $alias);
|
||||
}
|
||||
|
||||
return sprintf('GREATEST(%s, 0) as %s', $delta, $alias);
|
||||
}
|
||||
|
||||
private function creatorEntities(array $ids): array
|
||||
{
|
||||
return User::query()
|
||||
@@ -550,4 +789,34 @@ class LeaderboardService
|
||||
])
|
||||
->all();
|
||||
}
|
||||
|
||||
private function groupEntities(array $ids): array
|
||||
{
|
||||
return Group::query()
|
||||
->with(['owner.profile', 'recruitmentProfile', 'badges', 'members'])
|
||||
->whereIn('id', $ids)
|
||||
->public()
|
||||
->get()
|
||||
->mapWithKeys(function (Group $group): array {
|
||||
return [
|
||||
(int) $group->id => [
|
||||
'id' => (int) $group->id,
|
||||
'type' => Leaderboard::TYPE_GROUP,
|
||||
'name' => (string) $group->name,
|
||||
'headline' => (string) ($group->headline ?? ''),
|
||||
'url' => $group->publicUrl(),
|
||||
'avatar' => $group->avatarUrl(),
|
||||
'image' => $group->bannerUrl() ?: $group->avatarUrl(),
|
||||
'followers_count' => (int) ($group->followers_count ?? 0),
|
||||
'artworks_count' => (int) ($group->artworks_count ?? 0),
|
||||
'collections_count' => (int) ($group->collections_count ?? 0),
|
||||
'members_count' => (int) $group->members->where('status', Group::STATUS_ACTIVE)->count(),
|
||||
'is_recruiting' => (bool) ($group->recruitmentProfile?->is_recruiting ?? false),
|
||||
'trust_signals' => $this->groupReputation->trustSignals($group),
|
||||
'badges' => $this->groupReputation->groupBadges($group, 3),
|
||||
],
|
||||
];
|
||||
})
|
||||
->all();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user