307 lines
14 KiB
Go
307 lines
14 KiB
Go
package platform
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func (m *MockStore) seedAlertCenter() {
|
|
now := time.Now()
|
|
boolTrue := true
|
|
m.alertRules = []AlertRule{
|
|
{ID: "rule-speeding", Name: "持续超速告警", Description: "速度持续高于阈值", Severity: "critical", ValueType: "numeric", Metric: "speed_kmh", Operator: "gt", Threshold: 80, DurationSec: 60, RecoveryOperator: "lte", RecoveryThreshold: 75, RepeatIntervalSec: 600, ScopeProtocols: []string{"JT808", "GB32960"}, ScopeVINs: []string{}, NotificationChannels: []string{"in_app"}, Enabled: true, Version: 2, CreatedBy: "system", UpdatedBy: "platform-admin", CreatedAt: now.AddDate(0, -2, 0).Format(time.RFC3339), UpdatedAt: now.Add(-48 * time.Hour).Format(time.RFC3339)},
|
|
{ID: "rule-offline", Name: "离线超时", Description: "车辆超过阈值未上报", Severity: "major", ValueType: "numeric", Metric: "freshness_sec", Operator: "gt", Threshold: 3600, DurationSec: 0, RecoveryOperator: "lte", RecoveryThreshold: 300, RepeatIntervalSec: 3600, ScopeProtocols: []string{"JT808", "GB32960", "YUTONG_MQTT"}, ScopeVINs: []string{}, NotificationChannels: []string{"in_app"}, Enabled: true, Version: 1, CreatedBy: "system", UpdatedBy: "system", CreatedAt: now.AddDate(0, -3, 0).Format(time.RFC3339), UpdatedAt: now.AddDate(0, -3, 0).Format(time.RFC3339)},
|
|
{ID: "rule-alarm", Name: "协议告警位", Description: "原始协议告警位非零", Severity: "major", ValueType: "boolean", Metric: "alarm_active", Operator: "eq", BooleanThreshold: &boolTrue, DurationSec: 0, RecoveryOperator: "eq", RecoveryThreshold: 0, RepeatIntervalSec: 300, ScopeProtocols: []string{"JT808"}, ScopeVINs: []string{}, NotificationChannels: []string{"in_app"}, Enabled: false, Version: 1, CreatedBy: "platform-admin", UpdatedBy: "platform-admin", CreatedAt: now.Add(-72 * time.Hour).Format(time.RFC3339), UpdatedAt: now.Add(-72 * time.Hour).Format(time.RFC3339)},
|
|
}
|
|
locations := []string{"广东省深圳市南山区科技南路", "广东省广州市白云区机场高速", "广东省东莞市南城街道", "广东省佛山市顺德区伦教街道", "上海市临港新片区", "四川省成都市高新区"}
|
|
statuses := []string{"unprocessed", "unprocessed", "processing", "recovered", "closed", "ignored"}
|
|
severities := []string{"critical", "critical", "major", "major", "minor", "minor"}
|
|
plates := []string{"粤AG18312", "粤B7C526", "粤C9D872", "粤E6P987", "豫A88888", "川AHTWO1"}
|
|
vins := []string{"LB9A32A24R0LS1426", "LFP23A98V2P012345", "LS5A3A5E8N0123456", "LJ12BA3R1N0456789", "LMRKH9AC2R1004087", "LNXNEGRR7SR318212"}
|
|
for i := range statuses {
|
|
triggered := now.Add(-time.Duration(8+i*11) * time.Minute)
|
|
event := AlertEvent{ID: "alert-demo-" + string(rune('1'+i)), RuleID: "rule-speeding", RuleName: "持续超速告警", RuleVersion: 2, Severity: severities[i], Status: statuses[i], VIN: vins[i], Plate: plates[i], Protocol: []string{"JT808", "JT808", "JT808", "GB32960", "YUTONG_MQTT", "GB32960"}[i], Metric: "speed_kmh", Operator: "gt", TriggerValue: 96 - float64(i*3), Threshold: 80, Unit: "km/h", DurationSec: 60, Location: locations[i], SourceEventID: "source-event-00" + string(rune('1'+i)), EventAt: triggered.Add(-time.Second).Format(time.RFC3339), ReceivedAt: triggered.Format(time.RFC3339), TriggeredAt: triggered.Format(time.RFC3339), Version: 1}
|
|
if statuses[i] == "processing" {
|
|
event.Handler = "张三"
|
|
}
|
|
if statuses[i] == "recovered" || statuses[i] == "closed" || statuses[i] == "ignored" {
|
|
event.RecoveredAt = triggered.Add(5 * time.Minute).Format(time.RFC3339)
|
|
}
|
|
event.Actions = []AlertAction{{ID: int64(i + 1), Action: "trigger", ToStatus: "unprocessed", Actor: "alert-evaluator", Note: "规则命中并达到持续时间", CreatedAt: triggered.Format(time.RFC3339)}}
|
|
if statuses[i] != "unprocessed" {
|
|
event.Actions = append(event.Actions, AlertAction{ID: int64(20 + i), Action: statuses[i], FromStatus: "unprocessed", ToStatus: statuses[i], Actor: firstNonEmpty(event.Handler, "platform-admin"), CreatedAt: triggered.Add(time.Minute).Format(time.RFC3339)})
|
|
}
|
|
m.alertEvents = append(m.alertEvents, event)
|
|
}
|
|
m.nextAlertActionID = 100
|
|
for i := 0; i < 9; i++ {
|
|
m.alertNotifications = append(m.alertNotifications, AlertNotification{ID: int64(i + 1), EventID: m.alertEvents[i%len(m.alertEvents)].ID, Title: m.alertEvents[i%len(m.alertEvents)].RuleName, Content: plates[i%len(plates)] + " 触发告警,请及时处理", Severity: severities[i%len(severities)], Channel: "in_app", Read: i >= 7, CreatedAt: now.Add(-time.Duration(i+1) * time.Minute).Format(time.RFC3339)})
|
|
}
|
|
m.nextNotificationID = 10
|
|
}
|
|
|
|
func (m *MockStore) AlertSummary(_ context.Context, query AlertQuery) (AlertSummary, error) {
|
|
m.alertMu.RLock()
|
|
defer m.alertMu.RUnlock()
|
|
result := AlertSummary{AsOf: time.Now().Format(time.RFC3339)}
|
|
for _, event := range m.alertEvents {
|
|
if !keepMockAlertEvent(event, query) {
|
|
continue
|
|
}
|
|
switch event.Status {
|
|
case "unprocessed":
|
|
result.Unprocessed++
|
|
result.Active++
|
|
case "processing":
|
|
result.Processing++
|
|
result.Active++
|
|
case "recovered":
|
|
result.Recovered++
|
|
case "closed":
|
|
result.Closed++
|
|
case "ignored":
|
|
result.Ignored++
|
|
}
|
|
}
|
|
for _, item := range m.alertNotifications {
|
|
if !item.Read {
|
|
result.UnreadNotifications++
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (m *MockStore) ActiveAlertVINs(_ context.Context, protocol string) ([]string, error) {
|
|
m.alertMu.RLock()
|
|
defer m.alertMu.RUnlock()
|
|
seen := map[string]struct{}{}
|
|
vins := make([]string, 0)
|
|
for _, event := range m.alertEvents {
|
|
if event.Status != "unprocessed" && event.Status != "processing" {
|
|
continue
|
|
}
|
|
if protocol != "" && event.Protocol != protocol {
|
|
continue
|
|
}
|
|
if _, exists := seen[event.VIN]; exists {
|
|
continue
|
|
}
|
|
seen[event.VIN] = struct{}{}
|
|
vins = append(vins, event.VIN)
|
|
}
|
|
return vins, nil
|
|
}
|
|
|
|
func (m *MockStore) AlertEvents(_ context.Context, query AlertQuery) (Page[AlertEvent], error) {
|
|
m.alertMu.RLock()
|
|
defer m.alertMu.RUnlock()
|
|
items := make([]AlertEvent, 0, len(m.alertEvents))
|
|
for _, event := range m.alertEvents {
|
|
if keepMockAlertEvent(event, query) {
|
|
event.Actions = nil
|
|
items = append(items, event)
|
|
}
|
|
}
|
|
sort.SliceStable(items, func(i, j int) bool { return items[i].TriggeredAt > items[j].TriggeredAt })
|
|
total := len(items)
|
|
start := query.Offset
|
|
if start > total {
|
|
start = total
|
|
}
|
|
end := start + query.Limit
|
|
if end > total {
|
|
end = total
|
|
}
|
|
return Page[AlertEvent]{Items: append([]AlertEvent(nil), items[start:end]...), Total: total, Limit: query.Limit, Offset: query.Offset}, nil
|
|
}
|
|
|
|
func keepMockAlertEvent(event AlertEvent, query AlertQuery) bool {
|
|
keyword := strings.ToLower(query.Keyword)
|
|
if keyword != "" && !strings.Contains(strings.ToLower(event.VIN), keyword) && !strings.Contains(strings.ToLower(event.Plate), keyword) && !strings.Contains(strings.ToLower(event.RuleName), keyword) {
|
|
return false
|
|
}
|
|
if query.Severity != "" && query.Severity != "all" && event.Severity != query.Severity {
|
|
return false
|
|
}
|
|
if query.Status == "active" && event.Status != "unprocessed" && event.Status != "processing" {
|
|
return false
|
|
}
|
|
if query.Status != "" && query.Status != "all" && query.Status != "active" && event.Status != query.Status {
|
|
return false
|
|
}
|
|
if query.RuleID != "" && event.RuleID != query.RuleID {
|
|
return false
|
|
}
|
|
if query.Protocol != "" && !strings.EqualFold(event.Protocol, query.Protocol) {
|
|
return false
|
|
}
|
|
if query.DateFrom != "" && event.TriggeredAt < query.DateFrom {
|
|
return false
|
|
}
|
|
if query.DateTo != "" && event.TriggeredAt > query.DateTo {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (m *MockStore) AlertEvent(_ context.Context, id string) (AlertEvent, error) {
|
|
m.alertMu.RLock()
|
|
defer m.alertMu.RUnlock()
|
|
for _, event := range m.alertEvents {
|
|
if event.ID == id {
|
|
event.Actions = append([]AlertAction(nil), event.Actions...)
|
|
return event, nil
|
|
}
|
|
}
|
|
return AlertEvent{}, clientError{Code: "ALERT_EVENT_NOT_FOUND", Message: "告警事件不存在"}
|
|
}
|
|
|
|
func (m *MockStore) AlertRules(context.Context) ([]AlertRule, error) {
|
|
m.alertMu.RLock()
|
|
defer m.alertMu.RUnlock()
|
|
return append([]AlertRule(nil), m.alertRules...), nil
|
|
}
|
|
|
|
func (m *MockStore) SaveAlertRule(_ context.Context, input AlertRuleInput) (AlertRule, error) {
|
|
m.alertMu.Lock()
|
|
defer m.alertMu.Unlock()
|
|
now := time.Now().Format(time.RFC3339)
|
|
for i, current := range m.alertRules {
|
|
if current.ID != input.ID {
|
|
continue
|
|
}
|
|
if input.Version != current.Version {
|
|
return AlertRule{}, clientError{Code: "ALERT_RULE_VERSION_CONFLICT", Message: "规则已被其他用户更新,请刷新后重试"}
|
|
}
|
|
next := ruleFromInput(input)
|
|
next.Version = current.Version + 1
|
|
next.CreatedAt = current.CreatedAt
|
|
next.CreatedBy = current.CreatedBy
|
|
next.UpdatedAt = now
|
|
next.UpdatedBy = input.Actor
|
|
m.alertRules[i] = next
|
|
return next, nil
|
|
}
|
|
if input.Version != 0 {
|
|
return AlertRule{}, clientError{Code: "ALERT_RULE_NOT_FOUND", Message: "待更新规则不存在"}
|
|
}
|
|
next := ruleFromInput(input)
|
|
next.Version = 1
|
|
next.CreatedAt = now
|
|
next.UpdatedAt = now
|
|
next.CreatedBy = input.Actor
|
|
next.UpdatedBy = input.Actor
|
|
m.alertRules = append(m.alertRules, next)
|
|
return next, nil
|
|
}
|
|
|
|
func ruleFromInput(input AlertRuleInput) AlertRule {
|
|
return AlertRule{ID: input.ID, Name: input.Name, Description: input.Description, Severity: strings.ToLower(input.Severity), ValueType: strings.ToLower(input.ValueType), Metric: input.Metric, Operator: strings.ToLower(input.Operator), Threshold: input.Threshold, ThresholdHigh: input.ThresholdHigh, BooleanThreshold: input.BooleanThreshold, DurationSec: input.DurationSec, RecoveryOperator: strings.ToLower(input.RecoveryOperator), RecoveryThreshold: input.RecoveryThreshold, RepeatIntervalSec: input.RepeatIntervalSec, ScopeProtocols: append([]string(nil), input.ScopeProtocols...), ScopeVINs: append([]string(nil), input.ScopeVINs...), ScopeOEMs: append([]string(nil), input.ScopeOEMs...), ScopeModels: append([]string(nil), input.ScopeModels...), ScopeCompanies: append([]string(nil), input.ScopeCompanies...), NotificationChannels: append([]string(nil), input.NotificationChannels...), Enabled: input.Enabled}
|
|
}
|
|
|
|
func (m *MockStore) SetAlertRuleEnabled(_ context.Context, id string, update AlertRuleEnabledUpdate) (AlertRule, error) {
|
|
m.alertMu.Lock()
|
|
defer m.alertMu.Unlock()
|
|
for i := range m.alertRules {
|
|
if m.alertRules[i].ID == id {
|
|
if m.alertRules[i].Version != update.Version {
|
|
return AlertRule{}, clientError{Code: "ALERT_RULE_VERSION_CONFLICT", Message: "规则已被其他用户更新,请刷新后重试"}
|
|
}
|
|
m.alertRules[i].Enabled = update.Enabled
|
|
m.alertRules[i].Version++
|
|
m.alertRules[i].UpdatedBy = update.Actor
|
|
m.alertRules[i].UpdatedAt = time.Now().Format(time.RFC3339)
|
|
return m.alertRules[i], nil
|
|
}
|
|
}
|
|
return AlertRule{}, clientError{Code: "ALERT_RULE_NOT_FOUND", Message: "规则不存在"}
|
|
}
|
|
|
|
func (m *MockStore) ActOnAlert(_ context.Context, id string, request AlertActionRequest) (AlertEvent, error) {
|
|
m.alertMu.Lock()
|
|
defer m.alertMu.Unlock()
|
|
for i := range m.alertEvents {
|
|
event := &m.alertEvents[i]
|
|
if event.ID != id {
|
|
continue
|
|
}
|
|
if event.Version != request.Version {
|
|
return AlertEvent{}, clientError{Code: "ALERT_EVENT_VERSION_CONFLICT", Message: "事件已被其他用户更新,请刷新后重试"}
|
|
}
|
|
from := event.Status
|
|
to := from
|
|
switch request.Action {
|
|
case "acknowledge":
|
|
if from != "unprocessed" {
|
|
return AlertEvent{}, clientError{Code: "ALERT_ACTION_NOT_ALLOWED", Message: "只有未处理告警可以确认"}
|
|
}
|
|
to = "processing"
|
|
event.Handler = request.Actor
|
|
case "close":
|
|
if from != "unprocessed" && from != "processing" && from != "recovered" {
|
|
return AlertEvent{}, clientError{Code: "ALERT_ACTION_NOT_ALLOWED", Message: "当前状态不能关闭"}
|
|
}
|
|
to = "closed"
|
|
event.Handler = request.Actor
|
|
case "ignore":
|
|
if from != "unprocessed" && from != "processing" {
|
|
return AlertEvent{}, clientError{Code: "ALERT_ACTION_NOT_ALLOWED", Message: "当前状态不能忽略"}
|
|
}
|
|
to = "ignored"
|
|
event.Handler = request.Actor
|
|
}
|
|
m.nextAlertActionID++
|
|
event.Actions = append(event.Actions, AlertAction{ID: m.nextAlertActionID, Action: request.Action, FromStatus: from, ToStatus: to, Actor: request.Actor, Note: request.Note, CreatedAt: time.Now().Format(time.RFC3339)})
|
|
event.Status = to
|
|
event.Version++
|
|
return *event, nil
|
|
}
|
|
return AlertEvent{}, clientError{Code: "ALERT_EVENT_NOT_FOUND", Message: "告警事件不存在"}
|
|
}
|
|
|
|
func (m *MockStore) AlertNotifications(_ context.Context, query AlertNotificationQuery) (Page[AlertNotification], error) {
|
|
m.alertMu.RLock()
|
|
defer m.alertMu.RUnlock()
|
|
items := make([]AlertNotification, 0, len(m.alertNotifications))
|
|
for _, item := range m.alertNotifications {
|
|
if !query.UnreadOnly || !item.Read {
|
|
items = append(items, item)
|
|
}
|
|
}
|
|
total := len(items)
|
|
start := query.Offset
|
|
if start > total {
|
|
start = total
|
|
}
|
|
end := start + query.Limit
|
|
if end > total {
|
|
end = total
|
|
}
|
|
return Page[AlertNotification]{Items: append([]AlertNotification(nil), items[start:end]...), Total: total, Limit: query.Limit, Offset: query.Offset}, nil
|
|
}
|
|
|
|
func (m *MockStore) MarkAlertNotificationsRead(_ context.Context, request AlertNotificationReadRequest) (int, error) {
|
|
m.alertMu.Lock()
|
|
defer m.alertMu.Unlock()
|
|
ids := map[int64]bool{}
|
|
for _, id := range request.IDs {
|
|
ids[id] = true
|
|
}
|
|
count := 0
|
|
now := time.Now().Format(time.RFC3339)
|
|
for i := range m.alertNotifications {
|
|
if ids[m.alertNotifications[i].ID] && !m.alertNotifications[i].Read {
|
|
m.alertNotifications[i].Read = true
|
|
m.alertNotifications[i].ReadAt = now
|
|
count++
|
|
}
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
func (m *MockStore) EvaluateAlerts(context.Context) (AlertEvaluationResult, error) {
|
|
return AlertEvaluationResult{RulesEvaluated: len(m.alertRules), VehiclesScanned: len(m.vehicles), AsOf: time.Now().Format(time.RFC3339)}, nil
|
|
}
|