CaiWuXiTong/modules/EcommerceFinance/Repositories/WorkOrderMirrorRepository.php

371 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 Illuminate\Database\ConnectionInterface;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Carbon;
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
use Throwable;
/** 只读聚水潭协同工单镜像;HTTP 请求不得回退直连上游。 */
class WorkOrderMirrorRepository
{
public const DEFAULT_WORK_TYPE_NAME = '仓内未发';
public const MAIN_DATASET = 'operations_replica.rpt.work_order_main';
public const SUB_DATASET = 'operations_replica.rpt.work_order_sub';
public const CONFIG_DATASET = 'operations_replica.rpt.work_order_form_config';
public const VALUE_DATASET = 'operations_replica.rpt.work_order_form_value';
private const DATASETS = [
'main' => self::MAIN_DATASET,
'sub' => self::SUB_DATASET,
'config' => self::CONFIG_DATASET,
'values' => self::VALUE_DATASET,
];
private readonly ConnectionInterface $connection;
public function __construct(
private readonly BusinessMirrorDatasetRegistry $datasets,
private readonly GenericBusinessMirrorRepository $mirror,
) {
$this->connection = $mirror->connection();
}
/**
* @return array{
* ready:bool,
* code:string,
* last_succeeded_at:?string,
* datasets:array<string,array{dataset:string,ready:bool,status:string,last_succeeded_at:?string}>
* }
*/
public function syncStatus(): array
{
$ready = true;
$lastSucceeded = [];
$states = [];
foreach (self::DATASETS as $name => $key) {
try {
$dataset = $this->datasets->get($key);
$state = $this->mirror->state($dataset);
$datasetReady = $this->mirror->ready($dataset);
} catch (Throwable) {
$state = null;
$datasetReady = false;
}
$last = $this->dateValue($state['last_succeeded_at'] ?? null);
if ($last !== null) {
$lastSucceeded[] = $last;
}
$states[$name] = [
'dataset' => $key,
'ready' => $datasetReady,
'status' => (string) ($state['status'] ?? 'pending'),
'last_succeeded_at' => $last,
];
$ready = $ready && $datasetReady;
}
return [
'ready' => $ready,
'code' => $ready ? 'ready' : 'initial_sync_pending',
// The oldest successful component is the freshness of the joined result.
'last_succeeded_at' => $lastSucceeded === [] ? null : min($lastSucceeded),
'datasets' => $states,
];
}
/**
* @param array<string,string> $filters
* @return array{data:list<array<string,mixed>>,total:int,page:int,per_page:int,last_page:int,ready:bool,sync_status:array<string,mixed>}
*/
public function paginate(int $page, int $perPage, array $filters = []): array
{
$page = max($page, 1);
$perPage = min(max($perPage, 1), 100);
$syncStatus = $this->syncStatus();
if (! $syncStatus['ready']) {
return [
'data' => [],
'total' => 0,
'page' => $page,
'per_page' => $perPage,
'last_page' => 1,
'ready' => false,
'sync_status' => $syncStatus,
];
}
$main = $this->datasets->get(self::MAIN_DATASET)->targetTable();
$config = $this->datasets->get(self::CONFIG_DATASET)->targetTable();
$latestConfigs = $this->connection->table($config . ' AS config_versions')
->selectRaw('work_type, MAX(id) AS id')
->groupBy('work_type');
$query = $this->connection->table($main . ' AS m')
->joinSub($latestConfigs, 'config_versions', static function ($join): void {
$join->on('config_versions.work_type', '=', 'm.work_type');
})
->join($config . ' AS c', 'c.id', '=', 'config_versions.id')
->where('c.work_type_name', self::DEFAULT_WORK_TYPE_NAME)
->where(static function (Builder $query): void {
$query->whereNull('m.is_deleted')->orWhere('m.is_deleted', 0);
});
$this->applyFilters($query, $filters);
$total = (clone $query)->count();
$rows = $query
->select([
'm.id', 'm.work_type', 'c.work_type_name', 'c.group_name',
'm.order_no', 'm.title', 'm.status', 'm.create_user',
'm.expect_minute', 'm.submit_time', 'm.expect_end_time',
'm.end_time', 'm.score', 'm.update_user', 'm.description',
'm.create_time', 'm.update_time',
])
->orderByDesc('m.create_time')
->orderByDesc('m.id')
->forPage($page, $perPage)
->get()
->map(static fn (object $row): array => (array) $row)
->all();
$this->hydrateRelatedRows($rows);
return [
'data' => $rows,
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'last_page' => max((int) ceil($total / $perPage), 1),
'ready' => true,
'sync_status' => $syncStatus,
];
}
/**
* Return after-sales alert work orders for the after-sales expense page.
* The work-order values describe an operational alert; they are not added
* to the order/refund monetary columns.
*
* @param array<string,string> $filters
* @return array{data:list<array<string,mixed>>,total:int,page:int,per_page:int,last_page:int,ready:bool,sync_status:array<string,mixed>}
*/
public function paginateAfterSales(int $page, int $perPage, array $filters = []): array
{
$page = max($page, 1);
$perPage = min(max($perPage, 1), 50);
$syncStatus = $this->syncStatus();
if (! $syncStatus['ready']) {
return [
'data' => [],
'total' => 0,
'page' => $page,
'per_page' => $perPage,
'last_page' => 1,
'ready' => false,
'sync_status' => $syncStatus,
];
}
$main = $this->datasets->get(self::MAIN_DATASET)->targetTable();
$config = $this->datasets->get(self::CONFIG_DATASET)->targetTable();
$latestConfigs = $this->connection->table($config . ' AS config_versions')
->selectRaw('work_type, MAX(id) AS id')
->groupBy('work_type');
$query = $this->connection->table($main . ' AS m')
->joinSub($latestConfigs, 'config_versions', static function ($join): void {
$join->on('config_versions.work_type', '=', 'm.work_type');
})
->join($config . ' AS c', 'c.id', '=', 'config_versions.id')
->where('c.work_type_name', 'like', '%售后%')
->where(static function (Builder $active): void {
$active->whereNull('m.is_deleted')->orWhere('m.is_deleted', 0);
});
$this->applyAfterSalesFilters($query, $filters);
$total = (clone $query)->count();
$rows = $query
->select([
'm.id', 'm.work_type', 'c.work_type_name', 'c.group_name',
'm.order_no', 'm.title', 'm.status', 'm.create_user',
'm.submit_time', 'm.expect_end_time', 'm.end_time',
'm.description', 'm.create_time', 'm.update_time',
])
->orderByDesc('m.create_time')
->orderByDesc('m.id')
->forPage($page, $perPage)
->get()
->map(static fn (object $row): array => (array) $row)
->all();
$this->hydrateRelatedRows($rows);
return [
'data' => $rows,
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'last_page' => max((int) ceil($total / $perPage), 1),
'ready' => true,
'sync_status' => $syncStatus,
];
}
/** @param array<string,string> $filters */
private function applyFilters(Builder $query, array $filters): void
{
if (($filters['work_type'] ?? '') !== '') {
$query->where('m.work_type', $filters['work_type']);
}
if (($filters['order_no'] ?? '') !== '') {
$query->whereRaw("m.order_no LIKE ? ESCAPE '!'", [$this->contains($filters['order_no'])]);
}
if (($filters['status'] ?? '') !== '') {
$query->where('m.status', $filters['status']);
}
if (($filters['create_user'] ?? '') !== '') {
$query->where('m.create_user', $filters['create_user']);
}
if (($filters['created_from'] ?? '') !== '') {
$query->where('m.create_time', '>=', $filters['created_from'] . ' 00:00:00');
}
if (($filters['created_to'] ?? '') !== '') {
$query->where('m.create_time', '<', Carbon::parse($filters['created_to'])->addDay()->toDateString());
}
if (($filters['assign_user'] ?? '') !== '') {
$subTable = $this->datasets->get(self::SUB_DATASET)->targetTable();
$assignUser = $filters['assign_user'];
$query->whereExists(static function (Builder $subQuery) use ($subTable, $assignUser): void {
$subQuery->selectRaw('1')
->from($subTable . ' AS assigned_sub')
->whereColumn('assigned_sub.main_order_no', 'm.order_no')
->where('assigned_sub.assign_user', $assignUser)
->where(static function (Builder $active): void {
$active->whereNull('assigned_sub.is_deleted')->orWhere('assigned_sub.is_deleted', 0);
});
});
}
}
/** @param array<string,string> $filters */
private function applyAfterSalesFilters(Builder $query, array $filters): void
{
if (($filters['from'] ?? '') !== '') {
$query->where('m.create_time', '>=', $filters['from'] . ' 00:00:00');
}
if (($filters['to'] ?? '') !== '') {
$query->where('m.create_time', '<', Carbon::parse($filters['to'])->addDay()->toDateString());
}
if (($filters['order_no'] ?? '') !== '') {
$query->whereRaw("m.order_no LIKE ? ESCAPE '!'", [$this->contains($filters['order_no'])]);
}
if (($filters['shop_name'] ?? '') !== '') {
// Older Tmall/JD alert forms store the shop text in a generic
// question/field_1 value instead of the explicit shop_name key.
$this->whereFormValueLike($query, ['shop_name', 'question', 'field_1'], $filters['shop_name']);
}
if (($filters['sku_id'] ?? '') !== '') {
$this->whereFormValueLike($query, ['goods_id', 'id', 'question', 'field_1'], $filters['sku_id']);
}
}
/** @param list<string> $fieldCodes */
private function whereFormValueLike(Builder $query, array $fieldCodes, string $value): void
{
$valueTable = $this->datasets->get(self::VALUE_DATASET)->targetTable();
$query->whereExists(function (Builder $valueQuery) use ($valueTable, $fieldCodes, $value): void {
$valueQuery->selectRaw('1')
->from($valueTable . ' AS filter_values')
->whereColumn('filter_values.main_order_no', 'm.order_no')
->whereIn('filter_values.field_code', $fieldCodes)
->whereRaw("filter_values.field_value LIKE ? ESCAPE '!'", [$this->contains($value)])
->where(static function (Builder $active): void {
$active->whereNull('filter_values.is_deleted')->orWhere('filter_values.is_deleted', 0);
});
});
}
/** @param list<array<string,mixed>> $rows */
private function hydrateRelatedRows(array &$rows): void
{
$orderNumbers = array_values(array_unique(array_filter(array_map(
static fn (array $row): string => trim((string) ($row['order_no'] ?? '')),
$rows,
))));
if ($orderNumbers === []) {
return;
}
$subTable = $this->datasets->get(self::SUB_DATASET)->targetTable();
$subOrders = $this->connection->table($subTable)
->select([
'id', 'sub_order_no', 'main_order_no', 'title', 'assign_user',
'status', 'expect_minute', 'submit_time', 'expect_end_time',
'finish_time', 'content', 'notify_submit', 'notify_soon',
'notify_overdue', 'overdue_notify_count', 'create_time',
'update_time', 'taskid',
])
->whereIn('main_order_no', $orderNumbers)
->where(static function (Builder $query): void {
$query->whereNull('is_deleted')->orWhere('is_deleted', 0);
})
->orderBy('id')
->get()
->groupBy(static fn (object $row): string => (string) $row->main_order_no);
$valueTable = $this->datasets->get(self::VALUE_DATASET)->targetTable();
$formValues = $this->connection->table($valueTable)
->select([
'id', 'main_order_no', 'field_code', 'field_name', 'field_value',
'field_type', 'sort', 'create_time',
])
->whereIn('main_order_no', $orderNumbers)
->where(static function (Builder $query): void {
$query->whereNull('is_deleted')->orWhere('is_deleted', 0);
})
->orderBy('sort')
->orderBy('id')
->get()
->groupBy(static fn (object $row): string => (string) $row->main_order_no);
foreach ($rows as &$row) {
$orderNumber = (string) ($row['order_no'] ?? '');
$row['sub_orders'] = $subOrders->get($orderNumber, collect())
->map(static fn (object $subOrder): array => (array) $subOrder)
->values()
->all();
$row['form_values'] = $formValues->get($orderNumber, collect())
->map(static fn (object $value): array => (array) $value)
->values()
->all();
}
unset($row);
}
private function contains(string $value): string
{
return '%' . str_replace(['!', '%', '_'], ['!!', '!%', '!_'], $value) . '%';
}
private function dateValue(mixed $value): ?string
{
if ($value === null || $value === '') {
return null;
}
return $value instanceof \DateTimeInterface
? $value->format('Y-m-d H:i:s')
: (string) $value;
}
}