fix(model): centralize row locking in transactional flows

This commit is contained in:
CaIon
2026-07-07 12:40:09 +08:00
parent bae799ccb1
commit 70ea899e37
7 changed files with 177 additions and 25 deletions
+81
View File
@@ -1,6 +1,7 @@
package model
import (
"sync"
"testing"
"github.com/QuantumNous/new-api/common"
@@ -98,3 +99,83 @@ func TestSearchRedemptionsFiltersAndPaginates(t *testing.T) {
})
}
}
func setupRedeemFixture(t *testing.T, quota int) (userId int, key string) {
t.Helper()
require.NoError(t, DB.AutoMigrate(&Redemption{}))
require.NoError(t, DB.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Delete(&Redemption{}).Error)
t.Cleanup(func() {
require.NoError(t, DB.Session(&gorm.Session{AllowGlobalUpdate: true}).Unscoped().Delete(&Redemption{}).Error)
DB.Exec("DELETE FROM users")
DB.Exec("DELETE FROM logs")
})
user := &User{Username: "redeem-user", Password: "password", Status: common.UserStatusEnabled, Quota: 0}
require.NoError(t, DB.Create(user).Error)
key = "10000000000000000000000000000001"
redemption := &Redemption{
Name: "redeem-test",
Key: key,
Status: common.RedemptionCodeStatusEnabled,
Quota: quota,
CreatedTime: common.GetTimestamp(),
}
require.NoError(t, DB.Create(redemption).Error)
return user.Id, key
}
func TestRedeemCreditsQuotaExactlyOnce(t *testing.T) {
userId, key := setupRedeemFixture(t, 500)
quota, err := Redeem(key, userId)
require.NoError(t, err)
assert.Equal(t, 500, quota)
var user User
require.NoError(t, DB.First(&user, "id = ?", userId).Error)
assert.Equal(t, 500, user.Quota)
var redemption Redemption
require.NoError(t, DB.First(&redemption, "name = ?", "redeem-test").Error)
assert.Equal(t, common.RedemptionCodeStatusUsed, redemption.Status)
assert.Equal(t, userId, redemption.UsedUserId)
// Redeeming the same code again must fail and must not credit quota.
_, err = Redeem(key, userId)
require.Error(t, err)
require.NoError(t, DB.First(&user, "id = ?", userId).Error)
assert.Equal(t, 500, user.Quota)
}
// Exactly one of several concurrent redeems of the same code may win, and
// quota must be credited exactly once.
func TestRedeemConcurrentSingleSuccess(t *testing.T) {
userId, key := setupRedeemFixture(t, 300)
const goroutines = 5
successes := make([]bool, goroutines)
var wg sync.WaitGroup
wg.Add(goroutines)
for i := 0; i < goroutines; i++ {
go func(idx int) {
defer wg.Done()
if _, err := Redeem(key, userId); err == nil {
successes[idx] = true
}
}(i)
}
wg.Wait()
successCount := 0
for _, ok := range successes {
if ok {
successCount++
}
}
assert.Equal(t, 1, successCount, "exactly one concurrent redeem should succeed")
var user User
require.NoError(t, DB.First(&user, "id = ?", userId).Error)
assert.Equal(t, 300, user.Quota, "quota must be credited exactly once")
}