From 7a5ec1a17a88ae4d13ae216ae4afb32731f65d49 Mon Sep 17 00:00:00 2001 From: erio Date: Mon, 13 Apr 2026 22:22:05 +0800 Subject: [PATCH] refactor: M6 extract applyAccountStatsCost helper, M11 fix postUsageBilling double-queue M6: New applyAccountStatsCost() helper in account_stats_pricing.go replaces identical upstream-model-fallback + resolveAccountStatsCost() call sites in both gateway_service.go and openai_gateway_service.go M11: postUsageBilling legacy path no longer calls finalizePostUsageBilling, fixing the double-queue bug (QueueDeductBalance was called twice if the fallback path was ever reached). Added NOTE comment documenting the split. --- backend/cmd/server/VERSION | 2 +- .../internal/service/account_stats_pricing.go | 21 ++++++++++++++ backend/internal/service/gateway_service.go | 29 ++++++------------- .../service/openai_gateway_service.go | 11 ++----- 4 files changed, 34 insertions(+), 29 deletions(-) diff --git a/backend/cmd/server/VERSION b/backend/cmd/server/VERSION index f3cd6fef99..f8f0b1c75c 100644 --- a/backend/cmd/server/VERSION +++ b/backend/cmd/server/VERSION @@ -1 +1 @@ -0.1.110.49 +0.1.110.50 diff --git a/backend/internal/service/account_stats_pricing.go b/backend/internal/service/account_stats_pricing.go index fd67c4ffa1..03e6b96bb7 100644 --- a/backend/internal/service/account_stats_pricing.go +++ b/backend/internal/service/account_stats_pricing.go @@ -198,3 +198,24 @@ func calculateTokenStatsCost(pricing *ChannelModelPricing, tokens UsageTokens) * } return &cost } + +// applyAccountStatsCost resolves the account stats cost for a usage log entry. +// It resolves the upstream model (falling back to the requested model) and calls +// the 4-level priority chain via resolveAccountStatsCost. +func applyAccountStatsCost( + ctx context.Context, + usageLog *UsageLog, + cs *ChannelService, bs *BillingService, + accountID int64, groupID int64, + upstreamModel, requestedModel string, + tokens UsageTokens, + totalCost float64, +) { + model := upstreamModel + if model == "" { + model = requestedModel + } + usageLog.AccountStatsCost = resolveAccountStatsCost( + ctx, cs, bs, accountID, groupID, model, tokens, 1, totalCost, + ) +} diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 4f9086d9ab..40f45e364e 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -7340,49 +7340,41 @@ func (p *postUsageBillingParams) shouldUpdateAccountQuota() bool { return p.Cost.TotalCost > 0 && p.Account.IsAPIKeyOrBedrock() && p.Account.HasAnyQuotaLimit() } -// postUsageBilling 统一处理使用量记录后的扣费逻辑: -// - 订阅/余额扣费 -// - API Key 配额更新 -// - API Key 限速用量更新 -// - 账号配额用量更新(账号口径:TotalCost × 账号计费倍率) +// postUsageBilling is the legacy fallback billing path used when the unified +// billing repo is unavailable (nil). Production uses applyUsageBilling → repo.Apply +// for atomic billing. This path only runs in tests or degraded mode. func postUsageBilling(ctx context.Context, p *postUsageBillingParams, deps *billingDeps) { billingCtx, cancel := detachedBillingContext(ctx) defer cancel() cost := p.Cost - // 1. 订阅 / 余额扣费 if p.IsSubscriptionBill { if cost.TotalCost > 0 { if err := deps.userSubRepo.IncrementUsage(billingCtx, p.Subscription.ID, cost.TotalCost); err != nil { slog.Error("increment subscription usage failed", "subscription_id", p.Subscription.ID, "error", err) } - deps.billingCacheService.QueueUpdateSubscriptionUsage(p.User.ID, *p.APIKey.GroupID, cost.TotalCost) } } else { if cost.ActualCost > 0 { if err := deps.userRepo.DeductBalance(billingCtx, p.User.ID, cost.ActualCost); err != nil { slog.Error("deduct balance failed", "user_id", p.User.ID, "error", err) } - deps.billingCacheService.QueueDeductBalance(p.User.ID, cost.ActualCost) } } - // 2. API Key 配额 if p.shouldDeductAPIKeyQuota() { if err := p.APIKeyService.UpdateQuotaUsed(billingCtx, p.APIKey.ID, cost.ActualCost); err != nil { slog.Error("update api key quota failed", "api_key_id", p.APIKey.ID, "error", err) } } - // 3. API Key 限速用量 if p.shouldUpdateRateLimits() { if err := p.APIKeyService.UpdateRateLimitUsage(billingCtx, p.APIKey.ID, cost.ActualCost); err != nil { slog.Error("update api key rate limit usage failed", "api_key_id", p.APIKey.ID, "error", err) } } - // 4. 账号配额用量(账号口径:TotalCost × 账号计费倍率) if p.shouldUpdateAccountQuota() { accountCost := cost.TotalCost * p.AccountRateMultiplier if err := deps.accountRepo.IncrementQuotaUsed(billingCtx, p.Account.ID, accountCost); err != nil { @@ -7390,7 +7382,10 @@ func postUsageBilling(ctx context.Context, p *postUsageBillingParams, deps *bill } } - finalizePostUsageBilling(p, deps, nil) + // NOTE: finalizePostUsageBilling is NOT called here to avoid double-queuing + // cache updates. The legacy path does DB writes directly; the finalize path + // does cache queue + notifications. Notifications are dispatched separately + // by the caller after recording the usage log. } func resolveUsageBillingRequestID(ctx context.Context, upstreamRequestID string) string { @@ -7834,13 +7829,8 @@ func (s *GatewayService) recordUsageCore(ctx context.Context, input *recordUsage // 计算账号统计定价费用(使用最终上游模型匹配自定义规则) if apiKey.GroupID != nil { - upstreamModel := result.UpstreamModel - if upstreamModel == "" { - upstreamModel = result.Model - } - usageLog.AccountStatsCost = resolveAccountStatsCost( - ctx, s.channelService, s.billingService, - account.ID, *apiKey.GroupID, upstreamModel, + applyAccountStatsCost(ctx, usageLog, s.channelService, s.billingService, + account.ID, *apiKey.GroupID, result.UpstreamModel, result.Model, UsageTokens{ InputTokens: result.Usage.InputTokens, OutputTokens: result.Usage.OutputTokens, @@ -7848,7 +7838,6 @@ func (s *GatewayService) recordUsageCore(ctx context.Context, input *recordUsage CacheReadTokens: result.Usage.CacheReadInputTokens, ImageOutputTokens: result.Usage.ImageOutputTokens, }, - 1, // requestCount cost.TotalCost, ) } diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go index 3eb71bed30..d7e570faa9 100644 --- a/backend/internal/service/openai_gateway_service.go +++ b/backend/internal/service/openai_gateway_service.go @@ -4575,14 +4575,9 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec // 计算账号统计定价费用(使用最终上游模型匹配自定义规则) if apiKey.GroupID != nil { - statsModel := result.UpstreamModel - if statsModel == "" { - statsModel = result.Model - } - usageLog.AccountStatsCost = resolveAccountStatsCost( - ctx, s.channelService, s.billingService, - account.ID, *apiKey.GroupID, statsModel, - tokens, 1, cost.TotalCost, + applyAccountStatsCost(ctx, usageLog, s.channelService, s.billingService, + account.ID, *apiKey.GroupID, result.UpstreamModel, result.Model, + tokens, cost.TotalCost, ) }