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

75 lines
2.5 KiB
Go
Raw Normal View History

2026-08-13 02:22:24 +00:00
package usecase
import (
"context"
"fmt"
"strings"
"time"
"apps/backend/internal/module/radar/domain"
usageDomain "apps/backend/internal/module/usage/domain"
)
// GetCostPreview is deliberately read-only: it resolves the likely key mode
// and reads the current balance, but never calls PrepareCall or reserves a
// credit. The actual operation remains the only place that meters usage.
func (s *Service) GetCostPreview(ctx context.Context, ownerUID int64, action, watchID, productID string, candidateLimit int) (*domain.CostPreview, error) {
if ownerUID <= 0 {
return nil, fmt.Errorf("%w: owner_uid required", domain.ErrValidation)
}
action = strings.ToLower(strings.TrimSpace(action))
if action != "sweep" && action != "explore" && action != "demand_map_enrich" && action != "reply" {
return nil, fmt.Errorf("%w: unknown cost preview action %q", domain.ErrValidation, action)
}
if candidateLimit <= 0 {
candidateLimit = 20
}
if candidateLimit > 100 {
candidateLimit = 100
}
searchCalls := 0
if action == "sweep" || action == "explore" {
searchCalls = 1
if productID != "" {
if plan, err := s.BuildProductQueryPlan(ctx, ownerUID, productID); err == nil && len(plan.Groups) > 0 {
searchCalls = len(plan.Groups)
}
}
}
maxAI := 0
fixed := 0
min := searchCalls * usageDomain.MeterCost(usageDomain.MeterWebSearch)
max := min
switch action {
case "sweep", "explore":
maxAI = candidateLimit
max += maxAI * usageDomain.MeterCost(usageDomain.MeterAIResearch)
case "demand_map_enrich", "reply":
fixed = usageDomain.MeterCost(usageDomain.MeterAICopy)
min, max = fixed, fixed
}
mode := usageDomain.KeyModePlatform
meter := usageDomain.MeterWebSearch
if action == "demand_map_enrich" || action == "reply" {
meter = usageDomain.MeterAICopy
}
if s.ResolveKey != nil {
if resolved, _, err := s.ResolveKey(ctx, ownerUID, meter); err == nil && resolved != "" {
mode = resolved
}
}
remaining := 0
if s.Usage != nil {
if summary, err := s.Usage.GetSummary(ctx, ownerUID, ""); err == nil && summary != nil {
remaining = summary.Platform.CreditsRemaining
}
}
return &domain.CostPreview{
PreviewID: domain.NewID(), Action: action, KeyMode: mode,
FixedCredits: fixed, MinCredits: min, MaxCredits: max,
SearchCalls: searchCalls, MaxAICandidates: maxAI, RemainingCredits: remaining,
EstimateBasis: "免費前處理provider 成功後依 web_searchai_researchai_copy meter 計點",
ExpiresAt: time.Now().UTC().Add(5 * time.Minute).UnixNano(),
}, nil
}