CaiWuXiTong/tests/Unit/EcommerceFinance/OperationsReplicaSnapshotRefresherTest.php
renshen 09d593dd2d 订单数据接入聚水潭上游只读数据 (#27)
## 任务背景
订单数据页面已完成附件表头,本任务由 renshen 接入聚水潭上游真实只读数据。

## 任务目标
- 订单售后费用明细:按附件字段展示订单、商品、SKU 成本及售后关联数据。
- 商品推广费用明细:按商品/款式汇总推广费用。
- 不硬编码附件演示值;缺失字段保持空值或零。

## 预计修改路径
- modules/EcommerceFinance/Repositories/OrderDataRepository.php
- modules/EcommerceFinance/View/Composers/OrderDataPageComposer.php
- modules/EcommerceFinance/Providers/Main.php
- resources/views/business/pages/order-data/*.blade.php
- tests/Unit/EcommerceFinance/OrderDataRepositoryTest.php
- tests/Feature/Business/OrderDataPagesUpstreamTest.php

## 页面和路由
复用 order-data/after-sales-expenses、order-data/promotion-expenses 和 business.pages.show;不新增或修改 routes。

## Controller / Repository
新增 OrderDataRepository 和专用 View Composer;不修改 app/Http/Controllers/Business/Pages.php。

## 数据源、表和字段
source_key=operations_replica,数据库 JSReportReplica,schema=rpt。固定参数化只读查询使用 jst_Orders、jst_OrderItem、jst_sku、jst_skumap、jst_returnAndRefundInfo、jst_returnAndRefundItem、jst_warehouseInfo、ads_itemid_sales_cost_ratio_v2_di。字段限于订单/商品标识、时间、店铺、SKU 元数据、金额、售后数量金额及 push_fee。

## scope
- order-data.after-sales.list
- order-data.promotion.list

## 权限、菜单和公共组件
复用现有页面权限、菜单、UpstreamReadOnlyQueryFactory 和 UpstreamQueryCacheFactory;不修改权限和导航。

## 明确不修改
不修改 routes、Pages Controller、数据库迁移、上游连接配置、权限、菜单、京东钱包功能、共享数据库结构或任何上游数据。

## 范围补充:后台快照刷新
- modules/EcommerceFinance/Services/OperationsReplicaSnapshotRefresher.php
- tests/Unit/EcommerceFinance/OperationsReplicaSnapshotRefresherTest.php

刷新器仅为 order-data.after-sales.list 和 order-data.promotion.list 分派同一 OrderDataRepository 固定查询;其他 scope 继续复用现有 UpstreamBusinessInformation。

Reviewed-on: http://git.1080p.hk/WuYongJie/CaiWuXiTong/pulls/27
2026-08-11 14:50:56 +08:00

161 lines
5.4 KiB
PHP

<?php
namespace Tests\Unit\EcommerceFinance;
use App\Business\UpstreamBusinessInformation;
use Closure;
use InvalidArgumentException;
use Modules\EcommerceFinance\Repositories\OrderDataRepository;
use Modules\EcommerceFinance\Services\OperationsReplicaSnapshotRefresher;
use Modules\EcommerceFinance\Services\UpstreamQueryCache;
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
class OperationsReplicaSnapshotRefresherTest extends TestCase
{
/**
* @dataProvider snapshotScopes
*/
public function test_background_refresh_uses_the_same_fixed_business_snapshot_query(
string $scope,
array $parameters,
): void {
$cache = new class extends UpstreamQueryCache {
public array $calls = [];
public mixed $value = null;
public function __construct()
{
}
public function refresh(string $scope, array $parameters, Closure $resolver): string
{
$this->calls[] = compact('scope', 'parameters');
$this->value = $resolver();
return 'fresh';
}
};
$caches = new class($cache) extends UpstreamQueryCacheFactory {
public string $requestedSource = '';
public function __construct(private readonly UpstreamQueryCache $cache)
{
}
public function forSource(string $source): UpstreamQueryCache
{
$this->requestedSource = $source;
return $this->cache;
}
};
$information = new class extends UpstreamBusinessInformation {
public array $calls = [];
public function __construct()
{
}
public function snapshot(string $scope, array $parameters): array
{
$this->calls[] = compact('scope', 'parameters');
return ['scope' => $scope, 'parameters' => $parameters];
}
};
$orders = $this->createMock(OrderDataRepository::class);
$refresher = new OperationsReplicaSnapshotRefresher($caches, $information, $orders);
$this->assertSame('operations_replica', $refresher->sourceKey());
$this->assertSame('fresh', $refresher->refresh(compact('scope', 'parameters')));
$this->assertSame('operations_replica', $caches->requestedSource);
$this->assertSame([compact('scope', 'parameters')], $cache->calls);
$this->assertSame([compact('scope', 'parameters')], $information->calls);
$this->assertSame(['scope' => $scope, 'parameters' => $parameters], $cache->value);
}
/**
* @dataProvider orderDataScopes
*/
public function test_order_data_scopes_use_the_order_data_repository(
string $scope,
string $method,
array $parameters,
): void {
$cache = new class extends UpstreamQueryCache {
public mixed $value = null;
public function __construct()
{
}
public function refresh(string $scope, array $parameters, Closure $resolver): string
{
$this->value = $resolver();
return 'fresh';
}
};
$caches = new class($cache) extends UpstreamQueryCacheFactory {
public function __construct(private readonly UpstreamQueryCache $cache)
{
}
public function forSource(string $source): UpstreamQueryCache
{
return $this->cache;
}
};
$information = $this->createMock(UpstreamBusinessInformation::class);
$information->expects($this->never())->method('snapshot');
$orders = $this->createMock(OrderDataRepository::class);
$orders->expects($this->once())
->method($method)
->with($parameters['filters'], $parameters['page'], $parameters['limit'])
->willReturn(['rows' => [['id' => 1]]]);
$refresher = new OperationsReplicaSnapshotRefresher($caches, $information, $orders);
$this->assertSame('fresh', $refresher->refresh(compact('scope', 'parameters')));
$this->assertSame(['rows' => [['id' => 1]]], $cache->value);
}
public static function orderDataScopes(): array
{
$parameters = [
'filters' => ['date' => '2026-08-09'],
'page' => 1,
'limit' => 25,
];
return [
'after sales' => ['order-data.after-sales.list', 'afterSales', $parameters],
'promotion' => ['order-data.promotion.list', 'promotionExpenses', $parameters],
];
}
public static function snapshotScopes(): array
{
return [
'company' => ['company-information.read', ['page' => 2, 'perPage' => 50]],
'store' => ['store-information.read', ['page' => 1, 'perPage' => 25]],
'trademark' => ['trademark-information.read', ['page' => 3, 'perPage' => 100]],
];
}
public function test_unknown_business_snapshot_scope_is_rejected_before_connecting_to_an_upstream(): void
{
/** @var UpstreamBusinessInformation $information */
$information = (new ReflectionClass(UpstreamBusinessInformation::class))->newInstanceWithoutConstructor();
$this->expectException(InvalidArgumentException::class);
$information->snapshot('unknown.scope', []);
}
}