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

115 lines
3.2 KiB
Go
Raw Normal View History

2026-08-03 05:52:02 +00:00
package usecase
import (
"context"
"errors"
"fmt"
"apps/backend/internal/module/radar/domain"
)
/*
PlanQuota 提供會員方案的雷達上限spec §4.9
介面留在 radar module 是為了不讓 radar 依賴 usage module正式環境的實作在
internal/svc 橋接既有方案定義測試直接給固定值
*/
type PlanQuota interface {
RadarQuota(ctx context.Context, ownerUID int64) (maxActiveWatches, maxDailyOpportunities int, err error)
}
// FixedQuota 是測試與離線工具用的固定上限。
type FixedQuota struct {
MaxActiveWatches int
MaxDailyOpportunities int
}
func (q FixedQuota) RadarQuota(context.Context, int64) (int, int, error) {
return q.MaxActiveWatches, q.MaxDailyOpportunities, nil
}
/*
沒接上 PlanQuota 時的保守預設 = 最低方案
失敗方向要選少給而不是白送漏接線的結果是使用者看到上限提示來問客服
而不是所有人都拿到 Pro 的常駐監控量
*/
var fallbackQuota = FixedQuota{MaxActiveWatches: 1, MaxDailyOpportunities: 5}
func (s *Service) quota(ctx context.Context, ownerUID int64) (FixedQuota, error) {
if s.Quota == nil {
return fallbackQuota, nil
}
maxActive, maxDaily, err := s.Quota.RadarQuota(ctx, ownerUID)
if err != nil {
return FixedQuota{}, err
}
if maxActive <= 0 {
maxActive = fallbackQuota.MaxActiveWatches
}
if maxDaily <= 0 {
maxDaily = fallbackQuota.MaxDailyOpportunities
}
return FixedQuota{MaxActiveWatches: maxActive, MaxDailyOpportunities: maxDaily}, nil
}
func (s *Service) MaxActiveWatches(ctx context.Context, ownerUID int64) (int, error) {
q, err := s.quota(ctx, ownerUID)
if err != nil {
return 0, err
}
return q.MaxActiveWatches, nil
}
func (s *Service) MaxDailyOpportunities(ctx context.Context, ownerUID int64) (int, error) {
q, err := s.quota(ctx, ownerUID)
if err != nil {
return 0, err
}
return q.MaxDailyOpportunities, nil
}
/*
assertCanActivate 變成 active的兩道閘SP-01RW-01
exceptWatchID 是正在恢復的那一筆它目前不是 active所以不會被算進 CountActive
帶進來只為了在訊息與計算上表達清楚
既有超額者不強制降級spec §3.1這裡只擋再多一個
*/
func (s *Service) assertCanActivate(ctx context.Context, ownerUID int64, exceptWatchID string) error {
hasProfile, err := s.HasServiceProfile(ctx, ownerUID)
if err != nil {
return err
}
if !hasProfile {
return fmt.Errorf(
"%w: service profile required before activating a radar watch; fill in /api/v1/radar/service-profile first",
domain.ErrValidation,
)
}
maxActive, err := s.MaxActiveWatches(ctx, ownerUID)
if err != nil {
return err
}
active, err := s.Repo.CountActiveWatches(ctx, ownerUID)
if err != nil {
return err
}
if exceptWatchID != "" {
if w, err := s.Repo.GetWatch(ctx, exceptWatchID); err == nil && w.Status == domain.WatchActive {
active--
} else if err != nil && !errors.Is(err, domain.ErrNotFound) {
return err
}
}
if active >= int64(maxActive) {
return fmt.Errorf(
"%w: active watch limit reached (%d of %d on your plan); pause an existing watch or upgrade your plan",
domain.ErrValidation, active, maxActive,
)
}
return nil
}