From 24897850f3be6c976232fed6adc3c43c04070a32 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 22:24:06 +0800 Subject: [PATCH] feat(platform): add alert business impact board --- .../apps/web/src/pages/Quality.tsx | 110 ++++++++++++++++++ .../apps/web/src/styles/global.css | 44 +++++++ .../apps/web/src/test/App.test.tsx | 18 +++ 3 files changed, 172 insertions(+) diff --git a/vehicle-data-platform/apps/web/src/pages/Quality.tsx b/vehicle-data-platform/apps/web/src/pages/Quality.tsx index 6a485d00..6f39fac7 100644 --- a/vehicle-data-platform/apps/web/src/pages/Quality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Quality.tsx @@ -604,6 +604,60 @@ function notificationHandoffText({ ].join('\n'); } +function alertBusinessImpactText({ + summary, + rules, + priorityRows, + health, + filters, + platformRelease +}: { + summary: QualitySummary; + rules: AlertRuleRow[]; + priorityRows: PriorityIssueRow[]; + health: OpsHealth | null; + filters: Record; + platformRelease?: string; +}) { + const release = platformRelease?.trim(); + const p0Rows = priorityRows.filter((row) => row.priority === 'P0'); + const p1Rows = priorityRows.filter((row) => row.priority === 'P1'); + const activeRules = rules.filter((row) => row.count > 0); + const p0Rules = activeRules.filter((row) => row.level === 'P0'); + const primaryProtocol = summary.protocols?.[0]; + const primaryIssue = summary.issueTypes?.[0]; + const storageOk = !health || (health.tdengineWritable && health.mysqlWritable); + const capacityCount = Number(health?.capacityFindings?.length ?? 0); + const severity = !storageOk || p0Rows.length > 0 || p0Rules.length > 0 || Number(summary.errorCount ?? 0) > 0 + ? '高风险' + : Number(summary.issueVehicleCount ?? 0) > 0 || capacityCount > 0 + ? '需关注' + : '正常'; + const filterLines = [ + filters.keyword ? `关键词=${filters.keyword}` : '', + filters.protocol ? `数据来源=${qualityProtocolLabel(filters.protocol)}` : '', + filters.issueType ? `问题类型=${qualityIssueLabel(filters.issueType)}` : '' + ].filter(Boolean); + return [ + '【告警业务影响报告】', + ...(release ? [`运行版本:${release}`] : []), + `影响等级:${severity}`, + `当前筛选:${filterLines.length > 0 ? filterLines.join(';') : '全部告警'}`, + `影响车辆:${Number(summary.issueVehicleCount ?? 0).toLocaleString()} 辆`, + `问题记录:${Number(summary.issueRecordCount ?? 0).toLocaleString()} 条`, + `错误/警告:${Number(summary.errorCount ?? 0).toLocaleString()}/${Number(summary.warningCount ?? 0).toLocaleString()}`, + `优先级:P0 ${p0Rows.length.toLocaleString()} 条 / P1 ${p1Rows.length.toLocaleString()} 条`, + `活跃规则:${activeRules.length.toLocaleString()} 类 / P0规则 ${p0Rules.length.toLocaleString()} 类`, + `主要来源:${primaryProtocol ? `${qualityProtocolLabel(primaryProtocol.name)} ${primaryProtocol.count.toLocaleString()} 条` : '-'}`, + `主要问题:${primaryIssue ? `${qualityIssueLabel(primaryIssue.name)} ${primaryIssue.count.toLocaleString()} 条` : '-'}`, + `容量风险:${storageOk ? '存储可写' : '存储异常'};Kafka Lag ${formatLag(health?.kafkaLag)};容量发现 ${capacityCount.toLocaleString()} 项`, + `建议动作:${severity === '高风险' ? '立即通知责任团队并按 P0/P1 队列闭环' : severity === '需关注' ? '纳入当日治理,持续观察来源覆盖和字段完整性' : '保持监控'}`, + `告警事件:${qualityShareURL()}`, + `通知规则:${appURL(buildAppHash({ page: 'notification-rules' }))}`, + `运维质量:${appURL(buildAppHash({ page: 'ops-quality' }))}` + ].join('\n'); +} + async function copyText(value: string, label: string) { const text = value.trim(); if (!text) { @@ -667,6 +721,25 @@ export function Quality({ const p0RuleCount = notificationPlan?.p0RuleCount ?? rules.filter((item) => item.count > 0 && item.level === 'P0').length; const priorityRows = normalizePriorityIssues(notificationPlan?.priorityIssues) ?? priorityIssueRows(issues); const dispatchOwnerRows = alertDispatchOwnerRows(rules); + const priorityP0Count = priorityRows.filter((row) => row.priority === 'P0').length; + const priorityP1Count = priorityRows.filter((row) => row.priority === 'P1').length; + const activeRulesForImpact = rules.filter((row) => row.count > 0); + const storageWritable = !health || (health.tdengineWritable && health.mysqlWritable); + const capacityFindingCount = health?.capacityFindings?.length ?? 0; + const impactSeverity = !storageWritable || priorityP0Count > 0 || p0RuleCount > 0 || errorCount > 0 + ? { label: '高风险', color: 'red' as const, detail: '需要立即通知责任团队并按 P0/P1 队列闭环。' } + : issueVehicleCount > 0 || capacityFindingCount > 0 + ? { label: '需关注', color: 'orange' as const, detail: '纳入当日治理,持续观察来源覆盖和字段完整性。' } + : { label: '正常', color: 'green' as const, detail: '当前没有明显业务影响,保持监控。' }; + const primaryProtocolImpact = summaryProtocols[0]; + const primaryIssueImpact = summaryIssueTypes[0]; + const impactCards = [ + { label: '影响车辆', value: `${issueVehicleCount.toLocaleString()} 辆`, detail: `${issueRecordCount.toLocaleString()} 条问题记录`, color: issueVehicleCount > 0 ? 'orange' as const : 'green' as const }, + { label: '优先级', value: `P0 ${priorityP0Count} / P1 ${priorityP1Count}`, detail: `${p0RuleCount} 类 P0 规则`, color: priorityP0Count > 0 || p0RuleCount > 0 ? 'red' as const : 'green' as const }, + { label: '主要来源', value: primaryProtocolImpact ? qualityProtocolLabel(primaryProtocolImpact.name) : '-', detail: primaryProtocolImpact ? `${primaryProtocolImpact.count.toLocaleString()} 条` : '暂无来源问题', color: primaryProtocolImpact ? 'blue' as const : 'green' as const }, + { label: '主要问题', value: primaryIssueImpact ? qualityIssueLabel(primaryIssueImpact.name) : '-', detail: primaryIssueImpact ? `${primaryIssueImpact.count.toLocaleString()} 条` : '暂无问题类型', color: primaryIssueImpact ? 'orange' as const : 'green' as const }, + { label: '容量与存储', value: storageWritable ? '可写' : '异常', detail: `Kafka Lag ${formatLag(health?.kafkaLag)} / 发现 ${capacityFindingCount} 项`, color: storageWritable && capacityFindingCount === 0 ? 'green' as const : 'red' as const } + ]; const loadIssues = (values: Record = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => { setLoadingIssues(true); @@ -809,6 +882,16 @@ export function Quality({ platformRelease: health?.runtime?.platformRelease }), '告警通知交接包'); }; + const copyBusinessImpact = () => { + copyText(alertBusinessImpactText({ + summary: notificationPlan?.summary ?? summary, + rules, + priorityRows, + health, + filters, + platformRelease: health?.runtime?.platformRelease + }), '业务影响报告'); + }; return (
@@ -857,6 +940,33 @@ export function Quality({ + 业务影响评估} + style={{ marginTop: 16 }} + > +
+
+ {impactSeverity.label} +
{issueVehicleCount.toLocaleString()} 辆受影响
+
{impactSeverity.detail}
+ + {storageWritable ? '存储可写' : '存储异常'} + 0 ? 'orange' : 'green'}>{activeRulesForImpact.length} 类活跃规则 + 0 ? 'orange' : 'green'}>{capacityFindingCount} 项容量发现 + +
+
+ {impactCards.map((item) => ( +
+ {item.label} + {item.value} +
{item.detail}
+
+ ))} +
+
+
告警事件闭环} diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css index cb2f8018..a976e2fc 100644 --- a/vehicle-data-platform/apps/web/src/styles/global.css +++ b/vehicle-data-platform/apps/web/src/styles/global.css @@ -736,6 +736,48 @@ button.vp-realtime-command-item:focus-visible { line-height: 28px; } +.vp-alert-impact-board { + display: grid; + grid-template-columns: minmax(280px, 0.72fr) minmax(0, 1.28fr); + gap: 16px; +} + +.vp-alert-impact-summary { + min-height: 148px; + padding: 12px; + border: 1px solid rgba(22, 100, 255, 0.28); + border-radius: var(--vp-radius); + background: #f5f9ff; + display: grid; + gap: 10px; + align-content: start; +} + +.vp-alert-impact-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 12px; +} + +.vp-alert-impact-item { + min-height: 148px; + padding: 12px; + border: 1px solid var(--vp-border); + border-radius: var(--vp-radius); + background: #fbfcff; + display: grid; + gap: 10px; + align-content: start; +} + +.vp-alert-impact-item strong { + color: var(--vp-text); + font-size: 20px; + line-height: 26px; + font-weight: 700; + word-break: break-word; +} + .vp-alert-dispatch-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); @@ -1926,6 +1968,8 @@ button.vp-realtime-command-item:focus-visible { .vp-conclusion-grid, .vp-monitor-layout, .vp-alert-flow, + .vp-alert-impact-board, + .vp-alert-impact-grid, .vp-alert-dispatch-grid, .vp-alert-command-board, .vp-alert-command-grid, diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx index 5e316659..ce080cce 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -3236,6 +3236,16 @@ test('shows alert rule and notification policy workspace on quality page', async render(); expect(await screen.findByText('告警触发规则')).toBeInTheDocument(); + expect(screen.getByText('业务影响评估')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '复制业务影响报告' })).toBeInTheDocument(); + expect(screen.getAllByText('高风险').length).toBeGreaterThan(0); + expect(screen.getByText('3 辆受影响')).toBeInTheDocument(); + expect(screen.getByText('影响车辆')).toBeInTheDocument(); + expect(screen.getByText('3 辆')).toBeInTheDocument(); + expect(screen.getByText('P0 0 / P1 0')).toBeInTheDocument(); + expect(screen.getAllByText('车辆服务').length).toBeGreaterThan(0); + expect(screen.getAllByText('暂无数据来源').length).toBeGreaterThan(0); + expect(screen.getAllByText('异常').length).toBeGreaterThan(0); expect(screen.getByText('4 类活跃')).toBeInTheDocument(); expect(screen.getByText('3 类 P0')).toBeInTheDocument(); expect(screen.getByText('容量与存储风险')).toBeInTheDocument(); @@ -3251,6 +3261,14 @@ test('shows alert rule and notification policy workspace on quality page', async expect(screen.getByText('30 分钟升级')).toBeInTheDocument(); expect(screen.getByText('验收:来源恢复并持续 10 分钟,车辆服务可查到实时与历史证据')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '复制业务影响报告' })); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警业务影响报告】')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('运行版本:platform-20260704155607')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('影响等级:高风险')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('影响车辆:3 辆')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('主要来源:车辆服务 3 条')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('主要问题:暂无数据来源 2 条')); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining('容量风险:存储异常;Kafka Lag 42;容量发现 1 项')); fireEvent.click(screen.getByRole('button', { name: '复制通知策略Runbook' })); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【告警通知策略Runbook】')); expect(writeText).toHaveBeenCalledWith(expect.stringContaining('运行版本:platform-20260704155607'));