option('full')) { return self::SUCCESS; } try { $state = $mirror->state(); $full = $this->shouldRunFull($state); $result = $synchronizer->sync( $full, $this->option('batch') === null ? null : (int) $this->option('batch'), $this->option('max-batches') === null ? null : (int) $this->option('max-batches'), ); } catch (Throwable $exception) { Log::error('采购单价共享镜像同步失败。', ['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(); $weekday = min(max((int) config( 'upstream_mirrors.product_purchase_prices.reconcile_weekday', 0, ), 0), 6); $startHour = min(max((int) config( 'upstream_mirrors.product_purchase_prices.reconcile_start_hour', 2, ), 0), 23); $windowHours = min(max((int) config( 'upstream_mirrors.product_purchase_prices.reconcile_window_hours', 6, ), 1), 24 - $startHour); if ($now->dayOfWeek !== $weekday || $now->hour < $startHour || $now->hour >= $startHour + $windowHours) { return false; } $lastFull = $state['last_full_succeeded_at'] ?? null; if ($lastFull === null || $lastFull === '') { return true; } $hours = max((int) config( 'upstream_mirrors.product_purchase_prices.full_reconcile_hours', 168, ), 1); return Carbon::parse((string) $lastFull)->addHours($hours)->lte($now); } }