'待 ERP 开通', 'both' => '入驻 + 工作平台', 'onboarding' => '只有入驻', 'platform' => '只在工作平台(缺入驻资料)', 'mismatch' => '账户对不上', ]; private ?Collection $onboardingCache = null; public function __construct( private readonly CourierInformationMirrorRepository $mirror, private readonly CourierRequoteService $requotes, ) { } /** * @return array{rows:LengthAwarePaginator,counts:array,platform_ready:bool} */ public function page(array $filters, int $page, int $perPage): array { $all = $this->rows(); $platformReady = $all['platform_ready']; $rows = collect($all['rows']); $counts = [ 'all' => $rows->count(), 'both' => $rows->where('source', 'both')->count(), 'platform' => $rows->where('source', 'platform')->count(), 'onboarding' => $rows->where('source', 'onboarding')->count(), 'mismatch' => $rows->where('account_status', 'mismatch')->count(), 'erp_pending' => $rows->where('erp_status', 'pending')->count(), ]; $source = (string) ($filters['source'] ?? ''); if ($source === 'mismatch') { $rows = $rows->where('account_status', 'mismatch'); } elseif ($source === 'erp_pending') { $rows = $rows->where('erp_status', 'pending'); } elseif (array_key_exists($source, self::SOURCES)) { $rows = $rows->where('source', $source); } $keyword = trim((string) ($filters['search'] ?? '')); if ($keyword !== '') { $rows = $rows->filter(static fn (array $row): bool => str_contains( mb_strtolower($row['name'] . ' ' . ($row['expcode'] ?? '') . ' ' . ($row['onboarding']?->company_full_name ?? '')), mb_strtolower($keyword), )); } $rows = $rows->values(); return [ 'rows' => new LengthAwarePaginator( $rows->forPage($page, $perPage)->values(), $rows->count(), $perPage, $page, ['path' => request()->url(), 'query' => request()->except('page')], ), 'counts' => $counts, 'platform_ready' => $platformReady, ]; } /** * @return array{rows:list>,platform_ready:bool} */ public function rows(): array { try { $platform = $this->mirror->overviewSource(); } catch (Throwable $exception) { report($exception); $platform = null; } $onboardings = Schema::hasTable('business_courier_onboardings') ? CourierOnboarding::query() ->with('files') ->whereNotIn('status', [CourierOnboarding::STATUS_CANCELLED, CourierOnboarding::STATUS_REJECTED]) ->latest('id') ->get() : collect(); $byCode = $onboardings->filter(static fn (CourierOnboarding $o): bool => filled($o->workplatform_expcode)) ->keyBy(static fn (CourierOnboarding $o): string => (string) $o->workplatform_expcode); $byName = $onboardings->filter(static fn (CourierOnboarding $o): bool => filled($o->courier_name)) ->keyBy(static fn (CourierOnboarding $o): string => CourierOnboarding::normalizeName($o->courier_name)); $used = []; $rows = []; foreach ($platform['express'] ?? [] as $express) { $code = (string) $express->expcode; $onboarding = $byCode->get($code) ?? $byName->get(CourierOnboarding::normalizeName($express->expname)); if ($onboarding !== null && isset($used[$onboarding->id])) { $onboarding = null; } if ($onboarding !== null) { $used[$onboarding->id] = true; } $rows[] = $this->row($onboarding, $express, $platform['accounts'][$code] ?? []); } foreach ($onboardings as $onboarding) { if (! isset($used[$onboarding->id])) { $rows[] = $this->row($onboarding, null, []); } } usort($rows, static fn (array $a, array $b): int => [$a['sort'], $a['name']] <=> [$b['sort'], $b['name']]); return ['rows' => $rows, 'platform_ready' => $platform !== null]; } /** * @param list $accounts * @return array */ private function row(?CourierOnboarding $onboarding, ?object $express, array $accounts): array { $source = $onboarding && $express ? 'both' : ($express ? 'platform' : 'onboarding'); $platformNumbers = collect($accounts) ->map(static fn ($account): string => preg_replace('/\D+/', '', (string) ($account->account ?? ''))) ->filter() ->values(); $onboardingNumbers = collect([$onboarding?->bank_account_number, $onboarding?->private_account_number]) ->map(static fn ($number): string => preg_replace('/\D+/', '', (string) $number)) ->filter() ->values(); $accountStatus = match (true) { $source !== 'both' => null, $platformNumbers->isEmpty() => 'platform_missing', $onboardingNumbers->isEmpty() => 'onboarding_missing', $onboardingNumbers->diff($platformNumbers)->isEmpty() => 'match', $onboardingNumbers->intersect($platformNumbers)->isNotEmpty() => 'partial', default => 'mismatch', }; $required = $onboarding ? CourierOnboardingFile::requiredKindsFor($onboarding->entity_type) : []; $kinds = $onboarding ? $onboarding->files->pluck('kind')->unique()->all() : []; $missingFiles = array_values(array_diff($required, $kinds)); $effective = $onboarding && filled($onboarding->courier_name) && Schema::hasTable('business_courier_quote_versions') ? $this->requotes->effectiveVersion((string) $onboarding->courier_name) : null; // ERP 开通:入驻开通后等钉钉「新建快递」流程回填快递代码;工作平台老快递视为已开通。 $erpStatus = match (true) { $onboarding === null => 'opened', $onboarding->status !== CourierOnboarding::STATUS_ACTIVATED => 'na', $onboarding->erp_opened_at !== null || $express !== null => 'opened', default => 'pending', }; $erpAccountId = $onboarding?->erp_account_id ?: collect($accounts)->pluck('erpAccountId')->filter()->unique()->implode(' / '); return [ 'erp_status' => $erpStatus, 'erp_account_id' => $erpAccountId ?: null, 'erp_source' => $onboarding?->erpSourceLabel() ?: ($express ? '工作平台' : null), 'name' => (string) ($express->expname ?? $onboarding?->courier_name ?? $onboarding?->company_full_name ?? ''), 'expcode' => $express?->expcode ?? $onboarding?->workplatform_expcode, 'source' => $source, 'sort' => match (true) { $erpStatus === 'pending' => 0, $accountStatus === 'mismatch' => 1, $source === 'onboarding' => 2, $source === 'both' => 3, default => 4, }, 'platform' => $express, 'platform_accounts' => $accounts, 'onboarding' => $onboarding, 'account_status' => $accountStatus, 'missing_files' => array_map(static fn (string $kind): string => CourierOnboardingFile::labelFor($kind, $onboarding?->entity_type), $missingFiles), 'effective_version' => $effective?->version_code, 'linked_by' => $onboarding && $express ? ((string) $onboarding->workplatform_expcode === (string) $express->expcode ? 'code' : 'name') : null, ]; } /** * 「快递资料」表格右侧接上的入驻信息:本页工作平台快递按快递代码 / 系统快递名称对上的入驻, * 以及还没进工作平台(待 ERP 开通、或停用工作平台后新入驻)的快递,放在第一页最前面。 * * @param list $expressRows 本页工作平台快递 * @return array{matches:array>,extra:list>} */ public function expressAppend(array $expressRows, int $page, string $nameKeyword = '', string $codeFilter = ''): array { $onboardings = $this->onboardings(); if ($onboardings->isEmpty()) { return ['matches' => [], 'extra' => []]; } $byCode = $onboardings->filter(static fn (CourierOnboarding $o): bool => filled($o->workplatform_expcode)) ->keyBy(static fn (CourierOnboarding $o): string => (string) $o->workplatform_expcode); $byName = $onboardings->filter(static fn (CourierOnboarding $o): bool => filled($o->courier_name)) ->keyBy(static fn (CourierOnboarding $o): string => CourierOnboarding::normalizeName($o->courier_name)); $matches = []; foreach ($expressRows as $express) { $onboarding = $byCode->get((string) ($express->expcode ?? '')) ?? $byName->get(CourierOnboarding::normalizeName($express->expname ?? '')); if ($onboarding !== null) { $matches[(string) ($express->Id ?? $express->expcode)] = $this->onboardingInfo($onboarding, true); } } $extra = []; if ($page === 1) { try { $platform = $this->mirror->overviewSource(); } catch (Throwable) { $platform = null; } $codes = collect($platform['express'] ?? [])->pluck('expcode')->map(static fn ($code): string => (string) $code)->flip(); $names = collect($platform['express'] ?? [])->pluck('expname')->map(static fn ($name): string => CourierOnboarding::normalizeName($name))->flip(); foreach ($onboardings as $onboarding) { $inPlatform = (filled($onboarding->workplatform_expcode) && $codes->has((string) $onboarding->workplatform_expcode)) || (filled($onboarding->courier_name) && $names->has(CourierOnboarding::normalizeName($onboarding->courier_name))); if ($inPlatform) { continue; } if ($codeFilter !== '' && strcasecmp((string) $onboarding->workplatform_expcode, $codeFilter) !== 0) { continue; } if ($nameKeyword !== '' && ! str_contains(mb_strtolower((string) $onboarding->courier_name . ' ' . $onboarding->company_full_name), mb_strtolower($nameKeyword))) { continue; } $extra[] = $this->onboardingInfo($onboarding, false); } } return ['matches' => $matches, 'extra' => $extra]; } /** 快递资料里选中的一行对应的入驻(先按快递公司代码,再按系统快递名称)。 */ public function infoFor(?string $code, ?string $name): ?array { $onboardings = $this->onboardings(); $onboarding = (filled($code) ? $onboardings->first(static fn (CourierOnboarding $o): bool => strcasecmp((string) $o->workplatform_expcode, (string) $code) === 0) : null) ?? (filled($name) ? $onboardings->first(static fn (CourierOnboarding $o): bool => CourierOnboarding::normalizeName($o->courier_name) === CourierOnboarding::normalizeName($name)) : null); return $onboarding !== null ? $this->onboardingInfo($onboarding, true) : null; } /** @return Collection */ private function onboardings(): Collection { return $this->onboardingCache ??= Schema::hasTable('business_courier_onboardings') ? CourierOnboarding::query() ->with('files') ->whereNotIn('status', [CourierOnboarding::STATUS_CANCELLED, CourierOnboarding::STATUS_REJECTED]) ->latest('id') ->get() : collect(); } /** @return array */ private function onboardingInfo(CourierOnboarding $onboarding, bool $inPlatform): array { $required = CourierOnboardingFile::requiredKindsFor($onboarding->entity_type); $missing = array_values(array_diff($required, $onboarding->files->pluck('kind')->unique()->all())); $effective = filled($onboarding->courier_name) && Schema::hasTable('business_courier_quote_versions') ? $this->requotes->effectiveVersion((string) $onboarding->courier_name) : null; // 合同起止:协议签署里「合作合同」已签署的那份(最新一份)上填的合作期限。 $contract = app(\App\Support\CourierOnboardingService::class)->agreementSignatures($onboarding) ->first(static fn (\App\Models\Business\CourierAgreementSignature $signature): bool => $signature->document_type === 'cooperation-contract' && in_array($signature->status, [\App\Models\Business\CourierAgreementSignature::STATUS_COMPLETED, \App\Models\Business\CourierAgreementSignature::STATUS_ARCHIVED], true)); $contractPayload = $contract !== null && is_array($contract->payload) ? $contract->payload : []; return [ 'contract_start' => $contractPayload['cooperation_start_date'] ?? null, 'contract_end' => $contractPayload['cooperation_end_date'] ?? null, 'onboarding' => $onboarding, 'erp_status' => match (true) { $onboarding->status !== CourierOnboarding::STATUS_ACTIVATED => 'na', $inPlatform || $onboarding->erp_opened_at !== null => 'opened', default => 'pending', }, 'checks' => $onboarding->accountChecks(), 'missing_files' => array_map(static fn (string $kind): string => CourierOnboardingFile::labelFor($kind, $onboarding->entity_type), $missing), 'effective_version' => $effective?->version_code, ]; } public static function mask(?string $number): string { $digits = preg_replace('/\s+/', '', (string) $number); return $digits === '' ? '' : '尾号 ' . mb_substr($digits, -4); } }