*/ public function build(Request $request): array { $stores = array_values(array_unique(array_filter(array_map( static fn (mixed $store): string => trim((string) $store), (array) config('kuka_posting.stores', []), )))); if ($stores === []) { return $this->emptyResult($stores, '尚未配置灵珊店铺业绩的店铺范围。'); } try { $months = $this->availableMonths(); $month = $this->selectedMonth((string) $request->query('month', ''), $months); if ($month === null) { return $this->emptyResult($stores, '业绩明细暂时没有可汇总的月份。'); } $storeRows = cache()->remember( $this->cacheKey('stores', $month, $stores), now()->addMinutes(10), fn (): array => $this->storeRows($month, $stores), ); $monthlyTotals = cache()->remember( $this->cacheKey('months', implode('|', $months), $stores), now()->addMinutes(10), fn (): array => $this->monthlyTotals($months, $stores), ); $summary = $this->summary($storeRows); return [ 'stores' => $storeRows, 'months' => $months, 'month' => $month, 'month_label' => $this->monthLabel($month), 'summary' => $summary, 'monthly_totals' => $monthlyTotals, 'error' => null, ]; } catch (Throwable $exception) { report($exception); return $this->emptyResult( $stores, '灵珊店铺业绩暂时无法读取,请稍后重试;这不代表该月收入为 0。', ); } } /** * @return list */ private function availableMonths(): array { return cache()->remember( $this->cacheKey('available-months'), now()->addMinutes(10), function (): array { $range = $this->connection() ->table('store_receipt_order_match_rows') ->whereNotIn('platform', self::EXCLUDED_PLATFORMS) ->whereNotNull('report_month') ->selectRaw('MIN(report_month) as min_month, MAX(report_month) as max_month') ->first(); if (($range->min_month ?? null) === null || ($range->max_month ?? null) === null) { return []; } $end = CarbonImmutable::parse((string) $range->max_month)->startOfMonth(); $start = CarbonImmutable::parse((string) $range->min_month)->startOfMonth(); $earliest = now()->startOfMonth()->subMonthsNoOverflow(11); if ($start->lt($earliest)) { $start = $earliest; } $months = []; for ($cursor = $end; $cursor->gte($start); $cursor = $cursor->subMonthNoOverflow()) { $months[] = $cursor->format('Y-m'); } return $months; }, ); } /** * @param list $months */ private function selectedMonth(string $requested, array $months): ?string { if (preg_match('/^\d{4}-\d{2}$/', $requested) === 1 && in_array($requested, $months, true)) { return $requested; } $previousMonth = now()->subMonthNoOverflow()->format('Y-m'); return in_array($previousMonth, $months, true) ? $previousMonth : ($months[0] ?? null); } /** * @param list $stores * @return list> */ private function storeRows(string $month, array $stores): array { $aggregates = $this->connection() ->table('store_receipt_order_match_rows') ->where('report_month', $month . '-01') ->whereIn('store_name', $stores) ->whereNotIn('platform', self::EXCLUDED_PLATFORMS) ->selectRaw( 'store_name, COUNT(*) as row_count, SUM(COALESCE(income_amount, 0)) as income_amount, SUM(COALESCE(discount_amount, 0)) as discount_amount, SUM(COALESCE(offset_amount, 0)) as offset_amount, SUM(COALESCE(refund_amount, 0)) as refund_amount, SUM(COALESCE(default_compensation_amount, 0)) as default_compensation_amount, SUM(' . self::INCOME_SQL . ') as total_income, MAX(imported_at) as imported_at' ) ->groupBy('store_name') ->get() ->keyBy('store_name'); return array_map(static function (string $store) use ($aggregates): array { $row = $aggregates->get($store); return [ 'store_name' => $store, 'row_count' => (int) ($row->row_count ?? 0), 'income_amount' => round((float) ($row->income_amount ?? 0), 2), 'discount_amount' => round((float) ($row->discount_amount ?? 0), 2), 'offset_amount' => round((float) ($row->offset_amount ?? 0), 2), 'refund_amount' => round((float) ($row->refund_amount ?? 0), 2), 'default_compensation_amount' => round((float) ($row->default_compensation_amount ?? 0), 2), 'total_income' => round((float) ($row->total_income ?? 0), 2), 'imported_at' => $row->imported_at ?? null, 'has_data' => $row !== null, ]; }, $stores); } /** * @param list $months * @param list $stores * @return list */ private function monthlyTotals(array $months, array $stores): array { $totals = []; foreach ($months as $month) { $row = $this->connection() ->table('store_receipt_order_match_rows') ->where('report_month', $month . '-01') ->whereIn('store_name', $stores) ->whereNotIn('platform', self::EXCLUDED_PLATFORMS) ->selectRaw( 'COUNT(*) as row_count, SUM(' . self::INCOME_SQL . ') as total_income' ) ->first(); $totals[] = [ 'month' => $month, 'label' => $this->monthLabel($month), 'total_income' => round((float) ($row->total_income ?? 0), 2), 'row_count' => (int) ($row->row_count ?? 0), ]; } return $totals; } /** * @param list> $rows * @return array */ private function summary(array $rows): array { $summary = [ 'row_count' => 0, 'stores_with_data' => 0, 'income_amount' => 0.0, 'discount_amount' => 0.0, 'offset_amount' => 0.0, 'refund_amount' => 0.0, 'default_compensation_amount' => 0.0, 'total_income' => 0.0, 'imported_at' => null, ]; foreach ($rows as $row) { $summary['row_count'] += (int) $row['row_count']; $summary['stores_with_data'] += $row['has_data'] ? 1 : 0; foreach ([ 'income_amount', 'discount_amount', 'offset_amount', 'refund_amount', 'default_compensation_amount', 'total_income', ] as $field) { $summary[$field] += (float) $row[$field]; } if ($row['imported_at'] !== null && ($summary['imported_at'] === null || $row['imported_at'] > $summary['imported_at'])) { $summary['imported_at'] = $row['imported_at']; } } foreach ([ 'income_amount', 'discount_amount', 'offset_amount', 'refund_amount', 'default_compensation_amount', 'total_income', ] as $field) { $summary[$field] = round((float) $summary[$field], 2); } return $summary; } /** * @param list $stores * @return array */ private function emptyResult(array $stores, string $error): array { return [ 'stores' => array_map(static fn (string $store): array => [ 'store_name' => $store, 'row_count' => 0, 'income_amount' => 0.0, 'discount_amount' => 0.0, 'offset_amount' => 0.0, 'refund_amount' => 0.0, 'default_compensation_amount' => 0.0, 'total_income' => 0.0, 'imported_at' => null, 'has_data' => false, ], $stores), 'months' => [], 'month' => null, 'month_label' => '—', 'summary' => [ 'row_count' => 0, 'stores_with_data' => 0, 'income_amount' => 0.0, 'discount_amount' => 0.0, 'offset_amount' => 0.0, 'refund_amount' => 0.0, 'default_compensation_amount' => 0.0, 'total_income' => 0.0, 'imported_at' => null, ], 'monthly_totals' => [], 'error' => $error, ]; } private function connection() { return DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); } private function monthLabel(string $month): string { return substr($month, 0, 4) . '年' . (int) substr($month, 5, 2) . '月'; } /** * @param list $stores */ private function cacheKey(string $scope, string $period = '', array $stores = []): string { return 'kuka-posting-performance-v1:' . $scope . ':' . md5(json_encode([ $period, $stores, ], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)); } }