Files
SkinbaseNova/app/Console/Commands/ResetWindowedStatsCommand.php
2026-02-27 09:46:51 +01:00

98 lines
3.7 KiB
PHP

<?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;
}
}