fix: 修复 walletType 字段问题和 WebSocket 内存泄漏,优化账户导入 UI
后端修复: - 添加 walletType 字段到 Account 实体和数据库(V17 迁移) - 修复 refreshProxyAddress 方法使用正确的 walletType - 修复 WebSocketTicketService 内存泄漏(添加定时清理任务) 前端优化: - 账户导入改为 Modal 弹窗,提升用户体验 - 默认钱包类型改为 Web3 Wallet(safe),并将其选项排在首位 - 更新多语言文本:将 MetaMask(浏览器钱包)改为 Web3钱包 - 修复 walletTypeHelp Tooltip 换行显示问题
This commit is contained in:
@@ -73,6 +73,7 @@ data class AccountDto(
|
|||||||
val proxyAddress: String, // Polymarket 代理钱包地址
|
val proxyAddress: String, // Polymarket 代理钱包地址
|
||||||
val accountName: String?,
|
val accountName: String?,
|
||||||
val isEnabled: Boolean, // 是否启用(用于订单推送等功能的开关)
|
val isEnabled: Boolean, // 是否启用(用于订单推送等功能的开关)
|
||||||
|
val walletType: String = "magic", // 钱包类型:magic(邮箱/OAuth登录)或 safe(MetaMask浏览器钱包)
|
||||||
val apiKeyConfigured: Boolean, // API Key 是否已配置(不返回实际 Key)
|
val apiKeyConfigured: Boolean, // API Key 是否已配置(不返回实际 Key)
|
||||||
val apiSecretConfigured: Boolean, // API Secret 是否已配置
|
val apiSecretConfigured: Boolean, // API Secret 是否已配置
|
||||||
val apiPassphraseConfigured: Boolean, // API Passphrase 是否已配置
|
val apiPassphraseConfigured: Boolean, // API Passphrase 是否已配置
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ data class Account(
|
|||||||
@Column(name = "is_enabled", nullable = false)
|
@Column(name = "is_enabled", nullable = false)
|
||||||
val isEnabled: Boolean = true, // 是否启用(用于订单推送等功能的开关)
|
val isEnabled: Boolean = true, // 是否启用(用于订单推送等功能的开关)
|
||||||
|
|
||||||
|
@Column(name = "wallet_type", nullable = false, length = 20)
|
||||||
|
val walletType: String = "magic", // 钱包类型:magic(邮箱/OAuth登录)或 safe(MetaMask浏览器钱包)
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false)
|
@Column(name = "created_at", nullable = false)
|
||||||
val createdAt: Long = System.currentTimeMillis(),
|
val createdAt: Long = System.currentTimeMillis(),
|
||||||
|
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ class AccountService(
|
|||||||
accountName = accountName,
|
accountName = accountName,
|
||||||
isDefault = false, // 不再支持默认账户
|
isDefault = false, // 不再支持默认账户
|
||||||
isEnabled = request.isEnabled,
|
isEnabled = request.isEnabled,
|
||||||
|
walletType = request.walletType, // 保存钱包类型
|
||||||
createdAt = System.currentTimeMillis(),
|
createdAt = System.currentTimeMillis(),
|
||||||
updatedAt = System.currentTimeMillis()
|
updatedAt = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
@@ -211,9 +212,9 @@ class AccountService(
|
|||||||
val account = accountRepository.findById(accountId)
|
val account = accountRepository.findById(accountId)
|
||||||
.orElse(null) ?: return Result.failure(IllegalArgumentException("账户不存在"))
|
.orElse(null) ?: return Result.failure(IllegalArgumentException("账户不存在"))
|
||||||
|
|
||||||
// 重新获取代理地址
|
// 重新获取代理地址(使用保存的钱包类型)
|
||||||
val proxyAddress = runBlocking {
|
val proxyAddress = runBlocking {
|
||||||
val proxyResult = blockchainService.getProxyAddress(account.walletAddress)
|
val proxyResult = blockchainService.getProxyAddress(account.walletAddress, account.walletType)
|
||||||
if (proxyResult.isSuccess) {
|
if (proxyResult.isSuccess) {
|
||||||
proxyResult.getOrNull()
|
proxyResult.getOrNull()
|
||||||
?: throw IllegalStateException("获取代理地址返回空值")
|
?: throw IllegalStateException("获取代理地址返回空值")
|
||||||
@@ -250,7 +251,7 @@ class AccountService(
|
|||||||
accounts.forEach { account ->
|
accounts.forEach { account ->
|
||||||
try {
|
try {
|
||||||
val proxyAddress = runBlocking {
|
val proxyAddress = runBlocking {
|
||||||
val proxyResult = blockchainService.getProxyAddress(account.walletAddress)
|
val proxyResult = blockchainService.getProxyAddress(account.walletAddress, account.walletType)
|
||||||
if (proxyResult.isSuccess) {
|
if (proxyResult.isSuccess) {
|
||||||
proxyResult.getOrNull()
|
proxyResult.getOrNull()
|
||||||
} else {
|
} else {
|
||||||
@@ -443,6 +444,7 @@ class AccountService(
|
|||||||
proxyAddress = account.proxyAddress,
|
proxyAddress = account.proxyAddress,
|
||||||
accountName = account.accountName,
|
accountName = account.accountName,
|
||||||
isEnabled = account.isEnabled,
|
isEnabled = account.isEnabled,
|
||||||
|
walletType = account.walletType,
|
||||||
apiKeyConfigured = account.apiKey != null,
|
apiKeyConfigured = account.apiKey != null,
|
||||||
apiSecretConfigured = account.apiSecret != null,
|
apiSecretConfigured = account.apiSecret != null,
|
||||||
apiPassphraseConfigured = account.apiPassphrase != null,
|
apiPassphraseConfigured = account.apiPassphrase != null,
|
||||||
|
|||||||
+10
@@ -1,5 +1,6 @@
|
|||||||
package com.wrbug.polymarketbot.service.auth
|
package com.wrbug.polymarketbot.service.auth
|
||||||
|
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
@@ -80,4 +81,13 @@ class WebSocketTicketService {
|
|||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
tickets.entries.removeIf { it.value.expiresAt < now }
|
tickets.entries.removeIf { it.value.expiresAt < now }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定时清理过期票据(每分钟执行一次)
|
||||||
|
* 防止过期票据长时间占用内存
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedRate = 60_000) // 60秒 = 60000毫秒
|
||||||
|
fun scheduledCleanup() {
|
||||||
|
cleanupExpiredTickets()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 添加 wallet_type 字段到 wallet_accounts 表
|
||||||
|
-- 用于区分 Magic 和 Safe 两种钱包类型
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 使用存储过程检查并添加字段(如果不存在)
|
||||||
|
DELIMITER $$
|
||||||
|
|
||||||
|
CREATE PROCEDURE IF NOT EXISTS add_wallet_type_if_not_exists()
|
||||||
|
BEGIN
|
||||||
|
DECLARE column_exists INT DEFAULT 0;
|
||||||
|
|
||||||
|
SELECT COUNT(*) INTO column_exists
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'wallet_accounts'
|
||||||
|
AND COLUMN_NAME = 'wallet_type';
|
||||||
|
|
||||||
|
IF column_exists = 0 THEN
|
||||||
|
ALTER TABLE wallet_accounts
|
||||||
|
ADD COLUMN wallet_type VARCHAR(20) NOT NULL DEFAULT 'magic' COMMENT '钱包类型:magic(邮箱/OAuth登录)或 safe(MetaMask浏览器钱包)' AFTER is_enabled;
|
||||||
|
END IF;
|
||||||
|
END$$
|
||||||
|
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
-- 执行存储过程
|
||||||
|
CALL add_wallet_type_if_not_exists();
|
||||||
|
|
||||||
|
-- 删除存储过程
|
||||||
|
DROP PROCEDURE IF EXISTS add_wallet_type_if_not_exists;
|
||||||
|
|
||||||
|
-- 为现有账户设置默认walletType为magic(如果值为NULL)
|
||||||
|
UPDATE wallet_accounts SET wallet_type = 'magic' WHERE wallet_type IS NULL OR wallet_type = '';
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
|||||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||||
const { importAccount, loading } = useAccountStore()
|
const { importAccount, loading } = useAccountStore()
|
||||||
const [importType, setImportType] = useState<ImportType>('privateKey')
|
const [importType, setImportType] = useState<ImportType>('privateKey')
|
||||||
const [walletType, setWalletType] = useState<WalletType>('magic')
|
const [walletType, setWalletType] = useState<WalletType>('safe')
|
||||||
const [derivedAddress, setDerivedAddress] = useState<string>('')
|
const [derivedAddress, setDerivedAddress] = useState<string>('')
|
||||||
const [addressError, setAddressError] = useState<string>('')
|
const [addressError, setAddressError] = useState<string>('')
|
||||||
|
|
||||||
@@ -211,7 +211,10 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
|||||||
label={
|
label={
|
||||||
<span>
|
<span>
|
||||||
{t('accountImport.walletType')}{' '}
|
{t('accountImport.walletType')}{' '}
|
||||||
<Tooltip title={t('accountImport.walletTypeHelp')}>
|
<Tooltip
|
||||||
|
title={t('accountImport.walletTypeHelp')}
|
||||||
|
overlayInnerStyle={{ whiteSpace: 'pre-line', maxWidth: '300px' }}
|
||||||
|
>
|
||||||
<QuestionCircleOutlined style={{ color: '#999' }} />
|
<QuestionCircleOutlined style={{ color: '#999' }} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
@@ -221,12 +224,12 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
|
|||||||
value={walletType}
|
value={walletType}
|
||||||
onChange={(e) => setWalletType(e.target.value)}
|
onChange={(e) => setWalletType(e.target.value)}
|
||||||
>
|
>
|
||||||
<Radio value="magic">
|
|
||||||
{t('accountImport.walletTypeMagic')}
|
|
||||||
</Radio>
|
|
||||||
<Radio value="safe">
|
<Radio value="safe">
|
||||||
{t('accountImport.walletTypeSafe')}
|
{t('accountImport.walletTypeSafe')}
|
||||||
</Radio>
|
</Radio>
|
||||||
|
<Radio value="magic">
|
||||||
|
{t('accountImport.walletTypeMagic')}
|
||||||
|
</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
|||||||
@@ -201,9 +201,9 @@
|
|||||||
"addressError": "Cannot derive address from private key",
|
"addressError": "Cannot derive address from private key",
|
||||||
"addressErrorMnemonic": "Cannot derive address from mnemonic",
|
"addressErrorMnemonic": "Cannot derive address from mnemonic",
|
||||||
"walletType": "Wallet Type",
|
"walletType": "Wallet Type",
|
||||||
"walletTypeHelp": "Magic: Polymarket accounts logged in via email or social accounts (Google, Twitter, etc.); MetaMask: Polymarket accounts connected via browser wallets like MetaMask",
|
"walletTypeHelp": "Web3 Wallet: Polymarket accounts connected via browser wallets like MetaMask\nMagic: Polymarket accounts logged in via email or social accounts (Google, Twitter, etc.)",
|
||||||
"walletTypeMagic": "Magic (Email/Social Login)",
|
"walletTypeMagic": "Magic (Email/Social Login)",
|
||||||
"walletTypeSafe": "MetaMask (Browser Wallet)"
|
"walletTypeSafe": "Web3 Wallet"
|
||||||
},
|
},
|
||||||
"leader": {
|
"leader": {
|
||||||
"title": "Leader Management",
|
"title": "Leader Management",
|
||||||
|
|||||||
@@ -201,9 +201,9 @@
|
|||||||
"addressError": "无法从私钥推导地址",
|
"addressError": "无法从私钥推导地址",
|
||||||
"addressErrorMnemonic": "无法从助记词推导地址",
|
"addressErrorMnemonic": "无法从助记词推导地址",
|
||||||
"walletType": "钱包类型",
|
"walletType": "钱包类型",
|
||||||
"walletTypeHelp": "Magic:通过邮箱或社交账号(如 Google、Twitter)登录的 Polymarket 账户;MetaMask:使用 MetaMask 等浏览器钱包连接的 Polymarket 账户",
|
"walletTypeHelp": "Web3钱包:使用 MetaMask 等浏览器钱包连接的 Polymarket 账户\nMagic:通过邮箱或社交账号(如 Google、Twitter)登录的 Polymarket 账户",
|
||||||
"walletTypeMagic": "Magic(邮箱/社交账号登录)",
|
"walletTypeMagic": "Magic(邮箱/社交账号登录)",
|
||||||
"walletTypeSafe": "MetaMask(浏览器钱包)"
|
"walletTypeSafe": "Web3钱包"
|
||||||
},
|
},
|
||||||
"leader": {
|
"leader": {
|
||||||
"title": "Leader 管理",
|
"title": "Leader 管理",
|
||||||
|
|||||||
@@ -201,9 +201,9 @@
|
|||||||
"addressError": "無法從私鑰推導地址",
|
"addressError": "無法從私鑰推導地址",
|
||||||
"addressErrorMnemonic": "無法從助記詞推導地址",
|
"addressErrorMnemonic": "無法從助記詞推導地址",
|
||||||
"walletType": "錢包類型",
|
"walletType": "錢包類型",
|
||||||
"walletTypeHelp": "Magic:透過郵箱或社群帳號(如 Google、Twitter)登入的 Polymarket 帳戶;MetaMask:使用 MetaMask 等瀏覽器錢包連接的 Polymarket 帳戶",
|
"walletTypeHelp": "Web3錢包:使用 MetaMask 等瀏覽器錢包連接的 Polymarket 帳戶\nMagic:透過郵箱或社群帳號(如 Google、Twitter)登入的 Polymarket 帳戶",
|
||||||
"walletTypeMagic": "Magic(郵箱/社群帳號登入)",
|
"walletTypeMagic": "Magic(郵箱/社群帳號登入)",
|
||||||
"walletTypeSafe": "MetaMask(瀏覽器錢包)"
|
"walletTypeSafe": "Web3錢包"
|
||||||
},
|
},
|
||||||
"leader": {
|
"leader": {
|
||||||
"title": "Leader 管理",
|
"title": "Leader 管理",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useAccountStore } from '../store/accountStore'
|
|||||||
import type { Account } from '../types'
|
import type { Account } from '../types'
|
||||||
import { useMediaQuery } from 'react-responsive'
|
import { useMediaQuery } from 'react-responsive'
|
||||||
import { formatUSDC } from '../utils'
|
import { formatUSDC } from '../utils'
|
||||||
|
import AccountImportForm from '../components/AccountImportForm'
|
||||||
|
|
||||||
const { Title } = Typography
|
const { Title } = Typography
|
||||||
|
|
||||||
@@ -25,11 +26,20 @@ const AccountList: React.FC = () => {
|
|||||||
const [editAccount, setEditAccount] = useState<Account | null>(null)
|
const [editAccount, setEditAccount] = useState<Account | null>(null)
|
||||||
const [editForm] = Form.useForm()
|
const [editForm] = Form.useForm()
|
||||||
const [editLoading, setEditLoading] = useState(false)
|
const [editLoading, setEditLoading] = useState(false)
|
||||||
|
const [accountImportModalVisible, setAccountImportModalVisible] = useState(false)
|
||||||
|
const [accountImportForm] = Form.useForm()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAccounts()
|
fetchAccounts()
|
||||||
}, [fetchAccounts])
|
}, [fetchAccounts])
|
||||||
|
|
||||||
|
const handleAccountImportSuccess = async () => {
|
||||||
|
message.success(t('accountImport.importSuccess'))
|
||||||
|
setAccountImportModalVisible(false)
|
||||||
|
accountImportForm.resetFields()
|
||||||
|
fetchAccounts()
|
||||||
|
}
|
||||||
|
|
||||||
// 加载所有账户的余额
|
// 加载所有账户的余额
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadBalances = async () => {
|
const loadBalances = async () => {
|
||||||
@@ -494,7 +504,7 @@ const AccountList: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={() => navigate('/accounts/import')}
|
onClick={() => setAccountImportModalVisible(true)}
|
||||||
size={isMobile ? 'middle' : 'large'}
|
size={isMobile ? 'middle' : 'large'}
|
||||||
block={isMobile}
|
block={isMobile}
|
||||||
style={isMobile ? { minHeight: '44px' } : undefined}
|
style={isMobile ? { minHeight: '44px' } : undefined}
|
||||||
@@ -836,6 +846,34 @@ const AccountList: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* 导入账户 Modal */}
|
||||||
|
<Modal
|
||||||
|
title={t('accountImport.title')}
|
||||||
|
open={accountImportModalVisible}
|
||||||
|
onCancel={() => {
|
||||||
|
setAccountImportModalVisible(false)
|
||||||
|
accountImportForm.resetFields()
|
||||||
|
}}
|
||||||
|
footer={null}
|
||||||
|
width={isMobile ? '95%' : 600}
|
||||||
|
style={{ top: isMobile ? 20 : 50 }}
|
||||||
|
bodyStyle={{ padding: '24px', maxHeight: 'calc(100vh - 150px)', overflow: 'auto' }}
|
||||||
|
destroyOnClose
|
||||||
|
maskClosable
|
||||||
|
closable
|
||||||
|
>
|
||||||
|
<AccountImportForm
|
||||||
|
form={accountImportForm}
|
||||||
|
onSuccess={handleAccountImportSuccess}
|
||||||
|
onCancel={() => {
|
||||||
|
setAccountImportModalVisible(false)
|
||||||
|
accountImportForm.resetFields()
|
||||||
|
}}
|
||||||
|
showAlert={true}
|
||||||
|
showCancelButton={true}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export interface Account {
|
|||||||
proxyAddress: string // Polymarket 代理钱包地址
|
proxyAddress: string // Polymarket 代理钱包地址
|
||||||
accountName?: string
|
accountName?: string
|
||||||
isEnabled?: boolean // 是否启用
|
isEnabled?: boolean // 是否启用
|
||||||
|
walletType?: string // 钱包类型:magic(邮箱/OAuth登录)或 safe(MetaMask浏览器钱包)
|
||||||
apiKeyConfigured: boolean
|
apiKeyConfigured: boolean
|
||||||
apiSecretConfigured: boolean
|
apiSecretConfigured: boolean
|
||||||
apiPassphraseConfigured: boolean
|
apiPassphraseConfigured: boolean
|
||||||
@@ -42,6 +43,7 @@ export interface AccountImportRequest {
|
|||||||
privateKey: string
|
privateKey: string
|
||||||
walletAddress: string
|
walletAddress: string
|
||||||
accountName?: string
|
accountName?: string
|
||||||
|
walletType?: string // 钱包类型:magic(邮箱/OAuth登录)或 safe(MetaMask浏览器钱包)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user