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

173 lines
4.7 KiB
Go
Raw Normal View History

2026-08-03 05:52:02 +00:00
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
}