diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index f67c48b3..4b5dbea6 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -45,6 +45,7 @@ use Modules\EcommerceFinance\Repositories\SupplierPayableReportRepository; use Modules\EcommerceFinance\Repositories\TmallTaobaoSpreadBillRepository; use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory; use Modules\EcommerceFinance\View\PlatformPromotionAccountPageData; +use Modules\EcommerceFinance\View\OrderDataPageData; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; use Throwable; @@ -255,24 +256,11 @@ class Pages extends Controller $section = collect(SidebarPermissions::navigationFor(user()))->firstWhere('slug', 'order-data'); abort_unless($section && collect($section['pages'] ?? [])->contains('slug', 'bill-offset'), 403, '当前账号没有该页面权限。'); - $normalizeDate = static function (mixed $value): string { - $date = trim((string) $value); - if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $date, $matches) !== 1 - || ! checkdate((int) $matches[2], (int) $matches[3], (int) $matches[1])) { - return ''; - } - return $date; - }; - $filters = [ - 'from' => $normalizeDate($request->query('from')) ?: now()->subDays(6)->toDateString(), - 'to' => $normalizeDate($request->query('to')) ?: now()->toDateString(), - 'order_no' => mb_substr(trim((string) $request->query('order_no', '')), 0, 100), - 'shop_name' => mb_substr(trim((string) $request->query('shop_name', '')), 0, 100), - 'sku_id' => mb_substr(trim((string) $request->query('sku_id', '')), 0, 100), - ]; $repository = app(BillOffsetPageData::class); + $filters = $request->query(); $result = $repository->data($filters, 1, 100); abort_if($result['billOffsetError'] !== null, 503, $result['billOffsetError']); + $filters = $result['billOffsetFilters']; return response()->streamDownload(function () use ($repository, $filters, $result): void { $csv = fopen('php://output', 'w'); @@ -287,21 +275,27 @@ class Pages extends Controller 'shop_style_code' => '店铺款式编码', 'account_500112' => '500112冲减收入', ]; fputcsv($csv, array_values($labels)); - $page = 1; $current = $result; + $page = 1; do { foreach ($current['billOffsetRows'] as $row) { fputcsv($csv, array_map( - fn (string $field): string => $field === 'account_500112' - ? (string) (data_get($row, $field) ?? '') - : ($field === 'shop_name' - ? $this->csvText(data_get($row, $field) ?? '') - : $this->csvIdentifier(data_get($row, $field) ?? '')), + function (string $field) use ($row): string { + $value = data_get($row, $field); + + if ($field === 'account_500112') { + return $value === null || $value === '' ? '' : number_format((float) $value, 2, '.', ''); + } + + return $field === 'shop_name' + ? $this->csvText($value) + : $this->csvIdentifier($value); + }, array_keys($labels), )); } if (! $current['billOffsetRows']->hasMorePages()) break; - $current = $repository->data($filters, ++$page, 100); + $current = $repository->dataForNormalizedFilters($filters, ++$page, 100); if ($current['billOffsetError'] !== null) { throw new \RuntimeException('账单冲减导出未完成,请重试。'); } diff --git a/modules/EcommerceFinance/View/BillOffsetPageData.php b/modules/EcommerceFinance/View/BillOffsetPageData.php index b2ca5723..d4399842 100644 --- a/modules/EcommerceFinance/View/BillOffsetPageData.php +++ b/modules/EcommerceFinance/View/BillOffsetPageData.php @@ -2,34 +2,86 @@ namespace Modules\EcommerceFinance\View; -/** 账单冲减复用原始数据的订单售后费用明细,不重新推断科目。 */ +use Illuminate\Pagination\Paginator; +use Illuminate\Support\Collection; +use Throwable; + class BillOffsetPageData { - public function __construct(private readonly OrderDataPageData $orders) + public function __construct(private readonly OrderDataPageData $orderData) { } + /** 账单冲减复用原始数据的订单售后费用明细,不重新推断科目。 */ public function page(): array { return ['view' => 'business.pages.order-data.bill-offset'] + $this->data(); } - public function data(?array $filters = null, ?int $page = null, ?int $limit = null): array + /** @param array|null $input */ + public function data(?array $input = null, ?int $page = null, ?int $limit = null): array { - $source = $this->orders->afterSales($filters, $page, $limit, false); - $rows = $source['orderDataRows']; - $amounts = collect($rows->items())->pluck('account_500112'); - // 未接入的科目值保留为空;只有本页金额都已知时才显示合计。 + $filters = $this->orderData->afterSalesFilters($input); + + return $this->dataForNormalizedFilters( + $filters, + $page ?? (int) request('page', 1), + $limit ?? (int) setting('default.list_limit', 25), + ); + } + + /** + * Reuse an already normalized filter set for subsequent export pages. + * + * @param array{from:string,to:string,order_no:string,shop_name:string,sku_id:string} $filters + * @return array + */ + public function dataForNormalizedFilters(array $filters, int $page, int $limit): array + { + $page = max($page, 1); + $limit = min(max($limit, 1), 100); + $rows = $this->emptyPaginator($page, $limit); + $status = null; + $error = null; + + try { + $result = $this->orderData->afterSalesRows($filters, $page, $limit); + $rows = new Paginator( + Collection::make($result['rows'] ?? [])->map(static fn (array $row): object => (object) $row), + $limit, + $page, + [ + 'path' => request()->url(), + 'query' => request()->except('page'), + ], + ); + $rows->hasMorePagesWhen((bool) ($result['has_more'] ?? false)); + $status = data_get($result, 'status.X-Caiwu-Data-Status'); + } catch (Throwable $exception) { + report($exception); + $error = '订单售后费用数据暂时无法读取,请稍后重试。'; + } + + $amounts = Collection::make($rows->items())->pluck('account_500112'); + // 500112 未接入时保留为空;本页有任何未知金额时不生成误导性合计。 $amount = $amounts->isNotEmpty() && $amounts->every(static fn ($value): bool => is_numeric($value)) ? round((float) $amounts->sum(), 2) : null; return [ + 'billOffsetFilters' => $filters, 'billOffsetRows' => $rows, - 'billOffsetFilters' => $source['orderDataFilters'], 'billOffsetSummary' => ['count' => $rows->count(), 'amount' => $amount], - 'billOffsetStatus' => $source['orderDataStatus']['X-Caiwu-Data-Status'] ?? null, - 'billOffsetError' => $source['orderDataError'], + 'billOffsetStatus' => $status, + 'billOffsetError' => $error, ]; } + + private function emptyPaginator(int $page, int $limit): Paginator + { + return new Paginator([], $limit, $page, [ + 'path' => request()->url(), + 'query' => request()->except('page'), + ]); + } } diff --git a/modules/EcommerceFinance/View/OrderDataPageData.php b/modules/EcommerceFinance/View/OrderDataPageData.php index 7ee2d62f..95b20c99 100644 --- a/modules/EcommerceFinance/View/OrderDataPageData.php +++ b/modules/EcommerceFinance/View/OrderDataPageData.php @@ -25,42 +25,23 @@ class OrderDataPageData public function afterSales(?array $filters = null, ?int $page = null, ?int $limit = null, bool $includeWorkOrders = true): array { - $filters ??= [ - 'from' => $this->date(request('from')) ?: now()->subDays(6)->toDateString(), - 'to' => $this->date(request('to')) ?: now()->toDateString(), - 'order_no' => $this->text(request('order_no')), - 'shop_name' => $this->text(request('shop_name')), - 'sku_id' => $this->text(request('sku_id')), - ]; + $filters = $this->afterSalesFilters($filters); [$requestPage, $requestLimit] = $this->pageSettings(); $page = max($page ?? $requestPage, 1); $limit = min(max($limit ?? $requestLimit, 1), 100); try { - $mirror = $this->mirrorOrders ?? app(OrderDataMirrorRepository::class); - if ($mirror->ready()) { - $result = $mirror->afterSales($filters, $page, $limit); - - return [ - 'orderDataRows' => $this->paginator((array) ($result['rows'] ?? []), $page, $limit), - 'orderDataFilters' => $filters, - 'orderDataStatus' => ['X-Caiwu-Data-Status' => 'mirror'], - 'orderDataError' => null, - 'afterSalesWorkOrders' => $includeWorkOrders ? $this->afterSalesWorkOrders($filters) : $this->emptyAfterSalesWorkOrders(), - ]; - } - - $cache = $this->caches->forSource(self::SOURCE); - $result = $cache->remember( - 'order-data.after-sales.list', - compact('filters', 'page', 'limit'), - fn () => $this->orders->afterSales($filters, $page, $limit), - ); + $result = $this->afterSalesRows($filters, $page, $limit); return [ - 'orderDataRows' => $this->paginator((array) ($result['rows'] ?? []), $page, $limit), + 'orderDataRows' => $this->paginator( + (array) ($result['rows'] ?? []), + $page, + $limit, + (bool) ($result['has_more'] ?? false), + ), 'orderDataFilters' => $filters, - 'orderDataStatus' => $cache->responseHeaders(), + 'orderDataStatus' => $result['status'] ?? [], 'orderDataError' => null, 'afterSalesWorkOrders' => $includeWorkOrders ? $this->afterSalesWorkOrders($filters) : $this->emptyAfterSalesWorkOrders(), ]; @@ -73,6 +54,66 @@ class OrderDataPageData } } + /** + * Return the same normalized filters used by the raw after-sales page. + * Exporters and sibling views must use this contract instead of rebuilding + * date and text normalization independently. + * + * @param array|null $input + * @return array{from:string,to:string,order_no:string,shop_name:string,sku_id:string} + */ + public function afterSalesFilters(?array $input = null): array + { + $input ??= request()->query(); + + return [ + 'from' => $this->date($input['from'] ?? null) ?: now()->subDays(6)->toDateString(), + 'to' => $this->date($input['to'] ?? null) ?: now()->toDateString(), + 'order_no' => $this->text($input['order_no'] ?? null), + 'shop_name' => $this->text($input['shop_name'] ?? null), + 'sku_id' => $this->text($input['sku_id'] ?? null), + ]; + } + + /** + * Fetch raw after-sales rows for a page. Both the page and CSV export use + * this method so source selection, filters, joins and row normalization stay + * identical. Repositories over-fetch one row to determine whether another + * page exists; the sentinel is removed before this shared contract returns. + * + * @return array{rows:array>,has_more:bool,status:array} + */ + public function afterSalesRows(array $filters, int $page, int $limit): array + { + $limit = min(max($limit, 1), 100); + $mirror = $this->mirrorOrders ?? app(OrderDataMirrorRepository::class); + if ($mirror->ready()) { + $result = $mirror->afterSales($filters, $page, $limit); + $rows = array_values((array) ($result['rows'] ?? [])); + $hasMore = count($rows) > $limit; + + return [ + 'rows' => array_slice($rows, 0, $limit), + 'has_more' => $hasMore, + 'status' => ['X-Caiwu-Data-Status' => 'mirror'], + ]; + } + + $cache = $this->caches->forSource(self::SOURCE); + $result = $cache->remember( + 'order-data.after-sales.list', + compact('filters', 'page', 'limit'), + fn () => $this->orders->afterSales($filters, $page, $limit), + ); + $rows = array_values((array) ($result['rows'] ?? [])); + + return [ + 'rows' => array_slice($rows, 0, $limit), + 'has_more' => count($rows) > $limit, + 'status' => $cache->responseHeaders(), + ]; + } + public function promotionExpenses(): array { $filters = [ @@ -165,9 +206,9 @@ class OrderDataPageData ]; } - private function paginator(array $rows, int $page, int $limit): Paginator + private function paginator(array $rows, int $page, int $limit, ?bool $hasMore = null): Paginator { - return new Paginator( + $paginator = new Paginator( Collection::make($rows)->map(static fn (array $row): object => (object) $row), $limit, $page, @@ -176,6 +217,12 @@ class OrderDataPageData 'query' => request()->except('page'), ], ); + + if ($hasMore !== null) { + $paginator->hasMorePagesWhen($hasMore); + } + + return $paginator; } /** @return array{0:int,1:int} */ diff --git a/resources/views/business/pages/order-data/bill-offset.blade.php b/resources/views/business/pages/order-data/bill-offset.blade.php index 38d75862..79210998 100644 --- a/resources/views/business/pages/order-data/bill-offset.blade.php +++ b/resources/views/business/pages/order-data/bill-offset.blade.php @@ -1,7 +1,7 @@ @php $rows = $billOffsetRows ?? null; $filters = $billOffsetFilters ?? []; - $summary = $billOffsetSummary ?? ['amount' => 0, 'count' => 0]; + $summary = $billOffsetSummary ?? ['amount' => null, 'count' => 0]; $status = $billOffsetStatus ?? null; $columns = [ 'internal_order_no' => '内部订单号', @@ -31,15 +31,15 @@
- -
+ +
- -
+ +
@@ -72,7 +72,7 @@
本页记录数{{ number_format((int) ($summary['count'] ?? 0)) }}
-
本页冲减收入合计{{ $summary['amount'] === null ? '-' : number_format((float) $summary['amount'], 2) }}
+
本页 500112 冲减收入合计{{ ($summary['amount'] ?? null) === null ? '-' : number_format((float) $summary['amount'], 2) }}
@if ($status)
数据状态:{{ strtoupper((string) $status) }}
@@ -85,7 +85,7 @@ @foreach ($columns as $field => $header) @php $value = data_get($row, $field); @endphp - $field === 'account_500112'])>{{ ($value === null || $value === '') ? '-' : ($field === 'account_500112' ? number_format((float) $value, 2) : $value) }} + $field === 'account_500112'])>{{ ($value === null || $value === '') ? '-' : ($field === 'account_500112' ? number_format((float) $value, 2) : $value) }} @endforeach @empty @@ -95,7 +95,7 @@
- 当前页 {{ $rows?->count() ?? 0 }} 条 + 当前页 {{ $rows?->count() ?? 0 }} 条 @if ($status) · {{ strtoupper((string) $status) }} @endif
!$rows || $rows->onFirstPage()]) aria-label="上一页"> 第 {{ $rows?->currentPage() ?? 1 }} 页 diff --git a/tests/Feature/Business/PagesTest.php b/tests/Feature/Business/PagesTest.php index 74e03258..dd0759e2 100644 --- a/tests/Feature/Business/PagesTest.php +++ b/tests/Feature/Business/PagesTest.php @@ -4,11 +4,11 @@ namespace Tests\Feature\Business; use App\Models\Auth\Permission; use Mockery; -use Modules\EcommerceFinance\View\OrderDataPageData; use Modules\EcommerceFinance\Repositories\CourierFreightReconciliationMirrorRepository; use Modules\EcommerceFinance\Repositories\NonJingxiRefundMirrorRepository; use Modules\EcommerceFinance\Repositories\StoreReceivingAlipayBillMirrorRepository; use Modules\EcommerceFinance\Repositories\StoreReceivingDouyinBillMirrorRepository; +use Modules\EcommerceFinance\View\OrderDataPageData; use Tests\Feature\FeatureTestCase; class PagesTest extends FeatureTestCase @@ -323,6 +323,17 @@ class PagesTest extends FeatureTestCase Permission::where('name', 'read-admin-panel')->firstOrFail() ); + $filters = [ + 'from' => now()->subDays(6)->toDateString(), 'to' => now()->toDateString(), + 'order_no' => '', 'shop_name' => '', 'sku_id' => '', + ]; + $orderData = Mockery::mock(OrderDataPageData::class); + $orderData->shouldReceive('afterSalesFilters')->zeroOrMoreTimes()->andReturn($filters); + $orderData->shouldReceive('afterSalesRows')->zeroOrMoreTimes()->andReturn([ + 'rows' => [], 'has_more' => false, 'status' => ['X-Caiwu-Data-Status' => 'direct'], + ]); + $this->app->instance(OrderDataPageData::class, $orderData); + $this->loginAs(); $this->get(route('business.pages.show', [ @@ -348,7 +359,8 @@ class PagesTest extends FeatureTestCase ])) ->assertOk() ->assertSeeText('账单冲减') - ->assertSeeText('账单冲减镜像尚未完成首次同步'); + ->assertSeeText('暂无数据') + ->assertDontSeeText('账单冲减镜像尚未完成首次同步'); $this->withExceptionHandling(); @@ -362,55 +374,121 @@ class PagesTest extends FeatureTestCase } } - private function billOffsetSourceRows(array $rows, array $filters = [], bool $hasMore = false, ?string $error = null): array + public function testBillOffsetPageUsesAfterSalesRowsAndTheOriginalAccountField(): void { - $paginator = new \Illuminate\Pagination\Paginator($rows, 100, 1); - $paginator->hasMorePagesWhen($hasMore); - - return [ - 'orderDataRows' => $paginator, - 'orderDataFilters' => $filters, - 'orderDataStatus' => ['X-Caiwu-Data-Status' => 'mirror'], - 'orderDataError' => $error, + $this->user->attachPermission( + Permission::where('name', 'read-admin-panel')->firstOrFail() + ); + $filters = [ + 'from' => '2026-08-01', 'to' => '2026-08-31', 'order_no' => '', 'shop_name' => '', 'sku_id' => '', ]; - } - - public function testBillOffsetPageUsesTheSameOriginalOrderRowsAndAccountField(): void - { - $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); - $source = Mockery::mock(OrderDataPageData::class); - $source->shouldReceive('afterSales')->once()->with(null, null, null, false)->andReturn($this->billOffsetSourceRows([ - ['internal_order_no' => '9228703', 'online_order_no' => '5127724585360006527', - 'shop_name' => '测试店铺', 'product_code' => 'SKU-1', 'account_500112' => -20, - 'reversal_order_amount' => 987.65], - ], ['from' => '2026-08-01', 'to' => '2026-08-31'])); - $this->app->instance(OrderDataPageData::class, $source); + $orderData = Mockery::mock(OrderDataPageData::class); + $orderData->shouldReceive('afterSalesFilters')->once()->andReturn($filters); + $orderData->shouldReceive('afterSalesRows')->once()->with($filters, 1, 25)->andReturn([ + 'rows' => [[ + 'internal_order_no' => '9228703', 'online_order_no' => '5127724585360006527', + 'shop_name' => '天猫-亿家达旗舰店', 'shop_code' => 'SHOP-1', 'product_code' => 'DNZ005374N79V01', + 'style_code' => 'STYLE-1', 'shop_product_code' => 'SP-1', 'shop_style_code' => 'SI-1', 'account_500112' => -20, + ]], + 'has_more' => false, 'status' => ['X-Caiwu-Data-Status' => 'direct'], + ]); + $this->app->instance(OrderDataPageData::class, $orderData); $this->loginAs()->get(route('business.pages.show', [ 'section' => 'order-data', 'page' => 'bill-offset', 'from' => '2026-08-01', 'to' => '2026-08-31', - ]))->assertOk()->assertSeeText('9228703')->assertSeeText('测试店铺')->assertSeeText('-20.00') - ->assertDontSeeText('987.65')->assertSeeText('本页冲减收入合计') - ->assertSee('id="bill-offset-export-button"', false)->assertSeeText('数据状态:MIRROR'); + ]))->assertOk() + ->assertSeeText('9228703') + ->assertSeeText('天猫-亿家达旗舰店') + ->assertSeeText('-20.00') + ->assertSeeText('本页 500112 冲减收入合计') + ->assertSee(e(route('business.bill-offset.export', [ + 'from' => '2026-08-01', + 'to' => '2026-08-31', + ])), false) + ->assertSee('id="bill-offset-export-button"', false) + ->assertSeeText('数据状态:DIRECT'); } - public function testBillOffsetKeepsUnknownAccountAmountsEmpty(): void + public function testBillOffsetPageKeepsEmptyAccountFieldAsDash(): void + { + $this->user->attachPermission( + Permission::where('name', 'read-admin-panel')->firstOrFail() + ); + $filters = [ + 'from' => '2026-08-01', 'to' => '2026-08-31', 'order_no' => '', 'shop_name' => '', 'sku_id' => '', + ]; + $orderData = Mockery::mock(OrderDataPageData::class); + $orderData->shouldReceive('afterSalesFilters')->once()->andReturn($filters); + $orderData->shouldReceive('afterSalesRows')->once()->with($filters, 1, 25)->andReturn([ + 'rows' => [[ + 'internal_order_no' => 'ORDER-EMPTY', 'online_order_no' => 'ONLINE-EMPTY', + 'shop_name' => '空值店铺', 'account_500112' => null, + ]], + 'has_more' => false, 'status' => ['X-Caiwu-Data-Status' => 'direct'], + ]); + $this->app->instance(OrderDataPageData::class, $orderData); + + $this->loginAs()->get(route('business.pages.show', [ + 'section' => 'order-data', 'page' => 'bill-offset', 'from' => '2026-08-01', 'to' => '2026-08-31', + ]))->assertOk() + ->assertSeeText('ORDER-EMPTY') + ->assertSeeText('空值店铺') + ->assertSeeText('本页 500112 冲减收入合计') + ->assertSee('data-export-value=""', false) + ->assertSeeText('-') + ->assertDontSeeText('0.00'); + } + + public function testBillOffsetPageShowsReadFailureSeparatelyFromAnUnreadyMirror(): void + { + $this->user->attachPermission( + Permission::where('name', 'read-admin-panel')->firstOrFail() + ); + $filters = [ + 'from' => now()->subDays(6)->toDateString(), 'to' => now()->toDateString(), + 'order_no' => '', 'shop_name' => '', 'sku_id' => '', + ]; + $orderData = Mockery::mock(OrderDataPageData::class); + $orderData->shouldReceive('afterSalesFilters')->once()->andReturn($filters); + $orderData->shouldReceive('afterSalesRows')->once()->andThrow(new \RuntimeException('database unavailable')); + $this->app->instance(OrderDataPageData::class, $orderData); + + $this->loginAs()->get(route('business.pages.show', [ + 'section' => 'order-data', 'page' => 'bill-offset', + ]))->assertOk() + ->assertSeeText('订单售后费用数据暂时无法读取,请稍后重试。'); + } + + public function testBillOffsetExportKeepsUnknownAccountAmountsEmpty(): void { $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); - $source = Mockery::mock(OrderDataPageData::class); - $source->shouldReceive('afterSales')->once()->andReturn($this->billOffsetSourceRows([ - ['internal_order_no' => 'ORDER-EMPTY', 'account_500112' => null, 'reversal_order_amount' => 987.65], - ])); - $this->app->instance(OrderDataPageData::class, $source); - $this->loginAs()->get(route('business.pages.show', ['section' => 'order-data', 'page' => 'bill-offset'])) - ->assertOk()->assertSeeText('ORDER-EMPTY')->assertDontSeeText('987.65')->assertDontSeeText('0.00'); + $filters = [ + 'from' => '2026-08-01', 'to' => '2026-08-31', 'order_no' => 'ORDER-EMPTY', 'shop_name' => '', 'sku_id' => '', + ]; + $orderData = Mockery::mock(OrderDataPageData::class); + $orderData->shouldReceive('afterSalesFilters')->once()->with($filters)->andReturn($filters); + $orderData->shouldReceive('afterSalesRows')->once()->with($filters, 1, 100)->andReturn([ + 'rows' => [[ + 'internal_order_no' => 'ORDER-EMPTY', 'account_500112' => null, 'reversal_order_amount' => 987.65, + ]], + 'has_more' => false, 'status' => ['X-Caiwu-Data-Status' => 'direct'], + ]); + $this->app->instance(OrderDataPageData::class, $orderData); + + $csv = $this->loginAs()->get(route('business.bill-offset.export', $filters))->streamedContent(); + $this->assertStringNotContainsString('987.65', $csv); + $lines = preg_split('/\r?\n/', trim($csv)); + $this->assertSame('', str_getcsv($lines[1])[8]); } public function testBillOffsetShowsTheOriginalDataErrorAndBlocksAnEmptyExport(): void { $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); - $source = Mockery::mock(OrderDataPageData::class); - $source->shouldReceive('afterSales')->twice()->andReturn($this->billOffsetSourceRows([], [], false, '订单售后费用数据暂时无法读取,请稍后重试。')); - $this->app->instance(OrderDataPageData::class, $source); + $filters = ['from' => '2026-08-01', 'to' => '2026-08-31', 'order_no' => '', 'shop_name' => '', 'sku_id' => '']; + $orderData = Mockery::mock(OrderDataPageData::class); + $orderData->shouldReceive('afterSalesFilters')->twice()->andReturn($filters); + $orderData->shouldReceive('afterSalesRows')->twice()->andThrow(new \RuntimeException('database unavailable')); + $this->app->instance(OrderDataPageData::class, $orderData); $this->loginAs()->get(route('business.pages.show', ['section' => 'order-data', 'page' => 'bill-offset'])) ->assertOk()->assertSeeText('订单售后费用数据暂时无法读取,请稍后重试。'); $this->withExceptionHandling()->get(route('business.bill-offset.export'))->assertStatus(503); @@ -419,19 +497,29 @@ class PagesTest extends FeatureTestCase public function testBillOffsetExportIncludesAllPagesFromOriginalDataWithTheSameFilters(): void { $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); - $source = Mockery::mock(OrderDataPageData::class); $filters = ['from' => '2026-08-01', 'to' => '2026-08-31', 'order_no' => 'ORDER', 'shop_name' => '', 'sku_id' => '']; - $source->shouldReceive('afterSales')->once()->with($filters, 1, 100, false)->andReturn($this->billOffsetSourceRows([ - ['internal_order_no' => 'ORDER-1', 'online_order_no' => '001234567890123456789', 'shop_name' => '测试店铺', 'account_500112' => -20], - ], $filters, true)); - $source->shouldReceive('afterSales')->once()->with($filters, 2, 100, false)->andReturn($this->billOffsetSourceRows([ - ['internal_order_no' => 'ORDER-101', 'account_500112' => null, 'reversal_order_amount' => 987.65], - ], $filters)); - $this->app->instance(OrderDataPageData::class, $source); + $orderData = Mockery::mock(OrderDataPageData::class); + $orderData->shouldReceive('afterSalesFilters')->once()->with($filters)->andReturn($filters); + $orderData->shouldReceive('afterSalesRows')->once()->with($filters, 1, 100)->andReturn([ + 'rows' => [[ + 'internal_order_no' => 'ORDER-1', 'online_order_no' => '001234567890123456789', + 'shop_name' => '测试店铺', 'account_500112' => -20, + ]], + 'has_more' => true, 'status' => ['X-Caiwu-Data-Status' => 'direct'], + ]); + $orderData->shouldReceive('afterSalesRows')->once()->with($filters, 2, 100)->andReturn([ + 'rows' => [[ + 'internal_order_no' => 'ORDER-101', 'account_500112' => null, 'reversal_order_amount' => 987.65, + ]], + 'has_more' => false, 'status' => ['X-Caiwu-Data-Status' => 'direct'], + ]); + $this->app->instance(OrderDataPageData::class, $orderData); $response = $this->loginAs()->get(route('business.bill-offset.export', $filters)); $response->assertOk()->assertHeader('Content-Type', 'text/csv; charset=UTF-8'); $csv = $response->streamedContent(); $this->assertStringContainsString('500112冲减收入', $csv); + $this->assertStringContainsString('测试店铺', $csv); + $this->assertStringContainsString('-20.00', $csv); $this->assertStringContainsString("'001234567890123456789", $csv); $this->assertStringContainsString('ORDER-1', $csv); $this->assertStringContainsString('ORDER-101', $csv);