fix(billing): 异步任务退款时同步减少 used_quota (#6795)

* fix(billing): 异步任务退款时同步减少 used_quota
退款时仅恢复了 quota(剩余额度),但未同步减少 used_quota(已用额度),
导致"总额度"(quota + used_quota)随退款次数持续虚增,超出用户实际充值金额。

修复三处退款路径:
- RefundTaskQuota:任务失败完整退款
- RecalculateTaskQuota:差额结算退款分支
- controller/midjourney.go:Midjourney 任务失败退款

新增 model.UpdateUserUsedQuota 公开函数,仅调整 used_quota 不影响 request_count。

* fix(billing): 任务退款时同步扣减渠道 used_quota

* fix(billing): complete async task refund accounting

* style(model): group internal Midjourney fields

---------

Co-authored-by: CaIon <i@caion.me>
This commit is contained in:
wans10
2026-08-13 22:06:40 +08:00
committed by GitHub
co-authored by CaIon
parent ccd535ef8e
commit 58d4e9bd3b
9 changed files with 667 additions and 81 deletions
+55
View File
@@ -89,6 +89,61 @@ func TestUserUpdateDoesNotOverwriteConcurrentAccountingOrTokenChanges(t *testing
assert.Equal(t, "rotated-token", got.GetAccessToken())
}
func TestUsageAccountingSupportsSignedDirectAndBatchDeltas(t *testing.T) {
setupUserUpdateTestState(t)
resetBatchUpdateTestState(t)
user := User{
Id: 10,
Username: "usage-adjustment-user",
Password: "password",
Status: common.UserStatusEnabled,
UsedQuota: 1000,
RequestCount: 3,
}
channel := Channel{
Id: 10,
Name: "usage-adjustment-channel",
Key: "sk-test",
Status: common.ChannelStatusEnabled,
UsedQuota: 1000,
}
require.NoError(t, DB.Create(&user).Error)
require.NoError(t, DB.Create(&channel).Error)
UpdateUserUsedQuota(user.Id, -200)
UpdateUserUsedQuota(user.Id, 50)
UpdateChannelUsedQuota(channel.Id, -200)
UpdateChannelUsedQuota(channel.Id, 50)
var got User
require.NoError(t, DB.Select("used_quota", "request_count").First(&got, user.Id).Error)
assert.Equal(t, 850, got.UsedQuota)
assert.Equal(t, 3, got.RequestCount)
var gotChannel Channel
require.NoError(t, DB.Select("used_quota").First(&gotChannel, channel.Id).Error)
assert.Equal(t, int64(850), gotChannel.UsedQuota)
common.BatchUpdateEnabled = true
UpdateUserUsedQuota(user.Id, 400)
UpdateUserUsedQuota(user.Id, -100)
UpdateChannelUsedQuota(channel.Id, 400)
UpdateChannelUsedQuota(channel.Id, -100)
require.NoError(t, DB.Select("used_quota", "request_count").First(&got, user.Id).Error)
assert.Equal(t, 850, got.UsedQuota, "batch deltas must remain queued until flush")
assert.Equal(t, 3, got.RequestCount)
require.NoError(t, DB.Select("used_quota").First(&gotChannel, channel.Id).Error)
assert.Equal(t, int64(850), gotChannel.UsedQuota, "batch deltas must remain queued until flush")
batchUpdate()
require.NoError(t, DB.Select("used_quota", "request_count").First(&got, user.Id).Error)
assert.Equal(t, 1150, got.UsedQuota)
assert.Equal(t, 3, got.RequestCount)
require.NoError(t, DB.Select("used_quota").First(&gotChannel, channel.Id).Error)
assert.Equal(t, int64(1150), gotChannel.UsedQuota)
}
func TestUpdateUserAccessTokenOnlyUpdatesAccessToken(t *testing.T) {
setupUserUpdateTestState(t)