mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 02:25:00 +00:00
refactor: 抽象统一计费会话 BillingSession
将散落在多个文件中的预扣费/结算/退款逻辑抽象为统一的 BillingSession 生命周期管理: - 新增 BillingSettler 接口 (relay/common/billing.go) 避免循环引用 - 新增 FundingSource 接口 + WalletFunding / SubscriptionFunding 实现 (service/funding_source.go) - 新增 BillingSession 封装预扣/结算/退款原子操作 (service/billing_session.go) - 新增 SettleBilling 统一结算辅助函数,替换各 handler 中的 quotaDelta 模式 - 重写 PreConsumeBilling 为 BillingSession 工厂入口 - controller/relay.go 退款守卫改用 BillingSession.Refund() 修复的 Bug: - 令牌额度泄漏:PreConsumeTokenQuota 成功但 DecreaseUserQuota 失败时未回滚 - 订阅退款遗漏:FinalPreConsumedQuota=0 但 SubscriptionPreConsumed>0 时跳过退款 - 订阅多扣费:subConsume 强制为 1 但 FinalPreConsumedQuota 不同步 - 退款路径不统一:钱包/订阅退款逻辑现统一由 FundingSource.Refund 分派
This commit is contained in:
21
relay/common/billing.go
Normal file
21
relay/common/billing.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package common
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
// BillingSettler 抽象计费会话的生命周期操作。
|
||||
// 由 service.BillingSession 实现,存储在 RelayInfo 上以避免循环引用。
|
||||
type BillingSettler interface {
|
||||
// Settle 根据实际消耗额度进行结算,计算 delta = actualQuota - preConsumedQuota,
|
||||
// 同时调整资金来源(钱包/订阅)和令牌额度。
|
||||
Settle(actualQuota int) error
|
||||
|
||||
// Refund 退还所有预扣费额度(资金来源 + 令牌),幂等安全。
|
||||
// 通过 gopool 异步执行。如果已经结算或退款则不做任何操作。
|
||||
Refund(c *gin.Context)
|
||||
|
||||
// NeedsRefund 返回会话是否存在需要退还的预扣状态(未结算且未退款)。
|
||||
NeedsRefund() bool
|
||||
|
||||
// GetPreConsumedQuota 返回实际预扣的额度值(信任用户可能为 0)。
|
||||
GetPreConsumedQuota() int
|
||||
}
|
||||
@@ -115,6 +115,9 @@ type RelayInfo struct {
|
||||
SendResponseCount int
|
||||
ReceivedResponseCount int
|
||||
FinalPreConsumedQuota int // 最终预消耗的配额
|
||||
// Billing 是计费会话,封装了预扣费/结算/退款的统一生命周期。
|
||||
// 免费模型和按次计费(MJ/Task)时为 nil。
|
||||
Billing BillingSettler
|
||||
// BillingSource indicates whether this request is billed from wallet quota or subscription.
|
||||
// "" or "wallet" => wallet; "subscription" => subscription
|
||||
BillingSource string
|
||||
|
||||
@@ -423,29 +423,8 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage
|
||||
model.UpdateChannelUsedQuota(relayInfo.ChannelId, quota)
|
||||
}
|
||||
|
||||
quotaDelta := quota - relayInfo.FinalPreConsumedQuota
|
||||
|
||||
//logger.LogInfo(ctx, fmt.Sprintf("request quota delta: %s", logger.FormatQuota(quotaDelta)))
|
||||
|
||||
if quotaDelta > 0 {
|
||||
logger.LogInfo(ctx, fmt.Sprintf("预扣费后补扣费:%s(实际消耗:%s,预扣费:%s)",
|
||||
logger.FormatQuota(quotaDelta),
|
||||
logger.FormatQuota(quota),
|
||||
logger.FormatQuota(relayInfo.FinalPreConsumedQuota),
|
||||
))
|
||||
} else if quotaDelta < 0 {
|
||||
logger.LogInfo(ctx, fmt.Sprintf("预扣费后返还扣费:%s(实际消耗:%s,预扣费:%s)",
|
||||
logger.FormatQuota(-quotaDelta),
|
||||
logger.FormatQuota(quota),
|
||||
logger.FormatQuota(relayInfo.FinalPreConsumedQuota),
|
||||
))
|
||||
}
|
||||
|
||||
if quotaDelta != 0 {
|
||||
err := service.PostConsumeQuota(relayInfo, quotaDelta, relayInfo.FinalPreConsumedQuota, true)
|
||||
if err != nil {
|
||||
logger.LogError(ctx, "error consuming token remain quota: "+err.Error())
|
||||
}
|
||||
if err := service.SettleBilling(ctx, relayInfo, quota); err != nil {
|
||||
logger.LogError(ctx, "error settling billing: "+err.Error())
|
||||
}
|
||||
|
||||
logModel := modelName
|
||||
|
||||
Reference in New Issue
Block a user