import { Button, Card, Col, Form, Row, Select, Space, Table, Tag, Toast } from '@douyinfe/semi-ui'; import { IconCopy } from '@douyinfe/semi-icons'; import { useEffect, useState } from 'react'; import { api } from '../api/client'; import type { OpsHealth, QualityAlertRule, QualityNotificationPlan, QualityNotificationPolicy, QualitySummary, QualityIssueRow, QualityPriorityIssue } from '../api/types'; import { PageHeader } from '../components/PageHeader'; import { buildAppHash } from '../domain/appRoute'; import { qualityIssueLabel, qualityIssueOptions, qualityProtocolLabel, qualityProtocolOptions } from '../domain/qualityIssue'; import { qualityIssueVehicleLookup } from '../domain/vehicleLookup'; const statusColor: Record = { ok: 'green', warning: 'orange', error: 'red' }; function formatLag(value?: number | null) { return value == null ? '未接入' : value.toLocaleString(); } function storageReadStatus(health: OpsHealth | null) { if (!health) return 'pending'; return health.tdengineWritable && health.mysqlWritable ? 'ok' : 'error'; } function formatRequestTimeout(health: OpsHealth | null) { const value = health?.runtime?.requestTimeoutMs; return value == null || value <= 0 ? '未限制' : `${value.toLocaleString()} ms`; } const emptySummary: QualitySummary = { issueVehicleCount: 0, issueRecordCount: 0, errorCount: 0, warningCount: 0, protocols: [], issueTypes: [] }; const alertRuleTemplates = [ { issueType: 'NO_SOURCE', level: 'P0', owner: '平台接入', trigger: '绑定车辆连续无 GB32960、JT808、MQTT 来源证据', notify: '立即通知接入运维,30 分钟未恢复升级给业务责任人', sla: '30 分钟确认' }, { issueType: 'VIN_MISSING', level: 'P0', owner: '车辆档案', trigger: '来源有数据但无法归并到 VIN', notify: '通知档案维护人补齐车牌、手机号和 VIN 映射', sla: '2 小时修复' }, { issueType: 'LINK_GAP', level: 'P1', owner: '链路运维', trigger: '车辆或平台来源上报间断、Redis 在线状态过期', notify: '通知平台转发和网关值班人,持续异常进入日报', sla: '1 小时恢复' }, { issueType: 'FIELD_MISSING', level: 'P1', owner: '协议解析', trigger: '核心字段缺失导致定位、里程或统计不可用', notify: '通知解析负责人核对字段映射和原始记录样本', sla: '当日闭环' } ]; const notificationPolicies = [ { name: 'P0 实时中断', target: '接入运维 + 业务责任人', channel: '站内告警 / 邮件 / 企业微信', condition: '无来源、VIN 缺失、存储不可写', escalationMinutes: 30, acceptanceCriteria: '来源恢复并持续 10 分钟,车辆服务可查到实时与历史证据' }, { name: 'P1 数据质量', target: '协议解析 + 数据治理', channel: '站内告警 / 每日汇总邮件', condition: '字段缺失、链路间断、容量风险', escalationMinutes: 120, acceptanceCriteria: '核心字段恢复解析,影响车辆可通过原始记录与历史证据复核' }, { name: 'P2 趋势关注', target: '数智中心', channel: '周报 / 趋势看板', condition: '单源车辆、档案缺项、来源覆盖下降', escalationMinutes: 1440, acceptanceCriteria: '趋势原因已归档,责任人和后续治理动作明确' } ]; type AlertRuleRow = { issueType: string; title?: string; level: string; owner: string; trigger: string; notify: string; sla: string; count: number; }; type PriorityIssueRow = QualityIssueRow & { priority: 'P0' | 'P1'; actionLabel: string; actionDetail?: string; sla: string; vehicleLabel: string; realtimeHash?: string; historyHash?: string; rawHash?: string; vehicleHash?: string; notificationText?: string; }; type AlertDispatchOwnerRow = { owner: string; activeRuleCount: number; hitCount: number; p0Count: number; primaryIssueType: string; primarySla: string; primaryHitCount: number; }; function qualityParams(values: Record) { const params = new URLSearchParams(); if (values?.keyword) params.set('keyword', values.keyword); if (values?.protocol) params.set('protocol', values.protocol); if (values?.issueType) params.set('issueType', values.issueType); return params; } function qualityShareURL() { return `${window.location.origin}${window.location.pathname}${window.location.hash}`; } function appURL(hash: string) { return `${window.location.origin}${window.location.pathname}${hash}`; } function issueEvidenceDate(value?: string) { const match = String(value ?? '').match(/^(\d{4})-(\d{2})-(\d{2})/); return match ? `${match[1]}-${match[2]}-${match[3]}` : ''; } function nextDate(value: string) { const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim()); if (!match) return ''; const date = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]) + 1)); return date.toISOString().slice(0, 10); } function qualityActionRecommendation(row: QualityIssueRow) { if (row.issueType === 'NO_SOURCE') { return { label: '确认平台转发', color: 'orange' as const, detail: '车辆已绑定但没有任何来源证据,先确认平台转发、端口和订阅。' }; } if (row.issueType === 'VIN_MISSING' || !row.vin?.trim()) { return { label: '维护身份绑定', color: 'red' as const, detail: '数据已有来源但无法归并到 VIN,优先用车牌/手机号补齐绑定。' }; } if (row.issueType === 'LINK_GAP') { return { label: '排查来源链路', color: 'orange' as const, detail: '来源存在上报间断,优先检查平台转发、网络和消费延迟。' }; } if (row.issueType === 'FIELD_MISSING') { return { label: '核对解析字段', color: 'orange' as const, detail: '字段缺失会影响统计和展示,优先核对解析映射和原始记录。' }; } return { label: '查看车辆服务', color: 'blue' as const, detail: '进入车辆服务详情,结合来源证据继续排查。' }; } function issueCount(summary: QualitySummary, issueType: string) { const issueTypes = Array.isArray(summary.issueTypes) ? summary.issueTypes : []; return issueTypes.find((item) => item.name === issueType)?.count ?? 0; } const issuePriorityWeight: Record = { NO_SOURCE: 10, VIN_MISSING: 9, LINK_GAP: 8, FIELD_MISSING: 7 }; function alertRuleRows(summary: QualitySummary, health: OpsHealth | null): AlertRuleRow[] { const storageWritable = health == null || (health.tdengineWritable && health.mysqlWritable); const capacityCount = health?.capacityFindings?.length ?? 0; const rows = alertRuleTemplates.map((item) => ({ ...item, count: issueCount(summary, item.issueType) })); rows.push({ issueType: 'CAPACITY_RISK', level: storageWritable && capacityCount === 0 ? 'P2' : 'P0', owner: '基础设施', trigger: 'Kafka Lag、连接容量、Redis 在线 Key 或存储读写异常', notify: storageWritable ? '容量风险进入运维日报,超过阈值升级' : '立即通知基础设施值班人处理存储不可写', sla: storageWritable ? '当日评估' : '15 分钟恢复', count: capacityCount + (storageWritable ? 0 : 1) }); return rows; } function alertDispatchOwnerRows(rules: AlertRuleRow[]): AlertDispatchOwnerRow[] { const ownerMap = new Map(); rules.forEach((rule) => { const owner = rule.owner || '未分配'; const current = ownerMap.get(owner) ?? { owner, activeRuleCount: 0, hitCount: 0, p0Count: 0, primaryIssueType: rule.issueType, primarySla: rule.sla, primaryHitCount: 0 }; current.activeRuleCount += rule.count > 0 ? 1 : 0; current.hitCount += rule.count; current.p0Count += rule.count > 0 && rule.level === 'P0' ? 1 : 0; const shouldReplacePrimary = rule.count > current.primaryHitCount || (rule.count === current.primaryHitCount && rule.level === 'P0'); if (shouldReplacePrimary) { current.primaryIssueType = rule.issueType; current.primarySla = rule.sla; current.primaryHitCount = rule.count; } ownerMap.set(owner, current); }); return [...ownerMap.values()] .filter((row) => row.hitCount > 0 || row.activeRuleCount > 0) .sort((left, right) => right.p0Count - left.p0Count || right.hitCount - left.hitCount || left.owner.localeCompare(right.owner)); } function issueSla(issueType: string) { return alertRuleTemplates.find((item) => item.issueType === issueType)?.sla ?? '当日闭环'; } function priorityIssueLevel(row: QualityIssueRow): 'P0' | 'P1' { if (row.severity === 'error' || row.issueType === 'NO_SOURCE') { return 'P0'; } return 'P1'; } function priorityVehicleLabel(row: QualityIssueRow) { const identity = row.vin?.trim() || row.phone?.trim() || row.sourceEndpoint?.trim() || '-'; return [row.plate?.trim(), identity].filter(Boolean).join(' / '); } function priorityIssueRows(rows: QualityIssueRow[]): PriorityIssueRow[] { return rows .map((row) => { const action = qualityActionRecommendation(row); return { ...row, priority: priorityIssueLevel(row), actionLabel: action.label, actionDetail: action.detail, sla: issueSla(row.issueType), vehicleLabel: priorityVehicleLabel(row) }; }) .sort((a, b) => { if (a.priority !== b.priority) return a.priority === 'P0' ? -1 : 1; const weightDelta = (issuePriorityWeight[b.issueType] ?? 0) - (issuePriorityWeight[a.issueType] ?? 0); if (weightDelta !== 0) return weightDelta; return String(b.lastSeen ?? '').localeCompare(String(a.lastSeen ?? '')); }) .slice(0, 5); } function priorityIssueFromRow(row: QualityIssueRow): PriorityIssueRow { return priorityIssueRows([row])[0]; } function priorityIssueNotificationText(row: PriorityIssueRow) { const lookup = qualityIssueVehicleLookup(row); const dateFrom = issueEvidenceDate(row.lastSeen); const dateTo = nextDate(dateFrom); const evidenceFilters = { ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}) }; return [ `【${row.priority} 告警通知】${qualityIssueLabel(row.issueType)}`, `车辆:${row.vehicleLabel}`, `数据来源:${qualityProtocolLabel(row.protocol)}`, `问题:${qualityIssueLabel(row.issueType)}`, `建议动作:${row.actionLabel}`, `SLA:${row.sla}`, `最后时间:${row.lastSeen || '-'}`, `详情:${row.detail || '-'}`, `实时定位:${appURL(buildAppHash({ page: 'realtime', keyword: lookup.key, protocol: row.protocol }))}`, `轨迹证据:${appURL(buildAppHash({ page: 'history', keyword: lookup.key, protocol: row.protocol, filters: evidenceFilters }))}`, `原始记录:${appURL(buildAppHash({ page: 'history-query', keyword: lookup.key, protocol: row.protocol, filters: { tab: 'raw', ...evidenceFilters, includeFields: 'true' } }))}`, `车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: lookup.key, protocol: row.protocol }))}`, `告警筛选:${qualityShareURL()}` ].join('\n'); } function alertRuleTemplate(issueType: string) { return alertRuleTemplates.find((item) => item.issueType === issueType); } function acceptanceCriteria(row: PriorityIssueRow) { if (row.issueType === 'VIN_MISSING') { return '车辆能解析到 VIN,实时、历史和原始记录可通过同一车辆服务查询'; } if (row.issueType === 'NO_SOURCE') { return '至少一个生产来源恢复在线,车辆实时状态、历史轨迹和原始记录可查询'; } if (row.issueType === 'LINK_GAP') { return '来源恢复连续上报,实时在线窗口和历史轨迹无异常断点'; } if (row.issueType === 'FIELD_MISSING') { return '缺失字段恢复解析,统计、定位或里程依赖的核心字段可查询'; } return '车辆服务状态恢复正常,告警不再命中当前规则'; } function priorityIssueTicketText(row: PriorityIssueRow) { const lookup = qualityIssueVehicleLookup(row); const dateFrom = issueEvidenceDate(row.lastSeen); const dateTo = nextDate(dateFrom); const evidenceFilters = { ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}) }; const rule = alertRuleTemplate(row.issueType); return [ '【告警处置工单】', `标题:[${row.priority}] ${qualityIssueLabel(row.issueType)} - ${row.vehicleLabel}`, `优先级:${row.priority}`, `责任团队:${rule?.owner || '数据治理'}`, `触发规则:${rule?.trigger || qualityIssueLabel(row.issueType)}`, `车辆:${row.vehicleLabel}`, `数据来源:${qualityProtocolLabel(row.protocol)}`, `来源地址:${row.sourceEndpoint || '-'}`, `问题:${qualityIssueLabel(row.issueType)}`, `建议动作:${row.actionLabel}`, `SLA:${row.sla}`, `最后时间:${row.lastSeen || '-'}`, `问题说明:${row.detail || '-'}`, `处置说明:${row.actionDetail || '-'}`, '处置步骤:', '1. 打开车辆服务核对当前身份解析结果', '2. 核对实时定位、轨迹证据和原始记录', '3. 完成后确认车辆服务状态恢复,问题不再命中当前告警', `验收标准:${acceptanceCriteria(row)}`, `实时定位:${appURL(buildAppHash({ page: 'realtime', keyword: lookup.key, protocol: row.protocol }))}`, `轨迹证据:${appURL(buildAppHash({ page: 'history', keyword: lookup.key, protocol: row.protocol, filters: evidenceFilters }))}`, `原始记录:${appURL(buildAppHash({ page: 'history-query', keyword: lookup.key, protocol: row.protocol, filters: { tab: 'raw', ...evidenceFilters, includeFields: 'true' } }))}`, `车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: lookup.key, protocol: row.protocol }))}`, `告警筛选:${qualityShareURL()}` ].join('\n'); } function priorityIssueEvidencePackageText(row: PriorityIssueRow) { const lookup = qualityIssueVehicleLookup(row); const dateFrom = issueEvidenceDate(row.lastSeen); const dateTo = nextDate(dateFrom); const evidenceFilters = { ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}) }; return [ '【告警证据包】', `优先级:${row.priority}`, `车辆:${row.vehicleLabel}`, `数据来源:${qualityProtocolLabel(row.protocol)}`, `来源地址:${row.sourceEndpoint || '-'}`, `问题:${qualityIssueLabel(row.issueType)}`, `建议动作:${row.actionLabel}`, `SLA:${row.sla}`, `最后时间:${row.lastSeen || '-'}`, `详情:${row.detail || '-'}`, `验收标准:${acceptanceCriteria(row)}`, `实时定位:${appURL(buildAppHash({ page: 'realtime', keyword: lookup.key, protocol: row.protocol }))}`, `轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: lookup.key, protocol: row.protocol, filters: evidenceFilters }))}`, `原始记录:${appURL(buildAppHash({ page: 'history-query', keyword: lookup.key, protocol: row.protocol, filters: { tab: 'raw', ...evidenceFilters, includeFields: 'true' } }))}`, `里程复核:${appURL(buildAppHash({ page: 'mileage', keyword: lookup.key, protocol: row.protocol, filters: evidenceFilters }))}`, `车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: lookup.key, protocol: row.protocol }))}`, `告警筛选:${qualityShareURL()}` ].join('\n'); } function priorityDigestPolicyLines(policies?: QualityNotificationPolicy[]) { const policy = policies?.find((item) => item.name.startsWith('P0')) ?? policies?.[0]; if (!policy) return []; const escalation = formatEscalationMinutes(policy.escalationMinutes); return [ `通知升级:${[policy.name, escalation, policy.target].filter(Boolean).join(' / ')}`, ...(policy.acceptanceCriteria ? [`验收标准:${policy.acceptanceCriteria}`] : []) ]; } function priorityIssueDigestText(rows: PriorityIssueRow[], summary: QualitySummary, platformRelease?: string, policies?: QualityNotificationPolicy[]) { const p0Count = rows.filter((row) => row.priority === 'P0').length; const p1Count = rows.filter((row) => row.priority === 'P1').length; const release = platformRelease?.trim(); const policyLines = priorityDigestPolicyLines(policies); const lines = rows.map((row, index) => [ `${index + 1}. [${row.priority}] ${row.vehicleLabel}`, ` 来源:${qualityProtocolLabel(row.protocol)} / 问题:${qualityIssueLabel(row.issueType)}`, ` 建议动作:${row.actionLabel} / SLA:${row.sla}`, ` 最后时间:${row.lastSeen || '-'} / 详情:${row.detail || '-'}` ].join('\n')); return [ '【告警优先队列汇总】', `问题车辆:${summary.issueVehicleCount.toLocaleString()},问题记录:${summary.issueRecordCount.toLocaleString()}`, `P0:${p0Count.toLocaleString()} 条,P1:${p1Count.toLocaleString()} 条`, ...(release ? [`运行版本:${release}`] : []), ...policyLines, '', ...lines, '', `告警筛选:${qualityShareURL()}` ].join('\n'); } function slaMinutes(row: PriorityIssueRow) { if (row.issueType === 'NO_SOURCE') return 30; if (row.issueType === 'VIN_MISSING') return 120; if (row.issueType === 'LINK_GAP') return 60; if (row.issueType === 'FIELD_MISSING') return 24 * 60; if (row.sla.includes('15 分钟')) return 15; if (row.sla.includes('30 分钟')) return 30; if (row.sla.includes('1 小时')) return 60; if (row.sla.includes('2 小时')) return 120; return 24 * 60; } function formatDuration(minutes: number) { const value = Math.max(0, Math.round(minutes)); if (value < 60) return `${value} 分钟`; const hours = Math.floor(value / 60); const rest = value % 60; return rest > 0 ? `${hours} 小时 ${rest} 分钟` : `${hours} 小时`; } function formatEscalationMinutes(minutes?: number) { if (!Number.isFinite(minutes) || Number(minutes) <= 0) return ''; return `${formatDuration(Number(minutes))}升级`; } function parseIssueTime(value?: string) { const raw = String(value ?? '').trim(); if (!raw) return Number.NaN; const normalized = raw.includes('T') ? raw : raw.replace(' ', 'T'); const parsed = Date.parse(normalized); if (Number.isFinite(parsed)) return parsed; return Date.parse(`${raw.replace(' ', 'T')}+08:00`); } function escalationClock(row: PriorityIssueRow, nowMs = Date.now()) { const lastSeenMs = parseIssueTime(row.lastSeen); if (!Number.isFinite(lastSeenMs)) { return { status: 'SLA 待确认', color: 'grey' as const, detail: '缺少告警时间', deadline: '-' }; } const minutes = slaMinutes(row); const deadlineMs = lastSeenMs + minutes * 60 * 1000; const diffMinutes = (deadlineMs - nowMs) / 60000; const deadline = new Date(deadlineMs).toLocaleString('zh-CN', { hour12: false }); if (diffMinutes < 0) { return { status: '超 SLA', color: 'red' as const, detail: `已超 ${formatDuration(Math.abs(diffMinutes))}`, deadline }; } if (diffMinutes <= Math.max(15, minutes * 0.2)) { return { status: '即将升级', color: 'orange' as const, detail: `剩余 ${formatDuration(diffMinutes)}`, deadline }; } return { status: 'SLA 正常', color: 'green' as const, detail: `剩余 ${formatDuration(diffMinutes)}`, deadline }; } function ruleStatusColor(count: number, level: string): 'green' | 'orange' | 'red' | 'grey' { if (count <= 0) return 'green'; if (level === 'P0') return 'red'; if (level === 'P1') return 'orange'; return 'grey'; } function normalizeAlertRules(rows?: QualityAlertRule[]): AlertRuleRow[] | null { if (!Array.isArray(rows)) return null; return rows; } function normalizeNotificationPolicies(rows?: QualityNotificationPolicy[]): QualityNotificationPolicy[] | null { if (!Array.isArray(rows)) return null; return rows; } function normalizePriorityIssues(rows?: QualityPriorityIssue[]): PriorityIssueRow[] | null { if (!Array.isArray(rows)) return null; return rows; } function notificationPolicyRunbookText(policies: QualityNotificationPolicy[], rules: AlertRuleRow[], platformRelease?: string) { const activeRules = rules.filter((row) => row.count > 0); const release = platformRelease?.trim(); const lines = policies.map((policy, index) => [ `${index + 1}. ${policy.name} / ${policy.target}`, ` 触发:${policy.condition}`, ` 渠道:${policy.channel}`, ` 升级:${formatEscalationMinutes(policy.escalationMinutes) || '-'}`, ` 验收:${policy.acceptanceCriteria || '-'}` ].join('\n')); return [ '【告警通知策略Runbook】', `活跃规则:${activeRules.length.toLocaleString()} 类`, `P0规则:${activeRules.filter((row) => row.level === 'P0').length.toLocaleString()} 类`, ...(release ? [`运行版本:${release}`] : []), '', ...lines, '', `告警筛选:${qualityShareURL()}` ].join('\n'); } function notificationHandoffText({ rows, summary, rules, policies, filters, platformRelease }: { rows: PriorityIssueRow[]; summary: QualitySummary; rules: AlertRuleRow[]; policies: QualityNotificationPolicy[]; filters: Record; platformRelease?: string; }) { const release = platformRelease?.trim(); const activeRules = rules.filter((row) => row.count > 0); const p0Rows = rows.filter((row) => row.priority === 'P0'); const p1Rows = rows.filter((row) => row.priority === 'P1'); const primaryPolicy = policies.find((item) => item.name.startsWith('P0')) ?? policies[0]; const filterLines = [ filters.keyword ? `关键词=${filters.keyword}` : '', filters.protocol ? `数据来源=${qualityProtocolLabel(filters.protocol)}` : '', filters.issueType ? `问题类型=${qualityIssueLabel(filters.issueType)}` : '' ].filter(Boolean); const issueLines = rows.length > 0 ? rows.map((row, index) => { const lookup = qualityIssueVehicleLookup(row); const dateFrom = issueEvidenceDate(row.lastSeen); const dateTo = nextDate(dateFrom); const evidenceFilters = { ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}) }; return [ `${index + 1}. [${row.priority}] ${row.vehicleLabel}`, ` 来源:${qualityProtocolLabel(row.protocol)};问题:${qualityIssueLabel(row.issueType)};SLA:${row.sla}`, ` 建议动作:${row.actionLabel};最后时间:${row.lastSeen || '-'}`, ` 详情:${row.detail || '-'}`, ` 实时:${appURL(buildAppHash({ page: 'realtime', keyword: lookup.key, protocol: row.protocol }))}`, ` 轨迹:${appURL(buildAppHash({ page: 'history', keyword: lookup.key, protocol: row.protocol, filters: evidenceFilters }))}`, ` 原始记录:${appURL(buildAppHash({ page: 'history-query', keyword: lookup.key, protocol: row.protocol, filters: { tab: 'raw', ...evidenceFilters, includeFields: 'true' } }))}`, ` 车辆服务:${appURL(buildAppHash({ page: 'detail', keyword: lookup.key, protocol: row.protocol }))}` ].join('\n'); }) : ['当前筛选下暂无待通知车辆']; return [ '【告警通知交接包】', ...(release ? [`运行版本:${release}`] : []), `当前筛选:${filterLines.length > 0 ? filterLines.join(';') : '全部告警'}`, `问题车辆:${Number(summary.issueVehicleCount ?? 0).toLocaleString()};问题记录:${Number(summary.issueRecordCount ?? 0).toLocaleString()}`, `优先级:P0 ${p0Rows.length.toLocaleString()} 条 / P1 ${p1Rows.length.toLocaleString()} 条`, `活跃规则:${activeRules.length.toLocaleString()} 类${activeRules.length > 0 ? `(${activeRules.map((row) => `${row.level} ${row.title || qualityIssueLabel(row.issueType)} ${row.count}`).join(';')})` : ''}`, primaryPolicy ? `主通知策略:${[primaryPolicy.name, primaryPolicy.target, primaryPolicy.channel, formatEscalationMinutes(primaryPolicy.escalationMinutes)].filter(Boolean).join(' / ')}` : '主通知策略:-', primaryPolicy?.acceptanceCriteria ? `统一验收:${primaryPolicy.acceptanceCriteria}` : '统一验收:车辆服务状态恢复,实时、轨迹和原始记录可查询', '', '待通知车辆:', ...issueLines, '', `告警事件:${qualityShareURL()}`, `通知闭环:${appURL(buildAppHash({ page: 'notification-rules' }))}`, `运维质量:${appURL(buildAppHash({ page: 'ops-quality' }))}` ].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'); } function alertCustomerDecisionText({ summary, rules, priorityRows, health, filters, platformRelease }: { summary: QualitySummary; rules: AlertRuleRow[]; priorityRows: PriorityIssueRow[]; health: OpsHealth | null; filters: Record; platformRelease?: string; }) { const release = platformRelease?.trim(); const activeRules = rules.filter((row) => row.count > 0); const p0Rows = priorityRows.filter((row) => row.priority === 'P0'); const p1Rows = priorityRows.filter((row) => row.priority === 'P1'); 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 decision = !storageOk || p0Rows.length > 0 || activeRules.some((row) => row.level === 'P0') || Number(summary.errorCount ?? 0) > 0 ? '立即处置' : Number(summary.issueVehicleCount ?? 0) > 0 || capacityCount > 0 ? '当日跟进' : '持续观察'; const focusIssue = priorityRows[0]; const focusLookup = focusIssue ? qualityIssueVehicleLookup(focusIssue) : undefined; const filterLines = [ filters.keyword ? `关键词=${filters.keyword}` : '', filters.protocol ? `数据来源=${qualityProtocolLabel(filters.protocol)}` : '', filters.issueType ? `问题类型=${qualityIssueLabel(filters.issueType)}` : '' ].filter(Boolean); return [ '【客户告警决策说明】', ...(release ? [`运行版本:${release}`] : []), `决策结论:${decision}`, `筛选范围:${filterLines.length > 0 ? filterLines.join(';') : '全部告警'}`, `影响车辆:${Number(summary.issueVehicleCount ?? 0).toLocaleString()} 辆;问题记录:${Number(summary.issueRecordCount ?? 0).toLocaleString()} 条`, `优先队列:P0 ${p0Rows.length.toLocaleString()} / P1 ${p1Rows.length.toLocaleString()}`, `主要来源:${primaryProtocol ? `${qualityProtocolLabel(primaryProtocol.name)} ${primaryProtocol.count.toLocaleString()} 条` : '-'}`, `主要问题:${primaryIssue ? `${qualityIssueLabel(primaryIssue.name)} ${primaryIssue.count.toLocaleString()} 条` : '-'}`, `链路状态:Kafka Lag ${formatLag(health?.kafkaLag)};Redis 在线 ${formatLag(health?.redisOnlineKeys)};存储${storageOk ? '可写' : '异常'};容量发现 ${capacityCount.toLocaleString()} 项`, `焦点车辆:${focusIssue ? `${focusIssue.vehicleLabel} / ${qualityProtocolLabel(focusIssue.protocol)} / ${qualityIssueLabel(focusIssue.issueType)}` : '-'}`, '', '客户处置路径:', '1. 先确认影响车辆和 P0/P1 数量。', '2. P0 立即通知责任团队,超过 SLA 自动升级。', '3. 每辆焦点车都要打开实时、轨迹、原始记录、里程证据闭环。', '4. 恢复后确认告警不再命中,并保留交接包。', '', `告警事件:${qualityShareURL()}`, `实时监控:${appURL(buildAppHash({ page: 'realtime', keyword: focusLookup?.key || filters.keyword || '', protocol: focusIssue?.protocol || filters.protocol }))}`, `轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: focusLookup?.key || filters.keyword || '', protocol: focusIssue?.protocol || filters.protocol }))}`, `通知闭环:${appURL(buildAppHash({ page: 'notification-rules' }))}` ].join('\n'); } function alertRecoveryReceiptText({ summary, impactLabel, priorityRows, primaryPolicy, filters, platformRelease }: { summary: QualitySummary; impactLabel: string; priorityRows: PriorityIssueRow[]; primaryPolicy?: QualityNotificationPolicy; filters: Record; platformRelease?: string; }) { const release = platformRelease?.trim(); const filterLines = [ filters.keyword ? `关键词=${filters.keyword}` : '', filters.protocol ? `数据来源=${qualityProtocolLabel(filters.protocol)}` : '', filters.issueType ? `问题类型=${qualityIssueLabel(filters.issueType)}` : '' ].filter(Boolean); const focusRows = priorityRows.slice(0, 5).map((row, index) => { const lookup = qualityIssueVehicleLookup(row); return `${index + 1}. ${row.vehicleLabel} / ${qualityProtocolLabel(row.protocol)} / ${lookup.key || '-'} / ${qualityIssueLabel(row.issueType)}`; }); return [ '【客户告警恢复验收回执】', ...(release ? [`运行版本:${release}`] : []), `当前筛选:${filterLines.length > 0 ? filterLines.join(';') : '全部告警'}`, `当前影响:${Number(summary.issueVehicleCount ?? 0).toLocaleString()} 辆车 / ${Number(summary.issueRecordCount ?? 0).toLocaleString()} 条告警 / ${impactLabel}`, '恢复标准:实时可看 / 轨迹可回放 / 里程可核对 / 历史证据可导出', `通知策略:${primaryPolicy ? [primaryPolicy.name, primaryPolicy.target, primaryPolicy.channel].filter(Boolean).join(' / ') : '-'}`, `验收口径:${primaryPolicy?.acceptanceCriteria || '车辆服务状态恢复,实时、轨迹、历史和里程证据可查询。'}`, '', '待复核车辆:', ...(focusRows.length > 0 ? focusRows : ['当前没有优先车辆']), '', `告警事件:${qualityShareURL()}`, `实时监控:${appURL(buildAppHash({ page: 'realtime', filters }))}`, `轨迹回放:${appURL(buildAppHash({ page: 'history', filters }))}`, `里程统计:${appURL(buildAppHash({ page: 'mileage', filters }))}`, `历史导出:${appURL(buildAppHash({ page: 'history-query', filters: { ...filters, tab: 'raw', includeFields: 'true' } }))}` ].join('\n'); } async function copyText(value: string, label: string) { const text = value.trim(); if (!text) { Toast.warning(`${label}为空`); return; } try { await navigator.clipboard.writeText(text); Toast.success(`已复制${label}`); } catch { Toast.error(`复制${label}失败`); } } async function copyQualityShareURL() { await copyText(qualityShareURL(), '筛选链接'); } export function Quality({ onOpenVehicle, onOpenRealtime, onOpenHistory, onOpenRaw, onOpenMileage, onOpenNotificationRules, onHealthLoaded, onNotificationPlanLoaded, onFiltersChange, initialFilters = {} }: { onOpenVehicle: (vin: string, protocol?: string) => void; onOpenRealtime?: (filters: Record) => void; onOpenHistory?: (filters: Record) => void; onOpenRaw?: (filters: Record) => void; onOpenMileage?: (filters: Record) => void; onOpenNotificationRules?: () => void; onHealthLoaded?: (health: OpsHealth) => void; onNotificationPlanLoaded?: (plan: QualityNotificationPlan) => void; onFiltersChange?: (filters: Record) => void; initialFilters?: Record; }) { const [issues, setIssues] = useState([]); const [summary, setSummary] = useState(emptySummary); const [health, setHealth] = useState(null); const [notificationPlan, setNotificationPlan] = useState(null); const [loadingIssues, setLoadingIssues] = useState(true); const [loadingSummary, setLoadingSummary] = useState(true); const [loadingHealth, setLoadingHealth] = useState(true); const [filters, setFilters] = useState>(initialFilters); const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 20, total: 0 }); const summaryIssueTypes = Array.isArray(summary.issueTypes) ? summary.issueTypes : []; const summaryProtocols = Array.isArray(summary.protocols) ? summary.protocols : []; const issueVehicleCount = Number(summary.issueVehicleCount ?? 0); const issueRecordCount = Number(summary.issueRecordCount ?? 0); const errorCount = Number(summary.errorCount ?? 0); const warningCount = Number(summary.warningCount ?? 0); const primaryIssueType = summaryIssueTypes[0]?.name; const rules = normalizeAlertRules(notificationPlan?.rules) ?? alertRuleRows(summary, health); const policies = normalizeNotificationPolicies(notificationPlan?.policies) ?? notificationPolicies; const activeRuleCount = notificationPlan?.activeRuleCount ?? rules.filter((item) => item.count > 0).length; 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 focusIssue = priorityRows[0]; const focusLookup = focusIssue ? qualityIssueVehicleLookup(focusIssue) : undefined; 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 customerDecision = impactSeverity.label === '高风险' ? { label: '立即处置', color: 'red' as const, detail: '优先确认客户车辆服务是否受影响,并立即触发通知闭环。' } : impactSeverity.label === '需关注' ? { label: '当日跟进', color: 'orange' as const, detail: '纳入当日数据治理,持续观察来源恢复和车辆证据。' } : { label: '持续观察', color: 'green' as const, detail: '当前没有客户侧高优先级影响,保持常规监控。' }; const primaryPolicy = policies.find((item) => item.name.startsWith('P0')) ?? policies[0]; const secondaryPolicy = policies.find((item) => item.name.startsWith('P1')) ?? policies[1] ?? primaryPolicy; 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 customerNotificationSlaItems = [ { label: '客户影响范围', value: `${issueVehicleCount.toLocaleString()} 辆车`, detail: `${issueRecordCount.toLocaleString()} 条告警记录,优先判断实时、轨迹和里程是否受影响。`, color: issueVehicleCount > 0 ? 'orange' as const : 'green' as const, action: '影响摘要', onClick: () => copyBusinessImpact() }, { label: '通知负责人', value: primaryPolicy?.target || '待配置', detail: primaryPolicy ? `${primaryPolicy.name} / ${primaryPolicy.channel}` : '缺少通知策略,建议先配置责任团队。', color: primaryPolicy ? 'blue' as const : 'orange' as const, action: '通知闭环', onClick: () => onOpenNotificationRules?.() }, { label: '升级时限', value: formatEscalationMinutes(primaryPolicy?.escalationMinutes) || '未配置', detail: secondaryPolicy && secondaryPolicy !== primaryPolicy ? `P1 参考:${secondaryPolicy.target} / ${formatEscalationMinutes(secondaryPolicy.escalationMinutes) || '未配置'}` : '高优先级事件需要明确超时升级路径。', color: priorityP0Count > 0 ? 'red' as const : priorityP1Count > 0 ? 'orange' as const : 'green' as const, action: '复制策略', onClick: () => copyPolicyRunbook() }, { label: '恢复验收', value: primaryPolicy?.acceptanceCriteria ? '有标准' : '待补充', detail: primaryPolicy?.acceptanceCriteria || '车辆服务状态恢复,实时、轨迹、历史和里程证据可查询。', color: primaryPolicy?.acceptanceCriteria ? 'green' as const : 'orange' as const, action: '交接包', onClick: () => copyNotificationHandoff() } ]; const notificationChannelItems = (primaryPolicy?.channel || '站内告警') .split('/') .map((item) => item.trim()) .filter(Boolean) .map((channel) => ({ channel, target: primaryPolicy?.target || '待配置责任人', condition: primaryPolicy?.condition || '告警规则命中', escalation: formatEscalationMinutes(primaryPolicy?.escalationMinutes) || '未配置升级', acceptance: primaryPolicy?.acceptanceCriteria || '车辆服务状态恢复,实时、轨迹、历史和里程证据可查询。' })); const loadIssues = (values: Record = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => { setLoadingIssues(true); const params = qualityParams(values); params.set('limit', String(pageSize)); params.set('offset', String((page - 1) * pageSize)); api.alertEvents(params) .then((nextPage) => { setIssues(nextPage.items); setPagination({ currentPage: page, pageSize, total: nextPage.total }); }) .catch((error: Error) => Toast.error(error.message)) .finally(() => setLoadingIssues(false)); }; const loadSummary = (values: Record = filters) => { setLoadingSummary(true); api.alertEventSummary(qualityParams(values)) .then(setSummary) .catch((error: Error) => Toast.error(error.message)) .finally(() => setLoadingSummary(false)); }; const loadHealth = () => { setLoadingHealth(true); api.opsHealth() .then((nextHealth) => { setHealth(nextHealth); onHealthLoaded?.(nextHealth); }) .catch((error: Error) => Toast.error(error.message)) .finally(() => setLoadingHealth(false)); }; const loadNotificationPlan = (values: Record = filters, pageSize = pagination.pageSize) => { const params = qualityParams(values); params.set('limit', String(pageSize)); api.alertEventNotificationPlan(params) .then((plan) => { if (Array.isArray(plan.rules) && Array.isArray(plan.policies) && Array.isArray(plan.priorityIssues)) { setNotificationPlan(plan); onNotificationPlanLoaded?.(plan); } }) .catch(() => { setNotificationPlan(null); }); }; useEffect(() => { setFilters(initialFilters); loadSummary(initialFilters); loadIssues(initialFilters, 1, pagination.pageSize); loadHealth(); loadNotificationPlan(initialFilters, pagination.pageSize); }, [JSON.stringify(initialFilters)]); const applyFilters = (nextFilters: Record) => { setFilters(nextFilters); onFiltersChange?.(nextFilters); loadSummary(nextFilters); loadIssues(nextFilters, 1, pagination.pageSize); loadNotificationPlan(nextFilters, pagination.pageSize); }; const drillPrimaryIssue = () => { if (!primaryIssueType) { return; } applyFilters({ ...filters, issueType: primaryIssueType }); }; const drillProtocol = (protocol: string) => { applyFilters({ ...filters, protocol }); }; const drillIssueType = (issueType: string) => { applyFilters({ ...filters, issueType }); }; const filterSummary = [ filters.keyword ? `关键词:${filters.keyword}` : '', filters.protocol ? `数据来源:${qualityProtocolLabel(filters.protocol)}` : '', filters.issueType ? `问题类型:${qualityIssueLabel(filters.issueType)}` : '' ].filter(Boolean); const openIssueHistory = (row: QualityIssueRow) => { const lookup = qualityIssueVehicleLookup(row); const dateFrom = issueEvidenceDate(row.lastSeen); const dateTo = nextDate(dateFrom); onOpenHistory?.({ keyword: lookup.key, protocol: row.protocol, ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}) }); }; const openIssueRaw = (row: QualityIssueRow) => { const lookup = qualityIssueVehicleLookup(row); const dateFrom = issueEvidenceDate(row.lastSeen); const dateTo = nextDate(dateFrom); onOpenRaw?.({ keyword: lookup.key, protocol: row.protocol, ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}), includeFields: 'true' }); }; const openIssueRealtime = (row: QualityIssueRow) => { const lookup = qualityIssueVehicleLookup(row); onOpenRealtime?.({ keyword: lookup.key, protocol: row.protocol }); }; const openIssueMileage = (row: QualityIssueRow) => { const lookup = qualityIssueVehicleLookup(row); const dateFrom = issueEvidenceDate(row.lastSeen); const dateTo = nextDate(dateFrom); onOpenMileage?.({ keyword: lookup.key, protocol: row.protocol, ...(dateFrom ? { dateFrom } : {}), ...(dateTo ? { dateTo } : {}) }); }; const copyPriorityDigest = () => { if (priorityRows.length === 0) { Toast.warning('当前没有可复制的优先告警'); return; } copyText(priorityIssueDigestText(priorityRows, notificationPlan?.summary ?? summary, health?.runtime?.platformRelease, policies), '优先队列通知汇总'); }; const copyPolicyRunbook = () => { copyText(notificationPolicyRunbookText(policies, rules, health?.runtime?.platformRelease), '通知策略Runbook'); }; const copyNotificationHandoff = () => { copyText(notificationHandoffText({ rows: priorityRows, summary: notificationPlan?.summary ?? summary, rules, policies, filters, platformRelease: health?.runtime?.platformRelease }), '告警通知交接包'); }; const copyBusinessImpact = () => { copyText(alertBusinessImpactText({ summary: notificationPlan?.summary ?? summary, rules, priorityRows, health, filters, platformRelease: health?.runtime?.platformRelease }), '业务影响报告'); }; const copyAlertCustomerDecision = () => { copyText(alertCustomerDecisionText({ summary: notificationPlan?.summary ?? summary, rules, priorityRows, health, filters, platformRelease: health?.runtime?.platformRelease }), '客户告警决策说明'); }; const copyAlertRecoveryReceipt = () => { copyText(alertRecoveryReceiptText({ summary: notificationPlan?.summary ?? summary, impactLabel: impactSeverity.label, priorityRows, primaryPolicy, filters, platformRelease: health?.runtime?.platformRelease }), '客户告警恢复验收回执'); }; const alertRecoveryAcceptanceItems = [ { label: '实时恢复', value: storageWritable ? '可验证' : '先修复存储', detail: '确认客户能看到车辆实时状态、在线状态和最新协议字段。', color: storageWritable ? 'green' as const : 'red' as const, action: '实时确认', displayAction: '实时验收', onClick: () => { if (onOpenRealtime) { onOpenRealtime(filters); } else { window.location.hash = buildAppHash({ page: 'realtime', filters }); } } }, { label: '轨迹恢复', value: issueVehicleCount > 0 ? '需复核' : '正常', detail: '用受影响车辆回放轨迹,确认定位、速度和时间连续可查。', color: issueVehicleCount > 0 ? 'orange' as const : 'green' as const, action: '轨迹复核', displayAction: '轨迹验收', onClick: () => { if (onOpenHistory) { onOpenHistory(filters); } else { window.location.hash = buildAppHash({ page: 'history', filters }); } } }, { label: '里程恢复', value: primaryIssueType ? qualityIssueLabel(primaryIssueType) : '可核对', detail: '用区间里程和历史证据确认统计结果能解释、能导出、能复盘。', color: primaryIssueType ? 'orange' as const : 'green' as const, action: '里程核对', displayAction: '里程验收', onClick: () => { if (onOpenMileage) { onOpenMileage(filters); } else { window.location.hash = buildAppHash({ page: 'mileage', filters }); } } }, { label: '客户回执', value: primaryPolicy?.target || '责任人待配置', detail: '复制一份包含影响范围、验收标准、证据入口和待复核车辆的恢复回执。', color: primaryPolicy ? 'blue' as const : 'orange' as const, action: '复制回执', displayAction: '回执复制', onClick: copyAlertRecoveryReceipt } ]; const alertActionConclusionItems = [ { label: '影响范围', value: `${issueVehicleCount.toLocaleString()} 辆`, detail: `${issueRecordCount.toLocaleString()} 条告警记录,先判断是否影响客户看车、轨迹和里程。`, color: issueVehicleCount > 0 ? 'orange' as const : 'green' as const, action: '复制影响', onClick: () => copyBusinessImpact() }, { label: '通知策略', value: `P0 ${priorityP0Count.toLocaleString()} / P1 ${priorityP1Count.toLocaleString()}`, detail: priorityRows.length > 0 ? '已进入责任团队通知队列。' : '当前没有待通知车辆。', color: priorityP0Count > 0 ? 'red' as const : priorityP1Count > 0 ? 'orange' as const : 'green' as const, action: '复制通知', onClick: () => copyPriorityDigest() }, { label: 'SLA策略', value: customerDecision.label, detail: primaryPolicy ? `${primaryPolicy.name} / ${formatEscalationMinutes(primaryPolicy.escalationMinutes) || '未配置升级'}` : '需要补充通知策略和升级时限。', color: customerDecision.color, action: '通知闭环', onClick: () => onOpenNotificationRules?.() }, { label: '证据闭环', value: storageWritable ? '可复核' : '先修复存储', detail: '通知必须带上实时、轨迹、里程和历史证据入口。', color: storageWritable ? 'green' as const : 'red' as const, action: '复制交接', onClick: () => copyNotificationHandoff() }, { label: '恢复回执', value: primaryPolicy?.acceptanceCriteria ? '有标准' : '待补充', detail: primaryPolicy?.acceptanceCriteria || '恢复后确认车辆服务状态、轨迹、历史和里程证据可查询。', color: primaryPolicy?.acceptanceCriteria ? 'green' as const : 'orange' as const, action: '复制回执', onClick: () => copyAlertRecoveryReceipt() } ]; const alertCustomerServiceItems = [ { label: '影响车辆', value: `${issueVehicleCount.toLocaleString()} 辆`, detail: `${issueRecordCount.toLocaleString()} 条告警记录,先判断是否影响实时、轨迹和里程。`, color: issueVehicleCount > 0 ? 'orange' as const : 'green' as const, action: '复制影响', disabled: false, onClick: () => copyBusinessImpact() }, { label: '待通知', value: `P0 ${priorityP0Count.toLocaleString()} / P1 ${priorityP1Count.toLocaleString()}`, detail: priorityRows.length > 0 ? '已按优先级生成责任团队通知。' : '当前没有需要通知的告警车辆。', color: priorityP0Count > 0 ? 'red' as const : priorityP1Count > 0 ? 'orange' as const : 'green' as const, action: '复制通知', disabled: priorityRows.length === 0, onClick: () => copyPriorityDigest() }, { label: '焦点车辆', value: focusIssue?.vehicleLabel || '暂无', detail: focusIssue ? `${qualityProtocolLabel(focusIssue.protocol)} / ${qualityIssueLabel(focusIssue.issueType)}。` : '没有焦点车辆时保持监控。', color: focusIssue ? 'blue' as const : 'green' as const, action: '车辆服务', disabled: !focusIssue || !focusLookup?.key, onClick: () => focusIssue && onOpenVehicle(focusLookup?.key ?? '', focusIssue.protocol) }, { label: '证据闭环', value: priorityRows.length > 0 ? '可复核' : '待观察', detail: '通知必须带上车辆服务、实时、轨迹、原始记录和里程证据。', color: priorityRows.length > 0 ? 'blue' as const : 'green' as const, action: '复制交接', disabled: false, onClick: () => copyNotificationHandoff() }, { label: '恢复验收', value: primaryPolicy?.acceptanceCriteria ? '有标准' : '待补充', detail: primaryPolicy?.acceptanceCriteria || '车辆服务状态恢复,实时、轨迹、历史和里程证据可查询。', color: primaryPolicy?.acceptanceCriteria ? 'green' as const : 'orange' as const, action: '复制回执', disabled: false, onClick: () => copyAlertRecoveryReceipt() } ]; const alertClosureTimelineItems = [ { step: '1', label: '告警触发', value: `${issueRecordCount.toLocaleString()} 条事件`, detail: primaryIssueImpact ? `主要问题:${qualityIssueLabel(primaryIssueImpact.name)}` : '等待告警规则命中。', action: '查看事件', color: issueRecordCount > 0 ? 'orange' as const : 'green' as const, onClick: () => loadIssues(filters, pagination.currentPage, pagination.pageSize) }, { step: '2', label: '影响车辆', value: `${issueVehicleCount.toLocaleString()} 辆车`, detail: '先判断实时、轨迹、里程和历史导出是否受影响。', action: '影响摘要', color: issueVehicleCount > 0 ? 'orange' as const : 'green' as const, onClick: copyBusinessImpact }, { step: '3', label: '责任通知', value: primaryPolicy?.target || '责任人待配置', detail: primaryPolicy ? `${primaryPolicy.name} / ${formatEscalationMinutes(primaryPolicy.escalationMinutes) || '未配置升级'}` : '需要补充通知策略和升级时限。', action: '通知策略', color: primaryPolicy ? 'blue' as const : 'orange' as const, onClick: () => onOpenNotificationRules?.() }, { step: '4', label: '证据复核', value: '实时/轨迹/里程', detail: '每次通知都要带上可打开的车辆证据入口和筛选范围。', action: '证据交接', color: 'blue' as const, onClick: copyNotificationHandoff }, { step: '5', label: '恢复验收', value: primaryPolicy?.acceptanceCriteria ? '有标准' : '待补充', detail: primaryPolicy?.acceptanceCriteria || '确认车辆服务恢复,实时、轨迹、历史和里程证据可查。', action: '复制回执', color: primaryPolicy?.acceptanceCriteria ? 'green' as const : 'orange' as const, onClick: copyAlertRecoveryReceipt } ]; return (
告警处置结论栏 0 ? 'red' : priorityP1Count > 0 ? 'orange' : 'green'}> P0 {priorityP0Count.toLocaleString()} / P1 {priorityP1Count.toLocaleString()} 先判断客户受影响车辆,再确认通知、SLA、证据和恢复回执,避免告警停留在技术表格里。
{alertActionConclusionItems.map((item) => ( ))}
客户告警服务台 0 ? 'red' : priorityP1Count > 0 ? 'orange' : 'green'}> P0 {priorityP0Count.toLocaleString()} / P1 {priorityP1Count.toLocaleString()} {storageWritable ? '证据可查' : '存储异常'} 客户先看到哪些车受影响、是否要通知、证据是否齐全、恢复后如何验收。 告警页围绕车辆服务闭环:先看影响,再通知责任团队,最后用实时、轨迹、里程和历史证据证明恢复。
{alertCustomerServiceItems.map((item) => ( ))}
{customerDecision.label} 0 ? 'red' : priorityP1Count > 0 ? 'orange' : 'green'}> P0 {priorityP0Count.toLocaleString()} / P1 {priorityP1Count.toLocaleString()} {primaryPolicy?.name || '策略待配置'} 从告警触发、车辆影响、责任通知、证据复核到恢复验收,形成客户可追踪的闭环。 这条时间线把技术告警翻译成客户处理步骤:每一步都有责任、证据和下一步动作,便于值班、交接和复盘。
{alertClosureTimelineItems.map((item) => ( ))}
{customerDecision.label} 0 ? 'red' : 'green'}>P0 {priorityP0Count.toLocaleString()} {storageWritable ? '存储可写' : '存储异常'} 先判断影响车辆,再通知责任团队并回到车辆证据闭环 客户关心的是哪些车辆受影响、是否影响实时/轨迹/里程、是否需要通知、何时恢复;协议来源只作为证据辅助判断。 {onOpenNotificationRules ? : null}
{[ { label: '影响结论', value: customerDecision.label, detail: customerDecision.detail, color: customerDecision.color, action: '复制决策', disabled: false, onClick: copyAlertCustomerDecision }, { label: '待通知车辆', value: `P0 ${priorityP0Count.toLocaleString()} / P1 ${priorityP1Count.toLocaleString()}`, detail: priorityRows.length > 0 ? '已按车辆生成通知、证据和交接包。' : '当前没有进入优先队列的车辆。', color: priorityP0Count > 0 ? 'red' as const : priorityP1Count > 0 ? 'orange' as const : 'green' as const, action: '通知汇总', disabled: priorityRows.length === 0, onClick: copyPriorityDigest }, { label: '焦点车辆', value: focusIssue?.vehicleLabel || '暂无', detail: focusIssue ? `${qualityProtocolLabel(focusIssue.protocol)} / ${qualityIssueLabel(focusIssue.issueType)} / ${focusIssue.sla}` : '没有待处置车辆时保持监控。', color: focusIssue ? 'blue' as const : 'green' as const, action: '车辆服务', disabled: !focusIssue || !focusLookup?.key, onClick: () => focusIssue && onOpenVehicle(focusLookup?.key ?? '', focusIssue.protocol) }, { label: '主要问题', value: primaryIssueImpact ? qualityIssueLabel(primaryIssueImpact.name) : '-', detail: primaryIssueImpact ? `${primaryIssueImpact.count.toLocaleString()} 条命中,点击筛选该问题。` : '暂无客户侧问题类型。', color: primaryIssueImpact ? 'orange' as const : 'green' as const, action: '筛选问题', disabled: !primaryIssueImpact, onClick: () => primaryIssueImpact && drillIssueType(primaryIssueImpact.name) }, { label: '证据闭环', value: '实时/轨迹/原始/里程', detail: '所有通知都必须回到车辆证据,不只停留在来源或链路指标。', color: 'blue' as const, action: '交接包', disabled: false, onClick: copyNotificationHandoff } ].map((item) => ( ))}
{customerDecision.label} 0 ? 'red' : 'green'}>P0 {priorityP0Count.toLocaleString()} {primaryPolicy?.name || '策略待配置'} 把告警翻译成客户能理解的通知、时限和恢复标准 每一条告警都先回答影响哪些车、通知谁、多久升级、恢复后如何验收;链路、协议和原始记录只作为证据附件。 {onOpenNotificationRules ? : null}
{customerNotificationSlaItems.map((item) => ( ))}
{primaryPolicy?.name || '策略待配置'} 0 ? 'red' : priorityP1Count > 0 ? 'orange' : 'green'}> P0 {priorityP0Count.toLocaleString()} / P1 {priorityP1Count.toLocaleString()} {customerDecision.label} 通知不是结束,客户要看到触达、升级和恢复验收 把站内、邮件、企业微信这些触达方式按客户能理解的处置路径展示:谁收到、为什么收到、多久升级、恢复后怎么验收。 {onOpenNotificationRules ? : null}
{notificationChannelItems.map((item) => ( ))}
{impactSeverity.label} {storageWritable ? '存储可写' : '存储异常'} {primaryPolicy?.target || '责任人待配置'} 告警恢复不是把状态改绿,而是要证明客户能重新看实时、查轨迹、核里程、导出证据,并留下一份恢复回执。 车辆服务恢复必须用客户能验证的页面闭环:实时状态、轨迹回放、里程统计、历史导出都可打开,才算完成恢复验收。
{alertRecoveryAcceptanceItems.map((item) => ( ))}
{impactSeverity.label} 0 ? 'red' : 'green'}>P0 {priorityP0Count.toLocaleString()} 0 ? 'orange' : 'green'}>活跃规则 {activeRuleCount.toLocaleString()} {issueVehicleCount.toLocaleString()} 辆车受影响 告警中心面向客户处置:先判断影响车辆和优先级,再通知责任团队,并用实时、轨迹、原始记录、里程证据闭环。 {onOpenNotificationRules ? : null}
{[ { label: '影响车辆', value: `${issueVehicleCount.toLocaleString()} 辆`, detail: `${issueRecordCount.toLocaleString()} 条告警记录,优先确认车辆服务是否仍可用。`, color: issueVehicleCount > 0 ? 'orange' as const : 'green' as const, onClick: () => undefined, disabled: true }, { label: '优先队列', value: `P0 ${priorityP0Count.toLocaleString()} / P1 ${priorityP1Count.toLocaleString()}`, detail: priorityRows.length > 0 ? '已按优先级生成可复制通知和处置工单。' : '当前没有待通知车辆。', color: priorityP0Count > 0 ? 'red' as const : priorityP1Count > 0 ? 'orange' as const : 'green' as const, onClick: copyPriorityDigest, disabled: priorityRows.length === 0 }, { label: '主要来源', value: primaryProtocolImpact ? qualityProtocolLabel(primaryProtocolImpact.name) : '-', detail: primaryProtocolImpact ? `${primaryProtocolImpact.count.toLocaleString()} 条问题,点击筛选该来源。` : '暂无来源问题。', color: primaryProtocolImpact ? 'blue' as const : 'green' as const, onClick: () => primaryProtocolImpact && drillProtocol(primaryProtocolImpact.name), disabled: !primaryProtocolImpact }, { label: '主要问题', value: primaryIssueImpact ? qualityIssueLabel(primaryIssueImpact.name) : '-', detail: primaryIssueImpact ? `${primaryIssueImpact.count.toLocaleString()} 条命中,点击筛选该问题。` : '暂无问题类型。', color: primaryIssueImpact ? 'orange' as const : 'green' as const, onClick: () => primaryIssueImpact && drillIssueType(primaryIssueImpact.name), disabled: !primaryIssueImpact }, { label: '链路状态', value: storageWritable ? '可写' : '异常', detail: `Kafka Lag ${formatLag(health?.kafkaLag)},容量发现 ${capacityFindingCount.toLocaleString()} 项。`, color: storageWritable && capacityFindingCount === 0 ? 'green' as const : 'red' as const, onClick: loadHealth, disabled: false } ].map((item) => ( ))}
{focusIssue ? focusIssue.priority : '无待处置'} {focusIssue ? qualityIssueLabel(focusIssue.issueType) : '车辆服务正常'} {focusIssue?.vehicleLabel || '暂无优先车辆'} {focusIssue ? `${focusIssue.actionLabel}:${focusIssue.actionDetail || focusIssue.detail || '请结合实时、轨迹、原始记录和里程证据闭环。'}` : '当前没有进入优先队列的车辆,保持实时监控并定期复核告警规则。'}
{[ { label: '实时确认', value: focusIssue ? '看在线' : '待命', detail: '先确认车辆是否仍在线、最后上报来源和实时字段是否恢复。', action: '实时状态', color: focusIssue ? 'blue' as const : 'grey' as const, disabled: !focusIssue || !focusLookup?.key || !onOpenRealtime, onClick: () => focusIssue && openIssueRealtime(focusIssue) }, { label: '轨迹证据', value: focusIssue ? '看前后' : '待命', detail: '按告警发生日期打开轨迹,判断是否影响车辆定位和回放。', action: '轨迹回放', color: focusIssue ? 'green' as const : 'grey' as const, disabled: !focusIssue || !focusLookup?.key || !onOpenHistory, onClick: () => focusIssue && openIssueHistory(focusIssue) }, { label: '里程复核', value: focusIssue ? '核统计' : '待命', detail: '对同一天里程统计与轨迹证据做交叉校验,避免影响 BI。', action: '里程统计', color: focusIssue ? 'orange' as const : 'grey' as const, disabled: !focusIssue || !focusLookup?.key || !onOpenMileage, onClick: () => focusIssue && openIssueMileage(focusIssue) }, { label: '原始证据', value: focusIssue ? '查原始' : '待命', detail: '保留原始帧和解析字段,给平台、客户和研发确认责任边界。', action: '原始记录', color: focusIssue ? 'blue' as const : 'grey' as const, disabled: !focusIssue || !focusLookup?.key || !onOpenRaw, onClick: () => focusIssue && openIssueRaw(focusIssue) } ].map((item) => (
{item.label} {item.value} {item.detail}
))}
{[ { label: '问题车辆', value: issueVehicleCount.toLocaleString() }, { label: '问题记录', value: issueRecordCount.toLocaleString() }, { label: '错误 / 警告', value: `${errorCount}/${warningCount}` }, { label: '主要问题', value: primaryIssueType ? `${qualityIssueLabel(primaryIssueType)} ${summaryIssueTypes[0].count}` : '-', onClick: primaryIssueType ? drillPrimaryIssue : undefined } ].map((item) => ( {item.onClick ? ( ) : ( <>
{item.value}
{item.label}
)}
))}
{formatLag(health?.kafkaLag)} {formatLag(health?.activeConnections)} {formatLag(health?.redisOnlineKeys)} {health?.runtime?.platformRelease || '未标记'} {storageReadStatus(health) === 'pending' ? '检测中' : storageReadStatus(health) === 'ok' ? '正常' : '异常'} 业务影响评估} 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}
))}
告警事件闭环} style={{ marginTop: 16 }} >
{[ { label: '事件触发', value: `${activeRuleCount} 类活跃`, detail: '断链、无来源、VIN 缺失、字段缺失和容量异常进入告警池。' }, { label: '分级通知', value: `${p0RuleCount} 类 P0`, detail: '按错误、警告、关注分层推送给平台、运维和业务责任人。' }, { label: '处置回执', value: `${issueVehicleCount.toLocaleString()} 辆车`, detail: '告警需要关联车辆服务、来源证据和处理结论,形成可追踪闭环。' } ].map((item) => (
{item.label}
{item.value}
{item.detail}
))}
{dispatchOwnerRows.length === 0 ? ( 暂无待分派告警 ) : dispatchOwnerRows.map((item) => ( ))}
处置优先队列 )} style={{ marginTop: 16 }} > loading={loadingIssues} pagination={false} rowKey={(row?: PriorityIssueRow) => `${row?.priority ?? ''}-${row?.protocol ?? ''}-${row?.issueType ?? ''}-${row?.vehicleLabel ?? ''}-${row?.lastSeen ?? ''}`} dataSource={priorityRows} columns={[ { title: '优先级', width: 90, render: (_: unknown, row: PriorityIssueRow) => {row.priority} }, { title: '车辆', width: 210, dataIndex: 'vehicleLabel' }, { title: '问题', width: 140, render: (_: unknown, row: PriorityIssueRow) => qualityIssueLabel(row.issueType) }, { title: '建议动作', width: 150, dataIndex: 'actionLabel' }, { title: 'SLA', width: 130, dataIndex: 'sla' }, { title: '最后时间', width: 170, dataIndex: 'lastSeen' }, { title: '说明', dataIndex: 'detail' }, { title: '操作', width: 360, render: (_: unknown, row: PriorityIssueRow) => { const lookup = qualityIssueVehicleLookup(row); return ( ); } } ]} /> loading={loadingIssues} pagination={false} rowKey={(row?: PriorityIssueRow) => `clock-${row?.priority ?? ''}-${row?.protocol ?? ''}-${row?.issueType ?? ''}-${row?.vehicleLabel ?? ''}-${row?.lastSeen ?? ''}`} dataSource={priorityRows} columns={[ { title: '升级状态', width: 120, render: (_: unknown, row: PriorityIssueRow) => { const clock = escalationClock(row); return {clock.status}; } }, { title: '车辆', width: 210, dataIndex: 'vehicleLabel' }, { title: '优先级', width: 90, render: (_: unknown, row: PriorityIssueRow) => {row.priority} }, { title: '问题', width: 140, render: (_: unknown, row: PriorityIssueRow) => qualityIssueLabel(row.issueType) }, { title: 'SLA', width: 130, dataIndex: 'sla' }, { title: '剩余/超时', width: 150, render: (_: unknown, row: PriorityIssueRow) => escalationClock(row).detail }, { title: '升级截止', width: 210, render: (_: unknown, row: PriorityIssueRow) => escalationClock(row).deadline }, { title: '建议动作', dataIndex: 'actionLabel' } ]} />
告警触发规则{onOpenNotificationRules ? : null}}> pagination={false} dataSource={rules} rowKey="issueType" columns={[ { title: '规则', render: (_: unknown, row: AlertRuleRow) => row.title || (row.issueType === 'CAPACITY_RISK' ? '容量与存储风险' : qualityIssueLabel(row.issueType)) }, { title: '级别', width: 90, render: (_: unknown, row: AlertRuleRow) => {row.level} }, { title: '当前命中', width: 110, render: (_: unknown, row: AlertRuleRow) => row.count.toLocaleString() }, { title: '触发条件', dataIndex: 'trigger' }, { title: 'SLA', dataIndex: 'sla', width: 110 } ]} /> 通知策略} >
{policies.map((item) => (
{item.name} {item.target}
{item.condition}
{item.acceptanceCriteria ?
验收:{item.acceptanceCriteria}
: null}
{formatEscalationMinutes(item.escalationMinutes) ? {formatEscalationMinutes(item.escalationMinutes)} : null} {item.channel}
))}
{health?.capacityFindings?.length ? ( {health.capacityFindings.map((item) => ( {item} ))} ) : null} {filterSummary.length > 0 ? ( {filterSummary.map((item) => ( {item} ))} ) : null} qualityProtocolLabel(row.name) }, { title: '问题数', dataIndex: 'count', width: 120 }, { title: '操作', width: 140, render: (_: unknown, row: { name: string }) => ( ) } ]} />
qualityIssueLabel(row.name) }, { title: '问题数', dataIndex: 'count', width: 120 }, { title: '操作', width: 160, render: (_: unknown, row: { name: string }) => ( ) } ]} />
{ const nextFilters = values as Record; applyFilters(nextFilters); }} style={{ marginBottom: 12 }}> {qualityProtocolOptions.map((item) => ( {item.label} ))} {qualityIssueOptions.map((item) => ( {item.label} ))}
`${row?.protocol ?? ''}-${row?.issueType ?? ''}-${row?.lastSeen ?? ''}-${row?.detail ?? ''}`} dataSource={issues} pagination={{ currentPage: pagination.currentPage, pageSize: pagination.pageSize, total: pagination.total, showSizeChanger: true, onPageChange: (page) => loadIssues(filters, page, pagination.pageSize), onPageSizeChange: (pageSize) => loadIssues(filters, 1, pageSize) }} columns={[ { title: 'VIN', dataIndex: 'vin' }, { title: '车牌', dataIndex: 'plate' }, { title: '手机号', dataIndex: 'phone' }, { title: '来源地址', dataIndex: 'sourceEndpoint' }, { title: '数据来源', render: (_: unknown, row: QualityIssueRow) => qualityProtocolLabel(row.protocol) }, { title: '问题', render: (_: unknown, row: QualityIssueRow) => qualityIssueLabel(row.issueType) }, { title: '级别', render: (_: unknown, row: QualityIssueRow) => {row.severity} }, { title: '最后时间', dataIndex: 'lastSeen' }, { title: '说明', dataIndex: 'detail' }, { title: '处置建议', width: 260, render: (_: unknown, row: QualityIssueRow) => { const action = qualityActionRecommendation(row); return (
{action.label}
{action.detail}
); } }, { title: '操作', width: 480, render: (_: unknown, row: QualityIssueRow) => { const lookup = qualityIssueVehicleLookup(row); return ( ); } } ]} /> 链路健康} style={{ marginTop: 16 }} >
( {row.status} ) }, { title: '说明', dataIndex: 'detail' } ]} /> ); }