feat(analytics): hourly/daily rollups, 90-day raw click prune, schedule

main
boban 2026-05-16 01:03:49 +02:00
parent 7d47222591
commit f757f43926
4 changed files with 105 additions and 0 deletions

View File

@ -0,0 +1,22 @@
<?php
namespace App\Domains\Analytics\Jobs;
use App\Domains\Analytics\Models\Click;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
class PruneRawClicksJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
public function handle(): void
{
Click::where('clicked_at', '<', now()->subDays(90))
->chunkById(1000, function ($clicks) {
$clicks->toQuery()->delete();
});
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Domains\Analytics\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\DB;
class RollupDailyJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
public function handle(): void
{
$targetDay = now()->subDay()->startOfDay();
$nextDay = $targetDay->copy()->addDay();
DB::statement("
INSERT INTO clicks_daily (link_id, day, country, device, count, unique_visitors)
SELECT
link_id,
DATE(clicked_at) AS day,
COALESCE(country, '') AS country,
COALESCE(device, 'unknown') AS device,
COUNT(*) AS count,
COUNT(DISTINCT ip_hash) AS unique_visitors
FROM clicks
WHERE clicked_at >= ? AND clicked_at < ?
GROUP BY link_id, day, country, device
ON DUPLICATE KEY UPDATE
count = count + VALUES(count),
unique_visitors = GREATEST(unique_visitors, VALUES(unique_visitors))
", [$targetDay, $nextDay]);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Domains\Analytics\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\DB;
class RollupHourlyJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
public function handle(): void
{
$targetHour = now()->startOfHour()->subHour();
$nextHour = $targetHour->copy()->addHour();
DB::statement("
INSERT INTO clicks_hourly (link_id, hour, country, device, count, unique_visitors)
SELECT
link_id,
DATE_FORMAT(clicked_at, '%Y-%m-%d %H:00:00') AS hour,
COALESCE(country, '') AS country,
COALESCE(device, 'unknown') AS device,
COUNT(*) AS count,
COUNT(DISTINCT ip_hash) AS unique_visitors
FROM clicks
WHERE clicked_at >= ? AND clicked_at < ?
GROUP BY link_id, hour, country, device
ON DUPLICATE KEY UPDATE
count = count + VALUES(count),
unique_visitors = GREATEST(unique_visitors, VALUES(unique_visitors))
", [$targetHour, $nextHour]);
}
}

View File

@ -2,7 +2,16 @@
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
use App\Domains\Analytics\Jobs\RollupHourlyJob;
use App\Domains\Analytics\Jobs\RollupDailyJob;
use App\Domains\Analytics\Jobs\PruneRawClicksJob;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');
Schedule::job(new RollupHourlyJob)->everyFifteenMinutes();
Schedule::job(new RollupDailyJob)->dailyAt('02:00');
Schedule::job(new PruneRawClicksJob)->dailyAt('03:00');
Schedule::command('backup:run')->dailyAt('04:00');