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
+17 -9
View File
@@ -149,7 +149,7 @@ func Redeem(key string, userId int) (quota int, err error) {
}
common.RandomSleep()
err = DB.Transaction(func(tx *gorm.DB) error {
err := tx.Set("gorm:query_option", "FOR UPDATE").Where(keyCol+" = ?", key).First(redemption).Error
err := lockForUpdate(tx).Where(keyCol+" = ?", key).First(redemption).Error
if err != nil {
return errors.New("无效的兑换码")
}
@@ -159,15 +159,23 @@ func Redeem(key string, userId int) (quota int, err error) {
if redemption.ExpiredTime != 0 && redemption.ExpiredTime < common.GetTimestamp() {
return errors.New("该兑换码已过期")
}
err = tx.Model(&User{}).Where("id = ?", userId).Update("quota", gorm.Expr("quota + ?", redemption.Quota)).Error
if err != nil {
return err
// Compare-and-swap on status: only the transaction that flips
// enabled -> used may credit quota, so a concurrent redeem of the
// same code loses here even without a row lock (e.g. on SQLite).
result := tx.Model(&Redemption{}).
Where("id = ? AND status = ?", redemption.Id, common.RedemptionCodeStatusEnabled).
Updates(map[string]interface{}{
"redeemed_time": common.GetTimestamp(),
"status": common.RedemptionCodeStatusUsed,
"used_user_id": userId,
})
if result.Error != nil {
return result.Error
}
redemption.RedeemedTime = common.GetTimestamp()
redemption.Status = common.RedemptionCodeStatusUsed
redemption.UsedUserId = userId
err = tx.Save(redemption).Error
return err
if result.RowsAffected == 0 {
return errors.New("该兑换码已被使用")
}
return tx.Model(&User{}).Where("id = ?", userId).Update("quota", gorm.Expr("quota + ?", redemption.Quota)).Error
})
if err != nil {
common.SysError("redemption failed: " + err.Error())