CaiWuXiTong/app/Business/UpstreamBusinessInformation.php
WuYongJie 75b12df493 feat: 完善财务报表字段查询与排序
合并店铺和主体经营报表的字段查询布局,补齐数值筛选、排序和镜像状态提示。
2026-09-24 20:23:41 +08:00

712 lines
29 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Business;
use Illuminate\Pagination\LengthAwarePaginator;
use InvalidArgumentException;
use Modules\EcommerceFinance\Repositories\BusinessInformationMirrorRepository;
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery;
use Modules\EcommerceFinance\Repositories\InstallationDetailMirrorRepository;
use Modules\EcommerceFinance\Repositories\StoreReportRepository;
use Modules\EcommerceFinance\Repositories\StoreAccountMirrorRepository;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
use Throwable;
class UpstreamBusinessInformation
{
private const SOURCE = 'operations_replica';
public function __construct(
private readonly UpstreamQueryCacheFactory $caches,
private readonly BusinessInformationMirrorRepository $mirror,
private readonly InstallationDetailMirrorRepository $installationMirror,
// 兼容仍按旧构造方式调用的代码;容器解析时会正常注入仓储。
private readonly ?StoreReportRepository $storeReports = null,
private readonly ?StoreAccountMirrorRepository $storeAccounts = null,
) {}
public function paginate(string $section, string $page, int $perPage = 50): LengthAwarePaginator
{
$currentPage = max((int) request('page', 1), 1);
$perPage = min(max($perPage, 1), 100);
$key = "{$section}/{$page}";
$payload = match ($key) {
'company-management/basic-information' => $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),
default => ['data' => [], 'total' => 0],
};
return new LengthAwarePaginator(
array_map(static fn (array $row) => (object) $row, $payload['data']),
(int) $payload['total'],
$perPage,
$currentPage,
[
'path' => request()->url(),
'query' => request()->query(),
],
);
}
private function installationPage(int $page, int $perPage): array
{
$payload = $this->installationMirror->paginate($page, $perPage, InstallationDetailMirrorRepository::requestFilters());
if (! $payload['ready']) {
throw new ServiceUnavailableHttpException(null, '安装明细镜像尚未完成首次同步');
}
return ['data' => $payload['data'], 'total' => $payload['total']];
}
/**
* 将已登记的经营分析副本库查询还原成不经过页面缓存的固定只读查询。
*
* 生产后台刷新器使用这个入口生成共享快照;页面仍然只能通过
* UpstreamQueryCache 读取快照,不能绕过缓存直接访问上游。
*
* @return array{data: array<int,array<string,mixed>>, 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(<<<SQL
SELECT
Id AS id,
unifiedSocialCreditCode AS unified_social_credit_code,
businessLicenseName AS company_name,
registeredCapital AS subscribed_capital,
businessLicenseType AS company_type,
IncorporationDate AS establishment_date,
legalRepresentative AS legal_representative,
registeredAddress AS registered_address,
businessScope AS business_scope,
accountSerialNumber AS basic_deposit_account_number,
registeredCapitalVaildTime AS subscribed_at,
companyStatus AS company_status,
financialManager AS financial_officer,
supervisor AS supervisor,
taxCategoryAndType AS taxpayer_category,
NULL AS shareholders
FROM rpt.fys_companyInfo
ORDER BY Id DESC
OFFSET {$offset} ROWS FETCH NEXT {$perPage} ROWS ONLY
SQL);
return [
'data' => 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');
return $this->page(
'store-information.read',
$page,
$perPage,
$filters,
fn () => $this->snapshot('store-information.read', [
'page' => $page,
'perPage' => $perPage,
]),
);
}
private function storeReportPage(int $page, int $perPage): array
{
$filters = StoreReportRepository::requestFilters();
$rows = $this->storeReportRows($filters);
$offset = ($page - 1) * $perPage;
return ['data' => array_slice($rows, $offset, $perPage), 'total' => count($rows)];
}
/** @return array{data:array<int,array<string,mixed>>,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<string,string> $filters @return array{data:array<int,array<string,mixed>>,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<string,mixed> $row
* @param array<string,string> $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(<<<SQL
SELECT 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, NULL AS store_manager, NULL AS department_manager,
s.shopState AS store_status, s.shopBankAccount AS withdrawal_account, s.shopType AS store_type, s.channelType AS channel_mode
FROM rpt.fys_shopInfo AS s
OUTER APPLY (SELECT TOP 1 businessLicenseName,businessLicenseType FROM rpt.fys_companyInfo WHERE unifiedSocialCreditCode=NULLIF(s.unifiedSocialCreditCode,'') ORDER BY Id DESC) c
OUTER APPLY (SELECT TOP 1 trademarkName FROM rpt.fys_TrademarkInfo WHERE trademarkNumber=NULLIF(s.trademarkNumber,'') AND isDelete=0 ORDER BY Id DESC) t
ORDER BY s.Id DESC
SQL);
$rows = array_map(fn (array $row): array => $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');
$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'));
foreach ($rows as &$row) {
$row['company_details'] = $details[$row['entity']] ?? [];
$row['city'] = $this->cityFromDetails($row['company_details']);
}
unset($row);
$rows = array_values(array_filter($rows, fn (array $row): bool => $this->subjectMatchesFilters($row, $filters)));
$this->sortSubjectRows($rows, $filters);
$offset = ($page - 1) * $perPage;
$pageRows = array_slice($rows, $offset, $perPage);
return ['data' => $pageRows, 'total' => count($rows)];
}
/** @param array<string,mixed> $row @param array<string,string> $filters */
private function subjectMatchesFilters(array $row, array $filters): bool
{
if (($filters['company_type'] ?? '') !== '' && (string) ($row['company_type'] ?? '') !== (string) $filters['company_type']) {
return false;
}
foreach (['city', 'entity', '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<int,array<string,mixed>> $rows @param array<string,string> $filters */
private function sortSubjectRows(array &$rows, array $filters): void
{
$key = (string) ($filters['_sort'] ?? 'entity');
$allowed = [
'city', 'entity', 'company_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<int,array<string,mixed>> $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<string,array<int,array<string,mixed>>> */
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['taxCategoryAndType'] ?? $row['taxpayer_category'] ?? null,
'shareholders' => $row['shareholders'] ?? null,
]);
}
/** @param array<string,string> $filters @return array<int,array<string,mixed>> */
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) {
$match = $ownership[trim((string) ($store['erp_store_name'] ?? ''))] ?? null;
if ($match === null) {
continue;
}
// 未匹配到账号或字段为空时保留经营分析副本库原值。
foreach ([
'store_manager' => $match['operation_owner'],
'department_manager' => $match['operation_director'],
'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<string,array{operation_owner:string,operation_director:string,profit_center:string}> */
private function storeOwnership(): array
{
try {
return ($this->storeAccounts ?? app(StoreAccountMirrorRepository::class))->ownershipByStoreName();
} catch (\RuntimeException | \InvalidArgumentException $exception) {
// 账号镜像未登记或未就绪时保留经营分析副本库原值,不阻断店铺报表。
return [];
}
}
private function storeSnapshot(UpstreamReadOnlyQuery $query, int $page, int $perPage): array
{
$offset = ($page - 1) * $perPage;
$rows = $query->select(<<<SQL
SELECT
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
FROM rpt.fys_shopInfo AS s
OUTER APPLY (
SELECT TOP 1 company.businessLicenseName, company.businessLicenseType
FROM rpt.fys_companyInfo AS company
WHERE company.unifiedSocialCreditCode = NULLIF(s.unifiedSocialCreditCode, '')
ORDER BY company.Id DESC
) AS c
OUTER APPLY (
SELECT TOP 1 trademark.trademarkName
FROM rpt.fys_TrademarkInfo AS trademark
WHERE trademark.trademarkNumber = NULLIF(s.trademarkNumber, '')
AND trademark.isDelete = 0
ORDER BY trademark.Id DESC
) AS t
ORDER BY s.Id DESC
OFFSET {$offset} ROWS FETCH NEXT {$perPage} ROWS ONLY
SQL);
return [
'data' => 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(<<<SQL
SELECT
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
FROM rpt.fys_TrademarkInfo
WHERE isDelete = 0
ORDER BY Id DESC
OFFSET {$offset} ROWS FETCH NEXT {$perPage} ROWS ONLY
SQL);
return [
'data' => 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): array
{
if ($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<string,string> */
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) {
$value = trim((string) request($key, ''));
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<int,array<string,mixed>>, 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'],
];
}
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,
};
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);
}
}