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

75 lines
2.5 KiB
Go
Raw Permalink 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"
"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
}