$filters @return array */ public function settle(array $filters): array { $filters = $this->normaliseFilters($filters); if (! $this->mirror->ready()) { return $this->emptyResult('进销存镜像尚未完成首次同步。', false); } $rawRows = iterator_to_array( $this->mirror->exportRows($filters, 5000, false), false, ); if ($rawRows === []) { return $this->emptyResult('当前期间及筛选条件下没有进销存明细。', true); } $weights = $this->weights($rawRows); $daysInMonth = $this->daysInMonth((string) $filters['period'], (string) $filters['dimension']); $quoteCache = []; $rows = []; $missingWeight = []; $missingQuote = []; $inbound = $outbound = $storage = 0.0; foreach ($rawRows as $row) { $date = (string) ($row['date'] ?? ''); // Keep a null result in the cache as well. When no quote exists, // `??=` would query the same date again for every SKU/day row and // turn an otherwise bounded page render into an N+1 timeout. if (array_key_exists($date, $quoteCache)) { $quote = $quoteCache[$date]; } else { $quote = $this->quoteForDate($date); $quoteCache[$date] = $quote; } if ($quote === null) { $missingQuote[$date] = true; } $sku = (string) ($row['sku_code'] ?? ''); $weight = $weights[$sku] ?? null; $purchaseIn = max(0.0, (float) ($row['purchase_in'] ?? 0)); if ($weight === null && $purchaseIn > 0.0) { $missingWeight[$sku] = true; } $salesOut = abs(min(0.0, (float) ($row['sales_out'] ?? 0))); $otherOut = abs(min(0.0, (float) ($row['other_out'] ?? 0))); $closing = max(0.0, (float) ($row['closing_qty'] ?? 0)); // A zero inbound quantity does not require a product weight. Only a // positive inbound quantity is blocked by a missing weight. $inFee = $purchaseIn <= 0.0 ? 0.0 : ($quote !== null && $weight !== null ? $purchaseIn * $weight / 1000 * (float) $quote['inbound_rate_per_ton'] : null); $outFee = $quote !== null ? ($salesOut + $otherOut) * (float) $quote['outbound_rate_per_order'] : null; $storageFee = $quote !== null ? $closing / max((float) $quote['pieces_per_pallet'], 0.0001) * (float) $quote['pallet_area_sqm'] * (float) $quote['storage_rate_per_sqm_month'] / $daysInMonth : null; $total = $inFee === null || $outFee === null || $storageFee === null ? null : $inFee + $outFee + $storageFee; if ($inFee !== null) $inbound += $inFee; if ($outFee !== null) $outbound += $outFee; if ($storageFee !== null) $storage += $storageFee; $rows[] = $row + [ 'weight_kg' => $weight, 'inbound_fee' => $inFee, 'outbound_fee' => $outFee, 'storage_fee' => $storageFee, 'total_fee' => $total, 'quote_effective_at' => $quote['effective_at'] ?? null, ]; } $complete = $missingWeight === [] && $missingQuote === []; return [ 'ready' => true, 'complete' => $complete, 'message' => $complete ? null : '报价或商品重量资料不完整,金额仅供核对,不能作为最终结算。', 'rows' => $rows, 'inbound_fee' => $complete ? $inbound : null, 'outbound_fee' => $complete ? $outbound : null, 'storage_fee' => $complete ? $storage : null, 'total_fee' => $complete ? $inbound + $outbound + $storage : null, 'missing_weight_skus' => array_keys($missingWeight), 'missing_quote_dates' => array_keys($missingQuote), 'days_in_month' => $daysInMonth, 'filters' => $filters, ]; } /** @param list> $rows @return array */ private function weights(array $rows): array { $codes = collect($rows)->pluck('sku_code')->filter()->unique()->values(); if ($codes->isEmpty()) return []; $connection = $this->databases->connection((string) config('upstream_mirrors.connection', 'business_mirror')); $result = []; foreach ($codes->chunk(1000) as $chunk) { try { $connection->table('ops_jst_sku')->where('sku_type', 'normal')->whereIn('sku_id', $chunk->all()) ->orderByDesc('autoid')->get(['sku_id', 'weight'])->each(function (object $item) use (&$result): void { if (! array_key_exists((string) $item->sku_id, $result) && is_numeric($item->weight)) { $result[(string) $item->sku_id] = (float) $item->weight; } }); } catch (\Throwable) { return []; } } return $result; } /** @return array|null */ private function quoteForDate(string $date): ?array { $quotes = $this->quotes->settlementQuotes('三方-南昌樵舍仓', $date); return $quotes->last() ?: null; } private function daysInMonth(string $period, string $dimension): int { try { return $dimension === 'day' ? (int) date('t', strtotime($period)) : (int) date('t', strtotime($period . '-01')); } catch (\Throwable) { return 31; } } /** @param array $filters @return array */ private function normaliseFilters(array $filters): array { $dimension = in_array(($filters['dimension'] ?? 'month'), ['month', 'day'], true) ? $filters['dimension'] : 'month'; $period = trim((string) ($filters['period'] ?? '2026-08')); if ($dimension === 'month' && preg_match('/^\d{4}-\d{2}$/', $period) !== 1) $period = '2026-08'; if ($dimension === 'day' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $period) !== 1) $period = '2026-08-01'; return ['dimension' => $dimension, 'period' => $period] + $filters; } /** @return array */ private function emptyResult(string $message, bool $ready): array { return ['ready' => $ready, 'complete' => false, 'message' => $message, 'rows' => [], 'inbound_fee' => null, 'outbound_fee' => null, 'storage_fee' => null, 'total_fee' => null, 'missing_weight_skus' => [], 'missing_quote_dates' => [], 'days_in_month' => 31, 'filters' => []]; } }