diff --git a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx
index ec8ac8c4..d5711957 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx
@@ -1,5 +1,5 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
+import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';
import { MemoryRouter } from 'react-router-dom';
import type { AccessVehicleRow, Page } from '../../api/types';
@@ -55,3 +55,24 @@ test('removes old access rows immediately when the vehicle filter scope changes'
expect(await screen.findByText('新接入车牌')).toBeInTheDocument();
await waitFor(() => expect(screen.queryByText('正在更新车辆接入状态…')).not.toBeInTheDocument());
});
+
+test('reports and independently retries unresolved identity and threshold failures', async () => {
+ prepareBaseData();
+ mocks.accessVehicles.mockResolvedValue({ items: [accessRow('VIN001', '粤A00001')], total: 1, limit: 50, offset: 0 });
+ mocks.accessUnresolvedIdentities.mockRejectedValueOnce(new Error('待绑定身份服务超时')).mockResolvedValueOnce({ items: [{ id: 'identity-1', identifierMasked: '138****0001', protocol: 'JT808', plate: '粤A待核', latestSeenAt: '2026-07-16T04:00:00Z', recommendedAction: '核对 VIN' }], total: 1, limit: 20, offset: 0 });
+ mocks.accessThresholds.mockRejectedValueOnce(new Error('阈值配置读取失败')).mockResolvedValueOnce({ version: 1, defaultThresholdSec: 300, delayThresholdSec: 60, longOfflineSec: 86_400, protocols: [], updatedBy: 'test', updatedAt: '2026-07-16T04:00:00Z', audit: [] });
+ const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ render();
+
+ const identityError = await screen.findByText('待绑定身份服务超时');
+ const thresholdError = await screen.findByText('阈值配置读取失败');
+ fireEvent.click(within(identityError.closest('[role="alert"]')!).getByRole('button', { name: /重试/ }));
+ fireEvent.click(within(thresholdError.closest('[role="alert"]')!).getByRole('button', { name: /重试/ }));
+
+ expect(await screen.findByText('另有 1 条来源身份待绑定')).toBeInTheDocument();
+ expect(await screen.findByText('在线判定阈值 · v1')).toBeInTheDocument();
+ expect(screen.queryByText('待绑定身份服务超时')).not.toBeInTheDocument();
+ expect(screen.queryByText('阈值配置读取失败')).not.toBeInTheDocument();
+ expect(mocks.accessUnresolvedIdentities).toHaveBeenCalledTimes(2);
+ expect(mocks.accessThresholds).toHaveBeenCalledTimes(2);
+});
diff --git a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx
index 1fa5a371..769189eb 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx
@@ -112,9 +112,10 @@ export default function AccessPage() {
const apply = (next: Filters) => { setDraft(next); setCriteria(next); setOffset(0); setSelectedVIN(''); syncURL(next); };
const submit = (event: FormEvent) => { event.preventDefault(); apply(draft); };
const page = Math.floor(offset / limit) + 1; const totalPages = Math.max(1, Math.ceil((vehiclesQuery.data?.total ?? 0) / limit)); const summary = summaryQuery.data;
+ const refresh = () => Promise.all([summaryQuery.refetch(), vehiclesQuery.refetch(), unresolvedQuery.refetch(), thresholdQuery.refetch()]);
return
-
车辆接入管理
以主车辆为对象,对照应接协议、实际接入和各协议最新上报时间
数据时间 {summary?.asOf ? formatAccessTime(summary.asOf) : '—'}
+
{summaryQuery.isError ?
summaryQuery.refetch()} /> : null}
{[
@@ -122,7 +123,9 @@ export default function AccessPage() {
].map(([label, value, tone, connectionState]) => )}
{vehiclesQuery.isError ? vehiclesQuery.refetch()} /> : null}
| 车辆 | 品牌 / 车型 | 应接协议 | {PROTOCOLS.map((item) => {item} | )}综合状态 |
{rows.map((row) => setSelectedVIN(row.vin)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') setSelectedVIN(row.vin); }}>| {row.plate || '未绑定车牌'}{row.vin} | {row.oem || '品牌未维护'}{row.model || row.company || '车型未维护'} | {row.expectedProtocols.length} 项{row.expectedProtocols.join(' / ')} | {PROTOCOLS.map((protocol) => | )} |
)}
{vehiclesQuery.isFetching ?
正在更新车辆接入状态…
: null}{!vehiclesQuery.isFetching && !rows.length ?
当前筛选条件没有车辆
: null}
{selected ?
setSelectedVIN('')} /> : null}
+ {unresolvedQuery.isError ? unresolvedQuery.refetch()} /> : null}
+ {thresholdQuery.isError ? thresholdQuery.refetch()} /> : null}
thresholdDraft && updateThreshold.mutate(thresholdDraft)} />
;
}
diff --git a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx
index 1a70e32e..fd2f37c9 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx
@@ -1,5 +1,5 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { cleanup, render, screen } from '@testing-library/react';
+import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';
import OperationsPage from './OperationsPage';
@@ -26,3 +26,25 @@ test('reconciles service identities with bound and identity-required vehicles',
expect(screen.getByText('已绑定 1024 · 待绑定 11')).toBeInTheDocument();
expect(screen.queryByText('统一车辆视角')).not.toBeInTheDocument();
});
+
+test('keeps health evidence visible when source readiness fails and retries that section', async () => {
+ mocks.opsHealth.mockResolvedValue({
+ linkHealth: [], kafkaLag: 0, activeConnections: 10, capacityFindings: [], redisOnlineKeys: 5,
+ tdengineWritable: true, mysqlWritable: true,
+ runtime: { platformRelease: 'test-release', dataMode: 'production', requestTimeoutMs: 5000, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false }
+ });
+ mocks.sourceReadiness.mockRejectedValueOnce(new Error('来源就绪度暂时不可用')).mockResolvedValueOnce({
+ totalVehicles: 1035, boundVehicles: 1024, identityRequiredVehicles: 11, onlineVehicles: 234,
+ kafkaLag: 0, activeConnections: 10, redisOnlineKeys: 5, platformRelease: 'test-release', sources: []
+ });
+ const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ render();
+
+ expect(await screen.findByText('来源就绪度暂时不可用')).toBeInTheDocument();
+ expect(screen.getByText('test-release')).toBeInTheDocument();
+ fireEvent.click(screen.getByRole('button', { name: /重试/ }));
+
+ expect(await screen.findByText('1035 / 234')).toBeInTheDocument();
+ expect(screen.queryByText('来源就绪度暂时不可用')).not.toBeInTheDocument();
+ expect(mocks.sourceReadiness).toHaveBeenCalledTimes(2);
+});
diff --git a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx
index 50637180..df42d5ff 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx
@@ -15,6 +15,7 @@ export default function OperationsPage() {
return
{health.isError ?
: null}
+ {readiness.isError ?
readiness.refetch()} /> : null}
运行版本{data?.runtime.platformRelease || '未注入'}{data?.runtime.dataMode || 'unknown'}
活跃连接{data?.activeConnections?.toLocaleString('zh-CN') ?? '—'}capacity-check