option('full')) { return self::SUCCESS; } $requested = $this->argument('dataset'); $keys = $requested === null ? $this->scheduledKeys($datasets, $mirror) : [['key' => (string) $requested, 'full' => (bool) $this->option('full'), 'catchup' => false]]; $maxDatasets = max((int) ($this->option('max-datasets') ?? config('upstream_pipeline.sync.max_datasets', 8)), 1); $processed = 0; $failed = false; $deadline = microtime(true) + max((int) config('upstream_pipeline.sync.cycle_seconds', 30), 1); foreach ($keys as $task) { if ($processed >= $maxDatasets || ($processed > 0 && microtime(true) >= $deadline)) { break; } $key = $task['key']; try { $result = $synchronizer->sync( $key, (bool) $task['full'], $this->option('batch') === null ? null : (int) $this->option('batch'), $this->option('max-batches') === null ? ($task['catchup'] ? max((int) config('upstream_pipeline.sync.catchup_batches', 6), 1) : null) : (int) $this->option('max-batches'), ); } catch (Throwable $exception) { $failed = true; Log::error('共享业务镜像表同步失败。', ['dataset' => $key, 'exception' => $exception::class]); $this->error($key . ':同步失败,已保留上一版共享数据。'); $processed++; continue; } if ($result['status'] === 'requires_full_sync') { if ($requested !== null) { $this->warn($key . ':尚未完成首次全量同步,请显式使用 --full。'); } continue; } $processed++; $this->info(sprintf( '%s:%s,本轮 %d 批,累计处理 %d 行。', $key, $result['status'] === 'succeeded' ? '同步完成' : '同步进行中', $result['batches'], $result['rows'], )); } // 钉钉应付流程采用按流程聚合的专用镜像;接入同一入口后由现有 // 生产 cron 自动断点续传,避免额外维护一套调度脚本。 if ($requested === null && $dingPayableWorkflows !== null && ! app()->environment('testing')) { try { $result = $dingPayableWorkflows->sync( (bool) $this->option('full'), $this->option('batch') === null ? null : (int) $this->option('batch'), $this->option('max-batches') === null ? null : (int) $this->option('max-batches'), ); if ($result['status'] !== 'requires_full_sync') { $this->info(sprintf( 'operations_replica.rpt.ding_payable_workflows:%s,本轮 %d 批,累计处理 %d 个流程。', $result['status'] === 'succeeded' ? '同步完成' : '同步进行中', $result['batches'], $result['rows'], )); } } catch (Throwable $exception) { $failed = true; Log::error('钉钉应付流程镜像同步失败。', ['exception' => $exception::class]); $this->error('operations_replica.rpt.ding_payable_workflows:同步失败,已保留上一版共享数据。'); } // 线上代发业务报销流程复用同一套钉钉流程镜像同步链路,接入此处后 // 由现有生产 cron 自动断点续传,不额外维护调度脚本。 try { $daifaResult = $dingPayableWorkflows ->forConfigKey('ding_online_daifa_price') ->sync( (bool) $this->option('full'), $this->option('batch') === null ? null : (int) $this->option('batch'), $this->option('max-batches') === null ? null : (int) $this->option('max-batches'), ); if ($daifaResult['status'] !== 'requires_full_sync') { $this->info(sprintf( 'operations_replica.rpt.ding_online_daifa_price:%s,本轮 %d 批,累计处理 %d 条明细。', $daifaResult['status'] === 'succeeded' ? '同步完成' : '同步进行中', $daifaResult['batches'], $daifaResult['rows'], )); } } catch (Throwable $exception) { $failed = true; Log::error('钉钉线上代发价格流程镜像同步失败。', ['exception' => $exception::class]); $this->error('operations_replica.rpt.ding_online_daifa_price:同步失败,已保留上一版共享数据。'); } } return $failed ? self::FAILURE : self::SUCCESS; } /** @return list */ private function scheduledKeys( BusinessMirrorDatasetRegistry $datasets, GenericBusinessMirrorRepository $mirror, ): array { $scheduled = []; $dedicated = array_fill_keys(array_map( 'strval', (array) config('upstream_mirrors.table_sync.dedicated_datasets', []), ), true); foreach ($datasets->all() as $key => $dataset) { if (isset($dedicated[$key]) || ! $datasets->scheduled($key)) { continue; } $state = $mirror->state($dataset); $initial = ! is_array($state) || ($state['initial_completed_at'] ?? null) === null; if ($initial && ! $datasets->automaticInitialSyncAllowed($key)) continue; if (($state['status'] ?? '') === 'failed' && ! empty($state['last_attempted_at']) && now()->diffInSeconds(\Illuminate\Support\Carbon::parse($state['last_attempted_at'])) < 300) continue; $scheduled[] = [ 'key' => $key, 'full' => $initial || (bool) $this->option('full'), 'catchup' => $initial || ($state['status'] ?? '') === 'running', 'last_attempted_at' => (string) ($state['last_attempted_at'] ?? ''), ]; } usort($scheduled, fn (array $left, array $right): int => [$left['last_attempted_at'], $left['key']] <=> [$right['last_attempted_at'], $right['key']] ); // Alternate unfinished windows and routine refreshes: catch-up progresses // every cycle without starving small tables or monopolizing a source. $catchup = array_values(array_filter($scheduled, fn ($task) => $task['catchup'])); $routine = array_values(array_filter($scheduled, fn ($task) => ! $task['catchup'])); $ordered = []; while ($catchup !== [] || $routine !== []) { if ($routine !== []) $ordered[] = array_shift($routine); if ($catchup !== []) $ordered[] = array_shift($catchup); } return $ordered; } }