package platform import ( "context" "crypto/rand" "encoding/hex" "fmt" "strings" ) type alertStore interface { AlertSummary(context.Context, AlertQuery) (AlertSummary, error) AlertEvents(context.Context, AlertQuery) (Page[AlertEvent], error) AlertEvent(context.Context, string) (AlertEvent, error) AlertRules(context.Context) ([]AlertRule, error) SaveAlertRule(context.Context, AlertRuleInput) (AlertRule, error) SetAlertRuleEnabled(context.Context, string, AlertRuleEnabledUpdate) (AlertRule, error) ActOnAlert(context.Context, string, AlertActionRequest) (AlertEvent, error) AlertNotifications(context.Context, AlertNotificationQuery) (Page[AlertNotification], error) MarkAlertNotificationsRead(context.Context, AlertNotificationReadRequest) (int, error) } type alertEvaluatorStore interface { EvaluateAlerts(context.Context) (AlertEvaluationResult, error) } func (s *Service) alertStore() (alertStore, error) { store, ok := s.store.(alertStore) if !ok { return nil, fmt.Errorf("store does not provide durable alert center") } return store, nil } func (s *Service) AlertSummary(ctx context.Context, query AlertQuery) (AlertSummary, error) { store, err := s.alertStore() if err != nil { return AlertSummary{}, err } return store.AlertSummary(ctx, normalizeAlertQuery(query)) } func (s *Service) AlertEvents(ctx context.Context, query AlertQuery) (Page[AlertEvent], error) { store, err := s.alertStore() if err != nil { return Page[AlertEvent]{}, err } return store.AlertEvents(ctx, normalizeAlertQuery(query)) } func (s *Service) AlertEvent(ctx context.Context, id string) (AlertEvent, error) { if strings.TrimSpace(id) == "" { return AlertEvent{}, clientError{Code: "ALERT_EVENT_ID_REQUIRED", Message: "告警事件 ID 不能为空"} } store, err := s.alertStore() if err != nil { return AlertEvent{}, err } return store.AlertEvent(ctx, strings.TrimSpace(id)) } func (s *Service) AlertRules(ctx context.Context) ([]AlertRule, error) { store, err := s.alertStore() if err != nil { return nil, err } return store.AlertRules(ctx) } func (s *Service) SaveAlertRule(ctx context.Context, input AlertRuleInput) (AlertRule, error) { definitions, err := s.metricDefinitions(ctx) if err != nil { return AlertRule{}, err } if err := validateAlertRule(input, definitions...); err != nil { return AlertRule{}, err } input.ID = strings.TrimSpace(input.ID) if input.ID == "" { id, err := newAlertID("rule") if err != nil { return AlertRule{}, err } input.ID = id } input.Actor = firstNonEmpty(strings.TrimSpace(input.Actor), "platform-admin") input.ScopeProtocols = cleanUniqueStrings(input.ScopeProtocols) input.ScopeVINs = cleanUniqueStrings(input.ScopeVINs) input.ScopeOEMs = cleanUniqueStrings(input.ScopeOEMs) input.ScopeModels = cleanUniqueStrings(input.ScopeModels) input.ScopeCompanies = cleanUniqueStrings(input.ScopeCompanies) if err := validateAlertRuleScopes(input); err != nil { return AlertRule{}, err } input.NotificationChannels = normalizeAlertChannels(input.NotificationChannels) if strings.EqualFold(input.Operator, "changed") { input.DurationSec = 0 } if strings.EqualFold(input.ValueType, "boolean") && input.BooleanThreshold != nil { if *input.BooleanThreshold { input.Threshold = 1 } else { input.Threshold = 0 } } store, err := s.alertStore() if err != nil { return AlertRule{}, err } return store.SaveAlertRule(ctx, input) } func validateAlertRuleScopes(input AlertRuleInput) error { for _, scope := range []struct { name string values []string }{ {"协议", input.ScopeProtocols}, {"VIN", input.ScopeVINs}, {"厂家", input.ScopeOEMs}, {"车型", input.ScopeModels}, {"企业", input.ScopeCompanies}, } { if len(scope.values) > 500 { return clientError{Code: "ALERT_RULE_SCOPE_TOO_LARGE", Message: scope.name + "范围最多允许 500 项"} } for _, value := range scope.values { if len([]rune(value)) > 128 { return clientError{Code: "ALERT_RULE_SCOPE_VALUE_INVALID", Message: scope.name + "范围值不能超过 128 个字符"} } } } return nil } func (s *Service) SetAlertRuleEnabled(ctx context.Context, id string, update AlertRuleEnabledUpdate) (AlertRule, error) { if strings.TrimSpace(id) == "" || update.Version <= 0 { return AlertRule{}, clientError{Code: "ALERT_RULE_VERSION_REQUIRED", Message: "规则 ID 和版本不能为空"} } update.Actor = firstNonEmpty(strings.TrimSpace(update.Actor), "platform-admin") store, err := s.alertStore() if err != nil { return AlertRule{}, err } return store.SetAlertRuleEnabled(ctx, strings.TrimSpace(id), update) } func (s *Service) ActOnAlert(ctx context.Context, id string, request AlertActionRequest) (AlertEvent, error) { if strings.TrimSpace(id) == "" || request.Version <= 0 { return AlertEvent{}, clientError{Code: "ALERT_EVENT_VERSION_REQUIRED", Message: "事件 ID 和版本不能为空"} } request.Action = strings.ToLower(strings.TrimSpace(request.Action)) if request.Action != "acknowledge" && request.Action != "close" && request.Action != "ignore" { return AlertEvent{}, clientError{Code: "ALERT_ACTION_INVALID", Message: "仅支持确认、关闭或忽略告警"} } if len([]rune(request.Note)) > 200 { return AlertEvent{}, clientError{Code: "ALERT_NOTE_TOO_LONG", Message: "处置备注不能超过 200 字"} } request.Actor = firstNonEmpty(strings.TrimSpace(request.Actor), "platform-admin") store, err := s.alertStore() if err != nil { return AlertEvent{}, err } return store.ActOnAlert(ctx, strings.TrimSpace(id), request) } func (s *Service) AlertNotifications(ctx context.Context, query AlertNotificationQuery) (Page[AlertNotification], error) { if query.Limit <= 0 { query.Limit = 20 } if query.Limit > 100 { query.Limit = 100 } if query.Offset < 0 { query.Offset = 0 } store, err := s.alertStore() if err != nil { return Page[AlertNotification]{}, err } return store.AlertNotifications(ctx, query) } func (s *Service) MarkAlertNotificationsRead(ctx context.Context, request AlertNotificationReadRequest) (int, error) { if len(request.IDs) == 0 || len(request.IDs) > 100 { return 0, clientError{Code: "ALERT_NOTIFICATION_IDS_INVALID", Message: "请选择 1 到 100 条通知"} } request.Actor = firstNonEmpty(strings.TrimSpace(request.Actor), "platform-admin") store, err := s.alertStore() if err != nil { return 0, err } return store.MarkAlertNotificationsRead(ctx, request) } func (s *Service) EvaluateAlerts(ctx context.Context) (AlertEvaluationResult, error) { store, ok := s.store.(alertEvaluatorStore) if !ok { return AlertEvaluationResult{}, fmt.Errorf("store does not provide alert evaluation") } return store.EvaluateAlerts(ctx) } func normalizeAlertQuery(query AlertQuery) AlertQuery { query.Keyword = strings.TrimSpace(query.Keyword) query.Severity = strings.ToLower(strings.TrimSpace(query.Severity)) query.Status = strings.ToLower(strings.TrimSpace(query.Status)) query.RuleID = strings.TrimSpace(query.RuleID) query.Protocol = strings.TrimSpace(query.Protocol) if query.Limit <= 0 { query.Limit = 20 } if query.Limit > 200 { query.Limit = 200 } if query.Offset < 0 { query.Offset = 0 } return query } func validateAlertRule(input AlertRuleInput, catalog ...MetricDefinition) error { if strings.TrimSpace(input.Name) == "" || len([]rune(input.Name)) > 80 { return clientError{Code: "ALERT_RULE_NAME_INVALID", Message: "规则名称不能为空且不能超过 80 字"} } severity := strings.ToLower(strings.TrimSpace(input.Severity)) if severity != "critical" && severity != "major" && severity != "minor" { return clientError{Code: "ALERT_RULE_SEVERITY_INVALID", Message: "告警级别仅支持 critical、major、minor"} } valueType := strings.ToLower(strings.TrimSpace(input.ValueType)) if valueType != "numeric" && valueType != "boolean" { return clientError{Code: "ALERT_RULE_VALUE_TYPE_INVALID", Message: "规则值类型仅支持 numeric 或 boolean"} } if strings.TrimSpace(input.Metric) == "" { return clientError{Code: "ALERT_RULE_METRIC_REQUIRED", Message: "规则指标不能为空"} } var metric *MetricDefinition definitions := catalog if len(definitions) == 0 { definitions = metricDefinitions() } for index := range definitions { candidate := definitions[index] if candidate.Key == strings.TrimSpace(input.Metric) { metric = &candidate break } } if metric == nil || !metric.Alertable { return clientError{Code: "ALERT_RULE_METRIC_INVALID", Message: "规则指标不在可告警指标目录中"} } if _, supported := alertMetricValue(input.Metric, alertEvaluationEvidence{}); !supported { return clientError{Code: "ALERT_RULE_METRIC_UNSUPPORTED", Message: "规则指标尚未接入告警评估器"} } if metric.ValueType != valueType { return clientError{Code: "ALERT_RULE_METRIC_TYPE_MISMATCH", Message: "规则值类型与指标目录不一致"} } operator := strings.ToLower(strings.TrimSpace(input.Operator)) allowed := map[string]bool{"gt": true, "gte": true, "lt": true, "lte": true, "eq": true, "neq": true, "between": true, "outside": true, "changed": true} if !allowed[operator] { return clientError{Code: "ALERT_RULE_OPERATOR_INVALID", Message: "规则比较符无效"} } if (operator == "between" || operator == "outside") && (valueType != "numeric" || input.ThresholdHigh <= input.Threshold) { return clientError{Code: "ALERT_RULE_RANGE_INVALID", Message: "区间规则必须是数值型且上限大于下限"} } if operator == "changed" && valueType != "boolean" { return clientError{Code: "ALERT_RULE_CHANGE_INVALID", Message: "状态变化规则必须使用布尔值类型"} } if valueType == "boolean" && operator != "changed" && input.BooleanThreshold == nil { return clientError{Code: "ALERT_RULE_BOOLEAN_REQUIRED", Message: "布尔规则必须配置目标值"} } if input.DurationSec < 0 || input.DurationSec > 86400 || input.RepeatIntervalSec < 0 || input.RepeatIntervalSec > 604800 { return clientError{Code: "ALERT_RULE_INTERVAL_INVALID", Message: "持续时间或重复间隔超出允许范围"} } if input.Version < 0 { return clientError{Code: "ALERT_RULE_VERSION_INVALID", Message: "规则版本无效"} } return nil } func normalizeAlertChannels(values []string) []string { allowed := map[string]bool{"in_app": true, "sms": true, "email": true, "wecom": true} out := make([]string, 0, len(values)+1) seen := map[string]bool{} for _, value := range values { value = strings.ToLower(strings.TrimSpace(value)) if allowed[value] && !seen[value] { seen[value] = true out = append(out, value) } } if !seen["in_app"] { out = append([]string{"in_app"}, out...) } return out } func cleanUniqueStrings(values []string) []string { out := make([]string, 0, len(values)) seen := map[string]bool{} for _, value := range values { value = strings.TrimSpace(value) if value != "" && !seen[value] { seen[value] = true out = append(out, value) } } return out } func newAlertID(prefix string) (string, error) { buf := make([]byte, 10) if _, err := rand.Read(buf); err != nil { return "", err } return prefix + "-" + hex.EncodeToString(buf), nil }