fix: harden concurrent quota and status updates

This commit is contained in:
CaIon
2026-08-11 22:03:47 +08:00
parent 50e5377ea5
commit ccd535ef8e
14 changed files with 702 additions and 203 deletions
+13 -3
View File
@@ -600,6 +600,12 @@ func CompleteSubscriptionOrder(tradeNo string, providerPayload string, expectedP
if !plan.Enabled {
// still allow completion for already purchased orders
}
// 锁定用户行:并发完成同一用户的不同订单(包括多实例部署下)时,
// 使 CreateUserSubscriptionFromPlanTx 的 MaxPurchasePerUser 检查按用户串行。
var userRow User
if err := lockForUpdate(tx).Select("id").Where("id = ?", order.UserId).First(&userRow).Error; err != nil {
return err
}
subscription, err := CreateUserSubscriptionFromPlanTx(tx, order.UserId, plan, "order")
if err != nil {
return err
@@ -712,6 +718,11 @@ func AdminBindSubscription(userId int, planId int, sourceNote string) (string, e
}
groupChanged := false
err = DB.Transaction(func(tx *gorm.DB) error {
// 与 CompleteSubscriptionOrder 一致:先锁用户行,再做购买次数检查。
var userRow User
if err := lockForUpdate(tx).Select("id").Where("id = ?", userId).First(&userRow).Error; err != nil {
return err
}
subscription, err := CreateUserSubscriptionFromPlanTx(tx, userId, plan, "admin")
if err == nil {
groupChanged = subscription.PrevUserGroup != ""
@@ -737,9 +748,8 @@ func calcSubscriptionBalanceQuota(priceAmount float64) (int, error) {
}
quota := decimal.NewFromFloat(priceAmount).
Mul(decimal.NewFromFloat(common.QuotaPerUnit)).
Ceil().
IntPart()
return int(quota), nil
Ceil()
return common.QuotaFromDecimalStrict(quota)
}
// PurchaseSubscriptionWithBalance creates a subscription by deducting the user's wallet quota.