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: '通知解析负责人核对字段映射和 RAW 样本', sla: '当日闭环' } ]; const notificationPolicies = [ { name: 'P0 实时中断', target: '接入运维 + 业务责任人', channel: '站内告警 / 邮件 / 企业微信', condition: '无来源、VIN 缺失、存储不可写', escalationMinutes: 30, acceptanceCriteria: '来源恢复并持续 10 分钟,车辆服务可查到实时与历史证据' }, { name: 'P1 数据质量', target: '协议解析 + 数据治理', channel: '站内告警 / 每日汇总邮件', condition: '字段缺失、链路间断、容量风险', escalationMinutes: 120, acceptanceCriteria: '核心字段恢复解析,影响车辆可通过 RAW 与历史证据复核' }, { 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: '字段缺失会影响统计和展示,优先核对解析映射和原始 RAW。' }; } return { label: '查看车辆服务', color: 'blue' as const, detail: '进入车辆服务详情,结合来源证据继续排查。' }; } function issueCount(summary: QualitySummary, issueType: string) { return summary.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 }))}`, `RAW证据:${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,实时/历史/RAW 证据可通过同一车辆服务查询'; } if (row.issueType === 'NO_SOURCE') { return '至少一个生产来源恢复在线,车辆实时状态、历史轨迹和 RAW 证据可查询'; } 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. 核对实时定位、轨迹证据和 RAW 证据', '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 }))}`, `RAW证据:${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 }))}`, `RAW证据:${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 }))}`, ` RAW:${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}` : '统一验收:车辆服务状态恢复,实时、轨迹、RAW 证据可查询', '', '待通知车辆:', ...issueLines, '', `告警事件:${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) { 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 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 }), '告警通知交接包'); }; return (
{[ { 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 }} >
{[ { 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' } ]} /> ); }