true]); $cache = new class extends UpstreamQueryCache { public array $calls = []; public function __construct() { } public function remember(string $scope, array $parameters, Closure $resolver): mixed { $this->calls[] = compact('scope', 'parameters'); return $resolver(); } }; $caches = new class($cache) extends UpstreamQueryCacheFactory { public function __construct(private readonly UpstreamQueryCache $cache) { } public function forSource(string $source): UpstreamQueryCache { return $this->cache; } }; $mirror = Mockery::mock(BusinessInformationMirrorRepository::class); $mirror->shouldReceive('ready')->once()->with($scope)->andReturnFalse(); $information = new class($caches, $mirror) extends UpstreamBusinessInformation { public array $snapshots = []; public function snapshot(string $scope, array $parameters): array { $this->snapshots[] = compact('scope', 'parameters'); return ['data' => [], 'total' => 0]; } }; $this->app['request']->merge(['page' => 3]); $paginator = $information->paginate($section, $page, 25); $expected = ['scope' => $scope, 'parameters' => ['page' => 3, 'perPage' => 25]]; $this->assertSame(3, $paginator->currentPage()); $this->assertSame($expected, $cache->calls[0]); $this->assertSame($expected, $information->snapshots[0]); } /** * @dataProvider paginatedBusinessInformationPages */ public function test_ready_business_mirror_is_used_without_registering_a_page_snapshot( string $section, string $page, string $scope, ): void { config(['operations_replica.enabled' => false, 'upstreams.stored_configuration.enabled' => false]); $cache = new class extends UpstreamQueryCache { public function __construct() {} public function remember(string $scope, array $parameters, Closure $resolver): mixed { throw new \RuntimeException('镜像就绪后不应再读取分页快照。'); } }; $caches = new class($cache) extends UpstreamQueryCacheFactory { public function __construct(private readonly UpstreamQueryCache $cache) {} public function forSource(string $source): UpstreamQueryCache { return $this->cache; } }; $mirror = Mockery::mock(BusinessInformationMirrorRepository::class); $mirror->shouldReceive('ready')->once()->with($scope)->andReturnTrue(); $mirror->shouldReceive('paginate')->once()->with($scope, 2, 25, [])->andReturn([ 'data' => [['id' => 10]], 'total' => 51, ]); $this->app['request']->merge(['page' => 2]); $paginator = (new UpstreamBusinessInformation($caches, $mirror)) ->paginate($section, $page, 25); $this->assertSame(2, $paginator->currentPage()); $this->assertSame(51, $paginator->total()); $this->assertSame(10, $paginator->items()[0]->id); } public function test_store_basic_information_uses_account_owner_and_department_head(): void { $mirror = Mockery::mock(BusinessInformationMirrorRepository::class); $mirror->shouldReceive('ready')->once()->with('store-information.read')->andReturnTrue(); $mirror->shouldReceive('allStores')->once()->with([])->andReturn([ [ 'id' => 7, 'erp_store_name' => '拼多多_测试店铺', 'store_manager' => '旧店铺负责人', 'department_manager' => '旧部门负责人', 'store_status' => '0', 'platform' => '拼多多', ], [ 'id' => 8, 'erp_store_name' => '拼多多-其他店铺', 'store_manager' => '其他负责人', 'department_manager' => '其他部门负责人', 'store_status' => '6', 'platform' => '拼多多', ], ]); $accounts = Mockery::mock(StoreAccountMirrorRepository::class); $accounts->shouldReceive('ownershipByStoreName')->once()->andReturn([ StoreAccountMirrorRepository::storeNameKey('拼多多-测试店铺') => [ 'operation_owner' => '正确店铺负责人', 'operation_department_head' => '正确部门负责人', 'operation_director' => '运营总监', 'profit_center' => '运营中心A', ], ]); $this->app['request']->merge([ 'store_manager' => '正确店铺负责人', 'department_manager' => '正确部门负责人', ]); $paginator = (new UpstreamBusinessInformation( Mockery::mock(UpstreamQueryCacheFactory::class), $mirror, null, $accounts, ))->paginate('store-management', 'basic-information', 50); $this->assertSame(1, $paginator->total()); $this->assertSame('正确店铺负责人', $paginator->items()[0]->store_manager); $this->assertSame('正确部门负责人', $paginator->items()[0]->department_manager); $this->assertNotSame('运营总监', $paginator->items()[0]->department_manager); $this->assertSame('正常', $paginator->items()[0]->store_status); $summaryCards = collect($this->app['request']->attributes->get('business-page-summary-cards'))->keyBy('label'); $this->assertSame([], $summaryCards->all()); } public function test_base_information_pages_do_not_render_summary_cards(): void { $mirror = Mockery::mock(BusinessInformationMirrorRepository::class); $mirror->shouldReceive('ready')->times(2)->andReturnUsing( static fn (string $scope): bool => in_array($scope, ['company-information.read', 'trademark-information.read'], true), ); $mirror->shouldReceive('paginate')->once()->with('company-information.read', 1, 50, [])->andReturn([ 'data' => [], 'total' => 818, 'summary' => [ 'total' => 818, 'normal' => 520, 'deregistered' => 298, 'general_taxpayer' => 123, 'small_taxpayer' => 606, ], ]); $mirror->shouldReceive('paginate')->once()->with('trademark-information.read', 1, 50, [])->andReturn([ 'data' => [], 'total' => 593, 'summary' => [ 'total' => 593, 'enabled' => 548, 'disabled' => 45, 'holders' => 5, 'departments' => 6, ], ]); $information = new UpstreamBusinessInformation( Mockery::mock(UpstreamQueryCacheFactory::class), $mirror, null, ); $information->paginate('company-management', 'basic-information', 50); $companyCards = collect($this->app['request']->attributes->get('business-page-summary-cards'))->keyBy('label'); $this->assertSame([], $companyCards->all()); $information->paginate('trademark-management', 'information', 50); $trademarkCards = collect($this->app['request']->attributes->get('business-page-summary-cards'))->keyBy('label'); $this->assertSame([], $trademarkCards->all()); } public function test_bootstrap_without_mirror_explains_shared_connection_instead_of_requesting_upstream_credentials(): void { config(['system_database.mode' => 'bootstrap', 'upstreams.local_configuration.enabled' => false]); $caches = Mockery::mock(UpstreamQueryCacheFactory::class); $caches->shouldNotReceive('forSource'); $mirror = Mockery::mock(BusinessInformationMirrorRepository::class); $mirror->shouldReceive('ready')->once()->with('store-information.read')->andReturnFalse(); $this->expectException(\Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException::class); $this->expectExceptionMessage('无需另配上游 SQL Server'); (new UpstreamBusinessInformation($caches, $mirror)) ->paginate('store-management', 'basic-information'); } public function test_subject_operating_report_hides_income_months_without_data_by_default(): void { $mirror = Mockery::mock(BusinessInformationMirrorRepository::class); $mirror->shouldReceive('ready')->with('store-information.read')->andReturnTrue(); $mirror->shouldReceive('allStores')->andReturn([['erp_store_name' => '测试店铺']]); $mirror->shouldReceive('ready')->with('company-information.read')->andReturnTrue(); $mirror->shouldReceive('companiesByNames')->andReturn([]); $mirror->shouldReceive('companyClassificationsByNames')->andReturn([]); $storeReports = Mockery::mock(StoreReportRepository::class); $storeReports->shouldReceive('rows')->andReturn([[ 'entity' => '测试主体', 'company_type' => '店铺-普通公司', 'rolling_12m_income' => 188.5, 'income_01' => null, 'income_02' => 88.5, 'income_03' => 0.0, 'income_04' => null, 'income_05' => null, 'income_06' => null, 'income_07' => null, 'income_08' => null, 'income_09' => null, 'income_10' => null, 'income_11' => null, 'income_12' => null, ]]); $storeAccounts = Mockery::mock(StoreAccountMirrorRepository::class); $storeAccounts->shouldReceive('ownershipByStoreName')->andReturn([]); $information = new UpstreamBusinessInformation( Mockery::mock(UpstreamQueryCacheFactory::class), $mirror, $storeReports, $storeAccounts, ); $paginator = $information->paginate('company-management', 'subject-operating-report', 50); $this->assertSame( ['income_01', 'income_04', 'income_05', 'income_06', 'income_07', 'income_08', 'income_09', 'income_10', 'income_11', 'income_12'], $this->app['request']->attributes->get('subject-operating-report.default_hidden_columns'), ); $this->assertSame(1, $paginator->total()); $this->assertSame('测试主体', $paginator->items()[0]->entity); } public static function businessInformationPages(): array { return [ 'company' => ['company-management', 'basic-information', 'company-information.read'], 'store' => ['store-management', 'basic-information', 'store-information.read'], 'trademark' => ['trademark-management', 'information', 'trademark-information.read'], ]; } public static function paginatedBusinessInformationPages(): array { return [ 'company' => ['company-management', 'basic-information', 'company-information.read'], 'trademark' => ['trademark-management', 'information', 'trademark-information.read'], ]; } }