diff --git a/vehicle-data-platform/apps/web/src/pages/OpsQuality.tsx b/vehicle-data-platform/apps/web/src/pages/OpsQuality.tsx index 15421625..37bc2873 100644 --- a/vehicle-data-platform/apps/web/src/pages/OpsQuality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/OpsQuality.tsx @@ -57,6 +57,16 @@ type CapacityMetricRow = { detail: string; }; +type OpsActionItem = { + key: string; + title: string; + level: 'P0' | 'P1' | 'P2'; + owner: string; + evidence: string; + action: string; + acceptance: string; +}; + const emptyCapacityMetrics: CapacityMetrics = { activeConnections: 0, kafkaLag: 0, @@ -256,6 +266,124 @@ function opsCapacityHandoffText(health: OpsHealth | null) { ].join('\n'); } +function opsActionItems(health: OpsHealth | null): OpsActionItem[] { + const metrics = getCapacityMetrics(health); + const actions: OpsActionItem[] = []; + + if (metrics.bridgeAckPending > 0 || metrics.bridgeConsumerPending > 10000) { + actions.push({ + key: 'nats-kafka-bridge', + title: '稳定 NATS 到 Kafka 桥接', + level: metrics.bridgeAckPending > 100 ? 'P0' : 'P1', + owner: '接入网关 / 消息队列', + evidence: `ACK ${formatNumber(metrics.bridgeAckPending)},待消费 ${formatNumber(metrics.bridgeConsumerPending)}`, + action: '检查 Kafka broker、topic 写入、NATS bridge 日志和服务器磁盘;必要时临时扩容桥接消费者。', + acceptance: 'ACK pending 回到 0,consumer pending 连续下降,Kafka Lag 不再增长。' + }); + } + + if (metrics.fastWriterAckPending > 0 || metrics.fastWriterConsumerPending > 10000) { + actions.push({ + key: 'fast-writer', + title: '保护实时写入通道', + level: metrics.fastWriterAckPending > 10 ? 'P0' : 'P1', + owner: '实时投影 / 存储', + evidence: `ACK ${formatNumber(metrics.fastWriterAckPending)},待消费 ${formatNumber(metrics.fastWriterConsumerPending)}`, + action: '优先确认 Redis 在线投影和 TDengine 快写是否正常,再看字段扁平化写入耗时。', + acceptance: '实时 KV 延迟恢复到秒级内,在线 TTL 正常续期,快速写入 pending 清零。' + }); + } + + if (metrics.historyBatchPending > 0 || metrics.historyRowsPending > 0 || metrics.kafkaLag > 0) { + actions.push({ + key: 'history-consumer', + title: '疏通历史与统计消费', + level: metrics.kafkaLag > 1000 || metrics.historyRowsPending > 10000 ? 'P0' : 'P1', + owner: '历史消费 / 统计', + evidence: `Kafka Lag ${formatNumber(metrics.kafkaLag)},历史待写 ${formatNumber(metrics.historyRowsPending)} 行`, + action: '查看 Kafka 消费者组、TDengine 批量写入错误和统计任务耗时;恢复后补看轨迹、RAW、里程统计。', + acceptance: 'Lag 持续归零,历史待写行归零,按 VIN 的历史查询恢复毫秒到百毫秒级。' + }); + } + + if (metrics.activeConnections > 100000) { + actions.push({ + key: 'gateway-capacity', + title: '扩容接入网关容量', + level: 'P1', + owner: '网关 / ECS', + evidence: `活跃连接 ${formatNumber(metrics.activeConnections)}`, + action: '检查 FD、CPU、网卡、端口 accept backlog 和协议源分布;准备按协议拆分实例。', + acceptance: '连接数增长时 CPU 和内存稳定,登录、注册、位置、实时帧响应无超时。' + }); + } + + if (health && (!health.tdengineWritable || !health.mysqlWritable)) { + actions.push({ + key: 'storage-writable', + title: '恢复核心存储可写', + level: 'P0', + owner: '数据库 / 应用', + evidence: `TDengine ${statusText(health.tdengineWritable)},MySQL ${statusText(health.mysqlWritable)}`, + action: '先确认网络和账号,再看连接池、慢写、磁盘和表结构变更;避免实时链路被关系库写入拖慢。', + acceptance: '健康检查全部恢复正常,RAW、location、snapshot 和身份表写入成功。' + }); + } + + if (health && (health.activeConnections ?? 0) > 0 && (health.redisOnlineKeys ?? 0) === 0) { + actions.push({ + key: 'redis-online', + title: '校验 Redis 在线投影', + level: 'P1', + owner: '实时投影', + evidence: `活跃连接 ${formatNumber(health.activeConnections)},在线 Key ${formatNumber(health.redisOnlineKeys)}`, + action: '确认只有实时数据帧进入 Redis,检查 online TTL 写入、VIN 解析和协议过滤条件。', + acceptance: '按协议、VIN 的在线状态可查,离线时间统计有连续依据。' + }); + } + + if (health?.runtime?.amapWebJsConfigured && !health.runtime.amapSecurityProxyEnabled) { + actions.push({ + key: 'amap-security', + title: '收敛地图密钥暴露风险', + level: health.runtime.amapSecurityCodeExposed ? 'P1' : 'P2', + owner: '前端 / 平台 API', + evidence: amapSecurityText(health), + action: '启用高德安全代理,前端只保留 Web JS Key,服务端负责逆地理和安全密钥。', + acceptance: '轨迹回放和实时地图可用,浏览器端不暴露安全密钥。' + }); + } + + if (!actions.length) { + actions.push({ + key: 'steady-watch', + title: '维持生产容量巡检', + level: 'P2', + owner: '值班 / 平台', + evidence: '当前未发现积压或核心写入异常', + action: '继续观察 32960、808、MQTT 三类实时数据源、在线状态、地图服务和历史查询耗时。', + acceptance: '10W 车辆接入准备项持续无红色风险,实时投影和历史消费无积压。' + }); + } + + return actions; +} + +function opsCapacityActionPlanText(health: OpsHealth | null) { + const actions = opsActionItems(health); + return [ + '【车辆数据中台容量处置计划】', + `生成版本:${health?.runtime?.platformRelease || '-'}`, + ...actions.map((item, index) => [ + `${index + 1}. ${item.level} ${item.title}`, + `负责人:${item.owner}`, + `证据:${item.evidence}`, + `动作:${item.action}`, + `验收:${item.acceptance}` + ].join('\n')) + ].join('\n\n'); +} + async function copyText(value: string, label: string) { try { await navigator.clipboard.writeText(value); @@ -285,6 +413,7 @@ export function OpsQuality() { const capacityRisks = (health?.capacityFindings ?? []).map(capacityRisk); const capacityRows = capacityMetricRows(health); const bridgeRisks = capacityRisks.filter((item) => item.category === 'NATS -> Kafka'); + const actionItems = opsActionItems(health); return (