package rule import ( "context" "encoding/json" "errors" "fmt" "strings" "git.ilapage.cn/ila/yovision/Bell/server/app/event" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) var ErrInvalid = errors.New("规则字段无效") type Service struct{ DB *pgxpool.Pool } func validSeverity(value string) bool { return value == "low" || value == "medium" || value == "high" || value == "critical" } func severityRank(value string) int { return map[string]int{"low": 1, "medium": 2, "high": 3, "critical": 4}[value] } func (s Service) Create(ctx context.Context, input CreateInput) (Rule, error) { input.Code = strings.ToLower(strings.TrimSpace(input.Code)) input.Name = strings.TrimSpace(input.Name) input.MinimumSeverity = strings.ToLower(strings.TrimSpace(input.MinimumSeverity)) if input.Code == "" || input.Name == "" || !validSeverity(input.MinimumSeverity) { return Rule{}, ErrInvalid } if input.EventType != nil { v := strings.TrimSpace(*input.EventType) if v == "" { input.EventType = nil } else { input.EventType = &v } } if input.LocationContains != nil { v := strings.TrimSpace(*input.LocationContains) if v == "" { input.LocationContains = nil } else { input.LocationContains = &v } } var result Rule err := s.DB.QueryRow(ctx, `INSERT INTO bell_rules(code,name,event_type,minimum_severity,location_contains) VALUES($1,$2,$3,$4,$5) RETURNING id::text,code,name,enabled,event_type,minimum_severity,location_contains,version,created_at::text,updated_at::text`, input.Code, input.Name, input.EventType, input.MinimumSeverity, input.LocationContains).Scan(&result.ID, &result.Code, &result.Name, &result.Enabled, &result.EventType, &result.MinimumSeverity, &result.LocationContains, &result.Version, &result.CreatedAt, &result.UpdatedAt) return result, err } func (s Service) SetEnabled(ctx context.Context, id string, enabled bool) (Rule, error) { var result Rule err := s.DB.QueryRow(ctx, `UPDATE bell_rules SET enabled=$2,version=version+1,updated_at=now() WHERE id=$1 RETURNING id::text,code,name,enabled,event_type,minimum_severity,location_contains,version,created_at::text,updated_at::text`, id, enabled).Scan(&result.ID, &result.Code, &result.Name, &result.Enabled, &result.EventType, &result.MinimumSeverity, &result.LocationContains, &result.Version, &result.CreatedAt, &result.UpdatedAt) return result, err } func (s Service) List(ctx context.Context) ([]Rule, error) { rows, err := s.DB.Query(ctx, `SELECT id::text,code,name,enabled,event_type,minimum_severity,location_contains,version,created_at::text,updated_at::text FROM bell_rules ORDER BY created_at,id LIMIT 200`) if err != nil { return nil, err } defer rows.Close() items := []Rule{} for rows.Next() { var item Rule if err := rows.Scan(&item.ID, &item.Code, &item.Name, &item.Enabled, &item.EventType, &item.MinimumSeverity, &item.LocationContains, &item.Version, &item.CreatedAt, &item.UpdatedAt); err != nil { return nil, err } items = append(items, item) } return items, rows.Err() } func (s Service) Match(ctx context.Context, tx pgx.Tx, evt event.Event) error { rows, err := tx.Query(ctx, `SELECT id::text,code,name,enabled,event_type,minimum_severity,location_contains,version,created_at::text,updated_at::text FROM bell_rules WHERE enabled=true ORDER BY id`) if err != nil { return err } rules := []Rule{} for rows.Next() { var item Rule if err := rows.Scan(&item.ID, &item.Code, &item.Name, &item.Enabled, &item.EventType, &item.MinimumSeverity, &item.LocationContains, &item.Version, &item.CreatedAt, &item.UpdatedAt); err != nil { rows.Close() return err } rules = append(rules, item) } rows.Close() if err := rows.Err(); err != nil { return err } for _, current := range rules { matched := true reasons := []string{} if current.EventType != nil && *current.EventType != evt.EventType { matched = false reasons = append(reasons, "事件类型不匹配") } if severityRank(evt.Severity) < severityRank(current.MinimumSeverity) { matched = false reasons = append(reasons, "风险等级低于阈值") } if current.LocationContains != nil && !strings.Contains(strings.ToLower(evt.Location), strings.ToLower(*current.LocationContains)) { matched = false reasons = append(reasons, "地点条件不匹配") } explanation := "全部条件命中" if !matched { explanation = strings.Join(reasons, ";") } if _, err := tx.Exec(ctx, `INSERT INTO bell_rule_evaluations(event_id,rule_id,rule_version,matched,explanation) VALUES($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING`, evt.ID, current.ID, current.Version, matched, explanation); err != nil { return err } if !matched { continue } snapshot, _ := json.Marshal(current) summary := fmt.Sprintf("%s:%s", current.Name, evt.EventType) var alertID string err := tx.QueryRow(ctx, `INSERT INTO bell_alerts(primary_rule_id,correlation_key,severity,summary,location) VALUES($1,$2,$3,$4,$5) ON CONFLICT(primary_rule_id,correlation_key) WHERE status='open' DO UPDATE SET updated_at=now(),severity=CASE WHEN array_position(ARRAY['low','medium','high','critical'],EXCLUDED.severity)>array_position(ARRAY['low','medium','high','critical'],bell_alerts.severity) THEN EXCLUDED.severity ELSE bell_alerts.severity END RETURNING id::text`, current.ID, strings.ToLower(evt.Location), evt.Severity, summary, evt.Location).Scan(&alertID) if err != nil { return err } if _, err := tx.Exec(ctx, `INSERT INTO bell_alert_events(alert_id,event_id) VALUES($1,$2) ON CONFLICT DO NOTHING`, alertID, evt.ID); err != nil { return err } if _, err := tx.Exec(ctx, `INSERT INTO bell_rule_matches(alert_id,event_id,rule_id,rule_version,rule_snapshot,explanation) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT DO NOTHING`, alertID, evt.ID, current.ID, current.Version, snapshot, explanation); err != nil { return err } } return nil }