fix(topup): guard wallet quota during recharge
This commit is contained in:
+30
-14
@@ -216,31 +216,47 @@ func getMaxTopUpAmount() int64 {
|
||||
return maxStoredAmount.IntPart()
|
||||
}
|
||||
|
||||
func validateCreditedQuota(quota decimal.Decimal) error {
|
||||
func validateCreditedQuota(quota decimal.Decimal) (int, error) {
|
||||
value, err := common.QuotaFromDecimalStrict(quota)
|
||||
if err != nil {
|
||||
return errors.New("充值额度超出系统可表示范围")
|
||||
return 0, errors.New("充值额度超出系统可表示范围")
|
||||
}
|
||||
if value <= 0 {
|
||||
return errors.New("充值额度必须大于 0")
|
||||
return 0, errors.New("充值额度必须大于 0")
|
||||
}
|
||||
return nil
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func validateTopUpQuota(amount int64) error {
|
||||
func validateTopUpQuota(amount int64) (int, error) {
|
||||
quota, err := getTopUpQuota(amount)
|
||||
if err == nil && quota > 0 {
|
||||
return nil
|
||||
return quota, nil
|
||||
}
|
||||
maxAmount := getMaxTopUpAmount()
|
||||
if maxAmount > 0 && amount > maxAmount {
|
||||
return fmt.Errorf("单笔充值数量不能大于 %d", maxAmount)
|
||||
return 0, fmt.Errorf("单笔充值数量不能大于 %d", maxAmount)
|
||||
}
|
||||
return errors.New("充值数量无效")
|
||||
return 0, errors.New("充值数量无效")
|
||||
}
|
||||
|
||||
func rejectInvalidTopUpQuota(c *gin.Context, amount int64) bool {
|
||||
if err := validateTopUpQuota(amount); err != nil {
|
||||
func rejectInvalidCreditedQuota(c *gin.Context, userId int, quota decimal.Decimal) bool {
|
||||
creditedQuota, err := validateCreditedQuota(quota)
|
||||
if err == nil {
|
||||
err = model.ValidateTopUpQuotaCapacity(userId, creditedQuota)
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": err.Error()})
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func rejectInvalidTopUpQuota(c *gin.Context, userId int, amount int64) bool {
|
||||
creditedQuota, err := validateTopUpQuota(amount)
|
||||
if err == nil {
|
||||
err = model.ValidateTopUpQuotaCapacity(userId, creditedQuota)
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": err.Error()})
|
||||
return true
|
||||
}
|
||||
@@ -258,11 +274,11 @@ func RequestEpay(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", getMinTopup())})
|
||||
return
|
||||
}
|
||||
if rejectInvalidTopUpQuota(c, req.Amount) {
|
||||
id := c.GetInt("id")
|
||||
if rejectInvalidTopUpQuota(c, id, req.Amount) {
|
||||
return
|
||||
}
|
||||
|
||||
id := c.GetInt("id")
|
||||
group, err := model.GetUserGroup(id, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": "获取用户分组失败"})
|
||||
@@ -475,10 +491,10 @@ func RequestAmount(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", getMinTopup())})
|
||||
return
|
||||
}
|
||||
if rejectInvalidTopUpQuota(c, req.Amount) {
|
||||
id := c.GetInt("id")
|
||||
if rejectInvalidTopUpQuota(c, id, req.Amount) {
|
||||
return
|
||||
}
|
||||
id := c.GetInt("id")
|
||||
group, err := model.GetUserGroup(id, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": "获取用户分组失败"})
|
||||
|
||||
@@ -97,13 +97,17 @@ func (*CreemAdaptor) RequestPay(c *gin.Context, req *CreemPayRequest) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": "产品不存在"})
|
||||
return
|
||||
}
|
||||
if err := validateCreditedQuota(decimal.NewFromInt(selectedProduct.Quota)); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": err.Error()})
|
||||
|
||||
id := c.GetInt("id")
|
||||
if rejectInvalidCreditedQuota(c, id, decimal.NewFromInt(selectedProduct.Quota)) {
|
||||
return
|
||||
}
|
||||
|
||||
id := c.GetInt("id")
|
||||
user, _ := model.GetUserById(id, false)
|
||||
user, err := model.GetUserById(id, false)
|
||||
if err != nil || user == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": "用户不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
// 生成唯一的订单引用ID
|
||||
reference := fmt.Sprintf("creem-api-ref-%d-%d-%s", user.Id, time.Now().UnixMilli(), randstr.String(4))
|
||||
|
||||
@@ -7,11 +7,14 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestTopUpQuotaValidation(t *testing.T) {
|
||||
@@ -84,8 +87,9 @@ func TestValidateTopUpQuotaReturnsMaximumAmount(t *testing.T) {
|
||||
Div(decimal.NewFromFloat(common.QuotaPerUnit)).
|
||||
Floor().IntPart()
|
||||
|
||||
require.NoError(t, validateTopUpQuota(maxAmount))
|
||||
err := validateTopUpQuota(maxAmount + 1)
|
||||
_, err := validateTopUpQuota(maxAmount)
|
||||
require.NoError(t, err)
|
||||
_, err = validateTopUpQuota(maxAmount + 1)
|
||||
require.EqualError(t, err, "单笔充值数量不能大于 4294")
|
||||
}
|
||||
|
||||
@@ -115,12 +119,60 @@ func TestRequestAmountRejectsTopUpThatCannotBeSettled(t *testing.T) {
|
||||
assert.JSONEq(t, `{"message":"error","data":"单笔充值数量不能大于 4294"}`, recorder.Body.String())
|
||||
}
|
||||
|
||||
func TestRequestAmountRejectsTopUpThatWouldOverflowWallet(t *testing.T) {
|
||||
oldQuotaPerUnit := common.QuotaPerUnit
|
||||
oldDisplayType := operation_setting.GetGeneralSetting().QuotaDisplayType
|
||||
oldDB := model.DB
|
||||
common.QuotaPerUnit = 500000
|
||||
operation_setting.GetGeneralSetting().QuotaDisplayType = operation_setting.QuotaDisplayTypeUSD
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&model.User{}))
|
||||
model.DB = db
|
||||
t.Cleanup(func() {
|
||||
common.QuotaPerUnit = oldQuotaPerUnit
|
||||
operation_setting.GetGeneralSetting().QuotaDisplayType = oldDisplayType
|
||||
model.DB = oldDB
|
||||
sqlDB, dbErr := db.DB()
|
||||
if dbErr == nil {
|
||||
require.NoError(t, sqlDB.Close())
|
||||
}
|
||||
})
|
||||
|
||||
require.NoError(t, model.DB.Create(&model.User{
|
||||
Id: 42,
|
||||
Username: "topup_capacity_user",
|
||||
Quota: 1_000_000,
|
||||
Status: common.UserStatusEnabled,
|
||||
}).Error)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Set("id", 42)
|
||||
ctx.Request = httptest.NewRequest(
|
||||
http.MethodPost,
|
||||
"/api/user/amount",
|
||||
strings.NewReader(`{"amount":4294}`),
|
||||
)
|
||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
RequestAmount(ctx)
|
||||
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
assert.JSONEq(t, `{"message":"error","data":"top-up quota limit exceeded"}`, recorder.Body.String())
|
||||
}
|
||||
|
||||
func TestValidateCreditedQuotaRejectsOverflow(t *testing.T) {
|
||||
require.NoError(t, validateCreditedQuota(decimal.NewFromInt(common.MaxQuota-1)))
|
||||
require.EqualError(t, validateCreditedQuota(decimal.Zero), "充值额度必须大于 0")
|
||||
_, err := validateCreditedQuota(decimal.NewFromInt(common.MaxQuota - 1))
|
||||
require.NoError(t, err)
|
||||
_, err = validateCreditedQuota(decimal.Zero)
|
||||
require.EqualError(t, err, "充值额度必须大于 0")
|
||||
_, err = validateCreditedQuota(decimal.NewFromInt(common.MaxQuota))
|
||||
require.EqualError(
|
||||
t,
|
||||
validateCreditedQuota(decimal.NewFromInt(common.MaxQuota)),
|
||||
err,
|
||||
"充值额度超出系统可表示范围",
|
||||
)
|
||||
}
|
||||
@@ -135,8 +187,10 @@ func TestStripeCreditedQuotaIncludesGroupRatio(t *testing.T) {
|
||||
require.NoError(t, common.UpdateTopupGroupRatioByJSONString(oldTopupGroupRatio))
|
||||
})
|
||||
|
||||
require.NoError(t, validateCreditedQuota(getStripeCreditedQuota(2147, "vip")))
|
||||
require.Error(t, validateCreditedQuota(getStripeCreditedQuota(2148, "vip")))
|
||||
_, err := validateCreditedQuota(getStripeCreditedQuota(2147, "vip"))
|
||||
require.NoError(t, err)
|
||||
_, err = validateCreditedQuota(getStripeCreditedQuota(2148, "vip"))
|
||||
require.Error(t, err)
|
||||
|
||||
require.NoError(t, common.UpdateTopupGroupRatioByJSONString(`{"free":0}`))
|
||||
assert.True(t, decimal.NewFromInt(500000).Equal(getStripeCreditedQuota(1, "free")))
|
||||
|
||||
@@ -58,8 +58,7 @@ func (*StripeAdaptor) RequestAmount(c *gin.Context, req *StripePayRequest) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": "获取用户分组失败"})
|
||||
return
|
||||
}
|
||||
if err := validateCreditedQuota(getStripeCreditedQuota(req.Amount, group)); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": err.Error()})
|
||||
if rejectInvalidCreditedQuota(c, id, getStripeCreditedQuota(req.Amount, group)) {
|
||||
return
|
||||
}
|
||||
payMoney := getStripePayMoney(float64(req.Amount), group)
|
||||
@@ -95,12 +94,15 @@ func (*StripeAdaptor) RequestPay(c *gin.Context, req *StripePayRequest) {
|
||||
}
|
||||
|
||||
id := c.GetInt("id")
|
||||
user, _ := model.GetUserById(id, false)
|
||||
user, err := model.GetUserById(id, false)
|
||||
if err != nil || user == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": "用户不存在"})
|
||||
return
|
||||
}
|
||||
chargedMoney := GetChargedAmount(float64(req.Amount), *user)
|
||||
if err := validateCreditedQuota(
|
||||
if rejectInvalidCreditedQuota(c, id,
|
||||
decimal.NewFromFloat(chargedMoney).Mul(decimal.NewFromFloat(common.QuotaPerUnit)),
|
||||
); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": err.Error()})
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -123,11 +123,11 @@ func RequestWaffoAmount(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", waffoMinTopup)})
|
||||
return
|
||||
}
|
||||
if rejectInvalidTopUpQuota(c, req.Amount) {
|
||||
id := c.GetInt("id")
|
||||
if rejectInvalidTopUpQuota(c, id, req.Amount) {
|
||||
return
|
||||
}
|
||||
|
||||
id := c.GetInt("id")
|
||||
group, err := model.GetUserGroup(id, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": "获取用户分组失败"})
|
||||
@@ -160,11 +160,11 @@ func RequestWaffoPay(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", waffoMinTopup)})
|
||||
return
|
||||
}
|
||||
if rejectInvalidTopUpQuota(c, req.Amount) {
|
||||
id := c.GetInt("id")
|
||||
if rejectInvalidTopUpQuota(c, id, req.Amount) {
|
||||
return
|
||||
}
|
||||
|
||||
id := c.GetInt("id")
|
||||
user, err := model.GetUserById(id, false)
|
||||
if err != nil || user == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": "用户不存在"})
|
||||
|
||||
@@ -33,11 +33,11 @@ func RequestWaffoPancakeAmount(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", setting.WaffoPancakeMinTopUp)})
|
||||
return
|
||||
}
|
||||
if rejectInvalidTopUpQuota(c, req.Amount) {
|
||||
id := c.GetInt("id")
|
||||
if rejectInvalidTopUpQuota(c, id, req.Amount) {
|
||||
return
|
||||
}
|
||||
|
||||
id := c.GetInt("id")
|
||||
group, err := model.GetUserGroup(id, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": "获取用户分组失败"})
|
||||
@@ -354,11 +354,11 @@ func RequestWaffoPancakePay(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", setting.WaffoPancakeMinTopUp)})
|
||||
return
|
||||
}
|
||||
if rejectInvalidTopUpQuota(c, req.Amount) {
|
||||
id := c.GetInt("id")
|
||||
if rejectInvalidTopUpQuota(c, id, req.Amount) {
|
||||
return
|
||||
}
|
||||
|
||||
id := c.GetInt("id")
|
||||
user, err := model.GetUserById(id, false)
|
||||
if err != nil || user == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "error", "data": "用户不存在"})
|
||||
|
||||
Reference in New Issue
Block a user