修复共享快照处理中状态反馈 #46

Merged
WuYongJie merged 2 commits from fix/WuYongJie-marketing-balance-sync-status into main 2026-08-13 15:41:56 +08:00
6 changed files with 95 additions and 5 deletions

View File

@ -13,6 +13,7 @@ use Illuminate\Routing\Controller;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;
use Modules\EcommerceFinance\Exceptions\UpstreamSnapshotPendingException;
use Modules\EcommerceFinance\Repositories\AlibabaBillRepository;
use Modules\EcommerceFinance\Repositories\EdgeBandingRepository;
use Modules\EcommerceFinance\Repositories\InventoryRepository;
@ -248,6 +249,7 @@ class Pages extends Controller
'platform' => $tab,
'balances' => collect(),
'error' => null,
'pending' => false,
];
if (! in_array($tab, ['taobao-alipay', 'pinduoduo'], true)) {
@ -282,6 +284,9 @@ class Pages extends Controller
$pageSize,
),
);
} catch (UpstreamSnapshotPendingException $exception) {
$content['pending'] = true;
$result = ['rows' => [], 'total' => 0];
} catch (ServiceUnavailableHttpException $exception) {
$content['error'] = $exception->getMessage();
$result = ['rows' => [], 'total' => 0];

View File

@ -0,0 +1,16 @@
<?php
namespace Modules\EcommerceFinance\Exceptions;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
class UpstreamSnapshotPendingException extends ServiceUnavailableHttpException
{
public function __construct(int $retryAfter = 15)
{
parent::__construct(
$retryAfter,
'数据同步请求已登记,后台正在准备首份共享数据。'
);
}
}

View File

@ -8,6 +8,7 @@ use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Cache\Lock;
use Illuminate\Contracts\Cache\LockTimeoutException;
use Illuminate\Support\Facades\Log;
use Modules\EcommerceFinance\Exceptions\UpstreamSnapshotPendingException;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
use Throwable;
@ -102,10 +103,7 @@ class UpstreamQueryCache
$this->register($scope, $parameters, true);
if (! (bool) config('upstreams.snapshots.allow_inline_refresh', false)) {
throw new ServiceUnavailableHttpException(
15,
'数据同步请求已登记,后台正在准备首份共享数据,请稍后刷新。'
);
throw new UpstreamSnapshotPendingException();
}
$resolved = $this->resolve($scope, $parameters, $resolver, false);

View File

@ -24,6 +24,8 @@
.marketing-balance-pagination .page-item.disabled .page-link { color: #595959; cursor: not-allowed; }
.marketing-balance-pagination div.lg\:flex > div:first-child { display: none !important; }
.marketing-balance-pagination div.lg\:flex { justify-content: center; }
.marketing-balance-status { display: flex; align-items: center; gap: 10px; margin: 0 0 16px; border: 1px solid var(--vip-border, #D8D2C7); border-radius: 12px; background: var(--vip-content-accent-soft, #F7F0DE); padding: 12px 14px; color: #513E21; font-size: 0.875rem; line-height: 1.5; }
.marketing-balance-status .material-icons-outlined { flex: 0 0 auto; color: var(--vip-content-accent-dark, #856632); font-size: 14px; }
</style>
<div class="biz-card-head">
@ -71,6 +73,11 @@
@if (! empty($error))
<div class="biz-alert biz-alert-error" role="alert">{{ $error }}</div>
@elseif (! empty($pending))
<div class="marketing-balance-status" role="status" aria-live="polite" data-marketing-balance-pending>
<span class="material-icons-outlined" aria-hidden="true">sync</span>
<span>同步请求已进入后台队列,页面会自动检查首份共享数据,无需重复操作。</span>
</div>
@endif
@if ($supportsBalances)
@ -104,3 +111,32 @@
<div>该平台余额数据表尚未配置。</div>
</div>
@endif
@if (! empty($pending))
<script>
(function () {
var key = 'caiwu-marketing-balance-pending:' + window.location.pathname + window.location.search;
var attempts = Number(window.sessionStorage.getItem(key) || 0);
var maximumAttempts = 10;
if (attempts >= maximumAttempts) {
var status = document.querySelector('[data-marketing-balance-pending] span:last-child');
if (status) {
status.textContent = '后台仍在处理首份共享数据,请稍后手动刷新;无需重复提交查询。';
}
return;
}
window.sessionStorage.setItem(key, String(attempts + 1));
window.setTimeout(function () {
window.location.reload();
}, 30000);
}());
</script>
@elseif ($supportsBalances)
<script>
window.sessionStorage.removeItem('caiwu-marketing-balance-pending:' + window.location.pathname + window.location.search);
</script>
@endif

View File

@ -4,6 +4,7 @@ namespace Tests\Feature\Business;
use App\Models\Auth\Permission;
use Mockery;
use Modules\EcommerceFinance\Exceptions\UpstreamSnapshotPendingException;
use Modules\EcommerceFinance\Repositories\JdWalletBillRepository;
use Modules\EcommerceFinance\Repositories\PinduoduoSpreadBillRepository;
use Modules\EcommerceFinance\Repositories\TmallTaobaoSpreadBillRepository;
@ -196,6 +197,38 @@ class MarketingAccountBillsTabsTest extends FeatureTestCase
->assertSessionHas('error', '首份共享快照正在准备,系统已登记刷新请求;请稍后刷新页面后再导出。');
}
public function testMarketingAccountBalanceColdStartShowsProcessingStateAndAutomaticCheck(): void
{
$this->allowMarketingAccountBalancePage();
$cache = Mockery::mock(UpstreamQueryCache::class);
$cache->shouldReceive('remember')
->once()
->withArgs(fn (string $scope): bool => $scope === 'taobao-alipay-bills.list')
->andThrow(new UpstreamSnapshotPendingException());
$cache->shouldReceive('remember')
->once()
->withArgs(fn (string $scope): bool => $scope === 'taobao-alipay-bills.options')
->andReturn([]);
$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.show', [
'section' => 'billing-transactions',
'page' => 'marketing-account-balance',
'tab' => 'taobao-alipay',
]))
->assertOk()
->assertSeeText('同步请求已进入后台队列,页面会自动检查首份共享数据,无需重复操作。')
->assertSee('data-marketing-balance-pending', false)
->assertSee('window.location.reload()', false)
->assertDontSee('<div class="biz-alert biz-alert-error" role="alert">', false);
}
public function testMarketingAccountBalanceExportUnexpectedFailureReturnsRedactedRedirect(): void
{
$this->allowMarketingAccountBalancePage();

View File

@ -5,6 +5,7 @@ namespace Tests\Unit\EcommerceFinance;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use InvalidArgumentException;
use Modules\EcommerceFinance\Exceptions\UpstreamSnapshotPendingException;
use Modules\EcommerceFinance\Services\UpstreamOdbcConnection;
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
@ -103,9 +104,10 @@ class UpstreamQueryCacheTest extends TestCase
return ['total' => 1];
});
$this->fail('页面冷查询不应直接访问上游数据库。');
} catch (ServiceUnavailableHttpException $exception) {
} catch (UpstreamSnapshotPendingException $exception) {
$this->assertSame(0, $calls);
$this->assertStringContainsString('后台正在准备', $exception->getMessage());
$this->assertSame(15, $exception->getHeaders()['Retry-After']);
$this->assertNotEmpty(Cache::get('source_a:snapshots:active:v2'));
}
}