200 lines
7.5 KiB
PHP
200 lines
7.5 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Repositories;
|
||
|
||
use Illuminate\Database\ConnectionInterface;
|
||
use Illuminate\Support\Facades\Schema;
|
||
use RuntimeException;
|
||
|
||
class DingPayableWorkflowMirrorRepository
|
||
{
|
||
public const DATASET_KEY = 'operations_replica.rpt.ding_payable_workflows';
|
||
|
||
public const PROCESS_CODE = 'PROC-39DD70DE-DAB2-4A5F-B8F1-0B0B68BD0C88';
|
||
|
||
private readonly ConnectionInterface $connection;
|
||
|
||
public function __construct(GenericBusinessMirrorRepository $mirror)
|
||
{
|
||
$this->connection = $mirror->connection();
|
||
}
|
||
|
||
public function ready(): bool
|
||
{
|
||
$schema = Schema::connection($this->connection->getName());
|
||
if (! $schema->hasTable('ding_payable_workflow_mirror') || ! $schema->hasTable('business_mirror_table_states')) {
|
||
return false;
|
||
}
|
||
|
||
$state = $this->connection->table('business_mirror_table_states')
|
||
->where('dataset_key', self::DATASET_KEY)->first();
|
||
|
||
return $state !== null && $state->initial_completed_at !== null;
|
||
}
|
||
|
||
/** @return array{data:list<array<string,mixed>>,total:int,status:string,last_synced_at:?string} */
|
||
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);
|
||
$state = $this->connection->table('business_mirror_table_states')->where('dataset_key', self::DATASET_KEY)->first();
|
||
// 先在镜像库按固定业务关键词缩小候选流程,再解析 JSON;避免为每次页面请求
|
||
// 解码全部历史表单。关键词来自本任务的固定业务筛选,不是用户拼接 SQL。
|
||
// “仓”覆盖江西仓、重庆仓等实际用途写法;后续仍在解析明细时
|
||
// 同时校验类型和车费,避免仅因其他字段出现“仓”而误收流程。
|
||
$records = $this->connection->table('ding_payable_workflow_mirror')
|
||
->where('scheme_payload', 'like', '%快递运费%')
|
||
->where('scheme_payload', 'like', '%仓%')
|
||
->where('scheme_payload', 'like', '%车费%')
|
||
->orderByDesc('workflow_created_at')->orderByDesc('process_instance_id')->get();
|
||
$filtered = [];
|
||
|
||
foreach ($records as $record) {
|
||
$workflow = $this->decode((string) $record->workflow_payload, []);
|
||
if ((string) ($workflow['processCode'] ?? '') !== self::PROCESS_CODE) {
|
||
continue;
|
||
}
|
||
if (($filters['process_instance_id'] ?? '') !== ''
|
||
&& ! str_contains((string) ($workflow['processInstanceId'] ?? ''), (string) $filters['process_instance_id'])
|
||
&& ! str_contains((string) ($workflow['businessId'] ?? ''), (string) $filters['process_instance_id'])) {
|
||
continue;
|
||
}
|
||
if (! $this->matchesWorkflowFilters($workflow, $filters)) {
|
||
continue;
|
||
}
|
||
|
||
$scheme = $this->decode((string) $record->scheme_payload, []);
|
||
$forms = $this->formFields($scheme);
|
||
foreach ($this->detailRows($forms['应付明细'] ?? []) as $detail) {
|
||
$type = trim((string) ($detail['类型'] ?? ''));
|
||
$purpose = trim((string) ($detail['用途'] ?? ''));
|
||
if ($type !== '快递运费' || ! str_contains($purpose, '仓') || ! str_contains($purpose, '车费')) {
|
||
continue;
|
||
}
|
||
|
||
$filtered[] = [
|
||
'process_instance_id' => (string) ($workflow['processInstanceId'] ?? $record->process_instance_id),
|
||
'workflow' => $workflow,
|
||
'fields' => $forms,
|
||
'detail' => $detail,
|
||
'tasks' => $this->decode((string) ($record->task_payload ?? '[]'), []),
|
||
];
|
||
}
|
||
}
|
||
|
||
$total = count($filtered);
|
||
$data = array_slice($filtered, ($page - 1) * $perPage, $perPage);
|
||
|
||
return [
|
||
'data' => $data,
|
||
'total' => $total,
|
||
'status' => (string) ($state->status ?? 'unknown'),
|
||
'last_synced_at' => $state->last_succeeded_at ? (string) $state->last_succeeded_at : null,
|
||
];
|
||
}
|
||
|
||
/** @param array<string,mixed> $workflow @param array<string,mixed> $filters */
|
||
private function matchesWorkflowFilters(array $workflow, array $filters): bool
|
||
{
|
||
$createdFrom = trim((string) ($filters['created_from'] ?? ''));
|
||
if ($createdFrom !== '') {
|
||
$createdAt = $this->workflowDate($workflow['createAt'] ?? null);
|
||
if ($createdAt === null || $createdAt < $createdFrom) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
$finishedTo = trim((string) ($filters['finished_to'] ?? ''));
|
||
if ($finishedTo !== '') {
|
||
$finishTime = $this->workflowDate($workflow['finishTime'] ?? null);
|
||
if ($finishTime === null || $finishTime > $finishedTo) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
$requestedStatus = strtoupper(trim((string) ($filters['status'] ?? '')));
|
||
if ($requestedStatus !== '') {
|
||
$actualStatus = strtoupper(trim((string) ($workflow['status'] ?? '')));
|
||
$statusMatches = $requestedStatus === 'CANCELED'
|
||
? in_array($actualStatus, ['CANCELED', 'CANCELLED'], true)
|
||
: $actualStatus === $requestedStatus;
|
||
if (! $statusMatches) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private function workflowDate(mixed $value): ?string
|
||
{
|
||
$date = substr(trim((string) ($value ?? '')), 0, 10);
|
||
|
||
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) === 1 ? $date : null;
|
||
}
|
||
|
||
/** @return array<string,mixed> */
|
||
private function formFields(array $rows): array
|
||
{
|
||
$fields = [];
|
||
foreach ($rows as $row) {
|
||
if (! is_array($row)) {
|
||
continue;
|
||
}
|
||
$name = trim((string) ($row['name'] ?? ''));
|
||
if ($name === '') {
|
||
continue;
|
||
}
|
||
$value = $this->decode((string) ($row['value'] ?? ''), $row['value'] ?? null);
|
||
$fields[$name] = $value;
|
||
}
|
||
return $fields;
|
||
}
|
||
|
||
/** @return list<array<string,mixed>> */
|
||
private function detailRows(mixed $value): array
|
||
{
|
||
if (! is_array($value)) {
|
||
return [];
|
||
}
|
||
$details = [];
|
||
foreach ($value as $row) {
|
||
if (! is_array($row) || ! isset($row['rowValue']) || ! is_array($row['rowValue'])) {
|
||
continue;
|
||
}
|
||
$detail = [];
|
||
foreach ($row['rowValue'] as $field) {
|
||
if (! is_array($field) || ! isset($field['label'])) {
|
||
continue;
|
||
}
|
||
$detail[(string) $field['label']] = $this->scalarValue($field['value'] ?? null);
|
||
}
|
||
if ($detail !== []) {
|
||
$details[] = $detail;
|
||
}
|
||
}
|
||
return $details;
|
||
}
|
||
|
||
private function scalarValue(mixed $value): mixed
|
||
{
|
||
if (is_scalar($value) || $value === null) {
|
||
return $value;
|
||
}
|
||
return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
}
|
||
|
||
private function decode(string $json, mixed $fallback): mixed
|
||
{
|
||
if ($json === '') {
|
||
return $fallback;
|
||
}
|
||
$decoded = json_decode($json, true);
|
||
return json_last_error() === JSON_ERROR_NONE ? $decoded : $fallback;
|
||
}
|
||
}
|