package local_event import ( "errors" "fmt" "strings" "time" coreService "github.com/go-admin-team/go-admin-core/sdk/service" "gorm.io/gorm" ) var ( ErrInvalidFilter = errors.New("本地事件查询条件不符合要求") ErrNotFound = errors.New("本地事件候选不存在") ) type Service struct { coreService.Service Now func() time.Time } func (s *Service) List(request *PageRequest) ([]Response, int64, error) { filter, err := normalizeFilter(request) if err != nil { return nil, 0, err } query := s.Orm.Model(&EventCandidate{}) if filter.CandidateState != "" { query = query.Where("candidate_state = ?", filter.CandidateState) } if filter.EvidenceState != "" { query = query.Where("evidence_state = ?", filter.EvidenceState) } if filter.RuleRef != "" { query = query.Where("rule_ref = ?", filter.RuleRef) } if filter.occurredAfter != nil { query = query.Where("occurred_at >= ?", *filter.occurredAfter) } if filter.occurredBefore != nil { query = query.Where("occurred_at <= ?", *filter.occurredBefore) } if filter.Keyword != "" { pattern := "%" + strings.ToLower(filter.Keyword) + "%" query = query.Where("LOWER(id) LIKE ? OR LOWER(source_ref) LIKE ? OR LOWER(source_label) LIKE ? OR LOWER(rule_name) LIKE ?", pattern, pattern, pattern, pattern) } var count int64 if err = query.Count(&count).Error; err != nil { return nil, 0, fmt.Errorf("count local event candidates: %w", err) } var models []EventCandidate if err = query.Order("occurred_at DESC, id DESC").Limit(filter.GetPageSize()).Offset((filter.GetPageIndex() - 1) * filter.GetPageSize()).Find(&models).Error; err != nil { return nil, 0, fmt.Errorf("list local event candidates: %w", err) } result := make([]Response, 0, len(models)) for _, model := range models { result = append(result, s.response(model)) } return result, count, nil } func (s *Service) Get(id string) (Response, error) { id = strings.TrimSpace(id) if id == "" || len(id) > 64 { return Response{}, ErrInvalidFilter } var model EventCandidate if err := s.Orm.First(&model, "id = ?", id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return Response{}, ErrNotFound } return Response{}, fmt.Errorf("get local event candidate: %w", err) } return s.response(model), nil } type normalizedFilter struct { *PageRequest occurredAfter *time.Time occurredBefore *time.Time } func normalizeFilter(request *PageRequest) (*normalizedFilter, error) { if request == nil { request = &PageRequest{} } request.CandidateState = strings.TrimSpace(request.CandidateState) request.EvidenceState = strings.TrimSpace(request.EvidenceState) request.RuleRef = strings.TrimSpace(request.RuleRef) request.Keyword = strings.TrimSpace(request.Keyword) if request.GetPageSize() > 100 || len([]rune(request.Keyword)) > 128 || len(request.RuleRef) > 128 { return nil, ErrInvalidFilter } if request.CandidateState != "" && request.CandidateState != CandidateStateCandidate && request.CandidateState != CandidateStateConfirmed { return nil, ErrInvalidFilter } if request.EvidenceState != "" && request.EvidenceState != EvidenceStatePending && request.EvidenceState != EvidenceStateSuccess && request.EvidenceState != EvidenceStateFailed { return nil, ErrInvalidFilter } result := &normalizedFilter{PageRequest: request} var err error if strings.TrimSpace(request.OccurredAfter) != "" { value, parseErr := time.Parse(time.RFC3339, request.OccurredAfter) if parseErr != nil { return nil, ErrInvalidFilter } result.occurredAfter = &value } if strings.TrimSpace(request.OccurredBefore) != "" { value, parseErr := time.Parse(time.RFC3339, request.OccurredBefore) if parseErr != nil { return nil, ErrInvalidFilter } result.occurredBefore = &value } if result.occurredAfter != nil && result.occurredBefore != nil && result.occurredAfter.After(*result.occurredBefore) { err = ErrInvalidFilter } if err != nil { return nil, err } return result, nil } func (s *Service) response(model EventCandidate) Response { now := time.Now().UTC() if s.Now != nil { now = s.Now().UTC() } remaining := int(model.RetainUntil.Sub(now).Hours() / 24) state := "active" if !model.RetainUntil.After(now) { remaining, state = 0, "expired" } else if remaining < 3 { state = "expiring" } return Response{ ID: model.ID, OccurredAt: model.OccurredAt, SourceRef: model.SourceRef, SourceLabel: model.SourceLabel, RuleRef: model.RuleRef, RuleName: model.RuleName, RuleVersion: model.RuleVersion, ZoneRef: model.ZoneRef, CandidateState: model.CandidateState, EvidenceState: model.EvidenceState, EvidenceRef: model.EvidenceRef, EvidenceDetail: model.EvidenceDetail, RetainUntil: model.RetainUntil, RetentionState: state, RemainingDays: remaining, CreatedAt: model.CreatedAt, } }