mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 01:45:41 +00:00
将散落在多个文件中的预扣费/结算/退款逻辑抽象为统一的 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 分派
138 lines
3.8 KiB
Go
138 lines
3.8 KiB
Go
package service
|
||
|
||
import (
|
||
"time"
|
||
|
||
"github.com/QuantumNous/new-api/model"
|
||
)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// FundingSource — 资金来源接口(钱包 or 订阅)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
// FundingSource 抽象了预扣费的资金来源。
|
||
type FundingSource interface {
|
||
// Source 返回资金来源标识:"wallet" 或 "subscription"
|
||
Source() string
|
||
// PreConsume 从该资金来源预扣 amount 额度
|
||
PreConsume(amount int) error
|
||
// Settle 根据差额调整资金来源(正数补扣,负数退还)
|
||
Settle(delta int) error
|
||
// Refund 退还所有预扣费
|
||
Refund() error
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// WalletFunding — 钱包资金来源实现
|
||
// ---------------------------------------------------------------------------
|
||
|
||
type WalletFunding struct {
|
||
userId int
|
||
consumed int // 实际预扣的用户额度
|
||
}
|
||
|
||
func (w *WalletFunding) Source() string { return BillingSourceWallet }
|
||
|
||
func (w *WalletFunding) PreConsume(amount int) error {
|
||
if amount <= 0 {
|
||
return nil
|
||
}
|
||
if err := model.DecreaseUserQuota(w.userId, amount); err != nil {
|
||
return err
|
||
}
|
||
w.consumed = amount
|
||
return nil
|
||
}
|
||
|
||
func (w *WalletFunding) Settle(delta int) error {
|
||
if delta == 0 {
|
||
return nil
|
||
}
|
||
if delta > 0 {
|
||
return model.DecreaseUserQuota(w.userId, delta)
|
||
}
|
||
return model.IncreaseUserQuota(w.userId, -delta, false)
|
||
}
|
||
|
||
func (w *WalletFunding) Refund() error {
|
||
if w.consumed <= 0 {
|
||
return nil
|
||
}
|
||
return model.IncreaseUserQuota(w.userId, w.consumed, false)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// SubscriptionFunding — 订阅资金来源实现
|
||
// ---------------------------------------------------------------------------
|
||
|
||
type SubscriptionFunding struct {
|
||
requestId string
|
||
userId int
|
||
modelName string
|
||
amount int64 // 预扣的订阅额度(subConsume)
|
||
subscriptionId int
|
||
preConsumed int64
|
||
// 以下字段在 PreConsume 成功后填充,供 RelayInfo 同步使用
|
||
AmountTotal int64
|
||
AmountUsedAfter int64
|
||
PlanId int
|
||
PlanTitle string
|
||
}
|
||
|
||
func (s *SubscriptionFunding) Source() string { return BillingSourceSubscription }
|
||
|
||
func (s *SubscriptionFunding) PreConsume(_ int) error {
|
||
// amount 参数被忽略,使用内部 s.amount(已在构造时根据 preConsumedQuota 计算)
|
||
res, err := model.PreConsumeUserSubscription(s.requestId, s.userId, s.modelName, 0, s.amount)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
s.subscriptionId = res.UserSubscriptionId
|
||
s.preConsumed = res.PreConsumed
|
||
s.AmountTotal = res.AmountTotal
|
||
s.AmountUsedAfter = res.AmountUsedAfter
|
||
// 获取订阅计划信息
|
||
if planInfo, err := model.GetSubscriptionPlanInfoByUserSubscriptionId(res.UserSubscriptionId); err == nil && planInfo != nil {
|
||
s.PlanId = planInfo.PlanId
|
||
s.PlanTitle = planInfo.PlanTitle
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *SubscriptionFunding) Settle(delta int) error {
|
||
if delta == 0 {
|
||
return nil
|
||
}
|
||
return model.PostConsumeUserSubscriptionDelta(s.subscriptionId, int64(delta))
|
||
}
|
||
|
||
func (s *SubscriptionFunding) Refund() error {
|
||
if s.preConsumed <= 0 {
|
||
return nil
|
||
}
|
||
return refundWithRetry(func() error {
|
||
return model.RefundSubscriptionPreConsume(s.requestId)
|
||
})
|
||
}
|
||
|
||
// refundWithRetry 尝试多次执行退款操作以提高成功率,只能用于基于事务的退款函数!!!!!!
|
||
// try to refund with retries, only for refund functions based on transactions!!!
|
||
func refundWithRetry(fn func() error) error {
|
||
if fn == nil {
|
||
return nil
|
||
}
|
||
const maxAttempts = 3
|
||
var lastErr error
|
||
for i := 0; i < maxAttempts; i++ {
|
||
if err := fn(); err == nil {
|
||
return nil
|
||
} else {
|
||
lastErr = err
|
||
}
|
||
if i < maxAttempts-1 {
|
||
time.Sleep(time.Duration(200*(i+1)) * time.Millisecond)
|
||
}
|
||
}
|
||
return lastErr
|
||
}
|