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
|
|||
|
|
}
|