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 7eebb4e218
commit b71272d48b
3 changed files with 407 additions and 282 deletions

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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 (
<LanguageProvider>
<AuthProvider>
<App />
<ConfirmDialogProvider>
<App />
</ConfirmDialogProvider>
</AuthProvider>
</LanguageProvider>
)