114 lines
4.2 KiB
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;
|
|
}
|
|
}
|