173 lines
4.7 KiB
Go
173 lines
4.7 KiB
Go
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
|
||
}
|