129 lines
5.0 KiB
Go
129 lines
5.0 KiB
Go
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
|
|
}
|