option('full')) { return self::SUCCESS; } $datasetKey = (string) config( 'upstream_mirrors.product_catalog.dataset_key', 'operations_replica.rpt.jst_sku', ); try { $dataset = $datasets->get($datasetKey); $state = $mirror->state($dataset); $full = $this->shouldRunFull($state); $result = $synchronizer->sync( $datasetKey, $full, $this->option('batch') === null ? (int) config('upstream_mirrors.product_catalog.batch_size', 1000) : (int) $this->option('batch'), $this->option('max-batches') === null ? (int) config('upstream_mirrors.product_catalog.max_batches_per_run', 5) : (int) $this->option('max-batches'), ); } catch (Throwable $exception) { Log::error('普通商品共享镜像同步失败。', [ 'dataset' => $datasetKey, 'exception' => $exception::class, ]); $this->error('普通商品同步失败,已保留上一版共享数据。'); return self::FAILURE; } if ($result['status'] === 'requires_full_sync') { $this->warn('普通商品尚未完成首次全量同步,请完成备份后显式使用 --full。'); return self::SUCCESS; } $this->info(sprintf( '普通商品%s:%s,本轮 %d 批,累计处理 %d 行。', $result['mode'] === 'full' ? '全量校准' : '增量同步', $result['status'] === 'succeeded' ? '完成' : '进行中', $result['batches'], $result['rows'], )); return self::SUCCESS; } /** @param array|null $state */ private function shouldRunFull(?array $state): bool { if ((bool) $this->option('full')) { return true; } if (is_array($state) && ($state['run_mode'] ?? null) === 'full' && in_array($state['status'] ?? null, ['running', 'failed'], true)) { return true; } if (! is_array($state) || ($state['initial_completed_at'] ?? null) === null) { return false; } $now = now(); if (! $this->isReconcileWindow($now)) { return false; } $lastFull = $state['last_full_succeeded_at'] ?? null; if ($lastFull === null || $lastFull === '') { return true; } $hours = max((int) config('upstream_mirrors.product_catalog.full_reconcile_hours', 168), 1); return Carbon::parse((string) $lastFull)->addHours($hours)->lte($now); } private function isReconcileWindow(Carbon $now): bool { $weekday = min(max((int) config('upstream_mirrors.product_catalog.reconcile_weekday', 0), 0), 6); $startHour = min(max((int) config('upstream_mirrors.product_catalog.reconcile_start_hour', 2), 0), 23); $windowHours = min(max((int) config('upstream_mirrors.product_catalog.reconcile_window_hours', 6), 1), 24 - $startHour); return $now->dayOfWeek === $weekday && $now->hour >= $startHour && $now->hour < $startHour + $windowHours; } }