98 lines
3.7 KiB
PHP
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;
|
|
}
|
|
}
|