Keep similar-ai from tripping the global circuit on a lone URL 502, clamp Qdrant search to 100, and add Server-Timing plus slow-request logging. Studio shared props, Academy S3 exists caching, heat chunking, and Redis/scheduler hygiene stay in this rollout.
83 lines
2.9 KiB
PHP
83 lines
2.9 KiB
PHP
<?php
|
|
|
|
use App\Models\Artwork;
|
|
use App\Models\ArtworkMetricSnapshotHourly;
|
|
use App\Models\Leaderboard;
|
|
use App\Services\LeaderboardService;
|
|
|
|
function leaderboardArtwork(array $attrs = []): Artwork
|
|
{
|
|
return Artwork::withoutEvents(fn () => Artwork::factory()->create(array_merge([
|
|
'is_approved' => true,
|
|
'is_public' => true,
|
|
'published_at' => now()->subDays(40),
|
|
'deleted_at' => null,
|
|
], $attrs)));
|
|
}
|
|
|
|
function leaderboardSnapshot(int $artworkId, $hour, int $views): void
|
|
{
|
|
ArtworkMetricSnapshotHourly::create([
|
|
'artwork_id' => $artworkId,
|
|
'bucket_hour' => $hour,
|
|
'views_count' => $views,
|
|
'downloads_count' => 0,
|
|
'favourites_count' => 0,
|
|
'comments_count' => 0,
|
|
'shares_count' => 0,
|
|
]);
|
|
}
|
|
|
|
it('scores monthly artwork leaderboards from period view deltas not cumulative sums', function () {
|
|
$old = leaderboardArtwork();
|
|
$bornInMonth = leaderboardArtwork([
|
|
'published_at' => now()->subDays(4),
|
|
'created_at' => now()->subDays(4),
|
|
]);
|
|
|
|
leaderboardSnapshot($old->id, now()->subDays(20)->startOfHour(), 100);
|
|
leaderboardSnapshot($old->id, now()->subHour()->startOfHour(), 140);
|
|
leaderboardSnapshot($bornInMonth->id, now()->subDays(3)->startOfHour(), 25);
|
|
leaderboardSnapshot($bornInMonth->id, now()->subHour()->startOfHour(), 55);
|
|
|
|
app(LeaderboardService::class)->calculateArtworkLeaderboard(Leaderboard::PERIOD_MONTHLY);
|
|
|
|
$rows = Leaderboard::query()
|
|
->where('type', Leaderboard::TYPE_ARTWORK)
|
|
->where('period', Leaderboard::PERIOD_MONTHLY)
|
|
->get()
|
|
->keyBy('entity_id');
|
|
|
|
expect((int) $rows[$old->id]->score)->toBe(40)
|
|
->and((int) $rows[$bornInMonth->id]->score)->toBe(55)
|
|
->and((int) $rows[$old->id]->score)->not->toBe(240);
|
|
});
|
|
|
|
it('scores daily and weekly artwork leaderboards from the same delta rules', function () {
|
|
$artwork = leaderboardArtwork();
|
|
|
|
leaderboardSnapshot($artwork->id, now()->subDays(7)->subHour()->startOfHour(), 10);
|
|
leaderboardSnapshot($artwork->id, now()->subDays(3)->startOfHour(), 18);
|
|
leaderboardSnapshot($artwork->id, now()->subDay()->subHour()->startOfHour(), 18);
|
|
leaderboardSnapshot($artwork->id, now()->subHours(2)->startOfHour(), 21);
|
|
|
|
$service = app(LeaderboardService::class);
|
|
$service->calculateArtworkLeaderboard(Leaderboard::PERIOD_WEEKLY);
|
|
$service->calculateArtworkLeaderboard(Leaderboard::PERIOD_DAILY);
|
|
|
|
$weekly = Leaderboard::query()
|
|
->where('type', Leaderboard::TYPE_ARTWORK)
|
|
->where('period', Leaderboard::PERIOD_WEEKLY)
|
|
->where('entity_id', $artwork->id)
|
|
->value('score');
|
|
|
|
$daily = Leaderboard::query()
|
|
->where('type', Leaderboard::TYPE_ARTWORK)
|
|
->where('period', Leaderboard::PERIOD_DAILY)
|
|
->where('entity_id', $artwork->id)
|
|
->value('score');
|
|
|
|
expect((int) $weekly)->toBe(11)
|
|
->and((int) $daily)->toBe(3);
|
|
});
|