CaiWuXiTong/modules/EcommerceFinance/Services/JushuitanCostPriceHistorySynchronizer.php

155 lines
5.8 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Services;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Modules\EcommerceFinance\Repositories\JushuitanCostPriceHistoryMirrorRepository;
use RuntimeException;
use Throwable;
class JushuitanCostPriceHistorySynchronizer
{
public function __construct(
private readonly JushuitanOpenApiClient $upstream,
private readonly JushuitanCostPriceHistoryMirrorRepository $mirror,
) {
}
/** @return array{status:string,mode:string,pages:int,source_rows:int,rows:int} */
public function sync(bool $full = false, ?int $pageSize = null, ?int $maxPages = null): array
{
if (! $this->mirror->available()) {
throw new RuntimeException('jushuitan_cost_price_history_mirror_schema_missing');
}
$state = $this->mirror->state();
if (! $full && ($state['initial_completed_at'] ?? null) === null) {
return ['status' => 'requires_full_sync', 'mode' => 'incremental', 'pages' => 0, 'source_rows' => 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;
$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 = $this->historyStart();
$until = now();
} else {
$overlap = max((int) config('services.jushuitan.cost_price_history_overlap_hours', 48), 1);
$since = Carbon::parse((string) $state['last_succeeded_at'])->subHours($overlap);
$until = now();
}
$state = $this->mirror->startRun($mode, $generation, $since, $until, $resume);
$windowStartedAt = ($state['cursor_window_started_at'] ?? null) === null
? $since->copy()
: Carbon::parse((string) $state['cursor_window_started_at']);
$page = max((int) ($state['cursor_page'] ?? 1), 1);
$pageSize = min(max(
$pageSize ?? (int) config('services.jushuitan.cost_price_history_page_size', 50),
1,
), 50);
$maxPages = max(
$maxPages ?? (int) config('services.jushuitan.cost_price_history_max_pages', 30),
0,
);
$pages = 0;
$processedSourceRows = (int) ($state['last_processed_source_rows'] ?? 0);
$upsertedRows = (int) ($state['last_upserted_rows'] ?? 0);
$windowDays = min(max(
(int) config('services.jushuitan.cost_price_history_window_days', 7),
1,
), 7);
try {
while ($windowStartedAt->lt($until)) {
$windowEndsAt = $windowStartedAt->copy()->addDays($windowDays);
if ($windowEndsAt->gte($until)) {
$windowEndsAt = $until->copy();
}
$result = $this->upstream->getCostPriceOperationHistory(
$windowStartedAt,
$windowEndsAt,
$page,
$pageSize,
);
$records = $result['records'];
$processedSourceRows += $result['source_rows'];
$upsertedRows += count($records);
$pages++;
$hasNextPage = $result['has_next'];
// 相邻窗口在边界秒保留一次重叠,避免接口时间精度高于秒时漏掉记录;
// operation_id 的幂等 upsert 会消除边界重复。
$nextWindowStartedAt = $hasNextPage
? $windowStartedAt->copy()
: $windowEndsAt->copy();
$nextPage = $hasNextPage ? $page + 1 : 1;
$this->mirror->storePage(
$records,
$nextWindowStartedAt,
$nextPage,
$generation,
$processedSourceRows,
$upsertedRows,
);
$windowStartedAt = $nextWindowStartedAt;
$page = $nextPage;
if ($maxPages > 0 && $pages >= $maxPages && $windowStartedAt->lt($until)) {
return [
'status' => 'running',
'mode' => $mode,
'pages' => $pages,
'source_rows' => $processedSourceRows,
'rows' => $upsertedRows,
];
}
}
$this->mirror->complete(
$mode,
$generation,
$until,
$processedSourceRows,
$upsertedRows,
);
return [
'status' => 'succeeded',
'mode' => $mode,
'pages' => $pages,
'source_rows' => $processedSourceRows,
'rows' => $upsertedRows,
];
} catch (Throwable $exception) {
$message = trim($exception->getMessage());
$errorCode = preg_match('/^[a-z0-9_]+$/', $message) === 1
? $message
: $exception::class;
$this->mirror->fail($errorCode);
throw $exception;
}
}
private function historyStart(): Carbon
{
$configured = trim((string) config(
'services.jushuitan.cost_price_history_start',
'2000-01-01 00:00:00',
));
return Carbon::parse($configured === '' ? '2000-01-01 00:00:00' : $configured);
}
}