fix: 修复删除模型/交易所时界面卡死问题并增强依赖检查 (#578)

* fix: 修复删除模型/交易所时界面卡死问题并增强依赖检查
## 问题描述
1. 删除唯一的AI模型或交易所配置时,界面会卡死数秒
2. 删除后配置仍然显示在列表中
3. 可以删除被交易员使用的配置,导致数据不一致
## 修复内容
### 后端性能优化 (manager/trader_manager.go)
- 将循环内的重复数据库查询移到循环外
- 减少N次重复查询(GetAIModels + GetExchanges)为1次查询
- 大幅减少锁持有时间,从数秒降至毫秒级
### 前端显示修复 (web/src/components/AITradersPage.tsx)
- 过滤显示列表,只显示真正配置过的模型/交易所(有apiKey的)
- 删除后重新从后端获取最新数据,确保界面同步
### 前端依赖检查 (web/src/components/AITradersPage.tsx)
- 新增完整的依赖检查,包括停止状态的交易员
- 删除前检查是否有交易员使用该配置
- 显示使用该配置的交易员名称列表
- 阻止删除被使用的配置,保证数据一致性
### 多语言支持 (web/src/i18n/translations.ts)
- 添加依赖检查相关的中英文提示文本
- cannotDeleteModelInUse / cannotDeleteExchangeInUse
- tradersUsing / pleaseDeleteTradersFirst
## 测试建议
1. 创建交易员后尝试删除其使用的模型/交易所,应显示警告并阻止删除
2. 删除未使用的模型/交易所,应立即从列表消失且界面不卡死
3. 刷新页面后,已删除的配置不应再出现
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
* refactor: 重构删除配置函数减少重复代码
## 重构内容
- 创建通用的 handleDeleteConfig 函数
- 使用配置对象模式处理模型和交易所的删除逻辑
- 消除 handleDeleteModelConfig 和 handleDeleteExchangeConfig 之间的重复代码
## 重构效果
- 减少代码行数约 40%
- 提高代码可维护性和可读性
- 便于未来添加新的配置类型
## 功能保持不变
- 依赖检查逻辑完全相同
- 删除流程完全相同
- 用户体验完全相同
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
---------
Co-authored-by: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
Diego
2025-11-06 10:32:30 +08:00
committed by GitHub
parent 0d8b749a2c
commit 54744309dd
3 changed files with 172 additions and 70 deletions
+17 -14
View File
@@ -762,7 +762,21 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin
} }
} }
// 为每个交易员获取AI模型和交易所配置 // 🔧 性能优化:在循环外只查询一次AI模型和交易所配置
// 避免在循环中重复查询相同的数据,减少数据库压力和锁持有时间
aiModels, err := database.GetAIModels(userID)
if err != nil {
log.Printf("⚠️ 获取用户 %s 的AI模型配置失败: %v", userID, err)
return fmt.Errorf("获取AI模型配置失败: %w", err)
}
exchanges, err := database.GetExchanges(userID)
if err != nil {
log.Printf("⚠️ 获取用户 %s 的交易所配置失败: %v", userID, err)
return fmt.Errorf("获取交易所配置失败: %w", err)
}
// 为每个交易员加载配置
for _, traderCfg := range traders { for _, traderCfg := range traders {
// 检查是否已经加载过这个交易员 // 检查是否已经加载过这个交易员
if _, exists := tm.traders[traderCfg.ID]; exists { if _, exists := tm.traders[traderCfg.ID]; exists {
@@ -770,12 +784,7 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin
continue continue
} }
// 获取AI模型配置(使用该用户的配置) // 从已查询的列表中查找AI模型配置
aiModels, err := database.GetAIModels(userID)
if err != nil {
log.Printf("⚠️ 获取用户 %s 的AI模型配置失败: %v", userID, err)
continue
}
var aiModelCfg *config.AIModelConfig var aiModelCfg *config.AIModelConfig
// 优先精确匹配 model.ID(新版逻辑) // 优先精确匹配 model.ID(新版逻辑)
@@ -806,13 +815,7 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin
continue continue
} }
// 获取交易所配置(使用该用户的配置) // 从已查询的列表中查找交易所配置
exchanges, err := database.GetExchanges(userID)
if err != nil {
log.Printf("⚠️ 获取用户 %s 的交易所配置失败: %v", userID, err)
continue
}
var exchangeCfg *config.ExchangeConfig var exchangeCfg *config.ExchangeConfig
for _, exchange := range exchanges { for _, exchange := range exchanges {
if exchange.ID == traderCfg.ExchangeID { if exchange.ID == traderCfg.ExchangeID {
+144 -56
View File
@@ -131,9 +131,20 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
loadConfigs() loadConfigs()
}, [user, token]) }, [user, token])
// 显示所有用户的模型和交易所配置(用于调试 // 显示已配置的模型和交易所(有API Key的才算配置过
const configuredModels = allModels || [] const configuredModels = allModels?.filter((m) => m.apiKey && m.apiKey.trim() !== '') || []
const configuredExchanges = allExchanges || [] const configuredExchanges = allExchanges?.filter((e) => {
// Aster 交易所检查特殊字段
if (e.id === 'aster') {
return e.asterUser && e.asterUser.trim() !== ''
}
// Hyperliquid 只检查私钥
if (e.id === 'hyperliquid') {
return e.apiKey && e.apiKey.trim() !== ''
}
// 其他交易所检查 apiKey
return e.apiKey && e.apiKey.trim() !== ''
}) || []
// 只在创建交易员时使用已启用且配置完整的 // 只在创建交易员时使用已启用且配置完整的
const enabledModels = allModels?.filter((m) => m.enabled && m.apiKey) || [] const enabledModels = allModels?.filter((m) => m.enabled && m.apiKey) || []
@@ -167,19 +178,38 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
) )
}) || [] }) || []
// 检查模型是否正在被运行中的交易员使用 // 检查模型是否正在被运行中的交易员使用(用于UI禁用)
const isModelInUse = (modelId: string) => { const isModelInUse = (modelId: string) => {
return traders?.some((t) => t.ai_model === modelId && t.is_running) || false return traders?.some((t) => t.ai_model === modelId && t.is_running)
} }
// 检查交易所是否正在被运行中的交易员使用 // 检查交易所是否正在被运行中的交易员使用(用于UI禁用)
const isExchangeInUse = (exchangeId: string) => { const isExchangeInUse = (exchangeId: string) => {
return ( return (
traders?.some((t) => t.exchange_id === exchangeId && t.is_running) || traders?.some((t) => t.exchange_id === exchangeId && t.is_running)
false
) )
} }
// 检查模型是否被任何交易员使用(包括停止状态的)
const isModelUsedByAnyTrader = (modelId: string) => {
return traders?.some((t) => t.ai_model === modelId) || false
}
// 检查交易所是否被任何交易员使用(包括停止状态的)
const isExchangeUsedByAnyTrader = (exchangeId: string) => {
return traders?.some((t) => t.exchange_id === exchangeId) || false
}
// 获取使用特定模型的交易员列表
const getTradersUsingModel = (modelId: string) => {
return traders?.filter((t) => t.ai_model === modelId) || []
}
// 获取使用特定交易所的交易员列表
const getTradersUsingExchange = (exchangeId: string) => {
return traders?.filter((t) => t.exchange_id === exchangeId) || []
}
const handleCreateTrader = async (data: CreateTraderRequest) => { const handleCreateTrader = async (data: CreateTraderRequest) => {
try { try {
const model = allModels?.find((m) => m.id === data.ai_model_id) const model = allModels?.find((m) => m.id === data.ai_model_id)
@@ -298,27 +328,81 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
} }
} }
const handleDeleteModelConfig = async (modelId: string) => { // 通用删除配置处理函数
if (!confirm(t('confirmDeleteModel', language))) return const handleDeleteConfig = async <T extends { id: string }>(config: {
id: string
type: 'model' | 'exchange'
checkInUse: (id: string) => boolean
getUsingTraders: (id: string) => any[]
cannotDeleteKey: string
confirmDeleteKey: string
allItems: T[] | undefined
clearFields: (item: T) => T
buildRequest: (items: T[]) => any
updateApi: (request: any) => Promise<void>
refreshApi: () => Promise<T[]>
setItems: (items: T[]) => void
closeModal: () => void
errorKey: string
}) => {
// 检查是否有交易员正在使用
if (config.checkInUse(config.id)) {
const usingTraders = config.getUsingTraders(config.id)
const traderNames = usingTraders.map((t) => t.trader_name).join(', ')
alert(
t(config.cannotDeleteKey, language) +
'\n\n' +
t('tradersUsing', language) +
': ' +
traderNames +
'\n\n' +
t('pleaseDeleteTradersFirst', language)
)
return
}
if (!confirm(t(config.confirmDeleteKey, language))) return
try { try {
const updatedModels = const updatedItems =
allModels?.map((m) => config.allItems?.map((item) =>
m.id === modelId item.id === config.id ? config.clearFields(item) : item
? {
...m,
apiKey: '',
customApiUrl: '',
customModelName: '',
enabled: false,
}
: m
) || [] ) || []
const request = { const request = config.buildRequest(updatedItems)
await config.updateApi(request)
// 重新获取用户配置以确保数据同步
const refreshedItems = await config.refreshApi()
config.setItems(refreshedItems)
config.closeModal()
} catch (error) {
console.error(`Failed to delete ${config.type} config:`, error)
alert(t(config.errorKey, language))
}
}
const handleDeleteModelConfig = async (modelId: string) => {
await handleDeleteConfig({
id: modelId,
type: 'model',
checkInUse: isModelUsedByAnyTrader,
getUsingTraders: getTradersUsingModel,
cannotDeleteKey: 'cannotDeleteModelInUse',
confirmDeleteKey: 'confirmDeleteModel',
allItems: allModels,
clearFields: (m) => ({
...m,
apiKey: '',
customApiUrl: '',
customModelName: '',
enabled: false,
}),
buildRequest: (models) => ({
models: Object.fromEntries( models: Object.fromEntries(
updatedModels.map((model) => [ models.map((model) => [
model.provider, // 使用 provider 而不是 id model.provider,
{ {
enabled: model.enabled, enabled: model.enabled,
api_key: model.apiKey || '', api_key: model.apiKey || '',
@@ -327,16 +411,16 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}, },
]) ])
), ),
} }),
updateApi: api.updateModelConfigs,
await api.updateModelConfigs(request) refreshApi: api.getModelConfigs,
setAllModels(updatedModels) setItems: setAllModels,
setShowModelModal(false) closeModal: () => {
setEditingModel(null) setShowModelModal(false)
} catch (error) { setEditingModel(null)
console.error('Failed to delete model config:', error) },
alert(t('deleteConfigFailed', language)) errorKey: 'deleteConfigFailed',
} })
} }
const handleSaveModelConfig = async ( const handleSaveModelConfig = async (
@@ -413,19 +497,23 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
} }
const handleDeleteExchangeConfig = async (exchangeId: string) => { const handleDeleteExchangeConfig = async (exchangeId: string) => {
if (!confirm(t('confirmDeleteExchange', language))) return await handleDeleteConfig({
id: exchangeId,
try { type: 'exchange',
const updatedExchanges = checkInUse: isExchangeUsedByAnyTrader,
allExchanges?.map((e) => getUsingTraders: getTradersUsingExchange,
e.id === exchangeId cannotDeleteKey: 'cannotDeleteExchangeInUse',
? { ...e, apiKey: '', secretKey: '', enabled: false } confirmDeleteKey: 'confirmDeleteExchange',
: e allItems: allExchanges,
) || [] clearFields: (e) => ({
...e,
const request = { apiKey: '',
secretKey: '',
enabled: false,
}),
buildRequest: (exchanges) => ({
exchanges: Object.fromEntries( exchanges: Object.fromEntries(
updatedExchanges.map((exchange) => [ exchanges.map((exchange) => [
exchange.id, exchange.id,
{ {
enabled: exchange.enabled, enabled: exchange.enabled,
@@ -435,16 +523,16 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}, },
]) ])
), ),
} }),
updateApi: api.updateExchangeConfigs,
await api.updateExchangeConfigs(request) refreshApi: api.getExchangeConfigs,
setAllExchanges(updatedExchanges) setItems: setAllExchanges,
setShowExchangeModal(false) closeModal: () => {
setEditingExchange(null) setShowExchangeModal(false)
} catch (error) { setEditingExchange(null)
console.error('Failed to delete exchange config:', error) },
alert(t('deleteExchangeConfigFailed', language)) errorKey: 'deleteExchangeConfigFailed',
} })
} }
const handleSaveExchangeConfig = async ( const handleSaveExchangeConfig = async (
+11
View File
@@ -265,6 +265,11 @@ export const translations = {
addAIModel: 'Add AI Model', addAIModel: 'Add AI Model',
confirmDeleteModel: confirmDeleteModel:
'Are you sure you want to delete this AI model configuration?', 'Are you sure you want to delete this AI model configuration?',
cannotDeleteModelInUse:
'Cannot delete this AI model because it is being used by traders',
tradersUsing: 'Traders using this configuration',
pleaseDeleteTradersFirst:
'Please delete or reconfigure these traders first',
selectModel: 'Select AI Model', selectModel: 'Select AI Model',
pleaseSelectModel: 'Please select a model', pleaseSelectModel: 'Please select a model',
customBaseURL: 'Base URL (Optional)', customBaseURL: 'Base URL (Optional)',
@@ -281,6 +286,8 @@ export const translations = {
addExchange: 'Add Exchange', addExchange: 'Add Exchange',
confirmDeleteExchange: confirmDeleteExchange:
'Are you sure you want to delete this exchange configuration?', 'Are you sure you want to delete this exchange configuration?',
cannotDeleteExchangeInUse:
'Cannot delete this exchange because it is being used by traders',
pleaseSelectExchange: 'Please select an exchange', pleaseSelectExchange: 'Please select an exchange',
exchangeConfigWarning1: exchangeConfigWarning1:
'• API keys will be encrypted, recommend using read-only or futures trading permissions', '• API keys will be encrypted, recommend using read-only or futures trading permissions',
@@ -929,6 +936,9 @@ export const translations = {
editAIModel: '编辑AI模型', editAIModel: '编辑AI模型',
addAIModel: '添加AI模型', addAIModel: '添加AI模型',
confirmDeleteModel: '确定要删除此AI模型配置吗?', confirmDeleteModel: '确定要删除此AI模型配置吗?',
cannotDeleteModelInUse: '无法删除此AI模型,因为有交易员正在使用',
tradersUsing: '正在使用此配置的交易员',
pleaseDeleteTradersFirst: '请先删除或重新配置这些交易员',
selectModel: '选择AI模型', selectModel: '选择AI模型',
pleaseSelectModel: '请选择模型', pleaseSelectModel: '请选择模型',
customBaseURL: 'Base URL (可选)', customBaseURL: 'Base URL (可选)',
@@ -941,6 +951,7 @@ export const translations = {
editExchange: '编辑交易所', editExchange: '编辑交易所',
addExchange: '添加交易所', addExchange: '添加交易所',
confirmDeleteExchange: '确定要删除此交易所配置吗?', confirmDeleteExchange: '确定要删除此交易所配置吗?',
cannotDeleteExchangeInUse: '无法删除此交易所,因为有交易员正在使用',
pleaseSelectExchange: '请选择交易所', pleaseSelectExchange: '请选择交易所',
exchangeConfigWarning1: '• API密钥将被加密存储,建议使用只读或期货交易权限', exchangeConfigWarning1: '• API密钥将被加密存储,建议使用只读或期货交易权限',
exchangeConfigWarning2: '• 不要授予提现权限,确保资金安全', exchangeConfigWarning2: '• 不要授予提现权限,确保资金安全',