,paths:list,storeEntities:list,rows:Collection, * balances:array, * balancesByDate:array>, * balanceDays:list, * alerts:array, * reserveStandards:array,withdrawalAmounts:array,compliance:array, * summary:array{source:int,matched:int,finance:int,ambiguous:int,unmatched:int,not_ready:int}, * balanceColumnLabel:string,balanceDate:string,latestBalanceDate:?string,balanceDates:list,sort:array{field:string,direction:string} * } */ public function results(Request $request): array { $filters = $this->filters($request); $sort = $this->sorting($request); $platforms = $this->options('store_platform'); $paths = $this->options('receiving_method'); $storeEntities = $this->options('store_entity_name'); $query = StoreReceivingAccount::query(); if ($filters['platform'] !== '') { $query->where('store_platform', $filters['platform']); } if ($filters['withdrawalPath'] !== '') { $query->where('receiving_method', $filters['withdrawalPath']); } if ($filters['storeEntity'] !== '') { $query->where('store_entity_name', $filters['storeEntity']); } if ($filters['keyword'] !== '') { $query->where('store_name', 'like', '%' . addcslashes($filters['keyword'], '%_\\') . '%'); } $today = now()->startOfDay(); // 余额快照按日期长期保留:默认显示最新余额日期,也可指定历史日期回看。 $latestBalanceDate = $this->shopBalances->latestBalanceDate($today); $balanceDate = $filters['balanceDate'] !== '' ? $filters['balanceDate'] : ($latestBalanceDate ?? $today->copy()->subDay()->toDateString()); $labelDate = $filters['balanceDate'] !== '' ? CarbonImmutable::parse($balanceDate) : $today; $rows = $query->orderByDesc('id')->get(); $historyDates = WithdrawalBalanceAlertSynchronizer::datesEndingAt($balanceDate); $balancesByDate = $this->shopBalances->matchesForStoresOnDates($rows, $historyDates); $balances = array_map( static fn (array $dates): array => $dates[$balanceDate] ?? [ 'status' => 'not_ready', 'balance' => null, 'source_store_name' => null, 'source_platform' => null, ], $balancesByDate, ); $balanceCounts = collect($balances)->countBy( static fn (array $match): string => $match['status'], ); $financeCount = collect($balances)->filter( static fn (array $match): bool => ($match['data_source'] ?? null) === JushuitanShopBalanceRepository::DATA_SOURCE_FINANCE, )->count(); if ($filters['balanceStatus'] !== '') { $rows = $rows->filter( static fn (StoreReceivingAccount $row): bool => ($balances[(int) $row->id]['status'] ?? 'not_ready') === $filters['balanceStatus'], ); } $reserveStandards = $rows->mapWithKeys( static fn (StoreReceivingAccount $row): array => [ (int) $row->id => WithdrawalReserveStandard::amountFor( $row->store_platform, $row->erp_store_name, ), ], )->all(); $balanceDays = collect($historyDates)->map( static fn (string $date, int $index): array => [ 'key' => $index === 0 ? 'balance_two_days_ago' : ($index === 1 ? 'balance_yesterday' : 'balance'), 'date' => $date, 'label' => CarbonImmutable::parse($date)->format('n月j日') . '店铺余额', ], )->values()->all(); $alerts = WithdrawalBalanceAlertSynchronizer::detect( $rows, $balancesByDate, $reserveStandards, $historyDates, ); $withdrawalAmounts = $rows->mapWithKeys( fn (StoreReceivingAccount $row): array => [ (int) $row->id => $this->withdrawalAmountFor($row, $balanceDate), ], )->all(); $compliance = $rows->mapWithKeys( fn (StoreReceivingAccount $row): array => [ (int) $row->id => self::complianceFor( $balances[(int) $row->id] ?? ['status' => 'not_ready', 'balance' => null], $reserveStandards[(int) $row->id] ?? null, $withdrawalAmounts[(int) $row->id] ?? null, ), ], )->all(); $rows = $sort['field'] === '' ? $this->sortByBalanceStatus($rows, $balances) : $this->sortRows( $rows, $sort, $balancesByDate, $balanceDays, $alerts, $reserveStandards, $withdrawalAmounts, $compliance, ); return [ 'filters' => $filters, 'platforms' => $platforms, 'paths' => $paths, 'storeEntities' => $storeEntities, 'rows' => $rows, 'balances' => $balances, 'balancesByDate' => $balancesByDate, 'balanceDays' => $balanceDays, 'alerts' => $alerts, 'reserveStandards' => $reserveStandards, 'withdrawalAmounts' => $withdrawalAmounts, 'compliance' => $compliance, 'summary' => [ 'source' => $this->shopBalances->countForDate($balanceDate), 'matched' => (int) $balanceCounts->get('matched', 0), 'finance' => $financeCount, 'ambiguous' => (int) $balanceCounts->get('ambiguous', 0), 'unmatched' => (int) $balanceCounts->get('unmatched', 0), 'not_ready' => (int) $balanceCounts->get('not_ready', 0), ], 'balanceColumnLabel' => $labelDate->format('n月j日') . '店铺余额', 'balanceDate' => $balanceDate, 'latestBalanceDate' => $latestBalanceDate, 'balanceDates' => $this->shopBalances->availableDates($today), 'sort' => $sort, ]; } /** @return array{platform:string,withdrawalPath:string,storeEntity:string,keyword:string,balanceStatus:string,balanceDate:string} */ public function filters(Request $request): array { $clean = static fn (mixed $value, int $length = 80): string => mb_substr(trim((string) $value), 0, $length); $balanceStatus = $clean($request->query('balance_status')); $balanceDate = $clean($request->query('balance_date'), 10); if (! in_array($balanceStatus, ['matched', 'unmatched', 'ambiguous', 'not_ready'], true)) { $balanceStatus = ''; } $validDate = preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $balanceDate, $parts) === 1 && checkdate((int) $parts[2], (int) $parts[3], (int) $parts[1]); if (! $validDate || $balanceDate > now()->toDateString()) { $balanceDate = ''; } return [ 'platform' => $clean($request->query('platform')), 'withdrawalPath' => $clean($request->query('withdrawal_path')), 'storeEntity' => $clean($request->query('store_entity'), 191), 'keyword' => $clean($request->query('keyword'), 191), 'balanceStatus' => $balanceStatus, 'balanceDate' => $balanceDate, ]; } /** @return list */ private function options(string $column): array { return StoreReceivingAccount::query() ->whereNotNull($column) ->where($column, '<>', '') ->distinct() ->orderBy($column) ->pluck($column) ->filter() ->values() ->all(); } /** @return array{field:string,direction:string} */ private function sorting(Request $request): array { $field = trim((string) $request->query('sort')); $direction = strtolower(trim((string) $request->query('direction'))); return [ 'field' => in_array($field, self::SORT_FIELDS, true) ? $field : '', 'direction' => in_array($direction, ['asc', 'desc'], true) ? $direction : 'asc', ]; } /** * @param Collection $rows * @param array $balances * @return Collection */ private function sortByBalanceStatus(Collection $rows, array $balances): Collection { $statusOrder = ['matched' => 0, 'ambiguous' => 1, 'unmatched' => 2, 'not_ready' => 3]; return $rows->sort( static function (StoreReceivingAccount $left, StoreReceivingAccount $right) use ($balances, $statusOrder): int { $leftStatus = $balances[(int) $left->id]['status'] ?? 'not_ready'; $rightStatus = $balances[(int) $right->id]['status'] ?? 'not_ready'; $statusComparison = ($statusOrder[$leftStatus] ?? 99) <=> ($statusOrder[$rightStatus] ?? 99); return $statusComparison !== 0 ? $statusComparison : ((int) $right->id <=> (int) $left->id); }, )->values(); } /** * @param Collection $rows * @param array{field:string,direction:string} $sort * @param array> $balancesByDate * @param list $balanceDays * @param array $alerts * @param array $reserveStandards * @param array $withdrawalAmounts * @param array $compliance * @return Collection */ private function sortRows( Collection $rows, array $sort, array $balancesByDate, array $balanceDays, array $alerts, array $reserveStandards, array $withdrawalAmounts, array $compliance, ): Collection { return $rows->sort(function (StoreReceivingAccount $left, StoreReceivingAccount $right) use ( $sort, $balancesByDate, $balanceDays, $alerts, $reserveStandards, $withdrawalAmounts, $compliance, ): int { $leftValue = $this->sortValue($left, $sort['field'], $balancesByDate, $balanceDays, $alerts, $reserveStandards, $withdrawalAmounts, $compliance); $rightValue = $this->sortValue($right, $sort['field'], $balancesByDate, $balanceDays, $alerts, $reserveStandards, $withdrawalAmounts, $compliance); $comparison = $this->compareSortValues($leftValue, $rightValue, $sort['direction']); return $comparison !== 0 ? $comparison : ((int) $right->id <=> (int) $left->id); })->values(); } /** * @param array> $balancesByDate * @param list $balanceDays * @param array $alerts * @param array $reserveStandards * @param array $withdrawalAmounts * @param array $compliance */ private function sortValue( StoreReceivingAccount $row, string $field, array $balancesByDate, array $balanceDays, array $alerts, array $reserveStandards, array $withdrawalAmounts, array $compliance, ): mixed { $balanceFor = static function (string $dayKey) use ($row, $balancesByDate, $balanceDays): ?float { $date = null; foreach ($balanceDays as $day) { if ($day['key'] === $dayKey) { $date = $day['date']; break; } } $match = $date === null ? null : ($balancesByDate[(int) $row->id][$date] ?? null); return ($match['status'] ?? null) === 'matched' ? (float) $match['balance'] : null; }; return match ($field) { 'platform' => $row->store_platform, 'erp_store_name' => $row->erp_store_name, 'store_id' => $row->store_id, 'store_entity' => $row->store_entity_name, 'withdrawal_path' => $row->withdrawal_path, 'withdrawal_account' => $row->withdrawal_account_name, 'bank_card' => $row->bank_card_last_four, 'balance_two_days_ago' => $balanceFor('balance_two_days_ago'), 'balance_yesterday' => $balanceFor('balance_yesterday'), 'balance' => $balanceFor('balance'), 'balance_alert' => isset($alerts[(int) $row->id]) ? 1 : 0, 'reserve_standard' => $reserveStandards[(int) $row->id] ?? null, 'withdrawal_amount' => $withdrawalAmounts[(int) $row->id] ?? null, 'compliance' => $compliance[(int) $row->id] ?? null, default => null, }; } private function withdrawalAmountFor(StoreReceivingAccount $row, string $balanceDate): ?float { if ($row->withdrawal_balance_date !== $balanceDate || $row->withdrawal_amount === null) { return null; } return is_numeric($row->withdrawal_amount) ? (float) $row->withdrawal_amount : null; } /** @param array{status:string,balance:?float} $balance */ public static function complianceFor(array $balance, ?int $reserve, ?float $withdrawalAmount): ?string { if (($balance['status'] ?? '') !== 'matched' || $balance['balance'] === null || $reserve === null || $withdrawalAmount === null) { return null; } $remainingCents = (int) round((float) $balance['balance'] * 100) - (int) round($withdrawalAmount * 100); return $remainingCents <= $reserve * 100 ? 'yes' : 'no'; } private function compareSortValues(mixed $left, mixed $right, string $direction): int { $leftEmpty = $left === null || $left === ''; $rightEmpty = $right === null || $right === ''; if ($leftEmpty || $rightEmpty) { return $leftEmpty === $rightEmpty ? 0 : ($leftEmpty ? 1 : -1); } $comparison = is_numeric($left) && is_numeric($right) ? ((float) $left <=> (float) $right) : strnatcasecmp((string) $left, (string) $right); return $direction === 'desc' ? -$comparison : $comparison; } }