package lifecycle import ( "context" "encoding/json" "errors" "git.ilapage.cn/ila/yovision/Bell/server/app/alert/model" alertQuery "git.ilapage.cn/ila/yovision/Bell/server/app/alert/query" "git.ilapage.cn/ila/yovision/Bell/server/app/audit" "git.ilapage.cn/ila/yovision/Bell/server/app/auth" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) var ErrAlreadyHandled = errors.New("预警已由其他人员开始处理") var ErrInvalidTransition = errors.New("当前状态不能执行此操作") var ErrOutcomeRequired = errors.New("请选择现场处理结果") type CloseInput struct { Outcome string `json:"outcome"` Note string `json:"note"` } type Result struct { Detail model.Detail `json:"detail"` Idempotent bool `json:"idempotent"` } type Service struct { DB *pgxpool.Pool Audit audit.Store Query alertQuery.Service } func hasRole(user auth.User, role string) bool { for _, value := range user.Roles { if value == role { return true } } return false } func validOutcome(value string) bool { return value == "danger_confirmed" || value == "false_positive" || value == "site_normal" || value == "unable_to_confirm" } func (s Service) Ack(ctx context.Context, alertID string, actor auth.User) (Result, error) { tx, err := s.DB.Begin(ctx) if err != nil { return Result{}, err } var updated string err = tx.QueryRow(ctx, `UPDATE bell_alerts SET status='acknowledged',acknowledged_by=$2,acknowledged_at=now(),updated_at=now() WHERE id=$1 AND status='open' RETURNING id::text`, alertID, actor.ID).Scan(&updated) if err == nil { if _, err = tx.Exec(ctx, `INSERT INTO bell_alert_lifecycle_facts(alert_id,transition,actor_user_id) VALUES($1,'acknowledged',$2)`, alertID, actor.ID); err == nil { _, err = tx.Exec(ctx, `INSERT INTO bell_audit_log(actor_user_id,action,target_type,target_id,outcome,details) VALUES($1,'alert.ack','alert',$2,'success','{}')`, actor.ID, alertID) } if err == nil { err = tx.Commit(ctx) } if err != nil { _ = tx.Rollback(ctx) return Result{}, err } detail, err := s.Query.Get(ctx, alertID) return Result{Detail: detail}, err } _ = tx.Rollback(ctx) if !errors.Is(err, pgx.ErrNoRows) { return Result{}, err } detail, getErr := s.Query.Get(ctx, alertID) if getErr != nil { return Result{}, getErr } if detail.Alert.AcknowledgedBy != nil && *detail.Alert.AcknowledgedBy == actor.ID { _ = s.Audit.Record(ctx, &actor.ID, "alert.ack", "alert", &alertID, "failure", map[string]any{"reason": "duplicate"}) return Result{Detail: detail, Idempotent: true}, nil } _ = s.Audit.Record(ctx, &actor.ID, "alert.ack", "alert", &alertID, "failure", map[string]any{"reason": "already_handled"}) return Result{Detail: detail}, ErrAlreadyHandled } func (s Service) Close(ctx context.Context, alertID string, input CloseInput, actor auth.User) (Result, error) { if !validOutcome(input.Outcome) { _ = s.Audit.Record(ctx, &actor.ID, "alert.close", "alert", &alertID, "failure", map[string]any{"reason": "outcome_required"}) return Result{}, ErrOutcomeRequired } current, err := s.Query.Get(ctx, alertID) if err != nil { return Result{}, err } if current.Alert.Status == "closed" { if current.Alert.ClosedBy != nil && *current.Alert.ClosedBy == actor.ID && current.Alert.CloseOutcome != nil && *current.Alert.CloseOutcome == input.Outcome { return Result{Detail: current, Idempotent: true}, nil } _ = s.Audit.Record(ctx, &actor.ID, "alert.close", "alert", &alertID, "failure", map[string]any{"reason": "already_closed"}) return Result{Detail: current}, ErrInvalidTransition } admin := hasRole(actor, "administrator") if current.Alert.Status != "acknowledged" || (!admin && (current.Alert.AcknowledgedBy == nil || *current.Alert.AcknowledgedBy != actor.ID)) { _ = s.Audit.Record(ctx, &actor.ID, "alert.close", "alert", &alertID, "failure", map[string]any{"reason": "invalid_owner_or_state"}) return Result{Detail: current}, ErrInvalidTransition } details, _ := json.Marshal(map[string]any{"outcome": input.Outcome, "note": input.Note}) tx, err := s.DB.Begin(ctx) if err != nil { return Result{}, err } defer func() { _ = tx.Rollback(ctx) }() result, err := tx.Exec(ctx, `UPDATE bell_alerts SET status='closed',closed_by=$2,closed_at=now(),close_outcome=$3,close_note=nullif($4,''),updated_at=now() WHERE id=$1 AND status='acknowledged'`, alertID, actor.ID, input.Outcome, input.Note) if err != nil { return Result{}, err } if result.RowsAffected() != 1 { return Result{}, ErrInvalidTransition } if _, err = tx.Exec(ctx, `INSERT INTO bell_alert_lifecycle_facts(alert_id,transition,actor_user_id,details) VALUES($1,'closed',$2,$3::jsonb)`, alertID, actor.ID, string(details)); err != nil { return Result{}, err } if _, err = tx.Exec(ctx, `INSERT INTO bell_audit_log(actor_user_id,action,target_type,target_id,outcome,details) VALUES($1,'alert.close','alert',$2,'success',$3::jsonb)`, actor.ID, alertID, string(details)); err != nil { return Result{}, err } if err = tx.Commit(ctx); err != nil { return Result{}, err } detail, err := s.Query.Get(ctx, alertID) return Result{Detail: detail}, err }