CaiWuXiTong/modules/EcommerceFinance/Repositories/DingPayableWorkflowMirrorRepository.php

248 lines
8.9 KiB
PHP
Raw Permalink 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\Support\Facades\Schema;
use Modules\EcommerceFinance\Services\DingPayableWorkflowStatus;
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,
];
}
/**
* 校验某个审批实例里是否存在该附件,并返回附件元数据(下载接口使用,
* 防止拿任意 fileId 越权取钉盘文件)。
*
* @return array{fileName:string,fileType:string,fileSize:int}|null
*/
public function attachment(string $processInstanceId, string $fileId): ?array
{
if ($processInstanceId === '' || $fileId === '') {
return null;
}
$record = $this->connection->table('ding_payable_workflow_mirror')
->where('process_instance_id', $processInstanceId)
->first();
if ($record === null) {
return null;
}
$scheme = $this->decode((string) $record->scheme_payload, []);
$forms = is_array($scheme) ? $this->formFields($scheme) : [];
$files = [];
foreach (['附件', '未签字版合同附件或其他明细附件'] as $key) {
$value = $forms[$key] ?? null;
if (! is_array($value)) {
continue;
}
foreach ($value as $file) {
if (is_array($file)) {
$files[] = $file;
}
}
}
foreach ($files as $file) {
if ((string) ($file['fileId'] ?? '') !== $fileId) {
continue;
}
return [
'fileName' => trim((string) ($file['fileName'] ?? '')) ?: $fileId,
'fileType' => trim((string) ($file['fileType'] ?? '')),
'fileSize' => (int) ($file['fileSize'] ?? 0),
];
}
return 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;
}
}
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<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;
}
}