CaiWuXiTong/modules/EcommerceFinance/Repositories/CourierInformationMirrorRepository.php

390 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Modules\EcommerceFinance\Repositories;
use Carbon\CarbonInterface;
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;
/**
* 工作平台 2.0 的快递资料只读镜像。
*
* 这里不直接连接 JianSun;所有查询都落在 caiwu_business_mirror 中,
* 由 business-mirror:sync-tables 负责生成和刷新镜像表。
*/
class CourierInformationMirrorRepository
{
public const DATASET_KEY = 'jiansun.dbo.wp_express';
public const NETWORK_DATASET_KEY = 'jiansun.dbo.wp_express_expNet';
public const ACCOUNT_DATASET_KEY = 'jiansun.dbo.wp_express_account';
public const LOG_DATASET_KEY = 'jiansun.dbo.wp_express_extension';
private readonly ConnectionInterface $connection;
public function __construct(
private readonly BusinessMirrorDatasetRegistry $datasets,
private readonly GenericBusinessMirrorRepository $mirror,
) {
$this->connection = $mirror->connection();
}
public function available(): bool
{
$schema = Schema::connection($this->connection->getName());
return $schema->hasTable('business_mirror_table_states')
&& $schema->hasTable($this->dataset()->targetTable());
}
public function ready(): bool
{
return $this->mirror->ready($this->dataset());
}
/** @return array<string,mixed>|null */
public function state(): ?array
{
return $this->mirror->state($this->dataset());
}
/**
* @param array<string,mixed>|null $state
* @return array{code:string,lag_minutes:?int}
*/
public function syncStatus(?array $state = null, ?CarbonInterface $at = null): array
{
$state ??= $this->state();
if (! is_array($state)) {
return ['code' => 'unavailable', 'lag_minutes' => null];
}
$lastSucceeded = $this->timestamp($state['last_succeeded_at'] ?? null);
$lagMinutes = $lastSucceeded === null
? null
: (int) ceil(max(0, ($at ?? now())->getTimestamp() - $lastSucceeded) / 60);
$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 = ($state['run_mode'] ?? null) === 'full' ? 'reconciling' : 'syncing';
} elseif ($lagMinutes === null
|| $lagMinutes > max((int) config('upstream_mirrors.courier_information.stale_after_minutes', 60), 1)) {
$code = 'delayed';
} else {
$code = 'healthy';
}
return ['code' => $code, 'lag_minutes' => $lagMinutes];
}
/**
* @param array<string,string> $filters
* @return array{data:list<object>,total:int}
*/
public function paginate(array $filters, int $page, int $perPage): array
{
if (! $this->ready()) {
throw new RuntimeException('business_mirror_dataset_not_ready');
}
$page = max($page, 1);
$perPage = min(max($perPage, 1), 100);
$query = $this->filteredExpressQuery($filters);
$total = (clone $query)->count();
$rows = $query->orderBy('Id')
->forPage($page, $perPage)
->get()
->all();
return ['data' => $rows, 'total' => $total];
}
/**
* 快递资料中的去重快递公司名称,按资料 Id 的首次出现顺序返回。
*
* @return list<string>
*/
public function names(): array
{
if (! $this->ready()) {
throw new RuntimeException('business_mirror_dataset_not_ready');
}
return $this->connection
->table($this->dataset()->targetTable())
->select(['Id', 'expname'])
->whereNotNull('expname')
->orderBy('Id')
->get()
->map(static fn ($row): string => trim((string) ($row->expname ?? '')))
->filter(static fn (string $name): bool => $name !== '')
->unique()
->values()
->all();
}
/**
* @param array<string,string> $filters
* @return \Generator<int,object>
*/
public function exportRows(array $filters, int $batchSize = 500): \Generator
{
if (! $this->ready()) {
throw new RuntimeException('business_mirror_dataset_not_ready');
}
$batchSize = min(max($batchSize, 1), 5000);
$lastId = null;
while (true) {
$query = $this->filteredExpressQuery($filters);
if ($lastId !== null) {
$query->where('Id', '>', $lastId);
}
$rows = $query->orderBy('Id')->limit($batchSize)->get()->all();
if ($rows === []) {
return;
}
foreach ($rows as $row) {
yield $row;
}
$lastId = (int) $rows[array_key_last($rows)]->Id;
if (count($rows) < $batchSize) {
return;
}
}
}
/**
* 网点标签数据。快递编码为空时返回全部网点;传入编码时只返回对应网点。
* @return array{data:list<object>,total:int}
*/
public function paginateNetworks(?string $expcode, int $page, int $perPage): array
{
$dataset = $this->datasets->get(self::NETWORK_DATASET_KEY);
if (! $this->mirror->ready($dataset)) {
throw new RuntimeException('business_mirror_dataset_not_ready');
}
$query = $this->connection->table($dataset->targetTable())->select([
'id', 'expid', 'expcode', 'netcode', 'netno', 'netname', 'isbill',
'province', 'city', 'area', 'town', 'address', 'linker', 'linkermobile',
'status', 'apitype', 'apipltmname', 'created', 'expnum',
]);
if ($expcode !== null && $expcode !== '') {
$query->where('expcode', $expcode);
}
$page = max($page, 1);
$perPage = min(max($perPage, 1), 100);
$total = (clone $query)->count();
return [
'data' => $query->orderByDesc('id')->forPage($page, $perPage)->get()->all(),
'total' => $total,
];
}
/**
* 打款账户标签数据。
* @return array{data:list<object>,total:int}
*/
public function paginateAccounts(?string $expcode, int $page, int $perPage): array
{
$dataset = $this->datasets->get(self::ACCOUNT_DATASET_KEY);
if (! $this->mirror->ready($dataset)) {
throw new RuntimeException('business_mirror_dataset_not_ready');
}
$query = $this->connection->table($dataset->targetTable())->select([
'Id', 'expcode', 'expname', 'accountType', 'account', 'accountName',
'accountkhh', 'createTime', 'invoice', 'erpId', 'erpAccountId',
]);
if ($expcode !== null && $expcode !== '') {
$query->where('expcode', $expcode);
}
$page = max($page, 1);
$perPage = min(max($perPage, 1), 100);
$total = (clone $query)->count();
return [
'data' => $query->orderByDesc('Id')->forPage($page, $perPage)->get()->all(),
'total' => $total,
];
}
/**
* 返回指定快递名称对应的电子签章经办人账号。
*
* 工作平台将该值保存在 wp_express.dzqzjbr 中,不能用联系人电话
* 代替;两者在部分快递资料中可能不同。镜像未完成首次同步时返回空
* 数组,避免影响协议签署页面的正常打开。
*
* @param list<string> $expnames
* @return array<string,string>
*/
public function signingAccountsByName(array $expnames): array
{
if (! $this->ready()) {
return [];
}
$expnames = array_values(array_unique(array_filter(array_map(
static fn ($name): string => trim((string) $name),
$expnames,
), static fn (string $name): bool => $name !== '')));
if ($expnames === []) {
return [];
}
return $this->connection
->table($this->dataset()->targetTable())
->select(['expname', 'dzqzjbr', 'Id'])
->whereIn('expname', $expnames)
->orderBy('Id')
->get()
->groupBy(static fn ($row): string => (string) ($row->expname ?? ''))
->map(static function ($rows): string {
foreach ($rows as $row) {
$account = trim((string) ($row->dzqzjbr ?? ''));
if ($account !== '') {
return $account;
}
}
return '';
})
->filter(static fn (string $account): bool => $account !== '')
->all();
}
/**
* 快递资料操作日志。
* @return array{data:list<object>,total:int}
*/
public function paginateLogs(?string $expcode, int $page, int $perPage): array
{
$dataset = $this->datasets->get(self::LOG_DATASET_KEY);
if (! $this->mirror->ready($dataset)) {
throw new RuntimeException('business_mirror_dataset_not_ready');
}
$query = $this->connection->table($dataset->targetTable())->select([
'Id', 'expcode', 'doTime', 'createTime', 'modifyTime',
]);
if ($expcode !== null && $expcode !== '') {
$query->where('expcode', $expcode);
}
$page = max($page, 1);
$perPage = min(max($perPage, 1), 100);
$total = (clone $query)->count();
return [
'data' => $query->orderByDesc('Id')->forPage($page, $perPage)->get()->all(),
'total' => $total,
];
}
/** @return array{main:bool,networks:bool,accounts:bool,logs:bool,network_ready:bool,account_ready:bool,log_ready:bool} */
public function relatedAvailability(): array
{
try {
$schema = Schema::connection($this->connection->getName());
$networkDataset = $this->datasets->get(self::NETWORK_DATASET_KEY);
$accountDataset = $this->datasets->get(self::ACCOUNT_DATASET_KEY);
$logDataset = $this->datasets->get(self::LOG_DATASET_KEY);
return [
'main' => $schema->hasTable($this->dataset()->targetTable()),
'networks' => $schema->hasTable($networkDataset->targetTable()),
'accounts' => $schema->hasTable($accountDataset->targetTable()),
'logs' => $schema->hasTable($logDataset->targetTable()),
'network_ready' => $this->mirror->ready($networkDataset),
'account_ready' => $this->mirror->ready($accountDataset),
'log_ready' => $this->mirror->ready($logDataset),
];
} catch (Throwable) {
return ['main' => false, 'networks' => false, 'accounts' => false, 'logs' => false, 'network_ready' => false, 'account_ready' => false, 'log_ready' => false];
}
}
/** @param array<string,string> $filters */
private function filteredExpressQuery(array $filters): Builder
{
$query = $this->connection->table($this->dataset()->targetTable())->select([
'Id', 'expcode', 'expname', 'outcode', 'scexpcode', 'status', 'paymethod',
'addvalidsgn', 'kuaiyunsgn', 'autonosgn', 'packnosgn', 'skucountsplitsgn',
'weight0sgn', 'stopdate', 'tradebtime', 'tradeetime', 'signcom', 'signmode',
'linker', 'linkmobile', 'paytype', 'undefined1', 'undefined2', 'undefined3',
'volume0sgn', 'handnosgn', 'decryptsgn', 'pj1sgn', 'undefinedtype',
'undefined4', 'undefined5', 'dzqzjbr', 'guid', 'is_delete', 'exp_properties',
'modified',
]);
if (($filters['expcode'] ?? '') !== '') {
$query->where('expcode', $filters['expcode']);
}
if (($filters['expname'] ?? '') !== '') {
$query->whereRaw("expname LIKE ? ESCAPE '!'", [$this->contains($filters['expname'])]);
}
foreach (['undefined1', 'undefined2', 'undefined3'] as $column) {
if (($filters[$column] ?? '') !== '') {
$query->whereRaw($column . " LIKE ? ESCAPE '!'", [$this->contains($filters[$column])]);
}
}
if (($filters['tradebtime_from'] ?? '') !== '') {
$query->whereDate('tradebtime', '>=', $filters['tradebtime_from']);
}
if (($filters['tradebtime_to'] ?? '') !== '') {
$query->whereDate('tradebtime', '<=', $filters['tradebtime_to']);
}
if (($filters['status'] ?? '') !== '') {
$query->where('status', (int) $filters['status']);
}
return $query;
}
private function dataset(): \Modules\EcommerceFinance\Contracts\BusinessMirrorDataset
{
return $this->datasets->get(self::DATASET_KEY);
}
private function contains(string $value): string
{
return '%' . str_replace(['!', '%', '_'], ['!!', '!%', '!_'], trim($value)) . '%';
}
private function timestamp(mixed $value): ?int
{
if (! is_string($value) || $value === '') {
return null;
}
try {
return Carbon::parse($value)->getTimestamp();
} catch (Throwable) {
return null;
}
}
}