package usecase import ( "context" "fmt" "apps/backend/internal/module/radar/domain" ) type WatchInput struct { Terms []string ExcludeTerms []string Regions []string // Enabled=false 代表建立成 paused,可先備好關鍵字再開。 Enabled bool } type WatchPatch struct { Terms *[]string ExcludeTerms *[]string Regions *[]string } func (s *Service) CreateWatch(ctx context.Context, ownerUID int64, in WatchInput) (*domain.RadarWatch, error) { if ownerUID <= 0 { return nil, fmt.Errorf("%w: owner_uid required", domain.ErrValidation) } status := domain.WatchPaused if in.Enabled { status = domain.WatchActive } now := domain.NowNano() w := &domain.RadarWatch{ ID: domain.NewID(), OwnerUID: ownerUID, Terms: in.Terms, ExcludeTerms: in.ExcludeTerms, Regions: in.Regions, Status: status, CreatedAt: now, UpdatedAt: now, } if err := w.Normalize(); err != nil { return nil, err } if status == domain.WatchActive { if err := s.assertCanActivate(ctx, ownerUID, ""); err != nil { return nil, err } } if err := s.Repo.SaveWatch(ctx, w); err != nil { return nil, err } return w, nil } // GetWatch 以 owner 檢查取代單純 id 查詢:id 是 uuid,但不該靠不可預測性當授權。 func (s *Service) GetWatch(ctx context.Context, ownerUID int64, id string) (*domain.RadarWatch, error) { w, err := s.Repo.GetWatch(ctx, id) if err != nil { return nil, err } if w.OwnerUID != ownerUID { return nil, domain.ErrForbidden } return w, nil } func (s *Service) ListWatches(ctx context.Context, ownerUID int64, f domain.WatchListFilter) ([]*domain.RadarWatch, int64, error) { if ownerUID <= 0 { return nil, 0, fmt.Errorf("%w: owner_uid required", domain.ErrValidation) } if f.Status != "" && !domain.IsWatchStatus(f.Status) { return nil, 0, fmt.Errorf("%w: unknown status filter %q", domain.ErrValidation, f.Status) } if f.PageSize > 50 { f.PageSize = 50 } return s.Repo.ListWatches(ctx, ownerUID, f) } func (s *Service) ListActiveWatches(ctx context.Context, ownerUID int64) ([]*domain.RadarWatch, error) { return s.Repo.ListActiveWatches(ctx, ownerUID) } func (s *Service) CountActiveWatches(ctx context.Context, ownerUID int64) (int64, error) { return s.Repo.CountActiveWatches(ctx, ownerUID) } /* UpdateWatch 只改關鍵字與地區;狀態一律走 Pause/Resume/Archive。 nil 欄位代表不動,這樣「只改地區」不會意外清空關鍵字。 */ func (s *Service) UpdateWatch(ctx context.Context, ownerUID int64, id string, patch WatchPatch) (*domain.RadarWatch, error) { w, err := s.GetWatch(ctx, ownerUID, id) if err != nil { return nil, err } if w.Status == domain.WatchArchived { return nil, fmt.Errorf("%w: archived watch cannot be edited", domain.ErrValidation) } if patch.Terms != nil { w.Terms = *patch.Terms } if patch.ExcludeTerms != nil { w.ExcludeTerms = *patch.ExcludeTerms } if patch.Regions != nil { w.Regions = *patch.Regions } if err := w.Normalize(); err != nil { return nil, err } w.UpdatedAt = domain.NowNano() if err := s.Repo.SaveWatch(ctx, w); err != nil { return nil, err } return w, nil } func (s *Service) PauseWatch(ctx context.Context, ownerUID int64, id string) (*domain.RadarWatch, error) { return s.transition(ctx, ownerUID, id, domain.WatchPaused) } /* ResumeWatch 回到 active,因此要再過一次配額與服務檔案閘:暫停期間方案可能已降級, 不重驗就會讓人靠「暫停再恢復」繞過上限。 */ func (s *Service) ResumeWatch(ctx context.Context, ownerUID int64, id string) (*domain.RadarWatch, error) { w, err := s.GetWatch(ctx, ownerUID, id) if err != nil { return nil, err } if w.Status != domain.WatchActive { if err := s.assertCanActivate(ctx, ownerUID, w.ID); err != nil { return nil, err } } return s.applyTransition(ctx, w, domain.WatchActive) } // ArchiveWatch 是軟刪:歷史商機與統計都留著。 func (s *Service) ArchiveWatch(ctx context.Context, ownerUID int64, id string) error { _, err := s.transition(ctx, ownerUID, id, domain.WatchArchived) return err } func (s *Service) MarkWatchSwept(ctx context.Context, id string, at int64) error { if at <= 0 { at = domain.NowNano() } return s.Repo.TouchWatchSweptAt(ctx, id, at) } func (s *Service) transition(ctx context.Context, ownerUID int64, id, to string) (*domain.RadarWatch, error) { w, err := s.GetWatch(ctx, ownerUID, id) if err != nil { return nil, err } return s.applyTransition(ctx, w, to) } func (s *Service) applyTransition(ctx context.Context, w *domain.RadarWatch, to string) (*domain.RadarWatch, error) { if err := w.Transition(to); err != nil { return nil, err } if err := s.Repo.SaveWatch(ctx, w); err != nil { return nil, err } return w, nil }