diff --git a/controller/topup.go b/controller/topup.go index f284867143..9a0a83732d 100644 --- a/controller/topup.go +++ b/controller/topup.go @@ -23,6 +23,14 @@ import ( ) func GetTopUpInfo(c *gin.Context) { + id := c.GetInt("id") + group, err := model.GetUserGroup(id, true) + if err != nil { + common.ApiError(c, err) + return + } + topupGroupRatio := common.GetTopupGroupRatio(group) + // 获取支付方式 payMethods := operation_setting.PayMethods @@ -110,6 +118,7 @@ func GetTopUpInfo(c *gin.Context) { "waffo_pancake_min_topup": setting.WaffoPancakeMinTopUp, "amount_options": operation_setting.GetPaymentSetting().AmountOptions, "discount": operation_setting.GetPaymentSetting().AmountDiscount, + "topup_group_ratio": topupGroupRatio, "topup_link": common.TopUpLink, } common.ApiSuccess(c, data) diff --git a/web/classic/src/components/topup/RechargeCard.jsx b/web/classic/src/components/topup/RechargeCard.jsx index 1e2923cafc..2382fc75c9 100644 --- a/web/classic/src/components/topup/RechargeCard.jsx +++ b/web/classic/src/components/topup/RechargeCard.jsx @@ -99,6 +99,7 @@ const RechargeCard = ({ }) => { const onlineFormApiRef = useRef(null); const redeemFormApiRef = useRef(null); + const syncingPresetRef = useRef(false); const initialTabSetRef = useRef(false); const showAmountSkeleton = useMinimumLoadingTime(amountLoading); const [activeTab, setActiveTab] = useState('topup'); @@ -106,6 +107,18 @@ const RechargeCard = ({ !subscriptionLoading && subscriptionPlans.length > 0; const regularPayMethods = payMethods || []; + const releasePresetSyncLock = () => { + if (typeof window !== 'undefined' && window.requestAnimationFrame) { + window.requestAnimationFrame(() => { + syncingPresetRef.current = false; + }); + return; + } + setTimeout(() => { + syncingPresetRef.current = false; + }, 0); + }; + useEffect(() => { if (initialTabSetRef.current) return; if (subscriptionLoading) return; @@ -118,6 +131,11 @@ const RechargeCard = ({ setActiveTab('topup'); } }, [shouldShowSubscription, activeTab]); + + const currentTopupGroupRatio = Number(topupInfo?.topup_group_ratio) || 1; + const currentOriginalAmount = + Number(topUpCount || 0) * Number(priceRatio || 0) * currentTopupGroupRatio; + const topupContent = ( {/* 统计数据 */} @@ -261,6 +279,9 @@ const RechargeCard = ({ step={1} precision={0} onChange={async (value) => { + if (syncingPresetRef.current) { + return; + } if (value && value >= 1) { setTopUpCount(value); setSelectedPreset(null); @@ -297,6 +318,16 @@ const RechargeCard = ({ {renderAmount()} + {currentOriginalAmount > 0 && ( + + {t('原价')}:{currentOriginalAmount.toFixed(2)} + {' '} + {t('元')} + + )} } @@ -406,6 +437,11 @@ const RechargeCard = ({ {t('选择充值额度')} {(() => { const { symbol, rate, type } = getCurrencyConfig(); + const topupRate = + Number.isFinite(Number(priceRatio)) && + Number(priceRatio) > 0 + ? Number(priceRatio) + : 1; if (type === 'USD') return null; return ( @@ -416,7 +452,11 @@ const RechargeCard = ({ fontWeight: 'normal', }} > - (1 $ = {rate.toFixed(2)} {symbol}) + (1 $ = + {' '} + {(type === 'CNY' ? topupRate : rate).toFixed(2)} + {' '} + {symbol}) ); })()} @@ -425,11 +465,18 @@ const RechargeCard = ({ >
{presetAmounts.map((preset, index) => { - const discount = - preset.discount || - topupInfo?.discount?.[preset.value] || - 1.0; - const originalPrice = preset.value * priceRatio; + const presetValue = Number(preset.value); + const rawDiscount = + topupInfo?.discount?.[presetValue] ?? + topupInfo?.discount?.[String(presetValue)] ?? + preset.discount; + const discount = Number.isFinite(Number(rawDiscount)) + ? Number(rawDiscount) + : 1.0; + const topupGroupRatio = + Number(topupInfo?.topup_group_ratio) || 1; + const originalPrice = + presetValue * priceRatio * topupGroupRatio; const discountedPrice = originalPrice * discount; const hasDiscount = discount < 1.0; const actualPay = discountedPrice; @@ -437,31 +484,31 @@ const RechargeCard = ({ // 根据当前货币类型换算显示金额和数量 const { symbol, rate, type } = getCurrencyConfig(); - const statusStr = localStorage.getItem('status'); - let usdRate = 7; // 默认CNY汇率 - try { - if (statusStr) { - const s = JSON.parse(statusStr); - usdRate = s?.usd_exchange_rate || 7; - } - } catch (e) {} + const topupRate = + Number.isFinite(Number(priceRatio)) && + Number(priceRatio) > 0 + ? Number(priceRatio) + : 1; - let displayValue = preset.value; // 显示的数量 + let displayValue = presetValue; // 显示的数量 + let displayOriginalPay = originalPrice; let displayActualPay = actualPay; let displaySave = save; if (type === 'USD') { - // 数量保持USD,价格从CNY转USD - displayActualPay = actualPay / usdRate; - displaySave = save / usdRate; + // 数量保持USD,价格从充值货币转回USD + displayOriginalPay = originalPrice / topupRate; + displayActualPay = actualPay / topupRate; + displaySave = save / topupRate; } else if (type === 'CNY') { - // 数量转CNY,价格已是CNY - displayValue = preset.value * usdRate; + // 数量按充值价格折算为CNY + displayValue = presetValue * topupRate; } else if (type === 'CUSTOM') { // 数量和价格都转自定义货币 - displayValue = preset.value * rate; - displayActualPay = (actualPay / usdRate) * rate; - displaySave = (save / usdRate) * rate; + displayValue = presetValue * rate; + displayOriginalPay = (originalPrice / topupRate) * rate; + displayActualPay = (actualPay / topupRate) * rate; + displaySave = (save / topupRate) * rate; } return ( @@ -470,7 +517,7 @@ const RechargeCard = ({ style={{ cursor: 'pointer', border: - selectedPreset === preset.value + selectedPreset === presetValue ? '2px solid var(--semi-color-primary)' : '1px solid var(--semi-color-border)', height: '100%', @@ -478,11 +525,13 @@ const RechargeCard = ({ }} bodyStyle={{ padding: '12px' }} onClick={() => { - selectPresetAmount(preset); + syncingPresetRef.current = true; onlineFormApiRef.current?.setValue( 'topUpCount', - preset.value, + presetValue, ); + selectPresetAmount(preset); + releasePresetSyncLock(); }} >
@@ -492,6 +541,29 @@ const RechargeCard = ({ > {formatLargeNumber(displayValue)} {symbol} + +
+ {t('原价')} {symbol} + {displayOriginalPay.toFixed(2)} +
+
+ {t('实付')} {symbol} + {displayActualPay.toFixed(2)} {hasDiscount && ( {t('折').includes('off') @@ -503,19 +575,6 @@ const RechargeCard = ({ {t('折')} )} - -
- {t('实付')} {symbol} - {displayActualPay.toFixed(2)}, - {hasDiscount - ? `${t('节省')} ${symbol}${displaySave.toFixed(2)}` - : `${t('节省')} ${symbol}0.00`}
diff --git a/web/classic/src/components/topup/index.jsx b/web/classic/src/components/topup/index.jsx index 881e39e9ba..6d7164aa2d 100644 --- a/web/classic/src/components/topup/index.jsx +++ b/web/classic/src/components/topup/index.jsx @@ -40,6 +40,40 @@ import TransferModal from './modals/TransferModal'; import PaymentConfirmModal from './modals/PaymentConfirmModal'; import TopupHistoryModal from './modals/TopupHistoryModal'; +const parseDiscountMap = (data) => { + if (!data) { + return {}; + } + + let parsedData = data; + if (typeof data === 'string') { + try { + parsedData = JSON.parse(data); + } catch { + return {}; + } + } + + if ( + !parsedData || + typeof parsedData !== 'object' || + Array.isArray(parsedData) + ) { + return {}; + } + + return Object.entries(parsedData).reduce((result, [key, value]) => { + const numericKey = Number(key); + const numericValue = Number(value); + + if (Number.isFinite(numericKey) && Number.isFinite(numericValue)) { + result[numericKey] = numericValue; + } + + return result; + }, {}); +}; + const TopUp = () => { const { t } = useTranslation(); const [searchParams, setSearchParams] = useSearchParams(); @@ -110,6 +144,7 @@ const TopUp = () => { const [topupInfo, setTopupInfo] = useState({ amount_options: [], discount: {}, + topup_group_ratio: 1, }); const confirmPayMethods = [ @@ -575,9 +610,11 @@ const TopUp = () => { const res = await API.get('/api/user/topup/info'); const { message, data, success } = res.data; if (success) { + const parsedDiscounts = parseDiscountMap(data.discount); setTopupInfo({ amount_options: data.amount_options || [], - discount: data.discount || {}, + discount: parsedDiscounts, + topup_group_ratio: Number(data.topup_group_ratio) || 1, }); // 处理支付方式 @@ -680,8 +717,8 @@ const TopUp = () => { // 如果有自定义充值数量选项,使用它们替换默认的预设选项 if (data.amount_options && data.amount_options.length > 0) { const customPresets = data.amount_options.map((amount) => ({ - value: amount, - discount: data.discount[amount] || 1.0, + value: Number(amount), + discount: parsedDiscounts[amount] || 1.0, })); setPresetAmounts(customPresets); } @@ -849,12 +886,21 @@ const TopUp = () => { // 选择预设充值额度 const selectPresetAmount = (preset) => { - setTopUpCount(preset.value); - setSelectedPreset(preset.value); + const presetValue = Number(preset.value); + setTopUpCount(presetValue); + setSelectedPreset(presetValue); // 计算实际支付金额,考虑折扣 - const discount = preset.discount || topupInfo.discount[preset.value] || 1.0; - const discountedAmount = preset.value * priceRatio * discount; + const rawDiscount = + topupInfo.discount[presetValue] ?? + topupInfo.discount[String(presetValue)] ?? + preset.discount; + const discount = Number.isFinite(Number(rawDiscount)) + ? Number(rawDiscount) + : 1.0; + const topupGroupRatio = Number(topupInfo.topup_group_ratio) || 1; + const discountedAmount = + presetValue * Number(priceRatio) * topupGroupRatio * discount; setAmount(discountedAmount); }; diff --git a/web/default/src/features/wallet/components/recharge-form-card.tsx b/web/default/src/features/wallet/components/recharge-form-card.tsx index dd48285087..06b58a1d03 100644 --- a/web/default/src/features/wallet/components/recharge-form-card.tsx +++ b/web/default/src/features/wallet/components/recharge-form-card.tsx @@ -117,6 +117,8 @@ export function RechargeFormCard({ const hasWaffoPaymentMethods = Array.isArray(waffoPayMethods) && waffoPayMethods.length > 0 const minTopup = getMinTopupAmount(topupInfo) + const topupGroupRatio = Number(topupInfo?.topup_group_ratio) || 1 + const currentOriginalAmount = topupAmount * priceRatio * topupGroupRatio if (loading) { return ( @@ -199,19 +201,23 @@ export function RechargeFormCard({
{presetAmounts.map((preset, index) => { - const discount = - preset.discount || - topupInfo?.discount?.[preset.value] || - 1.0 + const rawDiscount = + topupInfo?.discount?.[preset.value] ?? + topupInfo?.discount?.[String(preset.value)] ?? + preset.discount + const discount = Number.isFinite(Number(rawDiscount)) + ? Number(rawDiscount) + : 1.0 const { displayValue, + originalPrice, actualPrice, - savedAmount, hasDiscount, } = calculatePresetPricing( preset.value, priceRatio, discount, + topupGroupRatio, usdExchangeRate ) return ( @@ -219,16 +225,28 @@ export function RechargeFormCard({ key={index} variant='outline' className={cn( - 'hover:border-foreground flex min-h-16 flex-col items-start rounded-lg px-3 py-2.5 text-left whitespace-normal sm:min-h-[72px] sm:p-4', + 'hover:border-foreground flex min-h-20 flex-col items-start rounded-lg px-3 py-2.5 text-left whitespace-normal sm:min-h-[92px] sm:p-4', selectedPreset === preset.value ? 'border-foreground bg-foreground/5' : 'border-muted' )} onClick={() => onSelectPreset(preset)} > -
-
- {formatNumber(displayValue)} +
+ {formatNumber(displayValue)} +
+
+ {t('Price')} {formatCurrency(originalPrice)} +
+
+
+ {t('You Pay')} {formatCurrency(actualPrice)}
{hasDiscount && (
@@ -236,15 +254,6 @@ export function RechargeFormCard({
)}
-
- Pay {formatCurrency(actualPrice)} - {hasDiscount && savedAmount > 0 && ( - - {' '} - • Save {formatCurrency(savedAmount)} - - )} -
) })} @@ -270,9 +279,16 @@ export function RechargeFormCard({ className='h-9 text-base sm:h-10 sm:text-lg' />
- - {t('Amount to pay:')} - +
+
+ {t('Amount to pay:')} +
+ {currentOriginalAmount > 0 && ( +
+ {t('Price')} {formatCurrency(currentOriginalAmount)} +
+ )} +
{calculating ? ( ) : ( diff --git a/web/default/src/features/wallet/lib/format.ts b/web/default/src/features/wallet/lib/format.ts index 1d0df8bf79..01c224663a 100644 --- a/web/default/src/features/wallet/lib/format.ts +++ b/web/default/src/features/wallet/lib/format.ts @@ -61,9 +61,10 @@ export function calculatePresetPricing( presetValue: number, priceRatio: number, discount: number, + topupGroupRatio: number = 1, usdExchangeRate: number = 1 ) { - const originalPrice = presetValue * priceRatio + const originalPrice = presetValue * priceRatio * topupGroupRatio const actualPrice = originalPrice * discount const savedAmount = originalPrice - actualPrice const hasDiscount = discount < 1.0 diff --git a/web/default/src/features/wallet/types.ts b/web/default/src/features/wallet/types.ts index 7699406a15..fc4eaf2ad1 100644 --- a/web/default/src/features/wallet/types.ts +++ b/web/default/src/features/wallet/types.ts @@ -111,6 +111,8 @@ export interface TopupInfo { amount_options: number[] /** Discount rates by amount */ discount: Record + /** Effective topup group ratio for current user */ + topup_group_ratio?: number /** Optional topup link for purchasing codes */ topup_link?: string /** Whether Creem topup is enabled */