Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion model/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -860,8 +860,32 @@ func UpdateChannelUsedQuota(id int, quota int) {
updateChannelUsedQuota(id, quota)
}

func DecreaseChannelUsedQuota(id int, quota int) {
if id <= 0 || quota <= 0 {
return
}
if common.BatchUpdateEnabled {
addNewRecord(BatchUpdateTypeChannelUsedQuota, id, -quota)
return
}
err := DB.Model(&Channel{}).Where("id = ?", id).Update(
"used_quota",
gorm.Expr("CASE WHEN used_quota < ? THEN 0 ELSE used_quota - ? END", quota, quota),
).Error
if err != nil {
common.SysLog(fmt.Sprintf("failed to decrease channel used quota: channel_id=%d, delta_quota=%d, error=%v", id, quota, err))
}
}

func updateChannelUsedQuota(id int, quota int) {
err := DB.Model(&Channel{}).Where("id = ?", id).Update("used_quota", gorm.Expr("used_quota + ?", quota)).Error
var expr clause.Expr
if quota < 0 {
refund := -quota
expr = gorm.Expr("CASE WHEN used_quota < ? THEN 0 ELSE used_quota - ? END", refund, refund)
} else {
expr = gorm.Expr("used_quota + ?", quota)
}
err := DB.Model(&Channel{}).Where("id = ?", id).Update("used_quota", expr).Error
if err != nil {
common.SysLog(fmt.Sprintf("failed to update channel used quota: channel_id=%d, delta_quota=%d, error=%v", id, quota, err))
}
Expand Down
39 changes: 32 additions & 7 deletions model/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,23 @@ func UpdateUserUsedQuotaAndRequestCount(id int, quota int) {
updateUserUsedQuotaAndRequestCount(id, quota, 1)
}

func DecreaseUserUsedQuota(id int, quota int) {
if id <= 0 || quota <= 0 {
return
}
if common.BatchUpdateEnabled {
addNewRecord(BatchUpdateTypeUsedQuota, id, -quota)
return
}
err := DB.Model(&User{}).Where("id = ?", id).Update(
"used_quota",
gorm.Expr("CASE WHEN used_quota < ? THEN 0 ELSE used_quota - ? END", quota, quota),
).Error
if err != nil {
common.SysLog("failed to decrease user used quota: " + err.Error())
}
}

func updateUserUsedQuotaAndRequestCount(id int, quota int, count int) {
err := DB.Model(&User{}).Where("id = ?", id).Updates(
map[string]interface{}{
Expand All @@ -1159,13 +1176,21 @@ func updateUserQuotaUsedQuotaAndRequestCount(id int, quota int, usedQuota int, r
return
}

err := DB.Model(&User{}).Where("id = ?", id).Updates(
map[string]interface{}{
"quota": gorm.Expr("quota + ?", quota),
"used_quota": gorm.Expr("used_quota + ?", usedQuota),
"request_count": gorm.Expr("request_count + ?", requestCount),
},
).Error
updates := map[string]interface{}{}
if quota != 0 {
updates["quota"] = gorm.Expr("quota + ?", quota)
}
if usedQuota > 0 {
updates["used_quota"] = gorm.Expr("used_quota + ?", usedQuota)
} else if usedQuota < 0 {
refund := -usedQuota
updates["used_quota"] = gorm.Expr("CASE WHEN used_quota < ? THEN 0 ELSE used_quota - ? END", refund, refund)
}
if requestCount != 0 {
updates["request_count"] = gorm.Expr("request_count + ?", requestCount)
}

err := DB.Model(&User{}).Where("id = ?", id).Updates(updates).Error
if err != nil {
common.SysLog("failed to batch update user quota, used quota and request count: " + err.Error())
}
Expand Down
8 changes: 7 additions & 1 deletion service/task_billing.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,11 @@ func RefundTaskQuota(ctx context.Context, task *model.Task, reason string) {
// 2. 退还令牌额度
taskAdjustTokenQuota(ctx, task, -quota)

// 3. 记录日志
// 3. 回退已用额度统计,避免退款后“剩余额度 + 已用额度”虚增。
model.DecreaseUserUsedQuota(task.UserId, quota)
model.DecreaseChannelUsedQuota(task.ChannelId, quota)

// 4. 记录日志
other := taskBillingOther(task)
other["task_id"] = task.TaskID
other["reason"] = reason
Expand Down Expand Up @@ -231,6 +235,8 @@ func RecalculateTaskQuota(ctx context.Context, task *model.Task, actualQuota int
} else {
logType = model.LogTypeRefund
logQuota = -quotaDelta
model.DecreaseUserUsedQuota(task.UserId, logQuota)
model.DecreaseChannelUsedQuota(task.ChannelId, logQuota)
}
other := taskBillingOther(task)
other["task_id"] = task.TaskID
Expand Down
41 changes: 41 additions & 0 deletions service/task_billing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,20 @@ func getUserQuota(t *testing.T, id int) int {
return user.Quota
}

func getUserUsedQuota(t *testing.T, id int) int {
t.Helper()
var user model.User
require.NoError(t, model.DB.Select("used_quota").Where("id = ?", id).First(&user).Error)
return user.UsedQuota
}

func getChannelUsedQuota(t *testing.T, id int) int64 {
t.Helper()
var channel model.Channel
require.NoError(t, model.DB.Select("used_quota").Where("id = ?", id).First(&channel).Error)
return channel.UsedQuota
}

func getTokenRemainQuota(t *testing.T, id int) int {
t.Helper()
var token model.Token
Expand Down Expand Up @@ -293,6 +307,29 @@ func TestRefundTaskQuota_NoToken(t *testing.T) {
assert.Equal(t, model.LogTypeRefund, log.Type)
}

func TestRefundTaskQuota_DecreasesUsageTotals(t *testing.T) {
truncate(t)
ctx := context.Background()

const userID, tokenID, channelID = 5, 5, 5
const initQuota, preConsumed = 10000, 1500
const tokenRemain = 5000

seedUser(t, userID, initQuota)
seedToken(t, tokenID, userID, "sk-usage-refund", tokenRemain)
seedChannel(t, channelID)
require.NoError(t, model.DB.Model(&model.User{}).Where("id = ?", userID).Update("used_quota", preConsumed).Error)
require.NoError(t, model.DB.Model(&model.Channel{}).Where("id = ?", channelID).Update("used_quota", preConsumed).Error)

task := makeTask(userID, channelID, preConsumed, tokenID, BillingSourceWallet, 0)

RefundTaskQuota(ctx, task, "failed task should not inflate total quota")

assert.Equal(t, initQuota+preConsumed, getUserQuota(t, userID))
assert.Zero(t, getUserUsedQuota(t, userID))
assert.Zero(t, getChannelUsedQuota(t, channelID))
}

// ===========================================================================
// RecalculateTaskQuota tests
// ===========================================================================
Expand Down Expand Up @@ -342,6 +379,8 @@ func TestRecalculate_NegativeDelta(t *testing.T) {
seedUser(t, userID, initQuota)
seedToken(t, tokenID, userID, "sk-recalc-neg", tokenRemain)
seedChannel(t, channelID)
require.NoError(t, model.DB.Model(&model.User{}).Where("id = ?", userID).Update("used_quota", preConsumed).Error)
require.NoError(t, model.DB.Model(&model.Channel{}).Where("id = ?", channelID).Update("used_quota", preConsumed).Error)

task := makeTask(userID, channelID, preConsumed, tokenID, BillingSourceWallet, 0)

Expand All @@ -355,6 +394,8 @@ func TestRecalculate_NegativeDelta(t *testing.T) {

// task.Quota updated
assert.Equal(t, actualQuota, task.Quota)
assert.Equal(t, actualQuota, getUserUsedQuota(t, userID))
assert.EqualValues(t, actualQuota, getChannelUsedQuota(t, channelID))

// Log type should be Refund
log := getLastLog(t)
Expand Down