diff --git a/vehicle-data-platform/apps/api/internal/platform/handler_test.go b/vehicle-data-platform/apps/api/internal/platform/handler_test.go index 8298c4ab..c30c30f9 100644 --- a/vehicle-data-platform/apps/api/internal/platform/handler_test.go +++ b/vehicle-data-platform/apps/api/internal/platform/handler_test.go @@ -879,6 +879,10 @@ func TestHandlerQualityNotificationPlan(t *testing.T) { if len(body.Data.Rules) == 0 || len(body.Data.Policies) == 0 || len(body.Data.PriorityIssues) == 0 { t.Fatalf("notification plan should include rules, policies and priority issues: %+v", body.Data) } + firstPolicy := body.Data.Policies[0] + if firstPolicy.EscalationMinutes <= 0 || firstPolicy.AcceptanceCriteria == "" { + t.Fatalf("notification policies should expose escalation minutes and acceptance criteria: %+v", firstPolicy) + } first := body.Data.PriorityIssues[0] if first.Priority != "P0" || first.ActionLabel == "" || first.SLA == "" || first.VehicleLabel == "" { t.Fatalf("priority issue should be enriched for notification: %+v", first) diff --git a/vehicle-data-platform/apps/api/internal/platform/model.go b/vehicle-data-platform/apps/api/internal/platform/model.go index 5ae53904..238dc627 100644 --- a/vehicle-data-platform/apps/api/internal/platform/model.go +++ b/vehicle-data-platform/apps/api/internal/platform/model.go @@ -312,10 +312,12 @@ type QualityAlertRule struct { } type QualityNotificationPolicy struct { - Name string `json:"name"` - Target string `json:"target"` - Channel string `json:"channel"` - Condition string `json:"condition"` + Name string `json:"name"` + Target string `json:"target"` + Channel string `json:"channel"` + Condition string `json:"condition"` + EscalationMinutes int `json:"escalationMinutes"` + AcceptanceCriteria string `json:"acceptanceCriteria"` } type QualityPriorityIssue struct { @@ -344,10 +346,10 @@ type OpsHealth struct { } type RuntimeInfo struct { - RequestTimeoutMs int `json:"requestTimeoutMs"` - AMapWebJSConfigured bool `json:"amapWebJsConfigured"` - AMapSecurityProxyEnabled bool `json:"amapSecurityProxyEnabled"` - AMapSecurityCodeExposed bool `json:"amapSecurityCodeExposed"` - AMapSecurityServiceHost string `json:"amapSecurityServiceHost"` - PlatformRelease string `json:"platformRelease"` + RequestTimeoutMs int `json:"requestTimeoutMs"` + AMapWebJSConfigured bool `json:"amapWebJsConfigured"` + AMapSecurityProxyEnabled bool `json:"amapSecurityProxyEnabled"` + AMapSecurityCodeExposed bool `json:"amapSecurityCodeExposed"` + AMapSecurityServiceHost string `json:"amapSecurityServiceHost"` + PlatformRelease string `json:"platformRelease"` } diff --git a/vehicle-data-platform/apps/api/internal/platform/service.go b/vehicle-data-platform/apps/api/internal/platform/service.go index ecf17c25..24524900 100644 --- a/vehicle-data-platform/apps/api/internal/platform/service.go +++ b/vehicle-data-platform/apps/api/internal/platform/service.go @@ -116,9 +116,30 @@ var qualityAlertRuleTemplates = []QualityAlertRule{ } var qualityNotificationPolicies = []QualityNotificationPolicy{ - {Name: "P0 实时中断", Target: "接入运维 + 业务责任人", Channel: "站内告警 / 邮件 / 企业微信", Condition: "无来源、VIN 缺失、存储不可写"}, - {Name: "P1 数据质量", Target: "协议解析 + 数据治理", Channel: "站内告警 / 每日汇总邮件", Condition: "字段缺失、链路间断、容量风险"}, - {Name: "P2 趋势关注", Target: "数智中心", Channel: "周报 / 趋势看板", Condition: "单源车辆、档案缺项、来源覆盖下降"}, + { + 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: "趋势原因已归档,责任人和后续治理动作明确", + }, } var qualityIssuePriorityWeight = map[string]int{ diff --git a/vehicle-data-platform/apps/web/src/api/client.test.ts b/vehicle-data-platform/apps/web/src/api/client.test.ts index 77020cea..88ef428f 100644 --- a/vehicle-data-platform/apps/web/src/api/client.test.ts +++ b/vehicle-data-platform/apps/web/src/api/client.test.ts @@ -179,7 +179,7 @@ test('qualityNotificationPlan reads alert rules and priority issues from backend data: { summary: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 1, warningCount: 0, protocols: [], issueTypes: [] }, rules: [{ issueType: 'NO_SOURCE', title: '无数据来源', level: 'P0', owner: '平台接入', trigger: '无来源', notify: '立即通知', sla: '30 分钟确认', count: 1 }], - policies: [{ name: 'P0 实时中断', target: '接入运维', channel: '邮件', condition: '无来源' }], + policies: [{ name: 'P0 实时中断', target: '接入运维', channel: '邮件', condition: '无来源', escalationMinutes: 30, acceptanceCriteria: '来源恢复并持续 10 分钟' }], priorityIssues: [{ vin: 'VIN001', plate: '粤A001', phone: '', sourceEndpoint: '', protocol: 'VEHICLE_SERVICE', issueType: 'NO_SOURCE', severity: 'error', lastSeen: '', detail: '无来源', priority: 'P0', actionLabel: '确认平台转发', actionDetail: '确认平台转发', sla: '30 分钟确认', vehicleLabel: '粤A001 / VIN001', realtimeHash: '#/realtime?keyword=VIN001', historyHash: '#/history?keyword=VIN001', rawHash: '#/history?keyword=VIN001&tab=raw', vehicleHash: '#/detail?keyword=VIN001', notificationText: '【P0 告警通知】无数据来源' }], activeRuleCount: 1, p0RuleCount: 1 @@ -193,6 +193,8 @@ test('qualityNotificationPlan reads alert rules and priority issues from backend expect(fetchMock).toHaveBeenCalledWith('/api/quality/notification-plan?issueType=NO_SOURCE', undefined); expect(result.rules[0].level).toBe('P0'); + expect(result.policies[0].escalationMinutes).toBe(30); + expect(result.policies[0].acceptanceCriteria).toContain('来源恢复'); expect(result.priorityIssues[0].notificationText).toContain('告警通知'); }); diff --git a/vehicle-data-platform/apps/web/src/api/types.ts b/vehicle-data-platform/apps/web/src/api/types.ts index 1886f803..5c609855 100644 --- a/vehicle-data-platform/apps/web/src/api/types.ts +++ b/vehicle-data-platform/apps/web/src/api/types.ts @@ -306,6 +306,8 @@ export interface QualityNotificationPolicy { target: string; channel: string; condition: string; + escalationMinutes: number; + acceptanceCriteria: string; } export interface QualityPriorityIssue extends QualityIssueRow { diff --git a/vehicle-data-platform/apps/web/src/pages/Quality.tsx b/vehicle-data-platform/apps/web/src/pages/Quality.tsx index 2fb2072a..adcc7e6f 100644 --- a/vehicle-data-platform/apps/web/src/pages/Quality.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Quality.tsx @@ -77,19 +77,25 @@ const notificationPolicies = [ name: 'P0 实时中断', target: '接入运维 + 业务责任人', channel: '站内告警 / 邮件 / 企业微信', - condition: '无来源、VIN 缺失、存储不可写' + condition: '无来源、VIN 缺失、存储不可写', + escalationMinutes: 30, + acceptanceCriteria: '来源恢复并持续 10 分钟,车辆服务可查到实时与历史证据' }, { name: 'P1 数据质量', target: '协议解析 + 数据治理', channel: '站内告警 / 每日汇总邮件', - condition: '字段缺失、链路间断、容量风险' + condition: '字段缺失、链路间断、容量风险', + escalationMinutes: 120, + acceptanceCriteria: '核心字段恢复解析,影响车辆可通过 RAW 与历史证据复核' }, { name: 'P2 趋势关注', target: '数智中心', channel: '周报 / 趋势看板', - condition: '单源车辆、档案缺项、来源覆盖下降' + condition: '单源车辆、档案缺项、来源覆盖下降', + escalationMinutes: 1440, + acceptanceCriteria: '趋势原因已归档,责任人和后续治理动作明确' } ]; @@ -377,6 +383,11 @@ function formatDuration(minutes: number) { 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; @@ -770,8 +781,12 @@ export function Quality({ {item.target}
{item.condition}
+ {item.acceptanceCriteria ?
验收:{item.acceptanceCriteria}
: null} - {item.channel} + + {formatEscalationMinutes(item.escalationMinutes) ? {formatEscalationMinutes(item.escalationMinutes)} : null} + {item.channel} + ))} diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx index 3325ade5..467f30bd 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -2935,6 +2935,8 @@ test('shows alert rule and notification policy workspace on quality page', async expect(screen.getByText('P0 实时中断')).toBeInTheDocument(); expect(screen.getByText('接入运维 + 业务责任人')).toBeInTheDocument(); expect(screen.getByText('站内告警 / 邮件 / 企业微信')).toBeInTheDocument(); + expect(screen.getByText('30 分钟升级')).toBeInTheDocument(); + expect(screen.getByText('验收:来源恢复并持续 10 分钟,车辆服务可查到实时与历史证据')).toBeInTheDocument(); }); test('shows actionable priority queue on quality page', async () => {