first(); abort_if(empty($section), 403, '当前账号没有已授权的侧边栏功能。'); $page = collect($section['pages'] ?? [])->first(); $parameters = ['section' => $section['slug']]; if ($page) { $parameters['page'] = $page['slug']; if (! empty($page['tabs'])) { $parameters['tab'] = $page['tabs'][0]['slug']; } } return redirect()->route('business.pages.show', $parameters); } public function show(string $section, ?string $page = null, ?string $tab = null) { $sectionConfig = collect(SidebarPermissions::navigationFor(user()))->firstWhere('slug', $section); abort_if(empty($sectionConfig), 403, '当前账号没有该页面权限。'); $pages = collect($sectionConfig['pages'] ?? []); if ($pages->isEmpty()) { abort_if($page !== null || $tab !== null, 404); return view('business.pages.show', [ 'section' => $sectionConfig, 'page' => null, 'tabs' => collect(), 'activeTab' => null, ]); } if ($page === null) { return $this->redirectToPage($sectionConfig, $pages->first()); } $pageConfig = $pages ->flatMap(fn (array $configuredPage) => [ $configuredPage, ...($configuredPage['children'] ?? []), ]) ->firstWhere('slug', $page); abort_if(empty($pageConfig), 404); // 如果该 page 是一个 children-only 的父级分组,访问它时跳到第一个子页面 if (! empty($pageConfig['children']) && empty($pageConfig['tabs'])) { return $this->redirectToPage($sectionConfig, $pageConfig['children'][0]); } $tabs = collect($pageConfig['tabs'] ?? []); if ($tabs->isEmpty()) { abort_if($tab !== null, 404); $content = $this->resolveContent($page, null); return view('business.pages.show', [ 'section' => $sectionConfig, 'page' => $pageConfig, 'tabs' => $tabs, 'activeTab' => null, 'content' => $content, 'edgeBanding' => $this->edgeBandingContent($page), 'inventoryData' => $this->inventoryContent($page), 'rawBoardData' => $this->rawBoardContent($page), ]); } if ($tab === null) { return $this->redirectToPage($sectionConfig, $pageConfig); } $activeTab = $tabs->firstWhere('slug', $tab); abort_if(empty($activeTab), 404); $content = $this->resolveContent($page, $tab); return view('business.pages.show', [ 'section' => $sectionConfig, 'page' => $pageConfig, 'tabs' => $tabs, 'activeTab' => $activeTab, 'content' => $content, 'edgeBanding' => $this->edgeBandingContent($page), 'inventoryData' => $this->inventoryContent($page), 'rawBoardData' => $this->rawBoardContent($page), ]); } /** * 根据页面和标签加载对应的业务内容数据 */ protected function resolveContent(string $page, ?string $tab): ?array { return match ($page) { 'marketing-account-bills' => $this->contentForMarketingAccountBills($tab), 'zhongcai-reconciliation' => $this->contentForZhongcaiReconciliation(), default => null, }; } /** * 封边条页面只读取上游快照;未启用或无快照时将错误交给页面显示, * 避免工作台因上游暂不可用直接返回 500。 */ protected function edgeBandingContent(string $page): ?array { $scope = match ($page) { 'edge-banding-prices' => 'edge-banding.prices', 'details' => 'edge-banding.details', 'edge-banding-payables' => 'edge-banding.payables', default => null, }; if ($scope === null) { return null; } $filters = match ($page) { 'edge-banding-prices' => $this->edgeBandingFilters([ 'raw_name', 'raw_code', 'production_code', 'supplier', ]), 'details' => $this->edgeBandingFilters([ 'date', 'production_code', 'raw_name', 'return_flag', 'outbound_supplier', 'arrival_supplier', 'outbound_no', ]), 'edge-banding-payables' => $this->edgeBandingFilters(['period', 'supplier']), default => [], }; if ($page === 'edge-banding-payables' && ! isset($filters['period'])) { $filters['period'] = '2026-06'; } $pageNumber = max((int) request('data_page', 1), 1); $limit = min(max((int) request('data_limit', 25), 1), 100); $cache = $this->upstreamCaches->forSource(self::EDGE_BANDING_SOURCE); try { $result = $cache->remember( $scope, ['filters' => $filters, 'page' => $pageNumber, 'limit' => $limit], fn () => match ($scope) { 'edge-banding.prices' => $this->edgeBanding->prices($filters, $pageNumber, $limit), 'edge-banding.details' => $this->edgeBanding->details($filters, $pageNumber, $limit), 'edge-banding.payables' => $this->edgeBanding->payables($filters, $pageNumber, $limit), }, ); return [ 'filters' => $filters, 'result' => $result, 'page' => $pageNumber, 'limit' => $limit, 'last_page' => max((int) ceil(max((int) ($result['total'] ?? 0), 0) / $limit), 1), 'status' => $cache->responseHeaders(), 'error' => null, ]; } catch (ServiceUnavailableHttpException $exception) { return [ 'filters' => $filters, 'result' => ['data' => [], 'total' => 0], 'page' => $pageNumber, 'limit' => $limit, 'last_page' => 1, 'status' => [], 'error' => $exception->getMessage(), ]; } catch (Throwable $exception) { report($exception); return [ 'filters' => $filters, 'result' => ['data' => [], 'total' => 0], 'page' => $pageNumber, 'limit' => $limit, 'last_page' => 1, 'status' => [], 'error' => '封边条数据暂时无法读取,请稍后重试。', ]; } } private function edgeBandingFilters(array $fields): array { $filters = []; foreach ($fields as $field) { $value = trim((string) request($field, '')); if ($value !== '') { $filters[$field] = $value; } } return $filters; } /** * 素板价格与对账页面使用 JianSun 只读查询,并保留共享快照链路。 */ protected function rawBoardContent(string $page): ?array { $scope = match ($page) { 'board-prices' => 'raw-board.prices', 'board-reconciliation' => 'raw-board.reconciliations', default => null, }; if ($scope === null) { return null; } $filters = $scope === 'raw-board.prices' ? $this->rawBoardFilters(['raw_name', 'raw_code', 'production_code', 'supplier']) : $this->rawBoardReconciliationFilters(); $pageNumber = max((int) request('data_page', 1), 1); $limit = min(max((int) request('data_limit', 25), 1), 100); try { $cache = $this->upstreamCaches->forSource(self::RAW_BOARD_SOURCE); $result = $cache->remember( $scope, ['filters' => $filters, 'page' => $pageNumber, 'limit' => $limit], fn () => $scope === 'raw-board.prices' ? $this->rawBoardPayables->prices($filters, $pageNumber, $limit) : $this->rawBoardPayables->reconciliations($filters, $pageNumber, $limit), ); return [ 'filters' => $filters, 'result' => $result, 'page' => $pageNumber, 'limit' => $limit, 'last_page' => max((int) ceil(max((int) ($result['total'] ?? 0), 0) / $limit), 1), 'status' => $cache->responseHeaders(), 'error' => null, ]; } catch (ServiceUnavailableHttpException $exception) { return $this->emptyRawBoardContent($filters, $pageNumber, $limit, $exception->getMessage()); } catch (Throwable $exception) { report($exception); return $this->emptyRawBoardContent( $filters, $pageNumber, $limit, '素板应付数据暂时无法读取,请稍后重试。', ); } } private function rawBoardFilters(array $fields): array { $filters = []; foreach ($fields as $field) { $value = mb_substr(trim((string) request($field, '')), 0, 100); if ($value !== '') { $filters[$field] = $value; } } return $filters; } private function rawBoardReconciliationFilters(): array { $filters = $this->rawBoardFilters([ 'production_code', 'raw_name', 'outbound_supplier', 'arrival_supplier', 'outbound_no', ]); foreach (['date_from', 'date_to'] as $field) { $value = $this->dateFilter(request($field)); if ($value !== '') { $filters[$field] = $value; } } $returnFlag = trim((string) request('return_flag', '')); if (in_array($returnFlag, ['normal', 'return'], true)) { $filters['return_flag'] = $returnFlag; } return $filters; } private function emptyRawBoardContent(array $filters, int $page, int $limit, string $error): array { return [ 'filters' => $filters, 'result' => ['data' => [], 'total' => 0, 'totals' => []], 'page' => $page, 'limit' => $limit, 'last_page' => 1, 'status' => [], 'error' => $error, ]; } /** * 板材库存页面使用 JianSun 最新素板快照,并保留生产共享快照链路。 */ protected function inventoryContent(string $page): ?array { if ($page !== 'board-inventory-details') { return null; } $filters = $this->inventoryFilters(); $pageNumber = max((int) request('data_page', 1), 1); $limit = min(max((int) request('data_limit', 25), 1), 100); try { $cache = $this->upstreamCaches->forSource(self::INVENTORY_SOURCE); $result = $cache->remember( 'inventory.board-details', ['filters' => $filters, 'page' => $pageNumber, 'limit' => $limit], fn () => $this->inventory->boardInventoryDetails($filters, $pageNumber, $limit), ); return [ 'filters' => $filters, 'result' => $result, 'page' => $pageNumber, 'limit' => $limit, 'last_page' => max((int) ceil(max((int) ($result['total'] ?? 0), 0) / $limit), 1), 'status' => $cache->responseHeaders(), 'error' => null, ]; } catch (ServiceUnavailableHttpException $exception) { return $this->emptyInventoryContent($filters, $pageNumber, $limit, $exception->getMessage()); } catch (Throwable $exception) { report($exception); return $this->emptyInventoryContent( $filters, $pageNumber, $limit, '板材库存数据暂时无法读取,请稍后重试。', ); } } private function inventoryFilters(): array { $filters = []; foreach (['manufacturer', 'specification', 'matching_value'] as $field) { $value = mb_substr(trim((string) request($field, '')), 0, 100); if ($value !== '') { $filters[$field] = $value; } } foreach (['date_from', 'date_to'] as $field) { $value = $this->dateFilter(request($field)); if ($value !== '') { $filters[$field] = $value; } } return $filters; } private function emptyInventoryContent(array $filters, int $page, int $limit, string $error): array { return [ 'filters' => $filters, 'result' => ['data' => [], 'total' => 0, 'totals' => []], 'page' => $page, 'limit' => $limit, 'last_page' => 1, 'status' => [], 'error' => $error, ]; } /** * 种菜应收对账 - 加载对账数据(21字段) */ protected function contentForZhongcaiReconciliation(): ?array { return [ 'view' => 'store_receivables.zhongcai_reconciliations.partials.embedded-table', 'records' => StoreZhongcaiReconciliation::orderBy('bill_date', 'desc') ->orderBy('id', 'desc') ->paginate(setting('default.list_limit', 25)), ]; } /** * 营销账户账单 - 加载指定平台的数据 * 淘系支付宝、拼多多使用独立的流水账单表,其他平台使用通用汇总账单表 */ protected function contentForMarketingAccountBills(string $tab): ?array { if (!in_array($tab, self::MARKETING_PLATFORMS, true)) { return null; } // 淘系支付宝 - 从上游 JianSun.tmall_spread_bills 读取并按表1字段展示(9列) if ($tab === 'taobao-alipay') { $request = request(); $storeName = mb_substr(trim((string) $request->query('store_name')), 0, 191); $fromDate = $this->dateFilter($request->query('from')); $toDate = $this->dateFilter($request->query('to')); $flowType = trim((string) $request->query('flow_type')); $flowType = in_array($flowType, ['income', 'expense'], true) ? $flowType : ''; $page = max(1, (int) $request->query('page', 1)); $pageSize = min(max((int) setting('default.list_limit', 25), 1), 100); $repository = app(TmallTaobaoSpreadBillRepository::class); $cache = $this->upstreamCaches->forSource('jiansun'); $filters = [ 'store_name' => $storeName !== '' ? $storeName : null, 'from' => $fromDate !== '' ? $fromDate : null, 'to' => $toDate !== '' ? $toDate : null, 'flow_type' => $flowType !== '' ? $flowType : null, ]; $result = $cache->remember( 'taobao-alipay-bills.list', ['filters' => $filters, 'page' => $page, 'limit' => $pageSize], fn () => $repository->paginate( $filters['store_name'], $filters['from'], $filters['to'], $filters['flow_type'], $page, $pageSize ) ); $summary = $cache->remember( 'taobao-alipay-bills.summary', ['filters' => $filters], fn () => $repository->summary( $filters['store_name'], $filters['from'], $filters['to'], $filters['flow_type'] ) ); $storeOptions = $cache->remember( 'taobao-alipay-bills.options', [], fn () => $repository->storeOptions() ); $bills = new LengthAwarePaginator( Collection::make($result['rows']), $result['total'], $pageSize, $page, [ 'path' => $request->url(), 'query' => $request->query(), ] ); return [ 'view' => 'banking.marketing_taobao_alipay_bills.partials.embedded-table', 'bills' => $bills, 'filters' => [ 'store_name' => $storeName, 'from' => $fromDate, 'to' => $toDate, 'flow_type' => $flowType, 'store_options' => $storeOptions, ], 'summary' => $summary, ]; } // 拼多多 - 从上游 JianSun.pdd_spread_bills 读取并按表1字段展示(7列) // 表1列:店铺名称 | 金额 | 余额 | 摘要 | 收支类型 | 资金类型 | 费用时间 if ($tab === 'pinduoduo') { $request = request(); $storeName = mb_substr(trim((string) $request->query('store_name')), 0, 191); $fromDate = $this->dateFilter($request->query('from')); $toDate = $this->dateFilter($request->query('to')); $flowType = trim((string) $request->query('flow_type')); $flowType = in_array($flowType, ['income', 'expense'], true) ? $flowType : ''; $page = max(1, (int) $request->query('page', 1)); $pageSize = min(max((int) setting('default.list_limit', 25), 1), 100); $repository = app(PinduoduoSpreadBillRepository::class); $cache = $this->upstreamCaches->forSource('jiansun'); $filters = [ 'store_name' => $storeName !== '' ? $storeName : null, 'from' => $fromDate !== '' ? $fromDate : null, 'to' => $toDate !== '' ? $toDate : null, 'flow_type' => $flowType !== '' ? $flowType : null, ]; // 页面只读取共享查询快照;冷查询由统一限流和熔断链路访问上游。 $result = $cache->remember( 'pinduoduo-bills.list', ['filters' => $filters, 'page' => $page, 'limit' => $pageSize], fn () => $repository->paginate( $filters['store_name'], $filters['from'], $filters['to'], $filters['flow_type'], $page, $pageSize ) ); $summary = $cache->remember( 'pinduoduo-bills.summary', ['filters' => $filters], fn () => $repository->summary( $filters['store_name'], $filters['from'], $filters['to'], $filters['flow_type'] ) ); $storeOptions = $cache->remember( 'pinduoduo-bills.options', [], fn () => $repository->storeOptions() ); // 包装成 Laravel 分页器,便于 Blade 用 $bills->links() $bills = new LengthAwarePaginator( Collection::make($result['rows']), $result['total'], $pageSize, $page, [ 'path' => $request->url(), 'query' => $request->query(), ] ); return [ 'view' => 'banking.marketing_pinduoduo_bills.partials.embedded-table', 'bills' => $bills, 'filters' => [ 'store_name' => $storeName, 'from' => $fromDate, 'to' => $toDate, 'flow_type' => $flowType, 'store_options' => $storeOptions, ], 'summary' => $summary, ]; } // 其他平台 - 使用通用汇总账单表(15字段) return [ 'view' => 'banking.marketing_account_bills.partials.embedded-table', 'platform' => $tab, 'accounts' => MarketingAccountBill::platform($tab) ->orderBy('bill_date', 'desc') ->orderBy('id', 'desc') ->paginate(setting('default.list_limit', 25)), ]; } protected function redirectToPage(array $section, array $page) { $parameters = [ 'section' => $section['slug'], 'page' => $page['slug'], ]; if (! empty($page['tabs'])) { $parameters['tab'] = $page['tabs'][0]['slug']; } return redirect()->route('business.pages.show', $parameters); } private function dateFilter(mixed $value): string { $date = trim((string) $value); if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) !== 1) { return ''; } [$year, $month, $day] = array_map('intval', explode('-', $date)); return checkdate($month, $day, $year) ? $date : ''; } }