CaiWuXiTong/modules/EcommerceFinance/Services/WorkOrderWaybillLookup.php

103 lines
4.5 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Services;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\DatabaseManager;
/** A small index into the existing raw mirror; the original JSON stays authoritative. */
class WorkOrderWaybillLookup
{
public const SOURCE = 'js_jst_work_order_info';
public const TABLE = 'work_order_waybill_lookup';
public const STATE = 'work_order_waybill_lookup_states';
private readonly ConnectionInterface $connection;
private ?bool $available = null;
public function __construct(DatabaseManager $database)
{
$this->connection = $database->connection((string) config('upstream_mirrors.connection', 'business_mirror'));
}
public function available(): bool
{
return $this->available ??= $this->connection->getSchemaBuilder()->hasTable(self::TABLE)
&& $this->connection->getSchemaBuilder()->hasTable(self::STATE);
}
public function ready(): bool
{
return $this->available() && $this->connection->table(self::STATE)->where('id', 1)->whereNotNull('completed_at')->exists();
}
/** Called inside the raw mirror's transaction, including changes to title/waybill. */
public function storeBatch(array $rows): void
{
if ($rows === [] || ! $this->available()) return;
$keys = [];
$values = [];
foreach ($rows as $row) {
$row = (array) $row;
$key = (string) $row['_mirror_key'];
$keys[] = $key;
if (($row['title'] ?? '') !== '查件') continue;
$fields = json_decode((string) ($row['businessFieldList'] ?? ''), true);
if (! is_array($fields)) continue;
$waybill = '';
// Same last-field-wins semantics as the existing settlement parser.
foreach ($fields as $field) {
if (is_array($field) && ($field['fieldId'] ?? null) === 'lIdField') {
$waybill = trim((string) ($field['value'] ?? ''));
}
}
if ($waybill !== '' && mb_strlen($waybill) <= 191) {
$values[] = ['mirror_key' => $key, 'waybill' => $waybill, 'source_id' => (int) ($row['Id'] ?? 0)];
}
}
// Remove renamed/invalid records too, so an old waybill never survives an update.
$this->connection->table(self::TABLE)->whereIn('mirror_key', $keys)->delete();
foreach (array_chunk($values, 500) as $chunk) {
$this->connection->table(self::TABLE)->insert($chunk);
}
}
public function prune(): void
{
if (! $this->available()) return;
$this->connection->table(self::TABLE)->whereNotExists(function ($query): void {
$query->selectRaw('1')->from(self::SOURCE)
->whereColumn(self::SOURCE . '._mirror_key', self::TABLE . '.mirror_key');
})->delete();
}
/** Resume a bounded keyset pass; production invokes this through the global sync lock. */
public function backfill(int $batchSize = 500, int $maxBatches = 2, int $pauseMs = 50): array
{
if (! $this->available()) throw new \RuntimeException('work_order_waybill_lookup_not_migrated');
$state = $this->connection->table(self::STATE)->where('id', 1)->first();
if ($state?->completed_at) return ['complete' => true, 'rows' => 0, 'batches' => 0];
$cursor = $state?->cursor;
$rows = $batches = 0;
$batchSize = min(max(1, $batchSize), 1000);
do {
$batch = $this->connection->table(self::SOURCE)->when($cursor, fn ($query) => $query->where('_mirror_key', '>', $cursor))
->orderBy('_mirror_key')->limit($batchSize)->get(['_mirror_key', 'Id', 'title', 'businessFieldList']);
$complete = $batch->count() < $batchSize;
$this->connection->transaction(function () use ($batch, $complete, &$cursor): void {
$this->storeBatch($batch->all());
$cursor = $batch->last()?->_mirror_key ?? $cursor;
if ($complete) $this->prune();
$this->connection->table(self::STATE)->updateOrInsert(['id' => 1], [
'cursor' => $cursor, 'completed_at' => $complete ? now() : null, 'updated_at' => now(),
]);
});
$rows += $batch->count();
$batches++;
if (! $complete && $pauseMs > 0) usleep(min($pauseMs, 1000) * 1000);
} while (! $complete && ($maxBatches === 0 || $batches < $maxBatches));
return compact('complete', 'rows', 'batches');
}
}