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

289 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\ArtworkAward;
use App\Models\ArtworkAwardStat;
use App\Services\ArtworkAwardService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* Migrates legacy `users_opinions` (projekti_old_skinbase) into `artwork_awards`.
*
* Score mapping (legacy score → new medal):
* 4 → gold (weight 3)
* 3 → silver (weight 2)
* 2 → bronze (weight 1)
* 1 → skipped (too low to map meaningfully)
*
* Usage:
* php artisan awards:import-legacy
* php artisan awards:import-legacy --dry-run
* php artisan awards:import-legacy --chunk=500
* php artisan awards:import-legacy --skip-stats (skip final stats recalc)
*/
class ImportLegacyAwards extends Command
{
protected $signature = 'awards:import-legacy
{--dry-run : Preview only — no writes to DB}
{--chunk=250 : Rows to process per batch}
{--skip-stats : Skip per-artwork stats recalculation at the end}
{--force : Overwrite existing awards instead of skipping duplicates}';
protected $description = 'Import legacy users_opinions into artwork_awards';
/** Maps legacy score value → medal string */
private const SCORE_MAP = [
4 => 'gold',
3 => 'silver',
2 => 'bronze',
];
public function handle(ArtworkAwardService $service): int
{
$dryRun = (bool) $this->option('dry-run');
$chunk = max(1, (int) $this->option('chunk'));
$skipStats = (bool) $this->option('skip-stats');
$force = (bool) $this->option('force');
if ($dryRun) {
$this->warn('[DRY-RUN] No data will be written.');
}
// Verify legacy connection is reachable
try {
DB::connection('legacy')->getPdo();
} catch (\Throwable $e) {
$this->error('Cannot connect to legacy database: ' . $e->getMessage());
return self::FAILURE;
}
if (! DB::connection('legacy')->getSchemaBuilder()->hasTable('users_opinions')) {
$this->error('Legacy table `users_opinions` not found.');
return self::FAILURE;
}
// Pre-load sets of valid artwork IDs and user IDs from the new DB
$this->info('Loading new-DB artwork and user ID sets…');
$validArtworkIds = DB::table('artworks')
->whereNull('deleted_at')
->pluck('id')
->flip() // flip so we can use isset() for O(1) lookup
->all();
$validUserIds = DB::table('users')
->whereNull('deleted_at')
->pluck('id')
->flip()
->all();
$this->info(sprintf(
'Found %d artworks and %d users in new DB.',
count($validArtworkIds),
count($validUserIds)
));
// Count legacy rows for progress bar
$total = DB::connection('legacy')
->table('users_opinions')
->count();
$this->info("Legacy rows to process: {$total}");
if ($total === 0) {
$this->warn('No legacy rows found. Nothing to do.');
return self::SUCCESS;
}
$stats = [
'imported' => 0,
'skipped_score' => 0,
'skipped_artwork' => 0,
'skipped_user' => 0,
'skipped_duplicate'=> 0,
'updated_force' => 0,
'errors' => 0,
];
$affectedArtworkIds = [];
$bar = $this->output->createProgressBar($total);
$bar->setFormat(' %current%/%max% [%bar%] %percent:3s%% | imported: %imported% | skipped: %skipped%');
$bar->setMessage('0', 'imported');
$bar->setMessage('0', 'skipped');
$bar->start();
DB::connection('legacy')
->table('users_opinions')
->orderBy('opinion_id')
->chunk($chunk, function ($rows) use (
&$stats,
&$affectedArtworkIds,
$validArtworkIds,
$validUserIds,
$dryRun,
$force,
$bar
) {
$inserts = [];
$now = now();
foreach ($rows as $row) {
$artworkId = (int) $row->artwork_id;
$userId = (int) $row->author_id; // author_id = the voter
$score = (int) $row->score;
$postedAt = $row->post_date ?? $now;
// --- score → medal ---
$medal = self::SCORE_MAP[$score] ?? null;
if ($medal === null) {
$stats['skipped_score']++;
$bar->advance();
continue;
}
// --- Artwork must exist in new DB ---
if (! isset($validArtworkIds[$artworkId])) {
$stats['skipped_artwork']++;
$bar->advance();
continue;
}
// --- User must exist in new DB ---
if (! isset($validUserIds[$userId])) {
$stats['skipped_user']++;
$bar->advance();
continue;
}
if (! $dryRun) {
if ($force) {
// Upsert: update medal if row already exists
$affected = DB::table('artwork_awards')
->where('artwork_id', $artworkId)
->where('user_id', $userId)
->update([
'medal' => $medal,
'weight' => ArtworkAward::WEIGHTS[$medal],
'updated_at' => $now,
]);
if ($affected > 0) {
$stats['updated_force']++;
$affectedArtworkIds[$artworkId] = true;
$bar->advance();
continue;
}
} else {
// Skip if already exists
if (
DB::table('artwork_awards')
->where('artwork_id', $artworkId)
->where('user_id', $userId)
->exists()
) {
$stats['skipped_duplicate']++;
$bar->advance();
continue;
}
}
$inserts[] = [
'artwork_id' => $artworkId,
'user_id' => $userId,
'medal' => $medal,
'weight' => ArtworkAward::WEIGHTS[$medal],
'created_at' => $postedAt,
'updated_at' => $postedAt,
];
$affectedArtworkIds[$artworkId] = true;
}
$stats['imported']++;
$bar->advance();
}
// Bulk insert the batch (DB::table bypasses the observer intentionally;
// stats are recalculated in bulk at the end for performance)
if (! $dryRun && ! empty($inserts)) {
try {
DB::table('artwork_awards')->insert($inserts);
} catch (\Throwable $e) {
// Fallback: insert one-by-one to isolate constraint violations
foreach ($inserts as $row) {
try {
DB::table('artwork_awards')->insertOrIgnore([$row]);
} catch (\Throwable) {
$stats['errors']++;
}
}
}
}
$skippedTotal = $stats['skipped_score']
+ $stats['skipped_artwork']
+ $stats['skipped_user']
+ $stats['skipped_duplicate'];
$bar->setMessage((string) $stats['imported'], 'imported');
$bar->setMessage((string) $skippedTotal, 'skipped');
});
$bar->finish();
$this->newLine(2);
// -------------------------------------------------------------------------
// Recalculate stats for every affected artwork
// -------------------------------------------------------------------------
if (! $dryRun && ! $skipStats && ! empty($affectedArtworkIds)) {
$artworkCount = count($affectedArtworkIds);
$this->info("Recalculating award stats for {$artworkCount} artworks…");
$statsBar = $this->output->createProgressBar($artworkCount);
$statsBar->start();
foreach (array_keys($affectedArtworkIds) as $artworkId) {
try {
$service->recalcStats($artworkId);
} catch (\Throwable $e) {
$this->newLine();
$this->warn("Stats recalc failed for artwork #{$artworkId}: {$e->getMessage()}");
}
$statsBar->advance();
}
$statsBar->finish();
$this->newLine(2);
}
// -------------------------------------------------------------------------
// Summary
// -------------------------------------------------------------------------
$this->table(
['Result', 'Count'],
[
['Imported (new rows)', $stats['imported']],
['Forced updates', $stats['updated_force']],
['Skipped bad score', $stats['skipped_score']],
['Skipped artwork gone', $stats['skipped_artwork']],
['Skipped user gone', $stats['skipped_user']],
['Skipped duplicate', $stats['skipped_duplicate']],
['Errors', $stats['errors']],
]
);
if ($dryRun) {
$this->warn('[DRY-RUN] Nothing was written. Re-run without --dry-run to apply.');
} else {
$this->info('Migration complete.');
}
return $stats['errors'] > 0 ? self::FAILURE : self::SUCCESS;
}
}