CaiWuXiTong/app/Console/Commands/SyncBusinessMirrors.php

182 lines
8.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Modules\EcommerceFinance\Repositories\GenericBusinessMirrorRepository;
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
use Modules\EcommerceFinance\Services\BusinessMirrorSynchronizer;
use Modules\EcommerceFinance\Services\DingPayableWorkflowMirrorSynchronizer;
use Throwable;
class SyncBusinessMirrors extends Command
{
protected $signature = 'business-mirror:sync-tables
{dataset? : 只同步指定数据集}
{--full : 执行或继续首次全量/校准同步}
{--batch= : 每批最多读取行数}
{--max-batches= : 每个数据集本轮最多批次数,0 表示直到完成}
{--max-datasets= : 本轮最多处理的数据集数量}';
protected $description = '将项目实际使用的上游表按稳定游标同步到共享业务镜像库';
public function handle(
BusinessMirrorDatasetRegistry $datasets,
BusinessMirrorSynchronizer $synchronizer,
GenericBusinessMirrorRepository $mirror,
?DingPayableWorkflowMirrorSynchronizer $dingPayableWorkflows = null,
): int
{
if (! (bool) config('upstream_mirrors.refresh_enabled', false) && ! $this->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<array{key:string,full:bool,catchup:bool,last_attempted_at:string}> */
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;
}
}