fix: settle excess token usage with negative balance (#304)
This commit is contained in:
1 parent
a23e399ccf
commit
c2a05db80f
3 files changed
+184
-11
No files matched your search
@@ -21,7 +21,6 @@ var (
|
||||
ErrTaskNotRetryable = errors.New("task is not retryable")
|
||||
ErrBillingStateConflict = errors.New("billing state conflict")
|
||||
ErrBillingUsageUnavailable = errors.New("billing usage unavailable")
|
||||
ErrBillingUnderreserved = errors.New("billing amount exceeds reservation")
|
||||
ErrChannelModelInUse = errors.New("channel model is in use")
|
||||
)
|
||||
|
||||
@@ -604,7 +603,10 @@ func (r *Repository) MarkBillingUncertain(id string, errorText string) error {
|
||||
}
|
||||
|
||||
func (r *Repository) SettleBillingOrder(id string, providerRequestID string) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
var observedUsage *BillingUsage
|
||||
var observedActual int64
|
||||
observedActualAvailable := false
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
var order model.BillingOrder
|
||||
if err := tx.First(&order, "id = ?", id).Error; err != nil {
|
||||
return err
|
||||
@@ -620,6 +622,7 @@ func (r *Repository) SettleBillingOrder(id string, providerRequestID string) err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
observedUsage = usage
|
||||
reserved := order.ReservedAmountMicrocredits
|
||||
if reserved <= 0 {
|
||||
reserved = order.AmountMicrocredits
|
||||
@@ -628,14 +631,14 @@ func (r *Repository) SettleBillingOrder(id string, providerRequestID string) err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if actual > reserved {
|
||||
return ErrBillingUnderreserved
|
||||
}
|
||||
refund := reserved - actual
|
||||
observedActual = actual
|
||||
observedActualAvailable = true
|
||||
refund := max(reserved-actual, int64(0))
|
||||
supplement := max(actual-reserved, int64(0))
|
||||
updated := tx.Model(&model.CreditAccount{}).
|
||||
Where("user_id = ? AND reserved_microcredits >= ?", order.UserID, reserved).
|
||||
Updates(map[string]any{
|
||||
"available_microcredits": gorm.Expr("available_microcredits + ?", refund),
|
||||
"available_microcredits": gorm.Expr("available_microcredits + ?", refund-supplement),
|
||||
"reserved_microcredits": gorm.Expr("reserved_microcredits - ?", reserved),
|
||||
"version": gorm.Expr("version + 1"), "updated_at": time.Now(),
|
||||
})
|
||||
@@ -660,10 +663,14 @@ func (r *Repository) SettleBillingOrder(id string, providerRequestID string) err
|
||||
if err := tx.Model(&order).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
consumeNote := ""
|
||||
if supplement > 0 {
|
||||
consumeNote = "Token 实际用量超过预授权,已补扣差额"
|
||||
}
|
||||
if err := tx.Create(&model.CreditLedgerEntry{ID: newRepositoryID(), UserID: order.UserID, Type: model.CreditLedgerConsume,
|
||||
AmountMicrocredits: -actual, ReservedDeltaMicrocredits: -reserved,
|
||||
AmountMicrocredits: -actual, AvailableDeltaMicrocredits: -supplement, ReservedDeltaMicrocredits: -reserved,
|
||||
AvailableAfterMicrocredits: account.AvailableMicrocredits, ReservedAfterMicrocredits: account.ReservedMicrocredits,
|
||||
BillingOrderID: order.ID, Model: order.Model, ChannelID: order.ChannelID, Scene: order.Scene}).Error; err != nil {
|
||||
BillingOrderID: order.ID, Model: order.Model, ChannelID: order.ChannelID, Scene: order.Scene, Note: consumeNote}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if refund > 0 {
|
||||
@@ -715,6 +722,26 @@ func (r *Repository) SettleBillingOrder(id string, providerRequestID string) err
|
||||
Scene: order.Scene,
|
||||
}).Error
|
||||
})
|
||||
if err != nil && observedUsage != nil {
|
||||
// usage 是上游已经确认的事实;即使结算因账户状态异常回滚,也要保留给用户和管理员核对。
|
||||
updates := map[string]any{
|
||||
"input_tokens": observedUsage.InputTokens, "output_tokens": observedUsage.OutputTokens,
|
||||
"cached_tokens": observedUsage.CachedTokens, "usage_available": true, "updated_at": time.Now(),
|
||||
}
|
||||
if observedActualAvailable {
|
||||
updates["actual_amount_microcredits"] = observedActual
|
||||
}
|
||||
if providerRequestID != "" {
|
||||
updates["provider_request_id"] = providerRequestID
|
||||
}
|
||||
usageErr := r.db.Model(&model.BillingOrder{}).
|
||||
Where("id = ? AND status NOT IN ?", id, []model.BillingStatus{model.BillingStatusSettled, model.BillingStatusRefunded}).
|
||||
Updates(updates).Error
|
||||
if usageErr != nil {
|
||||
return errors.Join(err, usageErr)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func zeroPricedTokenOrder(order model.BillingOrder) bool {
|
||||
@@ -822,8 +849,11 @@ func (r *Repository) AdjustCredits(userID string, actorUserID string, amount int
|
||||
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&account).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
updated := tx.Model(&model.CreditAccount{}).
|
||||
Where("user_id = ? AND available_microcredits + ? >= 0", userID, amount).
|
||||
accountQuery := tx.Model(&model.CreditAccount{}).Where("user_id = ?", userID)
|
||||
if amount < 0 {
|
||||
accountQuery = accountQuery.Where("available_microcredits + ? >= 0", amount)
|
||||
}
|
||||
updated := accountQuery.
|
||||
Updates(map[string]any{
|
||||
"available_microcredits": gorm.Expr("available_microcredits + ?", amount),
|
||||
"version": gorm.Expr("version + 1"),
|
||||
|
||||
Reference in new issue
Block a user