diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php
index ffa8e6bb..9172eda1 100644
--- a/app/Http/Controllers/Business/Pages.php
+++ b/app/Http/Controllers/Business/Pages.php
@@ -338,11 +338,29 @@ class Pages extends Controller
? app(TmallTaobaoSpreadBillRepository::class)
: app(PinduoduoSpreadBillRepository::class);
$scope = $isTaobao ? 'taobao-alipay-bills.list' : 'pinduoduo-bills.list';
- $result = $this->upstreamCaches->forSource('jiansun')->remember(
- $scope,
- ['view' => 'monthly-balances', 'filters' => $filters, 'page' => 1, 'limit' => 5000, 'export' => true],
- fn () => $repository->paginateMonthlyBalances($filters['store_name'], $filters['month'], 1, 5000),
- );
+ $redirectParameters = array_filter([
+ 'section' => $section,
+ 'page' => $page,
+ 'tab' => $tab,
+ 'store_name' => $storeName,
+ 'month' => $month,
+ ], static fn (mixed $value): bool => $value !== '');
+
+ try {
+ $result = $this->upstreamCaches->forSource('jiansun')->remember(
+ $scope,
+ ['view' => 'monthly-balances', 'filters' => $filters, 'page' => 1, 'limit' => 5000, 'export' => true],
+ fn () => $repository->paginateMonthlyBalances($filters['store_name'], $filters['month'], 1, 5000),
+ );
+ } catch (ServiceUnavailableHttpException) {
+ return redirect()->route('business.pages.show', $redirectParameters)
+ ->with('error', '首份共享快照正在准备,系统已登记刷新请求;请稍后刷新页面后再导出。');
+ } catch (Throwable $exception) {
+ report($exception);
+
+ return redirect()->route('business.pages.show', $redirectParameters)
+ ->with('error', '余额数据导出暂时无法完成,请稍后重试。');
+ }
$rows = $result['rows'] ?? [];
return response()->streamDownload(function () use ($rows): void {
diff --git a/resources/views/business/pages/show.blade.php b/resources/views/business/pages/show.blade.php
index fb9f8657..36d2e7fb 100644
--- a/resources/views/business/pages/show.blade.php
+++ b/resources/views/business/pages/show.blade.php
@@ -411,6 +411,10 @@
{{ session('success') }}
@endif
+ @if (is_string(session('error')))
+ {{ session('error') }}
+ @endif
+
@if ($errors->has('import'))
{{ $errors->first('import') }}
@endif
diff --git a/tests/Feature/Business/MarketingAccountBillsTabsTest.php b/tests/Feature/Business/MarketingAccountBillsTabsTest.php
index 8e46a15e..e5b0c98e 100644
--- a/tests/Feature/Business/MarketingAccountBillsTabsTest.php
+++ b/tests/Feature/Business/MarketingAccountBillsTabsTest.php
@@ -6,6 +6,7 @@ use App\Models\Auth\Permission;
use Mockery;
use Modules\EcommerceFinance\Repositories\JdWalletBillRepository;
use Modules\EcommerceFinance\Repositories\PinduoduoSpreadBillRepository;
+use Modules\EcommerceFinance\Repositories\TmallTaobaoSpreadBillRepository;
use Modules\EcommerceFinance\Services\UpstreamQueryCache;
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
@@ -156,6 +157,71 @@ class MarketingAccountBillsTabsTest extends FeatureTestCase
], $scopes);
}
+ public function testMarketingAccountBalanceExportColdStartReturnsActionableRedirect(): void
+ {
+ $this->allowMarketingAccountBalancePage();
+ $cache = Mockery::mock(UpstreamQueryCache::class);
+ $cache->shouldReceive('remember')
+ ->once()
+ ->withArgs(function (string $scope, array $parameters, callable $resolver): bool {
+ return $scope === 'taobao-alipay-bills.list'
+ && ($parameters['export'] ?? false) === true;
+ })
+ ->andThrow(new ServiceUnavailableHttpException(30, '首份共享快照正在准备。'));
+
+ $factory = Mockery::mock(UpstreamQueryCacheFactory::class);
+ $factory->shouldReceive('forSource')
+ ->once()
+ ->with('jiansun')
+ ->andReturn($cache);
+
+ $this->app->instance(UpstreamQueryCacheFactory::class, $factory);
+ $this->app->instance(TmallTaobaoSpreadBillRepository::class, Mockery::mock(TmallTaobaoSpreadBillRepository::class));
+
+ $this->loginAs()
+ ->get(route('business.pages.export', [
+ 'section' => 'billing-transactions',
+ 'page' => 'marketing-account-balance',
+ 'tab' => 'taobao-alipay',
+ 'store_name' => '测试店铺',
+ 'month' => '2026-08',
+ ]))
+ ->assertRedirect(route('business.pages.show', [
+ 'section' => 'billing-transactions',
+ 'page' => 'marketing-account-balance',
+ 'tab' => 'taobao-alipay',
+ 'store_name' => '测试店铺',
+ 'month' => '2026-08',
+ ]))
+ ->assertSessionHas('error', '首份共享快照正在准备,系统已登记刷新请求;请稍后刷新页面后再导出。');
+ }
+
+ public function testMarketingAccountBalanceExportUnexpectedFailureReturnsRedactedRedirect(): void
+ {
+ $this->allowMarketingAccountBalancePage();
+ $cache = Mockery::mock(UpstreamQueryCache::class);
+ $cache->shouldReceive('remember')->once()->andThrow(new \RuntimeException('sensitive DSN details'));
+
+ $factory = Mockery::mock(UpstreamQueryCacheFactory::class);
+ $factory->shouldReceive('forSource')->once()->with('jiansun')->andReturn($cache);
+
+ $this->app->instance(UpstreamQueryCacheFactory::class, $factory);
+ $this->app->instance(TmallTaobaoSpreadBillRepository::class, Mockery::mock(TmallTaobaoSpreadBillRepository::class));
+
+ $this->loginAs()
+ ->get(route('business.pages.export', [
+ 'section' => 'billing-transactions',
+ 'page' => 'marketing-account-balance',
+ 'tab' => 'taobao-alipay',
+ ]))
+ ->assertRedirect(route('business.pages.show', [
+ 'section' => 'billing-transactions',
+ 'page' => 'marketing-account-balance',
+ 'tab' => 'taobao-alipay',
+ ]))
+ ->assertSessionHas('error', '余额数据导出暂时无法完成,请稍后重试。');
+ }
+
private function allowMarketingAccountBillsPage(): void
{
$this->user->update(['locale' => 'zh-CN']);