CaiWuXiTong/modules/EcommerceFinance/Repositories/BusinessInformationMirrorRepository.php
WuYongJie 967b62ca2f feat: 数据逻辑配置页面支持在线维护
- 店铺主体商标数据逻辑配置新增编辑页:多商标分隔符、同日主体冲突口径、
  商标号缺失处理、爬虫断更开关与天数、四类预警责任人
- 店铺维度报表逻辑配置新增期初数据编辑页:114 家变更店铺可增删改,
  期初生效日随设置保存
- 规则与期初数据改存系统库 settings(扁平键),首次打开自动从
  config/store_report_initial_periods.php 固化,之后不再依赖代码文件
- 控制台任务通过 bootCompanyContext() 读取同一份公司级配置
- 新增迁移清理早期嵌套设置键
- 新增 2 个测试覆盖配置保存与期初店铺增删
2026-10-03 21:52:43 +08:00

616 lines
27 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 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);
}
}