fix(billing): harden tiered retry group-switch billing (#6570)

Follow-up to #6518 (issue #6480) addressing three review findings:

- Document and lock in arrears semantics for the wallet Reserve top-up:
  when an auto-group retry lands on a more expensive group, the full
  reservation delta is deducted unconditionally (balance may go
  negative), mirroring settlement, so the logged pre-consumed quota
  always reconciles with the actual balance movement. Genuine DB
  errors still fail the attempt with update_data_error. Subscription
  funding keeps its insufficient-quota behavior: subscriptions enforce
  a hard used<=total cap and do not support arrears.
- PriceData.FreeModel is cleared when a retry switches from a free
  group to a paid one, keeping it consistent with the billing session
  created at that point.
- getChannel refreshes GroupRatioInfo only after channel selection
  succeeds, and the retry loop records the channel in use_channel
  before PrepareTieredBillingForSelectedGroup can fail.
This commit is contained in:
Calcium-Ion
2026-08-01 09:35:51 +08:00
committed by GitHub
parent df43f80153
commit cfaba1dd67
4 changed files with 127 additions and 5 deletions
+4
View File
@@ -232,6 +232,10 @@ func (s *BillingSession) preConsume(c *gin.Context, quota int) *types.NewAPIErro
func (s *BillingSession) reserveFunding(delta int) error {
switch funding := s.funding.(type) {
case *WalletFunding:
// 与结算补扣(SettleBilling 正差额 → WalletFunding.Settle)语义一致:
// 全额无条件扣减,余额不足的部分记为欠费(余额可为负),不中断请求,
// 保证日志记录的预扣额度与用户余额的实际变动始终对账一致。
// DecreaseUserQuota 仅在数据库错误时失败。
if err := model.DecreaseUserQuota(funding.userId, delta, false); err != nil {
return types.NewError(err, types.ErrorCodeUpdateDataError, types.ErrOptionWithSkipRetry())
}