feat: implement tiered billing expression evaluation and related functionality
- Added support for tiered billing expressions in the billing system. - Introduced new types and functions for handling billing expressions, including caching and execution. - Updated existing billing logic to accommodate tiered billing scenarios. - Enhanced request handling to support incoming billing expression requests. - Added tests for tiered billing functionality to ensure correctness.
This commit is contained in:
@@ -27,6 +27,8 @@ type BillingSession struct {
|
||||
funding FundingSource
|
||||
preConsumedQuota int // 实际预扣额度(信任用户可能为 0)
|
||||
tokenConsumed int // 令牌额度实际扣减量
|
||||
extraReserved int // 发送前补充预扣的额度(订阅退款时需要单独回滚)
|
||||
trusted bool // 是否命中信任额度旁路
|
||||
fundingSettled bool // funding.Settle 已成功,资金来源已提交
|
||||
settled bool // Settle 全部完成(资金 + 令牌)
|
||||
refunded bool // Refund 已调用
|
||||
@@ -97,6 +99,8 @@ func (s *BillingSession) Refund(c *gin.Context) {
|
||||
tokenKey := s.relayInfo.TokenKey
|
||||
isPlayground := s.relayInfo.IsPlayground
|
||||
tokenConsumed := s.tokenConsumed
|
||||
extraReserved := s.extraReserved
|
||||
subscriptionId := s.relayInfo.SubscriptionId
|
||||
funding := s.funding
|
||||
|
||||
gopool.Go(func() {
|
||||
@@ -104,6 +108,11 @@ func (s *BillingSession) Refund(c *gin.Context) {
|
||||
if err := funding.Refund(); err != nil {
|
||||
common.SysLog("error refunding billing source: " + err.Error())
|
||||
}
|
||||
if extraReserved > 0 && funding.Source() == BillingSourceSubscription && subscriptionId > 0 {
|
||||
if err := model.PostConsumeUserSubscriptionDelta(subscriptionId, -int64(extraReserved)); err != nil {
|
||||
common.SysLog("error refunding subscription extra reserved quota: " + err.Error())
|
||||
}
|
||||
}
|
||||
// 2) 退还令牌额度
|
||||
if tokenConsumed > 0 && !isPlayground {
|
||||
if err := model.IncreaseTokenQuota(tokenId, tokenKey, tokenConsumed); err != nil {
|
||||
@@ -140,6 +149,34 @@ func (s *BillingSession) GetPreConsumedQuota() int {
|
||||
return s.preConsumedQuota
|
||||
}
|
||||
|
||||
func (s *BillingSession) Reserve(targetQuota int) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.settled || s.refunded || s.trusted || targetQuota <= s.preConsumedQuota {
|
||||
return nil
|
||||
}
|
||||
|
||||
delta := targetQuota - s.preConsumedQuota
|
||||
if delta <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := s.reserveFunding(delta); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.reserveToken(delta); err != nil {
|
||||
s.rollbackFundingReserve(delta)
|
||||
return err
|
||||
}
|
||||
|
||||
s.preConsumedQuota += delta
|
||||
s.tokenConsumed += delta
|
||||
s.extraReserved += delta
|
||||
s.syncRelayInfo()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PreConsume — 统一预扣费入口(含信任额度旁路)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -151,6 +188,7 @@ func (s *BillingSession) preConsume(c *gin.Context, quota int) *types.NewAPIErro
|
||||
|
||||
// ---- 信任额度旁路 ----
|
||||
if s.shouldTrust(c) {
|
||||
s.trusted = true
|
||||
effectiveQuota = 0
|
||||
logger.LogInfo(c, fmt.Sprintf("用户 %d 额度充足, 信任且不需要预扣费 (funding=%s)", s.relayInfo.UserId, s.funding.Source()))
|
||||
} else if effectiveQuota > 0 {
|
||||
@@ -191,6 +229,55 @@ func (s *BillingSession) preConsume(c *gin.Context, quota int) *types.NewAPIErro
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *BillingSession) reserveFunding(delta int) error {
|
||||
switch funding := s.funding.(type) {
|
||||
case *WalletFunding:
|
||||
if err := model.DecreaseUserQuota(funding.userId, delta); err != nil {
|
||||
return types.NewError(err, types.ErrorCodeUpdateDataError, types.ErrOptionWithSkipRetry())
|
||||
}
|
||||
funding.consumed += delta
|
||||
return nil
|
||||
case *SubscriptionFunding:
|
||||
if err := model.PostConsumeUserSubscriptionDelta(funding.subscriptionId, int64(delta)); err != nil {
|
||||
return types.NewErrorWithStatusCode(
|
||||
fmt.Errorf("订阅额度不足或未配置订阅: %s", err.Error()),
|
||||
types.ErrorCodeInsufficientUserQuota,
|
||||
http.StatusForbidden,
|
||||
types.ErrOptionWithSkipRetry(),
|
||||
types.ErrOptionWithNoRecordErrorLog(),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return types.NewError(fmt.Errorf("unsupported funding source: %s", s.funding.Source()), types.ErrorCodeUpdateDataError, types.ErrOptionWithSkipRetry())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BillingSession) rollbackFundingReserve(delta int) {
|
||||
switch funding := s.funding.(type) {
|
||||
case *WalletFunding:
|
||||
if err := model.IncreaseUserQuota(funding.userId, delta, false); err != nil {
|
||||
common.SysLog("error rolling back wallet funding reserve: " + err.Error())
|
||||
} else {
|
||||
funding.consumed -= delta
|
||||
}
|
||||
case *SubscriptionFunding:
|
||||
if err := model.PostConsumeUserSubscriptionDelta(funding.subscriptionId, -int64(delta)); err != nil {
|
||||
common.SysLog("error rolling back subscription funding reserve: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BillingSession) reserveToken(delta int) error {
|
||||
if delta <= 0 || s.relayInfo.IsPlayground {
|
||||
return nil
|
||||
}
|
||||
if err := PreConsumeTokenQuota(s.relayInfo, delta); err != nil {
|
||||
return types.NewErrorWithStatusCode(err, types.ErrorCodePreConsumeTokenQuotaFailed, http.StatusForbidden, types.ErrOptionWithSkipRetry(), types.ErrOptionWithNoRecordErrorLog())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// shouldTrust 统一信任额度检查,适用于钱包和订阅。
|
||||
func (s *BillingSession) shouldTrust(c *gin.Context) bool {
|
||||
// 异步任务(ForcePreConsume=true)必须预扣全额,不允许信任旁路
|
||||
@@ -235,10 +322,10 @@ func (s *BillingSession) syncRelayInfo() {
|
||||
|
||||
if sub, ok := s.funding.(*SubscriptionFunding); ok {
|
||||
info.SubscriptionId = sub.subscriptionId
|
||||
info.SubscriptionPreConsumed = sub.preConsumed
|
||||
info.SubscriptionPreConsumed = sub.preConsumed + int64(s.extraReserved)
|
||||
info.SubscriptionPostDelta = 0
|
||||
info.SubscriptionAmountTotal = sub.AmountTotal
|
||||
info.SubscriptionAmountUsedAfterPreConsume = sub.AmountUsedAfter
|
||||
info.SubscriptionAmountUsedAfterPreConsume = sub.AmountUsedAfter + int64(s.extraReserved)
|
||||
info.SubscriptionPlanId = sub.PlanId
|
||||
info.SubscriptionPlanTitle = sub.PlanTitle
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user