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 <nofx@local.dev>
This commit is contained in:
Professor-Chen
2025-11-30 12:22:20 +08:00
committed by GitHub
parent 11b6c6ba3e
commit c34a6c6bcf
3 changed files with 407 additions and 282 deletions

View File

@@ -5,7 +5,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
"math"
"net" "net"
"net/http" "net/http"
"nofx/auth" "nofx/auth"
@@ -13,7 +12,6 @@ import (
"nofx/config" "nofx/config"
"nofx/crypto" "nofx/crypto"
"nofx/decision" "nofx/decision"
"nofx/hook"
"nofx/manager" "nofx/manager"
"nofx/trader" "nofx/trader"
"strconv" "strconv"
@@ -27,22 +25,15 @@ import (
// Server HTTP API服务器 // Server HTTP API服务器
type Server struct { type Server struct {
router *gin.Engine router *gin.Engine
httpServer *http.Server
traderManager *manager.TraderManager traderManager *manager.TraderManager
database *config.Database database *config.Database
cryptoHandler *CryptoHandler cryptoHandler *CryptoHandler
backtestManager *backtest.Manager backtestManager *backtest.Manager
httpServer *http.Server
port int port int
} }
// NewServer 创建API服务器 // NewServer 创建API服务器
func NewServer( func NewServer(traderManager *manager.TraderManager, database *config.Database, cryptoService *crypto.CryptoService, backtestManager *backtest.Manager, port int) *Server {
traderManager *manager.TraderManager,
database *config.Database,
cryptoService *crypto.CryptoService,
backtestManager *backtest.Manager,
port int,
) *Server {
// 设置为Release模式减少日志输出 // 设置为Release模式减少日志输出
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
@@ -62,9 +53,6 @@ func NewServer(
backtestManager: backtestManager, backtestManager: backtestManager,
port: port, port: port,
} }
if s.backtestManager != nil {
s.backtestManager.SetAIResolver(s.hydrateBacktestAIConfig)
}
// 设置路由 // 设置路由
s.setupRoutes() s.setupRoutes()
@@ -130,11 +118,6 @@ func (s *Server) setupRoutes() {
// 需要认证的路由 // 需要认证的路由
protected := api.Group("/", s.authMiddleware()) protected := api.Group("/", s.authMiddleware())
{ {
if s.backtestManager != nil {
backtestGroup := protected.Group("/backtest")
s.registerBacktestRoutes(backtestGroup)
}
// 注销(加入黑名单) // 注销(加入黑名单)
protected.POST("/logout", s.handleLogout) protected.POST("/logout", s.handleLogout)
@@ -150,6 +133,7 @@ func (s *Server) setupRoutes() {
protected.POST("/traders/:id/start", s.handleStartTrader) protected.POST("/traders/:id/start", s.handleStartTrader)
protected.POST("/traders/:id/stop", s.handleStopTrader) protected.POST("/traders/:id/stop", s.handleStopTrader)
protected.PUT("/traders/:id/prompt", s.handleUpdateTraderPrompt) protected.PUT("/traders/:id/prompt", s.handleUpdateTraderPrompt)
protected.POST("/traders/:id/sync-balance", s.handleSyncBalance)
// AI模型配置 // AI模型配置
protected.GET("/models", s.handleGetModelConfigs) protected.GET("/models", s.handleGetModelConfigs)
@@ -171,7 +155,6 @@ func (s *Server) setupRoutes() {
protected.GET("/decisions/latest", s.handleLatestDecisions) protected.GET("/decisions/latest", s.handleLatestDecisions)
protected.GET("/statistics", s.handleStatistics) protected.GET("/statistics", s.handleStatistics)
protected.GET("/performance", s.handlePerformance) 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") betaModeStr, _ := s.database.GetSystemConfig("beta_mode")
betaMode := betaModeStr == "true" 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{ c.JSON(http.StatusOK, gin.H{
"beta_mode": betaMode, "beta_mode": betaMode,
"default_coins": defaultCoins, "default_coins": defaultCoins,
"btc_eth_leverage": btcEthLeverage, "btc_eth_leverage": btcEthLeverage,
"altcoin_leverage": altcoinLeverage, "altcoin_leverage": altcoinLeverage,
"registration_enabled": registrationEnabled,
}) })
} }
// handleGetServerIP 获取服务器IP地址用于白名单配置 // handleGetServerIP 获取服务器IP地址用于白名单配置
func (s *Server) handleGetServerIP(c *gin.Context) { 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 // 尝试通过第三方API获取公网IP
publicIP := getPublicIPFromAPI() publicIP := getPublicIPFromAPI()
@@ -431,8 +396,8 @@ type SafeModelConfig struct {
Name string `json:"name"` Name string `json:"name"`
Provider string `json:"provider"` Provider string `json:"provider"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
CustomAPIURL string `json:"customApiUrl"` // 自定义API URL通常不敏感 CustomAPIURL string `json:"customApiUrl"` // 自定义API URL通常不敏感
CustomModelName string `json:"customModelName"` // 自定义模型名(不敏感) CustomModelName string `json:"customModelName"` // 自定义模型名(不敏感)
} }
type ExchangeConfig struct { type ExchangeConfig struct {
@@ -453,8 +418,8 @@ type SafeExchangeConfig struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Testnet bool `json:"testnet,omitempty"` Testnet bool `json:"testnet,omitempty"`
HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"` // Hyperliquid钱包地址不敏感 HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"` // Hyperliquid钱包地址不敏感
AsterUser string `json:"asterUser"` // Aster用户名不敏感 AsterUser string `json:"asterUser"` // Aster用户名不敏感
AsterSigner string `json:"asterSigner"` // Aster签名者不敏感 AsterSigner string `json:"asterSigner"` // Aster签名者不敏感
} }
type UpdateModelConfigRequest struct { type UpdateModelConfigRequest struct {
@@ -512,9 +477,8 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
} }
} }
// 生成交易员ID (使用 UUID 确保唯一性,解决 Issue #893) // 生成交易员ID
// 保留前缀以便调试和日志追踪 traderID := fmt.Sprintf("%s_%s_%d", req.ExchangeID, req.AIModelID, time.Now().Unix())
traderID := fmt.Sprintf("%s_%s_%s", req.ExchangeID, req.AIModelID, uuid.New().String())
// 设置默认值 // 设置默认值
isCrossMargin := true // 默认为全仓模式 isCrossMargin := true // 默认为全仓模式
@@ -610,42 +574,32 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
if balanceErr != nil { if balanceErr != nil {
log.Printf("⚠️ 查询交易所余额失败,使用用户输入的初始资金: %v", balanceErr) log.Printf("⚠️ 查询交易所余额失败,使用用户输入的初始资金: %v", balanceErr)
} else { } else {
// 🔧 计算Total Equity = Wallet Balance + Unrealized Profit // 提取可用余额 - 支持多种字段名格式
// 这是账户的真实净值用作Initial Balance的基准 if availableBalance, ok := balanceInfo["availableBalance"].(float64); ok && availableBalance > 0 {
var totalWalletBalance float64 // Binance 格式: availableBalance (camelCase)
var totalUnrealizedProfit float64 actualBalance = availableBalance
log.Printf("✓ 查询到交易所实际余额: %.2f USDT (用户输入: %.2f USDT)", actualBalance, req.InitialBalance)
// 提取钱包余额 } else if availableBalance, ok := balanceInfo["available_balance"].(float64); ok && availableBalance > 0 {
if wb, ok := balanceInfo["totalWalletBalance"].(float64); ok { // 其他格式: available_balance (snake_case)
totalWalletBalance = wb actualBalance = availableBalance
} else if wb, ok := balanceInfo["wallet_balance"].(float64); ok { log.Printf("✓ 查询到交易所实际余额: %.2f USDT (用户输入: %.2f USDT)", actualBalance, req.InitialBalance)
totalWalletBalance = wb } else if totalBalance, ok := balanceInfo["totalWalletBalance"].(float64); ok && totalBalance > 0 {
} else if wb, ok := balanceInfo["balance"].(float64); ok { // Binance 格式: totalWalletBalance (camelCase)
totalWalletBalance = wb actualBalance = totalBalance
} log.Printf("✓ 查询到交易所总余额: %.2f USDT (用户输入: %.2f USDT)", actualBalance, req.InitialBalance)
} else if totalBalance, ok := balanceInfo["balance"].(float64); ok && totalBalance > 0 {
// 提取未实现盈亏 // 其他格式: balance
if up, ok := balanceInfo["totalUnrealizedProfit"].(float64); ok { actualBalance = totalBalance
totalUnrealizedProfit = up log.Printf("✓ 查询到交易所实际余额: %.2f USDT (用户输入: %.2f USDT)", actualBalance, req.InitialBalance)
} 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)
} else { } 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{ trader := &config.TraderRecord{
ID: traderID, ID: traderID,
UserID: userID, UserID: userID,
@@ -667,18 +621,23 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
} }
// 保存到数据库 // 保存到数据库
log.Printf("🔧 DEBUG: 准备调用 CreateTrader")
err = s.database.CreateTrader(trader) err = s.database.CreateTrader(trader)
if err != nil { if err != nil {
log.Printf("❌ 创建交易员失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("创建交易员失败: %v", err)}) c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("创建交易员失败: %v", err)})
return return
} }
log.Printf("🔧 DEBUG: CreateTrader 成功")
// 立即将新交易员加载到TraderManager中 // 立即将新交易员加载到TraderManager中
err = s.traderManager.LoadTraderByID(s.database, userID, traderID) log.Printf("🔧 DEBUG: 准备调用 LoadUserTraders")
err = s.traderManager.LoadUserTraders(s.database, userID)
if err != nil { 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) log.Printf("✓ 创建交易员成功: %s (模型: %s, 交易所: %s)", req.Name, req.AIModelID, req.ExchangeID)
@@ -692,18 +651,17 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
// UpdateTraderRequest 更新交易员请求 // UpdateTraderRequest 更新交易员请求
type UpdateTraderRequest struct { type UpdateTraderRequest struct {
Name string `json:"name" binding:"required"` Name string `json:"name" binding:"required"`
AIModelID string `json:"ai_model_id" binding:"required"` AIModelID string `json:"ai_model_id" binding:"required"`
ExchangeID string `json:"exchange_id" binding:"required"` ExchangeID string `json:"exchange_id" binding:"required"`
InitialBalance float64 `json:"initial_balance"` InitialBalance float64 `json:"initial_balance"`
ScanIntervalMinutes int `json:"scan_interval_minutes"` ScanIntervalMinutes int `json:"scan_interval_minutes"`
BTCETHLeverage int `json:"btc_eth_leverage"` BTCETHLeverage int `json:"btc_eth_leverage"`
AltcoinLeverage int `json:"altcoin_leverage"` AltcoinLeverage int `json:"altcoin_leverage"`
TradingSymbols string `json:"trading_symbols"` TradingSymbols string `json:"trading_symbols"`
CustomPrompt string `json:"custom_prompt"` CustomPrompt string `json:"custom_prompt"`
OverrideBasePrompt bool `json:"override_base_prompt"` OverrideBasePrompt bool `json:"override_base_prompt"`
SystemPromptTemplate string `json:"system_prompt_template"` IsCrossMargin *bool `json:"is_cross_margin"`
IsCrossMargin *bool `json:"is_cross_margin"`
} }
// handleUpdateTrader 更新交易员配置 // handleUpdateTrader 更新交易员配置
@@ -761,12 +719,6 @@ func (s *Server) handleUpdateTrader(c *gin.Context) {
scanIntervalMinutes = 3 scanIntervalMinutes = 3
} }
// 设置提示词模板,允许更新
systemPromptTemplate := req.SystemPromptTemplate
if systemPromptTemplate == "" {
systemPromptTemplate = existingTrader.SystemPromptTemplate // 如果请求中没有提供,保持原值
}
// 更新交易员配置 // 更新交易员配置
trader := &config.TraderRecord{ trader := &config.TraderRecord{
ID: traderID, ID: traderID,
@@ -780,7 +732,7 @@ func (s *Server) handleUpdateTrader(c *gin.Context) {
TradingSymbols: req.TradingSymbols, TradingSymbols: req.TradingSymbols,
CustomPrompt: req.CustomPrompt, CustomPrompt: req.CustomPrompt,
OverrideBasePrompt: req.OverrideBasePrompt, OverrideBasePrompt: req.OverrideBasePrompt,
SystemPromptTemplate: systemPromptTemplate, SystemPromptTemplate: existingTrader.SystemPromptTemplate, // 保持原值
IsCrossMargin: isCrossMargin, IsCrossMargin: isCrossMargin,
ScanIntervalMinutes: scanIntervalMinutes, ScanIntervalMinutes: scanIntervalMinutes,
IsRunning: existingTrader.IsRunning, // 保持原值 IsRunning: existingTrader.IsRunning, // 保持原值
@@ -793,25 +745,10 @@ func (s *Server) handleUpdateTrader(c *gin.Context) {
return 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 { if err != nil {
log.Printf("⚠️ 重新加载交易员到内存失败: %v", err) log.Printf("⚠️ 重新加载用户交易员到内存失败: %v", err)
} }
log.Printf("✓ 更新交易员成功: %s (模型: %s, 交易所: %s)", req.Name, req.AIModelID, req.ExchangeID) 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") traderID := c.Param("id")
// 校验交易员是否属于当前用户 // 校验交易员是否属于当前用户
traderRecord, _, _, err := s.database.GetTraderConfig(userID, traderID) _, _, _, err := s.database.GetTraderConfig(userID, traderID)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在或无访问权限"}) c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在或无访问权限"})
return return
} }
// 获取模板名称
templateName := traderRecord.SystemPromptTemplate
trader, err := s.traderManager.GetTrader(traderID) trader, err := s.traderManager.GetTrader(traderID)
if err != nil { if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"})
@@ -877,9 +811,6 @@ func (s *Server) handleStartTrader(c *gin.Context) {
return return
} }
// 重新加载系统提示词模板(确保使用最新的硬盘文件)
s.reloadPromptTemplatesWithLog(templateName)
// 启动交易员 // 启动交易员
go func() { go func() {
log.Printf("▶️ 启动交易员 %s (%s)", traderID, trader.GetName()) 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已更新"}) 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模型配置 // handleGetModelConfigs 获取AI模型配置
func (s *Server) handleGetModelConfigs(c *gin.Context) { func (s *Server) handleGetModelConfigs(c *gin.Context) {
userID := c.GetString("user_id") 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": "模型配置已更新"}) c.JSON(http.StatusOK, gin.H{"message": "模型配置已更新"})
} }
@@ -1075,6 +1113,23 @@ func (s *Server) handleGetExchangeConfigs(c *gin.Context) {
return return
} }
log.Printf("✅ 找到 %d 个交易所配置", len(exchanges)) 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)) 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": "交易所配置已更新"}) c.JSON(http.StatusOK, gin.H{"message": "交易所配置已更新"})
} }
@@ -1226,13 +1281,12 @@ func (s *Server) handleTraderList(c *gin.Context) {
// 返回完整的 AIModelID如 "admin_deepseek"),不要截断 // 返回完整的 AIModelID如 "admin_deepseek"),不要截断
// 前端需要完整 ID 来验证模型是否存在(与 handleGetTraderConfig 保持一致) // 前端需要完整 ID 来验证模型是否存在(与 handleGetTraderConfig 保持一致)
result = append(result, map[string]interface{}{ result = append(result, map[string]interface{}{
"trader_id": trader.ID, "trader_id": trader.ID,
"trader_name": trader.Name, "trader_name": trader.Name,
"ai_model": trader.AIModelID, // 使用完整 ID "ai_model": trader.AIModelID, // 使用完整 ID
"exchange_id": trader.ExchangeID, "exchange_id": trader.ExchangeID,
"is_running": isRunning, "is_running": isRunning,
"initial_balance": trader.InitialBalance, "initial_balance": trader.InitialBalance,
"system_prompt_template": trader.SystemPromptTemplate,
}) })
} }
@@ -1268,22 +1322,21 @@ func (s *Server) handleGetTraderConfig(c *gin.Context) {
aiModelID := traderConfig.AIModelID aiModelID := traderConfig.AIModelID
result := map[string]interface{}{ result := map[string]interface{}{
"trader_id": traderConfig.ID, "trader_id": traderConfig.ID,
"trader_name": traderConfig.Name, "trader_name": traderConfig.Name,
"ai_model": aiModelID, "ai_model": aiModelID,
"exchange_id": traderConfig.ExchangeID, "exchange_id": traderConfig.ExchangeID,
"initial_balance": traderConfig.InitialBalance, "initial_balance": traderConfig.InitialBalance,
"scan_interval_minutes": traderConfig.ScanIntervalMinutes, "scan_interval_minutes": traderConfig.ScanIntervalMinutes,
"btc_eth_leverage": traderConfig.BTCETHLeverage, "btc_eth_leverage": traderConfig.BTCETHLeverage,
"altcoin_leverage": traderConfig.AltcoinLeverage, "altcoin_leverage": traderConfig.AltcoinLeverage,
"trading_symbols": traderConfig.TradingSymbols, "trading_symbols": traderConfig.TradingSymbols,
"custom_prompt": traderConfig.CustomPrompt, "custom_prompt": traderConfig.CustomPrompt,
"override_base_prompt": traderConfig.OverrideBasePrompt, "override_base_prompt": traderConfig.OverrideBasePrompt,
"system_prompt_template": traderConfig.SystemPromptTemplate, "is_cross_margin": traderConfig.IsCrossMargin,
"is_cross_margin": traderConfig.IsCrossMargin, "use_coin_pool": traderConfig.UseCoinPool,
"use_coin_pool": traderConfig.UseCoinPool, "use_oi_top": traderConfig.UseOITop,
"use_oi_top": traderConfig.UseOITop, "is_running": isRunning,
"is_running": isRunning,
} }
c.JSON(http.StatusOK, result) c.JSON(http.StatusOK, result)
@@ -1405,15 +1458,7 @@ func (s *Server) handleLatestDecisions(c *gin.Context) {
return return
} }
// 从 query 参数读取 limit默认 5最大 50 records, err := trader.GetDecisionLogger().GetLatestRecords(5)
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)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取决策日志失败: %v", err), "error": fmt.Sprintf("获取决策日志失败: %v", err),
@@ -1512,16 +1557,22 @@ func (s *Server) handleEquityHistory(c *gin.Context) {
CycleNumber int `json:"cycle_number"` CycleNumber int `json:"cycle_number"`
} }
// 从AutoTrader获取当前初始余额(用作旧数据的fallback // 从AutoTrader获取初始余额于计算盈亏百分比
base := 0.0 initialBalance := 0.0
if status := trader.GetStatus(); status != nil { if status := trader.GetStatus(); status != nil {
if ib, ok := status["initial_balance"].(float64); ok && ib > 0 { 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{ c.JSON(http.StatusInternalServerError, gin.H{
"error": "无法获取初始余额", "error": "无法获取初始余额",
}) })
@@ -1531,24 +1582,14 @@ func (s *Server) handleEquityHistory(c *gin.Context) {
var history []EquityPoint var history []EquityPoint
for _, record := range records { for _, record := range records {
// TotalBalance字段实际存储的是TotalEquity // TotalBalance字段实际存储的是TotalEquity
// totalEquity := record.AccountState.TotalBalance totalEquity := record.AccountState.TotalBalance
// TotalUnrealizedProfit字段实际存储的是TotalPnL相对初始余额 // TotalUnrealizedProfit字段实际存储的是TotalPnL相对初始余额
// totalPnL := record.AccountState.TotalUnrealizedProfit totalPnL := record.AccountState.TotalUnrealizedProfit
walletBalance := record.AccountState.TotalBalance
unrealizedPnL := record.AccountState.TotalUnrealizedProfit
totalEquity := walletBalance + unrealizedPnL
// 🔄 使用历史记录中保存的initial_balance如果有
// 这样可以保持历史PNL%的准确性即使用户后来更新了initial_balance
if record.AccountState.InitialBalance > 0 {
base = record.AccountState.InitialBalance
}
totalPnL := totalEquity - base
// 计算盈亏百分比 // 计算盈亏百分比
totalPnLPct := 0.0 totalPnLPct := 0.0
if base > 0 { if initialBalance > 0 {
totalPnLPct = (totalPnL / base) * 100 totalPnLPct = (totalPnL / initialBalance) * 100
} }
history = append(history, EquityPoint{ history = append(history, EquityPoint{
@@ -1635,6 +1676,7 @@ func (s *Server) authMiddleware() gin.HandlerFunc {
} }
} }
// handleLogout 将当前token加入黑名单 // handleLogout 将当前token加入黑名单
func (s *Server) handleLogout(c *gin.Context) { func (s *Server) handleLogout(c *gin.Context) {
authHeader := c.GetHeader("Authorization") authHeader := c.GetHeader("Authorization")
@@ -1665,14 +1707,6 @@ func (s *Server) handleLogout(c *gin.Context) {
// handleRegister 处理用户注册请求 // handleRegister 处理用户注册请求
func (s *Server) handleRegister(c *gin.Context) { 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 { var req struct {
Email string `json:"email" binding:"required,email"` 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 { 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": "邮箱已被注册"}) c.JSON(http.StatusConflict, gin.H{"error": "邮箱已被注册"})
return return
} }
@@ -2014,63 +2035,44 @@ func (s *Server) Start() error {
addr := fmt.Sprintf(":%d", s.port) addr := fmt.Sprintf(":%d", s.port)
log.Printf("🌐 API服务器启动在 http://localhost%s", addr) log.Printf("🌐 API服务器启动在 http://localhost%s", addr)
log.Printf("📊 API文档:") log.Printf("📊 API文档:")
log.Printf(" • GET /api/health - 健康检查") log.Printf(" • GET /api/health - 健康检查")
log.Printf(" • 公共竞赛/排行榜相关接口") log.Printf(" • GET /api/traders - 公开的AI交易员排行榜前50名无需认证")
log.Printf(" - GET /api/traders - 公开的AI交易员排行榜(无需认证)") log.Printf(" GET /api/competition - 公开的竞赛数据(无需认证)")
log.Printf(" - GET /api/competition - 公开竞赛数据(无需认证)") log.Printf(" GET /api/top-traders - 前5名交易员数据(无需认证,表现对比用")
log.Printf(" - GET /api/top-traders - 前5名交易员(无需认证)") log.Printf(" GET /api/equity-history?trader_id=xxx - 公开的收益率历史数据(无需认证,竞赛用")
log.Printf(" - GET /api/equity-history - 指定trader收益率历史无需认证") log.Printf(" GET /api/equity-history-batch?trader_ids=a,b,c - 批量获取历史数据(无需认证,表现对比优化")
log.Printf(" - POST /api/equity-history-batch - 批量获取收益率历史(无需认证") log.Printf(" • GET /api/traders/:id/public-config - 公开的交易员配置(无需认证,不含敏感信息")
log.Printf(" - GET /api/traders/:id/public-config - 公开交易员配置(无需认证)") log.Printf(" • POST /api/traders - 创建新的AI交易员")
log.Printf(" • Backtest") log.Printf(" • DELETE /api/traders/:id - 删除AI交易员")
log.Printf(" - GET /api/backtest/runs - 回测运行列表") log.Printf(" • POST /api/traders/:id/start - 启动AI交易员")
log.Printf(" - POST /api/backtest/start - 启动新的回测") log.Printf(" POST /api/traders/:id/stop - 停止AI交易员")
log.Printf(" - POST /api/backtest/pause - 暂停指定回测") log.Printf(" • GET /api/models - 获取AI模型配置")
log.Printf(" - POST /api/backtest/resume - 恢复指定回测") log.Printf(" • PUT /api/models - 更新AI模型配置")
log.Printf(" - POST /api/backtest/stop - 停止指定回测") log.Printf(" • GET /api/exchanges - 获取交易所配置")
log.Printf(" - GET /api/backtest/status - 查询回测状态") log.Printf(" • PUT /api/exchanges - 更新交易所配置")
log.Printf(" - GET /api/backtest/equity - 回测净值曲线") log.Printf(" GET /api/status?trader_id=xxx - 指定trader的系统状态")
log.Printf(" - GET /api/backtest/trades - 回测交易记录") log.Printf(" GET /api/account?trader_id=xxx - 指定trader的账户信息")
log.Printf(" - GET /api/backtest/metrics - 回测统计指标") log.Printf(" GET /api/positions?trader_id=xxx - 指定trader的持仓列表")
log.Printf(" - GET /api/backtest/trace - 回测AI Trace") log.Printf(" GET /api/decisions?trader_id=xxx - 指定trader的决策日志")
log.Printf(" - GET /api/backtest/export - 导出回测数据ZIP") log.Printf(" GET /api/decisions/latest?trader_id=xxx - 指定trader的最新决策")
log.Printf(" • Trader / 配置(需认证)") log.Printf(" • GET /api/statistics?trader_id=xxx - 指定trader的统计信息")
log.Printf(" - POST /api/traders - 创建AI交易员") log.Printf(" • GET /api/performance?trader_id=xxx - 指定trader的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.Println() log.Println()
// 创建 http.Server 以支持 graceful shutdown
s.httpServer = &http.Server{ s.httpServer = &http.Server{
Addr: addr, Addr: addr,
Handler: s.router, Handler: s.router,
} }
return s.httpServer.ListenAndServe() return s.httpServer.ListenAndServe()
} }
// Shutdown 优雅关闭 API 服务器 // Shutdown 优雅关闭服务器
func (s *Server) Shutdown() error { func (s *Server) Shutdown() error {
if s.httpServer == nil { if s.httpServer == nil {
return nil return nil
} }
// 设置 5 秒超时
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
return s.httpServer.Shutdown(ctx) return s.httpServer.Shutdown(ctx)
} }
@@ -2138,17 +2140,16 @@ func (s *Server) handlePublicTraderList(c *gin.Context) {
result := make([]map[string]interface{}, 0, len(traders)) result := make([]map[string]interface{}, 0, len(traders))
for _, trader := range traders { for _, trader := range traders {
result = append(result, map[string]interface{}{ result = append(result, map[string]interface{}{
"trader_id": trader["trader_id"], "trader_id": trader["trader_id"],
"trader_name": trader["trader_name"], "trader_name": trader["trader_name"],
"ai_model": trader["ai_model"], "ai_model": trader["ai_model"],
"exchange": trader["exchange"], "exchange": trader["exchange"],
"is_running": trader["is_running"], "is_running": trader["is_running"],
"total_equity": trader["total_equity"], "total_equity": trader["total_equity"],
"total_pnl": trader["total_pnl"], "total_pnl": trader["total_pnl"],
"total_pnl_pct": trader["total_pnl_pct"], "total_pnl_pct": trader["total_pnl_pct"],
"position_count": trader["position_count"], "position_count": trader["position_count"],
"margin_used_pct": trader["margin_used_pct"], "margin_used_pct": trader["margin_used_pct"],
"system_prompt_template": trader["system_prompt_template"],
}) })
} }
@@ -2316,17 +2317,3 @@ func (s *Server) handleGetPublicTraderConfig(c *gin.Context) {
c.JSON(http.StatusOK, result) 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)
}
}

View File

@@ -186,9 +186,7 @@ func (d *Database) createTables() error {
use_oi_top BOOLEAN DEFAULT 0, use_oi_top BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, 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)
)`, )`,
// 用户表 // 用户表
@@ -390,6 +388,12 @@ func (d *Database) createTables() error {
log.Printf("⚠️ 迁移exchanges表失败: %v", err) log.Printf("⚠️ 迁移exchanges表失败: %v", err)
} }
// 修复traders表的外键约束问题
err = d.migrateTradersTable()
if err != nil {
log.Printf("⚠️ 迁移traders表失败: %v", err)
}
return nil return nil
} }
@@ -613,6 +617,91 @@ func (d *Database) migrateExchangesTable() error {
return nil 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 用户配置 // User 用户配置
type User struct { type User struct {
ID string `json:"id"` ID string `json:"id"`
@@ -744,32 +833,38 @@ func (d *Database) EnsureAdminUser() error {
// GetUserByEmail 通过邮箱获取用户 // GetUserByEmail 通过邮箱获取用户
func (d *Database) GetUserByEmail(email string) (*User, error) { func (d *Database) GetUserByEmail(email string) (*User, error) {
var user User var user User
var createdAt, updatedAt string
err := d.db.QueryRow(` err := d.db.QueryRow(`
SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at
FROM users WHERE email = ? FROM users WHERE email = ?
`, email).Scan( `, email).Scan(
&user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret, &user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret,
&user.OTPVerified, &user.CreatedAt, &user.UpdatedAt, &user.OTPVerified, &createdAt, &updatedAt,
) )
if err != nil { if err != nil {
return nil, err 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 return &user, nil
} }
// GetUserByID 通过ID获取用户 // GetUserByID 通过ID获取用户
func (d *Database) GetUserByID(userID string) (*User, error) { func (d *Database) GetUserByID(userID string) (*User, error) {
var user User var user User
var createdAt, updatedAt string
err := d.db.QueryRow(` err := d.db.QueryRow(`
SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at
FROM users WHERE id = ? FROM users WHERE id = ?
`, userID).Scan( `, userID).Scan(
&user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret, &user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret,
&user.OTPVerified, &user.CreatedAt, &user.UpdatedAt, &user.OTPVerified, &createdAt, &updatedAt,
) )
if err != nil { if err != nil {
return nil, err 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 return &user, nil
} }
@@ -826,14 +921,18 @@ func (d *Database) GetAIModels(userID string) ([]*AIModelConfig, error) {
models := make([]*AIModelConfig, 0) models := make([]*AIModelConfig, 0)
for rows.Next() { for rows.Next() {
var model AIModelConfig var model AIModelConfig
var createdAt, updatedAt string
err := rows.Scan( err := rows.Scan(
&model.ID, &model.UserID, &model.Name, &model.Provider, &model.ID, &model.UserID, &model.Name, &model.Provider,
&model.Enabled, &model.APIKey, &model.CustomAPIURL, &model.CustomModelName, &model.Enabled, &model.APIKey, &model.CustomAPIURL, &model.CustomModelName,
&model.CreatedAt, &model.UpdatedAt, &createdAt, &updatedAt,
) )
if err != nil { if err != nil {
return nil, err 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 // 解密API Key
model.APIKey = d.decryptSensitiveData(model.APIKey) model.APIKey = d.decryptSensitiveData(model.APIKey)
models = append(models, &model) models = append(models, &model)
@@ -861,6 +960,7 @@ func (d *Database) GetAIModel(userID, modelID string) (*AIModelConfig, error) {
for _, uid := range candidates { for _, uid := range candidates {
var model AIModelConfig var model AIModelConfig
var createdAt, updatedAt string
err := d.db.QueryRow(` err := d.db.QueryRow(`
SELECT id, user_id, name, provider, enabled, api_key, SELECT id, user_id, name, provider, enabled, api_key,
COALESCE(custom_api_url, ''), COALESCE(custom_model_name, ''), created_at, updated_at 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.APIKey,
&model.CustomAPIURL, &model.CustomAPIURL,
&model.CustomModelName, &model.CustomModelName,
&model.CreatedAt, &createdAt,
&model.UpdatedAt, &updatedAt,
) )
if err == nil { 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 行为保持一致) // 解密API Key与 GetAIModels 行为保持一致)
model.APIKey = d.decryptSensitiveData(model.APIKey) model.APIKey = d.decryptSensitiveData(model.APIKey)
return &model, nil return &model, nil
@@ -912,6 +1015,7 @@ func (d *Database) GetDefaultAIModel(userID string) (*AIModelConfig, error) {
func (d *Database) firstEnabledAIModel(userID string) (*AIModelConfig, error) { func (d *Database) firstEnabledAIModel(userID string) (*AIModelConfig, error) {
var model AIModelConfig var model AIModelConfig
var createdAt, updatedAt string
err := d.db.QueryRow(` err := d.db.QueryRow(`
SELECT id, user_id, name, provider, enabled, api_key, SELECT id, user_id, name, provider, enabled, api_key,
COALESCE(custom_api_url, ''), COALESCE(custom_model_name, ''), created_at, updated_at 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.APIKey,
&model.CustomAPIURL, &model.CustomAPIURL,
&model.CustomModelName, &model.CustomModelName,
&model.CreatedAt, &createdAt,
&model.UpdatedAt, &updatedAt,
) )
if err != nil { if err != nil {
return nil, err 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避免上层拿到加密串导致下游认证失败 // 解密API Key避免上层拿到加密串导致下游认证失败
model.APIKey = d.decryptSensitiveData(model.APIKey) model.APIKey = d.decryptSensitiveData(model.APIKey)
return &model, nil return &model, nil
@@ -1033,6 +1140,7 @@ func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) {
COALESCE(aster_private_key, '') as aster_private_key, COALESCE(aster_private_key, '') as aster_private_key,
COALESCE(lighter_wallet_addr, '') as lighter_wallet_addr, COALESCE(lighter_wallet_addr, '') as lighter_wallet_addr,
COALESCE(lighter_private_key, '') as lighter_private_key, COALESCE(lighter_private_key, '') as lighter_private_key,
COALESCE(lighter_api_key_private_key, '') as lighter_api_key_private_key,
created_at, updated_at created_at, updated_at
FROM exchanges WHERE user_id = ? ORDER BY id FROM exchanges WHERE user_id = ? ORDER BY id
`, userID) `, userID)
@@ -1045,23 +1153,30 @@ func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) {
exchanges := make([]*ExchangeConfig, 0) exchanges := make([]*ExchangeConfig, 0)
for rows.Next() { for rows.Next() {
var exchange ExchangeConfig var exchange ExchangeConfig
var createdAt, updatedAt string
err := rows.Scan( err := rows.Scan(
&exchange.ID, &exchange.UserID, &exchange.Name, &exchange.Type, &exchange.ID, &exchange.UserID, &exchange.Name, &exchange.Type,
&exchange.Enabled, &exchange.APIKey, &exchange.SecretKey, &exchange.Testnet, &exchange.Enabled, &exchange.APIKey, &exchange.SecretKey, &exchange.Testnet,
&exchange.HyperliquidWalletAddr, &exchange.AsterUser, &exchange.HyperliquidWalletAddr, &exchange.AsterUser,
&exchange.AsterSigner, &exchange.AsterPrivateKey, &exchange.AsterSigner, &exchange.AsterPrivateKey,
&exchange.LighterWalletAddr, &exchange.LighterPrivateKey, &exchange.LighterWalletAddr, &exchange.LighterPrivateKey,
&exchange.CreatedAt, &exchange.UpdatedAt, &exchange.LighterAPIKeyPrivateKey,
&createdAt, &updatedAt,
) )
if err != nil { if err != nil {
return nil, err 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.APIKey = d.decryptSensitiveData(exchange.APIKey)
exchange.SecretKey = d.decryptSensitiveData(exchange.SecretKey) exchange.SecretKey = d.decryptSensitiveData(exchange.SecretKey)
exchange.AsterPrivateKey = d.decryptSensitiveData(exchange.AsterPrivateKey) exchange.AsterPrivateKey = d.decryptSensitiveData(exchange.AsterPrivateKey)
exchange.LighterPrivateKey = d.decryptSensitiveData(exchange.LighterPrivateKey) exchange.LighterPrivateKey = d.decryptSensitiveData(exchange.LighterPrivateKey)
exchange.LighterAPIKeyPrivateKey = d.decryptSensitiveData(exchange.LighterAPIKeyPrivateKey)
exchanges = append(exchanges, &exchange) exchanges = append(exchanges, &exchange)
} }
@@ -1243,6 +1358,7 @@ func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) {
var traders []*TraderRecord var traders []*TraderRecord
for rows.Next() { for rows.Next() {
var trader TraderRecord var trader TraderRecord
var createdAt, updatedAt string
err := rows.Scan( err := rows.Scan(
&trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID, &trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID,
&trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning, &trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning,
@@ -1250,11 +1366,14 @@ func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) {
&trader.UseCoinPool, &trader.UseOITop, &trader.UseCoinPool, &trader.UseOITop,
&trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate, &trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate,
&trader.IsCrossMargin, &trader.IsCrossMargin,
&trader.CreatedAt, &trader.UpdatedAt, &createdAt, &updatedAt,
) )
if err != nil { if err != nil {
return nil, err 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) traders = append(traders, &trader)
} }
@@ -1307,6 +1426,9 @@ func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIM
var trader TraderRecord var trader TraderRecord
var aiModel AIModelConfig var aiModel AIModelConfig
var exchange ExchangeConfig var exchange ExchangeConfig
var traderCreatedAt, traderUpdatedAt string
var aiModelCreatedAt, aiModelUpdatedAt string
var exchangeCreatedAt, exchangeUpdatedAt string
err := d.db.QueryRow(` err := d.db.QueryRow(`
SELECT 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.aster_private_key, '') as aster_private_key,
COALESCE(e.lighter_wallet_addr, '') as lighter_wallet_addr, COALESCE(e.lighter_wallet_addr, '') as lighter_wallet_addr,
COALESCE(e.lighter_private_key, '') as lighter_private_key, 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 e.created_at, e.updated_at
FROM traders t FROM traders t
JOIN ai_models a ON t.ai_model_id = a.id AND t.user_id = a.user_id 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.UseCoinPool, &trader.UseOITop,
&trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate, &trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate,
&trader.IsCrossMargin, &trader.IsCrossMargin,
&trader.CreatedAt, &trader.UpdatedAt, &traderCreatedAt, &traderUpdatedAt,
&aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey, &aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey,
&aiModel.CustomAPIURL, &aiModel.CustomModelName, &aiModel.CustomAPIURL, &aiModel.CustomModelName,
&aiModel.CreatedAt, &aiModel.UpdatedAt, &aiModelCreatedAt, &aiModelUpdatedAt,
&exchange.ID, &exchange.UserID, &exchange.Name, &exchange.Type, &exchange.Enabled, &exchange.ID, &exchange.UserID, &exchange.Name, &exchange.Type, &exchange.Enabled,
&exchange.APIKey, &exchange.SecretKey, &exchange.Testnet, &exchange.APIKey, &exchange.SecretKey, &exchange.Testnet,
&exchange.HyperliquidWalletAddr, &exchange.AsterUser, &exchange.AsterSigner, &exchange.AsterPrivateKey, &exchange.HyperliquidWalletAddr, &exchange.AsterUser, &exchange.AsterSigner, &exchange.AsterPrivateKey,
&exchange.LighterWalletAddr, &exchange.LighterPrivateKey, &exchange.LighterWalletAddr, &exchange.LighterPrivateKey, &exchange.LighterAPIKeyPrivateKey,
&exchange.CreatedAt, &exchange.UpdatedAt, &exchangeCreatedAt, &exchangeUpdatedAt,
) )
if err != nil { if err != nil {
return nil, nil, nil, err 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) aiModel.APIKey = d.decryptSensitiveData(aiModel.APIKey)
exchange.APIKey = d.decryptSensitiveData(exchange.APIKey) exchange.APIKey = d.decryptSensitiveData(exchange.APIKey)
exchange.SecretKey = d.decryptSensitiveData(exchange.SecretKey) exchange.SecretKey = d.decryptSensitiveData(exchange.SecretKey)
exchange.AsterPrivateKey = d.decryptSensitiveData(exchange.AsterPrivateKey) exchange.AsterPrivateKey = d.decryptSensitiveData(exchange.AsterPrivateKey)
exchange.LighterPrivateKey = d.decryptSensitiveData(exchange.LighterPrivateKey) exchange.LighterPrivateKey = d.decryptSensitiveData(exchange.LighterPrivateKey)
exchange.LighterAPIKeyPrivateKey = d.decryptSensitiveData(exchange.LighterAPIKeyPrivateKey)
return &trader, &aiModel, &exchange, nil return &trader, &aiModel, &exchange, nil
} }
@@ -1396,16 +1528,19 @@ func (d *Database) CreateUserSignalSource(userID, coinPoolURL, oiTopURL string)
// GetUserSignalSource 获取用户信号源配置 // GetUserSignalSource 获取用户信号源配置
func (d *Database) GetUserSignalSource(userID string) (*UserSignalSource, error) { func (d *Database) GetUserSignalSource(userID string) (*UserSignalSource, error) {
var source UserSignalSource var source UserSignalSource
var createdAt, updatedAt string
err := d.db.QueryRow(` err := d.db.QueryRow(`
SELECT id, user_id, coin_pool_url, oi_top_url, created_at, updated_at SELECT id, user_id, coin_pool_url, oi_top_url, created_at, updated_at
FROM user_signal_sources WHERE user_id = ? FROM user_signal_sources WHERE user_id = ?
`, userID).Scan( `, userID).Scan(
&source.ID, &source.UserID, &source.CoinPoolURL, &source.OITopURL, &source.ID, &source.UserID, &source.CoinPoolURL, &source.OITopURL,
&source.CreatedAt, &source.UpdatedAt, &createdAt, &updatedAt,
) )
if err != nil { if err != nil {
return nil, err 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 return &source, nil
} }

View File

@@ -13,6 +13,7 @@ import HeaderBar from './components/HeaderBar'
import AILearning from './components/AILearning' import AILearning from './components/AILearning'
import { LanguageProvider, useLanguage } from './contexts/LanguageContext' import { LanguageProvider, useLanguage } from './contexts/LanguageContext'
import { AuthProvider, useAuth } from './contexts/AuthContext' import { AuthProvider, useAuth } from './contexts/AuthContext'
import { ConfirmDialogProvider } from './components/ConfirmDialog'
import { t, type Language } from './i18n/translations' import { t, type Language } from './i18n/translations'
import { useSystemConfig } from './hooks/useSystemConfig' import { useSystemConfig } from './hooks/useSystemConfig'
import { DecisionCard } from './components/DecisionCard' import { DecisionCard } from './components/DecisionCard'
@@ -1062,7 +1063,9 @@ export default function AppWithProviders() {
return ( return (
<LanguageProvider> <LanguageProvider>
<AuthProvider> <AuthProvider>
<App /> <ConfirmDialogProvider>
<App />
</ConfirmDialogProvider>
</AuthProvider> </AuthProvider>
</LanguageProvider> </LanguageProvider>
) )