fix(topup): settle recharge orders atomically
This commit is contained in:
+108
-38
@@ -106,12 +106,82 @@ func UpdatePendingTopUpStatus(tradeNo string, expectedPaymentProvider string, ta
|
||||
})
|
||||
}
|
||||
|
||||
// RechargeEpay 原子完成易支付订单:订单行锁、状态校验、成功更新与用户额度增加
|
||||
// 在同一个事务内完成,因此同一订单的并发/重复回调(包括多实例部署下)最多充值一次。
|
||||
// alreadyDone=true 表示订单此前已完成,本次为幂等重复回调。
|
||||
// 进程内的 LockOrder 只是优化,正确性由本函数的数据库行锁保证。
|
||||
func RechargeEpay(tradeNo string, actualPaymentMethod string, callerIp string) (alreadyDone bool, err error) {
|
||||
if tradeNo == "" {
|
||||
return false, errors.New("未提供支付单号")
|
||||
}
|
||||
|
||||
refCol := "`trade_no`"
|
||||
if common.UsingMainDatabase(common.DatabaseTypePostgreSQL) {
|
||||
refCol = `"trade_no"`
|
||||
}
|
||||
|
||||
var quotaToAdd int
|
||||
topUp := &TopUp{}
|
||||
err = DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := lockForUpdate(tx).Where(refCol+" = ?", tradeNo).First(topUp).Error; err != nil {
|
||||
return ErrTopUpNotFound
|
||||
}
|
||||
if topUp.PaymentProvider != PaymentProviderEpay {
|
||||
return ErrPaymentMethodMismatch
|
||||
}
|
||||
if topUp.Status == common.TopUpStatusSuccess {
|
||||
alreadyDone = true
|
||||
return nil
|
||||
}
|
||||
if topUp.Status != common.TopUpStatusPending {
|
||||
return ErrTopUpStatusInvalid
|
||||
}
|
||||
if actualPaymentMethod != "" && topUp.PaymentMethod != actualPaymentMethod {
|
||||
topUp.PaymentMethod = actualPaymentMethod
|
||||
}
|
||||
var quotaErr error
|
||||
quotaToAdd, quotaErr = common.QuotaFromDecimalStrict(
|
||||
decimal.NewFromInt(topUp.Amount).Mul(decimal.NewFromFloat(common.QuotaPerUnit)),
|
||||
)
|
||||
if quotaErr != nil || quotaToAdd <= 0 {
|
||||
return errors.New("无效的充值额度")
|
||||
}
|
||||
topUp.CompleteTime = common.GetTimestamp()
|
||||
topUp.Status = common.TopUpStatusSuccess
|
||||
if err := tx.Save(topUp).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
result := tx.Model(&User{}).Where("id = ?", topUp.UserId).Update("quota", gorm.Expr("quota + ?", quotaToAdd))
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
if !errors.Is(err, ErrTopUpNotFound) && !errors.Is(err, ErrPaymentMethodMismatch) && !errors.Is(err, ErrTopUpStatusInvalid) {
|
||||
common.SysError("epay topup failed: " + err.Error())
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
if alreadyDone {
|
||||
return true, nil
|
||||
}
|
||||
syncCreditUserQuotaCache(topUp.UserId, quotaToAdd, "epay topup")
|
||||
|
||||
common.SysLog(fmt.Sprintf("易支付充值成功 trade_no=%s user_id=%d quota_to_add=%d money=%.2f", topUp.TradeNo, topUp.UserId, quotaToAdd, topUp.Money))
|
||||
RecordTopupLog(topUp.UserId, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%f", logger.LogQuota(quotaToAdd), topUp.Money), callerIp, topUp.PaymentMethod, PaymentProviderEpay)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func Recharge(referenceId string, customerId string, callerIp string) (err error) {
|
||||
if referenceId == "" {
|
||||
return errors.New("未提供支付单号")
|
||||
}
|
||||
|
||||
var quota float64
|
||||
var quota int
|
||||
topUp := &TopUp{}
|
||||
|
||||
refCol := "`trade_no`"
|
||||
@@ -140,21 +210,23 @@ func Recharge(referenceId string, customerId string, callerIp string) (err error
|
||||
return err
|
||||
}
|
||||
|
||||
quota = topUp.Money * common.QuotaPerUnit
|
||||
err = tx.Model(&User{}).Where("id = ?", topUp.UserId).Updates(map[string]interface{}{"stripe_customer": customerId, "quota": gorm.Expr("quota + ?", quota)}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
quota, err = common.QuotaFromDecimalStrict(
|
||||
decimal.NewFromFloat(topUp.Money).Mul(decimal.NewFromFloat(common.QuotaPerUnit)),
|
||||
)
|
||||
if err != nil || quota <= 0 {
|
||||
return errors.New("无效的充值额度")
|
||||
}
|
||||
|
||||
return nil
|
||||
return tx.Model(&User{}).Where("id = ?", topUp.UserId).
|
||||
Updates(map[string]interface{}{"stripe_customer": customerId, "quota": gorm.Expr("quota + ?", quota)}).Error
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
common.SysError("topup failed: " + err.Error())
|
||||
return errors.New("充值失败,请稍后重试")
|
||||
}
|
||||
syncCreditUserQuotaCache(topUp.UserId, quota, "stripe topup")
|
||||
|
||||
RecordTopupLog(topUp.UserId, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%d", logger.FormatQuota(int(quota)), topUp.Amount), callerIp, topUp.PaymentMethod, PaymentMethodStripe)
|
||||
RecordTopupLog(topUp.UserId, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%d", logger.FormatQuota(quota), topUp.Amount), callerIp, topUp.PaymentMethod, PaymentMethodStripe)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -351,15 +423,17 @@ func ManualCompleteTopUp(tradeNo string, callerIp string) error {
|
||||
// 计算应充值额度:
|
||||
// - Stripe 订单:Money 代表经分组倍率换算后的美元数量,直接 * QuotaPerUnit
|
||||
// - 其他订单(如易支付):Amount 为美元数量,* QuotaPerUnit
|
||||
var quotaErr error
|
||||
if topUp.PaymentProvider == PaymentProviderStripe {
|
||||
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
|
||||
quotaToAdd = int(decimal.NewFromFloat(topUp.Money).Mul(dQuotaPerUnit).IntPart())
|
||||
quotaToAdd, quotaErr = common.QuotaFromDecimalStrict(
|
||||
decimal.NewFromFloat(topUp.Money).Mul(decimal.NewFromFloat(common.QuotaPerUnit)),
|
||||
)
|
||||
} else {
|
||||
dAmount := decimal.NewFromInt(topUp.Amount)
|
||||
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
|
||||
quotaToAdd = int(dAmount.Mul(dQuotaPerUnit).IntPart())
|
||||
quotaToAdd, quotaErr = common.QuotaFromDecimalStrict(
|
||||
decimal.NewFromInt(topUp.Amount).Mul(decimal.NewFromFloat(common.QuotaPerUnit)),
|
||||
)
|
||||
}
|
||||
if quotaToAdd <= 0 {
|
||||
if quotaErr != nil || quotaToAdd <= 0 {
|
||||
return errors.New("无效的充值额度")
|
||||
}
|
||||
|
||||
@@ -386,6 +460,7 @@ func ManualCompleteTopUp(tradeNo string, callerIp string) error {
|
||||
}
|
||||
|
||||
// 事务外记录日志,避免阻塞
|
||||
syncCreditUserQuotaCache(userId, quotaToAdd, "manual topup")
|
||||
RecordTopupLog(userId, fmt.Sprintf("管理员补单成功,充值金额: %v,支付金额:%f", logger.FormatQuota(quotaToAdd), payMoney), callerIp, paymentMethod, "admin")
|
||||
return nil
|
||||
}
|
||||
@@ -394,7 +469,7 @@ func RechargeCreem(referenceId string, customerEmail string, customerName string
|
||||
return errors.New("未提供支付单号")
|
||||
}
|
||||
|
||||
var quota int64
|
||||
var quota int
|
||||
topUp := &TopUp{}
|
||||
|
||||
refCol := "`trade_no`"
|
||||
@@ -424,7 +499,10 @@ func RechargeCreem(referenceId string, customerEmail string, customerName string
|
||||
}
|
||||
|
||||
// Creem 直接使用 Amount 作为充值额度(整数)
|
||||
quota = topUp.Amount
|
||||
quota, err = common.QuotaFromDecimalStrict(decimal.NewFromInt(topUp.Amount))
|
||||
if err != nil || quota <= 0 {
|
||||
return errors.New("无效的充值额度")
|
||||
}
|
||||
|
||||
// 构建更新字段,优先使用邮箱,如果邮箱为空则使用用户名
|
||||
updateFields := map[string]interface{}{
|
||||
@@ -446,18 +524,14 @@ func RechargeCreem(referenceId string, customerEmail string, customerName string
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.Model(&User{}).Where("id = ?", topUp.UserId).Updates(updateFields).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return tx.Model(&User{}).Where("id = ?", topUp.UserId).Updates(updateFields).Error
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
common.SysError("creem topup failed: " + err.Error())
|
||||
return errors.New("充值失败,请稍后重试")
|
||||
}
|
||||
syncCreditUserQuotaCache(topUp.UserId, quota, "creem topup")
|
||||
|
||||
RecordTopupLog(topUp.UserId, fmt.Sprintf("使用Creem充值成功,充值额度: %v,支付金额:%.2f", quota, topUp.Money), callerIp, topUp.PaymentMethod, PaymentMethodCreem)
|
||||
|
||||
@@ -495,10 +569,10 @@ func RechargeWaffo(tradeNo string, callerIp string) (err error) {
|
||||
return errors.New("充值订单状态错误")
|
||||
}
|
||||
|
||||
dAmount := decimal.NewFromInt(topUp.Amount)
|
||||
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
|
||||
quotaToAdd = int(dAmount.Mul(dQuotaPerUnit).IntPart())
|
||||
if quotaToAdd <= 0 {
|
||||
quotaToAdd, err = common.QuotaFromDecimalStrict(
|
||||
decimal.NewFromInt(topUp.Amount).Mul(decimal.NewFromFloat(common.QuotaPerUnit)),
|
||||
)
|
||||
if err != nil || quotaToAdd <= 0 {
|
||||
return errors.New("无效的充值额度")
|
||||
}
|
||||
|
||||
@@ -508,17 +582,14 @@ func RechargeWaffo(tradeNo string, callerIp string) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Model(&User{}).Where("id = ?", topUp.UserId).Update("quota", gorm.Expr("quota + ?", quotaToAdd)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return tx.Model(&User{}).Where("id = ?", topUp.UserId).Update("quota", gorm.Expr("quota + ?", quotaToAdd)).Error
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
common.SysError("waffo topup failed: " + err.Error())
|
||||
return errors.New("充值失败,请稍后重试")
|
||||
}
|
||||
syncCreditUserQuotaCache(topUp.UserId, quotaToAdd, "waffo topup")
|
||||
|
||||
if quotaToAdd > 0 {
|
||||
RecordTopupLog(topUp.UserId, fmt.Sprintf("Waffo充值成功,充值额度: %v,支付金额: %.2f", logger.FormatQuota(quotaToAdd), topUp.Money), callerIp, topUp.PaymentMethod, PaymentMethodWaffo)
|
||||
@@ -558,8 +629,10 @@ func RechargeWaffoPancake(tradeNo string) (err error) {
|
||||
return errors.New("充值订单状态错误")
|
||||
}
|
||||
|
||||
quotaToAdd = int(decimal.NewFromInt(topUp.Amount).Mul(decimal.NewFromFloat(common.QuotaPerUnit)).IntPart())
|
||||
if quotaToAdd <= 0 {
|
||||
quotaToAdd, err = common.QuotaFromDecimalStrict(
|
||||
decimal.NewFromInt(topUp.Amount).Mul(decimal.NewFromFloat(common.QuotaPerUnit)),
|
||||
)
|
||||
if err != nil || quotaToAdd <= 0 {
|
||||
return errors.New("无效的充值额度")
|
||||
}
|
||||
|
||||
@@ -569,17 +642,14 @@ func RechargeWaffoPancake(tradeNo string) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Model(&User{}).Where("id = ?", topUp.UserId).Update("quota", gorm.Expr("quota + ?", quotaToAdd)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return tx.Model(&User{}).Where("id = ?", topUp.UserId).Update("quota", gorm.Expr("quota + ?", quotaToAdd)).Error
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
common.SysError("waffo pancake topup failed: " + err.Error())
|
||||
return errors.New("充值失败,请稍后重试")
|
||||
}
|
||||
syncCreditUserQuotaCache(topUp.UserId, quotaToAdd, "waffo pancake topup")
|
||||
|
||||
if quotaToAdd > 0 {
|
||||
RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("Waffo Pancake充值成功,充值额度: %v,支付金额: %.2f", logger.FormatQuota(quotaToAdd), topUp.Money))
|
||||
|
||||
Reference in New Issue
Block a user