feat: 实现跟单订单跟踪和统计功能

- 添加订单跟踪实体和Repository(CopyOrderTracking, SellMatchRecord, SellMatchDetail)
- 实现订单跟踪服务(CopyOrderTrackingService),支持买入/卖出订单的创建和匹配
- 实现跟单统计服务(CopyTradingStatisticsService),支持盈亏统计和订单列表查询
- 添加失败交易记录(FailedTrade)和已处理交易去重(ProcessedTrade)
- 实现轮询服务(CopyTradingPollingService),使用Data API /activity接口轮询Leader交易
- 支持多元市场(使用outcomeIndex而不是YES/NO)
- 重试时重新生成salt并重新签名
- 添加风险控制检查(每日订单数、每日亏损、单笔订单大小限制)
- 支持价格容忍度和延迟跟单
- 添加数据库迁移文件(V8, V9, V10)
- 前端:账户管理显示代理钱包地址,添加复制功能,移除默认账户逻辑
- 修复API响应字段映射问题(orderID, transactionsHashes)
- 缩短轮询间隔到2秒
This commit is contained in:
WrBug
2025-12-02 03:34:43 +08:00
parent 844cd686eb
commit dffbc5124f
33 changed files with 2916 additions and 80 deletions
+90 -49
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Card, Table, Button, Space, Tag, Popconfirm, message, Typography, Spin, Modal, Descriptions, Divider, Form, Input, Checkbox, Alert } from 'antd'
import { PlusOutlined, StarOutlined, StarFilled, ReloadOutlined, EditOutlined } from '@ant-design/icons'
import { PlusOutlined, ReloadOutlined, EditOutlined, CopyOutlined } from '@ant-design/icons'
import { useAccountStore } from '../store/accountStore'
import type { Account } from '../types'
import { useMediaQuery } from 'react-responsive'
@@ -12,7 +12,7 @@ const { Title } = Typography
const AccountList: React.FC = () => {
const navigate = useNavigate()
const isMobile = useMediaQuery({ maxWidth: 768 })
const { accounts, loading, fetchAccounts, deleteAccount, setDefaultAccount, fetchAccountBalance, fetchAccountDetail, updateAccount } = useAccountStore()
const { accounts, loading, fetchAccounts, deleteAccount, fetchAccountBalance, fetchAccountDetail, updateAccount } = useAccountStore()
const [balanceMap, setBalanceMap] = useState<Record<number, { total: string; available: string; position: string }>>({})
const [balanceLoading, setBalanceLoading] = useState<Record<number, boolean>>({})
const [detailModalVisible, setDetailModalVisible] = useState(false)
@@ -71,13 +71,12 @@ const AccountList: React.FC = () => {
}
}
const handleSetDefault = async (account: Account) => {
try {
await setDefaultAccount(account.id)
message.success('设置默认账户成功')
} catch (error: any) {
message.error(error.message || '设置默认账户失败')
}
const handleCopy = (text: string, label: string) => {
navigator.clipboard.writeText(text).then(() => {
message.success(`${label}已复制到剪贴板`)
}).catch(() => {
message.error('复制失败')
})
}
const handleShowDetail = async (account: Account) => {
@@ -222,21 +221,34 @@ const AccountList: React.FC = () => {
title: '钱包地址',
dataIndex: 'walletAddress',
key: 'walletAddress',
render: (address: string) => (
<span style={{ fontFamily: 'monospace' }}>{address}</span>
render: (text: string) => (
<Space>
<span style={{ fontFamily: 'monospace', fontSize: '12px' }}>{text}</span>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopy(text, '钱包地址')}
title="复制钱包地址"
/>
</Space>
)
},
{
title: '默认账户',
dataIndex: 'isDefault',
key: 'isDefault',
render: (isDefault: boolean, record: Account) => (
<Button
type="text"
icon={isDefault ? <StarFilled style={{ color: '#faad14' }} /> : <StarOutlined />}
onClick={() => !isDefault && handleSetDefault(record)}
disabled={isDefault}
/>
title: '代理钱包地址',
dataIndex: 'proxyAddress',
key: 'proxyAddress',
render: (address: string) => (
<Space>
<span style={{ fontFamily: 'monospace', fontSize: '12px' }}>{address}</span>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopy(address, '代理钱包地址')}
title="复制代理钱包地址"
/>
</Space>
)
},
{
@@ -342,12 +354,28 @@ const AccountList: React.FC = () => {
fontFamily: 'monospace',
lineHeight: '1.4'
}}>
{record.walletAddress}
<div style={{ marginBottom: '4px' }}>
<strong>:</strong> {record.walletAddress}
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopy(record.walletAddress, '钱包地址')}
style={{ marginLeft: '4px', padding: '0 4px' }}
/>
</div>
<div>
<strong>:</strong> {record.proxyAddress}
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopy(record.proxyAddress, '代理钱包地址')}
style={{ marginLeft: '4px', padding: '0 4px' }}
/>
</div>
</div>
<div style={{ marginBottom: '8px', display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
<Tag color={record.isDefault ? 'gold' : 'default'} style={{ margin: 0 }}>
{record.isDefault ? '默认' : '普通'}
</Tag>
<Tag color={allConfigured ? 'success' : partialConfigured ? 'warning' : 'default'} style={{ margin: 0 }}>
{allConfigured ? '完整配置' : partialConfigured ? '部分配置' : '未配置'}
</Tag>
@@ -414,17 +442,6 @@ const AccountList: React.FC = () => {
>
</Button>
{!record.isDefault && (
<Button
size="small"
block
icon={<StarOutlined />}
onClick={() => handleSetDefault(record)}
style={{ minHeight: '32px' }}
>
</Button>
)}
<Popconfirm
title="确定要删除这个账户吗?"
description={
@@ -578,20 +595,44 @@ const AccountList: React.FC = () => {
{detailAccount.accountName || '-'}
</Descriptions.Item>
<Descriptions.Item label="钱包地址" span={isMobile ? 1 : 2}>
<span style={{
fontFamily: 'monospace',
fontSize: isMobile ? '11px' : '13px',
wordBreak: 'break-all',
lineHeight: '1.4',
display: 'block'
}}>
{detailAccount.walletAddress || '-'}
</span>
<Space>
<span style={{
fontFamily: 'monospace',
fontSize: isMobile ? '11px' : '13px',
wordBreak: 'break-all',
lineHeight: '1.4',
display: 'block'
}}>
{detailAccount.walletAddress || '-'}
</span>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopy(detailAccount.walletAddress || '', '钱包地址')}
title="复制钱包地址"
/>
</Space>
</Descriptions.Item>
<Descriptions.Item label="默认账户">
<Tag color={detailAccount.isDefault ? 'gold' : 'default'}>
{detailAccount.isDefault ? '是' : '否'}
</Tag>
<Descriptions.Item label="代理钱包地址" span={isMobile ? 1 : 2}>
<Space>
<span style={{
fontFamily: 'monospace',
fontSize: isMobile ? '11px' : '13px',
wordBreak: 'break-all',
lineHeight: '1.4',
display: 'block'
}}>
{detailAccount.proxyAddress || '-'}
</span>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopy(detailAccount.proxyAddress || '', '代理钱包地址')}
title="复制代理钱包地址"
/>
</Space>
</Descriptions.Item>
<Descriptions.Item label="总余额" span={isMobile ? 1 : 2}>
{detailBalanceLoading ? (
+3 -21
View File
@@ -14,7 +14,6 @@ interface AccountStore {
importAccount: (data: any) => Promise<void>
updateAccount: (data: any) => Promise<void>
deleteAccount: (accountId: number) => Promise<void>
setDefaultAccount: (accountId: number) => Promise<void>
fetchAccountDetail: (accountId: number) => Promise<Account>
fetchAccountBalance: (accountId: number) => Promise<{
availableBalance: string
@@ -38,10 +37,9 @@ export const useAccountStore = create<AccountStore>((set, get) => ({
const accounts = response.data.data.list || []
set({ accounts, loading: false })
// 设置默认账户为当前账户
const defaultAccount = accounts.find((a: Account) => a.isDefault)
if (defaultAccount) {
set({ currentAccount: defaultAccount })
// 如果没有当前账户,设置第一个账户为当前账户
if (accounts.length > 0 && !get().currentAccount) {
set({ currentAccount: accounts[0] })
}
} else {
set({ error: response.data.msg || '获取账户列表失败', loading: false })
@@ -103,22 +101,6 @@ export const useAccountStore = create<AccountStore>((set, get) => ({
}
},
setDefaultAccount: async (accountId) => {
set({ loading: true, error: null })
try {
const response = await apiService.accounts.setDefault({ accountId })
if (response.data.code === 0) {
await get().fetchAccounts()
} else {
set({ error: response.data.msg || '设置默认账户失败', loading: false })
throw new Error(response.data.msg || '设置默认账户失败')
}
} catch (error: any) {
set({ error: error.message || '设置默认账户失败', loading: false })
throw error
}
},
fetchAccountDetail: async (accountId) => {
set({ loading: true, error: null })
try {
+2
View File
@@ -13,8 +13,10 @@ export interface ApiResponse<T> {
export interface Account {
id: number
walletAddress: string
proxyAddress: string // Polymarket 代理钱包地址
accountName?: string
isDefault: boolean
isEnabled?: boolean // 是否启用
apiKeyConfigured: boolean
apiSecretConfigured: boolean
apiPassphraseConfigured: boolean