Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions controller/topup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -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)
Expand All @@ -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
}

Expand Down Expand Up @@ -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")
Expand Down
24 changes: 20 additions & 4 deletions controller/topup_stripe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Expand All @@ -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
}

Expand Down Expand Up @@ -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)
}
2 changes: 2 additions & 0 deletions i18n/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/zh-CN.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: "渠道不存在"
Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/zh-TW.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: "管道不存在"
Expand Down
7 changes: 7 additions & 0 deletions model/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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":
Expand All @@ -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":
Expand Down
2 changes: 2 additions & 0 deletions setting/operation_setting/payment_setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions setting/payment_stripe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
69 changes: 40 additions & 29 deletions web/src/components/topup/RechargeCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const RechargeCard = ({
priceRatio,
topUpCount,
minTopUp,
maxTopUp,
renderQuotaWithAmount,
getAmount,
setTopUpCount,
Expand Down Expand Up @@ -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) => {
Expand All @@ -255,35 +257,42 @@ 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}` : '')}
parser={(value) =>
value ? parseInt(value.replace(/[^\d]/g, '')) : 0
}
extraText={
<Skeleton
loading={showAmountSkeleton}
active
placeholder={
<Skeleton.Title
style={{
width: 120,
height: 20,
borderRadius: 6,
}}
/>
}
>
<Text type='secondary' className='text-red-600'>
{t('实付金额:')}
<span style={{ color: 'red' }}>
{renderAmount()}
</span>
</Text>
</Skeleton>
<div>
<Skeleton
loading={showAmountSkeleton}
active
placeholder={
<Skeleton.Title
style={{
width: 120,
height: 20,
borderRadius: 6,
}}
/>
}
>
<Text type='secondary' className='text-red-600'>
{t('实付金额:')}
<span style={{ color: 'red' }}>
{renderAmount()}
</span>
</Text>
</Skeleton>
{maxTopUp > 0 && topUpCount > maxTopUp && (
<div style={{ color: 'var(--semi-color-danger)', marginTop: 4, fontSize: 12 }}>
{t('充值数量不能大于') + ' ' + renderQuotaWithAmount(maxTopUp)}
</div>
)}
</div>
}
style={{ width: '100%' }}
/>
Expand All @@ -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 = (
<Button
Expand Down Expand Up @@ -334,12 +345,12 @@ const RechargeCard = ({
);

return disabled &&
minTopupVal > Number(topUpCount || 0) ? (
(minTopupVal > Number(topUpCount || 0) || exceedsMax) ? (
<Tooltip
content={
t('此支付方式最低充值金额为') +
' ' +
minTopupVal
exceedsMax
? t('充值数量不能大于') + ' ' + renderQuotaWithAmount(maxTopUp)
: t('此支付方式最低充值金额为') + ' ' + minTopupVal
}
key={payMethod.type}
>
Expand Down
24 changes: 23 additions & 1 deletion web/src/components/topup/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const TopUp = () => {
const [redemptionCode, setRedemptionCode] = useState('');
const [amount, setAmount] = useState(0.0);
const [minTopUp, setMinTopUp] = useState(statusState?.status?.min_topup || 1);
const [maxTopUp, setMaxTopUp] = useState(0);
const [topUpCount, setTopUpCount] = useState(
statusState?.status?.min_topup || 1,
);
Expand Down Expand Up @@ -175,6 +176,10 @@ const TopUp = () => {
showError(t('充值数量不能小于') + minTopUp);
return;
}
if (maxTopUp > 0 && topUpCount > maxTopUp) {
showError(t('充值数量不能大于') + ' ' + renderQuotaWithAmount(maxTopUp));
return;
}
setOpen(true);
} catch (error) {
showError(t('获取金额失败'));
Expand All @@ -197,7 +202,11 @@ const TopUp = () => {
}

if (topUpCount < minTopUp) {
showError('充值数量不能小于' + minTopUp);
showError(t('充值数量不能小于') + minTopUp);
return;
}
if (maxTopUp > 0 && topUpCount > maxTopUp) {
showError(t('充值数量不能大于') + ' ' + renderQuotaWithAmount(maxTopUp));
return;
}
setConfirmLoading(true);
Expand Down Expand Up @@ -450,10 +459,16 @@ const TopUp = () => {
: enableStripeTopUp
? data.stripe_min_topup
: 1;
const maxTopUpValue = enableOnlineTopUp
? data.max_topup || 0
: enableStripeTopUp
? data.stripe_max_topup || 0
: 0;
setEnableOnlineTopUp(enableOnlineTopUp);
setEnableStripeTopUp(enableStripeTopUp);
setEnableCreemTopUp(enableCreemTopUp);
setMinTopUp(minTopUpValue);
setMaxTopUp(maxTopUpValue);
setTopUpCount(minTopUpValue);

// 设置 Creem 产品
Expand Down Expand Up @@ -570,6 +585,9 @@ const TopUp = () => {
if (value === undefined) {
value = topUpCount;
}
if (maxTopUp > 0 && value > maxTopUp) {
return;
}
Comment on lines +588 to +590
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Stale amount displayed when the entered value exceeds the max.

When value > maxTopUp, the function returns without updating amount. If the user previously entered a valid value (e.g., $50 → amount shows ¥365), then changes to an invalid value exceeding maxTopUp, the old payment amount remains displayed — potentially misleading.

Consider resetting the amount before returning:

Proposed fix
     if (maxTopUp > 0 && value > maxTopUp) {
+      setAmount(0);
       return;
     }

Apply the same in getStripeAmount (line 617–619).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (maxTopUp > 0 && value > maxTopUp) {
return;
}
if (maxTopUp > 0 && value > maxTopUp) {
setAmount(0);
return;
}
🤖 Prompt for AI Agents
In `@web/src/components/topup/index.jsx` around lines 588 - 590, The check that
returns early when value > maxTopUp leaves the previous computed amount
displayed; update the amount state to a cleared value before returning so the UI
doesn't show a stale payment amount (i.e., set the component's amount/state to
an empty/zero value whenever value > maxTopUp), and apply the same change to the
getStripeAmount function where it does the same value > maxTopUp early return;
ensure you reference and update the shared amount state (the amount
variable/state setter) in both places before the return.

setAmountLoading(true);
try {
const res = await API.post('/api/user/amount', {
Expand All @@ -596,6 +614,9 @@ const TopUp = () => {
if (value === undefined) {
value = topUpCount;
}
if (maxTopUp > 0 && value > maxTopUp) {
return;
}
setAmountLoading(true);
try {
const res = await API.post('/api/user/stripe/amount', {
Expand Down Expand Up @@ -747,6 +768,7 @@ const TopUp = () => {
priceRatio={priceRatio}
topUpCount={topUpCount}
minTopUp={minTopUp}
maxTopUp={maxTopUp}
renderQuotaWithAmount={renderQuotaWithAmount}
getAmount={getAmount}
setTopUpCount={setTopUpCount}
Expand Down
Loading