CaiWuXiTong/modules/EcommerceFinance/Repositories/BusinessInformationMirrorRepository.php
WuYongJie d48cb7d639 feat: 店铺维度报表条件查询与主体归属字段接入
实际修改:
- 店铺维度报表新增真实数据库条件查询:店铺字段走共享镜像 SQL
  WHERE,主体/能否变更/生效/失效走生命周期行级过滤(左闭右开)
- 店铺负责人/部门负责人/利润中心改为店铺账号 PostgreSQL 镜像值,
  按店铺名称关联,未匹配时回退经营分析副本库原值
- 能否变更按主表 platform 判定(唯品会MP/淘工厂=否,其余=是),
  筛选改为 是/否 下拉;默认生效时间 2026-07-01,失效未记录显示存续中
- 主体维度经营报表接入生命周期收入窗口与注册地址解析市字段
- 同步更新视图测试并重写 StoreReportRepositoryTest 匹配 SQLite 实现

验证:容器内 tests/Feature/Business + tests/Unit/EcommerceFinance +
tests/Unit/Business 共 324 passed / 4 skipped;真实数据端到端验证
(无筛选 1935 行、负责人/利润中心/失效区间筛选抽样全对)。
无新增数据库迁移,不涉及生产数据写入。
2026-09-06 19:27:51 +08:00

293 lines
11 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
{
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);
}
}