From d9443e0d56018fe057e023cc85eec965976a4d71 Mon Sep 17 00:00:00 2001 From: WrBug Date: Sun, 4 Jan 2026 12:13:40 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20InputNumber=20f?= =?UTF-8?q?ormatter=20=E6=AD=A3=E5=88=99=E8=A1=A8=E8=BE=BE=E5=BC=8F?= =?UTF-8?q?=E9=94=99=E8=AF=AF=EF=BC=8C=E5=AF=BC=E8=87=B4=E8=BE=93=E5=85=A5?= =?UTF-8?q?=2010=20=E5=8F=98=E6=88=90=201=20=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 copyRatio InputNumber 的 parser 函数,正确处理 % 符号和最小值检查 - 修复所有 formatter 函数中的正则表达式:将 /\.?0+$/ 改为 /\.0+$/ - 添加调试日志用于问题排查(可后续移除) 修复的文件: - CopyTradingAdd.tsx - CopyTradingEdit.tsx - CopyTradingOrders/EditModal.tsx - CopyTradingList.tsx - TemplateList.tsx - TemplateEdit.tsx - TemplateAdd.tsx 问题原因: 错误的正则表达式 /\.?0+$/ 会匹配整数末尾的 0(如 "10" 的末尾 "0"), 导致 "10" 被错误替换为 "1"。 正确的正则表达式 /\.0+$/ 只匹配小数点后的尾随零。 --- frontend/src/pages/CopyTradingAdd.tsx | 106 +++++++++++++++++- frontend/src/pages/CopyTradingEdit.tsx | 106 +++++++++++++++++- frontend/src/pages/CopyTradingList.tsx | 4 +- .../src/pages/CopyTradingOrders/EditModal.tsx | 106 +++++++++++++++++- frontend/src/pages/TemplateAdd.tsx | 65 +++++++++-- frontend/src/pages/TemplateEdit.tsx | 65 +++++++++-- frontend/src/pages/TemplateList.tsx | 65 +++++++++-- 7 files changed, 479 insertions(+), 38 deletions(-) diff --git a/frontend/src/pages/CopyTradingAdd.tsx b/frontend/src/pages/CopyTradingAdd.tsx index 8a7dfe0..3b0a79e 100644 --- a/frontend/src/pages/CopyTradingAdd.tsx +++ b/frontend/src/pages/CopyTradingAdd.tsx @@ -344,13 +344,51 @@ const CopyTradingAdd: React.FC = () => { tooltip={t('copyTradingAdd.copyRatioTooltip') || '跟单比例表示跟单金额相对于 Leader 订单金额的百分比。例如:100% 表示 1:1 跟单,50% 表示半仓跟单,200% 表示双倍跟单'} > { + console.log('[CopyTradingAdd copyRatio parser] 输入值:', value, '类型:', typeof value) + // 移除 % 符号和其他非数字字符(保留小数点和负号) + const cleaned = (value || '').toString().replace(/%/g, '').trim() + console.log('[CopyTradingAdd copyRatio parser] 清理后:', cleaned) + const parsed = parseFloat(cleaned) || 0 + console.log('[CopyTradingAdd copyRatio parser] 解析后:', parsed) + if (parsed > 10000) { + console.log('[CopyTradingAdd copyRatio parser] 超过最大值,返回 10000') + return 10000 + } + if (parsed < 0.01) { + console.log('[CopyTradingAdd copyRatio parser] 小于最小值,返回 0.01') + return 0.01 + } + console.log('[CopyTradingAdd copyRatio parser] 返回:', parsed) + return parsed + }} + formatter={(value) => { + console.log('[CopyTradingAdd copyRatio formatter] 输入值:', value, '类型:', typeof value) + if (!value && value !== 0) { + console.log('[CopyTradingAdd copyRatio formatter] 空值,返回空字符串') + return '' + } + const num = parseFloat(value.toString()) + console.log('[CopyTradingAdd copyRatio formatter] 解析后:', num) + if (isNaN(num)) { + console.log('[CopyTradingAdd copyRatio formatter] NaN,返回空字符串') + return '' + } + if (num > 10000) { + console.log('[CopyTradingAdd copyRatio formatter] 超过最大值,返回 10000') + return '10000' + } + const result = num.toString().replace(/\.0+$/, '') + console.log('[CopyTradingAdd copyRatio formatter] 格式化后返回:', result) + return result + }} /> )} @@ -383,6 +421,12 @@ const CopyTradingAdd: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('copyTradingAdd.fixedAmountPlaceholder') || '固定金额,不随 Leader 订单大小变化,必须 >= 1'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> )} @@ -400,6 +444,12 @@ const CopyTradingAdd: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('copyTradingAdd.maxOrderSizePlaceholder') || '仅在比例模式下生效(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -427,6 +477,12 @@ const CopyTradingAdd: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('copyTradingAdd.minOrderSizePlaceholder') || '仅在比例模式下生效,必须 >= 1(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -443,6 +499,12 @@ const CopyTradingAdd: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('copyTradingAdd.maxDailyLossPlaceholder') || '默认 10000 USDC(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -471,6 +533,12 @@ const CopyTradingAdd: React.FC = () => { precision={2} style={{ width: '100%' }} placeholder={t('copyTradingAdd.priceTolerancePlaceholder') || '默认 5%(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -498,6 +566,12 @@ const CopyTradingAdd: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('copyTradingAdd.minOrderDepthPlaceholder') || '例如:100(可选,不填写表示不启用)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -512,6 +586,12 @@ const CopyTradingAdd: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('copyTradingAdd.maxSpreadPlaceholder') || '例如:0.05(5美分,可选,不填写表示不启用)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -531,6 +611,12 @@ const CopyTradingAdd: React.FC = () => { precision={4} style={{ width: '50%' }} placeholder={t('copyTradingAdd.minPricePlaceholder') || '最低价(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> - @@ -542,6 +628,12 @@ const CopyTradingAdd: React.FC = () => { precision={4} style={{ width: '50%' }} placeholder={t('copyTradingAdd.maxPricePlaceholder') || '最高价(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -560,6 +652,12 @@ const CopyTradingAdd: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('copyTradingAdd.maxPositionValuePlaceholder') || '例如:100(可选,不填写表示不启用)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> diff --git a/frontend/src/pages/CopyTradingEdit.tsx b/frontend/src/pages/CopyTradingEdit.tsx index db1abf8..c8b2469 100644 --- a/frontend/src/pages/CopyTradingEdit.tsx +++ b/frontend/src/pages/CopyTradingEdit.tsx @@ -237,13 +237,51 @@ const CopyTradingEdit: React.FC = () => { tooltip={t('copyTradingEdit.copyRatioTooltip') || '跟单比例表示跟单金额相对于 Leader 订单金额的百分比'} > { + console.log('[CopyTradingEdit copyRatio parser] 输入值:', value, '类型:', typeof value) + // 移除 % 符号和其他非数字字符(保留小数点和负号) + const cleaned = (value || '').toString().replace(/%/g, '').trim() + console.log('[CopyTradingEdit copyRatio parser] 清理后:', cleaned) + const parsed = parseFloat(cleaned) || 0 + console.log('[CopyTradingEdit copyRatio parser] 解析后:', parsed) + if (parsed > 10000) { + console.log('[CopyTradingEdit copyRatio parser] 超过最大值,返回 10000') + return 10000 + } + if (parsed < 0.01) { + console.log('[CopyTradingEdit copyRatio parser] 小于最小值,返回 0.01') + return 0.01 + } + console.log('[CopyTradingEdit copyRatio parser] 返回:', parsed) + return parsed + }} + formatter={(value) => { + console.log('[CopyTradingEdit copyRatio formatter] 输入值:', value, '类型:', typeof value) + if (!value && value !== 0) { + console.log('[CopyTradingEdit copyRatio formatter] 空值,返回空字符串') + return '' + } + const num = parseFloat(value.toString()) + console.log('[CopyTradingEdit copyRatio formatter] 解析后:', num) + if (isNaN(num)) { + console.log('[CopyTradingEdit copyRatio formatter] NaN,返回空字符串') + return '' + } + if (num > 10000) { + console.log('[CopyTradingEdit copyRatio formatter] 超过最大值,返回 10000') + return '10000' + } + const result = num.toString().replace(/\.0+$/, '') + console.log('[CopyTradingEdit copyRatio formatter] 格式化后返回:', result) + return result + }} /> )} @@ -276,6 +314,12 @@ const CopyTradingEdit: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('copyTradingEdit.fixedAmountPlaceholder') || '固定金额,不随 Leader 订单大小变化,必须 >= 1'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> )} @@ -293,6 +337,12 @@ const CopyTradingEdit: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('copyTradingEdit.maxOrderSizePlaceholder') || '仅在比例模式下生效(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -320,6 +370,12 @@ const CopyTradingEdit: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('copyTradingEdit.minOrderSizePlaceholder') || '仅在比例模式下生效,必须 >= 1(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -336,6 +392,12 @@ const CopyTradingEdit: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('copyTradingEdit.maxDailyLossPlaceholder') || '默认 10000 USDC(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -364,6 +426,12 @@ const CopyTradingEdit: React.FC = () => { precision={2} style={{ width: '100%' }} placeholder={t('copyTradingEdit.priceTolerancePlaceholder') || '默认 5%(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -391,6 +459,12 @@ const CopyTradingEdit: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('copyTradingEdit.minOrderDepthPlaceholder') || '例如:100(可选,不填写表示不启用)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -405,6 +479,12 @@ const CopyTradingEdit: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('copyTradingEdit.maxSpreadPlaceholder') || '例如:0.05(5美分,可选,不填写表示不启用)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -424,6 +504,12 @@ const CopyTradingEdit: React.FC = () => { precision={4} style={{ width: '50%' }} placeholder={t('copyTradingEdit.minPricePlaceholder') || '最低价(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> - @@ -435,6 +521,12 @@ const CopyTradingEdit: React.FC = () => { precision={4} style={{ width: '50%' }} placeholder={t('copyTradingEdit.maxPricePlaceholder') || '最高价(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -453,6 +545,12 @@ const CopyTradingEdit: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('copyTradingEdit.maxPositionValuePlaceholder') || '例如:100(可选,不填写表示不启用)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> diff --git a/frontend/src/pages/CopyTradingList.tsx b/frontend/src/pages/CopyTradingList.tsx index 4e08494..8ee8cbe 100644 --- a/frontend/src/pages/CopyTradingList.tsx +++ b/frontend/src/pages/CopyTradingList.tsx @@ -196,7 +196,7 @@ const CopyTradingList: React.FC = () => { render: (_: any, record: CopyTrading) => ( {record.copyMode === 'RATIO' - ? `${t('copyTradingList.ratioMode') || '比例'} ${record.copyRatio}x` + ? `${t('copyTradingList.ratioMode') || '比例'} ${(parseFloat(record.copyRatio || '0') * 100).toFixed(2).replace(/\.0+$/, '')}%` : `${t('copyTradingList.fixedAmountMode') || '固定'} ${formatUSDC(record.fixedAmount || '0')}` } @@ -466,7 +466,7 @@ const CopyTradingList: React.FC = () => { color: '#666' }}> {record.copyMode === 'RATIO' - ? `${t('copyTradingList.ratioMode') || '比例'} ${record.copyRatio}x` + ? `${t('copyTradingList.ratioMode') || '比例'} ${(parseFloat(record.copyRatio || '0') * 100).toFixed(2).replace(/\.0+$/, '')}%` : `${t('copyTradingList.fixedAmountMode') || '固定'} ${formatUSDC(record.fixedAmount || '0')}` } diff --git a/frontend/src/pages/CopyTradingOrders/EditModal.tsx b/frontend/src/pages/CopyTradingOrders/EditModal.tsx index ecca19c..61942b3 100644 --- a/frontend/src/pages/CopyTradingOrders/EditModal.tsx +++ b/frontend/src/pages/CopyTradingOrders/EditModal.tsx @@ -237,13 +237,51 @@ const EditModal: React.FC = ({ tooltip={t('copyTradingEdit.copyRatioTooltip') || '跟单比例表示跟单金额相对于 Leader 订单金额的百分比'} > { + console.log('[EditModal copyRatio parser] 输入值:', value, '类型:', typeof value) + // 移除 % 符号和其他非数字字符(保留小数点和负号) + const cleaned = (value || '').toString().replace(/%/g, '').trim() + console.log('[EditModal copyRatio parser] 清理后:', cleaned) + const parsed = parseFloat(cleaned) || 0 + console.log('[EditModal copyRatio parser] 解析后:', parsed) + if (parsed > 10000) { + console.log('[EditModal copyRatio parser] 超过最大值,返回 10000') + return 10000 + } + if (parsed < 0.01) { + console.log('[EditModal copyRatio parser] 小于最小值,返回 0.01') + return 0.01 + } + console.log('[EditModal copyRatio parser] 返回:', parsed) + return parsed + }} + formatter={(value) => { + console.log('[EditModal copyRatio formatter] 输入值:', value, '类型:', typeof value) + if (!value && value !== 0) { + console.log('[EditModal copyRatio formatter] 空值,返回空字符串') + return '' + } + const num = parseFloat(value.toString()) + console.log('[EditModal copyRatio formatter] 解析后:', num) + if (isNaN(num)) { + console.log('[EditModal copyRatio formatter] NaN,返回空字符串') + return '' + } + if (num > 10000) { + console.log('[EditModal copyRatio formatter] 超过最大值,返回 10000') + return '10000' + } + const result = num.toString().replace(/\.0+$/, '') + console.log('[EditModal copyRatio formatter] 格式化后返回:', result) + return result + }} /> )} @@ -276,6 +314,12 @@ const EditModal: React.FC = ({ precision={4} style={{ width: '100%' }} placeholder={t('copyTradingEdit.fixedAmountPlaceholder') || '固定金额,不随 Leader 订单大小变化,必须 >= 1'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> )} @@ -293,6 +337,12 @@ const EditModal: React.FC = ({ precision={4} style={{ width: '100%' }} placeholder={t('copyTradingEdit.maxOrderSizePlaceholder') || '仅在比例模式下生效(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -320,6 +370,12 @@ const EditModal: React.FC = ({ precision={4} style={{ width: '100%' }} placeholder={t('copyTradingEdit.minOrderSizePlaceholder') || '仅在比例模式下生效,必须 >= 1(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -336,6 +392,12 @@ const EditModal: React.FC = ({ precision={4} style={{ width: '100%' }} placeholder={t('copyTradingEdit.maxDailyLossPlaceholder') || '默认 10000 USDC(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -364,6 +426,12 @@ const EditModal: React.FC = ({ precision={2} style={{ width: '100%' }} placeholder={t('copyTradingEdit.priceTolerancePlaceholder') || '默认 5%(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -391,6 +459,12 @@ const EditModal: React.FC = ({ precision={4} style={{ width: '100%' }} placeholder={t('copyTradingEdit.minOrderDepthPlaceholder') || '例如:100(可选,不填写表示不启用)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -405,6 +479,12 @@ const EditModal: React.FC = ({ precision={4} style={{ width: '100%' }} placeholder={t('copyTradingEdit.maxSpreadPlaceholder') || '例如:0.05(5美分,可选,不填写表示不启用)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -424,6 +504,12 @@ const EditModal: React.FC = ({ precision={4} style={{ width: '50%' }} placeholder={t('copyTradingEdit.minPricePlaceholder') || '最低价(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> - @@ -435,6 +521,12 @@ const EditModal: React.FC = ({ precision={4} style={{ width: '50%' }} placeholder={t('copyTradingEdit.maxPricePlaceholder') || '最高价(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -453,6 +545,12 @@ const EditModal: React.FC = ({ precision={4} style={{ width: '100%' }} placeholder={t('copyTradingEdit.maxPositionValuePlaceholder') || '例如:100(可选,不填写表示不启用)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> diff --git a/frontend/src/pages/TemplateAdd.tsx b/frontend/src/pages/TemplateAdd.tsx index 63a8a1a..0256425 100644 --- a/frontend/src/pages/TemplateAdd.tsx +++ b/frontend/src/pages/TemplateAdd.tsx @@ -127,23 +127,24 @@ const TemplateAdd: React.FC = () => { tooltip={t('templateAdd.copyRatioTooltip') || '跟单比例表示跟单金额相对于 Leader 订单金额的百分比。例如:100% 表示 1:1 跟单,50% 表示半仓跟单,200% 表示双倍跟单'} > { const parsed = parseFloat(value || '0') - if (parsed > 1000) return 1000 + if (parsed > 10000) return 10000 return parsed }} formatter={(value) => { - if (!value) return '' + if (!value && value !== 0) return '' const num = parseFloat(value.toString()) - if (num > 1000) return '1000' - return value.toString() + if (isNaN(num)) return '' + if (num > 10000) return '10000' + return num.toString().replace(/\.0+$/, '') }} /> @@ -177,6 +178,12 @@ const TemplateAdd: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('templateAdd.fixedAmountPlaceholder') || '固定金额,不随 Leader 订单大小变化,必须 >= 1'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> )} @@ -194,6 +201,12 @@ const TemplateAdd: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('templateAdd.maxOrderSizePlaceholder') || '仅在比例模式下生效(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -221,6 +234,12 @@ const TemplateAdd: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('templateAdd.minOrderSizePlaceholder') || '仅在比例模式下生效,必须 >= 1(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -251,6 +270,12 @@ const TemplateAdd: React.FC = () => { precision={2} style={{ width: '100%' }} placeholder={t('templateAdd.priceTolerancePlaceholder') || '默认 5%(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -265,6 +290,12 @@ const TemplateAdd: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('templateAdd.minOrderDepthPlaceholder') || '例如:100(可选,不填写表示不启用)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -279,6 +310,12 @@ const TemplateAdd: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('templateAdd.maxSpreadPlaceholder') || '例如:0.05(5美分,可选,不填写表示不启用)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -298,6 +335,12 @@ const TemplateAdd: React.FC = () => { precision={4} style={{ width: '50%' }} placeholder={t('templateAdd.minPricePlaceholder') || '最低价(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> - @@ -309,6 +352,12 @@ const TemplateAdd: React.FC = () => { precision={4} style={{ width: '50%' }} placeholder={t('templateAdd.maxPricePlaceholder') || '最高价(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> diff --git a/frontend/src/pages/TemplateEdit.tsx b/frontend/src/pages/TemplateEdit.tsx index 8d0e212..2aa379d 100644 --- a/frontend/src/pages/TemplateEdit.tsx +++ b/frontend/src/pages/TemplateEdit.tsx @@ -162,23 +162,24 @@ const TemplateEdit: React.FC = () => { tooltip={t('templateEdit.copyRatioTooltip') || '跟单比例表示跟单金额相对于 Leader 订单金额的百分比。例如:100% 表示 1:1 跟单,50% 表示半仓跟单,200% 表示双倍跟单'} > { const parsed = parseFloat(value || '0') - if (parsed > 1000) return 1000 + if (parsed > 10000) return 10000 return parsed }} formatter={(value) => { - if (!value) return '' + if (!value && value !== 0) return '' const num = parseFloat(value.toString()) - if (num > 1000) return '1000' - return value.toString() + if (isNaN(num)) return '' + if (num > 10000) return '10000' + return num.toString().replace(/\.0+$/, '') }} /> @@ -213,6 +214,12 @@ const TemplateEdit: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('templateEdit.fixedAmountPlaceholder') || '固定金额,不随 Leader 订单大小变化,必须 >= 1'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> )} @@ -230,6 +237,12 @@ const TemplateEdit: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('templateEdit.maxOrderSizePlaceholder') || '仅在比例模式下生效(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -257,6 +270,12 @@ const TemplateEdit: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('templateEdit.minOrderSizePlaceholder') || '仅在比例模式下生效,必须 >= 1(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -287,6 +306,12 @@ const TemplateEdit: React.FC = () => { precision={2} style={{ width: '100%' }} placeholder={t('templateEdit.priceTolerancePlaceholder') || '默认 5%(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -301,6 +326,12 @@ const TemplateEdit: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('templateEdit.minOrderDepthPlaceholder') || '例如:100(可选,不填写表示不启用)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -315,6 +346,12 @@ const TemplateEdit: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder={t('templateEdit.maxSpreadPlaceholder') || '例如:0.05(5美分,可选,不填写表示不启用)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -334,6 +371,12 @@ const TemplateEdit: React.FC = () => { precision={4} style={{ width: '50%' }} placeholder={t('templateEdit.minPricePlaceholder') || '最低价(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> - @@ -345,6 +388,12 @@ const TemplateEdit: React.FC = () => { precision={4} style={{ width: '50%' }} placeholder={t('templateEdit.maxPricePlaceholder') || '最高价(可选)'} + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> diff --git a/frontend/src/pages/TemplateList.tsx b/frontend/src/pages/TemplateList.tsx index 5eeeb5e..dc1bb9f 100644 --- a/frontend/src/pages/TemplateList.tsx +++ b/frontend/src/pages/TemplateList.tsx @@ -471,23 +471,24 @@ const TemplateList: React.FC = () => { tooltip="跟单比例表示跟单金额相对于 Leader 订单金额的百分比。例如:100% 表示 1:1 跟单,50% 表示半仓跟单,200% 表示双倍跟单" > { const parsed = parseFloat(value || '0') - if (parsed > 1000) return 1000 + if (parsed > 10000) return 10000 return parsed }} formatter={(value) => { - if (!value) return '' + if (!value && value !== 0) return '' const num = parseFloat(value.toString()) - if (num > 1000) return '1000' - return value.toString() + if (isNaN(num)) return '' + if (num > 10000) return '10000' + return num.toString().replace(/\.0+$/, '') }} /> @@ -520,6 +521,12 @@ const TemplateList: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder="固定金额,不随 Leader 订单大小变化,必须 >= 1" + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> )} @@ -537,6 +544,12 @@ const TemplateList: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder="仅在比例模式下生效(可选)" + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -564,6 +577,12 @@ const TemplateList: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder="仅在比例模式下生效,必须 >= 1(可选)" + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -594,6 +613,12 @@ const TemplateList: React.FC = () => { precision={2} style={{ width: '100%' }} placeholder="默认 5%(可选)" + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -619,6 +644,12 @@ const TemplateList: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder="例如:100(可选,不填写表示不启用)" + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -633,6 +664,12 @@ const TemplateList: React.FC = () => { precision={4} style={{ width: '100%' }} placeholder="例如:0.05(5美分,可选,不填写表示不启用)" + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> @@ -652,6 +689,12 @@ const TemplateList: React.FC = () => { precision={4} style={{ width: '50%' }} placeholder="最低价(留空不限制)" + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> - @@ -663,6 +706,12 @@ const TemplateList: React.FC = () => { precision={4} style={{ width: '50%' }} placeholder="最高价(留空不限制)" + formatter={(value) => { + if (!value && value !== 0) return '' + const num = parseFloat(value.toString()) + if (isNaN(num)) return '' + return num.toString().replace(/\.0+$/, '') + }} /> From 185cade11d0904ad2b41b4e8c31ac758a528bea0 Mon Sep 17 00:00:00 2001 From: WrBug Date: Sun, 4 Jan 2026 12:46:35 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=8C=89=E6=AF=94?= =?UTF-8?q?=E4=BE=8B=E8=B7=9F=E5=8D=95=E9=87=91=E9=A2=9D=E7=B2=BE=E5=BA=A6?= =?UTF-8?q?=E9=97=AE=E9=A2=98=EF=BC=8C=E5=AF=B9=E9=87=91=E9=A2=9D=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E5=90=91=E4=B8=8A=E5=8F=96=E6=95=B4=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复按比例计算的订单金额因精度问题导致低于最小限制的问题 - 对按比例计算的金额进行向上取整到2位小数(USDC精度) - 如果向上取整后的金额仍低于最小限制,调整 buyQuantity 以满足最小限制 - 优化 BigDecimal.div 扩展函数,支持精度和舍入模式参数(带默认值) 修复的文件: - MathExt.kt: 添加 div 扩展函数的重载版本,支持精度和舍入模式参数 - CopyOrderTrackingService.kt: 修复订单金额精度处理逻辑 问题: 订单金额 0.000999999860888892000000 因精度问题低于最小限制 1.00000000, 导致订单被错误跳过。 解决方案: 1. 对按比例计算的金额向上取整到2位小数 2. 如果向上取整后仍低于最小限制,调整数量以满足最小限制 3. 确保调整后的订单金额 >= minOrderSize --- .../statistics/CopyOrderTrackingService.kt | 45 ++++++++++++++++--- .../com/wrbug/polymarketbot/util/MathExt.kt | 20 +++++---- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyOrderTrackingService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyOrderTrackingService.kt index 73a6b21..913ad9f 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyOrderTrackingService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyOrderTrackingService.kt @@ -376,15 +376,46 @@ open class CopyOrderTrackingService( // 验证订单数量限制(仅比例模式) var finalBuyQuantity = buyQuantity if (copyTrading.copyMode == "RATIO") { - val orderAmount = buyQuantity.multi(trade.price.toSafeBigDecimal()) - if (orderAmount.lt(copyTrading.minOrderSize)) { - logger.warn("订单金额低于最小限制,跳过: copyTradingId=${copyTrading.id}, amount=$orderAmount, min=${copyTrading.minOrderSize}") - continue + val tradePrice = trade.price.toSafeBigDecimal() + val rawOrderAmount = buyQuantity.multi(tradePrice) + + // 对按比例计算的金额进行向上取整处理(确保满足最小限制) + // 向上取整到 2 位小数(USDC 精度) + val roundedOrderAmount = rawOrderAmount.setScale(2, java.math.RoundingMode.CEILING) + + // 如果原始金额或向上取整后的金额小于最小限制,调整 buyQuantity 以满足最小限制 + // 这样可以避免精度问题导致订单被错误地跳过 + if (roundedOrderAmount.lt(copyTrading.minOrderSize)) { + logger.debug("订单金额(向上取整后)低于最小限制,调整数量以满足最小限制: copyTradingId=${copyTrading.id}, rawAmount=$rawOrderAmount, roundedAmount=$roundedOrderAmount, min=${copyTrading.minOrderSize}") + // 计算满足最小限制所需的数量(向上取整) + val minQuantity = copyTrading.minOrderSize.div(tradePrice, 8, java.math.RoundingMode.CEILING) + if (minQuantity.lte(BigDecimal.ZERO)) { + logger.warn("计算出的最小数量为0或负数,跳过: copyTradingId=${copyTrading.id}") + continue + } + // 使用调整后的数量 + finalBuyQuantity = minQuantity + logger.debug("已调整数量以满足最小限制: copyTradingId=${copyTrading.id}, originalQuantity=$buyQuantity, adjustedQuantity=$finalBuyQuantity, adjustedAmount=${finalBuyQuantity.multi(tradePrice)}") + } else if (rawOrderAmount.lt(copyTrading.minOrderSize)) { + // 原始金额小于最小限制,但向上取整后满足,调整数量以满足最小限制 + logger.debug("订单金额(精度处理后)低于最小限制,调整数量以满足最小限制: copyTradingId=${copyTrading.id}, rawAmount=$rawOrderAmount, min=${copyTrading.minOrderSize}") + // 计算满足最小限制所需的数量(向上取整) + val minQuantity = copyTrading.minOrderSize.div(tradePrice, 8, java.math.RoundingMode.CEILING) + if (minQuantity.lte(BigDecimal.ZERO)) { + logger.warn("计算出的最小数量为0或负数,跳过: copyTradingId=${copyTrading.id}") + continue + } + // 使用调整后的数量 + finalBuyQuantity = minQuantity + logger.debug("已调整数量以满足最小限制: copyTradingId=${copyTrading.id}, originalQuantity=$buyQuantity, adjustedQuantity=$finalBuyQuantity, adjustedAmount=${finalBuyQuantity.multi(tradePrice)}") } - if (orderAmount.gt(copyTrading.maxOrderSize)) { - logger.warn("订单金额超过最大限制,调整数量: copyTradingId=${copyTrading.id}, amount=$orderAmount, max=${copyTrading.maxOrderSize}") + + // 检查最大限制(使用调整后的数量) + val finalOrderAmount = finalBuyQuantity.multi(tradePrice) + if (finalOrderAmount.gt(copyTrading.maxOrderSize)) { + logger.warn("订单金额超过最大限制,调整数量: copyTradingId=${copyTrading.id}, amount=$finalOrderAmount, max=${copyTrading.maxOrderSize}") // 调整数量到最大值 - val adjustedQuantity = copyTrading.maxOrderSize.div(trade.price.toSafeBigDecimal()) + val adjustedQuantity = copyTrading.maxOrderSize.div(tradePrice, 8, java.math.RoundingMode.DOWN) if (adjustedQuantity.lte(BigDecimal.ZERO)) { logger.warn("调整后的数量为0或负数,跳过: copyTradingId=${copyTrading.id}") continue diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/MathExt.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/MathExt.kt index 826412b..7102604 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/MathExt.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/MathExt.kt @@ -26,21 +26,23 @@ fun BigDecimal.multi(value: Any): BigDecimal { return BigDecimal.ZERO } + /** - * BigDecimal除法扩展函数 + * BigDecimal除法扩展函数(带精度和舍入模式) * 安全地将BigDecimal与任意数值类型相除 * @param value 除数,支持BigDecimal、BigInteger类型或可转换为BigDecimal的字符串 - * @return 除法结果,精度为18位小数,使用四舍五入模式,如果转换失败返回IllegalBigDecimal + * @param scale 精度(小数位数) + * @param roundingMode 舍入模式 + * @return 除法结果,如果转换失败返回IllegalBigDecimal */ -fun BigDecimal.div(value: Any): BigDecimal { +fun BigDecimal.div(value: Any, scale: Int = 18, roundingMode: RoundingMode = RoundingMode.HALF_UP): BigDecimal { kotlin.runCatching { - if (value is BigDecimal) { - return divide(value, 18, RoundingMode.HALF_UP).stripTrailingZeros() + val divisor = when (value) { + is BigDecimal -> value + is BigInteger -> value.toBigDecimal() + else -> BigDecimal(value.toString()) } - if (value is BigInteger) { - return divide(value.toSafeBigDecimal(), 18, RoundingMode.HALF_UP).stripTrailingZeros() - } - return divide(BigDecimal(value.toString()), 18, RoundingMode.HALF_UP).stripTrailingZeros() + return divide(divisor, scale, roundingMode) } return IllegalBigDecimal } From 95930332df0f40b77ff5cb3ba1251ea2fd96c20c Mon Sep 17 00:00:00 2001 From: WrBug Date: Sun, 4 Jan 2026 12:49:20 +0800 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E5=88=B7?= =?UTF-8?q?=E6=96=B0=E4=BB=A3=E7=90=86=E9=92=B1=E5=8C=85=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0copyRatio=E7=B2=BE=E5=BA=A6=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除刷新代理钱包相关接口和方法 - AccountController: 移除 /refresh-proxy 和 /refresh-all-proxies 接口 - AccountService: 移除 refreshProxyAddress 和 refreshAllProxyAddresses 方法 - 增加 copyRatio 字段精度支持 - CopyTrading: copyRatio 精度从 DECIMAL(10,2) 增加到 DECIMAL(20,8) - CopyTradingTemplate: copyRatio 精度从 DECIMAL(10,2) 增加到 DECIMAL(20,8) - 新增数据库迁移脚本 V18__increase_copy_ratio_precision.sql - 前端工具函数 - 添加 formatNumber 函数用于格式化数字显示 修复的文件: - AccountController.kt - AccountService.kt - CopyTrading.kt - CopyTradingTemplate.kt - frontend/src/utils/index.ts - V18__increase_copy_ratio_precision.sql --- .../controller/accounts/AccountController.kt | 59 -------------- .../wrbug/polymarketbot/entity/CopyTrading.kt | 2 +- .../entity/CopyTradingTemplate.kt | 2 +- .../service/accounts/AccountService.kt | 79 ------------------- .../V18__increase_copy_ratio_precision.sql | 14 ++++ frontend/src/utils/index.ts | 31 ++++++++ 6 files changed, 47 insertions(+), 140 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V18__increase_copy_ratio_precision.sql diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/accounts/AccountController.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/accounts/AccountController.kt index 152ba85..44ac456 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/accounts/AccountController.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/accounts/AccountController.kt @@ -95,65 +95,6 @@ class AccountController( } } - /** - * 刷新账户的代理地址 - * 使用最新的代理地址计算逻辑(支持 Magic 和 Safe 两种类型) - */ - @PostMapping("/refresh-proxy") - fun refreshProxyAddress(@RequestBody request: AccountDetailRequest): ResponseEntity> { - return try { - if (request.accountId == null || request.accountId <= 0) { - return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID, messageSource = messageSource)) - } - - val result = accountService.refreshProxyAddress(request.accountId) - result.fold( - onSuccess = { account -> - ResponseEntity.ok(ApiResponse.success(account)) - }, - onFailure = { e -> - logger.error("刷新代理地址失败: ${e.message}", e) - when (e) { - is IllegalArgumentException -> ResponseEntity.ok( - ApiResponse.error( - ErrorCode.PARAM_ERROR, - e.message, - messageSource - ) - ) - - else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource)) - } - } - ) - } catch (e: Exception) { - logger.error("刷新代理地址异常: ${e.message}", e) - ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource)) - } - } - - /** - * 批量刷新所有账户的代理地址 - */ - @PostMapping("/refresh-all-proxies") - fun refreshAllProxyAddresses(): ResponseEntity>> { - return try { - val result = accountService.refreshAllProxyAddresses() - result.fold( - onSuccess = { accounts -> - ResponseEntity.ok(ApiResponse.success(accounts)) - }, - onFailure = { e -> - logger.error("批量刷新代理地址失败: ${e.message}", e) - ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource)) - } - ) - } catch (e: Exception) { - logger.error("批量刷新代理地址异常: ${e.message}", e) - ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource)) - } - } - /** * 删除账户 */ diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/CopyTrading.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/CopyTrading.kt index ec2aa46..22da6b4 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/CopyTrading.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/CopyTrading.kt @@ -32,7 +32,7 @@ data class CopyTrading( @Column(name = "copy_mode", nullable = false, length = 10) val copyMode: String = "RATIO", // "RATIO" 或 "FIXED" - @Column(name = "copy_ratio", nullable = false, precision = 10, scale = 2) + @Column(name = "copy_ratio", nullable = false, precision = 20, scale = 8) val copyRatio: BigDecimal = BigDecimal.ONE, // 仅在 copyMode="RATIO" 时生效 @Column(name = "fixed_amount", precision = 20, scale = 8) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/CopyTradingTemplate.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/CopyTradingTemplate.kt index 9a3d355..866ce86 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/CopyTradingTemplate.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/CopyTradingTemplate.kt @@ -20,7 +20,7 @@ data class CopyTradingTemplate( @Column(name = "copy_mode", nullable = false, length = 10) val copyMode: String = "RATIO", // "RATIO" 或 "FIXED" - @Column(name = "copy_ratio", nullable = false, precision = 10, scale = 2) + @Column(name = "copy_ratio", nullable = false, precision = 20, scale = 8) val copyRatio: BigDecimal = BigDecimal.ONE, // 仅在 copyMode="RATIO" 时生效 @Column(name = "fixed_amount", precision = 20, scale = 8) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt index e60300e..f9f7048 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt @@ -202,85 +202,6 @@ class AccountService( } } - /** - * 刷新账户的代理地址 - * 使用最新的代理地址计算逻辑(支持 Magic 和 Safe 两种类型) - */ - @Transactional - fun refreshProxyAddress(accountId: Long): Result { - return try { - val account = accountRepository.findById(accountId) - .orElse(null) ?: return Result.failure(IllegalArgumentException("账户不存在")) - - // 重新获取代理地址(使用保存的钱包类型) - val proxyAddress = runBlocking { - val proxyResult = blockchainService.getProxyAddress(account.walletAddress, account.walletType) - if (proxyResult.isSuccess) { - proxyResult.getOrNull() - ?: throw IllegalStateException("获取代理地址返回空值") - } else { - val error = proxyResult.exceptionOrNull() - throw IllegalStateException("获取代理地址失败: ${error?.message}") - } - } - - // 更新账户 - val updated = account.copy( - proxyAddress = proxyAddress, - updatedAt = System.currentTimeMillis() - ) - val saved = accountRepository.save(updated) - - logger.info("刷新代理地址成功: accountId=${accountId}, oldProxy=${account.proxyAddress}, newProxy=${proxyAddress}") - Result.success(toDto(saved)) - } catch (e: Exception) { - logger.error("刷新代理地址失败: accountId=${accountId}", e) - Result.failure(e) - } - } - - /** - * 刷新所有账户的代理地址 - */ - @Transactional - fun refreshAllProxyAddresses(): Result> { - return try { - val accounts = accountRepository.findAll() - val updatedAccounts = mutableListOf() - - accounts.forEach { account -> - try { - val proxyAddress = runBlocking { - val proxyResult = blockchainService.getProxyAddress(account.walletAddress, account.walletType) - if (proxyResult.isSuccess) { - proxyResult.getOrNull() - } else { - null - } - } - - if (proxyAddress != null && proxyAddress != account.proxyAddress) { - val updated = account.copy( - proxyAddress = proxyAddress, - updatedAt = System.currentTimeMillis() - ) - val saved = accountRepository.save(updated) - logger.info("刷新代理地址成功: accountId=${account.id}, oldProxy=${account.proxyAddress}, newProxy=${proxyAddress}") - updatedAccounts.add(toDto(saved)) - } - } catch (e: Exception) { - logger.warn("刷新账户 ${account.id} 代理地址失败: ${e.message}") - } - } - - logger.info("批量刷新代理地址完成: 更新了 ${updatedAccounts.size} 个账户") - Result.success(updatedAccounts) - } catch (e: Exception) { - logger.error("批量刷新代理地址失败", e) - Result.failure(e) - } - } - /** * 删除账户 */ diff --git a/backend/src/main/resources/db/migration/V18__increase_copy_ratio_precision.sql b/backend/src/main/resources/db/migration/V18__increase_copy_ratio_precision.sql new file mode 100644 index 0000000..361de07 --- /dev/null +++ b/backend/src/main/resources/db/migration/V18__increase_copy_ratio_precision.sql @@ -0,0 +1,14 @@ +-- ============================================ +-- V18: 增加 copy_ratio 字段的精度,支持更小的小数值 +-- ============================================ + +-- 修改 copy_trading 表的 copy_ratio 字段精度 +-- 从 DECIMAL(10, 2) 改为 DECIMAL(20, 8),支持更小的跟单比例值(如 0.001) +ALTER TABLE copy_trading +MODIFY COLUMN copy_ratio DECIMAL(20, 8) NOT NULL DEFAULT 1.00000000 COMMENT '跟单比例(仅在copyMode=RATIO时生效)'; + +-- 修改 copy_trading_templates 表的 copy_ratio 字段精度 +-- 从 DECIMAL(10, 2) 改为 DECIMAL(20, 8),支持更小的跟单比例值(如 0.001) +ALTER TABLE copy_trading_templates +MODIFY COLUMN copy_ratio DECIMAL(20, 8) NOT NULL DEFAULT 1.00000000 COMMENT '跟单比例(仅在copyMode=RATIO时生效)'; + diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts index acfab40..7acea1a 100644 --- a/frontend/src/utils/index.ts +++ b/frontend/src/utils/index.ts @@ -1,3 +1,34 @@ +/** + * 格式化数字,自动去除尾随零 + * @param value - 数字值(字符串或数字) + * @param maxDecimals - 最大小数位数(默认不限制) + * @returns 格式化后的字符串,如果值为空或无效则返回 '' + * @example + * formatNumber(100.00) => "100" + * formatNumber(100.50) => "100.5" + * formatNumber(100.55) => "100.55" + */ +export const formatNumber = (value: string | number | undefined | null, maxDecimals?: number): string => { + if (value === undefined || value === null || value === '') { + return '' + } + + const num = typeof value === 'string' ? parseFloat(value) : value + if (isNaN(num)) { + return '' + } + + // 如果有最大小数位数限制,先截断 + if (maxDecimals !== undefined) { + const multiplier = Math.pow(10, maxDecimals) + const truncated = Math.floor(num * multiplier) / multiplier + return truncated.toFixed(maxDecimals).replace(/\.?0+$/, '') + } + + // 直接转换为字符串,然后去除尾随零 + return num.toString().replace(/\.?0+$/, '') +} + /** * 格式化 USDC 金额 * 最多显示 4 位小数,自动去除尾随零(截断,不四舍五入) From 8b73121c5d679754cf91b7042671d79632b53035 Mon Sep 17 00:00:00 2001 From: WrBug Date: Sun, 4 Jan 2026 13:07:08 +0800 Subject: [PATCH 4/4] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=20RELEASE.md?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=20v1.1.5=20=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 RELEASE.md 顶部添加 v1.1.5 release notes - 删除临时文件:PR12_REVIEW.md、RELEASE_v1.1.4.md、RELEASE_v1.1.4_简版.md、RELEASE_v1.1.5.md - 统一使用 RELEASE.md 管理所有版本发布说明 --- RELEASE.md | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/RELEASE.md b/RELEASE.md index 4710932..c89d23e 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,3 +1,133 @@ +# v1.1.5 + +## 🔧 功能优化与改进 + +### 前端优化 + +#### 优化 InputNumber 输入框格式化 +- 优化数值输入框的格式化逻辑,修正正则表达式以正确处理整数显示 +- 更新所有相关 InputNumber 组件的 formatter 函数,确保显示准确性 +- 影响的组件:CopyTradingAdd、CopyTradingEdit、EditModal、TemplateAdd、TemplateEdit、TemplateList +- 影响范围:跟单配置、模板配置中的所有数值输入框 + +#### 优化数字显示格式 +- 添加 `formatNumber` 工具函数,自动去除小数尾随零(如 100.00 → 100) +- 统一所有数值输入框的显示格式,提升用户体验 + +### 后端优化 + +#### 优化按比例跟单金额计算逻辑 +- 优化按比例计算的订单金额处理,使用向上取整确保满足最小限制要求 +- 对订单金额进行向上取整处理(保留 2 位小数精度) +- 自动调整订单数量以满足最小限制要求 +- 使用 `RoundingMode.CEILING` 确保金额满足最小限制 +- 影响范围:按比例跟单的订单创建逻辑 +- 技术细节: + - 扩展 `BigDecimal.div()` 扩展函数,支持指定精度和舍入模式 + - 在 `CopyOrderTrackingService` 中优化金额计算和验证逻辑 + +#### 增强 copyRatio 精度支持 +- 将 copyRatio 字段精度从 DECIMAL(10,2) 增加到 DECIMAL(20,8) +- 支持更精确的跟单比例设置(最小 0.01%,最大 10000%) +- 影响的实体:CopyTrading、CopyTradingTemplate +- 数据库迁移:新增 V18 迁移脚本,自动升级数据库字段精度 + +## 🔧 功能优化 + +### 移除刷新代理钱包接口 +- **移除接口**: + - `POST /api/accounts/refresh-proxy` - 刷新单个账户的代理地址 + - `POST /api/accounts/refresh-all-proxies` - 刷新所有账户的代理地址 +- **原因**:代理地址应在账户导入时自动计算,无需手动刷新 +- **影响范围**:AccountController、AccountService +- **向后兼容性**:这些接口已不再使用,移除不影响现有功能 + +### 前端跟单比例配置优化 +- **最小比例**:从 10% 降低到 0.01%,支持更灵活的跟单比例设置 +- **最大比例**:增加到 10000%,满足大比例跟单需求 +- **显示格式**:比例模式显示为百分比(如 "100%" 而不是 "1x") +- **输入验证**:增强输入验证,确保比例在合理范围内 + +## 📊 变更统计 + +- **提交数量**:3 个提交 +- **文件变更**:15 个文件 +- **代码变更**:+575 行 / -194 行(净增加 381 行) + +### 详细文件变更 + +**后端变更**: +- `AccountController.kt` - 移除刷新代理钱包接口(-59 行) +- `AccountService.kt` - 移除刷新代理钱包方法(-79 行) +- `CopyTrading.kt` - 增加 copyRatio 精度 +- `CopyTradingTemplate.kt` - 增加 copyRatio 精度 +- `CopyOrderTrackingService.kt` - 优化按比例跟单金额计算逻辑(+45 行) +- `MathExt.kt` - 扩展 div 函数支持精度和舍入模式(+20 行) +- `V18__increase_copy_ratio_precision.sql` - 数据库迁移脚本(+14 行) + +**前端变更**: +- `CopyTradingAdd.tsx` - 优化 formatter、优化比例配置(+106 行) +- `CopyTradingEdit.tsx` - 优化 formatter、优化比例配置(+106 行) +- `CopyTradingList.tsx` - 优化比例显示格式 +- `CopyTradingOrders/EditModal.tsx` - 优化 formatter、优化比例配置(+106 行) +- `TemplateAdd.tsx` - 优化 formatter、优化比例配置(+65 行) +- `TemplateEdit.tsx` - 优化 formatter、优化比例配置(+65 行) +- `TemplateList.tsx` - 优化 formatter、优化比例配置(+65 行) +- `utils/index.ts` - 添加 formatNumber 工具函数(+31 行) + +## 🔧 技术细节 + +### 数据库变更 +- **迁移脚本**:`V18__increase_copy_ratio_precision.sql` +- **变更内容**: + - `copy_trading.copy_ratio`: DECIMAL(10,2) → DECIMAL(20,8) + - `copy_trading_templates.copy_ratio`: DECIMAL(10,2) → DECIMAL(20,8) +- **自动执行**:升级时会自动执行迁移脚本 + +### API 变更 +- **移除接口**: + - `POST /api/accounts/refresh-proxy` + - `POST /api/accounts/refresh-all-proxies` +- **无新增接口** + +### 前端变更 +- **工具函数**:新增 `formatNumber()` 函数,用于格式化数字显示 +- **组件更新**:所有数值输入框统一使用新的 formatter 函数 +- **显示优化**:跟单模式的比例显示为百分比格式 + +## 📝 升级说明 + +### 数据库升级 +本次版本包含数据库迁移脚本,升级时会自动执行: +- 自动增加 `copy_ratio` 字段的精度 +- 现有数据不受影响,精度升级是向后兼容的 + +### 配置变更 +无需额外配置变更。 + +### 兼容性 +- **向后兼容**:所有变更都是向后兼容的 +- **API 兼容**:移除的接口不影响现有功能(这些接口已不再使用) +- **数据兼容**:数据库字段精度升级不会影响现有数据 + +## 🎯 主要改进 + +1. **优化输入框格式化**:优化数值输入框的显示逻辑 +2. **优化跟单金额计算**:确保按比例跟单的金额满足最小限制要求 +3. **提升精度支持**:支持更精确的跟单比例设置(0.01% - 10000%) +4. **代码清理**:移除不再使用的刷新代理钱包接口 + +## 🔗 相关链接 + +- [GitHub Tag](https://github.com/WrBug/PolyHermes/releases/tag/v1.1.5) +- [变更日志](https://github.com/WrBug/PolyHermes/compare/v1.1.4...v1.1.5) + +## 🙏 致谢 + +感谢所有贡献者和测试用户的反馈与支持! + +--- + # v1.1.2 ## 🚀 主要功能