快递应付:清空运费对账页面并停用错误数据源 (#338)

This commit is contained in:
limeng 2026-10-15 14:08:05 +08:00
parent 1131d073ce
commit cccd321195
4 changed files with 67 additions and 303 deletions

View File

@ -48,7 +48,6 @@ use Modules\EcommerceFinance\Repositories\AlibabaAccountBalanceRepository;
use Modules\EcommerceFinance\Repositories\AlibabaBillRepository;
use Modules\EcommerceFinance\View\BillOffsetPageData;
use Modules\EcommerceFinance\Repositories\CourierInformationMirrorRepository;
use Modules\EcommerceFinance\Repositories\CourierFreightReconciliationMirrorRepository;
use Modules\EcommerceFinance\Repositories\CourierSurchargeSettlementMirrorRepository;
use Modules\EcommerceFinance\Repositories\ShippingOrderRepository;
use Modules\EcommerceFinance\Repositories\ThirdPartyWarehouseStorageMirrorRepository;
@ -420,7 +419,6 @@ class Pages extends Controller
private readonly ProductCatalogMirrorRepository $productCatalog,
private readonly ProductPriceMirrorRepository $productPrices,
private readonly CourierInformationMirrorRepository $courierInformation,
private readonly CourierFreightReconciliationMirrorRepository $courierFreightReconciliations,
private readonly ThirdPartyWarehouseStorageMirrorRepository $thirdPartyWarehouseStorage,
private readonly RawBoardPayablesRepository $rawBoardPayables,
private readonly DingPayableWorkflowMirrorRepository $dingPayableWorkflows,
@ -574,18 +572,11 @@ class Pages extends Controller
}
if ($section === 'courier-payables' && $page === 'freight-reconciliation' && request()->boolean('courier_summary')) {
try {
$summary = $this->courierFreightReconciliations->requestSummary($this->freightReconciliationFilters());
$response = response()->json(['data' => $summary], $summary === null ? 202 : 200, [
'Cache-Control' => 'private, no-store', 'X-Accel-Buffering' => 'no',
]);
// Explicit framing lets clients finish reading before the
// application termination callback computes the aggregate.
return $response->header('Content-Length', strlen($response->getContent()));
} catch (Throwable $exception) {
report($exception);
return response()->json(['message' => '完整汇总暂时无法计算,明细仍可正常查看。'], 503, ['Cache-Control' => 'private, no-store']);
}
return response()->json([
'data' => ['count' => 0, 'freight' => 0],
'status' => 'disabled',
'message' => '原数据源已停用,本页面不再展示原运费对账数据。',
], 200, ['Cache-Control' => 'private, no-store']);
}
return view('business.pages.show', $this->viewData(
@ -4660,75 +4651,11 @@ class Pages extends Controller
return $text !== '' && is_numeric($text) ? (float) $text : null;
}
private function freightReconciliationFilters(): array
{
$request = request();
$date = static function (mixed $value): string {
$value = trim((string) $value);
return preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) === 1 ? $value : '';
};
return [
'express_no' => mb_substr(trim((string) $request->query('express_no', '')), 0, 80),
'express_name' => mb_substr(trim((string) $request->query('express_name', '')), 0, 80),
'month' => mb_substr(trim((string) $request->query('month', '')), 0, 20),
'status' => mb_substr(trim((string) $request->query('status', '')), 0, 40),
'modified_from' => $date($request->query('modified_from', '')),
'modified_to' => $date($request->query('modified_to', '')),
];
}
protected function contentForFreightReconciliation(): array
{
$request = request();
$filters = $this->freightReconciliationFilters();
$page = max((int) $request->query('page', 1), 1);
$limit = min(max((int) setting('default.list_limit', 25), 1), 100);
$rows = new Paginator([], $limit, $page, [
'path' => $request->url(),
'query' => $request->except('page'),
]);
$state = null;
$sync = ['code' => 'unavailable', 'last_succeeded_at' => null, 'lag_minutes' => null];
$summary = ['count' => null, 'freight' => null];
$error = null;
$pending = false;
try {
if (! $this->courierFreightReconciliations->available()) {
$error = '共享业务镜像尚未建立,运费对账暂时无法读取。';
} elseif (! $this->courierFreightReconciliations->ready()) {
$state = $this->courierFreightReconciliations->state();
$sync = $this->courierFreightReconciliations->syncStatus($state);
$pending = true;
$error = '运费对账镜像正在完成首次同步,完成前不会把未同步数据误判为无数据。';
} else {
$result = $this->courierFreightReconciliations->paginate($filters, $page, $limit);
$rows = new Paginator(
Collection::make($result['data'] ?? []),
$limit,
$page,
['path' => $request->url(), 'query' => $request->except('page')],
);
$rows->hasMorePagesWhen((bool) ($result['has_more'] ?? false));
$state = $result['state'] ?? null;
$sync = $result['sync'] ?? $sync;
$summary = $result['summary'] ?? $summary;
}
} catch (Throwable $exception) {
report($exception);
$error = '运费对账数据暂时无法读取,请稍后重试。';
}
// 原来源口径不适用;保留入口,但不读取明细、同步状态或汇总。
return [
'view' => 'business.pages.courier-payables.freight-reconciliation',
'freightReconciliations' => $rows,
'freightReconciliationFilters' => $filters,
'freightReconciliationSummary' => $summary,
'freightReconciliationState' => $state,
'freightReconciliationSync' => $sync,
'freightReconciliationError' => $error,
'freightReconciliationPending' => $pending,
'freightReconciliationSummaryUrl' => $error === null ? $request->fullUrlWithQuery(['courier_summary' => 1, 'page' => null]) : null,
];
}

