111 lines
3.8 KiB
PHP
111 lines
3.8 KiB
PHP
<?php
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use Illuminate\Console\Command;
|
||
use Illuminate\Support\Carbon;
|
||
use Illuminate\Support\Facades\Log;
|
||
use Modules\EcommerceFinance\Repositories\ProductPurchasePriceMirrorRepository;
|
||
use Modules\EcommerceFinance\Services\ProductPurchasePriceMirrorSynchronizer;
|
||
use Throwable;
|
||
|
||
class SyncProductPurchasePriceMirror extends Command
|
||
{
|
||
protected $signature = 'business-mirror:sync-product-purchase-prices
|
||
{--full : 强制启动或继续首次全量校准}
|
||
{--batch= : 每批最多读取行数}
|
||
{--max-batches= : 本轮最多批次数,0 表示直到完成}';
|
||
|
||
protected $description = '同步采购单管理明细和普通商品单价,并保留可断点的共享业务镜像';
|
||
|
||
public function handle(
|
||
ProductPurchasePriceMirrorRepository $mirror,
|
||
ProductPurchasePriceMirrorSynchronizer $synchronizer,
|
||
): int {
|
||
if (! (bool) config('upstream_mirrors.refresh_enabled', false) && ! $this->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<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();
|
||
$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);
|
||
}
|
||
}
|