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

@@ -0,0 +1,203 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\Artwork;
use App\Services\Uploads\UploadStorageService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Throwable;
final class AuditArtworkThumbnailsCommand extends Command
{
protected $signature = 'artworks:audit-thumbnails
{--id= : Audit only this artwork ID}
{--limit= : Stop after processing this many artworks}
{--chunk=200 : Number of artworks to scan per batch}
{--variant=* : Specific thumbnail variants to check (defaults to all configured derivatives)}
{--dry-run : Report missing thumbnails without updating the artworks table}';
protected $description = 'Check artwork thumbnails on the configured object storage disk and mark artworks with missing thumbnails.';
public function handle(UploadStorageService $storage): int
{
$artworkId = $this->option('id') !== null ? max(1, (int) $this->option('id')) : null;
$limit = $this->option('limit') !== null ? max(1, (int) $this->option('limit')) : null;
$chunkSize = max(1, min((int) $this->option('chunk'), 1000));
$dryRun = (bool) $this->option('dry-run');
$variants = $this->resolveVariants();
if ($variants === []) {
$this->error('No thumbnail variants are configured. Check uploads.derivatives.');
return self::FAILURE;
}
if (! $dryRun && ! Schema::hasColumns('artworks', [
'has_missing_thumbnails',
'missing_thumbnail_variants_json',
'thumbnails_checked_at',
])) {
$this->error('Artwork thumbnail audit columns are missing. Run the latest database migrations first.');
return self::FAILURE;
}
$diskName = $storage->objectDiskName();
$diskConfig = config("filesystems.disks.{$diskName}");
if (! is_array($diskConfig)) {
$this->error("Filesystem disk [{$diskName}] is not configured.");
return self::FAILURE;
}
$disk = Storage::disk($diskName);
$this->info(sprintf(
'Starting thumbnail audit. disk=%s variants=%s chunk=%d limit=%s dry_run=%s',
$diskName,
implode(',', $variants),
$chunkSize,
$limit !== null ? (string) $limit : 'all',
$dryRun ? 'yes' : 'no',
));
$query = Artwork::query()
->select(['id', 'hash', 'thumb_ext'])
->orderBy('id');
if ($artworkId !== null) {
$query->whereKey($artworkId);
}
$processed = 0;
$healthy = 0;
$missing = 0;
$written = 0;
$failed = 0;
$query->chunkById($chunkSize, function ($artworks) use ($storage, $disk, $variants, $limit, $dryRun, &$processed, &$healthy, &$missing, &$written, &$failed) {
foreach ($artworks as $artwork) {
if ($limit !== null && $processed >= $limit) {
return false;
}
try {
$missingVariants = $this->resolveMissingVariants($artwork, $variants, $storage, $disk);
$hasMissing = $missingVariants !== [];
if ($hasMissing) {
$missing++;
$this->warn(sprintf(
'Artwork %d missing thumbnails: %s',
(int) $artwork->id,
implode(',', $missingVariants),
));
} else {
$healthy++;
}
if (! $dryRun) {
$this->persistAuditResult((int) $artwork->id, $hasMissing, $missingVariants);
$written++;
}
} catch (Throwable $exception) {
$failed++;
$this->warn(sprintf('Artwork %d audit failed: %s', (int) $artwork->id, $exception->getMessage()));
}
$processed++;
}
return true;
});
$this->info(sprintf(
'Thumbnail audit complete. processed=%d healthy=%d missing=%d written=%d failed=%d',
$processed,
$healthy,
$missing,
$written,
$failed,
));
return $failed > 0 ? self::FAILURE : self::SUCCESS;
}
/**
* @return list<string>
*/
private function resolveVariants(): array
{
$configured = array_keys((array) config('uploads.derivatives', []));
$configured = array_values(array_filter(array_map(
static fn ($variant): string => strtolower(trim((string) $variant)),
$configured,
)));
$requested = (array) $this->option('variant');
if ($requested === []) {
return $configured;
}
$normalizedRequested = array_values(array_unique(array_filter(array_map(
static fn ($variant): string => strtolower(trim((string) $variant)),
$requested,
))));
$invalid = array_values(array_diff($normalizedRequested, $configured));
if ($invalid !== []) {
$this->error('Unknown thumbnail variants: ' . implode(', ', $invalid));
$this->line('Configured variants: ' . implode(', ', $configured));
return [];
}
return $normalizedRequested;
}
/**
* @param list<string> $variants
* @return list<string>
*/
private function resolveMissingVariants(Artwork $artwork, array $variants, UploadStorageService $storage, mixed $disk): array
{
$hash = strtolower((string) preg_replace('/[^a-z0-9]/', '', (string) ($artwork->hash ?? '')));
$thumbExt = strtolower(ltrim((string) ($artwork->thumb_ext ?? ''), '.'));
if ($hash === '' || $thumbExt === '') {
return $variants;
}
$filename = $hash . '.' . $thumbExt;
$missing = [];
foreach ($variants as $variant) {
$objectPath = $storage->objectPathForVariant($variant, $hash, $filename);
if (! $disk->exists($objectPath)) {
$missing[] = $variant;
}
}
return $missing;
}
/**
* @param list<string> $missingVariants
*/
private function persistAuditResult(int $artworkId, bool $hasMissing, array $missingVariants): void
{
DB::table('artworks')
->where('id', $artworkId)
->update([
'has_missing_thumbnails' => $hasMissing,
'missing_thumbnail_variants_json' => $missingVariants === []
? null
: json_encode(array_values($missingVariants), JSON_UNESCAPED_SLASHES),
'thumbnails_checked_at' => now(),
]);
}
}