package service import ( "context" "git.sundynix.cn/Blizzard/Veyra/internal/apperror" "git.sundynix.cn/Blizzard/Veyra/internal/model" "github.com/google/uuid" "go.uber.org/zap" "time" ) type NotificationRepository interface { ProcessNext(context.Context) (bool, error) List(context.Context, string) ([]model.Notification, error) MarkRead(context.Context, string, string) error Tasks(context.Context) ([]model.NotificationTask, error) SaveQuote(context.Context, model.QuoteSnapshot, string, string) (model.QuoteSnapshot, error) Quotes(context.Context) ([]model.QuoteSnapshot, error) } type NotificationService struct { repo NotificationRepository development bool } func NewNotificationService(repo NotificationRepository, development bool) *NotificationService { return &NotificationService{repo, development} } func (s *NotificationService) authorize(actor model.User, admin bool) error { if actor.Status != "ACTIVE" || (admin && actor.Role != "ADMIN") || (!admin && actor.Role != "USER") { return apperror.ErrForbidden } if s == nil || s.repo == nil || !s.development { return apperror.ErrUnavailable } return nil } func (s *NotificationService) Run(ctx context.Context, logger *zap.Logger) { if s == nil || s.repo == nil || !s.development { return } timer := time.NewTicker(2 * time.Second) defer timer.Stop() for { select { case <-ctx.Done(): return case <-timer.C: for i := 0; i < 50; i++ { worked, err := s.repo.ProcessNext(ctx) if err != nil { if ctx.Err() == nil { logger.Error("notification processing failed") } break } if !worked { break } } } } } func (s *NotificationService) List(ctx context.Context, actor model.User) ([]model.Notification, error) { if err := s.authorize(actor, false); err != nil { return nil, err } return s.repo.List(ctx, actor.ID) } func (s *NotificationService) MarkRead(ctx context.Context, actor model.User, id string) error { if err := s.authorize(actor, false); err != nil { return err } if _, err := uuid.Parse(id); err != nil { return apperror.ErrInvalidInput } return s.repo.MarkRead(ctx, actor.ID, id) } func (s *NotificationService) Tasks(ctx context.Context, actor model.User) ([]model.NotificationTask, error) { if err := s.authorize(actor, true); err != nil { return nil, err } return s.repo.Tasks(ctx) } func (s *NotificationService) Quotes(ctx context.Context, actor model.User) ([]model.QuoteSnapshot, error) { if err := s.authorize(actor, true); err != nil { return nil, err } return s.repo.Quotes(ctx) } type SimulationQuoteInput struct { Symbol string `json:"symbol"` Exchange string `json:"exchange"` Price string `json:"price"` ObservedAt time.Time `json:"observed_at"` } func (s *NotificationService) SaveQuote(ctx context.Context, actor model.User, input SimulationQuoteInput, requestID string) (model.QuoteSnapshot, error) { if err := s.authorize(actor, true); err != nil { return model.QuoteSnapshot{}, err } price, err := model.ParseMoney(input.Price) if err != nil || price == 0 || !fillSymbolPattern.MatchString(input.Symbol) || (input.Exchange != "SSE" && input.Exchange != "SZSE") || input.ObservedAt.Before(time.Now().Add(-time.Minute)) || input.ObservedAt.After(time.Now().Add(time.Minute)) { return model.QuoteSnapshot{}, apperror.ErrInvalidInput } return s.repo.SaveQuote(ctx, model.QuoteSnapshot{Symbol: input.Symbol, Exchange: input.Exchange, PriceMinor: price, ObservedAt: input.ObservedAt, DataMode: "simulation"}, actor.ID, requestID) }