实际修改: - 店铺维度报表新增真实数据库条件查询:店铺字段走共享镜像 SQL WHERE,主体/能否变更/生效/失效走生命周期行级过滤(左闭右开) - 店铺负责人/部门负责人/利润中心改为店铺账号 PostgreSQL 镜像值, 按店铺名称关联,未匹配时回退经营分析副本库原值 - 能否变更按主表 platform 判定(唯品会MP/淘工厂=否,其余=是), 筛选改为 是/否 下拉;默认生效时间 2026-07-01,失效未记录显示存续中 - 主体维度经营报表接入生命周期收入窗口与注册地址解析市字段 - 同步更新视图测试并重写 StoreReportRepositoryTest 匹配 SQLite 实现 验证:容器内 tests/Feature/Business + tests/Unit/EcommerceFinance + tests/Unit/Business 共 324 passed / 4 skipped;真实数据端到端验证 (无筛选 1935 行、负责人/利润中心/失效区间筛选抽样全对)。 无新增数据库迁移,不涉及生产数据写入。
293 lines
11 KiB
PHP
293 lines
11 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
|
||
{
|
||
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),
|
||
'store-information.read' => $this->stores($page, $perPage),
|
||
'trademark-information.read' => $this->trademarks($page, $perPage),
|
||
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()->orderByDesc('s.Id');
|
||
$this->applyStoreFilters($query, $filters);
|
||
|
||
return $query
|
||
->get()
|
||
->map(static fn (object $row): array => (array) $row)
|
||
->all();
|
||
}
|
||
|
||
/**
|
||
* Return the stable store-information rows used to seed the receiving
|
||
* account pages. The query stays on the shared business mirror and only
|
||
* reads the approved shop/company fields.
|
||
*
|
||
* @return list<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, '=', $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
|
||
{
|
||
$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);
|
||
|
||
return $this->page($query, $page, $perPage, 'Id');
|
||
}
|
||
|
||
/** @return array{data: array<int,array<string,mixed>>, total: int} */
|
||
private function stores(int $page, int $perPage): array
|
||
{
|
||
return $this->page($this->storeQuery(), $page, $perPage, 's.Id');
|
||
}
|
||
|
||
private function storeQuery()
|
||
{
|
||
$latestCompanies = $this->connection->table('ops_fys_company_info')
|
||
->selectRaw('unifiedSocialCreditCode, MAX(Id) AS latest_id')
|
||
->whereNotNull('unifiedSocialCreditCode')
|
||
->where('unifiedSocialCreditCode', '<>', '')
|
||
->groupBy('unifiedSocialCreditCode');
|
||
|
||
$query = $this->connection->table('ops_fys_shop_info AS s')
|
||
->leftJoinSub($latestCompanies, 'company_keys', function ($join): void {
|
||
$join->on('company_keys.unifiedSocialCreditCode', '=', 's.unifiedSocialCreditCode');
|
||
})
|
||
->leftJoin('ops_fys_company_info AS c', 'c.Id', '=', 'company_keys.latest_id')
|
||
->leftJoinSub(
|
||
$this->connection->table('ops_fys_trademark_info AS trademark_keys')
|
||
->selectRaw('trademark_keys.trademarkNumber, MAX(trademark_keys.Id) AS latest_id')
|
||
->where('trademark_keys.isDelete', 0)
|
||
->whereNotNull('trademark_keys.trademarkNumber')
|
||
->where('trademark_keys.trademarkNumber', '<>', '')
|
||
->groupBy('trademark_keys.trademarkNumber'),
|
||
'trademark_keys',
|
||
function ($join): void {
|
||
$join->on('trademark_keys.trademarkNumber', '=', 's.trademarkNumber');
|
||
},
|
||
)
|
||
->leftJoin('ops_fys_trademark_info AS t', 't.Id', '=', 'trademark_keys.latest_id')
|
||
->selectRaw(<<<'SQL'
|
||
s.Id AS id,
|
||
s.sotreId AS store_id,
|
||
s.openTime AS opened_at,
|
||
s.renewalTime AS renewed_at,
|
||
s.erpShopName AS erp_store_name,
|
||
s.unifiedSocialCreditCode AS unified_social_credit_code,
|
||
c.businessLicenseName AS company_name,
|
||
c.businessLicenseType AS company_type,
|
||
s.trademarkNumber AS trademark_number,
|
||
t.trademarkName AS trademark_name,
|
||
s.platform AS platform,
|
||
s.responsibleDepartment AS profit_center,
|
||
s.shopHead AS store_manager,
|
||
s.customerDeptManager AS department_manager,
|
||
s.shopState AS store_status,
|
||
s.shopBankAccount AS withdrawal_account,
|
||
s.shopType AS store_type,
|
||
s.channelType AS channel_mode
|
||
SQL);
|
||
|
||
return $query;
|
||
}
|
||
|
||
/** @return array{data: array<int,array<string,mixed>>, total: int} */
|
||
private function trademarks(int $page, int $perPage): 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);
|
||
|
||
return $this->page($query, $page, $perPage, 'Id');
|
||
}
|
||
|
||
/** @return array{data: array<int,array<string,mixed>>, total: int} */
|
||
private function page($query, int $page, int $perPage, string $orderColumn): array
|
||
{
|
||
$total = (clone $query)->count();
|
||
$rows = $query->orderByDesc($orderColumn)
|
||
->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);
|
||
}
|
||
}
|