CaiWuXiTong/modules/EcommerceFinance/Services/ProductPriceMirrorSynchronizer.php

117 lines
4.6 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 Modules\EcommerceFinance\Services;
use Illuminate\Support\Carbon;
use Modules\EcommerceFinance\Repositories\ProductPriceMirrorRepository;
use Modules\EcommerceFinance\Repositories\ProductPriceRepository;
use RuntimeException;
use Throwable;
class ProductPriceMirrorSynchronizer
{
public function __construct(
private readonly ProductPriceRepository $upstream,
private readonly ProductPriceMirrorRepository $mirror,
) {
}
/** @return array{status:string,mode:string,batches:int,rows:int} */
public function sync(bool $full = false, ?int $batchSize = null, ?int $maxBatches = null): array
{
if (! $this->mirror->ready()) {
throw new RuntimeException('business_mirror_schema_missing');
}
$state = $this->mirror->state();
if (! $full && ($state['initial_completed_at'] ?? null) === null) {
return ['status' => 'requires_full_sync', 'mode' => 'incremental', 'batches' => 0, 'rows' => 0];
}
$mode = $full ? 'full' : 'incremental';
$resume = is_array($state)
&& ($state['run_mode'] ?? null) === $mode
&& in_array($state['status'] ?? null, ['running', 'failed'], true)
&& ($state['window_started_at'] ?? null) !== null
&& ($state['window_ends_at'] ?? null) !== null;
if ($resume) {
$since = Carbon::parse((string) $state['window_started_at']);
$until = Carbon::parse((string) $state['window_ends_at']);
} elseif ($full) {
$since = Carbon::create(1900, 1, 1, 0, 0, 0, config('app.timezone'));
$until = now()->addSecond();
} else {
$overlapHours = max((int) config('upstream_mirrors.product_prices.overlap_hours', 48), 1);
$since = Carbon::parse((string) $state['last_succeeded_at'])->subHours($overlapHours);
$until = now()->addSecond();
}
$state = $this->mirror->startRun($mode, $since, $until, $resume);
$cursor = $this->cursorFromState($state);
$batchSize ??= (int) config('upstream_mirrors.product_prices.batch_size', 500);
$batchSize = min(max($batchSize, 1), 2000);
$maxBatches ??= (int) config('upstream_mirrors.product_prices.max_batches_per_run', 4);
$maxBatches = max($maxBatches, 0);
$batches = 0;
$rows = (int) ($state['last_upserted_rows'] ?? 0);
try {
while (true) {
$batch = $this->upstream->mirrorBatch(
$cursor,
$since->format('Y-m-d H:i:s'),
$until->format('Y-m-d H:i:s'),
$batchSize,
);
if ($batch === []) {
$this->mirror->complete($mode, $until, $rows);
return ['status' => 'succeeded', 'mode' => $mode, 'batches' => $batches, 'rows' => $rows];
}
$last = $batch[array_key_last($batch)];
$cursor = [
'completed_at' => (string) ($last['cursor_completed_at'] ?? $last['completed_at']),
'business_id' => (string) $last['business_id'],
'barcode' => (string) $last['barcode'],
];
$rows += count($batch);
$batches++;
$this->mirror->storeBatch($batch, $cursor, $rows);
if (count($batch) < $batchSize) {
$this->mirror->complete($mode, $until, $rows);
return ['status' => 'succeeded', 'mode' => $mode, 'batches' => $batches, 'rows' => $rows];
}
if ($maxBatches > 0 && $batches >= $maxBatches) {
return ['status' => 'running', 'mode' => $mode, 'batches' => $batches, 'rows' => $rows];
}
}
} catch (Throwable $exception) {
$this->mirror->fail($exception::class);
throw $exception;
}
}
/** @return array{completed_at:string,business_id:string,barcode:string}|null */
private function cursorFromState(array $state): ?array
{
if (($state['cursor_completed_at'] ?? null) === null) {
return null;
}
return [
// 直接复用 SQL Server 返回的 datetime2(7) ISO 字符串;Carbon 只有
// 6 位微秒精度,解析再格式化会丢掉最后一位并破坏稳定游标。
'completed_at' => (string) $state['cursor_completed_at'],
'business_id' => (string) ($state['cursor_business_id'] ?? ''),
'barcode' => (string) ($state['cursor_product_code'] ?? ''),
];
}
}