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

100 lines
3.3 KiB
Go
Raw Normal View History

2026-08-03 05:52:02 +00:00
package usecase
import (
"context"
"errors"
"fmt"
"apps/backend/internal/module/ai"
"apps/backend/internal/module/radar/domain"
usageUC "apps/backend/internal/module/usage/usecase"
)
type Service struct {
Repo domain.Repository
// Quota 未接時退回最低方案上限,見 watch_quota.go。
Quota PlanQuota
// Usage 為 nil 時不扣點(單元測試路徑)。
Usage *usageUC.Service
// AI 是測試fallback clientAIRegistryResolveAI 才是正式路徑。
AI ai.Client
AIRegistry *ai.Registry
ResolveAI func(ctx context.Context, uid int64) (provider, model, apiKey string, err error)
ResolveKey func(ctx context.Context, uid int64, meter string) (mode, apiKey string, err error)
// PainTerms 可空:接不上只影響建議品質。
PainTerms PainTermSource
// SweepJobs 每日排程手動觸發共用nil 時 ScheduleDailySweeps 回 ErrNotReady。
SweepJobs SweepJobScheduler
// Dual-path fetch (prefer HitFetch adapter over individual providers).
HitFetch HitFetcher
Search ThreadSearcher
Chrome ChromeSearcher
DevMode DevModeReader
CrawlerSession CrawlerSessionReader
// Notifier for sweep failures (optional).
Notifier SweepNotifier
// CRM bridge for accept → contact (optional until M4).
CRM ContactBinder
// Health gates auto-send of public replies (AccountHealth throttle).
Health HealthGate
}
// ContactBinder creates or binds a CRM contact when accepting an opportunity.
type ContactBinder interface {
BindOpportunity(ctx context.Context, ownerUID int64, opp *domain.Opportunity) (contactID string, err error)
}
func New(repo domain.Repository) *Service {
return &Service{Repo: repo}
}
/*
GetServiceProfile 未建檔時回 domain.ErrNotFound由呼叫端決定怎麼表達
HTTP 層會把它翻成 exists=false 200表單本來就要能開空的 usecase 不能
自己回一個零值檔案 那樣沒建檔建了一份空的就分不出來 SP-01
訂閱閘門正是靠這個差別
*/
func (s *Service) GetServiceProfile(ctx context.Context, ownerUID int64) (*domain.ServiceProfile, error) {
if ownerUID <= 0 {
return nil, fmt.Errorf("%w: owner_uid required", domain.ErrValidation)
}
return s.Repo.GetServiceProfile(ctx, ownerUID)
}
func (s *Service) HasServiceProfile(ctx context.Context, ownerUID int64) (bool, error) {
_, err := s.GetServiceProfile(ctx, ownerUID)
if errors.Is(err, domain.ErrNotFound) {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
// UpsertServiceProfile 整份覆寫。owner_uid 由呼叫端從 JWT 取request 帶的一律忽略。
func (s *Service) UpsertServiceProfile(ctx context.Context, ownerUID int64, in *domain.ServiceProfile) (*domain.ServiceProfile, error) {
if in == nil {
return nil, fmt.Errorf("%w: profile required", domain.ErrValidation)
}
in.OwnerUID = ownerUID
if err := in.Normalize(); err != nil {
return nil, err
}
now := domain.NowNano()
in.UpdatedAt = now
in.CreatedAt = now
if existing, err := s.Repo.GetServiceProfile(ctx, ownerUID); err == nil {
in.CreatedAt = existing.CreatedAt
} else if !errors.Is(err, domain.ErrNotFound) {
return nil, err
}
if err := s.Repo.SaveServiceProfile(ctx, in); err != nil {
return nil, err
}
return s.Repo.GetServiceProfile(ctx, ownerUID)
}