1127 lines
46 KiB
PHP
1127 lines
46 KiB
PHP
<?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 ?StoreReportRepository $storeReports = null,
|
||
private readonly ?StoreAccountMirrorRepository $storeAccounts = null,
|
||
private readonly ?InstallationDetailMirrorRepository $installationMirror = 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),
|
||
'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<array<string,mixed>>,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<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');
|
||
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<array<string,mixed>> $rows
|
||
* @return list<array<string,mixed>>
|
||
*/
|
||
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<string,string|list<string>> $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<array<string,mixed>> $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<string,string|list<string>> $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<array<string,mixed>> $rows @param array<string,string|list<string>> $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<array<string,mixed>> $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<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');
|
||
$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<array<string,mixed>> $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<array<string,mixed>> $rows
|
||
* @return list<string>
|
||
*/
|
||
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<string,mixed> $row @param array<string,string> $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<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', '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<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['finance_tax_type'] ?? $row['taxCategoryAndType'] ?? $row['taxpayer_category'] ?? null,
|
||
'business_registration_type' => $row['business_registration_type'] ?? 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) {
|
||
$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<string,array{operation_owner:string,operation_department_head:string,operation_director:string,profit_center:string}> */
|
||
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(<<<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,
|
||
?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<string,string|list<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) {
|
||
$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<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'],
|
||
'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);
|
||
}
|
||
}
|