feat: ship creator journey v2 and profile updates

This commit is contained in:
2026-04-12 21:42:07 +02:00
parent a2457f4e49
commit d5cff21ea2
335 changed files with 20147 additions and 1545 deletions

View File

@@ -5,20 +5,20 @@ 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`.
* Migrates legacy `users_opinions` (projekti_old_skinbase) into `artwork_medals`.
*
* 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)
* 5 gold
* 4 gold
* 3 silver
* 2 silver
* 1 bronze
* 0 bronze
*
* Usage:
* php artisan awards:import-legacy
@@ -29,22 +29,38 @@ use Illuminate\Support\Facades\Schema;
class ImportLegacyAwards extends Command
{
protected $signature = 'awards:import-legacy
{--connection=legacy : Legacy database connection name}
{--artwork-id=* : Restrict import to one or more artwork IDs}
{--show-duplicates : Output skipped duplicate artwork/user pairs at the end}
{--duplicates-limit=100 : Maximum duplicate rows to print when --show-duplicates is used}
{--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';
protected $description = 'Import legacy users_opinions into artwork_medals';
/** Maps legacy score value → medal string */
private const SCORE_MAP = [
4 => 'gold',
0 => 'bronze',
1 => 'bronze',
2 => 'silver',
3 => 'silver',
2 => 'bronze',
4 => 'gold',
5 => 'gold',
];
public function handle(ArtworkAwardService $service): int
{
$legacyConnection = (string) $this->option('connection');
$artworkIds = collect((array) $this->option('artwork-id'))
->map(static fn (mixed $value): int => (int) $value)
->filter(static fn (int $value): bool => $value > 0)
->unique()
->values()
->all();
$showDuplicates = (bool) $this->option('show-duplicates');
$duplicatesLimit = max(1, (int) $this->option('duplicates-limit'));
$dryRun = (bool) $this->option('dry-run');
$chunk = max(1, (int) $this->option('chunk'));
$skipStats = (bool) $this->option('skip-stats');
@@ -56,17 +72,24 @@ class ImportLegacyAwards extends Command
// Verify legacy connection is reachable
try {
DB::connection('legacy')->getPdo();
DB::connection($legacyConnection)->getPdo();
} catch (\Throwable $e) {
$this->error('Cannot connect to legacy database: ' . $e->getMessage());
$this->error("Cannot connect to legacy database connection [{$legacyConnection}]: " . $e->getMessage());
return self::FAILURE;
}
if (! DB::connection('legacy')->getSchemaBuilder()->hasTable('users_opinions')) {
$this->error('Legacy table `users_opinions` not found.');
if (! DB::connection($legacyConnection)->getSchemaBuilder()->hasTable('users_opinions')) {
$this->error("Legacy table `users_opinions` not found on connection [{$legacyConnection}].");
return self::FAILURE;
}
$legacyQuery = DB::connection($legacyConnection)->table('users_opinions');
if ($artworkIds !== []) {
$legacyQuery->whereIn('artwork_id', $artworkIds);
$this->info('Restricting import to artwork IDs: ' . implode(', ', $artworkIds));
}
// 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')
@@ -88,9 +111,7 @@ class ImportLegacyAwards extends Command
));
// Count legacy rows for progress bar
$total = DB::connection('legacy')
->table('users_opinions')
->count();
$total = (clone $legacyQuery)->count();
$this->info("Legacy rows to process: {$total}");
@@ -105,11 +126,13 @@ class ImportLegacyAwards extends Command
'skipped_artwork' => 0,
'skipped_user' => 0,
'skipped_duplicate'=> 0,
'reported_duplicate'=> 0,
'updated_force' => 0,
'errors' => 0,
];
$affectedArtworkIds = [];
$duplicateRows = [];
$bar = $this->output->createProgressBar($total);
$bar->setFormat(' %current%/%max% [%bar%] %percent:3s%% | imported: %imported% | skipped: %skipped%');
@@ -117,24 +140,30 @@ class ImportLegacyAwards extends Command
$bar->setMessage('0', 'skipped');
$bar->start();
DB::connection('legacy')
->table('users_opinions')
$legacyQuery
->orderBy('opinion_id')
->chunk($chunk, function ($rows) use (
&$stats,
&$affectedArtworkIds,
&$duplicateRows,
$validArtworkIds,
$validUserIds,
$dryRun,
$force,
$showDuplicates,
$duplicatesLimit,
$bar
) {
$inserts = [];
$now = now();
foreach ($rows as $row) {
// Legacy users_opinions semantics:
// - artwork_id = the artwork being scored
// - author_id = the artwork owner / author
// - user_id = the voter who gave the score
$artworkId = (int) $row->artwork_id;
$userId = (int) $row->author_id; // author_id = the voter
$userId = (int) $row->user_id;
$score = (int) $row->score;
$postedAt = $row->post_date ?? $now;
@@ -163,11 +192,11 @@ class ImportLegacyAwards extends Command
if (! $dryRun) {
if ($force) {
// Upsert: update medal if row already exists
$affected = DB::table('artwork_awards')
$affected = DB::table('artwork_medals')
->where('artwork_id', $artworkId)
->where('user_id', $userId)
->update([
'medal' => $medal,
'medal_type' => $medal,
'weight' => ArtworkAward::WEIGHTS[$medal],
'updated_at' => $now,
]);
@@ -180,13 +209,26 @@ class ImportLegacyAwards extends Command
}
} else {
// Skip if already exists
if (
DB::table('artwork_awards')
->where('artwork_id', $artworkId)
->where('user_id', $userId)
->exists()
) {
$existingMedal = DB::table('artwork_medals')
->where('artwork_id', $artworkId)
->where('user_id', $userId)
->value('medal_type');
if ($existingMedal !== null) {
$stats['skipped_duplicate']++;
if ($showDuplicates && count($duplicateRows) < $duplicatesLimit) {
$duplicateRows[] = [
'opinion_id' => (int) ($row->opinion_id ?? 0),
'artwork_id' => $artworkId,
'user_id' => $userId,
'legacy_score' => $score,
'legacy_medal' => $medal,
'existing_medal' => (string) $existingMedal,
];
$stats['reported_duplicate']++;
}
$bar->advance();
continue;
}
@@ -195,7 +237,7 @@ class ImportLegacyAwards extends Command
$inserts[] = [
'artwork_id' => $artworkId,
'user_id' => $userId,
'medal' => $medal,
'medal_type' => $medal,
'weight' => ArtworkAward::WEIGHTS[$medal],
'created_at' => $postedAt,
'updated_at' => $postedAt,
@@ -212,12 +254,12 @@ class ImportLegacyAwards extends Command
// stats are recalculated in bulk at the end for performance)
if (! $dryRun && ! empty($inserts)) {
try {
DB::table('artwork_awards')->insert($inserts);
DB::table('artwork_medals')->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]);
DB::table('artwork_medals')->insertOrIgnore([$row]);
} catch (\Throwable) {
$stats['errors']++;
}
@@ -277,6 +319,30 @@ class ImportLegacyAwards extends Command
]
);
if ($showDuplicates && $stats['skipped_duplicate'] > 0) {
$this->newLine();
$this->info(sprintf(
'Duplicate rows skipped: %d. Showing %d row(s)%s.',
$stats['skipped_duplicate'],
count($duplicateRows),
$stats['skipped_duplicate'] > count($duplicateRows) ? " (truncated by --duplicates-limit={$duplicatesLimit})" : ''
));
if ($duplicateRows !== []) {
$this->table(
['Legacy opinion', 'Artwork ID', 'Voter user_id', 'Legacy score', 'Legacy medal', 'Existing medal'],
array_map(static fn (array $row): array => [
$row['opinion_id'],
$row['artwork_id'],
$row['user_id'],
$row['legacy_score'],
$row['legacy_medal'],
$row['existing_medal'],
], $duplicateRows)
);
}
}
if ($dryRun) {
$this->warn('[DRY-RUN] Nothing was written. Re-run without --dry-run to apply.');
} else {