- 店铺主体商标数据逻辑配置新增编辑页:多商标分隔符、同日主体冲突口径、 商标号缺失处理、爬虫断更开关与天数、四类预警责任人 - 店铺维度报表逻辑配置新增期初数据编辑页:114 家变更店铺可增删改, 期初生效日随设置保存 - 规则与期初数据改存系统库 settings(扁平键),首次打开自动从 config/store_report_initial_periods.php 固化,之后不再依赖代码文件 - 控制台任务通过 bootCompanyContext() 读取同一份公司级配置 - 新增迁移清理早期嵌套设置键 - 新增 2 个测试覆盖配置保存与期初店铺增删
616 lines
27 KiB
PHP
616 lines
27 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Repositories;
|
||
|
||
use Illuminate\Database\ConnectionInterface;
|
||
use InvalidArgumentException;
|
||
use Modules\EcommerceFinance\Contracts\BusinessMirrorDataset;
|
||
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
|
||
use RuntimeException;
|
||
|
||
class BusinessInformationMirrorRepository
|
||
{
|
||
private const DATASETS = [
|
||
'company-information.read' => '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<int,array<string,mixed>>, total: int} */
|
||
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<string,string> $filters @return array<int,array<string,mixed>> */
|
||
public function allStores(array $filters = []): array
|
||
{
|
||
if (! $this->ready('store-information.read')) {
|
||
throw new RuntimeException('business_mirror_dataset_not_ready');
|
||
}
|
||
|
||
$query = $this->storeQuery($this->crawlerSubqueries())->orderByDesc('s.Id');
|
||
$this->applyStoreFilters($query, $filters);
|
||
|
||
$rows = $query->get()->map(static fn (object $row): array => (array) $row)->all();
|
||
$trademarks = $this->crawlerTrademarkPairsForAll(
|
||
array_map(static fn (array $row): string => (string) ($row['erp_store_name'] ?? ''), $rows),
|
||
);
|
||
|
||
return array_map(
|
||
fn (array $row): array => $this->applyCrawlerPriority(
|
||
$row,
|
||
$trademarks[(string) ($row['erp_store_name'] ?? '')] ?? [],
|
||
),
|
||
$rows,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 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<array<string,mixed>>
|
||
*/
|
||
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',
|
||
// 页面“店铺ID(唯一值)”列展示的就是 ERP 店铺名称,筛选按同一列匹配。
|
||
'store_id' => 's.erpShopName',
|
||
'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']));
|
||
}
|
||
|
||
if (trim((string) ($filters['status'] ?? '')) !== '') {
|
||
$query->where('s.shopState', '=', (string) $filters['status']);
|
||
}
|
||
|
||
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<string,array<int,array<string,mixed>>> */
|
||
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 [];
|
||
}
|
||
|
||
return $this->connection->table('ops_fys_company_info')
|
||
->whereIn('businessLicenseName', $names)
|
||
->orderByDesc('Id')
|
||
->get()
|
||
->map(static fn (object $row): array => (array) $row)
|
||
->groupBy(static fn (array $row): string => (string) $row['businessLicenseName'])
|
||
->map(static fn ($rows): array => $rows->values()->all())
|
||
->all();
|
||
}
|
||
|
||
/** @return array{data: array<int,array<string,mixed>>, total: int} */
|
||
private function companies(int $page, int $perPage, array $filters): array
|
||
{
|
||
$query = $this->connection->table('ops_fys_company_info')
|
||
->selectRaw(<<<'SQL'
|
||
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
|
||
SQL);
|
||
|
||
$this->applyFieldFilters($query, $filters, [
|
||
'unified_social_credit_code' => 'unifiedSocialCreditCode', 'company_name' => 'businessLicenseName',
|
||
'establishment_date' => 'IncorporationDate',
|
||
'registered_at' => 'IncorporationDate',
|
||
'legal_representative' => 'legalRepresentative', 'registered_address' => 'registeredAddress',
|
||
'city' => 'registeredAddress',
|
||
'business_scope' => 'businessScope', 'subscribed_at' => 'registeredCapitalVaildTime',
|
||
'financial_officer' => 'financialManager',
|
||
'billing_officer' => 'financialManager',
|
||
'supervisor' => 'supervisor',
|
||
], ['subscribed_capital' => 'registeredCapital'], [
|
||
'company_type' => 'businessLicenseType',
|
||
'taxpayer_type' => 'taxCategoryAndType',
|
||
'taxpayer_category' => 'taxCategoryAndType',
|
||
]);
|
||
$companyStatus = trim((string) ($filters['company_status'] ?? ''));
|
||
if ($companyStatus !== '') {
|
||
$statusCode = ['正常' => '0', '已注销' => '1'][$companyStatus] ?? $companyStatus;
|
||
$query->where('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('legalRepresentative', 'like', $like)
|
||
->orWhere('supervisor', 'like', $like)
|
||
->orWhere('financialManager', 'like', $like);
|
||
});
|
||
}
|
||
return $this->page($query, $page, $perPage, 'Id', $filters, [
|
||
'unified_social_credit_code' => 'unifiedSocialCreditCode', 'company_name' => 'businessLicenseName',
|
||
'taxpayer_type' => 'taxCategoryAndType', 'company_status' => 'companyStatus',
|
||
'subscribed_capital' => 'registeredCapital', 'registered_at' => 'IncorporationDate',
|
||
'legal_representative' => 'legalRepresentative', 'supervisor' => 'supervisor',
|
||
'billing_officer' => 'financialManager', 'registered_address' => 'registeredAddress',
|
||
'city' => 'registeredAddress', 'business_scope' => 'businessScope',
|
||
'basic_deposit_account' => 'accountSerialNumber', 'subscribed_at' => 'registeredCapitalVaildTime',
|
||
]);
|
||
}
|
||
|
||
/** @return array{data: array<int,array<string,mixed>>, total: int} */
|
||
private function stores(int $page, int $perPage, array $filters): array
|
||
{
|
||
$crawler = $this->crawlerSubqueries();
|
||
$query = $this->storeQuery($crawler);
|
||
$this->applyFieldFilters($query, $filters, [
|
||
'store_id' => 's.sotreId', 'store_name' => 's.erpShopName',
|
||
'business_department' => 's.responsibleDepartment', 'store_manager' => 's.shopHead',
|
||
'department_manager' => 's.customerDeptManager', 'opened_at' => 's.openTime', 'renewed_at' => 's.renewalTime',
|
||
], [], ['platform' => 's.platform']);
|
||
$this->applyCrawlerPriorityFilters($query, $filters, $crawler);
|
||
$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', '=', '');
|
||
});
|
||
}
|
||
$result = $this->page($query, $page, $perPage, 's.Id', $filters, [
|
||
'platform' => 's.platform', 'store_id' => 's.sotreId', 'store_name' => 's.erpShopName',
|
||
'store_status' => 's.shopState',
|
||
'business_department' => 's.responsibleDepartment', 'store_manager' => 's.shopHead',
|
||
'department_manager' => 's.customerDeptManager', 'opened_at' => 's.openTime',
|
||
'renewed_at' => 's.renewalTime', 'withdrawal_account' => 's.shopBankAccount',
|
||
]);
|
||
|
||
$result['data'] = array_map(
|
||
fn (array $row): array => $this->applyCrawlerPriority(
|
||
$row,
|
||
$this->crawlerTrademarkPairsFor((string) ($row['erp_store_name'] ?? '')),
|
||
),
|
||
$result['data'],
|
||
);
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 店铺基础信息页的展示优先级:爬虫抓到的店铺主体、商标名称和商标号优先,
|
||
* 爬虫没有覆盖的店铺保留财务系统原有链路的结果。
|
||
*
|
||
* @param array<string,mixed> $row
|
||
* @return array<string,mixed>
|
||
*/
|
||
/**
|
||
* @param array<string,mixed> $row
|
||
* @param list<array{number:string,name:string}> $trademarkPairs
|
||
* @return array<string,mixed>
|
||
*/
|
||
private function applyCrawlerPriority(array $row, array $trademarkPairs = []): array
|
||
{
|
||
// 多商标拼接放在 PHP 层:SQLite 测试库不支持 group_concat 的 order by 语法。
|
||
if ($trademarkPairs !== []) {
|
||
$separator = \Modules\EcommerceFinance\Services\DataLogicSettings::separator();
|
||
$row['crawler_trademark_name'] = implode($separator, array_filter(array_column($trademarkPairs, 'name')));
|
||
$row['crawler_trademark_number'] = implode($separator, array_filter(array_column($trademarkPairs, 'number')));
|
||
}
|
||
|
||
// 保留财务系统登记主体,供店铺维度报表生成期初期间;展示仍按爬虫优先。
|
||
$row['system_company_name'] = $row['company_name'] ?? null;
|
||
$company = trim((string) ($row['crawler_company_name'] ?? ''));
|
||
if ($company !== '') {
|
||
$row['company_name'] = $company;
|
||
}
|
||
|
||
$trademarkName = trim((string) ($row['crawler_trademark_name'] ?? ''));
|
||
if ($trademarkName !== '') {
|
||
$row['trademark_name'] = $trademarkName;
|
||
}
|
||
|
||
$trademarkNumber = trim((string) ($row['crawler_trademark_number'] ?? ''));
|
||
if ($trademarkNumber !== '') {
|
||
$row['trademark_number'] = $trademarkNumber;
|
||
}
|
||
|
||
unset(
|
||
$row['crawler_company_name'],
|
||
$row['crawler_trademark_name'],
|
||
$row['crawler_trademark_number'],
|
||
);
|
||
|
||
return $row;
|
||
}
|
||
|
||
/**
|
||
* 某店铺爬虫抓到的全部商标,按商标号排序,保证名称与号码一一对应。
|
||
*
|
||
* @return list<array{number:string,name:string}>
|
||
*/
|
||
/** @return list<array{number:string,name:string}> */
|
||
private function crawlerTrademarkPairsFor(string $storeName): array
|
||
{
|
||
$all = $this->crawlerTrademarkPairsForAll([$storeName]);
|
||
|
||
return $all[trim($storeName)] ?? [];
|
||
}
|
||
|
||
/**
|
||
* 批量加载多个店铺的爬虫商标,避免列表页逐行查询。
|
||
*
|
||
* @param list<string> $storeNames
|
||
* @return array<string,list<array{number:string,name:string}>>|array{}
|
||
*/
|
||
private function crawlerTrademarkPairsForAll(array $storeNames): array
|
||
{
|
||
$names = array_values(array_unique(array_filter(array_map('trim', $storeNames))));
|
||
if ($names === [] || ! $this->connection->getSchemaBuilder()->hasTable('ops_crawl_shop_trademark')) {
|
||
return [];
|
||
}
|
||
|
||
$result = [];
|
||
foreach (array_chunk($names, 500) as $chunk) {
|
||
$ranked = $this->connection->table('ops_crawl_shop_trademark')
|
||
->selectRaw('id, shop_name, trademark_no, trademark_name, crawl_date, row_number() over (partition by shop_name, trademark_no order by crawl_date desc, id desc) AS row_rank')
|
||
->whereIn('shop_name', $chunk);
|
||
$rows = $this->connection->table($ranked, 'crawler_trademark')
|
||
->where('row_rank', 1)
|
||
->orderBy('shop_name')
|
||
->orderBy('trademark_no')
|
||
->get();
|
||
|
||
foreach ($rows as $row) {
|
||
$store = trim((string) $row->shop_name);
|
||
$number = trim((string) $row->trademark_no);
|
||
if ($store === '' || $number === '') {
|
||
continue;
|
||
}
|
||
$result[$store][] = [
|
||
'number' => $number,
|
||
'name' => trim((string) $row->trademark_name),
|
||
];
|
||
}
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/** @param array<string,mixed> $crawler @param array<string,string> $filters */
|
||
private function applyCrawlerPriorityFilters($query, array $filters, array $crawler): void
|
||
{
|
||
$entity = mb_substr(trim((string) ($filters['store_entity'] ?? '')), 0, 191);
|
||
if ($entity !== '') {
|
||
$like = '%' . addcslashes($entity, '%_\\') . '%';
|
||
$query->where(function ($query) use ($crawler, $like): void {
|
||
$query->where('c.businessLicenseName', 'like', $like);
|
||
if ($crawler['store_entity'] !== null) {
|
||
$query->orWhere($crawler['store_entity'], 'like', $like);
|
||
}
|
||
});
|
||
}
|
||
|
||
foreach ([
|
||
'trademark' => ['crawler' => 'trademark_name', 'fallback' => 't.trademarkName'],
|
||
'trademark_number' => ['crawler' => 'trademark_no', 'fallback' => 's.trademarkNumber'],
|
||
] as $key => $columns) {
|
||
$value = mb_substr(trim((string) ($filters[$key] ?? '')), 0, 191);
|
||
if ($value === '') {
|
||
continue;
|
||
}
|
||
|
||
$like = '%' . addcslashes($value, '%_\\') . '%';
|
||
if ($crawler['trademark_table'] === null) {
|
||
$query->where($columns['fallback'], 'like', $like);
|
||
continue;
|
||
}
|
||
|
||
// 多商标店铺不能在联表结果里只匹配最新一条,这里按全量商标做 EXISTS 匹配。
|
||
$query->where(function ($query) use ($columns, $like, $crawler): void {
|
||
$query->where($columns['fallback'], 'like', $like)
|
||
->orWhereExists(function ($sub) use ($columns, $like, $crawler): void {
|
||
$sub->selectRaw('1')
|
||
->from($crawler['trademark_table'] . ' AS crawler_trademark_filter')
|
||
->whereColumn('crawler_trademark_filter.shop_name', 's.erpShopName')
|
||
->where('crawler_trademark_filter.' . $columns['crawler'], 'like', $like);
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 爬虫表的“每店铺最新一版”子查询。镜像缺失时返回 null,页面回退原链路。
|
||
*
|
||
* @return array{company:?object,store_entity:?string,trademark_table:?string}
|
||
*/
|
||
private function crawlerSubqueries(): array
|
||
{
|
||
$absence = ['company' => null, 'store_entity' => null, 'trademark_table' => null];
|
||
$schema = $this->connection->getSchemaBuilder();
|
||
if (! $schema->hasTable('ops_crawl_shop_company') && ! $schema->hasTable('ops_crawl_shop_trademark')) {
|
||
return $absence;
|
||
}
|
||
|
||
$company = null;
|
||
if ($schema->hasTable('ops_crawl_shop_company')) {
|
||
$ranked = $this->connection->table('ops_crawl_shop_company')
|
||
->selectRaw('id, shop_name, company_name, tax_no, crawl_date, row_number() over (partition by shop_name order by crawl_date desc, id desc) AS row_rank');
|
||
$company = $this->connection->table($ranked, 'crawler_company')
|
||
->where('row_rank', 1)
|
||
->select(['shop_name', 'company_name', 'tax_no']);
|
||
}
|
||
|
||
return [
|
||
'company' => $company,
|
||
'store_entity' => $company === null ? null : 'crawler_company.company_name',
|
||
'trademark_table' => $schema->hasTable('ops_crawl_shop_trademark') ? 'ops_crawl_shop_trademark' : null,
|
||
];
|
||
}
|
||
|
||
private function storeQuery(array $crawler = ['company' => null])
|
||
{
|
||
$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')
|
||
->when($crawler['company'] !== null, function ($query) use ($crawler): void {
|
||
$query->leftJoinSub($crawler['company'], 'crawler_company', function ($join): void {
|
||
$join->on('crawler_company.shop_name', '=', 's.erpShopName');
|
||
});
|
||
})
|
||
->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.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,
|
||
NULL AS crawler_company_name,
|
||
NULL AS crawler_trademark_name,
|
||
NULL AS crawler_trademark_number
|
||
SQL);
|
||
|
||
if ($crawler['company'] !== null) {
|
||
$query->addSelect([
|
||
'crawler_company.company_name AS crawler_company_name',
|
||
'crawler_company.tax_no AS crawler_tax_no',
|
||
]);
|
||
}
|
||
|
||
return $query;
|
||
}
|
||
|
||
/** @return array{data: array<int,array<string,mixed>>, total: int} */
|
||
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);
|
||
}
|
||
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',
|
||
]);
|
||
}
|
||
|
||
/** @param array<string,string> $textColumns @param array<string,string> $exactColumns @param array<string,string> $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<int,array<string,mixed>>, 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);
|
||
}
|
||
}
|