feat(operations): add automated reconciliation center

This commit is contained in:
lingniu
2026-07-16 19:14:53 +08:00
parent bbab018d55
commit d67f42b4f4
25 changed files with 1697 additions and 39 deletions

View File

@@ -5,7 +5,8 @@ import OperationsPage from './OperationsPage';
const mocks = vi.hoisted(() => ({
opsHealth: vi.fn(), sourceReadiness: vi.fn(), session: vi.fn(), vehicleCoverage: vi.fn(),
vehicleSourceDiagnostic: vi.fn(), updateVehicleSourcePolicy: vi.fn()
vehicleSourceDiagnostic: vi.fn(), updateVehicleSourcePolicy: vi.fn(),
reconciliationSummary: vi.fn(), reconciliationIssues: vi.fn(), reconciliationIssue: vi.fn(), updateReconciliationIssue: vi.fn()
}));
vi.mock('../../api/client', () => ({ api: mocks }));
@@ -13,8 +14,60 @@ afterEach(() => { cleanup(); Object.values(mocks).forEach((mock) => mock.mockRes
function seedSession() {
mocks.session.mockResolvedValue({ name: '平台管理员', role: 'admin', userType: 'admin', authMode: 'enforce', menuKeys: ['operations'] });
mocks.reconciliationSummary.mockResolvedValue({
active: 1, pending: 1, confirmed: 0, recovered: 2, overSla: 1,
byRule: [{ name: 'POSITION_DRIFT', count: 1 }], bySeverity: [{ name: 'major', count: 1 }],
trend: [{ date: '2026-07-16', detected: 1, new: 1, active: 1, recovered: 0 }],
lastRunAt: '2026-07-16 02:15:00', asOf: '2026-07-16 10:00:00'
});
mocks.reconciliationIssues.mockResolvedValue({
items: [{
id: 'issue-1', ruleCode: 'POSITION_DRIFT', category: 'location', severity: 'major', status: 'pending',
vin: 'VIN001', plate: '粤A00001', protocolA: 'GB32960', protocolB: 'JT808', title: '多来源实时位置漂移',
summary: '两个来源相差 1286 米', evidence: { distanceM: 1286 }, firstSeenAt: '2026-07-16 08:00:00',
lastSeenAt: '2026-07-16 10:00:00', occurrenceCount: 3, recoveredAt: '', resolutionNote: '', resolvedBy: '', version: 1
}],
total: 1, limit: 50, offset: 0
});
mocks.reconciliationIssue.mockResolvedValue({
id: 'issue-1', ruleCode: 'POSITION_DRIFT', category: 'location', severity: 'major', status: 'pending',
vin: 'VIN001', plate: '粤A00001', protocolA: 'GB32960', protocolB: 'JT808', title: '多来源实时位置漂移',
summary: '两个来源相差 1286 米', evidence: { distanceM: 1286 }, firstSeenAt: '2026-07-16 08:00:00',
lastSeenAt: '2026-07-16 10:00:00', occurrenceCount: 3, recoveredAt: '', resolutionNote: '', resolvedBy: '', version: 1,
actions: [{ id: 1, action: 'detect', fromStatus: '', toStatus: 'pending', actor: 'reconciliation-evaluator', note: '规则首次发现差异', createdAt: '2026-07-16 08:00:00' }]
});
}
test('renders reconciliation queue, loads evidence on demand and records review conclusion', async () => {
seedSession();
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.mockResolvedValue({ totalVehicles: 1, boundVehicles: 1, identityRequiredVehicles: 0, onlineVehicles: 1, sources: [] });
mocks.updateReconciliationIssue.mockResolvedValue({
...(await mocks.reconciliationIssue()),
status: 'confirmed_source_a',
resolutionNote: '来源 A 原始报文可信',
resolvedBy: '平台管理员',
version: 2
});
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
expect(await screen.findByText('数据差异中心')).toBeInTheDocument();
fireEvent.click(await screen.findByText('多来源实时位置漂移'));
expect(await screen.findByText('规则证据')).toBeInTheDocument();
expect(screen.getByText('1286')).toBeInTheDocument();
fireEvent.change(screen.getByLabelText('处置状态'), { target: { value: 'confirmed_source_a' } });
fireEvent.change(screen.getByLabelText('说明(必填)'), { target: { value: '来源 A 原始报文可信' } });
fireEvent.click(screen.getByRole('button', { name: '保存复核结论' }));
await waitFor(() => expect(mocks.updateReconciliationIssue).toHaveBeenCalledWith('issue-1', {
version: 1, status: 'confirmed_source_a', note: '来源 A 原始报文可信'
}));
});
test('reconciles service identities with bound and identity-required vehicles', async () => {
seedSession();
mocks.opsHealth.mockResolvedValue({
@@ -92,8 +145,12 @@ test('fuzzy searches a vehicle and renders all source diagnosis evidence', async
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
fireEvent.change(screen.getByLabelText('按车牌或 VIN 搜索诊断车辆'), { target: { value: '粤A' } });
expect(await screen.findByText('粤A00001')).toBeInTheDocument();
fireEvent.click(screen.getByText('粤A00001'));
const candidateButton = await waitFor(() => {
const button = document.querySelector<HTMLButtonElement>('.v2-source-candidates button');
expect(button).toBeTruthy();
return button!;
});
fireEvent.click(candidateButton);
expect(await screen.findByText('当前推荐 G7')).toBeInTheDocument();
expect(screen.getByText('终端 133****0001')).toBeInTheDocument();
expect(screen.getByText('10s')).toBeInTheDocument();