CaiWuXiTong/modules/EcommerceFinance/Services/ProductPurchasePriceMirrorSynchronizer.php

116 lines
4.6 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Services;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Modules\EcommerceFinance\Repositories\ProductPurchasePriceMirrorRepository;
use Modules\EcommerceFinance\Repositories\ProductPurchasePriceRepository;
use RuntimeException;
use Throwable;
class ProductPurchasePriceMirrorSynchronizer
{
public function __construct(
private readonly ProductPurchasePriceRepository $upstream,
private readonly ProductPurchasePriceMirrorRepository $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->available()) {
throw new RuntimeException('product_purchase_price_mirror_schema_missing');
}
$state = $this->mirror->state();
if (! $full && (
($state['initial_completed_at'] ?? null) === null
|| (int) ($state['projection_version'] ?? 1) < ProductPurchasePriceMirrorRepository::CURRENT_PROJECTION_VERSION
)) {
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)
&& (int) ($state['projection_version'] ?? 1) >= ProductPurchasePriceMirrorRepository::CURRENT_PROJECTION_VERSION
&& ($state['window_started_at'] ?? null) !== null
&& ($state['window_ends_at'] ?? null) !== null;
$generation = $resume && is_string($state['generation'] ?? null)
? (string) $state['generation']
: (string) Str::uuid();
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 {
$overlap = max((int) config('upstream_mirrors.product_purchase_prices.overlap_hours', 48), 1);
$since = Carbon::parse((string) $state['last_succeeded_at'])->subHours($overlap);
$until = now()->addSecond();
}
$state = $this->mirror->startRun($mode, $generation, $since, $until, $resume);
$cursor = ($state['cursor_updated_at'] ?? null) === null
? null
: [
'updated_at' => (string) $state['cursor_updated_at'],
'item_id' => (int) ($state['cursor_item_id'] ?? 0),
];
$batchSize = min(max(
$batchSize ?? (int) config('upstream_mirrors.product_purchase_prices.batch_size', 1000),
1,
), 5000);
$maxBatches = max(
$maxBatches ?? (int) config('upstream_mirrors.product_purchase_prices.max_batches_per_run', 5),
0,
);
$batches = 0;
$rows = (int) ($state['last_upserted_rows'] ?? 0);
try {
while (true) {
$batch = $this->upstream->mirrorBatch(
$cursor,
$since->format('Y-m-d\TH:i:s.v'),
$until->format('Y-m-d\TH:i:s.v'),
$batchSize,
);
if ($batch === []) {
$this->mirror->complete($mode, $generation, $until, $rows);
return ['status' => 'succeeded', 'mode' => $mode, 'batches' => $batches, 'rows' => $rows];
}
$last = $batch[array_key_last($batch)];
$cursor = [
'updated_at' => (string) $last['purchase_updated_at'],
'item_id' => (int) $last['item_id'],
];
$rows += count($batch);
$batches++;
$this->mirror->storeBatch($batch, $cursor, $generation, $rows);
if (count($batch) < $batchSize) {
$this->mirror->complete($mode, $generation, $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;
}
}
}