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:
@@ -23,6 +23,9 @@ type Midjourney struct {
|
||||
Quota int `json:"quota"`
|
||||
Buttons string `json:"buttons"`
|
||||
Properties string `json:"properties"`
|
||||
|
||||
TokenId int `json:"-" gorm:"default:0"`
|
||||
BillingChannelId int `json:"-" gorm:"default:0"`
|
||||
}
|
||||
|
||||
// TaskQueryParams 用于包含所有搜索条件的结构体,可以根据需求添加更多字段
|
||||
@@ -170,6 +173,19 @@ func (midjourney *Midjourney) Update() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (midjourney *Midjourney) UpdateBillingState() error {
|
||||
return DB.Model(midjourney).
|
||||
Select("quota", "token_id", "billing_channel_id").
|
||||
Updates(midjourney).Error
|
||||
}
|
||||
|
||||
func (midjourney *Midjourney) GetBillingChannelId() int {
|
||||
if midjourney.BillingChannelId > 0 {
|
||||
return midjourney.BillingChannelId
|
||||
}
|
||||
return midjourney.ChannelId
|
||||
}
|
||||
|
||||
// UpdateWithStatus performs a conditional UPDATE guarded by fromStatus (CAS).
|
||||
// Returns (true, nil) if this caller won the update, (false, nil) if
|
||||
// another process already moved the task out of fromStatus.
|
||||
|
||||
@@ -1353,6 +1353,17 @@ func UpdateUserUsedQuotaAndRequestCount(id int, quota int) {
|
||||
updateUserUsedQuotaAndRequestCount(id, quota, 1)
|
||||
}
|
||||
|
||||
// UpdateUserUsedQuota adjusts accumulated usage without changing request count.
|
||||
func UpdateUserUsedQuota(id int, quota int) {
|
||||
if common.BatchUpdateEnabled {
|
||||
addNewRecord(BatchUpdateTypeUsedQuota, id, quota)
|
||||
return
|
||||
}
|
||||
if err := DB.Model(&User{}).Where("id = ?", id).Update("used_quota", gorm.Expr("used_quota + ?", quota)).Error; err != nil {
|
||||
common.SysLog("failed to update user used quota: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func updateUserUsedQuotaAndRequestCount(id int, quota int, count int) {
|
||||
err := DB.Model(&User{}).Where("id = ?", id).Updates(
|
||||
map[string]interface{}{
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user