Files
SkinbaseNova/app/Console/Commands/MetricsSnapshotHourlyCommand.php

114 lines
4.2 KiB
PHP

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* Collect hourly metric snapshots for artworks.
*
* Runs on cron every hour. Inserts a row per artwork into
* artwork_metric_snapshots_hourly with the current totals.
* Deltas are computed by the heat recalculation command.
*
* Usage: php artisan nova:metrics-snapshot-hourly
* php artisan nova:metrics-snapshot-hourly --days=30 --chunk=500 --dry-run
*/
class MetricsSnapshotHourlyCommand extends Command
{
protected $signature = 'nova:metrics-snapshot-hourly
{--days=60 : Only snapshot artworks created within this many days}
{--chunk=1000 : Chunk size for DB queries}
{--dry-run : Log what would be written without persisting}';
protected $description = 'Collect hourly metric snapshots for rising/heat calculation';
public function handle(): int
{
$days = (int) $this->option('days');
$chunk = (int) $this->option('chunk');
$dryRun = (bool) $this->option('dry-run');
$bucketHour = now()->startOfHour();
$this->info("[nova:metrics-snapshot-hourly] bucket={$bucketHour->toDateTimeString()} days={$days} chunk={$chunk}" . ($dryRun ? ' (dry-run)' : ''));
$snapshotCount = 0;
$skipCount = 0;
// Query artworks eligible for snapshotting:
// - created within $days OR has a ranking_score above 0
// First collect eligible IDs, then process in chunks
$eligibleIds = DB::table('artworks')
->leftJoin('artwork_stats as s', 's.artwork_id', '=', 'artworks.id')
->where(function ($q) use ($days) {
$q->where('artworks.created_at', '>=', now()->subDays($days))
->orWhere(function ($q2) {
$q2->whereNotNull('s.ranking_score')
->where('s.ranking_score', '>', 0);
});
})
->whereNull('artworks.deleted_at')
->where('artworks.is_approved', true)
->pluck('artworks.id');
if ($eligibleIds->isEmpty()) {
$this->info('No eligible artworks found.');
return self::SUCCESS;
}
foreach ($eligibleIds->chunk($chunk) as $chunkIds) {
$artworkIds = $chunkIds->values()->all();
$stats = DB::table('artwork_stats')
->whereIn('artwork_id', $artworkIds)
->get()
->keyBy('artwork_id');
$rows = [];
foreach ($artworkIds as $artworkId) {
$stat = $stats->get($artworkId);
$rows[] = [
'artwork_id' => $artworkId,
'bucket_hour' => $bucketHour,
'views_count' => (int) ($stat?->views ?? 0),
'downloads_count' => (int) ($stat?->downloads ?? 0),
'favourites_count' => (int) ($stat?->favorites ?? 0),
'comments_count' => (int) ($stat?->comments_count ?? 0),
'shares_count' => (int) ($stat?->shares_count ?? 0),
'created_at' => now(),
];
}
if ($dryRun) {
$snapshotCount += count($rows);
continue;
}
if (!empty($rows)) {
// Upsert: if (artwork_id, bucket_hour) already exists, update totals
DB::table('artwork_metric_snapshots_hourly')->upsert(
$rows,
['artwork_id', 'bucket_hour'],
['views_count', 'downloads_count', 'favourites_count', 'comments_count', 'shares_count']
);
$snapshotCount += count($rows);
}
}
$this->info("Snapshots written: {$snapshotCount} | Skipped: {$skipCount}");
Log::info('[nova:metrics-snapshot-hourly] completed', [
'bucket' => $bucketHour->toDateTimeString(),
'written' => $snapshotCount,
'skipped' => $skipCount,
'dry_run' => $dryRun,
]);
return self::SUCCESS;
}
}