View File

@ -1,131 +1,32 @@
@php
$filters = $freightReconciliationFilters ?? [];
$summary = $freightReconciliationSummary ?? [];
$summaryReady = isset($summary['count'], $summary['freight']);
$sync = $freightReconciliationSync ?? ['code' => 'unavailable', 'last_succeeded_at' => null, 'lag_minutes' => null];
$statusLabels = ['fresh' => '数据新鲜', 'stale' => '数据可能滞后', 'syncing' => '同步中', 'initializing' => '首次同步中', 'pending' => '等待同步', 'failed' => '同步失败', 'initial_failed' => '首次同步失败', 'unavailable' => '镜像不可用'];
$money = static fn (mixed $value): string => number_format((float) ($value ?? 0), 2);
$number = static fn (mixed $value): string => $value === null || $value === '' ? '-' : rtrim(rtrim(number_format((float) $value, 4, '.', ''), '0'), '.');
@endphp
<div class="courier-reconciliation-page">
<style>
.courier-reconciliation-page { color: #29251f; }
.courier-reconciliation-page .finance-table-field-picker__panel { left: 0; right: auto; }
.courier-reconciliation-page .recon-control { border: 1px solid #d9d0c2; border-radius: .375rem; padding: .5rem .7rem; font-size: .8125rem; background: #fff; outline: none; }
.courier-reconciliation-page .recon-control:focus { border-color: #b18a4a; box-shadow: 0 0 0 2px rgba(177,138,74,.15); }
.courier-reconciliation-page .recon-button { border-radius: .375rem; padding: .5rem 1rem; font-size: .8125rem; }
.courier-reconciliation-page .recon-button--primary { background: #5d492c; color: #fff; }
.courier-reconciliation-page .recon-button--primary:hover { background: #49391f; }
.courier-reconciliation-page .recon-button--secondary { border: 1px solid #d9d0c2; color: #655b4e; background: #fff; }
.courier-reconciliation-page .recon-button--secondary:hover { background: #f7f3ed; }
.courier-reconciliation-page .recon-table th { background: #f5f1ea; color: #6f675c; font-weight: 600; }
.courier-reconciliation-page .recon-table td, .courier-reconciliation-page .recon-table th { border-bottom: 1px solid #eee8de; padding: .65rem .75rem; white-space: nowrap; }
.courier-reconciliation-page .recon-table tbody tr:hover { background: #fcfaf7; }
</style>
<div class="flex flex-col gap-4 mb-5 sm:flex-row sm:items-start sm:justify-between">
<div>
<h1 class="text-xl font-semibold text-gray-800">{{ $page['title'] ?? '运费对账' }}</h1>
<p class="mt-1 text-xs text-gray-500">数据来源:JianSun · dbo.jst_order_express_details + dbo.Jst_SendData_View_extension + dbo.jst_salesOutboundInfo · 共享业务镜像</p>
<div class="mb-5 rounded-lg border border-[#D8D2C7] bg-[#F7F0DE] px-4 py-3 text-sm text-[#171717]" role="status">
<p class="font-semibold">原数据源已停用</p>
<p class="mt-1 text-xs text-[#595959]">原数据来源不适用于运费对账,已清空本页面展示;接入正确数据源前,不再展示原明细或运费汇总。</p>
</div>
<div class="flex items-center gap-2 text-xs">
<span class="rounded-full border border-[#d9d0c2] bg-white px-3 py-1.5 text-[#6f675c]">{{ $statusLabels[$sync['code'] ?? 'unavailable'] ?? '镜像状态未知' }}</span>
@if (!empty($sync['last_succeeded_at'])) <span class="text-gray-500">最近同步 {{ $sync['last_succeeded_at'] }}</span> @endif
<div class="mb-5 grid grid-cols-2 gap-3">
<div class="rounded-lg border border-[#D8D2C7] bg-white px-4 py-3">
<div class="text-xs text-[#595959]">记录数</div>
<div class="mt-1 text-lg font-semibold tabular-nums text-[#171717]" data-summary-count>0</div>
</div>
<div class="rounded-lg border border-[#D8D2C7] bg-white px-4 py-3">
<div class="text-xs text-[#595959]">运费合计</div>
<div class="mt-1 text-lg font-semibold tabular-nums text-[#171717]" data-summary-freight>¥ 0.00</div>
</div>
</div>
@if (!empty($freightReconciliationError))
<div class="mb-4 rounded border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">{{ $freightReconciliationError }}</div>
@endif
<div class="mb-5" data-courier-summary data-summary-ready="{{ $summaryReady ? 'true' : 'false' }}" data-summary-url="{{ $freightReconciliationSummaryUrl ?? '' }}">
<div class="grid grid-cols-2 gap-3">
<div class="rounded-lg border border-[#e7e0d5] bg-white px-4 py-3 shadow-sm"><div class="text-xs text-gray-500">记录数</div><div class="mt-1 text-lg font-semibold text-[#4b3a24]" data-summary-count>{{ $summaryReady ? number_format((int) $summary['count']) : '—' }}</div></div>
<div class="rounded-lg border border-[#e7e0d5] bg-white px-4 py-3 shadow-sm"><div class="text-xs text-gray-500">运费合计</div><div class="mt-1 text-lg font-semibold text-[#4b3a24]" data-summary-freight>{{ $summaryReady ? '¥ ' . $money($summary['freight']) : '—' }}</div></div>
</div>
<p class="mt-2 text-xs text-gray-500" data-summary-status role="status" aria-live="polite">@if ($summaryReady) 完整筛选汇总{{ !empty($summary['computed_at']) ? ' · ' . $summary['computed_at'] : '' }} @elseif (!empty($freightReconciliationSummaryUrl)) 正在计算完整筛选汇总,明细可继续查看… @else 同步完成后提供汇总 @endif</p>
</div>
<div class="mb-5 rounded-lg border border-[#e7e0d5] bg-white p-4 shadow-sm">
<form method="GET" class="flex flex-wrap items-end gap-3" aria-label="运费对账筛选">
@foreach ([
['name' => 'express_no', 'label' => '快递单号', 'placeholder' => '包含匹配'],
['name' => 'express_name', 'label' => '快递名称', 'placeholder' => '包含匹配'],
['name' => 'month', 'label' => '月份', 'placeholder' => '如 2026-08'],
['name' => 'status', 'label' => '物流状态', 'placeholder' => '精确匹配'],
] as $field)
<label class="flex min-w-[150px] flex-col gap-1 text-xs text-gray-500"><span>{{ $field['label'] }}</span><input class="recon-control" type="text" name="{{ $field['name'] }}" value="{{ $filters[$field['name']] ?? '' }}" placeholder="{{ $field['placeholder'] }}"></label>
@endforeach
<label class="flex min-w-[145px] flex-col gap-1 text-xs text-gray-500"><span>修改时间起</span><input class="recon-control" type="date" name="modified_from" value="{{ $filters['modified_from'] ?? '' }}"></label>
<label class="flex min-w-[145px] flex-col gap-1 text-xs text-gray-500"><span>修改时间止</span><input class="recon-control" type="date" name="modified_to" value="{{ $filters['modified_to'] ?? '' }}"></label>
<div class="flex gap-2"><button class="recon-button recon-button--primary" type="submit">查询</button><a class="recon-button recon-button--secondary" href="{{ url()->current() }}">重置</a></div>
</form>
</div>
<div data-finance-table-surface class="relative rounded-lg border border-[#e7e0d5] bg-white shadow-sm">
<div class="overflow-x-auto"><table class="recon-table min-w-full text-sm"><thead><tr>
<div data-finance-table-surface class="relative rounded-lg border border-[#D8D2C7] bg-white">
<div class="overflow-x-auto">
<table class="recon-table min-w-full text-xs text-[#171717]">
<thead class="bg-[#F7F0DE] text-[#595959]"><tr>
@foreach (['快递单号','商品ID','发货时间','交易订单号','省','市','区','条形码','快递名称1','快递名称2','数量','商品名称','平台','店铺名称','仓库','体积(cm3)','供应商','长(cm)','宽(cm)','高(cm)','运费','对账重量(kg)','对账体积(cm3)'] as $heading)
<th class="text-left">{{ $heading }}</th>
<th class="whitespace-nowrap border-b border-[#D8D2C7] px-3 py-2 text-left font-semibold">{{ $heading }}</th>
@endforeach
</tr></thead><tbody>
@forelse ($freightReconciliations ?? [] as $row)
<tr>
<td class="font-mono text-xs text-[#4b3a24]">{{ $row['waybill'] ?? '-' }}</td>
<td>{{ $row['product_id'] ?? '-' }}</td>
<td>{{ $row['send_time'] ?? '-' }}</td>
<td>{{ $row['transaction_order_no'] ?? '-' }}</td>
<td>{{ $row['province'] ?? '-' }}</td><td>{{ $row['city'] ?? '-' }}</td><td>{{ $row['district'] ?? '-' }}</td><td>{{ $row['barcode'] ?? '-' }}</td>
<td>{{ $row['express_name'] ?? '-' }}</td><td>{{ $row['express_name_2'] ?? '-' }}</td><td>{{ $row['quantity'] ?? '-' }}</td>
<td>{{ $row['product_name'] ?? '-' }}</td><td>{{ $row['platform'] ?? '-' }}</td><td>{{ $row['shop_name'] ?? '-' }}</td><td>{{ $row['warehouse'] ?? '-' }}</td>
<td class="text-right tabular-nums">{{ $number($row['volume'] ?? null) }}</td><td>{{ $row['supplier'] ?? '-' }}</td>
@foreach (['length','width','height','freight','reconciliation_weight','reconciliation_volume'] as $numberField)
<td class="text-right tabular-nums">{{ $row[$numberField] === null ? '-' : ($numberField === 'freight' ? $money($row[$numberField]) : $number($row[$numberField])) }}</td>
@endforeach
</tr>
@empty
<tr><td colspan="23" class="px-4 py-16 text-center text-sm text-gray-500">@if ($freightReconciliationPending ?? false) 正在等待物流明细镜像首次同步完成… @else 暂无符合条件的运费对账数据 @endif</td></tr>
@endforelse
</tbody></table></div>
@if ($freightReconciliations ?? null)
<div class="flex flex-wrap items-center justify-between border-t border-[#eee8de] px-4 py-3 text-sm text-gray-500"><span><span data-recon-page-totals data-per-page="{{ $freightReconciliations->perPage() }}">@if ($summaryReady) 共 {{ number_format((int) $summary['count']) }} 条 · 共 {{ (int) ceil($summary['count'] / $freightReconciliations->perPage()) }} 页 @else 总条数 / 总页数计算中 @endif</span> · 当前第 {{ $freightReconciliations->currentPage() }} 页 · 本页 {{ $freightReconciliations->count() }} 条</span><div>{{ $freightReconciliations->links() }}</div></div>
@endif
</tr></thead>
<tbody>
<tr><td colspan="23" class="px-4 py-16 text-center text-sm text-[#595959]">本页面数据已清空,等待接入正确的运费对账数据源。</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
(() => {
const root = document.querySelector('[data-courier-summary]');
const url = root?.dataset.summaryUrl;
if (!url || root.dataset.summaryReady === 'true') return;
const locationKey = window.location.href;
let attempts = 0;
const load = async () => {
if (window.location.href !== locationKey) return;
try {
const response = await fetch(url, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' } });
if (window.location.href !== locationKey) return;
if (response.status === 202 && ++attempts < 100) {
window.setTimeout(load, 3000);
return;
}
if (!response.ok || response.status === 202) throw new Error('summary_unavailable');
const { data } = await response.json();
const current = document.querySelector('[data-courier-summary]');
if (!current || !data) return;
current.querySelector('[data-summary-count]').textContent = new Intl.NumberFormat('zh-CN').format(data.count);
current.querySelector('[data-summary-freight]').textContent = '¥ ' + new Intl.NumberFormat('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(data.freight);
current.querySelector('[data-summary-status]').textContent = '完整筛选汇总 · ' + data.computed_at;
current.dataset.summaryReady = 'true';
const totals = document.querySelector('[data-recon-page-totals]');
if (totals) totals.textContent = '共 ' + new Intl.NumberFormat('zh-CN').format(data.count) + ' 条 · 共 ' + Math.ceil(data.count / Number(totals.dataset.perPage)) + ' 页';
} catch (_) {
const status = document.querySelector('[data-courier-summary] [data-summary-status]');
if (window.location.href === locationKey && status) status.textContent = '完整汇总暂时无法计算,明细仍可正常查看。';
const totals = document.querySelector('[data-recon-page-totals]');
if (window.location.href === locationKey && totals) totals.textContent = '总条数 / 总页数暂不可用';
}
};
load();
})();
</script>

View File

@ -3,103 +3,65 @@
namespace Tests\Feature\Business;
use App\Models\Auth\Permission;
use Illuminate\Testing\TestResponse;
use Modules\EcommerceFinance\Repositories\CourierFreightReconciliationMirrorRepository;
use Tests\Feature\FeatureTestCase;
class CourierFreightReconciliationPageTest extends FeatureTestCase
{
protected function renderFreightPage(bool $summaryReady = true): TestResponse
protected function setUp(): void
{
parent::setUp();
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$repository = \Mockery::mock(CourierFreightReconciliationMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('ready')->once()->andReturnTrue();
$repository->shouldReceive('paginate')->once()->andReturn([
'data' => [[
'waybill' => '00123456789012345678', 'product_id' => 'ITEM-1',
'send_time' => '2026-09-01 12:00:00', 'transaction_order_no' => 'SO-1',
'province' => '江苏省', 'city' => '常州市', 'district' => '武进区',
'barcode' => 'SKU-1', 'express_name' => '测试快递', 'express_name_2' => '测试网点',
'quantity' => 2, 'product_name' => '验收商品', 'platform' => '测试平台',
'shop_name' => '验收店铺', 'warehouse' => '验收仓库', 'volume' => 7161,
'supplier' => '验收供应商', 'length' => 11, 'width' => 21, 'height' => 31,
'freight' => 8.8, 'reconciliation_weight' => 0.7, 'reconciliation_volume' => 7161,
]],
'total' => $summaryReady ? 1 : null, 'has_more' => false,
'summary' => $summaryReady ? ['count' => 1, 'freight' => 8.8] : null,
'sync' => ['code' => 'fresh', 'last_succeeded_at' => '2026-09-09 10:00:00', 'lag_minutes' => 0],
]);
foreach (['available', 'ready', 'state', 'syncStatus', 'paginate', 'requestSummary'] as $method) {
$repository->shouldNotReceive($method);
}
$this->app->instance(CourierFreightReconciliationMirrorRepository::class, $repository);
return $this->loginAs()->get(route('business.pages.show', [
'section' => 'courier-payables', 'page' => 'freight-reconciliation',
]));
}
public function test_rendered_values_match_their_column_headings(): void
public function test_page_stays_empty_with_old_filters_and_page_number(): void
{
$response = $this->renderFreightPage()->assertOk();
$response = $this->loginAs()->get(route('business.pages.show', [
'section' => 'courier-payables', 'page' => 'freight-reconciliation',
'express_no' => 'OLD-WAYBILL', 'month' => '2026-09',
]) . '&page=2');
$response->assertOk()
->assertSeeText('原数据源已停用')
->assertSeeText('本页面数据已清空,等待接入正确的运费对账数据源。')
->assertSee('data-summary-count>0', false)
->assertSee('data-summary-freight>¥ 0.00', false)
->assertDontSeeText('OLD-WAYBILL')
->assertDontSeeText('最近同步')
->assertDontSeeText('正在计算完整筛选汇总')
->assertDontSee('data-summary-url', false)
->assertDontSee('jst_order_express_details', false);
$document = new \DOMDocument();
$previous = libxml_use_internal_errors(true);
$document->loadHTML('<?xml encoding="UTF-8">' . $response->getContent());
libxml_clear_errors();
libxml_use_internal_errors($previous);
$xpath = new \DOMXPath($document);
$headers = $xpath->query('//table[contains(@class,"recon-table")]/thead/tr/th');
$values = $xpath->query('//table[contains(@class,"recon-table")]/tbody/tr[1]/td');
$this->assertCount(23, $headers);
$this->assertCount(23, $values);
$row = [];
foreach ($headers as $index => $header) $row[trim($header->textContent)] = trim($values->item($index)->textContent);
$this->assertSame('7161', $row['体积(cm3)']);
$this->assertSame('验收供应商', $row['供应商']);
$this->assertSame('11', $row['长(cm)']);
$this->assertSame('21', $row['宽(cm)']);
$this->assertSame('31', $row['高(cm)']);
$this->assertSame('8.80', $row['运费']);
$this->assertSame('00123456789012345678', $row['快递单号']);
$this->assertCount(23, $xpath->query('//table[contains(@class,"recon-table")]/thead/tr/th'));
$this->assertCount(1, $xpath->query('//table[contains(@class,"recon-table")]/tbody/tr/td[@colspan="23"]'));
}
public function test_rows_remain_available_while_the_complete_summary_is_pending(): void
public function test_old_summary_endpoint_returns_zero_without_querying_the_disabled_source(): void
{
$this->renderFreightPage(false)->assertOk()
->assertSeeText('验收商品')
->assertSeeText('正在计算完整筛选汇总')
->assertSee('data-summary-ready="false"', false)
->assertSee('data-summary-count>—', false);
}
public function test_summary_request_keeps_filters_and_returns_only_the_summary(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$repository = \Mockery::mock(CourierFreightReconciliationMirrorRepository::class);
$repository->shouldReceive('requestSummary')->once()
->withArgs(fn (array $filters) => $filters['month'] === '2026-09' && $filters['express_no'] === 'YT')
->andReturn(['count' => 48, 'freight' => 8.8, 'computed_at' => '2026-09-09 10:00:00']);
$this->app->instance(CourierFreightReconciliationMirrorRepository::class, $repository);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'courier-payables', 'page' => 'freight-reconciliation',
'courier_summary' => 1, 'month' => '2026-09', 'express_no' => ' YT ',
]))->assertOk()->assertJsonPath('data.count', 48)->assertJsonPath('data.freight', 8.8);
}
public function test_busy_summary_returns_pending_without_starting_another_calculation(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$repository = \Mockery::mock(CourierFreightReconciliationMirrorRepository::class);
$repository->shouldReceive('requestSummary')->once()->andReturnNull();
$this->app->instance(CourierFreightReconciliationMirrorRepository::class, $repository);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'courier-payables', 'page' => 'freight-reconciliation', 'courier_summary' => 1,
]))->assertStatus(202)->assertJsonPath('data', null)->assertHeader('Content-Length', '13')->assertHeader('X-Accel-Buffering', 'no');
'courier_summary' => 1, 'month' => '2026-09', 'express_no' => 'OLD-WAYBILL',
]))->assertOk()->assertExactJson([
'data' => ['count' => 0, 'freight' => 0],
'status' => 'disabled',
'message' => '原数据源已停用,本页面不再展示原运费对账数据。',
])->assertHeader('Cache-Control', 'no-store, private');
}
public function test_summary_is_not_available_without_login(): void
{
$repository = \Mockery::mock(CourierFreightReconciliationMirrorRepository::class);
$repository->shouldNotReceive('requestSummary');
$this->app->instance(CourierFreightReconciliationMirrorRepository::class, $repository);
$this->withExceptionHandling()->get(route('business.pages.show', [
'company_id' => $this->company->id,
'section' => 'courier-payables', 'page' => 'freight-reconciliation', 'courier_summary' => 1,

View File

@ -396,56 +396,30 @@ class PagesTest extends FeatureTestCase
->assertDontSeeText('已从共享业务镜像读取');
}
public function testFreightReconciliationPageRendersSharedMirrorRowsAndSummary(): void
public function testFreightReconciliationPageIsEmptyAfterDisablingTheOldSource(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$repository = Mockery::mock(CourierFreightReconciliationMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('ready')->once()->andReturnTrue();
$repository->shouldReceive('paginate')->once()->with(
Mockery::on(static fn (array $filters): bool => $filters['express_no'] === 'FIXTURE'),
1,
Mockery::type('int'),
)->andReturn([
'data' => [[
'detail_id' => 1,
'waybill' => 'FIXTURE-WAYBILL', 'product_id' => 'ITEM-1',
'send_time' => '2026-08-01 09:00:00', 'transaction_order_no' => 'SO-1',
'province' => '江苏省', 'city' => '常州市', 'district' => '武进区', 'barcode' => 'SKU-1',
'express_name' => '测试快递', 'express_name_2' => '测试快递', 'quantity' => 1,
'product_name' => '测试商品', 'platform' => '淘宝天猫', 'shop_name' => '测试店铺',
'warehouse' => '测试仓库', 'volume' => 1.2, 'supplier' => '测试供应商',
'length' => 1.0, 'width' => 2.0, 'height' => 3.0, 'freight' => 20.0,
'weight' => 1.25, 'reconciliation_weight' => 1.25, 'reconciliation_volume' => 1.2,
'month' => '2026-08', 'status' => '已发货',
]],
'total' => 1,
'summary' => ['count' => 1, 'freight' => 20.0, 'payable_amount' => 20.0, 'actual_payment' => 0.0, 'confirmed_amount' => 0.0, 'reconciliation_difference' => 0.0],
'state' => ['status' => 'succeeded'],
'sync' => ['code' => 'fresh', 'last_succeeded_at' => '2026-08-02 09:00:00', 'lag_minutes' => 1],
]);
$repository->shouldNotReceive('available', 'ready', 'paginate', 'requestSummary');
$this->app->instance(CourierFreightReconciliationMirrorRepository::class, $repository);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'courier-payables',
'page' => 'freight-reconciliation',
'express_no' => 'FIXTURE',
]))
->assertOk()
->assertSeeText('FIXTURE-WAYBILL')
->assertSeeText('原数据源已停用')
->assertSeeText('条形码')
->assertSeeText('交易订单号')
->assertSeeText('对账体积(cm3)')
->assertSeeText('SKU-1')
->assertSeeText('测试供应商')
->assertSeeText('运费合计')
->assertSeeText('20.00')
->assertSeeText('数据新鲜')
->assertSeeText('共享业务镜像')
->assertSee('data-summary-count>0', false)
->assertDontSeeText('共享业务镜像')
->assertDontSee('data-summary-url', false)
->assertDontSee('class="business-blank-page"', false);
}