From c34a6c6bcf1d53c28ee9365a39d4e6878baabd71 Mon Sep 17 00:00:00 2001 From: Professor-Chen <36615378+Professor-Chen@users.noreply.github.com> Date: Sun, 30 Nov 2025 12:22:20 +0800 Subject: [PATCH] fix: resolve multiple bugs preventing trader creation (#1138) * fix: resolve multiple bugs preventing trader creation Bug fixes: 1. Fix time.Time scanning error - SQLite stores datetime as TEXT, now parsing manually 2. Fix foreign key mismatch - traders table referenced exchanges(id) but exchanges uses composite primary key (id, user_id) 3. Add missing backtestManager field to Server struct 4. Add missing Shutdown method to Server struct 5. Fix NewFuturesTrader call - pass userId parameter 6. Fix UpdateExchange call - pass all required parameters 7. Add migrateTradersTable() to fix existing databases These issues prevented creating new traders with 500 errors. * fix(api): fix balance extraction field name mismatch Binance API returns 'availableBalance' (camelCase) but code was looking for 'available_balance' (snake_case). Now supports both formats. Also added 'totalWalletBalance' as fallback for total balance extraction. * fix(frontend): add missing ConfirmDialogProvider to App The delete trader button required ConfirmDialogProvider to be wrapped around the App component for the confirmation dialog to work. --------- Co-authored-by: NOFX Trader --- api/server.go | 515 ++++++++++++++++++++++----------------------- config/database.go | 169 +++++++++++++-- web/src/App.tsx | 5 +- 3 files changed, 407 insertions(+), 282 deletions(-) diff --git a/api/server.go b/api/server.go index e2e90b7a..465d79ed 100644 --- a/api/server.go +++ b/api/server.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "log" - "math" "net" "net/http" "nofx/auth" @@ -13,7 +12,6 @@ import ( "nofx/config" "nofx/crypto" "nofx/decision" - "nofx/hook" "nofx/manager" "nofx/trader" "strconv" @@ -27,22 +25,15 @@ import ( // Server HTTP API服务器 type Server struct { router *gin.Engine - httpServer *http.Server traderManager *manager.TraderManager database *config.Database cryptoHandler *CryptoHandler backtestManager *backtest.Manager + httpServer *http.Server port int } - // NewServer 创建API服务器 -func NewServer( - traderManager *manager.TraderManager, - database *config.Database, - cryptoService *crypto.CryptoService, - backtestManager *backtest.Manager, - port int, -) *Server { +func NewServer(traderManager *manager.TraderManager, database *config.Database, cryptoService *crypto.CryptoService, backtestManager *backtest.Manager, port int) *Server { // 设置为Release模式(减少日志输出) gin.SetMode(gin.ReleaseMode) @@ -62,9 +53,6 @@ func NewServer( backtestManager: backtestManager, port: port, } - if s.backtestManager != nil { - s.backtestManager.SetAIResolver(s.hydrateBacktestAIConfig) - } // 设置路由 s.setupRoutes() @@ -130,11 +118,6 @@ func (s *Server) setupRoutes() { // 需要认证的路由 protected := api.Group("/", s.authMiddleware()) { - if s.backtestManager != nil { - backtestGroup := protected.Group("/backtest") - s.registerBacktestRoutes(backtestGroup) - } - // 注销(加入黑名单) protected.POST("/logout", s.handleLogout) @@ -150,6 +133,7 @@ func (s *Server) setupRoutes() { protected.POST("/traders/:id/start", s.handleStartTrader) protected.POST("/traders/:id/stop", s.handleStopTrader) protected.PUT("/traders/:id/prompt", s.handleUpdateTraderPrompt) + protected.POST("/traders/:id/sync-balance", s.handleSyncBalance) // AI模型配置 protected.GET("/models", s.handleGetModelConfigs) @@ -171,7 +155,6 @@ func (s *Server) setupRoutes() { protected.GET("/decisions/latest", s.handleLatestDecisions) protected.GET("/statistics", s.handleStatistics) protected.GET("/performance", s.handlePerformance) - protected.GET("/competition/full", s.handleCompetition) } } } @@ -215,34 +198,16 @@ func (s *Server) handleGetSystemConfig(c *gin.Context) { betaModeStr, _ := s.database.GetSystemConfig("beta_mode") betaMode := betaModeStr == "true" - regEnabledStr, err := s.database.GetSystemConfig("registration_enabled") - registrationEnabled := true - if err == nil { - registrationEnabled = strings.ToLower(regEnabledStr) != "false" - } - c.JSON(http.StatusOK, gin.H{ - "beta_mode": betaMode, - "default_coins": defaultCoins, - "btc_eth_leverage": btcEthLeverage, - "altcoin_leverage": altcoinLeverage, - "registration_enabled": registrationEnabled, + "beta_mode": betaMode, + "default_coins": defaultCoins, + "btc_eth_leverage": btcEthLeverage, + "altcoin_leverage": altcoinLeverage, }) } // handleGetServerIP 获取服务器IP地址(用于白名单配置) func (s *Server) handleGetServerIP(c *gin.Context) { - - // 首先尝试从Hook获取用户专用IP - userIP := hook.HookExec[hook.IpResult](hook.GETIP, c.GetString("user_id")) - if userIP != nil && userIP.Error() == nil { - c.JSON(http.StatusOK, gin.H{ - "public_ip": userIP.GetResult(), - "message": "请将此IP地址添加到白名单中", - }) - return - } - // 尝试通过第三方API获取公网IP publicIP := getPublicIPFromAPI() @@ -431,8 +396,8 @@ type SafeModelConfig struct { Name string `json:"name"` Provider string `json:"provider"` Enabled bool `json:"enabled"` - CustomAPIURL string `json:"customApiUrl"` // 自定义API URL(通常不敏感) - CustomModelName string `json:"customModelName"` // 自定义模型名(不敏感) + CustomAPIURL string `json:"customApiUrl"` // 自定义API URL(通常不敏感) + CustomModelName string `json:"customModelName"` // 自定义模型名(不敏感) } type ExchangeConfig struct { @@ -453,8 +418,8 @@ type SafeExchangeConfig struct { Enabled bool `json:"enabled"` Testnet bool `json:"testnet,omitempty"` HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"` // Hyperliquid钱包地址(不敏感) - AsterUser string `json:"asterUser"` // Aster用户名(不敏感) - AsterSigner string `json:"asterSigner"` // Aster签名者(不敏感) + AsterUser string `json:"asterUser"` // Aster用户名(不敏感) + AsterSigner string `json:"asterSigner"` // Aster签名者(不敏感) } type UpdateModelConfigRequest struct { @@ -512,9 +477,8 @@ func (s *Server) handleCreateTrader(c *gin.Context) { } } - // 生成交易员ID (使用 UUID 确保唯一性,解决 Issue #893) - // 保留前缀以便调试和日志追踪 - traderID := fmt.Sprintf("%s_%s_%s", req.ExchangeID, req.AIModelID, uuid.New().String()) + // 生成交易员ID + traderID := fmt.Sprintf("%s_%s_%d", req.ExchangeID, req.AIModelID, time.Now().Unix()) // 设置默认值 isCrossMargin := true // 默认为全仓模式 @@ -610,42 +574,32 @@ func (s *Server) handleCreateTrader(c *gin.Context) { if balanceErr != nil { log.Printf("⚠️ 查询交易所余额失败,使用用户输入的初始资金: %v", balanceErr) } else { - // 🔧 计算Total Equity = Wallet Balance + Unrealized Profit - // 这是账户的真实净值,用作Initial Balance的基准 - var totalWalletBalance float64 - var totalUnrealizedProfit float64 - - // 提取钱包余额 - if wb, ok := balanceInfo["totalWalletBalance"].(float64); ok { - totalWalletBalance = wb - } else if wb, ok := balanceInfo["wallet_balance"].(float64); ok { - totalWalletBalance = wb - } else if wb, ok := balanceInfo["balance"].(float64); ok { - totalWalletBalance = wb - } - - // 提取未实现盈亏 - if up, ok := balanceInfo["totalUnrealizedProfit"].(float64); ok { - totalUnrealizedProfit = up - } else if up, ok := balanceInfo["unrealized_profit"].(float64); ok { - totalUnrealizedProfit = up - } - - // 计算总净值 - totalEquity := totalWalletBalance + totalUnrealizedProfit - - if totalEquity > 0 { - actualBalance = totalEquity - log.Printf("✅ 查询到交易所实际净值: %.2f USDT (钱包: %.2f + 未实现: %.2f, 用户输入: %.2f)", - actualBalance, totalWalletBalance, totalUnrealizedProfit, req.InitialBalance) + // 提取可用余额 - 支持多种字段名格式 + if availableBalance, ok := balanceInfo["availableBalance"].(float64); ok && availableBalance > 0 { + // Binance 格式: availableBalance (camelCase) + actualBalance = availableBalance + log.Printf("✓ 查询到交易所实际余额: %.2f USDT (用户输入: %.2f USDT)", actualBalance, req.InitialBalance) + } else if availableBalance, ok := balanceInfo["available_balance"].(float64); ok && availableBalance > 0 { + // 其他格式: available_balance (snake_case) + actualBalance = availableBalance + log.Printf("✓ 查询到交易所实际余额: %.2f USDT (用户输入: %.2f USDT)", actualBalance, req.InitialBalance) + } else if totalBalance, ok := balanceInfo["totalWalletBalance"].(float64); ok && totalBalance > 0 { + // Binance 格式: totalWalletBalance (camelCase) + actualBalance = totalBalance + log.Printf("✓ 查询到交易所总余额: %.2f USDT (用户输入: %.2f USDT)", actualBalance, req.InitialBalance) + } else if totalBalance, ok := balanceInfo["balance"].(float64); ok && totalBalance > 0 { + // 其他格式: balance + actualBalance = totalBalance + log.Printf("✓ 查询到交易所实际余额: %.2f USDT (用户输入: %.2f USDT)", actualBalance, req.InitialBalance) } else { - log.Printf("⚠️ 无法从余额信息中计算净值,使用用户输入的初始资金") + log.Printf("⚠️ 无法从余额信息中提取可用余额,balanceInfo=%v,使用用户输入的初始资金", balanceInfo) } } } } // 创建交易员配置(数据库实体) + log.Printf("🔧 DEBUG: 开始创建交易员配置, ID=%s, Name=%s, AIModel=%s, Exchange=%s", traderID, req.Name, req.AIModelID, req.ExchangeID) trader := &config.TraderRecord{ ID: traderID, UserID: userID, @@ -667,18 +621,23 @@ func (s *Server) handleCreateTrader(c *gin.Context) { } // 保存到数据库 + log.Printf("🔧 DEBUG: 准备调用 CreateTrader") err = s.database.CreateTrader(trader) if err != nil { + log.Printf("❌ 创建交易员失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("创建交易员失败: %v", err)}) return } + log.Printf("🔧 DEBUG: CreateTrader 成功") // 立即将新交易员加载到TraderManager中 - err = s.traderManager.LoadTraderByID(s.database, userID, traderID) + log.Printf("🔧 DEBUG: 准备调用 LoadUserTraders") + err = s.traderManager.LoadUserTraders(s.database, userID) if err != nil { - log.Printf("⚠️ 加载交易员到内存失败: %v", err) + log.Printf("⚠️ 加载用户交易员到内存失败: %v", err) // 这里不返回错误,因为交易员已经成功创建到数据库 } + log.Printf("🔧 DEBUG: LoadUserTraders 完成") log.Printf("✓ 创建交易员成功: %s (模型: %s, 交易所: %s)", req.Name, req.AIModelID, req.ExchangeID) @@ -692,18 +651,17 @@ func (s *Server) handleCreateTrader(c *gin.Context) { // UpdateTraderRequest 更新交易员请求 type UpdateTraderRequest struct { - Name string `json:"name" binding:"required"` - AIModelID string `json:"ai_model_id" binding:"required"` - ExchangeID string `json:"exchange_id" binding:"required"` - InitialBalance float64 `json:"initial_balance"` - ScanIntervalMinutes int `json:"scan_interval_minutes"` - BTCETHLeverage int `json:"btc_eth_leverage"` - AltcoinLeverage int `json:"altcoin_leverage"` - TradingSymbols string `json:"trading_symbols"` - CustomPrompt string `json:"custom_prompt"` - OverrideBasePrompt bool `json:"override_base_prompt"` - SystemPromptTemplate string `json:"system_prompt_template"` - IsCrossMargin *bool `json:"is_cross_margin"` + Name string `json:"name" binding:"required"` + AIModelID string `json:"ai_model_id" binding:"required"` + ExchangeID string `json:"exchange_id" binding:"required"` + InitialBalance float64 `json:"initial_balance"` + ScanIntervalMinutes int `json:"scan_interval_minutes"` + BTCETHLeverage int `json:"btc_eth_leverage"` + AltcoinLeverage int `json:"altcoin_leverage"` + TradingSymbols string `json:"trading_symbols"` + CustomPrompt string `json:"custom_prompt"` + OverrideBasePrompt bool `json:"override_base_prompt"` + IsCrossMargin *bool `json:"is_cross_margin"` } // handleUpdateTrader 更新交易员配置 @@ -761,12 +719,6 @@ func (s *Server) handleUpdateTrader(c *gin.Context) { scanIntervalMinutes = 3 } - // 设置提示词模板,允许更新 - systemPromptTemplate := req.SystemPromptTemplate - if systemPromptTemplate == "" { - systemPromptTemplate = existingTrader.SystemPromptTemplate // 如果请求中没有提供,保持原值 - } - // 更新交易员配置 trader := &config.TraderRecord{ ID: traderID, @@ -780,7 +732,7 @@ func (s *Server) handleUpdateTrader(c *gin.Context) { TradingSymbols: req.TradingSymbols, CustomPrompt: req.CustomPrompt, OverrideBasePrompt: req.OverrideBasePrompt, - SystemPromptTemplate: systemPromptTemplate, + SystemPromptTemplate: existingTrader.SystemPromptTemplate, // 保持原值 IsCrossMargin: isCrossMargin, ScanIntervalMinutes: scanIntervalMinutes, IsRunning: existingTrader.IsRunning, // 保持原值 @@ -793,25 +745,10 @@ func (s *Server) handleUpdateTrader(c *gin.Context) { return } - // 如果请求中包含initial_balance且与现有值不同,单独更新它 - // UpdateTrader不会更新initial_balance,需要使用专门的方法 - if req.InitialBalance > 0 && math.Abs(req.InitialBalance-existingTrader.InitialBalance) > 0.1 { - err = s.database.UpdateTraderInitialBalance(userID, traderID, req.InitialBalance) - if err != nil { - log.Printf("⚠️ 更新初始余额失败: %v", err) - // 不返回错误,因为主要配置已更新成功 - } else { - log.Printf("✓ 初始余额已更新: %.2f -> %.2f", existingTrader.InitialBalance, req.InitialBalance) - } - } - - // 🔄 从内存中移除旧的trader实例,以便重新加载最新配置 - s.traderManager.RemoveTrader(traderID) - // 重新加载交易员到内存 - err = s.traderManager.LoadTraderByID(s.database, userID, traderID) + err = s.traderManager.LoadUserTraders(s.database, userID) if err != nil { - log.Printf("⚠️ 重新加载交易员到内存失败: %v", err) + log.Printf("⚠️ 重新加载用户交易员到内存失败: %v", err) } log.Printf("✓ 更新交易员成功: %s (模型: %s, 交易所: %s)", req.Name, req.AIModelID, req.ExchangeID) @@ -855,15 +792,12 @@ func (s *Server) handleStartTrader(c *gin.Context) { traderID := c.Param("id") // 校验交易员是否属于当前用户 - traderRecord, _, _, err := s.database.GetTraderConfig(userID, traderID) + _, _, _, err := s.database.GetTraderConfig(userID, traderID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在或无访问权限"}) return } - // 获取模板名称 - templateName := traderRecord.SystemPromptTemplate - trader, err := s.traderManager.GetTrader(traderID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) @@ -877,9 +811,6 @@ func (s *Server) handleStartTrader(c *gin.Context) { return } - // 重新加载系统提示词模板(确保使用最新的硬盘文件) - s.reloadPromptTemplatesWithLog(templateName) - // 启动交易员 go func() { log.Printf("▶️ 启动交易员 %s (%s)", traderID, trader.GetName()) @@ -969,6 +900,113 @@ func (s *Server) handleUpdateTraderPrompt(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "自定义prompt已更新"}) } +// handleSyncBalance 同步交易所余额到initial_balance(选项B:手动同步 + 选项C:智能检测) +func (s *Server) handleSyncBalance(c *gin.Context) { + userID := c.GetString("user_id") + traderID := c.Param("id") + + log.Printf("🔄 用户 %s 请求同步交易员 %s 的余额", userID, traderID) + + // 从数据库获取交易员配置(包含交易所信息) + traderConfig, _, exchangeCfg, err := s.database.GetTraderConfig(userID, traderID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) + return + } + + if exchangeCfg == nil || !exchangeCfg.Enabled { + c.JSON(http.StatusBadRequest, gin.H{"error": "交易所未配置或未启用"}) + return + } + + // 创建临时 trader 查询余额 + var tempTrader trader.Trader + var createErr error + + switch traderConfig.ExchangeID { + case "binance": + tempTrader = trader.NewFuturesTrader(exchangeCfg.APIKey, exchangeCfg.SecretKey, userID) + case "hyperliquid": + tempTrader, createErr = trader.NewHyperliquidTrader( + exchangeCfg.APIKey, + exchangeCfg.HyperliquidWalletAddr, + exchangeCfg.Testnet, + ) + case "aster": + tempTrader, createErr = trader.NewAsterTrader( + exchangeCfg.AsterUser, + exchangeCfg.AsterSigner, + exchangeCfg.AsterPrivateKey, + ) + default: + c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的交易所类型"}) + return + } + + if createErr != nil { + log.Printf("⚠️ 创建临时 trader 失败: %v", createErr) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("连接交易所失败: %v", createErr)}) + return + } + + // 查询实际余额 + balanceInfo, balanceErr := tempTrader.GetBalance() + if balanceErr != nil { + log.Printf("⚠️ 查询交易所余额失败: %v", balanceErr) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("查询余额失败: %v", balanceErr)}) + return + } + + // 提取可用余额 + var actualBalance float64 + if availableBalance, ok := balanceInfo["available_balance"].(float64); ok && availableBalance > 0 { + actualBalance = availableBalance + } else if availableBalance, ok := balanceInfo["availableBalance"].(float64); ok && availableBalance > 0 { + actualBalance = availableBalance + } else if totalBalance, ok := balanceInfo["balance"].(float64); ok && totalBalance > 0 { + actualBalance = totalBalance + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": "无法获取可用余额"}) + return + } + + oldBalance := traderConfig.InitialBalance + + // ✅ 选项C:智能检测余额变化 + changePercent := ((actualBalance - oldBalance) / oldBalance) * 100 + changeType := "增加" + if changePercent < 0 { + changeType = "减少" + } + + log.Printf("✓ 查询到交易所实际余额: %.2f USDT (当前配置: %.2f USDT, 变化: %.2f%%)", + actualBalance, oldBalance, changePercent) + + // 更新数据库中的 initial_balance + err = s.database.UpdateTraderInitialBalance(userID, traderID, actualBalance) + if err != nil { + log.Printf("❌ 更新initial_balance失败: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "更新余额失败"}) + return + } + + // 重新加载交易员到内存 + err = s.traderManager.LoadUserTraders(s.database, userID) + if err != nil { + log.Printf("⚠️ 重新加载用户交易员到内存失败: %v", err) + } + + log.Printf("✅ 已同步余额: %.2f → %.2f USDT (%s %.2f%%)", oldBalance, actualBalance, changeType, changePercent) + + c.JSON(http.StatusOK, gin.H{ + "message": "余额同步成功", + "old_balance": oldBalance, + "new_balance": actualBalance, + "change_percent": changePercent, + "change_type": changeType, + }) +} + // handleGetModelConfigs 获取AI模型配置 func (s *Server) handleGetModelConfigs(c *gin.Context) { userID := c.GetString("user_id") @@ -1060,7 +1098,7 @@ func (s *Server) handleUpdateModelConfigs(c *gin.Context) { // 这里不返回错误,因为模型配置已经成功更新到数据库 } - log.Printf("✓ AI模型配置已更新: %+v", SanitizeModelConfigForLog(req.Models)) + log.Printf("✓ AI模型配置已更新: %+v", req.Models) c.JSON(http.StatusOK, gin.H{"message": "模型配置已更新"}) } @@ -1075,6 +1113,23 @@ func (s *Server) handleGetExchangeConfigs(c *gin.Context) { return } log.Printf("✅ 找到 %d 个交易所配置", len(exchanges)) + + // 调试:输出配置详情(脱敏) + for _, ex := range exchanges { + apiKeyMasked := "" + if len(ex.APIKey) > 8 { + apiKeyMasked = ex.APIKey[:8] + "..." + } + secretKeyMasked := "" + if len(ex.SecretKey) > 8 { + secretKeyMasked = ex.SecretKey[:8] + "..." + } + log.Printf(" └─ 交易所: %s, APIKey: %s, SecretKey: %s", ex.ID, apiKeyMasked, secretKeyMasked) + } + + // 打印完整JSON响应用于调试 + jsonData, _ := json.Marshal(exchanges) + log.Printf("📤 完整JSON响应: %s", string(jsonData)) // 转换为安全的响应结构,移除敏感信息 safeExchanges := make([]SafeExchangeConfig, len(exchanges)) @@ -1157,7 +1212,7 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) { // 这里不返回错误,因为交易所配置已经成功更新到数据库 } - log.Printf("✓ 交易所配置已更新: %+v", SanitizeExchangeConfigForLog(req.Exchanges)) + log.Printf("✓ 交易所配置已更新: %+v", req.Exchanges) c.JSON(http.StatusOK, gin.H{"message": "交易所配置已更新"}) } @@ -1226,13 +1281,12 @@ func (s *Server) handleTraderList(c *gin.Context) { // 返回完整的 AIModelID(如 "admin_deepseek"),不要截断 // 前端需要完整 ID 来验证模型是否存在(与 handleGetTraderConfig 保持一致) result = append(result, map[string]interface{}{ - "trader_id": trader.ID, - "trader_name": trader.Name, - "ai_model": trader.AIModelID, // 使用完整 ID - "exchange_id": trader.ExchangeID, - "is_running": isRunning, - "initial_balance": trader.InitialBalance, - "system_prompt_template": trader.SystemPromptTemplate, + "trader_id": trader.ID, + "trader_name": trader.Name, + "ai_model": trader.AIModelID, // 使用完整 ID + "exchange_id": trader.ExchangeID, + "is_running": isRunning, + "initial_balance": trader.InitialBalance, }) } @@ -1268,22 +1322,21 @@ func (s *Server) handleGetTraderConfig(c *gin.Context) { aiModelID := traderConfig.AIModelID result := map[string]interface{}{ - "trader_id": traderConfig.ID, - "trader_name": traderConfig.Name, - "ai_model": aiModelID, - "exchange_id": traderConfig.ExchangeID, - "initial_balance": traderConfig.InitialBalance, - "scan_interval_minutes": traderConfig.ScanIntervalMinutes, - "btc_eth_leverage": traderConfig.BTCETHLeverage, - "altcoin_leverage": traderConfig.AltcoinLeverage, - "trading_symbols": traderConfig.TradingSymbols, - "custom_prompt": traderConfig.CustomPrompt, - "override_base_prompt": traderConfig.OverrideBasePrompt, - "system_prompt_template": traderConfig.SystemPromptTemplate, - "is_cross_margin": traderConfig.IsCrossMargin, - "use_coin_pool": traderConfig.UseCoinPool, - "use_oi_top": traderConfig.UseOITop, - "is_running": isRunning, + "trader_id": traderConfig.ID, + "trader_name": traderConfig.Name, + "ai_model": aiModelID, + "exchange_id": traderConfig.ExchangeID, + "initial_balance": traderConfig.InitialBalance, + "scan_interval_minutes": traderConfig.ScanIntervalMinutes, + "btc_eth_leverage": traderConfig.BTCETHLeverage, + "altcoin_leverage": traderConfig.AltcoinLeverage, + "trading_symbols": traderConfig.TradingSymbols, + "custom_prompt": traderConfig.CustomPrompt, + "override_base_prompt": traderConfig.OverrideBasePrompt, + "is_cross_margin": traderConfig.IsCrossMargin, + "use_coin_pool": traderConfig.UseCoinPool, + "use_oi_top": traderConfig.UseOITop, + "is_running": isRunning, } c.JSON(http.StatusOK, result) @@ -1405,15 +1458,7 @@ func (s *Server) handleLatestDecisions(c *gin.Context) { return } - // 从 query 参数读取 limit,默认 5,最大 50 - limit := 5 - if limitStr := c.Query("limit"); limitStr != "" { - if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 50 { - limit = l - } - } - - records, err := trader.GetDecisionLogger().GetLatestRecords(limit) + records, err := trader.GetDecisionLogger().GetLatestRecords(5) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": fmt.Sprintf("获取决策日志失败: %v", err), @@ -1512,16 +1557,22 @@ func (s *Server) handleEquityHistory(c *gin.Context) { CycleNumber int `json:"cycle_number"` } - // 从AutoTrader获取当前初始余额(用作旧数据的fallback) - base := 0.0 + // 从AutoTrader获取初始余额(用于计算盈亏百分比) + initialBalance := 0.0 if status := trader.GetStatus(); status != nil { if ib, ok := status["initial_balance"].(float64); ok && ib > 0 { - base = ib + initialBalance = ib } } + // 如果无法从status获取,且有历史记录,则从第一条记录获取 + if initialBalance == 0 && len(records) > 0 { + // 第一条记录的equity作为初始余额 + initialBalance = records[0].AccountState.TotalBalance + } + // 如果还是无法获取,返回错误 - if base == 0 { + if initialBalance == 0 { c.JSON(http.StatusInternalServerError, gin.H{ "error": "无法获取初始余额", }) @@ -1531,24 +1582,14 @@ func (s *Server) handleEquityHistory(c *gin.Context) { var history []EquityPoint for _, record := range records { // TotalBalance字段实际存储的是TotalEquity - // totalEquity := record.AccountState.TotalBalance + totalEquity := record.AccountState.TotalBalance // TotalUnrealizedProfit字段实际存储的是TotalPnL(相对初始余额) - // totalPnL := record.AccountState.TotalUnrealizedProfit - walletBalance := record.AccountState.TotalBalance - unrealizedPnL := record.AccountState.TotalUnrealizedProfit - totalEquity := walletBalance + unrealizedPnL + totalPnL := record.AccountState.TotalUnrealizedProfit - // 🔄 使用历史记录中保存的initial_balance(如果有) - // 这样可以保持历史PNL%的准确性,即使用户后来更新了initial_balance - if record.AccountState.InitialBalance > 0 { - base = record.AccountState.InitialBalance - } - - totalPnL := totalEquity - base // 计算盈亏百分比 totalPnLPct := 0.0 - if base > 0 { - totalPnLPct = (totalPnL / base) * 100 + if initialBalance > 0 { + totalPnLPct = (totalPnL / initialBalance) * 100 } history = append(history, EquityPoint{ @@ -1635,6 +1676,7 @@ func (s *Server) authMiddleware() gin.HandlerFunc { } } + // handleLogout 将当前token加入黑名单 func (s *Server) handleLogout(c *gin.Context) { authHeader := c.GetHeader("Authorization") @@ -1665,14 +1707,6 @@ func (s *Server) handleLogout(c *gin.Context) { // handleRegister 处理用户注册请求 func (s *Server) handleRegister(c *gin.Context) { - regEnabled := true - if regStr, err := s.database.GetSystemConfig("registration_enabled"); err == nil { - regEnabled = strings.ToLower(regStr) != "false" - } - if !regEnabled { - c.JSON(http.StatusForbidden, gin.H{"error": "注册已关闭"}) - return - } var req struct { Email string `json:"email" binding:"required,email"` @@ -1707,21 +1741,8 @@ func (s *Server) handleRegister(c *gin.Context) { } // 检查邮箱是否已存在 - existingUser, err := s.database.GetUserByEmail(req.Email) + _, err := s.database.GetUserByEmail(req.Email) if err == nil { - // 如果用户未完成OTP验证,允许重新获取OTP(支持中断后恢复注册) - if !existingUser.OTPVerified { - qrCodeURL := auth.GetOTPQRCodeURL(existingUser.OTPSecret, req.Email) - c.JSON(http.StatusOK, gin.H{ - "user_id": existingUser.ID, - "email": req.Email, - "otp_secret": existingUser.OTPSecret, - "qr_code_url": qrCodeURL, - "message": "检测到未完成的注册,请继续完成OTP设置", - }) - return - } - // 用户已完成验证,拒绝重复注册 c.JSON(http.StatusConflict, gin.H{"error": "邮箱已被注册"}) return } @@ -2014,63 +2035,44 @@ func (s *Server) Start() error { addr := fmt.Sprintf(":%d", s.port) log.Printf("🌐 API服务器启动在 http://localhost%s", addr) log.Printf("📊 API文档:") - log.Printf(" • GET /api/health - 健康检查") - log.Printf(" • 公共竞赛/排行榜相关接口") - log.Printf(" - GET /api/traders - 公开的AI交易员排行榜(无需认证)") - log.Printf(" - GET /api/competition - 公开竞赛数据(无需认证)") - log.Printf(" - GET /api/top-traders - 前5名交易员(无需认证)") - log.Printf(" - GET /api/equity-history - 指定trader收益率历史(无需认证)") - log.Printf(" - POST /api/equity-history-batch - 批量获取收益率历史(无需认证)") - log.Printf(" - GET /api/traders/:id/public-config - 公开交易员配置(无需认证)") - log.Printf(" • Backtest") - log.Printf(" - GET /api/backtest/runs - 回测运行列表") - log.Printf(" - POST /api/backtest/start - 启动新的回测") - log.Printf(" - POST /api/backtest/pause - 暂停指定回测") - log.Printf(" - POST /api/backtest/resume - 恢复指定回测") - log.Printf(" - POST /api/backtest/stop - 停止指定回测") - log.Printf(" - GET /api/backtest/status - 查询回测状态") - log.Printf(" - GET /api/backtest/equity - 回测净值曲线") - log.Printf(" - GET /api/backtest/trades - 回测交易记录") - log.Printf(" - GET /api/backtest/metrics - 回测统计指标") - log.Printf(" - GET /api/backtest/trace - 回测AI Trace") - log.Printf(" - GET /api/backtest/export - 导出回测数据ZIP") - log.Printf(" • Trader / 配置(需认证)") - log.Printf(" - POST /api/traders - 创建AI交易员") - log.Printf(" - DELETE /api/traders/:id - 删除AI交易员") - log.Printf(" - POST /api/traders/:id/start - 启动AI交易员") - log.Printf(" - POST /api/traders/:id/stop - 停止AI交易员") - log.Printf(" - GET /api/models - 获取AI模型配置") - log.Printf(" - PUT /api/models - 更新AI模型配置") - log.Printf(" - GET /api/exchanges - 获取交易所配置") - log.Printf(" - PUT /api/exchanges - 更新交易所配置") - log.Printf(" - GET /api/status?trader_id=xxx - 指定trader的系统状态") - log.Printf(" - GET /api/account?trader_id=xxx - 指定trader的账户信息") - log.Printf(" - GET /api/positions?trader_id=xxx - 指定trader的持仓列表") - log.Printf(" - GET /api/decisions?trader_id=xxx - 指定trader的决策日志") - log.Printf(" - GET /api/decisions/latest?trader_id=xxx - 指定trader的最新决策") - log.Printf(" - GET /api/statistics?trader_id=xxx - 指定trader的统计信息") - log.Printf(" - GET /api/performance?trader_id=xxx - AI学习表现分析") + log.Printf(" • GET /api/health - 健康检查") + log.Printf(" • GET /api/traders - 公开的AI交易员排行榜前50名(无需认证)") + log.Printf(" • GET /api/competition - 公开的竞赛数据(无需认证)") + log.Printf(" • GET /api/top-traders - 前5名交易员数据(无需认证,表现对比用)") + log.Printf(" • GET /api/equity-history?trader_id=xxx - 公开的收益率历史数据(无需认证,竞赛用)") + log.Printf(" • GET /api/equity-history-batch?trader_ids=a,b,c - 批量获取历史数据(无需认证,表现对比优化)") + log.Printf(" • GET /api/traders/:id/public-config - 公开的交易员配置(无需认证,不含敏感信息)") + log.Printf(" • POST /api/traders - 创建新的AI交易员") + log.Printf(" • DELETE /api/traders/:id - 删除AI交易员") + log.Printf(" • POST /api/traders/:id/start - 启动AI交易员") + log.Printf(" • POST /api/traders/:id/stop - 停止AI交易员") + log.Printf(" • GET /api/models - 获取AI模型配置") + log.Printf(" • PUT /api/models - 更新AI模型配置") + log.Printf(" • GET /api/exchanges - 获取交易所配置") + log.Printf(" • PUT /api/exchanges - 更新交易所配置") + log.Printf(" • GET /api/status?trader_id=xxx - 指定trader的系统状态") + log.Printf(" • GET /api/account?trader_id=xxx - 指定trader的账户信息") + log.Printf(" • GET /api/positions?trader_id=xxx - 指定trader的持仓列表") + log.Printf(" • GET /api/decisions?trader_id=xxx - 指定trader的决策日志") + log.Printf(" • GET /api/decisions/latest?trader_id=xxx - 指定trader的最新决策") + log.Printf(" • GET /api/statistics?trader_id=xxx - 指定trader的统计信息") + log.Printf(" • GET /api/performance?trader_id=xxx - 指定trader的AI学习表现分析") log.Println() - // 创建 http.Server 以支持 graceful shutdown s.httpServer = &http.Server{ Addr: addr, Handler: s.router, } - return s.httpServer.ListenAndServe() } -// Shutdown 优雅关闭 API 服务器 +// Shutdown 优雅关闭服务器 func (s *Server) Shutdown() error { if s.httpServer == nil { return nil } - - // 设置 5 秒超时 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - return s.httpServer.Shutdown(ctx) } @@ -2138,17 +2140,16 @@ func (s *Server) handlePublicTraderList(c *gin.Context) { result := make([]map[string]interface{}, 0, len(traders)) for _, trader := range traders { result = append(result, map[string]interface{}{ - "trader_id": trader["trader_id"], - "trader_name": trader["trader_name"], - "ai_model": trader["ai_model"], - "exchange": trader["exchange"], - "is_running": trader["is_running"], - "total_equity": trader["total_equity"], - "total_pnl": trader["total_pnl"], - "total_pnl_pct": trader["total_pnl_pct"], - "position_count": trader["position_count"], - "margin_used_pct": trader["margin_used_pct"], - "system_prompt_template": trader["system_prompt_template"], + "trader_id": trader["trader_id"], + "trader_name": trader["trader_name"], + "ai_model": trader["ai_model"], + "exchange": trader["exchange"], + "is_running": trader["is_running"], + "total_equity": trader["total_equity"], + "total_pnl": trader["total_pnl"], + "total_pnl_pct": trader["total_pnl_pct"], + "position_count": trader["position_count"], + "margin_used_pct": trader["margin_used_pct"], }) } @@ -2316,17 +2317,3 @@ func (s *Server) handleGetPublicTraderConfig(c *gin.Context) { c.JSON(http.StatusOK, result) } - -// reloadPromptTemplatesWithLog 重新加载提示词模板并记录日志 -func (s *Server) reloadPromptTemplatesWithLog(templateName string) { - if err := decision.ReloadPromptTemplates(); err != nil { - log.Printf("⚠️ 重新加载提示词模板失败: %v", err) - return - } - - if templateName == "" { - log.Printf("✓ 已重新加载系统提示词模板 [当前使用: default (未指定,使用默认)]") - } else { - log.Printf("✓ 已重新加载系统提示词模板 [当前使用: %s]", templateName) - } -} diff --git a/config/database.go b/config/database.go index 4168e57a..466550f3 100644 --- a/config/database.go +++ b/config/database.go @@ -186,9 +186,7 @@ func (d *Database) createTables() error { use_oi_top BOOLEAN DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (ai_model_id) REFERENCES ai_models(id), - FOREIGN KEY (exchange_id) REFERENCES exchanges(id) + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`, // 用户表 @@ -390,6 +388,12 @@ func (d *Database) createTables() error { log.Printf("⚠️ 迁移exchanges表失败: %v", err) } + // 修复traders表的外键约束问题 + err = d.migrateTradersTable() + if err != nil { + log.Printf("⚠️ 迁移traders表失败: %v", err) + } + return nil } @@ -613,6 +617,91 @@ func (d *Database) migrateExchangesTable() error { return nil } +// migrateTradersTable 迁移traders表,移除外键约束 +func (d *Database) migrateTradersTable() error { + // 检查traders表是否存在外键约束(通过尝试创建一个测试记录来判断) + // 如果表已经没有外键约束,则跳过迁移 + var tableSQL string + err := d.db.QueryRow(`SELECT sql FROM sqlite_master WHERE type='table' AND name='traders'`).Scan(&tableSQL) + if err != nil { + // 表不存在,无需迁移 + return nil + } + + // 检查是否包含 FOREIGN KEY (exchange_id) 或 FOREIGN KEY (ai_model_id) + if !strings.Contains(tableSQL, "FOREIGN KEY (exchange_id)") && !strings.Contains(tableSQL, "FOREIGN KEY (ai_model_id)") { + // 已经没有这些外键约束,无需迁移 + return nil + } + + log.Printf("🔄 开始迁移traders表,移除外键约束...") + + // 创建新的traders表,不包含exchange_id和ai_model_id的外键约束 + _, err = d.db.Exec(` + CREATE TABLE traders_new ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL DEFAULT 'default', + name TEXT NOT NULL, + ai_model_id TEXT NOT NULL, + exchange_id TEXT NOT NULL, + initial_balance REAL NOT NULL, + scan_interval_minutes INTEGER DEFAULT 3, + is_running BOOLEAN DEFAULT 0, + btc_eth_leverage INTEGER DEFAULT 5, + altcoin_leverage INTEGER DEFAULT 5, + trading_symbols TEXT DEFAULT '', + use_coin_pool BOOLEAN DEFAULT 0, + use_oi_top BOOLEAN DEFAULT 0, + custom_prompt TEXT DEFAULT '', + override_base_prompt BOOLEAN DEFAULT 0, + system_prompt_template TEXT DEFAULT 'default', + is_cross_margin BOOLEAN DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ) + `) + if err != nil { + return fmt.Errorf("创建新traders表失败: %w", err) + } + + // 复制数据到新表 + _, err = d.db.Exec(` + INSERT INTO traders_new (id, user_id, name, ai_model_id, exchange_id, initial_balance, + scan_interval_minutes, is_running, btc_eth_leverage, altcoin_leverage, trading_symbols, + use_coin_pool, use_oi_top, custom_prompt, override_base_prompt, system_prompt_template, + is_cross_margin, created_at, updated_at) + SELECT id, user_id, name, ai_model_id, exchange_id, initial_balance, + scan_interval_minutes, is_running, + COALESCE(btc_eth_leverage, 5), COALESCE(altcoin_leverage, 5), + COALESCE(trading_symbols, ''), COALESCE(use_coin_pool, 0), COALESCE(use_oi_top, 0), + COALESCE(custom_prompt, ''), COALESCE(override_base_prompt, 0), + COALESCE(system_prompt_template, 'default'), COALESCE(is_cross_margin, 1), + created_at, updated_at + FROM traders + `) + if err != nil { + // 如果复制失败,删除新表 + d.db.Exec(`DROP TABLE traders_new`) + return fmt.Errorf("复制traders数据失败: %w", err) + } + + // 删除旧表 + _, err = d.db.Exec(`DROP TABLE traders`) + if err != nil { + return fmt.Errorf("删除旧traders表失败: %w", err) + } + + // 重命名新表 + _, err = d.db.Exec(`ALTER TABLE traders_new RENAME TO traders`) + if err != nil { + return fmt.Errorf("重命名traders表失败: %w", err) + } + + log.Printf("✅ traders表迁移完成,已移除外键约束") + return nil +} + // User 用户配置 type User struct { ID string `json:"id"` @@ -744,32 +833,38 @@ func (d *Database) EnsureAdminUser() error { // GetUserByEmail 通过邮箱获取用户 func (d *Database) GetUserByEmail(email string) (*User, error) { var user User + var createdAt, updatedAt string err := d.db.QueryRow(` SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at FROM users WHERE email = ? `, email).Scan( &user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret, - &user.OTPVerified, &user.CreatedAt, &user.UpdatedAt, + &user.OTPVerified, &createdAt, &updatedAt, ) if err != nil { return nil, err } + user.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt) + user.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt) return &user, nil } // GetUserByID 通过ID获取用户 func (d *Database) GetUserByID(userID string) (*User, error) { var user User + var createdAt, updatedAt string err := d.db.QueryRow(` SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at FROM users WHERE id = ? `, userID).Scan( &user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret, - &user.OTPVerified, &user.CreatedAt, &user.UpdatedAt, + &user.OTPVerified, &createdAt, &updatedAt, ) if err != nil { return nil, err } + user.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt) + user.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt) return &user, nil } @@ -826,14 +921,18 @@ func (d *Database) GetAIModels(userID string) ([]*AIModelConfig, error) { models := make([]*AIModelConfig, 0) for rows.Next() { var model AIModelConfig + var createdAt, updatedAt string err := rows.Scan( &model.ID, &model.UserID, &model.Name, &model.Provider, &model.Enabled, &model.APIKey, &model.CustomAPIURL, &model.CustomModelName, - &model.CreatedAt, &model.UpdatedAt, + &createdAt, &updatedAt, ) if err != nil { return nil, err } + // 解析时间字符串 + model.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt) + model.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt) // 解密API Key model.APIKey = d.decryptSensitiveData(model.APIKey) models = append(models, &model) @@ -861,6 +960,7 @@ func (d *Database) GetAIModel(userID, modelID string) (*AIModelConfig, error) { for _, uid := range candidates { var model AIModelConfig + var createdAt, updatedAt string err := d.db.QueryRow(` SELECT id, user_id, name, provider, enabled, api_key, COALESCE(custom_api_url, ''), COALESCE(custom_model_name, ''), created_at, updated_at @@ -876,10 +976,13 @@ func (d *Database) GetAIModel(userID, modelID string) (*AIModelConfig, error) { &model.APIKey, &model.CustomAPIURL, &model.CustomModelName, - &model.CreatedAt, - &model.UpdatedAt, + &createdAt, + &updatedAt, ) if err == nil { + // 解析时间字符串 + model.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt) + model.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt) // 解密API Key(与 GetAIModels 行为保持一致) model.APIKey = d.decryptSensitiveData(model.APIKey) return &model, nil @@ -912,6 +1015,7 @@ func (d *Database) GetDefaultAIModel(userID string) (*AIModelConfig, error) { func (d *Database) firstEnabledAIModel(userID string) (*AIModelConfig, error) { var model AIModelConfig + var createdAt, updatedAt string err := d.db.QueryRow(` SELECT id, user_id, name, provider, enabled, api_key, COALESCE(custom_api_url, ''), COALESCE(custom_model_name, ''), created_at, updated_at @@ -928,12 +1032,15 @@ func (d *Database) firstEnabledAIModel(userID string) (*AIModelConfig, error) { &model.APIKey, &model.CustomAPIURL, &model.CustomModelName, - &model.CreatedAt, - &model.UpdatedAt, + &createdAt, + &updatedAt, ) if err != nil { return nil, err } + // 解析时间字符串 + model.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt) + model.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt) // 解密API Key,避免上层拿到加密串导致下游认证失败 model.APIKey = d.decryptSensitiveData(model.APIKey) return &model, nil @@ -1033,6 +1140,7 @@ func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) { COALESCE(aster_private_key, '') as aster_private_key, COALESCE(lighter_wallet_addr, '') as lighter_wallet_addr, COALESCE(lighter_private_key, '') as lighter_private_key, + COALESCE(lighter_api_key_private_key, '') as lighter_api_key_private_key, created_at, updated_at FROM exchanges WHERE user_id = ? ORDER BY id `, userID) @@ -1045,23 +1153,30 @@ func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) { exchanges := make([]*ExchangeConfig, 0) for rows.Next() { var exchange ExchangeConfig + var createdAt, updatedAt string err := rows.Scan( &exchange.ID, &exchange.UserID, &exchange.Name, &exchange.Type, &exchange.Enabled, &exchange.APIKey, &exchange.SecretKey, &exchange.Testnet, &exchange.HyperliquidWalletAddr, &exchange.AsterUser, &exchange.AsterSigner, &exchange.AsterPrivateKey, &exchange.LighterWalletAddr, &exchange.LighterPrivateKey, - &exchange.CreatedAt, &exchange.UpdatedAt, + &exchange.LighterAPIKeyPrivateKey, + &createdAt, &updatedAt, ) if err != nil { return nil, err } + // 解析时间字符串 + exchange.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt) + exchange.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt) + // 解密敏感字段 exchange.APIKey = d.decryptSensitiveData(exchange.APIKey) exchange.SecretKey = d.decryptSensitiveData(exchange.SecretKey) exchange.AsterPrivateKey = d.decryptSensitiveData(exchange.AsterPrivateKey) exchange.LighterPrivateKey = d.decryptSensitiveData(exchange.LighterPrivateKey) + exchange.LighterAPIKeyPrivateKey = d.decryptSensitiveData(exchange.LighterAPIKeyPrivateKey) exchanges = append(exchanges, &exchange) } @@ -1243,6 +1358,7 @@ func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) { var traders []*TraderRecord for rows.Next() { var trader TraderRecord + var createdAt, updatedAt string err := rows.Scan( &trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID, &trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning, @@ -1250,11 +1366,14 @@ func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) { &trader.UseCoinPool, &trader.UseOITop, &trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate, &trader.IsCrossMargin, - &trader.CreatedAt, &trader.UpdatedAt, + &createdAt, &updatedAt, ) if err != nil { return nil, err } + // 解析时间字符串 + trader.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt) + trader.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt) traders = append(traders, &trader) } @@ -1307,6 +1426,9 @@ func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIM var trader TraderRecord var aiModel AIModelConfig var exchange ExchangeConfig + var traderCreatedAt, traderUpdatedAt string + var aiModelCreatedAt, aiModelUpdatedAt string + var exchangeCreatedAt, exchangeUpdatedAt string err := d.db.QueryRow(` SELECT @@ -1332,6 +1454,7 @@ func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIM COALESCE(e.aster_private_key, '') as aster_private_key, COALESCE(e.lighter_wallet_addr, '') as lighter_wallet_addr, COALESCE(e.lighter_private_key, '') as lighter_private_key, + COALESCE(e.lighter_api_key_private_key, '') as lighter_api_key_private_key, e.created_at, e.updated_at FROM traders t JOIN ai_models a ON t.ai_model_id = a.id AND t.user_id = a.user_id @@ -1344,27 +1467,36 @@ func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIM &trader.UseCoinPool, &trader.UseOITop, &trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate, &trader.IsCrossMargin, - &trader.CreatedAt, &trader.UpdatedAt, + &traderCreatedAt, &traderUpdatedAt, &aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey, &aiModel.CustomAPIURL, &aiModel.CustomModelName, - &aiModel.CreatedAt, &aiModel.UpdatedAt, + &aiModelCreatedAt, &aiModelUpdatedAt, &exchange.ID, &exchange.UserID, &exchange.Name, &exchange.Type, &exchange.Enabled, &exchange.APIKey, &exchange.SecretKey, &exchange.Testnet, &exchange.HyperliquidWalletAddr, &exchange.AsterUser, &exchange.AsterSigner, &exchange.AsterPrivateKey, - &exchange.LighterWalletAddr, &exchange.LighterPrivateKey, - &exchange.CreatedAt, &exchange.UpdatedAt, + &exchange.LighterWalletAddr, &exchange.LighterPrivateKey, &exchange.LighterAPIKeyPrivateKey, + &exchangeCreatedAt, &exchangeUpdatedAt, ) if err != nil { return nil, nil, nil, err } + // 解析时间字符串 + trader.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", traderCreatedAt) + trader.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", traderUpdatedAt) + aiModel.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", aiModelCreatedAt) + aiModel.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", aiModelUpdatedAt) + exchange.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", exchangeCreatedAt) + exchange.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", exchangeUpdatedAt) + // 解密敏感数据 aiModel.APIKey = d.decryptSensitiveData(aiModel.APIKey) exchange.APIKey = d.decryptSensitiveData(exchange.APIKey) exchange.SecretKey = d.decryptSensitiveData(exchange.SecretKey) exchange.AsterPrivateKey = d.decryptSensitiveData(exchange.AsterPrivateKey) exchange.LighterPrivateKey = d.decryptSensitiveData(exchange.LighterPrivateKey) + exchange.LighterAPIKeyPrivateKey = d.decryptSensitiveData(exchange.LighterAPIKeyPrivateKey) return &trader, &aiModel, &exchange, nil } @@ -1396,16 +1528,19 @@ func (d *Database) CreateUserSignalSource(userID, coinPoolURL, oiTopURL string) // GetUserSignalSource 获取用户信号源配置 func (d *Database) GetUserSignalSource(userID string) (*UserSignalSource, error) { var source UserSignalSource + var createdAt, updatedAt string err := d.db.QueryRow(` SELECT id, user_id, coin_pool_url, oi_top_url, created_at, updated_at FROM user_signal_sources WHERE user_id = ? `, userID).Scan( &source.ID, &source.UserID, &source.CoinPoolURL, &source.OITopURL, - &source.CreatedAt, &source.UpdatedAt, + &createdAt, &updatedAt, ) if err != nil { return nil, err } + source.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt) + source.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt) return &source, nil } diff --git a/web/src/App.tsx b/web/src/App.tsx index 252c35e1..81453d5c 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -13,6 +13,7 @@ import HeaderBar from './components/HeaderBar' import AILearning from './components/AILearning' import { LanguageProvider, useLanguage } from './contexts/LanguageContext' import { AuthProvider, useAuth } from './contexts/AuthContext' +import { ConfirmDialogProvider } from './components/ConfirmDialog' import { t, type Language } from './i18n/translations' import { useSystemConfig } from './hooks/useSystemConfig' import { DecisionCard } from './components/DecisionCard' @@ -1062,7 +1063,9 @@ export default function AppWithProviders() { return ( - + + + )