storing analytics data
This commit is contained in:
97
app/Console/Commands/ResetWindowedStatsCommand.php
Normal file
97
app/Console/Commands/ResetWindowedStatsCommand.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* php artisan skinbase:reset-windowed-stats --period=24h|7d
|
||||
*
|
||||
* Resets / recomputes the sliding-window stats columns in artwork_stats:
|
||||
*
|
||||
* views_24h / views_7d
|
||||
* — Zeroed on each reset because we have no per-view event log.
|
||||
* Artworks re-accumulate from the next view event onward.
|
||||
* (Low-traffic reset window: 03:30 means minimal trending disruption.)
|
||||
*
|
||||
* downloads_24h / downloads_7d
|
||||
* — Recomputed accurately from the artwork_downloads event log.
|
||||
* A single bulk UPDATE with a correlated COUNT() is safe here because
|
||||
* it runs once nightly/weekly, not in the hot path.
|
||||
*
|
||||
* Scheduled in routes/console.php:
|
||||
* --period=24h daily at 03:30
|
||||
* --period=7d weekly (Monday) at 03:30
|
||||
*/
|
||||
class ResetWindowedStatsCommand extends Command
|
||||
{
|
||||
protected $signature = 'skinbase:reset-windowed-stats
|
||||
{--period=24h : Window to reset: 24h or 7d}';
|
||||
|
||||
protected $description = 'Reset windowed view/download counters in artwork_stats';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$period = (string) $this->option('period');
|
||||
|
||||
if (! in_array($period, ['24h', '7d'], true)) {
|
||||
$this->error("Invalid period '{$period}'. Use 24h or 7d.");
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
[$viewsCol, $downloadsCol, $cutoff] = match ($period) {
|
||||
'24h' => ['views_24h', 'downloads_24h', now()->subDay()],
|
||||
default => ['views_7d', 'downloads_7d', now()->subDays(7)],
|
||||
};
|
||||
|
||||
$start = microtime(true);
|
||||
|
||||
// ── 1. Zero the views window column ──────────────────────────────────
|
||||
// We have no per-view event log, so we reset the accumulator.
|
||||
$viewsReset = DB::table('artwork_stats')->update([$viewsCol => 0]);
|
||||
|
||||
// ── 2. Recompute downloads window from the event log ─────────────────
|
||||
// artwork_downloads has created_at, so each row's window is accurate.
|
||||
// Chunked PHP loop avoids MySQL-only functions (GREATEST, INTERVAL)
|
||||
// so this command works in both MySQL (production) and SQLite (tests).
|
||||
$downloadsRecomputed = 0;
|
||||
|
||||
DB::table('artwork_stats')
|
||||
->orderBy('artwork_id')
|
||||
->chunk(1000, function ($rows) use ($downloadsCol, $cutoff, &$downloadsRecomputed): void {
|
||||
foreach ($rows as $row) {
|
||||
$count = DB::table('artwork_downloads')
|
||||
->where('artwork_id', $row->artwork_id)
|
||||
->where('created_at', '>=', $cutoff)
|
||||
->count();
|
||||
|
||||
DB::table('artwork_stats')
|
||||
->where('artwork_id', $row->artwork_id)
|
||||
->update([$downloadsCol => max(0, $count)]);
|
||||
|
||||
$downloadsRecomputed++;
|
||||
}
|
||||
});
|
||||
|
||||
$elapsed = round(microtime(true) - $start, 2);
|
||||
|
||||
$this->info("Period: {$period}");
|
||||
$this->info(" {$viewsCol}: zeroed {$viewsReset} rows");
|
||||
$this->info(" {$downloadsCol}: recomputed {$downloadsRecomputed} rows ({$elapsed}s)");
|
||||
|
||||
Log::info('ResetWindowedStats complete', [
|
||||
'period' => $period,
|
||||
'views_col' => $viewsCol,
|
||||
'views_rows_reset' => $viewsReset,
|
||||
'downloads_col' => $downloadsCol,
|
||||
'downloads_recomputed' => $downloadsRecomputed,
|
||||
'elapsed_s' => $elapsed,
|
||||
]);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user