'operations_replica.rpt.fys_companyInfo', 'store-information.read' => 'operations_replica.rpt.fys_shopInfo', 'trademark-information.read' => 'operations_replica.rpt.fys_TrademarkInfo', ]; private readonly ConnectionInterface $connection; public function __construct( private readonly BusinessMirrorDatasetRegistry $datasets, private readonly GenericBusinessMirrorRepository $mirror, ) { $this->connection = $mirror->connection(); } public function ready(string $scope): bool { return $this->mirror->ready($this->dataset($scope)); } /** @return array{data: array>, total: int, summary?: array} */ public function paginate(string $scope, int $page, int $perPage, array $filters = []): array { if (! $this->ready($scope)) { throw new RuntimeException('business_mirror_dataset_not_ready'); } $page = max($page, 1); $perPage = min(max($perPage, 1), 100); return match ($scope) { 'company-information.read' => $this->companies($page, $perPage, $filters), 'store-information.read' => $this->stores($page, $perPage, $filters), 'trademark-information.read' => $this->trademarks($page, $perPage, $filters), default => throw new InvalidArgumentException('未知的业务资料镜像范围:' . $scope), }; } /** @param array $filters @return array> */ public function allStores(array $filters = []): array { if (! $this->ready('store-information.read')) { throw new RuntimeException('business_mirror_dataset_not_ready'); } $query = $this->storeQuery()->orderByDesc('s.Id'); $this->applyStoreFilters($query, $filters); return $query ->get() ->map(static fn (object $row): array => (array) $row) ->all(); } /** * Return the stable store-information rows used to seed the receiving * account pages. The query stays on the shared business mirror and only * reads the approved shop/company fields. * * @return list> */ public function receivingAccountStores(): array { if (! $this->ready('store-information.read')) { throw new RuntimeException('business_mirror_dataset_not_ready'); } $latestCompanies = $this->connection->table('ops_fys_company_info') ->selectRaw('unifiedSocialCreditCode, MAX(Id) AS latest_id') ->whereNotNull('unifiedSocialCreditCode') ->where('unifiedSocialCreditCode', '<>', '') ->groupBy('unifiedSocialCreditCode'); return $this->connection->table('ops_fys_shop_info AS s') ->leftJoinSub($latestCompanies, 'company_keys', function ($join): void { $join->on('company_keys.unifiedSocialCreditCode', '=', 's.unifiedSocialCreditCode'); }) ->leftJoin('ops_fys_company_info AS c', 'c.Id', '=', 'company_keys.latest_id') ->selectRaw(<<<'SQL' s.Id AS id, s.sotreId AS store_id, s.shopName AS shop_name, s.erpShopName AS erp_store_name, s.unifiedSocialCreditCode AS unified_social_credit_code, c.businessLicenseName AS company_name, s.platform AS source_platform, s.shopState AS store_status, s.shopType AS store_type SQL) ->orderBy('s.Id') ->get() ->map(static fn (object $row): array => (array) $row) ->all(); } /** 店铺维度报表的店铺字段查询条件;日期左闭右开 [from, to)。 */ private function applyStoreFilters($query, array $filters): void { foreach ([ 'erp_store_name' => 's.erpShopName', 'store_name' => 's.shopName', 'store_id' => 's.sotreId', 'profit_center' => 's.responsibleDepartment', 'store_manager' => 's.shopHead', 'department_manager' => 's.customerDeptManager', ] as $key => $column) { $value = trim((string) ($filters[$key] ?? '')); if ($value !== '') { $query->where($column, 'like', '%' . addcslashes($value, '%_\\') . '%'); } } if (trim((string) ($filters['company_type'] ?? '')) !== '') { $query->where('c.businessLicenseType', '=', trim((string) $filters['company_type'])); } $status = trim((string) ($filters['store_status'] ?? '')); if ($status === '') { $status = trim((string) ($filters['status'] ?? '')); } if ($status !== '') { $statusCode = ['正常' => '0', '关闭中' => '3', '已关闭' => '6'][$status] ?? $status; $query->where('s.shopState', '=', $statusCode); } if (trim((string) ($filters['opened_at_from'] ?? '')) !== '') { $query->where('s.openTime', '>=', $filters['opened_at_from']); } if (trim((string) ($filters['opened_at_to'] ?? '')) !== '') { $query->where('s.openTime', '<', $filters['opened_at_to']); } } /** @return array>> */ public function companiesByNames(array $names): array { if (! $this->ready('company-information.read')) { throw new RuntimeException('business_mirror_dataset_not_ready'); } $names = array_values(array_unique(array_filter(array_map('trim', $names)))); if ($names === []) { return []; } $classifications = $this->companyClassificationsByNames($names); return $this->connection->table('ops_fys_company_info') ->whereIn('businessLicenseName', $names) ->orderByDesc('Id') ->get() ->map(function (object $row) use ($classifications): array { $result = (array) $row; $classification = $classifications[trim((string) $row->businessLicenseName)] ?? []; $result['finance_tax_type'] = $classification['tax_type'] ?? null; $result['business_registration_type'] = $classification['business_registration_type'] ?? null; return $result; }) ->groupBy(static fn (array $row): string => (string) $row['businessLicenseName']) ->map(static fn ($rows): array => $rows->values()->all()) ->all(); } /** @return array */ public function companyClassificationsByNames(array $names): array { $names = array_values(array_unique(array_filter(array_map('trim', $names)))); if ($names === [] || ! $this->connection->getSchemaBuilder()->hasTable('fin_companies')) { return []; } return $this->connection->table('fin_companies') ->select(['name', 'status', 'tax_type', 'business_type']) ->whereIn('name', $names) ->get() ->mapWithKeys(static function (object $row): array { $businessType = trim((string) ($row->business_type ?? '')); $status = trim((string) ($row->status ?? '')); return [trim((string) $row->name) => [ 'status' => $status !== '' ? $status : null, 'tax_type' => trim((string) ($row->tax_type ?? '')) ?: null, 'business_registration_type' => in_array($businessType, ['核定', '个人独资企业'], true) ? $businessType : '/', ]]; }) ->all(); } /** @return array{data: array>, total: int, summary: array} */ private function companies(int $page, int $perPage, array $filters): array { $query = $this->connection->table('ops_fys_company_info AS company') ->leftJoin('fin_companies AS finance_company', 'finance_company.name', '=', 'company.businessLicenseName') ->selectRaw(<<<'SQL' company.Id AS id, company.unifiedSocialCreditCode AS unified_social_credit_code, company.businessLicenseName AS company_name, company.registeredCapital AS subscribed_capital, company.businessLicenseType AS company_type, company.IncorporationDate AS establishment_date, company.legalRepresentative AS legal_representative, company.registeredAddress AS registered_address, company.businessScope AS business_scope, company.accountSerialNumber AS basic_deposit_account_number, company.registeredCapitalVaildTime AS subscribed_at, company.companyStatus AS company_status, company.financialManager AS financial_officer, company.supervisor AS supervisor, COALESCE(finance_company.tax_type, company.taxCategoryAndType) AS taxpayer_category, finance_company.business_type AS business_registration_type, NULL AS shareholders SQL); $this->applyFieldFilters($query, $filters, [ 'unified_social_credit_code' => 'company.unifiedSocialCreditCode', 'company_name' => 'company.businessLicenseName', 'establishment_date' => 'company.IncorporationDate', 'registered_at' => 'company.IncorporationDate', 'legal_representative' => 'company.legalRepresentative', 'registered_address' => 'company.registeredAddress', 'city' => 'company.registeredAddress', 'business_scope' => 'company.businessScope', 'subscribed_at' => 'company.registeredCapitalVaildTime', 'financial_officer' => 'company.financialManager', 'billing_officer' => 'company.financialManager', 'supervisor' => 'company.supervisor', ], ['subscribed_capital' => 'company.registeredCapital'], [ 'company_type' => 'company.businessLicenseType', 'taxpayer_type' => 'company.taxCategoryAndType', 'taxpayer_category' => 'company.taxCategoryAndType', 'business_registration_type' => 'finance_company.business_type', ]); $companyStatus = trim((string) ($filters['company_status'] ?? '')); if ($companyStatus !== '') { $statusCode = ['正常' => '0', '已注销' => '1'][$companyStatus] ?? $companyStatus; $query->where('company.companyStatus', '=', $statusCode); } $relatedPeople = mb_substr(trim((string) ($filters['related_people'] ?? '')), 0, 191); if ($relatedPeople !== '') { $like = '%' . addcslashes($relatedPeople, '%_\\') . '%'; $query->where(function ($query) use ($like): void { $query->where('company.legalRepresentative', 'like', $like) ->orWhere('company.supervisor', 'like', $like) ->orWhere('company.financialManager', 'like', $like); }); } $summary = $this->summaryCounts($query, <<<'SQL' COUNT(*) AS total, COALESCE(SUM(CASE WHEN company.companyStatus = '0' THEN 1 ELSE 0 END), 0) AS normal, COALESCE(SUM(CASE WHEN company.companyStatus = '1' THEN 1 ELSE 0 END), 0) AS deregistered, COALESCE(SUM(CASE WHEN COALESCE(finance_company.tax_type, company.taxCategoryAndType) = ? THEN 1 ELSE 0 END), 0) AS general_taxpayer, COALESCE(SUM(CASE WHEN COALESCE(finance_company.tax_type, company.taxCategoryAndType) = ? THEN 1 ELSE 0 END), 0) AS small_taxpayer SQL, ['一般纳税人', '小规模纳税人']); return [ ...$this->page($query, $page, $perPage, 'company.Id', $filters, [ 'unified_social_credit_code' => 'company.unifiedSocialCreditCode', 'company_name' => 'company.businessLicenseName', 'taxpayer_type' => 'company.taxCategoryAndType', 'company_status' => 'company.companyStatus', 'subscribed_capital' => 'company.registeredCapital', 'registered_at' => 'company.IncorporationDate', 'legal_representative' => 'company.legalRepresentative', 'supervisor' => 'company.supervisor', 'billing_officer' => 'company.financialManager', 'registered_address' => 'company.registeredAddress', 'city' => 'company.registeredAddress', 'business_scope' => 'company.businessScope', 'basic_deposit_account' => 'company.accountSerialNumber', 'subscribed_at' => 'company.registeredCapitalVaildTime', 'business_registration_type' => 'finance_company.business_type', ]), 'summary' => $summary, ]; } /** @return array{data: array>, total: int, summary: array} */ private function stores(int $page, int $perPage, array $filters): array { $query = $this->storeQuery(); $this->applyFieldFilters($query, $filters, [ 'store_id' => 's.sotreId', 'store_name' => 's.erpShopName', 'store_entity' => 'c.businessLicenseName', 'trademark' => 't.trademarkName', 'trademark_number' => 's.trademarkNumber', 'business_department' => 's.responsibleDepartment', 'store_manager' => 's.shopHead', 'department_manager' => 's.customerDeptManager', 'opened_at' => 's.openTime', 'renewed_at' => 's.renewalTime', ], [], ['platform' => 's.platform']); $storeStatus = trim((string) ($filters['store_status'] ?? '')); if ($storeStatus !== '') { $statusCode = ['正常' => '0', '关闭中' => '3', '已关闭' => '6'][$storeStatus] ?? $storeStatus; $query->where('s.shopState', '=', $statusCode); } $withdrawalAccount = trim((string) ($filters['withdrawal_account'] ?? '')); if ($withdrawalAccount === '已配置') { $query->whereNotNull('s.shopBankAccount')->where('s.shopBankAccount', '<>', ''); } elseif ($withdrawalAccount === '未配置') { $query->where(function ($query): void { $query->whereNull('s.shopBankAccount')->orWhere('s.shopBankAccount', '=', ''); }); } $summary = $this->summaryCounts($query, <<<'SQL' COUNT(*) AS total, COALESCE(SUM(CASE WHEN s.shopState = 0 THEN 1 ELSE 0 END), 0) AS normal, COALESCE(SUM(CASE WHEN s.shopState = 3 THEN 1 ELSE 0 END), 0) AS closing, COALESCE(SUM(CASE WHEN s.shopState = 6 THEN 1 ELSE 0 END), 0) AS closed, COUNT(DISTINCT CASE WHEN s.platform IS NOT NULL AND s.platform <> '' THEN s.platform END) AS platforms SQL); return [ ...$this->page($query, $page, $perPage, 's.Id', $filters, [ 'platform' => 's.platform', 'store_id' => 's.sotreId', 'store_name' => 's.erpShopName', 'store_entity' => 'c.businessLicenseName', 'store_status' => 's.shopState', 'trademark' => 't.trademarkName', 'trademark_number' => 's.trademarkNumber', 'business_department' => 's.responsibleDepartment', 'store_manager' => 's.shopHead', 'department_manager' => 's.customerDeptManager', 'opened_at' => 's.openTime', 'renewed_at' => 's.renewalTime', 'withdrawal_account' => 's.shopBankAccount', ]), 'summary' => $summary, ]; } private function storeQuery() { $latestCompanies = $this->connection->table('ops_fys_company_info') ->selectRaw('unifiedSocialCreditCode, MAX(Id) AS latest_id') ->whereNotNull('unifiedSocialCreditCode') ->where('unifiedSocialCreditCode', '<>', '') ->groupBy('unifiedSocialCreditCode'); $query = $this->connection->table('ops_fys_shop_info AS s') ->leftJoinSub($latestCompanies, 'company_keys', function ($join): void { $join->on('company_keys.unifiedSocialCreditCode', '=', 's.unifiedSocialCreditCode'); }) ->leftJoin('ops_fys_company_info AS c', 'c.Id', '=', 'company_keys.latest_id') ->leftJoinSub( $this->connection->table('ops_fys_trademark_info AS trademark_keys') ->selectRaw('trademark_keys.trademarkNumber, MAX(trademark_keys.Id) AS latest_id') ->where('trademark_keys.isDelete', 0) ->whereNotNull('trademark_keys.trademarkNumber') ->where('trademark_keys.trademarkNumber', '<>', '') ->groupBy('trademark_keys.trademarkNumber'), 'trademark_keys', function ($join): void { $join->on('trademark_keys.trademarkNumber', '=', 's.trademarkNumber'); }, ) ->leftJoin('ops_fys_trademark_info AS t', 't.Id', '=', 'trademark_keys.latest_id') ->selectRaw(<<<'SQL' s.Id AS id, s.sotreId AS store_id, s.openTime AS opened_at, s.renewalTime AS renewed_at, s.erpShopName AS erp_store_name, s.shopName AS store_name, s.unifiedSocialCreditCode AS unified_social_credit_code, c.businessLicenseName AS company_name, c.businessLicenseType AS company_type, s.trademarkNumber AS trademark_number, t.trademarkName AS trademark_name, s.platform AS platform, s.responsibleDepartment AS profit_center, s.shopHead AS store_manager, s.customerDeptManager AS department_manager, s.shopState AS store_status, s.shopBankAccount AS withdrawal_account, s.shopType AS store_type, s.channelType AS channel_mode SQL); return $query; } /** @return array{data: array>, total: int, summary: array} */ private function trademarks(int $page, int $perPage, array $filters): array { $query = $this->connection->table('ops_fys_trademark_info') ->where('isDelete', 0) ->selectRaw(<<<'SQL' Id AS id, trademarkNumber AS trademark_number, trademarkName AS trademark_name, internationalClassification AS trademark_type, trademarkStatus AS trademark_status, registrationDate AS registration_date, termOfValidity AS expiry_date, approvedCommodities AS approved_goods, transferenceUser AS final_holder, registrationAddress AS registered_address, custodyDepartment AS custody_department, remark AS notes SQL); $this->applyFieldFilters($query, $filters, [ 'trademark_number' => 'trademarkNumber', 'trademark_name' => 'trademarkName', 'registration_date' => 'registrationDate', 'expiry_date' => 'termOfValidity', 'international_classification' => 'internationalClassification', 'approved_goods' => 'approvedCommodities', 'final_holder' => 'transferenceUser', 'registered_address' => 'registrationAddress', 'custody_department' => 'custodyDepartment', ]); $status = trim((string) ($filters['trademark_status'] ?? '')); if ($status !== '') { $statusCode = ['未启用' => '0', '有效' => '1'][$status] ?? $status; $query->where('trademarkStatus', '=', $statusCode); } $summary = $this->summaryCounts($query, <<<'SQL' COUNT(*) AS total, COALESCE(SUM(CASE WHEN trademarkStatus = '1' THEN 1 ELSE 0 END), 0) AS enabled, COALESCE(SUM(CASE WHEN trademarkStatus = '0' THEN 1 ELSE 0 END), 0) AS disabled, COUNT(DISTINCT CASE WHEN transferenceUser IS NOT NULL AND transferenceUser <> '' THEN transferenceUser END) AS holders, COUNT(DISTINCT CASE WHEN custodyDepartment IS NOT NULL AND custodyDepartment <> '' THEN custodyDepartment END) AS departments SQL); return [ ...$this->page($query, $page, $perPage, 'Id', $filters, [ 'trademark_name' => 'trademarkName', 'trademark_number' => 'trademarkNumber', 'trademark_status' => 'trademarkStatus', 'registration_date' => 'registrationDate', 'expiry_date' => 'termOfValidity', 'international_classification' => 'internationalClassification', 'approved_goods' => 'approvedCommodities', 'final_holder' => 'transferenceUser', 'registered_address' => 'registrationAddress', 'custody_department' => 'custodyDepartment', ]), 'summary' => $summary, ]; } /** @return array */ private function summaryCounts($query, string $sql, array $bindings = []): array { $row = (clone $query) ->select([]) ->selectRaw($sql, $bindings) ->first(); return array_map(static fn ($value): int => (int) $value, (array) $row); } /** @param array $textColumns @param array $exactColumns @param array $enumColumns */ private function applyFieldFilters($query, array $filters, array $textColumns, array $exactColumns = [], array $enumColumns = []): void { foreach ($textColumns as $key => $column) { $value = mb_substr(trim((string) ($filters[$key] ?? '')), 0, 191); if ($value !== '') { $query->where($column, 'like', '%' . addcslashes($value, '%_\\') . '%'); } } foreach ($exactColumns as $key => $column) { $value = trim((string) ($filters[$key] ?? '')); if ($value !== '') { $query->where($column, '=', $value); } } foreach ($enumColumns as $key => $column) { $value = trim((string) ($filters[$key] ?? '')); if ($value !== '') { $query->where($column, '=', $value); } } foreach (['opened_at' => 's.openTime', 'renewed_at' => 's.renewalTime', 'registration_date' => 'registrationDate', 'expiry_date' => 'termOfValidity'] as $key => $column) { if (! array_key_exists($key, $textColumns)) continue; foreach (['_from' => '>=', '_to' => '<='] as $suffix => $operator) { $date = trim((string) ($filters[$key . $suffix] ?? '')); if (preg_match('/^\\d{4}-\\d{2}-\\d{2}$/D', $date)) $query->where($column, $operator, $date); } } } /** @return array{data: array>, total: int} */ private function page($query, int $page, int $perPage, string $orderColumn, array $filters = [], array $sortColumns = []): array { $total = (clone $query)->count(); $sortColumn = $sortColumns[$filters['_sort'] ?? ''] ?? null; $direction = ($filters['_direction'] ?? 'asc') === 'desc' ? 'desc' : 'asc'; if ($sortColumn !== null) { $query->orderBy($sortColumn, $direction)->orderByDesc($orderColumn); } else { $query->orderByDesc($orderColumn); } $rows = $query ->forPage($page, $perPage) ->get() ->map(static fn (object $row): array => (array) $row) ->all(); return ['data' => $rows, 'total' => $total]; } private function dataset(string $scope): BusinessMirrorDataset { $key = self::DATASETS[$scope] ?? null; if ($key === null) { throw new InvalidArgumentException('未知的业务资料镜像范围:' . $scope); } return $this->datasets->get($key); } }