-
本页 500112 冲减收入合计{{ number_format($pageAmount, 2) }}
+
记录数{{ number_format((int) ($summary['count'] ?? 0)) }}
+
冲减收入合计{{ number_format((float) ($summary['amount'] ?? 0), 2) }}
- @if ($status)
-
数据状态:{{ strtoupper((string) $status) }}
+ @if (!empty($sync['last_succeeded_at']))
+
数据状态:{{ strtoupper((string) ($sync['status'] ?? 'mirror')) }} · 最近同步 {{ $sync['last_succeeded_at'] }}
@endif
@@ -130,36 +115,5 @@
trigger.addEventListener('click', () => { input.focus(); if (typeof input.showPicker === 'function') input.showPicker(); else input.click(); });
});
- const button = document.getElementById('bill-offset-export-button');
- if (!button || button.dataset.exportBound === 'true') return;
- button.dataset.exportBound = 'true';
- button.addEventListener('click', () => {
- const table = document.getElementById(button.dataset.exportTable || '');
- const bodyRows = table ? Array.from(table.querySelectorAll('tbody tr:not(.is-empty)')) : [];
- if (!table || bodyRows.length === 0) return;
-
- const protectSpreadsheetFormula = (value) => /^[=+\-@\t\r]/.test(value) ? `'${value}` : value;
- const csvCell = (value, isNumber = false) => {
- const raw = String(value ?? '').replace(/\r\n?/g, '\n');
- const normalized = isNumber ? raw : protectSpreadsheetFormula(raw);
- return `"${normalized.replace(/"/g, '""')}"`;
- };
- const header = Array.from(table.querySelectorAll('thead th'))
- .map((cell) => csvCell(cell.textContent.trim()));
- const rows = bodyRows.map((row) => Array.from(row.querySelectorAll('td'))
- .map((cell) => csvCell(
- cell.dataset.exportValue ?? cell.textContent.trim(),
- cell.dataset.exportNumber === 'true',
- )));
- const csv = [header, ...rows].map((row) => row.join(',')).join('\r\n');
- const blob = new Blob(['', csv], { type: 'text/csv;charset=utf-8' });
- const url = URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.href = url;
- link.download = button.dataset.exportFilename || '账单冲减.csv';
- document.body.appendChild(link);
- link.click();
- window.setTimeout(() => { link.remove(); URL.revokeObjectURL(url); }, 1000);
- });
})();
diff --git a/tests/Feature/Business/PagesTest.php b/tests/Feature/Business/PagesTest.php
index d8d037ca..5c09a8da 100644
--- a/tests/Feature/Business/PagesTest.php
+++ b/tests/Feature/Business/PagesTest.php
@@ -306,6 +306,22 @@ class PagesTest extends FeatureTestCase
->assertSeeText('数据状态:SUCCEEDED');
}
+ public function testBillOffsetPageShowsReadFailureSeparatelyFromAnUnreadyMirror(): void
+ {
+ $this->user->attachPermission(
+ Permission::where('name', 'read-admin-panel')->firstOrFail()
+ );
+ $repository = Mockery::mock(BillOffsetMirrorRepository::class);
+ $repository->shouldReceive('paginate')->once()->andThrow(new \RuntimeException('database unavailable'));
+ $this->app->instance(BillOffsetMirrorRepository::class, $repository);
+
+ $this->loginAs()->get(route('business.pages.show', [
+ 'section' => 'order-data', 'page' => 'bill-offset',
+ ]))->assertOk()
+ ->assertSeeText('账单冲减数据暂时无法读取,请稍后重试。')
+ ->assertDontSeeText('账单冲减镜像尚未完成首次同步,当前不把空镜像解释为没有数据。');
+ }
+
public function testBillOffsetExportReturnsCsvForTheFilteredRows(): void
{
$this->user->attachPermission(
@@ -338,6 +354,42 @@ class PagesTest extends FeatureTestCase
$this->assertStringContainsString('-20', $csv);
}
+ public function testBillOffsetExportIncludesAllPagesForTheSameFilters(): void
+ {
+ $this->user->attachPermission(
+ Permission::where('name', 'read-admin-panel')->firstOrFail()
+ );
+ $repository = Mockery::mock(BillOffsetMirrorRepository::class);
+ $filters = [
+ 'from' => '2026-08-01', 'to' => '2026-08-31', 'order_no' => '', 'shop_name' => '', 'sku_id' => '',
+ ];
+ $repository->shouldReceive('paginate')->once()->with($filters, 1, 100)->andReturn([
+ 'data' => [[
+ 'internal_order_no' => 'ORDER-1', 'online_order_no' => 'ONLINE-1', 'shop_name' => '店铺一',
+ 'shop_code' => 'SHOP-1', 'product_code' => 'SKU-1', 'style_code' => 'STYLE-1',
+ 'shop_product_code' => 'SP-1', 'shop_style_code' => 'SI-1', 'amount' => -20,
+ ]],
+ 'total' => 101, 'sync' => ['status' => 'succeeded'],
+ ]);
+ $repository->shouldReceive('paginate')->once()->with($filters, 2, 100)->andReturn([
+ 'data' => [[
+ 'internal_order_no' => 'ORDER-101', 'online_order_no' => 'ONLINE-101', 'shop_name' => '店铺一',
+ 'shop_code' => 'SHOP-1', 'product_code' => 'SKU-101', 'style_code' => 'STYLE-101',
+ 'shop_product_code' => 'SP-101', 'shop_style_code' => 'SI-101', 'amount' => 18.25,
+ ]],
+ 'total' => 101, 'sync' => ['status' => 'succeeded'],
+ ]);
+ $this->app->instance(BillOffsetMirrorRepository::class, $repository);
+
+ $csv = $this->loginAs()->get(route('business.bill-offset.export', [
+ 'from' => '2026-08-01', 'to' => '2026-08-31',
+ ]))->streamedContent();
+
+ $this->assertStringContainsString('ORDER-1', $csv);
+ $this->assertStringContainsString('ORDER-101', $csv);
+ $this->assertSame(3, substr_count($csv, "\n"));
+ }
+
public function testFinancialAfterSalesPageRendersMirroredJushuitanRefunds(): void
{
$this->user->attachPermission(
diff --git a/tests/Unit/EcommerceFinance/BillOffsetMirrorRepositoryTest.php b/tests/Unit/EcommerceFinance/BillOffsetMirrorRepositoryTest.php
index 7e82c470..569f9739 100644
--- a/tests/Unit/EcommerceFinance/BillOffsetMirrorRepositoryTest.php
+++ b/tests/Unit/EcommerceFinance/BillOffsetMirrorRepositoryTest.php
@@ -64,11 +64,13 @@ class BillOffsetMirrorRepositoryTest extends TestCase
DB::connection('business_mirror')->table('ops_jst_original_bill_info')->insert([
['Id' => 1, 'fee_time' => '2026-08-01 10:00:00', 'fee_name' => '500112冲减收入', 'inout_type' => '收入', 'so_id' => 'SO-1', 'sku_id' => 'SKU-1', 'amount' => -20],
['Id' => 2, 'fee_time' => '2026-08-02 10:00:00', 'fee_name' => '500112冲减收入', 'inout_type' => '收入', 'so_id' => 'SO-2', 'sku_id' => 'SKU-2', 'amount' => 18.25],
+ ['Id' => 3, 'fee_time' => '2026-08-03 10:00:00', 'fee_name' => '冲减收入', 'inout_type' => '收入', 'so_id' => 'SO-3', 'sku_id' => 'SKU-3', 'amount' => 7],
['Id' => 4, 'fee_time' => '2026-08-01 10:00:00', 'fee_name' => '订单收入', 'inout_type' => '收入', 'so_id' => 'SO-1', 'sku_id' => 'SKU-1', 'amount' => 100],
]);
DB::connection('business_mirror')->table('ops_jst_orders')->insert([
['o_id' => 1001, 'so_id' => 'SO-1', 'shop_name' => '测试店铺', 'shop_id' => 'SHOP-1'],
['o_id' => 1002, 'so_id' => 'SO-2', 'shop_name' => '其他店铺', 'shop_id' => 'SHOP-2'],
+ ['o_id' => 1003, 'so_id' => 'SO-3', 'shop_name' => '历史店铺', 'shop_id' => 'SHOP-3'],
]);
DB::connection('business_mirror')->table('ops_jst_order_item')->insert([
['o_id' => 1001, 'sku_id' => 'SKU-1', 'i_id' => 'STYLE-1', 'shop_sku_id' => 'SP-1', 'shop_i_id' => 'SI-1'],
@@ -82,6 +84,11 @@ class BillOffsetMirrorRepositoryTest extends TestCase
$this->assertSame('STYLE-1', $result['data'][0]['style_code']);
$this->assertSame(-20.0, $result['summary']['amount']);
$this->assertSame('succeeded', $result['sync']['status']);
+
+ $all = $this->repository->paginate([], 1, 10);
+ $this->assertSame(3, $all['total']);
+ $this->assertSame(5.25, $all['summary']['amount']);
+ $this->assertSame('SO-3', $all['data'][0]['online_order_no']);
}
public function test_it_rejects_unready_mirror(): void
diff --git a/tests/Unit/EcommerceFinance/OrderDataMirrorRepositoryTest.php b/tests/Unit/EcommerceFinance/OrderDataMirrorRepositoryTest.php
index f602b41e..fed58fee 100644
--- a/tests/Unit/EcommerceFinance/OrderDataMirrorRepositoryTest.php
+++ b/tests/Unit/EcommerceFinance/OrderDataMirrorRepositoryTest.php
@@ -76,6 +76,7 @@ class OrderDataMirrorRepositoryTest extends TestCase
$this->assertSame(40.0, $row['product_cost']);
$this->assertSame(1.0, $row['post_return_quantity']);
$this->assertSame('https://item.jd.com/1001.html', $row['product_link']);
+ $this->assertNull($row['account_500112']);
$this->assertNull($row['account_8001201']);
}
diff --git a/tests/Unit/EcommerceFinance/OrderDataRepositoryTest.php b/tests/Unit/EcommerceFinance/OrderDataRepositoryTest.php
index 2e061c04..de6f9c2a 100644
--- a/tests/Unit/EcommerceFinance/OrderDataRepositoryTest.php
+++ b/tests/Unit/EcommerceFinance/OrderDataRepositoryTest.php
@@ -101,6 +101,7 @@ class OrderDataRepositoryTest extends TestCase
$this->assertSame('https://item.jd.com/1001.html', $result['rows'][0]['product_link']);
$this->assertSame('AF-1', $result['rows'][0]['after_sales_no']);
$this->assertSame(1.0, $result['rows'][0]['post_return_quantity']);
+ $this->assertNull($result['rows'][0]['account_500112']);
$this->assertNull($result['rows'][0]['account_8001201']);
}
--
2.47.2
From 98ec0d0e23a9c3a975e14c763274e39dfcb18863 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=8B=8F=E6=9C=A8?= <594326199@qq.com>
Date: Wed, 9 Sep 2026 16:52:24 +0800
Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=E8=B4=A6=E5=8D=95=E5=86=B2=E5=87=8F?=
=?UTF-8?q?=E5=A4=8D=E7=94=A8=E5=94=AE=E5=90=8E=E8=B4=B9=E7=94=A8=E5=8F=96?=
=?UTF-8?q?=E6=95=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/Http/Controllers/Business/Pages.php | 60 ++++-----
.../View/BillOffsetPageData.php | 79 ++++++------
.../View/OrderDataPageData.php | 107 +++++++++++-----
.../pages/order-data/bill-offset.blade.php | 34 ++---
tests/Feature/Business/PagesTest.php | 121 +++++++++++++-----
5 files changed, 246 insertions(+), 155 deletions(-)
diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php
index df0ca982..0290a46e 100644
--- a/app/Http/Controllers/Business/Pages.php
+++ b/app/Http/Controllers/Business/Pages.php
@@ -25,7 +25,6 @@ use Illuminate\Support\Collection;
use Illuminate\Database\QueryException;
use Modules\EcommerceFinance\Exceptions\UpstreamSnapshotPendingException;
use Modules\EcommerceFinance\Repositories\AlibabaBillRepository;
-use Modules\EcommerceFinance\Repositories\BillOffsetMirrorRepository;
use Modules\EcommerceFinance\Repositories\CourierInformationMirrorRepository;
use Modules\EcommerceFinance\Repositories\CourierFreightReconciliationMirrorRepository;
use Modules\EcommerceFinance\Repositories\EdgeBandingRepository;
@@ -44,6 +43,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;
@@ -254,35 +254,17 @@ 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(BillOffsetMirrorRepository::class);
- $page = 1;
+ $orderData = app(OrderDataPageData::class);
+ $filters = $orderData->afterSalesFilters($request->query());
+ $limit = 100;
try {
- $result = $repository->paginate($filters, $page, 100);
- } catch (\RuntimeException $exception) {
- if ($exception->getMessage() === 'bill_offset_mirror_not_ready') {
- abort(503, '账单冲减镜像尚未完成首次同步');
- }
-
- throw $exception;
+ $result = $orderData->afterSalesRows($filters, 1, $limit);
+ } catch (Throwable $exception) {
+ report($exception);
+ abort(503, '订单售后费用数据暂时无法读取,请稍后重试。');
}
- abort_if(($result['sync']['status'] ?? 'unknown') === 'unknown', 503, '账单冲减镜像尚未完成首次同步');
- return response()->streamDownload(function () use ($repository, $filters, $result): void {
+ return response()->streamDownload(function () use ($orderData, $filters, $result, $limit): void {
$csv = fopen('php://output', 'w');
if ($csv === false) {
throw new \RuntimeException('无法创建账单冲减导出流。');
@@ -292,23 +274,31 @@ class Pages extends Controller
'internal_order_no' => '内部订单号', 'online_order_no' => '线上订单号',
'shop_name' => '店铺名称', 'shop_code' => '店铺编码', 'product_code' => '商品编码',
'style_code' => '款式编码(参考)', 'shop_product_code' => '店铺商品编码',
- 'shop_style_code' => '店铺款式编码', 'amount' => '500112冲减收入',
+ 'shop_style_code' => '店铺款式编码', 'account_500112' => '500112冲减收入',
];
fputcsv($csv, array_values($labels));
- $page = 1;
$current = $result;
+ $page = 1;
do {
- foreach ($current['data'] as $row) {
+ foreach (($current['rows'] ?? []) as $row) {
fputcsv($csv, array_map(
- fn (string $field): string => $this->csvText($row[$field] ?? ''),
+ function (string $field) use ($row): string {
+ $value = $row[$field] ?? null;
+ if ($field === 'account_500112' && $value !== null && $value !== '') {
+ $value = number_format((float) $value, 2, '.', '');
+ }
+
+ return $this->csvText($value);
+ },
array_keys($labels),
));
}
- $page++;
- if (($page - 1) * 100 < ($current['total'] ?? 0)) {
- $current = $repository->paginate($filters, $page, 100);
+ $hasMore = (bool) ($current['has_more'] ?? false);
+ if ($hasMore) {
+ $page++;
+ $current = $orderData->afterSalesRows($filters, $page, $limit);
}
- } while (($page - 1) * 100 < ($current['total'] ?? 0));
+ } while ($hasMore);
fclose($csv);
}, '账单冲减_' . now()->format('Ymd_His') . '.csv', [
'Content-Type' => 'text/csv; charset=UTF-8',
diff --git a/modules/EcommerceFinance/View/BillOffsetPageData.php b/modules/EcommerceFinance/View/BillOffsetPageData.php
index 68ebb8d1..ffbf47cc 100644
--- a/modules/EcommerceFinance/View/BillOffsetPageData.php
+++ b/modules/EcommerceFinance/View/BillOffsetPageData.php
@@ -2,79 +2,78 @@
namespace Modules\EcommerceFinance\View;
-use Illuminate\Pagination\LengthAwarePaginator;
+use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;
-use Modules\EcommerceFinance\Repositories\BillOffsetMirrorRepository;
use Throwable;
class BillOffsetPageData
{
- public function __construct(private readonly BillOffsetMirrorRepository $bills)
+ public function __construct(private readonly OrderDataPageData $orderData)
{
}
/** @return array
*/
public function page(): 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->orderData->afterSalesFilters();
$page = max((int) request('page', 1), 1);
$limit = min(max((int) setting('default.list_limit', 25), 1), 100);
- $rows = new LengthAwarePaginator([], 0, $limit, $page, [
- 'path' => request()->url(),
- 'query' => request()->except('page'),
- ]);
- $summary = ['amount' => 0.0, 'count' => 0];
- $sync = ['status' => 'unavailable', 'last_succeeded_at' => null];
+ $rows = $this->emptyPaginator($page, $limit);
+ $status = null;
+ $pageAmount = null;
$error = null;
try {
- $result = $this->bills->paginate($filters, $page, $limit);
- $rows = new LengthAwarePaginator(
- Collection::make($result['data'] ?? []),
- (int) ($result['total'] ?? 0),
+ $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')],
+ [
+ 'path' => request()->url(),
+ 'query' => request()->except('page'),
+ ],
);
- $summary = $result['summary'] ?? $summary;
- $sync = $result['sync'] ?? $sync;
+ $rows->hasMorePagesWhen((bool) ($result['has_more'] ?? false));
+ $status = data_get($result, 'status.X-Caiwu-Data-Status');
+ $pageAmount = $this->pageAmount($rows->items());
} catch (Throwable $exception) {
- $mirrorNotReady = str_contains($exception->getMessage(), 'bill_offset_mirror_not_ready');
- if (! $mirrorNotReady) {
- report($exception);
- }
- $error = $mirrorNotReady
- ? '账单冲减镜像尚未完成首次同步,当前不把空镜像解释为没有数据。'
- : '账单冲减数据暂时无法读取,请稍后重试。';
+ report($exception);
+ $error = '订单售后费用数据暂时无法读取,请稍后重试。';
}
return [
'view' => 'business.pages.order-data.bill-offset',
'billOffsetRows' => $rows,
'billOffsetFilters' => $filters,
- 'billOffsetSummary' => $summary,
- 'billOffsetSync' => $sync,
+ 'billOffsetStatus' => $status,
+ 'billOffsetPageAmount' => $pageAmount,
'billOffsetError' => $error,
];
}
- private function text(mixed $value): string
+ /** @param array $items */
+ private function pageAmount(array $items): ?float
{
- return mb_substr(trim((string) $value), 0, 100);
+ $amount = null;
+
+ foreach ($items as $item) {
+ $value = data_get($item, 'account_500112');
+ if ($value === null || $value === '') {
+ continue;
+ }
+
+ $amount = ($amount ?? 0.0) + (float) $value;
+ }
+
+ return $amount === null ? null : round($amount, 2);
}
- private function date(mixed $value): string
+ private function emptyPaginator(int $page, int $limit): Paginator
{
- $value = trim((string) $value);
- if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $value, $matches) !== 1) {
- return '';
- }
- return checkdate((int) $matches[2], (int) $matches[3], (int) $matches[1]) ? $value : '';
+ 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 3a369b40..498877e2 100644
--- a/modules/EcommerceFinance/View/OrderDataPageData.php
+++ b/modules/EcommerceFinance/View/OrderDataPageData.php
@@ -25,40 +25,21 @@ class OrderDataPageData
public function afterSales(): 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();
[$page, $limit] = $this->pageSettings();
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' => $this->afterSalesWorkOrders($filters),
- ];
- }
-
- $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' => $this->afterSalesWorkOrders($filters),
];
@@ -71,6 +52,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 = [
@@ -163,9 +204,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,
@@ -174,6 +215,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 9bb8df8a..be412acf 100644
--- a/resources/views/business/pages/order-data/bill-offset.blade.php
+++ b/resources/views/business/pages/order-data/bill-offset.blade.php
@@ -1,8 +1,8 @@
@php
$rows = $billOffsetRows ?? null;
$filters = $billOffsetFilters ?? [];
- $summary = $billOffsetSummary ?? ['amount' => 0, 'count' => 0];
- $sync = $billOffsetSync ?? ['status' => 'unavailable', 'last_succeeded_at' => null];
+ $status = $billOffsetStatus ?? null;
+ $pageAmount = $billOffsetPageAmount ?? null;
$columns = [
'internal_order_no' => '内部订单号',
'online_order_no' => '线上订单号',
@@ -12,7 +12,7 @@
'style_code' => '款式编码(参考)',
'shop_product_code' => '店铺商品编码',
'shop_style_code' => '店铺款式编码',
- 'amount' => '500112冲减收入',
+ 'account_500112' => '500112冲减收入',
];
@endphp
@@ -31,15 +31,15 @@