199 lines
7.6 KiB
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;
|
|
}
|
|
}
|