$this->companyPage($currentPage, $perPage), 'store-management/basic-information' => $this->storePage($currentPage, $perPage), 'store-management/store-accounts' => $this->storeAccountPage($currentPage, $perPage), 'store-management/store-report' => $this->storeReportPage($currentPage, $perPage), 'company-management/subject-operating-report' => $this->subjectOperatingReportPage($currentPage, $perPage), 'trademark-management/information' => $this->trademarkPage($currentPage, $perPage), 'financial-operations/installation-details' => $this->installationPage($currentPage, $perPage), 'financial-risk-control/process-statistics' => $this->shopInfoFlowPage($currentPage, $perPage), default => ['data' => [], 'total' => 0], }; request()->attributes->set( 'business-page-summary-cards', app(BusinessSummaryCards::class)->forPage($key, $payload['summary'] ?? []), ); return new LengthAwarePaginator( array_map(static fn (array $row) => (object) $row, $payload['data']), (int) $payload['total'], $perPage, $currentPage, [ 'path' => request()->url(), 'query' => request()->query(), ], ); } /** * 财务经营风险管控 › 流程统计:店铺申请流程(开店 / 变更 / 关店)只读镜像。 * * @return array{data:list>,total:int} */ private function shopInfoFlowPage(int $page, int $perPage): array { try { return app(\Modules\EcommerceFinance\Repositories\ShopInfoFlowMirrorRepository::class)->paginate( $page, $perPage, $this->pageFilters('financial-risk-control', 'process-statistics'), ); } catch (\RuntimeException $exception) { report($exception); throw new ServiceUnavailableHttpException(null, '店铺申请流程共享镜像尚未完成首次同步。'); } } private function installationPage(int $page, int $perPage): array { $mirror = $this->installationMirror ?? app(InstallationDetailMirrorRepository::class); $payload = $mirror->paginate($page, $perPage, InstallationDetailMirrorRepository::requestFilters()); if (! $payload['ready']) { throw new ServiceUnavailableHttpException(null, '安装明细镜像尚未完成首次同步'); } return ['data' => $payload['data'], 'total' => $payload['total']]; } /** * 将已登记的经营分析副本库查询还原成不经过页面缓存的固定只读查询。 * * 生产后台刷新器使用这个入口生成共享快照;页面仍然只能通过 * UpstreamQueryCache 读取快照,不能绕过缓存直接访问上游。 * * @return array{data: array>, total: int} */ public function snapshot(string $scope, array $parameters): array { $page = max((int) ($parameters['page'] ?? 1), 1); $perPage = min(max((int) ($parameters['perPage'] ?? 50), 1), 100); $query = new UpstreamReadOnlyQuery(self::SOURCE); return match ($scope) { 'company-information.read' => $this->companySnapshot($query, $page, $perPage), 'store-information.read' => $this->storeSnapshot($query, $page, $perPage), 'trademark-information.read' => $this->trademarkSnapshot($query, $page, $perPage), default => throw new InvalidArgumentException('未知的经营分析副本库快照类型:' . $scope), }; } private function companyPage(int $page, int $perPage): array { $filters = $this->pageFilters('company-management', 'basic-information'); return $this->page( 'company-information.read', $page, $perPage, $filters, fn () => $this->snapshot('company-information.read', [ 'page' => $page, 'perPage' => $perPage, ]), ); } private function companySnapshot(UpstreamReadOnlyQuery $query, int $page, int $perPage): array { $offset = ($page - 1) * $perPage; $rows = $query->select(<< array_map(fn (array $row) => $this->mapCompany($row), $rows), 'total' => (int) $query->scalar('SELECT COUNT(*) FROM rpt.fys_companyInfo'), ]; } private function storePage(int $page, int $perPage): array { $filters = $this->pageFilters('store-management', 'basic-information'); if ($this->mirror->ready('store-information.read')) { $storeFilters = array_diff_key( $filters, array_flip(['store_manager', 'department_manager', '_sort', '_direction']), ); return $this->pageStoreInformation( $page, $perPage, $filters, $this->storesWithAccountOwnership(array_map( fn (array $row): array => $this->mapStore($row), $this->mirror->allStores($storeFilters), )), ); } $payload = $this->page( 'store-information.read', $page, $perPage, array_diff_key($filters, array_flip(['store_manager', 'department_manager'])), fn () => $this->snapshot('store-information.read', [ 'page' => $page, 'perPage' => $perPage, ]), false, ); $rows = $this->storesWithAccountOwnership($payload['data']); $rows = array_values(array_filter( $rows, fn (array $row): bool => $this->storeOwnershipMatchesFilters($row, $filters), )); return ['data' => $rows, 'total' => count($rows)]; } /** * 店铺基础信息页每次读取时把负责人字段覆盖为店铺账号镜像中的值。 * 账号店铺名与 ERP 店铺名只差下划线/空格,因此统一归一后关联。 * * @param list> $rows * @return list> */ public function storesWithAccountOwnership(array $rows, bool $requireAccountOwnership = false): array { $ownership = $this->storeOwnership($requireAccountOwnership); foreach ($rows as &$row) { $key = StoreAccountMirrorRepository::storeNameKey($row['erp_store_name'] ?? ''); $match = $ownership[$key] ?? null; if ($match === null) { continue; } foreach ([ 'store_manager' => $match['operation_owner'], 'department_manager' => $match['operation_department_head'], ] as $field => $value) { if ($value !== '') { $row[$field] = $value; } } } unset($row); return $rows; } /** @param array> $filters */ private function pageStoreInformation(int $page, int $perPage, array $filters, array $rows): array { $rows = array_values(array_filter( $rows, fn (array $row): bool => $this->storeOwnershipMatchesFilters($row, $filters), )); $summary = $this->storeInformationSummary($rows); $this->sortStoreInformation($rows, $filters); $total = count($rows); $offset = ($page - 1) * $perPage; return [ 'data' => array_slice($rows, $offset, $perPage), 'total' => $total, 'summary' => $summary, ]; } /** @param list> $rows */ private function storeInformationSummary(array $rows): array { $stores = []; $platforms = []; foreach ($rows as $row) { $key = trim((string) ($row['id'] ?? $row['erp_store_name'] ?? $row['store_name'] ?? '')); if ($key === '') { continue; } $stores[$key] = match ((string) ($row['store_status'] ?? '')) { '0', '正常' => '正常', '3', '关闭中' => '关闭中', '6', '已关闭', '关店' => '已关闭', default => (string) ($row['store_status'] ?? ''), }; $platform = trim((string) ($row['platform'] ?? '')); if ($platform !== '') { $platforms[$platform] = true; } } $statusCounts = array_count_values($stores); return [ 'total' => count($stores), 'normal' => (int) ($statusCounts['正常'] ?? 0), 'closing' => (int) ($statusCounts['关闭中'] ?? 0), 'closed' => (int) ($statusCounts['已关闭'] ?? 0), 'platforms' => count($platforms), ]; } /** @param array> $filters */ private function storeOwnershipMatchesFilters(array $row, array $filters): bool { foreach (['store_manager', 'department_manager'] as $key) { $expected = trim((string) ($filters[$key] ?? '')); if ($expected !== '' && mb_stripos((string) ($row[$key] ?? ''), $expected) === false) { return false; } } return true; } /** @param list> $rows @param array> $filters */ private function sortStoreInformation(array &$rows, array $filters): void { $key = (string) ($filters['_sort'] ?? ''); if ($key === '') { return; } $direction = ($filters['_direction'] ?? 'asc') === 'desc' ? -1 : 1; usort($rows, static function (array $left, array $right) use ($key, $direction): int { $comparison = strnatcasecmp( (string) ($left[$key] ?? ''), (string) ($right[$key] ?? ''), ) * $direction; return $comparison !== 0 ? $comparison : ((int) ($right['id'] ?? 0) <=> (int) ($left['id'] ?? 0)); }); } private function storeReportPage(int $page, int $perPage): array { $filters = StoreReportRepository::requestFilters(); $rows = $this->storeReportRows($filters); // 整列为 0 的月份视为没有销量,默认隐藏;仍可从「筛选字段」手动打开。 request()->attributes->set( 'store-report.default_hidden_columns', $this->columnsWithoutData($rows, 'sales_', zeroCountsAsData: false), ); $offset = ($page - 1) * $perPage; return [ 'data' => array_slice($rows, $offset, $perPage), 'total' => count($rows), 'summary' => $this->storeReportSummary($rows), ]; } /** @param list> $rows */ private function storeReportSummary(array $rows): array { $normalStores = []; $previousMonth = now()->startOfMonth()->subMonthNoOverflow(); $currentMonth = now()->startOfMonth(); $previousMonthKey = 'sales_' . $previousMonth->format('m'); $currentMonthKey = 'sales_' . $currentMonth->format('m'); $previousSales = 0.0; $currentSales = 0.0; $previousStores = []; $currentStores = []; foreach ($rows as $row) { if (trim((string) ($row['status'] ?? '')) !== '开店中') { continue; } $key = trim((string) ($row['erp_store_name'] ?? '')); if ($key === '') { $key = trim((string) ($row['store_id'] ?? $row['id'] ?? '')); } if ($key === '') { continue; } $normalStores[$key] = true; $previousValue = (float) ($row[$previousMonthKey] ?? 0); $currentValue = (float) ($row[$currentMonthKey] ?? 0); $previousSales += $previousValue; $currentSales += $currentValue; if ($previousValue > 0) { $previousStores[$key] = true; } if ($currentValue > 0) { $currentStores[$key] = true; } } return [ 'normal_stores' => count($normalStores), 'previous_month_sales' => round($previousSales, 2), 'current_month_sales' => round($currentSales, 2), 'previous_month_stores' => count($previousStores), 'current_month_stores' => count($currentStores), 'previous_month_key' => $previousMonthKey, 'current_month_key' => $currentMonthKey, 'previous_month_label' => $previousMonth->isSameYear($currentMonth) ? '上月(' . $previousMonth->format('n月') . ')' : '上月(' . $previousMonth->format('Y年n月') . ')', 'current_month_label' => '当月(' . $currentMonth->format('n月') . ')', ]; } /** @return array{data:array>,total:int} */ private function storeAccountPage(int $page, int $perPage): array { try { return ($this->storeAccounts ?? app(StoreAccountMirrorRepository::class))->paginate( $page, $perPage, $this->pageFilters('store-management', 'store-accounts'), ); } catch (\RuntimeException $exception) { report($exception); throw new ServiceUnavailableHttpException(null, '店铺账号共享镜像尚未完成首次同步。'); } } /** @param array $filters @return array{data:array>,total:int} */ private function allStores(array $filters = []): array { if ($this->mirror->ready('store-information.read')) { $rows = $this->mirror->allStores($filters); return ['data' => array_map(fn (array $row): array => $this->mapStore($row), $rows), 'total' => count($rows)]; } $cached = $this->caches->forSource(self::SOURCE)->remember('store-information.read', ['all' => true], fn () => $this->storeSnapshotAll(new UpstreamReadOnlyQuery(self::SOURCE))); $rows = array_values(array_filter( $cached['data'], fn (array $row): bool => $this->storeMatchesFilters($row, $filters), )); return ['data' => $rows, 'total' => count($rows)]; } /** * 镜像不可用时的兜底路径:在已映射的快照行上应用同样的店铺字段条件。 * * @param array $row * @param array $filters */ private function storeMatchesFilters(array $row, array $filters): bool { foreach (['erp_store_name', 'store_name', 'store_id', 'profit_center', 'store_manager', 'department_manager'] as $key) { $expected = trim((string) ($filters[$key] ?? '')); if ($expected === '') { continue; } $actual = trim((string) ($row[$key] ?? '')); if (! str_contains(mb_strtolower($actual), mb_strtolower($expected))) { return false; } } $companyType = trim((string) ($filters['company_type'] ?? '')); if ($companyType !== '' && trim((string) ($row['company_type'] ?? '')) !== $companyType) { return false; } $statusMap = ['0' => '正常', '3' => '关闭中', '6' => '已关闭']; $status = trim((string) ($filters['status'] ?? '')); if ($status !== '' && (string) ($row['store_status'] ?? '') !== ($statusMap[$status] ?? $status)) { return false; } $from = trim((string) ($filters['opened_at_from'] ?? '')); $to = trim((string) ($filters['opened_at_to'] ?? '')); if ($from !== '' || $to !== '') { $value = trim((string) ($row['opened_at'] ?? '')); if ($value === '') { return false; } if ($from !== '' && strcmp($value, $from) < 0) { return false; } if ($to !== '' && strcmp($value, $to) >= 0) { return false; } } return true; } private function storeSnapshotAll(UpstreamReadOnlyQuery $query): array { $rows = $query->select(<< $this->mapStore($row), $rows); return ['data' => $rows, 'total' => count($rows)]; } private function subjectOperatingReportPage(int $page, int $perPage): array { $filters = $this->pageFilters('company-management', 'subject-operating-report'); $companyStatusReady = $this->mirror->ready('company-information.read'); $companyStatus = trim((string) request('company_status', '')); if (in_array($companyStatus, ['正常', '已注销'], true)) { $filters['company_status'] = $companyStatus; } $rows = $this->storeReportRows(array_intersect_key($filters, array_flip(['company_type']))); $entities = []; foreach ($rows as $row) { $entity = trim((string) ($row['entity'] ?? '')); if ($entity === '') { continue; } $entities[$entity] ??= [ 'city' => null, 'entity' => $entity, 'company_type' => $row['company_type'] ?? null, 'rolling_12m_income' => null, 'can_change_entity' => null, 'plan' => null, 'new_entity' => null, ]; if (empty($entities[$entity]['company_type']) && ! empty($row['company_type'])) { $entities[$entity]['company_type'] = $row['company_type']; } $this->sumNullable($entities[$entity], 'rolling_12m_income', $row['rolling_12m_income'] ?? null); for ($month = 1; $month <= 12; $month++) { $key = sprintf('income_%02d', $month); $entities[$entity][$key] ??= null; $this->sumNullable($entities[$entity], $key, $row[$key] ?? null); } } $rows = array_values($entities); $details = $this->companyDetails(array_column($rows, 'entity')); try { $classifications = $this->mirror->companyClassificationsByNames(array_column($rows, 'entity')); } catch (Throwable) { $classifications = []; } foreach ($rows as &$row) { $row['company_details'] = $details[$row['entity']] ?? []; $row['city'] = $this->cityFromDetails($row['company_details']); $row['company_status'] = $row['company_details'][0]['company_status'] ?? null; $classification = $classifications[$row['entity']] ?? []; $taxType = $classification['tax_type'] ?? $row['company_details'][0]['taxpayer_category'] ?? null; $businessType = $classification['business_registration_type'] ?? $row['company_details'][0]['business_registration_type'] ?? null; $classificationStatus = trim((string) ($classification['status'] ?? '')); $retained = $classificationStatus !== '' ? $classificationStatus === '保留' : $row['company_status'] === '正常'; $row['company_status'] = $retained ? '正常' : '已注销'; // Excel 台账明确有税务类型时始终展示;没有时再回退到上游公司资料。 $row['tax_type'] = $this->normalizeTaxType($taxType) ?? '/'; $businessType = trim((string) $businessType); $row['business_registration_type'] = $retained && in_array($businessType, ['核定', '个人独资企业'], true) ? $businessType : '/'; } unset($row); // 整列为空的月份默认隐藏;金额 0 仍视为有账单,不隐藏。 request()->attributes->set( 'subject-operating-report.default_hidden_columns', $this->columnsWithoutData($rows, 'income_', zeroCountsAsData: true), ); $rows = array_values(array_filter($rows, fn (array $row): bool => $this->subjectMatchesFilters($row, $filters))); $summary = $this->subjectSummary($rows, $companyStatusReady); $this->sortSubjectRows($rows, $filters); $offset = ($page - 1) * $perPage; $pageRows = array_slice($rows, $offset, $perPage); return ['data' => $pageRows, 'total' => count($rows), 'summary' => $summary]; } /** @param list> $rows */ private function subjectSummary(array $rows, bool $companyStatusReady): array { $normal = 0; $deregistered = 0; $smallTaxpayers = 0; $generalTaxpayers = 0; $previousMonth = now()->startOfMonth()->subMonthNoOverflow(); $currentMonth = now()->startOfMonth(); $previousMonthKey = 'income_' . $previousMonth->format('m'); $currentMonthKey = 'income_' . $currentMonth->format('m'); $previousIncome = 0.0; $currentMonthIncome = 0.0; $previousIncomeEntities = 0; $currentIncomeEntities = 0; foreach ($rows as $row) { $status = trim((string) ($row['company_status'] ?? '')); if ($status === '正常') { $normal++; } elseif ($status === '已注销') { $deregistered++; } if ($status !== '正常') { continue; } $taxType = (string) ($row['tax_type'] ?? ''); if (str_contains($taxType, '小规模')) { $smallTaxpayers++; } if (str_contains($taxType, '一般纳税人')) { $generalTaxpayers++; } $previousValue = (float) ($row[$previousMonthKey] ?? 0); $currentValue = (float) ($row[$currentMonthKey] ?? 0); $previousIncome += $previousValue; $currentMonthIncome += $currentValue; if ($previousValue > 0) { $previousIncomeEntities++; } if ($currentValue > 0) { $currentIncomeEntities++; } } return [ 'total' => count($rows), 'normal' => $normal, 'deregistered' => $deregistered, 'small_taxpayer' => $smallTaxpayers, 'general_taxpayer' => $generalTaxpayers, 'previous_income' => round($previousIncome, 2), 'current_month_income' => round($currentMonthIncome, 2), 'previous_income_entities' => $previousIncomeEntities, 'current_income_entities' => $currentIncomeEntities, 'previous_month_key' => $previousMonthKey, 'current_month_key' => $currentMonthKey, 'previous_month_label' => $previousMonth->isSameYear($currentMonth) ? '上月(' . $previousMonth->format('n月') . ')' : '上月(' . $previousMonth->format('Y年n月') . ')', 'current_month_label' => '当月(' . $currentMonth->format('n月') . ')', 'company_status_ready' => $companyStatusReady, ]; } private function normalizeTaxType(mixed $value): ?string { $taxType = trim((string) $value); if ($taxType === '') { return null; } return match ($taxType) { '小规模' => '小规模纳税人', '一般纳税人', '小规模纳税人', '个体工商户', '无税务登记' => $taxType, default => $taxType, }; } /** * 返回整列都没有数据的月份列;这类列只默认隐藏,字段和数据不会被删除。 * * @param list> $rows * @return list */ private function columnsWithoutData(array $rows, string $prefix, bool $zeroCountsAsData): array { $columns = []; for ($month = 1; $month <= 12; $month++) { $key = sprintf('%s%02d', $prefix, $month); foreach ($rows as $row) { $value = $row[$key] ?? null; if ($value === null || $value === '') { continue; } if (! $zeroCountsAsData && (float) $value === 0.0) { continue; } continue 2; } $columns[] = $key; } return $columns; } /** @param array $row @param array $filters */ private function subjectMatchesFilters(array $row, array $filters): bool { $companyStatus = trim((string) ($filters['company_status'] ?? '')); if ($companyStatus !== '' && (string) ($row['company_status'] ?? '') !== $companyStatus) { return false; } if (($filters['company_type'] ?? '') !== '' && (string) ($row['company_type'] ?? '') !== (string) $filters['company_type']) { return false; } foreach (['city', 'entity', 'tax_type', 'business_registration_type', 'plan', 'new_entity'] as $key) { $needle = trim((string) ($filters[$key] ?? '')); if ($needle !== '' && mb_stripos((string) ($row[$key] ?? ''), $needle) === false) { return false; } } foreach (['rolling_12m_income', ...array_map(static fn (int $month): string => sprintf('income_%02d', $month), range(1, 12))] as $key) { $needle = str_replace(',', '', trim((string) ($filters[$key] ?? ''))); if ($needle !== '' && (! is_numeric($needle) || ! is_numeric($row[$key] ?? null) || abs((float) $row[$key] - (float) $needle) > 0.000001)) { return false; } } $canChange = trim((string) ($filters['can_change_entity'] ?? '')); return $canChange === '' || (string) ($row['can_change_entity'] ?? '') === $canChange; } /** @param array> $rows @param array $filters */ private function sortSubjectRows(array &$rows, array $filters): void { $key = (string) ($filters['_sort'] ?? 'entity'); $allowed = [ 'city', 'entity', 'company_type', 'tax_type', 'business_registration_type', 'rolling_12m_income', ...array_map(static fn (int $month): string => sprintf('income_%02d', $month), range(1, 12)), 'can_change_entity', 'plan', 'new_entity', ]; if (! in_array($key, $allowed, true)) { return; } $direction = ($filters['_direction'] ?? 'asc') === 'desc' ? -1 : 1; usort($rows, static function (array $left, array $right) use ($key, $direction): int { $leftValue = $left[$key] ?? null; $rightValue = $right[$key] ?? null; if (is_numeric($leftValue) && is_numeric($rightValue)) { $comparison = ((float) $leftValue <=> (float) $rightValue) * $direction; } else { $comparison = strcmp((string) ($leftValue ?? ''), (string) ($rightValue ?? '')) * $direction; } return $comparison !== 0 ? $comparison : strcmp((string) ($left['entity'] ?? ''), (string) ($right['entity'] ?? '')); }); } private function sumNullable(array &$row, string $key, mixed $value): void { if ($value === null || $value === '') { return; } $row[$key] = (float) ($row[$key] ?? 0) + (float) $value; } /** * 「市」来自主体基础资料的注册地址:取第一条能解析出地级市的记录 * (详情按 Id 倒序,即优先最新资料);资料缺失或地址无法识别时返回 null, * 页面按统一缺失值展示。 * * @param array> $details */ private function cityFromDetails(array $details): ?string { foreach ($details as $detail) { $city = RegisteredAddressCity::parse($detail['registered_address'] ?? null); if ($city !== null) { return $city; } } return null; } /** @return array>> */ private function companyDetails(array $names): array { $names = array_values(array_unique(array_filter(array_map('trim', $names)))); if ($names === []) { return []; } try { if ($this->mirror->ready('company-information.read')) { $rows = $this->mirror->companiesByNames($names); } else { $placeholders = implode(',', array_fill(0, count($names), '?')); $rawRows = (new UpstreamReadOnlyQuery(self::SOURCE))->select( "SELECT * FROM rpt.fys_companyInfo WHERE businessLicenseName IN ($placeholders) ORDER BY Id DESC", $names, ); $rows = []; foreach ($rawRows as $rawRow) { $rows[(string) ($rawRow['businessLicenseName'] ?? '')][] = $rawRow; } } } catch (Throwable) { return []; } $result = []; foreach ($rows as $name => $companyRows) { $result[$name] = array_map(fn (array $row): array => $this->mapCompanyDetail($row), $companyRows); } return $result; } private function mapCompanyDetail(array $row): array { return $this->mapCompany([ 'id' => $row['Id'] ?? $row['id'] ?? null, 'unified_social_credit_code' => $row['unifiedSocialCreditCode'] ?? $row['unified_social_credit_code'] ?? null, 'company_name' => $row['businessLicenseName'] ?? $row['company_name'] ?? null, 'subscribed_capital' => $row['registeredCapital'] ?? $row['subscribed_capital'] ?? null, 'company_type' => $row['businessLicenseType'] ?? $row['company_type'] ?? null, 'establishment_date' => $row['IncorporationDate'] ?? $row['establishment_date'] ?? null, 'legal_representative' => $row['legalRepresentative'] ?? $row['legal_representative'] ?? null, 'registered_address' => $row['registeredAddress'] ?? $row['registered_address'] ?? null, 'business_scope' => $row['businessScope'] ?? $row['business_scope'] ?? null, 'basic_deposit_account_number' => $row['accountSerialNumber'] ?? $row['basic_deposit_account_number'] ?? null, 'subscribed_at' => $row['registeredCapitalVaildTime'] ?? $row['subscribed_at'] ?? null, 'company_status' => $row['companyStatus'] ?? $row['company_status'] ?? null, 'financial_officer' => $row['financialManager'] ?? $row['financial_officer'] ?? null, 'supervisor' => $row['supervisor'] ?? null, 'taxpayer_category' => $row['finance_tax_type'] ?? $row['taxCategoryAndType'] ?? $row['taxpayer_category'] ?? null, 'business_registration_type' => $row['business_registration_type'] ?? null, 'shareholders' => $row['shareholders'] ?? null, ]); } /** @param array $filters @return array> */ private function storeReportRows(array $filters = []): array { // 店铺负责人、部门负责人、利润中心展示店铺账号镜像(PostgreSQL)中的值; // 这三个字段的筛选条件随行级过滤按覆盖后的值匹配,不再走镜像 SQL。 $ownership = $this->storeOwnership(); $storeFilters = array_diff_key($filters, array_flip(['store_manager', 'department_manager', 'profit_center'])); $stores = $this->allStores($storeFilters); foreach ($stores['data'] as &$store) { $key = StoreAccountMirrorRepository::storeNameKey($store['erp_store_name'] ?? ''); $match = $ownership[$key] ?? null; if ($match === null) { continue; } // 未匹配到账号或字段为空时保留经营分析副本库原值。 foreach ([ 'store_manager' => $match['operation_owner'], 'department_manager' => $match['operation_department_head'], 'profit_center' => $match['profit_center'], ] as $field => $value) { if ($value !== '') { $store[$field] = $value; } } } unset($store); try { return ($this->storeReports ?? app(StoreReportRepository::class))->rows($stores['data'], null, $filters); } catch (\RuntimeException $exception) { report($exception); throw new ServiceUnavailableHttpException(null, '店铺报表数据暂时无法读取,请稍后重试。'); } } /** @return array */ private function storeOwnership(bool $requireAccountOwnership = false): array { try { return ($this->storeAccounts ?? app(StoreAccountMirrorRepository::class))->ownershipByStoreName(); } catch (\RuntimeException | \InvalidArgumentException $exception) { if ($requireAccountOwnership) { throw new \RuntimeException('店铺账号密码共享镜像尚未就绪,无法关联负责人。', 0, $exception); } // 账号镜像未登记或未就绪时保留经营分析副本库原值,不阻断店铺报表。 return []; } } private function storeSnapshot(UpstreamReadOnlyQuery $query, int $page, int $perPage): array { $offset = ($page - 1) * $perPage; $rows = $query->select(<< array_map(fn (array $row) => $this->mapStore($row), $rows), 'total' => (int) $query->scalar('SELECT COUNT(*) FROM rpt.fys_shopInfo'), ]; } private function trademarkPage(int $page, int $perPage): array { $filters = $this->pageFilters('trademark-management', 'information'); return $this->page( 'trademark-information.read', $page, $perPage, $filters, fn () => $this->snapshot('trademark-information.read', [ 'page' => $page, 'perPage' => $perPage, ]), ); } private function trademarkSnapshot(UpstreamReadOnlyQuery $query, int $page, int $perPage): array { $offset = ($page - 1) * $perPage; $rows = $query->select(<< array_map(fn (array $row) => $this->mapTrademark($row), $rows), 'total' => (int) $query->scalar('SELECT COUNT(*) FROM rpt.fys_TrademarkInfo WHERE isDelete = 0'), ]; } private function page( string $scope, int $page, int $perPage, array $filters, callable $resolver, ?bool $mirrorReady = null, ): array { if ($mirrorReady ?? $this->mirror->ready($scope)) { return $this->normalize($scope, $this->mirror->paginate($scope, $page, $perPage, $filters)); } if ($filters !== []) { throw new ServiceUnavailableHttpException(null, '当前数据源暂不支持所选条件查询,请检查共享业务镜像状态。'); } if ((app(\App\Utilities\SystemDatabaseConfiguration::class)->current()['mode'] ?? '') === 'bootstrap' && ! config('upstreams.local_configuration.enabled', false)) { throw new ServiceUnavailableHttpException(null, '当前仍使用本机临时引导库。请在“系统设置 → 数据库管理 → 系统数据库”连接团队共享数据库,然后重新登录;无需另配上游 SQL Server。'); } $cache = $this->caches->forSource(self::SOURCE); return $cache->remember($scope, compact('page', 'perPage'), function () use ($resolver) { return $resolver(new UpstreamReadOnlyQuery(self::SOURCE)); }); } /** @return array> */ private function pageFilters(string $section, string $page): array { $fields = collect(config('business_navigation')) ->firstWhere('slug', $section)['pages'] ?? []; $pageConfig = collect($fields)->flatMap(fn (array $item) => [$item, ...($item['children'] ?? [])])->firstWhere('slug', $page); $allowed = collect($pageConfig['directory']['fields'] ?? []) ->reject(fn (array $field) => ($field['kind'] ?? '') === 'secret' && ($field['name'] ?? '') !== 'withdrawal_account') ->pluck('name')->all(); if ($section === 'company-management' && $page === 'basic-information') { $allowed = array_values(array_unique([...$allowed, 'company_type', 'related_people'])); } $filters = []; foreach ($allowed as $name) { foreach ([$name, $name . '_from', $name . '_to'] as $key) { $raw = request($key, ''); // 多选筛选(xxx[])传进来是数组:去重后原样带下去,由各自的仓库取并集。 if (is_array($raw)) { $values = []; foreach ($raw as $item) { if (! is_scalar($item)) continue; $item = mb_substr(trim((string) $item), 0, 191); if ($item !== '') $values[$item] = true; if (count($values) >= 50) break; } if ($values !== []) $filters[$key] = array_keys($values); continue; } $value = trim((string) $raw); if ($value !== '') $filters[$key] = mb_substr($value, 0, 191); } } $sort = trim((string) request('sort', '')); if (in_array($sort, $allowed, true)) { $filters['_sort'] = $sort; $filters['_direction'] = request('direction') === 'desc' ? 'desc' : 'asc'; } return $filters; } /** @param array{data: array>, total: int} $payload */ private function normalize(string $scope, array $payload): array { $mapper = match ($scope) { 'company-information.read' => fn (array $row): array => $this->mapCompany($row), 'store-information.read' => fn (array $row): array => $this->mapStore($row), 'trademark-information.read' => fn (array $row): array => $this->mapTrademark($row), default => throw new InvalidArgumentException('未知的经营分析副本库资料类型:' . $scope), }; return [ 'data' => array_map($mapper, $payload['data']), 'total' => (int) $payload['total'], 'summary' => (array) ($payload['summary'] ?? []), ]; } private function mapCompany(array $row): array { $row = $this->toUtf8($row); $row['establishment_date'] = $this->date($row['establishment_date'] ?? null); $row['subscribed_at'] = $this->date($row['subscribed_at'] ?? null); $row['city'] = RegisteredAddressCity::parse($row['registered_address'] ?? null); $row['company_status'] = match ((string) ($row['company_status'] ?? '')) { '0' => '正常', '1' => '已注销', default => $row['company_status'] ?? null, }; $businessType = trim((string) ($row['business_registration_type'] ?? '')); $row['business_registration_type'] = in_array($businessType, ['核定', '个人独资企业'], true) ? $businessType : '/'; return $row; } private function mapStore(array $row): array { $row = $this->toUtf8($row); $row['opened_at'] = $this->date($row['opened_at'] ?? null); $row['renewed_at'] = $this->date($row['renewed_at'] ?? null); $row['store_status'] = match ((string) ($row['store_status'] ?? '')) { '0' => '正常', '3' => '关闭中', '6' => '已关闭', default => $row['store_status'] ?? null, }; return $row; } private function mapTrademark(array $row): array { $row = $this->toUtf8($row); $row['registration_date'] = $this->date($row['registration_date'] ?? null); $row['expiry_date'] = $this->date($row['expiry_date'] ?? null); $row['trademark_status'] = match ((string) ($row['trademark_status'] ?? '')) { '0' => '未启用', '1' => '有效', default => $row['trademark_status'] ?? null, }; return $row; } private function toUtf8(array $row): array { foreach ($row as &$value) { if (is_string($value) && ! mb_check_encoding($value, 'UTF-8')) { $value = mb_convert_encoding($value, 'UTF-8', 'GB18030'); } } unset($value); return $row; } private function date($value): ?string { if ($value === null || $value === '') { return null; } return substr((string) $value, 0, 10); } }