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:
CaIon
2026-03-16 16:00:22 +08:00
parent a4fd2246ba
commit 91ed4e196a
34 changed files with 4797 additions and 26 deletions
+89 -2
View File
@@ -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 {