thread-master/apps/backend/internal/module/radar/usecase/watch.go

173 lines
4.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 只改關鍵字與地區;狀態一律走 PauseResumeArchive。
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
}