120 lines
4.3 KiB
PHP
120 lines
4.3 KiB
PHP
<?php
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use Illuminate\Console\Command;
|
||
use Illuminate\Support\Carbon;
|
||
use Illuminate\Support\Facades\Log;
|
||
use Modules\EcommerceFinance\Repositories\GenericBusinessMirrorRepository;
|
||
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
|
||
use Modules\EcommerceFinance\Services\BusinessMirrorSynchronizer;
|
||
use Throwable;
|
||
|
||
class SyncProductCatalogMirror extends Command
|
||
{
|
||
protected $signature = 'business-mirror:sync-product-catalog
|
||
{--full : 强制启动或继续普通商品全量校准}
|
||
{--batch= : 每批最多读取行数}
|
||
{--max-batches= : 本轮最多批次数,0 表示直到完成}';
|
||
|
||
protected $description = '高频同步聚水潭普通商品,并在每周低峰期执行可断点全量校准';
|
||
|
||
public function handle(
|
||
BusinessMirrorDatasetRegistry $datasets,
|
||
BusinessMirrorSynchronizer $synchronizer,
|
||
GenericBusinessMirrorRepository $mirror,
|
||
): int {
|
||
if (! (bool) config('upstream_mirrors.refresh_enabled', false) && ! $this->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<string,mixed>|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;
|
||
}
|
||
}
|