feat(platform): add alert policy escalation
This commit is contained in:
@@ -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 {
|
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)
|
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]
|
first := body.Data.PriorityIssues[0]
|
||||||
if first.Priority != "P0" || first.ActionLabel == "" || first.SLA == "" || first.VehicleLabel == "" {
|
if first.Priority != "P0" || first.ActionLabel == "" || first.SLA == "" || first.VehicleLabel == "" {
|
||||||
t.Fatalf("priority issue should be enriched for notification: %+v", first)
|
t.Fatalf("priority issue should be enriched for notification: %+v", first)
|
||||||
|
|||||||
@@ -312,10 +312,12 @@ type QualityAlertRule struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type QualityNotificationPolicy struct {
|
type QualityNotificationPolicy struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Target string `json:"target"`
|
Target string `json:"target"`
|
||||||
Channel string `json:"channel"`
|
Channel string `json:"channel"`
|
||||||
Condition string `json:"condition"`
|
Condition string `json:"condition"`
|
||||||
|
EscalationMinutes int `json:"escalationMinutes"`
|
||||||
|
AcceptanceCriteria string `json:"acceptanceCriteria"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type QualityPriorityIssue struct {
|
type QualityPriorityIssue struct {
|
||||||
@@ -344,10 +346,10 @@ type OpsHealth struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RuntimeInfo struct {
|
type RuntimeInfo struct {
|
||||||
RequestTimeoutMs int `json:"requestTimeoutMs"`
|
RequestTimeoutMs int `json:"requestTimeoutMs"`
|
||||||
AMapWebJSConfigured bool `json:"amapWebJsConfigured"`
|
AMapWebJSConfigured bool `json:"amapWebJsConfigured"`
|
||||||
AMapSecurityProxyEnabled bool `json:"amapSecurityProxyEnabled"`
|
AMapSecurityProxyEnabled bool `json:"amapSecurityProxyEnabled"`
|
||||||
AMapSecurityCodeExposed bool `json:"amapSecurityCodeExposed"`
|
AMapSecurityCodeExposed bool `json:"amapSecurityCodeExposed"`
|
||||||
AMapSecurityServiceHost string `json:"amapSecurityServiceHost"`
|
AMapSecurityServiceHost string `json:"amapSecurityServiceHost"`
|
||||||
PlatformRelease string `json:"platformRelease"`
|
PlatformRelease string `json:"platformRelease"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,9 +116,30 @@ var qualityAlertRuleTemplates = []QualityAlertRule{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var qualityNotificationPolicies = []QualityNotificationPolicy{
|
var qualityNotificationPolicies = []QualityNotificationPolicy{
|
||||||
{Name: "P0 实时中断", Target: "接入运维 + 业务责任人", Channel: "站内告警 / 邮件 / 企业微信", Condition: "无来源、VIN 缺失、存储不可写"},
|
{
|
||||||
{Name: "P1 数据质量", Target: "协议解析 + 数据治理", Channel: "站内告警 / 每日汇总邮件", Condition: "字段缺失、链路间断、容量风险"},
|
Name: "P0 实时中断",
|
||||||
{Name: "P2 趋势关注", Target: "数智中心", Channel: "周报 / 趋势看板", Condition: "单源车辆、档案缺项、来源覆盖下降"},
|
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{
|
var qualityIssuePriorityWeight = map[string]int{
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ test('qualityNotificationPlan reads alert rules and priority issues from backend
|
|||||||
data: {
|
data: {
|
||||||
summary: { issueVehicleCount: 1, issueRecordCount: 1, errorCount: 1, warningCount: 0, protocols: [], issueTypes: [] },
|
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 }],
|
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 告警通知】无数据来源' }],
|
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,
|
activeRuleCount: 1,
|
||||||
p0RuleCount: 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(fetchMock).toHaveBeenCalledWith('/api/quality/notification-plan?issueType=NO_SOURCE', undefined);
|
||||||
expect(result.rules[0].level).toBe('P0');
|
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('告警通知');
|
expect(result.priorityIssues[0].notificationText).toContain('告警通知');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -306,6 +306,8 @@ export interface QualityNotificationPolicy {
|
|||||||
target: string;
|
target: string;
|
||||||
channel: string;
|
channel: string;
|
||||||
condition: string;
|
condition: string;
|
||||||
|
escalationMinutes: number;
|
||||||
|
acceptanceCriteria: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QualityPriorityIssue extends QualityIssueRow {
|
export interface QualityPriorityIssue extends QualityIssueRow {
|
||||||
|
|||||||
@@ -77,19 +77,25 @@ const notificationPolicies = [
|
|||||||
name: 'P0 实时中断',
|
name: 'P0 实时中断',
|
||||||
target: '接入运维 + 业务责任人',
|
target: '接入运维 + 业务责任人',
|
||||||
channel: '站内告警 / 邮件 / 企业微信',
|
channel: '站内告警 / 邮件 / 企业微信',
|
||||||
condition: '无来源、VIN 缺失、存储不可写'
|
condition: '无来源、VIN 缺失、存储不可写',
|
||||||
|
escalationMinutes: 30,
|
||||||
|
acceptanceCriteria: '来源恢复并持续 10 分钟,车辆服务可查到实时与历史证据'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'P1 数据质量',
|
name: 'P1 数据质量',
|
||||||
target: '协议解析 + 数据治理',
|
target: '协议解析 + 数据治理',
|
||||||
channel: '站内告警 / 每日汇总邮件',
|
channel: '站内告警 / 每日汇总邮件',
|
||||||
condition: '字段缺失、链路间断、容量风险'
|
condition: '字段缺失、链路间断、容量风险',
|
||||||
|
escalationMinutes: 120,
|
||||||
|
acceptanceCriteria: '核心字段恢复解析,影响车辆可通过 RAW 与历史证据复核'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'P2 趋势关注',
|
name: 'P2 趋势关注',
|
||||||
target: '数智中心',
|
target: '数智中心',
|
||||||
channel: '周报 / 趋势看板',
|
channel: '周报 / 趋势看板',
|
||||||
condition: '单源车辆、档案缺项、来源覆盖下降'
|
condition: '单源车辆、档案缺项、来源覆盖下降',
|
||||||
|
escalationMinutes: 1440,
|
||||||
|
acceptanceCriteria: '趋势原因已归档,责任人和后续治理动作明确'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -377,6 +383,11 @@ function formatDuration(minutes: number) {
|
|||||||
return rest > 0 ? `${hours} 小时 ${rest} 分钟` : `${hours} 小时`;
|
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) {
|
function parseIssueTime(value?: string) {
|
||||||
const raw = String(value ?? '').trim();
|
const raw = String(value ?? '').trim();
|
||||||
if (!raw) return Number.NaN;
|
if (!raw) return Number.NaN;
|
||||||
@@ -770,8 +781,12 @@ export function Quality({
|
|||||||
<strong>{item.target}</strong>
|
<strong>{item.target}</strong>
|
||||||
</Space>
|
</Space>
|
||||||
<div className="vp-alert-policy-detail">{item.condition}</div>
|
<div className="vp-alert-policy-detail">{item.condition}</div>
|
||||||
|
{item.acceptanceCriteria ? <div className="vp-alert-policy-detail">验收:{item.acceptanceCriteria}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
<Tag color="blue">{item.channel}</Tag>
|
<Space wrap>
|
||||||
|
{formatEscalationMinutes(item.escalationMinutes) ? <Tag color="red">{formatEscalationMinutes(item.escalationMinutes)}</Tag> : null}
|
||||||
|
<Tag color="blue">{item.channel}</Tag>
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2935,6 +2935,8 @@ test('shows alert rule and notification policy workspace on quality page', async
|
|||||||
expect(screen.getByText('P0 实时中断')).toBeInTheDocument();
|
expect(screen.getByText('P0 实时中断')).toBeInTheDocument();
|
||||||
expect(screen.getByText('接入运维 + 业务责任人')).toBeInTheDocument();
|
expect(screen.getByText('接入运维 + 业务责任人')).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 () => {
|
test('shows actionable priority queue on quality page', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user