CaiWuXiTong/modules/EcommerceFinance/Repositories/CourierFreightReconciliationMirrorRepository.php

199 lines
7.6 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Repositories;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
use RuntimeException;
use Throwable;
/** 快递应付运费对账的共享业务镜像只读访问。 */
class CourierFreightReconciliationMirrorRepository
{
public const DATASET = 'jiansun.dbo.fys_express_courier_settlement';
/** Only columns present in js_express_courier_settlement may be exposed. */
private const COLUMNS = [
'Id', 'month', 'status', 'courier_company_name', 'expresscode', 'expressname',
'replenish_courier', 'opening_balance', 'courier_provided_amount',
'reconciliation_difference', 'confirmed_amount', 'waybill', 'advance_payment',
'payable_amount', 'actual_payment', 'reconciliation_person', 'file_status',
'create_time', 'modify_time', 'is_delete', 'taskid', 'guid', 'creator',
'compensation_amount', 'other_replenishment', 'corporate_payment_amount',
'personal_payment_amount',
];
private readonly ConnectionInterface $connection;
public function __construct(
private readonly BusinessMirrorDatasetRegistry $datasets,
private readonly GenericBusinessMirrorRepository $mirror,
) {
$this->connection = $mirror->connection();
}
public function available(): bool
{
try {
$schema = Schema::connection($this->connection->getName());
$dataset = $this->datasets->get(self::DATASET);
return $schema->hasTable('business_mirror_table_states')
&& $schema->hasTable($dataset->targetTable());
} catch (Throwable) {
return false;
}
}
public function ready(): bool
{
try {
return $this->mirror->ready($this->datasets->get(self::DATASET));
} catch (Throwable) {
return false;
}
}
/** @return array<string,mixed>|null */
public function state(): ?array
{
try {
return $this->mirror->state($this->datasets->get(self::DATASET));
} catch (Throwable) {
return null;
}
}
/** @return array{code:string,last_succeeded_at:?string,lag_minutes:?int} */
public function syncStatus(?array $state = null): array
{
$state ??= $this->state();
if (! is_array($state)) {
return ['code' => 'unavailable', 'last_succeeded_at' => null, 'lag_minutes' => null];
}
$lastSucceeded = $state['last_succeeded_at'] ?? null;
$lagMinutes = null;
if (is_string($lastSucceeded) && $lastSucceeded !== '') {
try {
$lagMinutes = (int) ceil(max(0, now()->getTimestamp() - Carbon::parse($lastSucceeded)->getTimestamp()) / 60);
} catch (Throwable) {
$lagMinutes = null;
}
}
$status = (string) ($state['status'] ?? 'pending');
if (($state['initial_completed_at'] ?? null) === null) {
$code = $status === 'failed' ? 'initial_failed' : ($status === 'running' ? 'initializing' : 'pending');
} elseif ($status === 'failed') {
$code = 'failed';
} elseif ($status === 'running') {
$code = 'syncing';
} elseif ($lagMinutes === null || $lagMinutes > max((int) config('upstream_mirrors.courier_information.stale_after_minutes', 60), 1)) {
$code = 'stale';
} else {
$code = 'fresh';
}
return ['code' => $code, 'last_succeeded_at' => $lastSucceeded, 'lag_minutes' => $lagMinutes];
}
/**
* @param array<string,string> $filters
* @return array{data:list<array<string,mixed>>,total:int,summary:array<string,float|int>,state:array<string,mixed>|null,sync:array<string,mixed>}
*/
public function paginate(array $filters, int $page, int $perPage): array
{
$dataset = $this->datasets->get(self::DATASET);
if (! $this->mirror->ready($dataset)) {
throw new RuntimeException('courier_freight_reconciliation_mirror_not_ready');
}
$page = max($page, 1);
$perPage = min(max($perPage, 1), 100);
$query = $this->filteredQuery($dataset->targetTable(), $filters);
$total = (clone $query)->count();
$summary = (clone $query)->selectRaw(
'COUNT(*) AS aggregate_count, '
. 'COALESCE(SUM(payable_amount), 0) AS payable_amount, '
. 'COALESCE(SUM(actual_payment), 0) AS actual_payment, '
. 'COALESCE(SUM(confirmed_amount), 0) AS confirmed_amount, '
. 'COALESCE(SUM(reconciliation_difference), 0) AS reconciliation_difference'
)->first();
$rows = (clone $query)->select(self::COLUMNS)
->orderByDesc('Id')
->forPage($page, $perPage)
->get()
->map(fn (object $row): array => $this->mapRow($row))
->all();
$state = $this->mirror->state($dataset);
return [
'data' => $rows,
'total' => $total,
'summary' => [
'count' => (int) ($summary->aggregate_count ?? 0),
'payable_amount' => (float) ($summary->payable_amount ?? 0),
'actual_payment' => (float) ($summary->actual_payment ?? 0),
'confirmed_amount' => (float) ($summary->confirmed_amount ?? 0),
'reconciliation_difference' => (float) ($summary->reconciliation_difference ?? 0),
],
'state' => $state,
'sync' => $this->syncStatus($state),
];
}
private function filteredQuery(string $table, array $filters): Builder
{
$query = $this->connection->table($table)
->where(static function (Builder $query): void {
$query->whereNull('is_delete')->orWhere('is_delete', '=', 0);
});
$this->containsFilter($query, 'waybill', $filters['express_no'] ?? '');
$this->containsFilter($query, 'expressname', $filters['express_name'] ?? '');
if (($filters['month'] ?? '') !== '') {
$query->where('month', '=', $filters['month']);
}
if (($filters['status'] ?? '') !== '') {
$query->where('status', '=', $filters['status']);
}
if (($filters['modified_from'] ?? '') !== '') {
$query->where('modify_time', '>=', $filters['modified_from'] . ' 00:00:00');
}
if (($filters['modified_to'] ?? '') !== '') {
$query->where('modify_time', '<', date('Y-m-d 00:00:00', strtotime($filters['modified_to'] . ' +1 day')));
}
return $query;
}
private function containsFilter(Builder $query, string $column, mixed $value): void
{
$value = trim((string) $value);
if ($value === '') {
return;
}
$escaped = str_replace(['!', '%', '_'], ['!!', '!%', '!_'], $value);
$query->whereRaw($column . " LIKE ? ESCAPE '!'", ['%' . $escaped . '%']);
}
/** @return array<string,mixed> */
private function mapRow(object $row): array
{
$mapped = [];
foreach (self::COLUMNS as $column) {
$mapped[$column] = $row->{$column} ?? null;
}
foreach (['opening_balance', 'courier_provided_amount', 'reconciliation_difference', 'confirmed_amount', 'advance_payment', 'payable_amount', 'actual_payment', 'compensation_amount', 'other_replenishment', 'corporate_payment_amount', 'personal_payment_amount'] as $amount) {
$mapped[$amount] = $mapped[$amount] === null ? null : (float) $mapped[$amount];
}
return $mapped;
}
}