diff --git a/controller/topup.go b/controller/topup.go index a810eba760..579dad51df 100644 --- a/controller/topup.go +++ b/controller/topup.go @@ -9,6 +9,7 @@ import ( "time" "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/i18n" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/service" @@ -55,7 +56,9 @@ func GetTopUpInfo(c *gin.Context) { "creem_products": setting.CreemProducts, "pay_methods": payMethods, "min_topup": operation_setting.MinTopUp, + "max_topup": operation_setting.GetPaymentSetting().MaxTopUp, "stripe_min_topup": setting.StripeMinTopUp, + "stripe_max_topup": setting.StripeMaxTopUp, "amount_options": operation_setting.GetPaymentSetting().AmountOptions, "discount": operation_setting.GetPaymentSetting().AmountDiscount, } @@ -125,6 +128,19 @@ func getMinTopup() int64 { return int64(minTopup) } +func getMaxTopup() int64 { + maxTopup := operation_setting.GetPaymentSetting().MaxTopUp + if maxTopup <= 0 { + return 0 + } + if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { + dMaxTopup := decimal.NewFromInt(int64(maxTopup)) + dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) + maxTopup = int(dMaxTopup.Mul(dQuotaPerUnit).IntPart()) + } + return int64(maxTopup) +} + func RequestEpay(c *gin.Context) { var req EpayRequest err := c.ShouldBindJSON(&req) @@ -133,7 +149,11 @@ func RequestEpay(c *gin.Context) { return } if req.Amount < getMinTopup() { - c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", getMinTopup())}) + c.JSON(200, gin.H{"message": "error", "data": i18n.T(c, i18n.MsgTopupAmountTooLow, map[string]any{"Min": getMinTopup()})}) + return + } + if maxTopup := getMaxTopup(); maxTopup > 0 && req.Amount > maxTopup { + c.JSON(200, gin.H{"message": "error", "data": i18n.T(c, i18n.MsgTopupAmountTooHigh, map[string]any{"Max": maxTopup})}) return } @@ -321,7 +341,11 @@ func RequestAmount(c *gin.Context) { } if req.Amount < getMinTopup() { - c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", getMinTopup())}) + c.JSON(200, gin.H{"message": "error", "data": i18n.T(c, i18n.MsgTopupAmountTooLow, map[string]any{"Min": getMinTopup()})}) + return + } + if maxTopup := getMaxTopup(); maxTopup > 0 && req.Amount > maxTopup { + c.JSON(200, gin.H{"message": "error", "data": i18n.T(c, i18n.MsgTopupAmountTooHigh, map[string]any{"Max": maxTopup})}) return } id := c.GetInt("id") diff --git a/controller/topup_stripe.go b/controller/topup_stripe.go index e1718cc5ec..215b4f5a77 100644 --- a/controller/topup_stripe.go +++ b/controller/topup_stripe.go @@ -11,6 +11,7 @@ import ( "time" "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/i18n" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/setting" "github.com/QuantumNous/new-api/setting/operation_setting" @@ -48,7 +49,11 @@ type StripeAdaptor struct { func (*StripeAdaptor) RequestAmount(c *gin.Context, req *StripePayRequest) { if req.Amount < getStripeMinTopup() { - c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", getStripeMinTopup())}) + c.JSON(200, gin.H{"message": "error", "data": i18n.T(c, i18n.MsgTopupAmountTooLow, map[string]any{"Min": getStripeMinTopup()})}) + return + } + if maxTopup := getStripeMaxTopup(); maxTopup > 0 && req.Amount > maxTopup { + c.JSON(200, gin.H{"message": "error", "data": i18n.T(c, i18n.MsgTopupAmountTooHigh, map[string]any{"Max": maxTopup})}) return } id := c.GetInt("id") @@ -71,11 +76,11 @@ func (*StripeAdaptor) RequestPay(c *gin.Context, req *StripePayRequest) { return } if req.Amount < getStripeMinTopup() { - c.JSON(200, gin.H{"message": fmt.Sprintf("充值数量不能小于 %d", getStripeMinTopup()), "data": 10}) + c.JSON(200, gin.H{"message": i18n.T(c, i18n.MsgTopupAmountTooLow, map[string]any{"Min": getStripeMinTopup()}), "data": 10}) return } - if req.Amount > 10000 { - c.JSON(200, gin.H{"message": "充值数量不能大于 10000", "data": 10}) + if maxTopup := getStripeMaxTopup(); maxTopup > 0 && req.Amount > maxTopup { + c.JSON(200, gin.H{"message": i18n.T(c, i18n.MsgTopupAmountTooHigh, map[string]any{"Max": maxTopup}), "data": 10}) return } @@ -352,3 +357,14 @@ func getStripeMinTopup() int64 { } return int64(minTopup) } + +func getStripeMaxTopup() int64 { + maxTopup := setting.StripeMaxTopUp + if maxTopup <= 0 { + return 0 + } + if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { + maxTopup = maxTopup * int(common.QuotaPerUnit) + } + return int64(maxTopup) +} diff --git a/i18n/keys.go b/i18n/keys.go index 4d98540a77..9bb599027b 100644 --- a/i18n/keys.go +++ b/i18n/keys.go @@ -147,6 +147,8 @@ const ( MsgTopupOrderStatus = "topup.order_status" MsgTopupFailed = "topup.failed" MsgTopupInvalidQuota = "topup.invalid_quota" + MsgTopupAmountTooLow = "topup.amount_too_low" + MsgTopupAmountTooHigh = "topup.amount_too_high" ) // Channel related messages diff --git a/i18n/locales/en.yaml b/i18n/locales/en.yaml index 54dbf9181b..96f0998e0e 100644 --- a/i18n/locales/en.yaml +++ b/i18n/locales/en.yaml @@ -129,6 +129,8 @@ topup.order_not_exists: "Top-up order does not exist" topup.order_status: "Top-up order status error" topup.failed: "Top-up failed, please try again later" topup.invalid_quota: "Invalid top-up quota" +topup.amount_too_low: "Top-up amount cannot be less than {{.Min}}" +topup.amount_too_high: "Top-up amount cannot be greater than {{.Max}}" # Channel messages channel.not_exists: "Channel does not exist" diff --git a/i18n/locales/zh-CN.yaml b/i18n/locales/zh-CN.yaml index 4e0b5cd15d..95c791a5e6 100644 --- a/i18n/locales/zh-CN.yaml +++ b/i18n/locales/zh-CN.yaml @@ -130,6 +130,8 @@ topup.order_not_exists: "充值订单不存在" topup.order_status: "充值订单状态错误" topup.failed: "充值失败,请稍后重试" topup.invalid_quota: "无效的充值额度" +topup.amount_too_low: "充值数量不能小于 {{.Min}}" +topup.amount_too_high: "充值数量不能大于 {{.Max}}" # Channel messages channel.not_exists: "渠道不存在" diff --git a/i18n/locales/zh-TW.yaml b/i18n/locales/zh-TW.yaml index dcdd331b39..f3d0d1e16f 100644 --- a/i18n/locales/zh-TW.yaml +++ b/i18n/locales/zh-TW.yaml @@ -130,6 +130,8 @@ topup.order_not_exists: "充值訂單不存在" topup.order_status: "充值訂單狀態錯誤" topup.failed: "充值失敗,請稍後重試" topup.invalid_quota: "無效的充值額度" +topup.amount_too_low: "充值數量不能小於 {{.Min}}" +topup.amount_too_high: "充值數量不能大於 {{.Max}}" # Channel messages channel.not_exists: "管道不存在" diff --git a/model/option.go b/model/option.go index 697e77dfe7..4cd0af883e 100644 --- a/model/option.go +++ b/model/option.go @@ -79,7 +79,9 @@ func InitOptionMap() { common.OptionMap["Price"] = strconv.FormatFloat(operation_setting.Price, 'f', -1, 64) common.OptionMap["USDExchangeRate"] = strconv.FormatFloat(operation_setting.USDExchangeRate, 'f', -1, 64) common.OptionMap["MinTopUp"] = strconv.Itoa(operation_setting.MinTopUp) + common.OptionMap["MaxTopUp"] = strconv.Itoa(operation_setting.GetPaymentSetting().MaxTopUp) common.OptionMap["StripeMinTopUp"] = strconv.Itoa(setting.StripeMinTopUp) + common.OptionMap["StripeMaxTopUp"] = strconv.Itoa(setting.StripeMaxTopUp) common.OptionMap["StripeApiSecret"] = setting.StripeApiSecret common.OptionMap["StripeWebhookSecret"] = setting.StripeWebhookSecret common.OptionMap["StripePriceId"] = setting.StripePriceId @@ -338,6 +340,9 @@ func updateOptionMap(key string, value string) (err error) { operation_setting.USDExchangeRate, _ = strconv.ParseFloat(value, 64) case "MinTopUp": operation_setting.MinTopUp, _ = strconv.Atoi(value) + case "MaxTopUp": + val, _ := strconv.Atoi(value) + operation_setting.GetPaymentSetting().MaxTopUp = val case "StripeApiSecret": setting.StripeApiSecret = value case "StripeWebhookSecret": @@ -348,6 +353,8 @@ func updateOptionMap(key string, value string) (err error) { setting.StripeUnitPrice, _ = strconv.ParseFloat(value, 64) case "StripeMinTopUp": setting.StripeMinTopUp, _ = strconv.Atoi(value) + case "StripeMaxTopUp": + setting.StripeMaxTopUp, _ = strconv.Atoi(value) case "StripePromotionCodesEnabled": setting.StripePromotionCodesEnabled = value == "true" case "CreemApiKey": diff --git a/setting/operation_setting/payment_setting.go b/setting/operation_setting/payment_setting.go index 84162f4e52..ddd189bea5 100644 --- a/setting/operation_setting/payment_setting.go +++ b/setting/operation_setting/payment_setting.go @@ -5,12 +5,14 @@ import "github.com/QuantumNous/new-api/setting/config" type PaymentSetting struct { AmountOptions []int `json:"amount_options"` AmountDiscount map[int]float64 `json:"amount_discount"` // 充值金额对应的折扣,例如 100 元 0.9 表示 100 元充值享受 9 折优惠 + MaxTopUp int `json:"max_topup"` // 0 means no limit } // 默认配置 var paymentSetting = PaymentSetting{ AmountOptions: []int{10, 20, 50, 100, 200, 500}, AmountDiscount: map[int]float64{}, + MaxTopUp: 0, } func init() { diff --git a/setting/payment_stripe.go b/setting/payment_stripe.go index d97120c852..d35e21eb92 100644 --- a/setting/payment_stripe.go +++ b/setting/payment_stripe.go @@ -5,4 +5,5 @@ var StripeWebhookSecret = "" var StripePriceId = "" var StripeUnitPrice = 8.0 var StripeMinTopUp = 1 +var StripeMaxTopUp = 0 // 0 means no limit var StripePromotionCodesEnabled = false diff --git a/web/src/components/topup/RechargeCard.jsx b/web/src/components/topup/RechargeCard.jsx index 1ce0230986..e0adee8aad 100644 --- a/web/src/components/topup/RechargeCard.jsx +++ b/web/src/components/topup/RechargeCard.jsx @@ -66,6 +66,7 @@ const RechargeCard = ({ priceRatio, topUpCount, minTopUp, + maxTopUp, renderQuotaWithAmount, getAmount, setTopUpCount, @@ -238,11 +239,12 @@ const RechargeCard = ({ label={t('充值数量')} disabled={!enableOnlineTopUp && !enableStripeTopUp} placeholder={ - t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp) + maxTopUp > 0 + ? t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp) + t(',最高 ') + renderQuotaWithAmount(maxTopUp) + : t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp) } value={topUpCount} min={minTopUp} - max={999999999} step={1} precision={0} onChange={async (value) => { @@ -255,8 +257,8 @@ const RechargeCard = ({ onBlur={(e) => { const value = parseInt(e.target.value); if (!value || value < 1) { - setTopUpCount(1); - getAmount(1); + setTopUpCount(minTopUp); + getAmount(minTopUp); } }} formatter={(value) => (value ? `${value}` : '')} @@ -264,26 +266,33 @@ const RechargeCard = ({ value ? parseInt(value.replace(/[^\d]/g, '')) : 0 } extraText={ - - } - > - - {t('实付金额:')} - - {renderAmount()} - - - +
+ + } + > + + {t('实付金额:')} + + {renderAmount()} + + + + {maxTopUp > 0 && topUpCount > maxTopUp && ( +
+ {t('充值数量不能大于') + ' ' + renderQuotaWithAmount(maxTopUp)} +
+ )} +
} style={{ width: '100%' }} /> @@ -295,10 +304,12 @@ const RechargeCard = ({ {payMethods.map((payMethod) => { const minTopupVal = Number(payMethod.min_topup) || 0; const isStripe = payMethod.type === 'stripe'; + const exceedsMax = maxTopUp > 0 && Number(topUpCount || 0) > maxTopUp; const disabled = (!enableOnlineTopUp && !isStripe) || (!enableStripeTopUp && isStripe) || - minTopupVal > Number(topUpCount || 0); + minTopupVal > Number(topUpCount || 0) || + exceedsMax; const buttonEl = (