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

185 lines
5.4 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
}
firstEverWatch := false
if status == domain.WatchActive {
if err := s.assertCanActivate(ctx, ownerUID, ""); err != nil {
return nil, err
}
// 只在使用者從未建過任何訂閱時才判定「首巡」,避免每次新增都額外燒一次巡檢成本。
if _, total, err := s.Repo.ListWatches(ctx, ownerUID, domain.WatchListFilter{Page: 1, PageSize: 1}); err == nil {
firstEverWatch = total == 0
}
}
if err := s.Repo.SaveWatch(ctx, w); err != nil {
return nil, err
}
if firstEverWatch && s.SweepJobs != nil {
// 不等每日排程:讓第一組訂閱的使用者最快隔一小段時間就看到結果,而不是等到隔天。
// 失敗不阻斷建立,每日排程仍會補上。
if _, sweepErr := s.SweepJobs.ScheduleRadarSweep(ctx, ownerUID, w.ID, domain.NowNano()); sweepErr == nil {
w.FirstSweepTriggered = true
}
}
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
}