Compare commits

..

2 Commits

Author SHA1 Message Date
JiaJun
2d88e4a204 refactor(withdraw): 优化快速提现金额计算逻辑
- 引入舍入单位计算,避免小额差值导致的选项过多问题
- 实现最小最大钻石数量的舍入处理
- 重构快速提现金额生成算法,提高计算精度
- 使用索引映射方式重新实现金额选项生成
- 更新项目索引统计信息,反映代码结构变化
2026-07-10 14:34:32 +08:00
JiaJun
6fd9862019 refactor(game): 重构提现功能中的钻石金额范围计算逻辑
- 移除硬编码的快速提现金额配置,改为动态计算钻石金额边界
- 添加新的辅助函数 getWithdrawDiamondBounds 和 getQuickWithdrawAmounts
- 在桌面和移动端提现组件中调整钻石金额字段的位置
- 更新初始金额设置逻辑以使用新的钻石边界计算
- 优化快速金额选择卡片的ID生成方式,包含支付渠道和类型信息
- 添加对支付方法最小/最大金额限制的支持
2026-07-10 14:21:49 +08:00
5 changed files with 233 additions and 140 deletions

View File

@@ -1,7 +1,7 @@
<!-- gitnexus:start --> <!-- gitnexus:start -->
# GitNexus — Code Intelligence # GitNexus — Code Intelligence
This project is indexed by GitNexus as **36-character-flower** (3095 symbols, 6119 relationships, 264 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. This project is indexed by GitNexus as **36-character-flower** (3365 symbols, 6609 relationships, 288 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

View File

@@ -1,7 +1,7 @@
<!-- gitnexus:start --> <!-- gitnexus:start -->
# GitNexus — Code Intelligence # GitNexus — Code Intelligence
This project is indexed by GitNexus as **36-character-flower** (3095 symbols, 6119 relationships, 264 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. This project is indexed by GitNexus as **36-character-flower** (3365 symbols, 6609 relationships, 288 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

View File

@@ -390,62 +390,6 @@ function DesktopWithdraw() {
> >
<div className="flex min-h-full min-w-0 flex-[1.7] flex-col px-design-16 py-design-14"> <div className="flex min-h-full min-w-0 flex-[1.7] flex-col px-design-16 py-design-14">
<div className="flex flex-col gap-design-12"> <div className="flex flex-col gap-design-12">
<WithdrawField
label={t('gameDesktop.withdraw.fields.diamondAmount')}
>
<AmountShell
amount={vm.amount}
availableBalanceText={t(
'gameDesktop.withdraw.availableBalance',
{ amount: formatNumber(vm.availableBalance) },
)}
onAmountChange={handleAmountChange}
onMinus={() => handleAmountChange(vm.amount - 1)}
onPlus={() => handleAmountChange(vm.amount + 1)}
/>
{hasSubmitted && vm.amountRequiredError ? (
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
{t('gameDesktop.withdraw.errors.amountRequired')}
</div>
) : null}
{hasSubmitted && vm.amountExceedsBalance ? (
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
{t('gameDesktop.withdraw.errors.amountExceedsBalance')}
</div>
) : null}
{hasSubmitted && vm.amountBelowPaymentMinimum ? (
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
{t('gameDesktop.withdraw.errors.amountBelowPaymentMinimum', {
amount: vm.paymentMinimumAmountLabel,
})}
</div>
) : null}
{hasSubmitted && vm.amountAbovePaymentMaximum ? (
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
{t('gameDesktop.withdraw.errors.amountAbovePaymentMaximum', {
amount: vm.paymentMaximumAmountLabel,
})}
</div>
) : null}
</WithdrawField>
<div className="flex items-start gap-design-14">
<div className="w-design-132 shrink-0" />
<div className="grid min-w-0 flex-1 grid-cols-3 gap-design-10">
{vm.quickAmounts.map((option) => (
<QuickAmountCard
key={option.id}
amount={option.diamonds}
preview={option.preview}
active={option.id === activeQuickAmountId}
onClick={() =>
handleQuickAmountSelect(option.id, option.diamonds)
}
/>
))}
</div>
</div>
<WithdrawField <WithdrawField
label={t('gameDesktop.withdraw.fields.paymentChannel')} label={t('gameDesktop.withdraw.fields.paymentChannel')}
> >
@@ -516,6 +460,62 @@ function DesktopWithdraw() {
</div> </div>
</WithdrawField> </WithdrawField>
<WithdrawField
label={t('gameDesktop.withdraw.fields.diamondAmount')}
>
<AmountShell
amount={vm.amount}
availableBalanceText={t(
'gameDesktop.withdraw.availableBalance',
{ amount: formatNumber(vm.availableBalance) },
)}
onAmountChange={handleAmountChange}
onMinus={() => handleAmountChange(vm.amount - 1)}
onPlus={() => handleAmountChange(vm.amount + 1)}
/>
{hasSubmitted && vm.amountRequiredError ? (
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
{t('gameDesktop.withdraw.errors.amountRequired')}
</div>
) : null}
{hasSubmitted && vm.amountExceedsBalance ? (
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
{t('gameDesktop.withdraw.errors.amountExceedsBalance')}
</div>
) : null}
{hasSubmitted && vm.amountBelowPaymentMinimum ? (
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
{t('gameDesktop.withdraw.errors.amountBelowPaymentMinimum', {
amount: vm.paymentMinimumAmountLabel,
})}
</div>
) : null}
{hasSubmitted && vm.amountAbovePaymentMaximum ? (
<div className="pl-design-2 text-design-13 text-[#F44F4F]">
{t('gameDesktop.withdraw.errors.amountAbovePaymentMaximum', {
amount: vm.paymentMaximumAmountLabel,
})}
</div>
) : null}
</WithdrawField>
<div className="flex items-start gap-design-14">
<div className="w-design-132 shrink-0" />
<div className="grid min-w-0 flex-1 grid-cols-3 gap-design-10">
{vm.quickAmounts.map((option) => (
<QuickAmountCard
key={option.id}
amount={option.diamonds}
preview={option.preview}
active={option.id === activeQuickAmountId}
onClick={() =>
handleQuickAmountSelect(option.id, option.diamonds)
}
/>
))}
</div>
</div>
<WithdrawField <WithdrawField
label={t('gameDesktop.withdraw.fields.currencyType')} label={t('gameDesktop.withdraw.fields.currencyType')}
alignStart={false} alignStart={false}

View File

@@ -380,59 +380,6 @@ function MobileWithdraw() {
> >
<div className="min-h-0 flex-1 overflow-y-auto px-design-8 py-design-7"> <div className="min-h-0 flex-1 overflow-y-auto px-design-8 py-design-7">
<div className="flex flex-col !gap-design-10"> <div className="flex flex-col !gap-design-10">
<WithdrawField
label={t('gameDesktop.withdraw.fields.diamondAmount')}
>
<AmountShell
amount={vm.amount}
availableBalanceText={t(
'gameDesktop.withdraw.availableBalance',
{ amount: formatNumber(vm.availableBalance) },
)}
onAmountChange={handleAmountChange}
onMinus={() => handleAmountChange(vm.amount - 1)}
onPlus={() => handleAmountChange(vm.amount + 1)}
/>
{hasSubmitted && vm.amountRequiredError ? (
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
{t('gameDesktop.withdraw.errors.amountRequired')}
</div>
) : null}
{hasSubmitted && vm.amountExceedsBalance ? (
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
{t('gameDesktop.withdraw.errors.amountExceedsBalance')}
</div>
) : null}
{hasSubmitted && vm.amountBelowPaymentMinimum ? (
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
{t('gameDesktop.withdraw.errors.amountBelowPaymentMinimum', {
amount: vm.paymentMinimumAmountLabel,
})}
</div>
) : null}
{hasSubmitted && vm.amountAbovePaymentMaximum ? (
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
{t('gameDesktop.withdraw.errors.amountAbovePaymentMaximum', {
amount: vm.paymentMaximumAmountLabel,
})}
</div>
) : null}
</WithdrawField>
<div className="grid min-w-0 grid-cols-3 gap-design-5">
{vm.quickAmounts.map((option) => (
<QuickAmountCard
key={option.id}
amount={option.diamonds}
preview={option.preview}
active={option.id === activeQuickAmountId}
onClick={() =>
handleQuickAmountSelect(option.id, option.diamonds)
}
/>
))}
</div>
<WithdrawField <WithdrawField
label={t('gameDesktop.withdraw.fields.paymentChannel')} label={t('gameDesktop.withdraw.fields.paymentChannel')}
> >
@@ -500,6 +447,59 @@ function MobileWithdraw() {
</div> </div>
</WithdrawField> </WithdrawField>
<WithdrawField
label={t('gameDesktop.withdraw.fields.diamondAmount')}
>
<AmountShell
amount={vm.amount}
availableBalanceText={t(
'gameDesktop.withdraw.availableBalance',
{ amount: formatNumber(vm.availableBalance) },
)}
onAmountChange={handleAmountChange}
onMinus={() => handleAmountChange(vm.amount - 1)}
onPlus={() => handleAmountChange(vm.amount + 1)}
/>
{hasSubmitted && vm.amountRequiredError ? (
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
{t('gameDesktop.withdraw.errors.amountRequired')}
</div>
) : null}
{hasSubmitted && vm.amountExceedsBalance ? (
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
{t('gameDesktop.withdraw.errors.amountExceedsBalance')}
</div>
) : null}
{hasSubmitted && vm.amountBelowPaymentMinimum ? (
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
{t('gameDesktop.withdraw.errors.amountBelowPaymentMinimum', {
amount: vm.paymentMinimumAmountLabel,
})}
</div>
) : null}
{hasSubmitted && vm.amountAbovePaymentMaximum ? (
<div className="pl-design-2 text-design-10 text-[#F44F4F]">
{t('gameDesktop.withdraw.errors.amountAbovePaymentMaximum', {
amount: vm.paymentMaximumAmountLabel,
})}
</div>
) : null}
</WithdrawField>
<div className="grid min-w-0 grid-cols-3 gap-design-5">
{vm.quickAmounts.map((option) => (
<QuickAmountCard
key={option.id}
amount={option.diamonds}
preview={option.preview}
active={option.id === activeQuickAmountId}
onClick={() =>
handleQuickAmountSelect(option.id, option.diamonds)
}
/>
))}
</div>
<WithdrawField <WithdrawField
label={t('gameDesktop.withdraw.fields.currencyType')} label={t('gameDesktop.withdraw.fields.currencyType')}
> >

View File

@@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
DEFAULT_WITHDRAW_CONFIG, DEFAULT_WITHDRAW_CONFIG,
QUICK_FIAT_AMOUNTS,
WITHDRAW_EMAIL_PATTERN, WITHDRAW_EMAIL_PATTERN,
WITHDRAW_PHONE_PATTERN, WITHDRAW_PHONE_PATTERN,
} from '@/constants' } from '@/constants'
@@ -10,22 +9,88 @@ import { useDepositWithdrawConfig } from '@/hooks/use-deposit-withdraw-config'
import { useAuthStore } from '@/store' import { useAuthStore } from '@/store'
import type { DepositWithdrawConfig } from '@/type' import type { DepositWithdrawConfig } from '@/type'
const QUICK_WITHDRAW_OPTION_COUNT = 6
interface WithdrawDiamondBounds {
maximumDiamonds: number
minimumDiamonds: number
}
function formatNumber(locale: string, value: number) { function formatNumber(locale: string, value: number) {
return new Intl.NumberFormat(locale).format(value) return new Intl.NumberFormat(locale).format(value)
} }
function getInitialWithdrawAmount( function getInitialWithdrawAmount(bounds: WithdrawDiamondBounds | null) {
return bounds?.minimumDiamonds ?? 0
}
function getWithdrawDiamondBounds(
selectedExchangeRate: number, selectedExchangeRate: number,
maxWithdrawAmount: number, maxWithdrawAmount: number,
) { minimumFiatAmount: number | null,
if (maxWithdrawAmount <= 0) { maximumFiatAmount: number | null,
return 0 ): WithdrawDiamondBounds | null {
if (selectedExchangeRate <= 0 || maxWithdrawAmount <= 0) {
return null
} }
return Math.min( const minimumDiamonds = Math.max(
maxWithdrawAmount, 1,
Math.max(1, Math.round(QUICK_FIAT_AMOUNTS[0] / selectedExchangeRate)), minimumFiatAmount === null
? 1
: Math.ceil(minimumFiatAmount / selectedExchangeRate),
) )
const methodMaximumDiamonds =
maximumFiatAmount === null
? maxWithdrawAmount
: Math.floor(maximumFiatAmount / selectedExchangeRate)
const maximumDiamonds = Math.min(maxWithdrawAmount, methodMaximumDiamonds)
if (maximumDiamonds < minimumDiamonds) {
return null
}
return {
maximumDiamonds,
minimumDiamonds,
}
}
function getQuickWithdrawAmounts(bounds: WithdrawDiamondBounds) {
const { maximumDiamonds, minimumDiamonds } = bounds
const roundingUnit =
10 ** Math.max(0, Math.floor(Math.log10(maximumDiamonds)) - 1)
const roundedMinimumDiamonds =
Math.ceil(minimumDiamonds / roundingUnit) * roundingUnit
const roundedMaximumDiamonds =
Math.floor(maximumDiamonds / roundingUnit) * roundingUnit
if (roundedMinimumDiamonds > roundedMaximumDiamonds) {
return []
}
const availableOptionCount =
(roundedMaximumDiamonds - roundedMinimumDiamonds) / roundingUnit + 1
if (availableOptionCount <= QUICK_WITHDRAW_OPTION_COUNT) {
return Array.from(
{ length: availableOptionCount },
(_, index) => roundedMinimumDiamonds + index * roundingUnit,
)
}
const lastOptionIndex = availableOptionCount - 1
const optionIndexes = new Set<number>([0, lastOptionIndex])
for (let index = 1; index < QUICK_WITHDRAW_OPTION_COUNT - 1; index += 1) {
optionIndexes.add(
Math.round((lastOptionIndex * index) / (QUICK_WITHDRAW_OPTION_COUNT - 1)),
)
}
return [...optionIndexes]
.sort((left, right) => left - right)
.map((index) => roundedMinimumDiamonds + index * roundingUnit)
} }
function getCurrencyExchangeRate( function getCurrencyExchangeRate(
@@ -170,6 +235,21 @@ export function useWithdrawVm() {
const selectedPaymentMaximumAmount = selectedPaymentMethod const selectedPaymentMaximumAmount = selectedPaymentMethod
? toOptionalNumber(selectedPaymentMethod.maximumAmount) ? toOptionalNumber(selectedPaymentMethod.maximumAmount)
: null : null
const withdrawDiamondBounds = useMemo(
() =>
getWithdrawDiamondBounds(
selectedExchangeRate,
maxWithdrawAmount,
selectedPaymentMinimumAmount,
selectedPaymentMaximumAmount,
),
[
maxWithdrawAmount,
selectedExchangeRate,
selectedPaymentMaximumAmount,
selectedPaymentMinimumAmount,
],
)
const amountBelowPaymentMinimum = const amountBelowPaymentMinimum =
amount > 0 && amount > 0 &&
selectedPaymentMinimumAmount !== null && selectedPaymentMinimumAmount !== null &&
@@ -274,13 +354,11 @@ export function useWithdrawVm() {
}, [bankCode, sortedBanks]) }, [bankCode, sortedBanks])
useEffect(() => { useEffect(() => {
if (!hasInitializedAmount && selectedExchangeRate > 0) { if (!hasInitializedAmount && withdrawDiamondBounds) {
setAmount( setAmount(getInitialWithdrawAmount(withdrawDiamondBounds))
getInitialWithdrawAmount(selectedExchangeRate, maxWithdrawAmount),
)
setHasInitializedAmount(true) setHasInitializedAmount(true)
} }
}, [hasInitializedAmount, maxWithdrawAmount, selectedExchangeRate, setAmount]) }, [hasInitializedAmount, setAmount, withdrawDiamondBounds])
useEffect(() => { useEffect(() => {
if (amount > maxWithdrawAmount) { if (amount > maxWithdrawAmount) {
@@ -289,19 +367,26 @@ export function useWithdrawVm() {
}, [amount, maxWithdrawAmount, setAmount]) }, [amount, maxWithdrawAmount, setAmount])
const quickAmounts = useMemo(() => { const quickAmounts = useMemo(() => {
if (!selectedCurrency || selectedExchangeRate <= 0) { if (!selectedCurrency || !withdrawDiamondBounds) {
return [] return []
} }
return QUICK_FIAT_AMOUNTS.map((fiatAmount) => ({ return getQuickWithdrawAmounts(withdrawDiamondBounds).map((diamonds) => ({
diamonds: Math.min( diamonds,
maxWithdrawAmount, id: `quick-${paymentChannelCode}-${paymentType}-${selectedCurrency.code}-${diamonds}`,
Math.max(1, Math.round(fiatAmount / selectedExchangeRate)), preview: `${selectedCurrency.code} ${formatNumber(
), locale,
id: `quick-${selectedCurrency.code}-${fiatAmount}`, diamonds * selectedExchangeRate,
preview: `${selectedCurrency.code} ${formatNumber(locale, fiatAmount)}`, )}`,
})) }))
}, [locale, maxWithdrawAmount, selectedCurrency, selectedExchangeRate]) }, [
locale,
paymentChannelCode,
paymentType,
selectedCurrency,
selectedExchangeRate,
withdrawDiamondBounds,
])
const resetForm = useCallback(() => { const resetForm = useCallback(() => {
const nextPaymentChannelCode = sortedPayChannels[0]?.code ?? '' const nextPaymentChannelCode = sortedPayChannels[0]?.code ?? ''
@@ -311,6 +396,7 @@ export function useWithdrawVm() {
) ?? null ) ?? null
const nextCurrency = const nextCurrency =
getChannelCurrencies(config, nextPaymentChannel)[0] ?? null getChannelCurrencies(config, nextPaymentChannel)[0] ?? null
const nextPaymentMethod = nextPaymentChannel?.methods[0] ?? null
const nextExchangeRate = nextCurrency const nextExchangeRate = nextCurrency
? getCurrencyExchangeRate( ? getCurrencyExchangeRate(
config, config,
@@ -318,16 +404,23 @@ export function useWithdrawVm() {
nextCurrency.withdrawCoinsPerFiatValue || 1, nextCurrency.withdrawCoinsPerFiatValue || 1,
) )
: 0 : 0
const nextWithdrawDiamondBounds = getWithdrawDiamondBounds(
setAmountState( nextExchangeRate,
nextExchangeRate > 0 maxWithdrawAmount,
? getInitialWithdrawAmount(nextExchangeRate, maxWithdrawAmount) nextPaymentMethod
: 0, ? toOptionalNumber(nextPaymentMethod.minimumAmount)
: null,
nextPaymentMethod
? toOptionalNumber(nextPaymentMethod.maximumAmount)
: null,
) )
setHasInitializedAmount(true) const nextAmount = getInitialWithdrawAmount(nextWithdrawDiamondBounds)
setAmountState(nextAmount)
setHasInitializedAmount(nextAmount > 0)
setCurrencyCode(nextCurrency?.code ?? '') setCurrencyCode(nextCurrency?.code ?? '')
setPaymentChannelCode(nextPaymentChannelCode) setPaymentChannelCode(nextPaymentChannelCode)
setPaymentType(nextPaymentChannel?.methods[0]?.code ?? '') setPaymentType(nextPaymentMethod?.code ?? '')
setBankCode('') setBankCode('')
setHolderName('') setHolderName('')
setBankAccount('') setBankAccount('')