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

100 lines
3.3 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"
"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)
}