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>,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 $workflow @param array $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; } } if (! DingPayableWorkflowStatus::matchesFilter( $workflow, (string) ($filters['status'] ?? ''), )) { 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 */ 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> */ 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; } }