diff --git a/.env.example b/.env.example index bcff8c82..50ad92dd 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,18 @@ # NOFX Environment Variables Template # Copy this file to .env and modify the values as needed +# PostgreSQL数据库配置 +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +POSTGRES_DB=nofx +POSTGRES_USER=nofx +POSTGRES_PASSWORD=nofx123456 + +# Redis配置 +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD=redis123456 + # Ports Configuration # Backend API server port (internal: 8080, external: configurable) NOFX_BACKEND_PORT=8080 diff --git a/api/server.go b/api/server.go index 94ae4a60..b196a297 100644 --- a/api/server.go +++ b/api/server.go @@ -21,12 +21,12 @@ import ( type Server struct { router *gin.Engine traderManager *manager.TraderManager - database *config.Database + database config.DatabaseInterface port int } // NewServer 创建API服务器 -func NewServer(traderManager *manager.TraderManager, database *config.Database, port int) *Server { +func NewServer(traderManager *manager.TraderManager, database config.DatabaseInterface, port int) *Server { // 设置为Release模式(减少日志输出) gin.SetMode(gin.ReleaseMode) diff --git a/config/database.go b/config/database.go index 719fd07f..932982b4 100644 --- a/config/database.go +++ b/config/database.go @@ -13,6 +13,7 @@ import ( "strings" "time" + _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" ) @@ -21,8 +22,53 @@ type Database struct { db *sql.DB } +// DatabaseInterface 数据库接口 +type DatabaseInterface interface { + CreateUser(user *User) error + EnsureAdminUser() error + GetUserByEmail(email string) (*User, error) + GetUserByID(userID string) (*User, error) + GetAllUsers() ([]string, error) + UpdateUserOTPVerified(userID string, verified bool) error + GetAIModels(userID string) ([]*AIModelConfig, error) + UpdateAIModel(userID, id string, enabled bool, apiKey, customAPIURL, customModelName string) error + GetExchanges(userID string) ([]*ExchangeConfig, error) + UpdateExchange(userID, id string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error + CreateAIModel(userID, id, name, provider string, enabled bool, apiKey, customAPIURL string) error + CreateExchange(userID, id, name, typ string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error + CreateTrader(trader *TraderRecord) error + GetTraders(userID string) ([]*TraderRecord, error) + UpdateTraderStatus(userID, id string, isRunning bool) error + UpdateTrader(trader *TraderRecord) error + UpdateTraderCustomPrompt(userID, id string, customPrompt string, overrideBase bool) error + DeleteTrader(userID, id string) error + GetTraderConfig(userID, traderID string) (*TraderRecord, *AIModelConfig, *ExchangeConfig, error) + GetSystemConfig(key string) (string, error) + SetSystemConfig(key, value string) error + CreateUserSignalSource(userID, coinPoolURL, oiTopURL string) error + GetUserSignalSource(userID string) (*UserSignalSource, error) + UpdateUserSignalSource(userID, coinPoolURL, oiTopURL string) error + GetCustomCoins() []string + LoadBetaCodesFromFile(filePath string) error + ValidateBetaCode(code string) (bool, error) + UseBetaCode(code, userEmail string) error + GetBetaCodeStats() (total, used int, err error) + Close() error +} + // NewDatabase 创建配置数据库 -func NewDatabase(dbPath string) (*Database, error) { +func NewDatabase(dbPath string) (DatabaseInterface, error) { + // 检查是否启用PostgreSQL + if os.Getenv("POSTGRES_HOST") != "" { + // 使用PostgreSQL + pgDB, err := NewPostgreSQLDatabase() + if err != nil { + return nil, fmt.Errorf("创建PostgreSQL数据库失败: %w", err) + } + return pgDB, nil + } + + // 使用SQLite(兼容模式) db, err := sql.Open("sqlite3", dbPath) if err != nil { return nil, fmt.Errorf("打开数据库失败: %w", err) diff --git a/config/database_pg.go b/config/database_pg.go new file mode 100644 index 00000000..b8dd560f --- /dev/null +++ b/config/database_pg.go @@ -0,0 +1,701 @@ +package config + +import ( + "database/sql" + "encoding/json" + "fmt" + "log" + "nofx/market" + "os" + "slices" + "strings" + "time" + + _ "github.com/lib/pq" +) + +// PostgreSQLDatabase PostgreSQL数据库配置 +type PostgreSQLDatabase struct { + db *sql.DB +} + +// NewPostgreSQLDatabase 创建PostgreSQL数据库连接 +func NewPostgreSQLDatabase() (*PostgreSQLDatabase, error) { + // 从环境变量获取数据库连接信息 + host := getEnv("POSTGRES_HOST", "localhost") + port := getEnv("POSTGRES_PORT", "5432") + dbname := getEnv("POSTGRES_DB", "nofx") + user := getEnv("POSTGRES_USER", "nofx") + password := getEnv("POSTGRES_PASSWORD", "nofx123456") + + // 构建连接字符串 + dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + host, port, user, password, dbname) + + log.Printf("📋 连接PostgreSQL数据库: %s:%s/%s", host, port, dbname) + + db, err := sql.Open("postgres", dsn) + if err != nil { + return nil, fmt.Errorf("打开PostgreSQL数据库失败: %w", err) + } + + // 测试连接 + if err := db.Ping(); err != nil { + return nil, fmt.Errorf("连接PostgreSQL数据库失败: %w", err) + } + + // 设置连接池参数 + db.SetMaxOpenConns(25) + db.SetMaxIdleConns(5) + db.SetConnMaxLifetime(time.Hour) + + database := &PostgreSQLDatabase{db: db} + log.Printf("✅ PostgreSQL数据库连接成功") + + return database, nil +} + +// getEnv 获取环境变量,如果不存在返回默认值 +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +// CreateUser 创建用户 +func (d *PostgreSQLDatabase) CreateUser(user *User) error { + _, err := d.db.Exec(` + INSERT INTO users (id, email, password_hash, otp_secret, otp_verified) + VALUES ($1, $2, $3, $4, $5) + `, user.ID, user.Email, user.PasswordHash, user.OTPSecret, user.OTPVerified) + return err +} + +// EnsureAdminUser 确保admin用户存在(用于管理员模式) +func (d *PostgreSQLDatabase) EnsureAdminUser() error { + // 检查admin用户是否已存在 + var count int + err := d.db.QueryRow(`SELECT COUNT(*) FROM users WHERE id = 'admin'`).Scan(&count) + if err != nil { + return err + } + + // 如果已存在,直接返回 + if count > 0 { + return nil + } + + // 创建admin用户(密码为空,因为管理员模式下不需要密码) + adminUser := &User{ + ID: "admin", + Email: "admin@localhost", + PasswordHash: "", // 管理员模式下不使用密码 + OTPSecret: "", + OTPVerified: true, + } + + return d.CreateUser(adminUser) +} + +// GetUserByEmail 通过邮箱获取用户 +func (d *PostgreSQLDatabase) GetUserByEmail(email string) (*User, error) { + var user User + err := d.db.QueryRow(` + SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at + FROM users WHERE email = $1 + `, email).Scan( + &user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret, + &user.OTPVerified, &user.CreatedAt, &user.UpdatedAt, + ) + if err != nil { + return nil, err + } + return &user, nil +} + +// GetUserByID 通过ID获取用户 +func (d *PostgreSQLDatabase) GetUserByID(userID string) (*User, error) { + var user User + err := d.db.QueryRow(` + SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at + FROM users WHERE id = $1 + `, userID).Scan( + &user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret, + &user.OTPVerified, &user.CreatedAt, &user.UpdatedAt, + ) + if err != nil { + return nil, err + } + return &user, nil +} + +// GetAllUsers 获取所有用户ID列表 +func (d *PostgreSQLDatabase) GetAllUsers() ([]string, error) { + rows, err := d.db.Query(`SELECT id FROM users ORDER BY id`) + if err != nil { + return nil, err + } + defer rows.Close() + + var userIDs []string + for rows.Next() { + var userID string + if err := rows.Scan(&userID); err != nil { + return nil, err + } + userIDs = append(userIDs, userID) + } + return userIDs, nil +} + +// UpdateUserOTPVerified 更新用户OTP验证状态 +func (d *PostgreSQLDatabase) UpdateUserOTPVerified(userID string, verified bool) error { + _, err := d.db.Exec(`UPDATE users SET otp_verified = $1 WHERE id = $2`, verified, userID) + return err +} + +// GetAIModels 获取用户的AI模型配置 +func (d *PostgreSQLDatabase) GetAIModels(userID string) ([]*AIModelConfig, error) { + rows, err := d.db.Query(` + SELECT id, user_id, name, provider, enabled, api_key, + COALESCE(custom_api_url, '') as custom_api_url, + COALESCE(custom_model_name, '') as custom_model_name, + created_at, updated_at + FROM ai_models WHERE user_id = $1 ORDER BY id + `, userID) + if err != nil { + return nil, err + } + defer rows.Close() + + // 初始化为空切片而不是nil,确保JSON序列化为[]而不是null + models := make([]*AIModelConfig, 0) + for rows.Next() { + var model AIModelConfig + err := rows.Scan( + &model.ID, &model.UserID, &model.Name, &model.Provider, + &model.Enabled, &model.APIKey, &model.CustomAPIURL, &model.CustomModelName, + &model.CreatedAt, &model.UpdatedAt, + ) + if err != nil { + return nil, err + } + models = append(models, &model) + } + + return models, nil +} + +// UpdateAIModel 更新AI模型配置,如果不存在则创建用户特定配置 +func (d *PostgreSQLDatabase) UpdateAIModel(userID, id string, enabled bool, apiKey, customAPIURL, customModelName string) error { + // 先尝试精确匹配 ID(新版逻辑,支持多个相同 provider 的模型) + var existingID string + err := d.db.QueryRow(` + SELECT id FROM ai_models WHERE user_id = $1 AND id = $2 LIMIT 1 + `, userID, id).Scan(&existingID) + + if err == nil { + // 找到了现有配置(精确匹配 ID),更新它 + _, err = d.db.Exec(` + UPDATE ai_models SET enabled = $1, api_key = $2, custom_api_url = $3, custom_model_name = $4, updated_at = CURRENT_TIMESTAMP + WHERE id = $5 AND user_id = $6 + `, enabled, apiKey, customAPIURL, customModelName, existingID, userID) + return err + } + + // ID 不存在,尝试兼容旧逻辑:将 id 作为 provider 查找 + provider := id + err = d.db.QueryRow(` + SELECT id FROM ai_models WHERE user_id = $1 AND provider = $2 LIMIT 1 + `, userID, provider).Scan(&existingID) + + if err == nil { + // 找到了现有配置(通过 provider 匹配,兼容旧版),更新它 + log.Printf("⚠️ 使用旧版 provider 匹配更新模型: %s -> %s", provider, existingID) + _, err = d.db.Exec(` + UPDATE ai_models SET enabled = $1, api_key = $2, custom_api_url = $3, custom_model_name = $4, updated_at = CURRENT_TIMESTAMP + WHERE id = $5 AND user_id = $6 + `, enabled, apiKey, customAPIURL, customModelName, existingID, userID) + return err + } + + // 没有找到任何现有配置,创建新的 + // 推断 provider(从 id 中提取,或者直接使用 id) + if provider == id && (provider == "deepseek" || provider == "qwen") { + // id 本身就是 provider + provider = id + } else { + // 从 id 中提取 provider(假设格式是 userID_provider 或 timestamp_userID_provider) + parts := strings.Split(id, "_") + if len(parts) >= 2 { + provider = parts[len(parts)-1] // 取最后一部分作为 provider + } else { + provider = id + } + } + + // 获取模型的基本信息 + var name string + err = d.db.QueryRow(` + SELECT name FROM ai_models WHERE provider = $1 LIMIT 1 + `, provider).Scan(&name) + if err != nil { + // 如果找不到基本信息,使用默认值 + if provider == "deepseek" { + name = "DeepSeek AI" + } else if provider == "qwen" { + name = "Qwen AI" + } else { + name = provider + " AI" + } + } + + // 如果传入的 ID 已经是完整格式(如 "admin_deepseek_custom1"),直接使用 + // 否则生成新的 ID + newModelID := id + if id == provider { + // id 就是 provider,生成新的用户特定 ID + newModelID = fmt.Sprintf("%s_%s", userID, provider) + } + + log.Printf("✓ 创建新的 AI 模型配置: ID=%s, Provider=%s, Name=%s", newModelID, provider, name) + _, err = d.db.Exec(` + INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `, newModelID, userID, name, provider, enabled, apiKey, customAPIURL, customModelName) + + return err +} + +// GetExchanges 获取用户的交易所配置 +func (d *PostgreSQLDatabase) GetExchanges(userID string) ([]*ExchangeConfig, error) { + rows, err := d.db.Query(` + SELECT id, user_id, name, type, enabled, api_key, secret_key, testnet, + COALESCE(hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr, + COALESCE(aster_user, '') as aster_user, + COALESCE(aster_signer, '') as aster_signer, + COALESCE(aster_private_key, '') as aster_private_key, + created_at, updated_at + FROM exchanges WHERE user_id = $1 ORDER BY id + `, userID) + if err != nil { + return nil, err + } + defer rows.Close() + + // 初始化为空切片而不是nil,确保JSON序列化为[]而不是null + exchanges := make([]*ExchangeConfig, 0) + for rows.Next() { + var exchange ExchangeConfig + 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.CreatedAt, &exchange.UpdatedAt, + ) + if err != nil { + return nil, err + } + exchanges = append(exchanges, &exchange) + } + + return exchanges, nil +} + +// UpdateExchange 更新交易所配置,如果不存在则创建用户特定配置 +func (d *PostgreSQLDatabase) UpdateExchange(userID, id string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error { + log.Printf("🔧 UpdateExchange: userID=%s, id=%s, enabled=%v", userID, id, enabled) + + // 首先尝试更新现有的用户配置 + result, err := d.db.Exec(` + UPDATE exchanges SET enabled = $1, api_key = $2, secret_key = $3, testnet = $4, + hyperliquid_wallet_addr = $5, aster_user = $6, aster_signer = $7, aster_private_key = $8, updated_at = CURRENT_TIMESTAMP + WHERE id = $9 AND user_id = $10 + `, enabled, apiKey, secretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, id, userID) + if err != nil { + log.Printf("❌ UpdateExchange: 更新失败: %v", err) + return err + } + + // 检查是否有行被更新 + rowsAffected, err := result.RowsAffected() + if err != nil { + log.Printf("❌ UpdateExchange: 获取影响行数失败: %v", err) + return err + } + + log.Printf("📊 UpdateExchange: 影响行数 = %d", rowsAffected) + + // 如果没有行被更新,说明用户没有这个交易所的配置,需要创建 + if rowsAffected == 0 { + log.Printf("💡 UpdateExchange: 没有现有记录,创建新记录") + + // 根据交易所ID确定基本信息 + var name, typ string + if id == "binance" { + name = "Binance Futures" + typ = "cex" + } else if id == "hyperliquid" { + name = "Hyperliquid" + typ = "dex" + } else if id == "aster" { + name = "Aster DEX" + typ = "dex" + } else { + name = id + " Exchange" + typ = "cex" + } + + log.Printf("🆕 UpdateExchange: 创建新记录 ID=%s, name=%s, type=%s", id, name, typ) + + // 创建用户特定的配置,使用原始的交易所ID + _, err = d.db.Exec(` + INSERT INTO exchanges (id, user_id, name, type, enabled, api_key, secret_key, testnet, + hyperliquid_wallet_addr, aster_user, aster_signer, aster_private_key, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `, id, userID, name, typ, enabled, apiKey, secretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey) + + if err != nil { + log.Printf("❌ UpdateExchange: 创建记录失败: %v", err) + } else { + log.Printf("✅ UpdateExchange: 创建记录成功") + } + return err + } + + log.Printf("✅ UpdateExchange: 更新现有记录成功") + return nil +} + +// CreateAIModel 创建AI模型配置 +func (d *PostgreSQLDatabase) CreateAIModel(userID, id, name, provider string, enabled bool, apiKey, customAPIURL string) error { + _, err := d.db.Exec(` + INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (id) DO NOTHING + `, id, userID, name, provider, enabled, apiKey, customAPIURL) + return err +} + +// CreateExchange 创建交易所配置 +func (d *PostgreSQLDatabase) CreateExchange(userID, id, name, typ string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error { + _, err := d.db.Exec(` + INSERT INTO exchanges (id, user_id, name, type, enabled, api_key, secret_key, testnet, hyperliquid_wallet_addr, aster_user, aster_signer, aster_private_key) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + ON CONFLICT (id, user_id) DO NOTHING + `, id, userID, name, typ, enabled, apiKey, secretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey) + return err +} + +// CreateTrader 创建交易员 +func (d *PostgreSQLDatabase) CreateTrader(trader *TraderRecord) error { + _, err := d.db.Exec(` + INSERT INTO traders (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) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) + `, trader.ID, trader.UserID, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance, trader.ScanIntervalMinutes, trader.IsRunning, trader.BTCETHLeverage, trader.AltcoinLeverage, trader.TradingSymbols, trader.UseCoinPool, trader.UseOITop, trader.CustomPrompt, trader.OverrideBasePrompt, trader.SystemPromptTemplate, trader.IsCrossMargin) + return err +} + +// GetTraders 获取用户的交易员 +func (d *PostgreSQLDatabase) GetTraders(userID string) ([]*TraderRecord, error) { + rows, err := d.db.Query(` + SELECT id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, + COALESCE(btc_eth_leverage, 5) as btc_eth_leverage, COALESCE(altcoin_leverage, 5) as altcoin_leverage, + COALESCE(trading_symbols, '') as trading_symbols, + COALESCE(use_coin_pool, false) as use_coin_pool, COALESCE(use_oi_top, false) as use_oi_top, + COALESCE(custom_prompt, '') as custom_prompt, COALESCE(override_base_prompt, false) as override_base_prompt, + COALESCE(system_prompt_template, 'default') as system_prompt_template, + COALESCE(is_cross_margin, true) as is_cross_margin, created_at, updated_at + FROM traders WHERE user_id = $1 ORDER BY created_at DESC + `, userID) + if err != nil { + return nil, err + } + defer rows.Close() + + var traders []*TraderRecord + for rows.Next() { + var trader TraderRecord + err := rows.Scan( + &trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID, + &trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning, + &trader.BTCETHLeverage, &trader.AltcoinLeverage, &trader.TradingSymbols, + &trader.UseCoinPool, &trader.UseOITop, + &trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate, + &trader.IsCrossMargin, + &trader.CreatedAt, &trader.UpdatedAt, + ) + if err != nil { + return nil, err + } + traders = append(traders, &trader) + } + + return traders, nil +} + +// UpdateTraderStatus 更新交易员状态 +func (d *PostgreSQLDatabase) UpdateTraderStatus(userID, id string, isRunning bool) error { + _, err := d.db.Exec(`UPDATE traders SET is_running = $1 WHERE id = $2 AND user_id = $3`, isRunning, id, userID) + return err +} + +// UpdateTrader 更新交易员配置 +func (d *PostgreSQLDatabase) UpdateTrader(trader *TraderRecord) error { + _, err := d.db.Exec(` + UPDATE traders SET + name = $1, ai_model_id = $2, exchange_id = $3, initial_balance = $4, + scan_interval_minutes = $5, btc_eth_leverage = $6, altcoin_leverage = $7, + trading_symbols = $8, custom_prompt = $9, override_base_prompt = $10, + system_prompt_template = $11, is_cross_margin = $12, updated_at = CURRENT_TIMESTAMP + WHERE id = $13 AND user_id = $14 + `, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance, + trader.ScanIntervalMinutes, trader.BTCETHLeverage, trader.AltcoinLeverage, + trader.TradingSymbols, trader.CustomPrompt, trader.OverrideBasePrompt, + trader.SystemPromptTemplate, trader.IsCrossMargin, trader.ID, trader.UserID) + return err +} + +// UpdateTraderCustomPrompt 更新交易员自定义Prompt +func (d *PostgreSQLDatabase) UpdateTraderCustomPrompt(userID, id string, customPrompt string, overrideBase bool) error { + _, err := d.db.Exec(`UPDATE traders SET custom_prompt = $1, override_base_prompt = $2 WHERE id = $3 AND user_id = $4`, customPrompt, overrideBase, id, userID) + return err +} + +// DeleteTrader 删除交易员 +func (d *PostgreSQLDatabase) DeleteTrader(userID, id string) error { + _, err := d.db.Exec(`DELETE FROM traders WHERE id = $1 AND user_id = $2`, id, userID) + return err +} + +// GetTraderConfig 获取交易员完整配置(包含AI模型和交易所信息) +func (d *PostgreSQLDatabase) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIModelConfig, *ExchangeConfig, error) { + var trader TraderRecord + var aiModel AIModelConfig + var exchange ExchangeConfig + + err := d.db.QueryRow(` + SELECT + t.id, t.user_id, t.name, t.ai_model_id, t.exchange_id, t.initial_balance, t.scan_interval_minutes, t.is_running, t.created_at, t.updated_at, + a.id, a.user_id, a.name, a.provider, a.enabled, a.api_key, a.created_at, a.updated_at, + e.id, e.user_id, e.name, e.type, e.enabled, e.api_key, e.secret_key, e.testnet, + COALESCE(e.hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr, + COALESCE(e.aster_user, '') as aster_user, + COALESCE(e.aster_signer, '') as aster_signer, + COALESCE(e.aster_private_key, '') as aster_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 + JOIN exchanges e ON t.exchange_id = e.id AND t.user_id = e.user_id + WHERE t.id = $1 AND t.user_id = $2 + `, traderID, userID).Scan( + &trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID, + &trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning, + &trader.CreatedAt, &trader.UpdatedAt, + &aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey, + &aiModel.CreatedAt, &aiModel.UpdatedAt, + &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.CreatedAt, &exchange.UpdatedAt, + ) + + if err != nil { + return nil, nil, nil, err + } + + return &trader, &aiModel, &exchange, nil +} + +// GetSystemConfig 获取系统配置 +func (d *PostgreSQLDatabase) GetSystemConfig(key string) (string, error) { + var value string + err := d.db.QueryRow(`SELECT value FROM system_config WHERE key = $1`, key).Scan(&value) + return value, err +} + +// SetSystemConfig 设置系统配置 +func (d *PostgreSQLDatabase) SetSystemConfig(key, value string) error { + _, err := d.db.Exec(` + INSERT INTO system_config (key, value) VALUES ($1, $2) + ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = CURRENT_TIMESTAMP + `, key, value) + return err +} + +// CreateUserSignalSource 创建用户信号源配置 +func (d *PostgreSQLDatabase) CreateUserSignalSource(userID, coinPoolURL, oiTopURL string) error { + _, err := d.db.Exec(` + INSERT INTO user_signal_sources (user_id, coin_pool_url, oi_top_url, updated_at) + VALUES ($1, $2, $3, CURRENT_TIMESTAMP) + ON CONFLICT (user_id) DO UPDATE SET + coin_pool_url = $2, oi_top_url = $3, updated_at = CURRENT_TIMESTAMP + `, userID, coinPoolURL, oiTopURL) + return err +} + +// GetUserSignalSource 获取用户信号源配置 +func (d *PostgreSQLDatabase) GetUserSignalSource(userID string) (*UserSignalSource, error) { + var source UserSignalSource + 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 = $1 + `, userID).Scan( + &source.ID, &source.UserID, &source.CoinPoolURL, &source.OITopURL, + &source.CreatedAt, &source.UpdatedAt, + ) + if err != nil { + return nil, err + } + return &source, nil +} + +// UpdateUserSignalSource 更新用户信号源配置 +func (d *PostgreSQLDatabase) UpdateUserSignalSource(userID, coinPoolURL, oiTopURL string) error { + _, err := d.db.Exec(` + UPDATE user_signal_sources SET coin_pool_url = $1, oi_top_url = $2, updated_at = CURRENT_TIMESTAMP + WHERE user_id = $3 + `, coinPoolURL, oiTopURL, userID) + return err +} + +// GetCustomCoins 获取所有交易员自定义币种 +func (d *PostgreSQLDatabase) GetCustomCoins() []string { + var symbol string + var symbols []string + + err := d.db.QueryRow(` + SELECT STRING_AGG(custom_coins, ',') as symbol + FROM traders WHERE custom_coins != '' + `).Scan(&symbol) + + // 检测用户是否未配置币种 - 兼容性 + if err != nil || symbol == "" { + symbolJSON, _ := d.GetSystemConfig("default_coins") + if err := json.Unmarshal([]byte(symbolJSON), &symbols); err != nil { + log.Printf("⚠️ 解析default_coins配置失败: %v,使用硬编码默认值", err) + symbols = []string{"BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT"} + } + } + + // filter Symbol + for _, s := range strings.Split(symbol, ",") { + if s == "" { + continue + } + coin := market.Normalize(s) + if !slices.Contains(symbols, coin) { + symbols = append(symbols, coin) + } + } + return symbols +} + +// LoadBetaCodesFromFile 从文件加载内测码到数据库 +func (d *PostgreSQLDatabase) LoadBetaCodesFromFile(filePath string) error { + // 读取文件内容 + content, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("读取内测码文件失败: %w", err) + } + + // 按行分割内测码 + lines := strings.Split(string(content), "\n") + var codes []string + for _, line := range lines { + code := strings.TrimSpace(line) + if code != "" && !strings.HasPrefix(code, "#") { + codes = append(codes, code) + } + } + + // 批量插入内测码 + tx, err := d.db.Begin() + if err != nil { + return fmt.Errorf("开始事务失败: %w", err) + } + defer tx.Rollback() + + stmt, err := tx.Prepare(`INSERT INTO beta_codes (code) VALUES ($1) ON CONFLICT (code) DO NOTHING`) + if err != nil { + return fmt.Errorf("准备语句失败: %w", err) + } + defer stmt.Close() + + insertedCount := 0 + for _, code := range codes { + result, err := stmt.Exec(code) + if err != nil { + log.Printf("插入内测码 %s 失败: %v", code, err) + continue + } + + if rowsAffected, _ := result.RowsAffected(); rowsAffected > 0 { + insertedCount++ + } + } + + if err := tx.Commit(); err != nil { + return fmt.Errorf("提交事务失败: %w", err) + } + + log.Printf("✅ 成功加载 %d 个内测码到数据库 (总计 %d 个)", insertedCount, len(codes)) + return nil +} + +// ValidateBetaCode 验证内测码是否有效且未使用 +func (d *PostgreSQLDatabase) ValidateBetaCode(code string) (bool, error) { + var used bool + err := d.db.QueryRow(`SELECT used FROM beta_codes WHERE code = $1`, code).Scan(&used) + if err != nil { + if err == sql.ErrNoRows { + return false, nil // 内测码不存在 + } + return false, err + } + return !used, nil // 内测码存在且未使用 +} + +// UseBetaCode 使用内测码(标记为已使用) +func (d *PostgreSQLDatabase) UseBetaCode(code, userEmail string) error { + result, err := d.db.Exec(` + UPDATE beta_codes SET used = true, used_by = $1, used_at = CURRENT_TIMESTAMP + WHERE code = $2 AND used = false + `, userEmail, code) + if err != nil { + return err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return err + } + + if rowsAffected == 0 { + return fmt.Errorf("内测码无效或已被使用") + } + + return nil +} + +// GetBetaCodeStats 获取内测码统计信息 +func (d *PostgreSQLDatabase) GetBetaCodeStats() (total, used int, err error) { + err = d.db.QueryRow(`SELECT COUNT(*) FROM beta_codes`).Scan(&total) + if err != nil { + return 0, 0, err + } + + err = d.db.QueryRow(`SELECT COUNT(*) FROM beta_codes WHERE used = true`).Scan(&used) + if err != nil { + return 0, 0, err + } + + return total, used, nil +} + +// Close 关闭数据库连接 +func (d *PostgreSQLDatabase) Close() error { + return d.db.Close() +} \ No newline at end of file diff --git a/db/init.sql b/db/init.sql new file mode 100644 index 00000000..dbd9a335 --- /dev/null +++ b/db/init.sql @@ -0,0 +1,169 @@ +-- PostgreSQL初始化脚本 +-- AI交易系统数据库迁移 + +-- 用户表 +CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + otp_secret TEXT, + otp_verified BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- AI模型配置表 +CREATE TABLE IF NOT EXISTS ai_models ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL DEFAULT 'default', + name TEXT NOT NULL, + provider TEXT NOT NULL, + enabled BOOLEAN DEFAULT FALSE, + api_key TEXT DEFAULT '', + custom_api_url TEXT DEFAULT '', + custom_model_name TEXT DEFAULT '', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +-- 交易所配置表 +CREATE TABLE IF NOT EXISTS exchanges ( + id TEXT NOT NULL, + user_id TEXT NOT NULL DEFAULT 'default', + name TEXT NOT NULL, + type TEXT NOT NULL, -- 'cex' or 'dex' + enabled BOOLEAN DEFAULT FALSE, + api_key TEXT DEFAULT '', + secret_key TEXT DEFAULT '', + testnet BOOLEAN DEFAULT FALSE, + -- Hyperliquid 特定字段 + hyperliquid_wallet_addr TEXT DEFAULT '', + -- Aster 特定字段 + aster_user TEXT DEFAULT '', + aster_signer TEXT DEFAULT '', + aster_private_key TEXT DEFAULT '', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id, user_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +-- 用户信号源配置表 +CREATE TABLE IF NOT EXISTS user_signal_sources ( + id SERIAL PRIMARY KEY, + user_id TEXT NOT NULL, + coin_pool_url TEXT DEFAULT '', + oi_top_url TEXT DEFAULT '', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE(user_id) +); + +-- 交易员配置表 +CREATE TABLE IF NOT EXISTS traders ( + 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 FALSE, + btc_eth_leverage INTEGER DEFAULT 5, + altcoin_leverage INTEGER DEFAULT 5, + trading_symbols TEXT DEFAULT '', + use_coin_pool BOOLEAN DEFAULT FALSE, + use_oi_top BOOLEAN DEFAULT FALSE, + custom_prompt TEXT DEFAULT '', + override_base_prompt BOOLEAN DEFAULT FALSE, + system_prompt_template TEXT DEFAULT 'default', + is_cross_margin BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP 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, user_id) REFERENCES exchanges(id, user_id) +); + +-- 系统配置表 +CREATE TABLE IF NOT EXISTS system_config ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 内测码表 +CREATE TABLE IF NOT EXISTS beta_codes ( + code TEXT PRIMARY KEY, + used BOOLEAN DEFAULT FALSE, + used_by TEXT DEFAULT '', + used_at TIMESTAMP DEFAULT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 自动更新 updated_at 函数 +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- 创建触发器:自动更新 updated_at +CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_ai_models_updated_at BEFORE UPDATE ON ai_models + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_exchanges_updated_at BEFORE UPDATE ON exchanges + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_traders_updated_at BEFORE UPDATE ON traders + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_user_signal_sources_updated_at BEFORE UPDATE ON user_signal_sources + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_system_config_updated_at BEFORE UPDATE ON system_config + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +-- 插入默认数据 + +-- 初始化AI模型(使用default用户) +INSERT INTO ai_models (id, user_id, name, provider, enabled) VALUES +('deepseek', 'default', 'DeepSeek', 'deepseek', FALSE), +('qwen', 'default', 'Qwen', 'qwen', FALSE) +ON CONFLICT (id) DO NOTHING; + +-- 初始化交易所(使用default用户) +INSERT INTO exchanges (id, user_id, name, type, enabled) VALUES +('binance', 'default', 'Binance Futures', 'binance', FALSE), +('hyperliquid', 'default', 'Hyperliquid', 'hyperliquid', FALSE), +('aster', 'default', 'Aster DEX', 'aster', FALSE) +ON CONFLICT (id, user_id) DO NOTHING; + +-- 初始化系统配置 +INSERT INTO system_config (key, value) VALUES +('admin_mode', 'true'), +('beta_mode', 'false'), +('api_server_port', '8080'), +('use_default_coins', 'true'), +('default_coins', '["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]'), +('max_daily_loss', '10.0'), +('max_drawdown', '20.0'), +('stop_trading_minutes', '60'), +('btc_eth_leverage', '5'), +('altcoin_leverage', '5'), +('jwt_secret', '') +ON CONFLICT (key) DO NOTHING; + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_ai_models_user_id ON ai_models(user_id); +CREATE INDEX IF NOT EXISTS idx_exchanges_user_id ON exchanges(user_id); +CREATE INDEX IF NOT EXISTS idx_traders_user_id ON traders(user_id); +CREATE INDEX IF NOT EXISTS idx_traders_running ON traders(is_running); +CREATE INDEX IF NOT EXISTS idx_beta_codes_used ON beta_codes(used); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index a9d35026..6a60bf54 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,44 @@ services: + # PostgreSQL Database + postgres: + image: postgres:15-alpine + container_name: nofx-postgres + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-nofx} + POSTGRES_USER: ${POSTGRES_USER:-nofx} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-nofx123456} + volumes: + - postgres_data:/var/lib/postgresql/data + - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro + ports: + - "${POSTGRES_PORT:-5433}:5432" + networks: + - nofx-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-nofx}"] + interval: 10s + timeout: 5s + retries: 5 + + # Redis Cache + redis: + image: redis:7-alpine + container_name: nofx-redis + restart: unless-stopped + command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-redis123456} + volumes: + - redis_data:/data + ports: + - "${REDIS_PORT:-6380}:6379" + networks: + - nofx-network + healthcheck: + test: ["CMD", "redis-cli", "--raw", "incr", "ping"] + interval: 10s + timeout: 3s + retries: 5 + # Backend service (API and core logic) nofx: build: @@ -10,13 +50,25 @@ services: - "${NOFX_BACKEND_PORT:-8080}:8080" volumes: - ./config.json:/app/config.json:ro - - ./config.db:/app/config.db - ./beta_codes.txt:/app/beta_codes.txt:ro - ./decision_logs:/app/decision_logs - ./prompts:/app/prompts - /etc/localtime:/etc/localtime:ro # Sync host time environment: - TZ=${NOFX_TIMEZONE:-Asia/Shanghai} # Set timezone + - POSTGRES_HOST=postgres + - POSTGRES_PORT=5432 + - POSTGRES_DB=${POSTGRES_DB:-nofx} + - POSTGRES_USER=${POSTGRES_USER:-nofx} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-nofx123456} + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_PASSWORD=${REDIS_PASSWORD:-redis123456} + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy networks: - nofx-network healthcheck: @@ -48,4 +100,8 @@ services: networks: nofx-network: - driver: bridge \ No newline at end of file + driver: bridge + +volumes: + postgres_data: + redis_data: \ No newline at end of file diff --git a/go.mod b/go.mod index 72291ee0..a9dcea75 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,8 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.0 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 - github.com/mattn/go-sqlite3 v1.14.16 + github.com/lib/pq v1.10.9 + github.com/mattn/go-sqlite3 v1.14.32 github.com/pquerna/otp v1.4.0 github.com/sonirico/go-hyperliquid v0.17.0 golang.org/x/crypto v0.42.0 diff --git a/go.sum b/go.sum index 655fcf92..18fb8d77 100644 --- a/go.sum +++ b/go.sum @@ -107,6 +107,8 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -118,8 +120,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= -github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= diff --git a/main.go b/main.go index 8aa83dde..30c2abc9 100644 --- a/main.go +++ b/main.go @@ -41,7 +41,7 @@ type ConfigFile struct { } // syncConfigToDatabase 从config.json读取配置并同步到数据库 -func syncConfigToDatabase(database *config.Database) error { +func syncConfigToDatabase(database config.DatabaseInterface) error { // 检查config.json是否存在 if _, err := os.Stat("config.json"); os.IsNotExist(err) { log.Printf("📄 config.json不存在,跳过同步") @@ -110,7 +110,7 @@ func syncConfigToDatabase(database *config.Database) error { } // loadBetaCodesToDatabase 加载内测码文件到数据库 -func loadBetaCodesToDatabase(database *config.Database) error { +func loadBetaCodesToDatabase(database config.DatabaseInterface) error { betaCodeFile := "beta_codes.txt" // 检查内测码文件是否存在 diff --git a/manager/trader_manager.go b/manager/trader_manager.go index 4ebcf20b..86c47db8 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -39,7 +39,7 @@ func NewTraderManager() *TraderManager { } // LoadTradersFromDatabase 从数据库加载所有交易员到内存 -func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) error { +func (tm *TraderManager) LoadTradersFromDatabase(database config.DatabaseInterface) error { tm.mu.Lock() defer tm.mu.Unlock() @@ -709,7 +709,7 @@ func containsUserPrefix(traderID string) bool { } // LoadUserTraders 为特定用户加载交易员到内存 -func (tm *TraderManager) LoadUserTraders(database *config.Database, userID string) error { +func (tm *TraderManager) LoadUserTraders(database config.DatabaseInterface, userID string) error { tm.mu.Lock() defer tm.mu.Unlock() diff --git a/migrate_actual_data.sql b/migrate_actual_data.sql new file mode 100644 index 00000000..812594b3 --- /dev/null +++ b/migrate_actual_data.sql @@ -0,0 +1,115 @@ +-- 实际数据迁移脚本 - 从SQLite迁移到PostgreSQL +-- 执行方式: psql -h localhost -p 5433 -U nofx -d nofx -f migrate_actual_data.sql + +-- 首先插入default用户(满足外键约束) +INSERT INTO users (id, email, password_hash, otp_secret, otp_verified, created_at, updated_at) VALUES +('default', 'default@localhost', '', '', true, '2025-11-03 09:09:52', '2025-11-03 09:09:52') +ON CONFLICT (id) DO NOTHING; + +-- 插入AI模型数据(转换布尔值:0->false, 1->true) +INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at) VALUES +('deepseek', 'default', 'DeepSeek', 'deepseek', false, '', '', '', '2025-11-03 09:09:52', '2025-11-03 09:09:52'), +('qwen', 'default', 'Qwen', 'qwen', false, '', '', '', '2025-11-03 09:09:52', '2025-11-03 09:09:52') +ON CONFLICT (id) DO NOTHING; + +-- 插入交易所数据(转换布尔值) +INSERT INTO exchanges (id, user_id, name, type, enabled, api_key, secret_key, testnet, hyperliquid_wallet_addr, aster_user, aster_signer, aster_private_key, created_at, updated_at) VALUES +('binance', 'default', 'Binance Futures', 'binance', false, '', '', false, '', '', '', '', '2025-11-03 09:09:52', '2025-11-03 09:09:52'), +('hyperliquid', 'default', 'Hyperliquid', 'hyperliquid', false, '', '', false, '', '', '', '', '2025-11-03 09:09:52', '2025-11-03 09:09:52'), +('aster', 'default', 'Aster DEX', 'aster', false, '', '', false, '', '', '', '', '2025-11-03 09:09:52', '2025-11-03 09:09:52') +ON CONFLICT (id, user_id) DO NOTHING; + +-- 插入系统配置数据 +INSERT INTO system_config (key, value, updated_at) VALUES +('coin_pool_api_url', '', '2025-11-03 09:09:52'), +('btc_eth_leverage', '5', '2025-11-03 09:09:52'), +('api_server_port', '8080', '2025-11-03 09:09:52'), +('oi_top_api_url', '', '2025-11-03 09:09:52'), +('stop_trading_minutes', '60', '2025-11-03 09:09:52'), +('default_coins', '["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]', '2025-11-03 09:09:52'), +('altcoin_leverage', '5', '2025-11-03 09:09:52'), +('beta_mode', 'true', '2025-11-03 09:09:52'), +('use_default_coins', 'true', '2025-11-03 09:09:52'), +('max_daily_loss', '10.0', '2025-11-03 09:09:52'), +('jwt_secret', 'Qk0kAa+d0iIEzXVHXbNbm+UaN3RNabmWtH8rDWZ5OPf+4GX8pBflAHodfpbipVMyrw1fsDanHsNBjhgbDeK9Jg==', '2025-11-03 09:09:52'), +('admin_mode', 'false', '2025-11-03 09:09:52'), +('max_drawdown', '20.0', '2025-11-03 09:09:52'), +('encryption_public_key', '-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxDsGHRSFXqR2YFoWMNWC +8s0FlVE2KglHjLnm1f+i5yPfuTYkTUbVDu6RZuqLJdvhX+UO0x1XnwFIhZqmEfro +8Myr5+RnItl7QGqWWcKry4ZlPHroMwIK50WJt316KUKVUv7wUMMLoUUq7yctI8V/ +thRX+ZRaErJJU9DWkSqjYOVdc+KwsZnN9WifoYhp6veTKmJ1kJOd6AVtF+KJ/z0R +hFarXjaQ89vf/oUgKahS/BUH7P6jpP+L+7z8G650oygp3Pn66eq+ttcUdc20WiBj +K5eDBUJUUeNmdesqZXBafhJBhsQyilC0+LgI+3laSkGh3odMdY5Mf9lnke9mfX8E +RQIDAQAB +-----END PUBLIC KEY-----', '2025-11-03 09:09:52'), +('encryption_public_key_version', 'mock-v1', '2025-11-03 09:09:52') +ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = EXCLUDED.updated_at; + +-- 插入内测码数据(转换布尔值:0->false, 1->true) +INSERT INTO beta_codes (code, used, used_by, used_at, created_at) VALUES +('2aw4wm', false, '', NULL, '2025-11-03 09:09:52'), +('34cvds', false, '', NULL, '2025-11-03 09:09:52'), +('3f39nc', false, '', NULL, '2025-11-03 09:09:52'), +('3qmg67', false, '', NULL, '2025-11-03 09:09:52'), +('5rjp6k', false, '', NULL, '2025-11-03 09:09:52'), +('65a3e6', false, '', NULL, '2025-11-03 09:09:52'), +('6hzgpr', false, '', NULL, '2025-11-03 09:09:52'), +('6wruwb', false, '', NULL, '2025-11-03 09:09:52'), +('8bdf7a', false, '', NULL, '2025-11-03 09:09:52'), +('8jxnp5', false, '', NULL, '2025-11-03 09:09:52'), +('8xp3xq', false, '', NULL, '2025-11-03 09:09:52'), +('9r5uev', false, '', NULL, '2025-11-03 09:09:52'), +('adbn7p', false, '', NULL, '2025-11-03 09:09:52'), +('azm8y4', false, '', NULL, '2025-11-03 09:09:52'), +('b6tfqu', false, '', NULL, '2025-11-03 09:09:52'), +('bs32f9', false, '', NULL, '2025-11-03 09:09:52'), +('ctz8gn', false, '', NULL, '2025-11-03 09:09:52'), +('d8rmq8', false, '', NULL, '2025-11-03 09:09:52'), +('dmf2yt', false, '', NULL, '2025-11-03 09:09:52'), +('dz7e8d', false, '', NULL, '2025-11-03 09:09:52'), +('e9ptrm', false, '', NULL, '2025-11-03 09:09:52'), +('f25m8s', false, '', NULL, '2025-11-03 09:09:52'), +('feuzgb', false, '', NULL, '2025-11-03 09:09:52'), +('fnd7z7', false, '', NULL, '2025-11-03 09:09:52'), +('h43s95', false, '', NULL, '2025-11-03 09:09:52'), +('hgs7gq', false, '', NULL, '2025-11-03 09:09:52'), +('huhkra', false, '', NULL, '2025-11-03 09:09:52'), +('mhqch4', false, '', NULL, '2025-11-03 09:09:52'), +('mqwkau', false, '', NULL, '2025-11-03 09:09:52'), +('mwfssp', false, '', NULL, '2025-11-03 09:09:52'), +('na7629', false, '', NULL, '2025-11-03 09:09:52'), +('pb5c2n', false, '', NULL, '2025-11-03 09:09:52'), +('q5k6jt', false, '', NULL, '2025-11-03 09:09:52'), +('qrurb8', false, '', NULL, '2025-11-03 09:09:52'), +('rssybm', false, '', NULL, '2025-11-03 09:09:52'), +('s7hbk7', false, '', NULL, '2025-11-03 09:09:52'), +('sj8rus', false, '', NULL, '2025-11-03 09:09:52'), +('sxy53c', false, '', NULL, '2025-11-03 09:09:52'), +('t8fjmk', false, '', NULL, '2025-11-03 09:09:52'), +('udmqcb', false, '', NULL, '2025-11-03 09:09:52'), +('um6xu6', false, '', NULL, '2025-11-03 09:09:52'), +('uzwb4r', false, '', NULL, '2025-11-03 09:09:52'), +('w2uh55', false, '', NULL, '2025-11-03 09:09:52'), +('wejxcq', false, '', NULL, '2025-11-03 09:09:52'), +('wtaama', false, '', NULL, '2025-11-03 09:09:52'), +('x82qvu', false, '', NULL, '2025-11-03 09:09:52'), +('ygg4d4', false, '', NULL, '2025-11-03 09:09:52'), +('yv8hnn', false, '', NULL, '2025-11-03 09:09:52'), +('z9ywv8', false, '', NULL, '2025-11-03 09:09:52'), +('znpa5t', false, '', NULL, '2025-11-03 09:09:52') +ON CONFLICT (code) DO NOTHING; + +-- 数据迁移验证查询 +SELECT 'Migration Summary:' as status; +SELECT 'ai_models' as table_name, COUNT(*) as count FROM ai_models +UNION ALL +SELECT 'exchanges', COUNT(*) FROM exchanges +UNION ALL +SELECT 'system_config', COUNT(*) FROM system_config +UNION ALL +SELECT 'beta_codes', COUNT(*) FROM beta_codes; + +-- 显示当前配置 +SELECT 'Current System Config:' as status; +SELECT key, value FROM system_config ORDER BY key; \ No newline at end of file diff --git a/migrate_data.sql b/migrate_data.sql new file mode 100644 index 00000000..0f946cc1 --- /dev/null +++ b/migrate_data.sql @@ -0,0 +1,49 @@ +-- PostgreSQL数据迁移脚本 +-- 从SQLite导出的数据转换为PostgreSQL格式 + +-- 注意:这个脚本需要根据实际的SQLite导出数据进行调整 +-- 主要差异: +-- 1. SQLite的AUTOINCREMENT -> PostgreSQL的SERIAL +-- 2. 布尔值:SQLite的0/1 -> PostgreSQL的false/true +-- 3. 日期时间格式可能需要调整 +-- 4. 主键冲突处理:使用ON CONFLICT + +-- 如果有实际数据,请在这里添加INSERT语句 +-- 例如: + +-- 插入用户数据(如果有) +-- INSERT INTO users (id, email, password_hash, otp_secret, otp_verified, created_at, updated_at) +-- VALUES (...) ON CONFLICT (id) DO NOTHING; + +-- 插入AI模型配置(如果有自定义) +-- INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at) +-- VALUES (...) ON CONFLICT (id) DO NOTHING; + +-- 插入交易所配置(如果有自定义) +-- INSERT INTO exchanges (id, user_id, name, type, enabled, api_key, secret_key, testnet, hyperliquid_wallet_addr, aster_user, aster_signer, aster_private_key, created_at, updated_at) +-- VALUES (...) ON CONFLICT (id, user_id) DO NOTHING; + +-- 插入交易员配置(如果有) +-- INSERT INTO traders (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) +-- VALUES (...) ON CONFLICT (id) DO NOTHING; + +-- 插入系统配置(如果有自定义) +-- INSERT INTO system_config (key, value, updated_at) +-- VALUES (...) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; + +-- 插入内测码(如果有) +-- INSERT INTO beta_codes (code, used, used_by, used_at, created_at) +-- VALUES (...) ON CONFLICT (code) DO NOTHING; + +-- 数据迁移完成后的验证查询 +-- SELECT 'users' as table_name, COUNT(*) as count FROM users +-- UNION ALL +-- SELECT 'ai_models', COUNT(*) FROM ai_models +-- UNION ALL +-- SELECT 'exchanges', COUNT(*) FROM exchanges +-- UNION ALL +-- SELECT 'traders', COUNT(*) FROM traders +-- UNION ALL +-- SELECT 'system_config', COUNT(*) FROM system_config +-- UNION ALL +-- SELECT 'beta_codes', COUNT(*) FROM beta_codes; \ No newline at end of file diff --git a/migrate_to_postgres.sh b/migrate_to_postgres.sh new file mode 100755 index 00000000..6b3ee90d --- /dev/null +++ b/migrate_to_postgres.sh @@ -0,0 +1,137 @@ +#!/bin/bash + +# PostgreSQL数据迁移脚本 - 一键迁移 +# 用于将SQLite数据迁移到PostgreSQL + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 检测Docker Compose命令 +DOCKER_COMPOSE_CMD="" +if command -v "docker-compose" &> /dev/null; then + DOCKER_COMPOSE_CMD="docker-compose" +elif command -v "docker" &> /dev/null && docker compose version &> /dev/null; then + DOCKER_COMPOSE_CMD="docker compose" +else + echo -e "${RED}❌ 错误:找不到 docker-compose 或 docker compose 命令${NC}" + echo "请安装 Docker Compose 或确保 Docker 支持 compose 子命令" + exit 1 +fi + +echo -e "${BLUE}🔄 开始数据库迁移...${NC}" +echo -e "${BLUE}📋 使用命令: ${DOCKER_COMPOSE_CMD}${NC}" + +# 检查必要文件 +if [ ! -f "migrate_actual_data.sql" ]; then + echo -e "${RED}❌ 错误:找不到 migrate_actual_data.sql 文件${NC}" + echo "请确保在项目根目录执行此脚本" + exit 1 +fi + +if [ ! -f "docker-compose.yml" ]; then + echo -e "${RED}❌ 错误:找不到 docker-compose.yml 文件${NC}" + echo "请确保在项目根目录执行此脚本" + exit 1 +fi + +# 停止现有服务(避免端口冲突) +echo -e "${YELLOW}🛑 停止现有服务...${NC}" +$DOCKER_COMPOSE_CMD down 2>/dev/null || true + +# 启动PostgreSQL和Redis服务 +echo -e "${YELLOW}🚀 启动PostgreSQL和Redis服务...${NC}" +$DOCKER_COMPOSE_CMD up postgres redis -d + +# 等待服务启动 +echo -e "${YELLOW}⏳ 等待服务启动...${NC}" +sleep 15 + +# 检查PostgreSQL连接 +echo -e "${BLUE}🔌 测试数据库连接...${NC}" +max_retries=12 +retry_count=0 + +while [ $retry_count -lt $max_retries ]; do + if $DOCKER_COMPOSE_CMD exec postgres pg_isready -U nofx > /dev/null 2>&1; then + echo -e "${GREEN}✅ PostgreSQL连接正常${NC}" + break + else + retry_count=$((retry_count + 1)) + echo -e "${YELLOW}⏳ 等待PostgreSQL启动... (${retry_count}/${max_retries})${NC}" + sleep 5 + fi +done + +if [ $retry_count -eq $max_retries ]; then + echo -e "${RED}❌ 无法连接到PostgreSQL,请检查服务状态${NC}" + $DOCKER_COMPOSE_CMD logs postgres + exit 1 +fi + +# 复制迁移脚本到容器 +echo -e "${BLUE}📦 复制迁移脚本到容器...${NC}" +POSTGRES_CONTAINER=$($DOCKER_COMPOSE_CMD ps -q postgres) +if [ -z "$POSTGRES_CONTAINER" ]; then + echo -e "${RED}❌ 找不到PostgreSQL容器${NC}" + exit 1 +fi + +docker cp migrate_actual_data.sql ${POSTGRES_CONTAINER}:/tmp/migrate_actual_data.sql + +# 验证文件复制成功 +if ! $DOCKER_COMPOSE_CMD exec postgres test -f /tmp/migrate_actual_data.sql; then + echo -e "${RED}❌ 迁移脚本复制失败${NC}" + exit 1 +fi + +# 执行数据迁移 +echo -e "${BLUE}🔄 执行数据迁移...${NC}" +if $DOCKER_COMPOSE_CMD exec postgres env PAGER="" psql -U nofx -d nofx -f /tmp/migrate_actual_data.sql; then + echo -e "${GREEN}✅ 数据迁移成功!${NC}" +else + echo -e "${RED}❌ 数据迁移失败${NC}" + exit 1 +fi + +# 验证数据 +echo -e "${BLUE}🔍 验证迁移结果...${NC}" +$DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx --pset pager=off -c " +SELECT '=== 数据库迁移验证 ===' as info; +SELECT + relname as \"表名\", + n_live_tup as \"记录数\" +FROM pg_stat_user_tables +WHERE n_live_tup > 0 +ORDER BY relname; +" + +# 显示系统配置(简化版本,避免长文本问题) +echo -e "${BLUE}📋 显示关键配置...${NC}" +$DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx --pset pager=off -c " +SELECT COUNT(*) as \"配置项总数\" FROM system_config; +SELECT 'admin_mode: ' || COALESCE((SELECT value FROM system_config WHERE key='admin_mode'), 'N/A') as \"管理员模式\"; +SELECT 'beta_mode: ' || COALESCE((SELECT value FROM system_config WHERE key='beta_mode'), 'N/A') as \"内测模式\"; +SELECT 'api_server_port: ' || COALESCE((SELECT value FROM system_config WHERE key='api_server_port'), 'N/A') as \"API端口\"; +" + +echo "" +echo -e "${GREEN}🎉 数据库迁移完成!${NC}" +echo "" +echo -e "${BLUE}📋 后续步骤:${NC}" +echo -e "1. 启动完整应用: ${YELLOW}$DOCKER_COMPOSE_CMD up${NC}" +echo -e "2. 验证功能: 访问 ${YELLOW}http://localhost:3000${NC}" +echo -e "3. 备份原SQLite: ${YELLOW}mv config.db config.db.backup${NC}" +echo "" +echo -e "${BLUE}🔧 如需回滚到SQLite:${NC}" +echo -e "1. 停止服务: ${YELLOW}$DOCKER_COMPOSE_CMD down${NC}" +echo -e "2. 删除环境变量: ${YELLOW}unset POSTGRES_HOST${NC} 或编辑 .env 文件" +echo -e "3. 恢复备份: ${YELLOW}mv config.db.backup config.db${NC}" +echo -e "4. 重启: ${YELLOW}$DOCKER_COMPOSE_CMD up${NC}" +echo "" +echo -e "${GREEN}🚀 PostgreSQL迁移成功!系统已升级到现代化数据库架构${NC}" \ No newline at end of file diff --git a/sqlite_backup.sql b/sqlite_backup.sql new file mode 100644 index 00000000..0abf0ebd --- /dev/null +++ b/sqlite_backup.sql @@ -0,0 +1,207 @@ +PRAGMA foreign_keys=OFF; +BEGIN TRANSACTION; +CREATE TABLE ai_models ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL DEFAULT 'default', + name TEXT NOT NULL, + provider TEXT NOT NULL, + enabled BOOLEAN DEFAULT 0, + api_key TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, custom_api_url TEXT DEFAULT '', custom_model_name TEXT DEFAULT '', + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); +INSERT INTO ai_models VALUES('deepseek','default','DeepSeek','deepseek',0,'','2025-11-03 09:09:52','2025-11-03 09:09:52','',''); +INSERT INTO ai_models VALUES('qwen','default','Qwen','qwen',0,'','2025-11-03 09:09:52','2025-11-03 09:09:52','',''); +CREATE TABLE exchange_secrets ( + exchange_id TEXT NOT NULL, + user_id TEXT NOT NULL, + credential_type TEXT NOT NULL, + ciphertext BLOB NOT NULL, + nonce BLOB NOT NULL, + kms_ciphertext BLOB NOT NULL, + kms_key_version TEXT NOT NULL, + public_key_version TEXT NOT NULL, + algorithm TEXT NOT NULL, + aad BLOB NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (exchange_id, user_id, credential_type), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); +CREATE TABLE user_signal_sources ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + coin_pool_url TEXT DEFAULT '', + oi_top_url TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE(user_id) + ); +CREATE TABLE traders ( + 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, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, custom_prompt TEXT DEFAULT '', override_base_prompt BOOLEAN DEFAULT 0, is_cross_margin BOOLEAN DEFAULT 1, use_default_coins BOOLEAN DEFAULT 1, custom_coins TEXT DEFAULT '', system_prompt_template TEXT DEFAULT 'default', + 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) + ); +CREATE TABLE users ( + id TEXT PRIMARY KEY, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + otp_secret TEXT, + otp_verified BOOLEAN DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); +CREATE TABLE system_config ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); +INSERT INTO system_config VALUES('coin_pool_api_url','','2025-11-03 09:09:52'); +INSERT INTO system_config VALUES('btc_eth_leverage','5','2025-11-03 09:09:52'); +INSERT INTO system_config VALUES('api_server_port','8080','2025-11-03 09:09:52'); +INSERT INTO system_config VALUES('oi_top_api_url','','2025-11-03 09:09:52'); +INSERT INTO system_config VALUES('stop_trading_minutes','60','2025-11-03 09:09:52'); +INSERT INTO system_config VALUES('default_coins','["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]','2025-11-03 09:09:52'); +INSERT INTO system_config VALUES('altcoin_leverage','5','2025-11-03 09:09:52'); +INSERT INTO system_config VALUES('beta_mode','true','2025-11-03 09:09:52'); +INSERT INTO system_config VALUES('use_default_coins','true','2025-11-03 09:09:52'); +INSERT INTO system_config VALUES('max_daily_loss','10.0','2025-11-03 09:09:52'); +INSERT INTO system_config VALUES('jwt_secret','Qk0kAa+d0iIEzXVHXbNbm+UaN3RNabmWtH8rDWZ5OPf+4GX8pBflAHodfpbipVMyrw1fsDanHsNBjhgbDeK9Jg==','2025-11-03 09:09:52'); +INSERT INTO system_config VALUES('admin_mode','false','2025-11-03 09:09:52'); +INSERT INTO system_config VALUES('max_drawdown','20.0','2025-11-03 09:09:52'); +INSERT INTO system_config VALUES('encryption_public_key',unistr('-----BEGIN PUBLIC KEY-----\u000aMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxDsGHRSFXqR2YFoWMNWC\u000a8s0FlVE2KglHjLnm1f+i5yPfuTYkTUbVDu6RZuqLJdvhX+UO0x1XnwFIhZqmEfro\u000a8Myr5+RnItl7QGqWWcKry4ZlPHroMwIK50WJt316KUKVUv7wUMMLoUUq7yctI8V/\u000athRX+ZRaErJJU9DWkSqjYOVdc+KwsZnN9WifoYhp6veTKmJ1kJOd6AVtF+KJ/z0R\u000ahFarXjaQ89vf/oUgKahS/BUH7P6jpP+L+7z8G650oygp3Pn66eq+ttcUdc20WiBj\u000aK5eDBUJUUeNmdesqZXBafhJBhsQyilC0+LgI+3laSkGh3odMdY5Mf9lnke9mfX8E\u000aRQIDAQAB\u000a-----END PUBLIC KEY-----'),'2025-11-03 09:09:52'); +INSERT INTO system_config VALUES('encryption_public_key_version','mock-v1','2025-11-03 09:09:52'); +CREATE TABLE beta_codes ( + code TEXT PRIMARY KEY, + used BOOLEAN DEFAULT 0, + used_by TEXT DEFAULT '', + used_at DATETIME DEFAULT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); +INSERT INTO beta_codes VALUES('2aw4wm',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('34cvds',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('3f39nc',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('3qmg67',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('5rjp6k',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('65a3e6',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('6hzgpr',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('6wruwb',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('8bdf7a',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('8jxnp5',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('8xp3xq',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('9r5uev',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('adbn7p',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('azm8y4',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('b6tfqu',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('bs32f9',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('ctz8gn',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('d8rmq8',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('dmf2yt',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('dz7e8d',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('e9ptrm',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('f25m8s',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('feuzgb',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('fnd7z7',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('h43s95',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('hgs7gq',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('huhkra',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('mhqch4',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('mqwkau',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('mwfssp',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('na7629',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('pb5c2n',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('q5k6jt',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('qrurb8',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('rssybm',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('s7hbk7',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('sj8rus',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('sxy53c',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('t8fjmk',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('udmqcb',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('um6xu6',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('uzwb4r',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('w2uh55',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('wejxcq',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('wtaama',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('x82qvu',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('ygg4d4',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('yv8hnn',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('z9ywv8',0,'',NULL,'2025-11-03 09:09:52'); +INSERT INTO beta_codes VALUES('znpa5t',0,'',NULL,'2025-11-03 09:09:52'); +CREATE TABLE IF NOT EXISTS "exchanges" ( + id TEXT NOT NULL, + user_id TEXT NOT NULL DEFAULT 'default', + name TEXT NOT NULL, + type TEXT NOT NULL, + enabled BOOLEAN DEFAULT 0, + api_key TEXT DEFAULT '', + secret_key TEXT DEFAULT '', + testnet BOOLEAN DEFAULT 0, + hyperliquid_wallet_addr TEXT DEFAULT '', + aster_user TEXT DEFAULT '', + aster_signer TEXT DEFAULT '', + aster_private_key TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id, user_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); +INSERT INTO exchanges VALUES('binance','default','Binance Futures','binance',0,'','',0,'','','','','2025-11-03 09:09:52','2025-11-03 09:09:52'); +INSERT INTO exchanges VALUES('hyperliquid','default','Hyperliquid','hyperliquid',0,'','',0,'','','','','2025-11-03 09:09:52','2025-11-03 09:09:52'); +INSERT INTO exchanges VALUES('aster','default','Aster DEX','aster',0,'','',0,'','','','','2025-11-03 09:09:52','2025-11-03 09:09:52'); +CREATE TRIGGER update_users_updated_at + AFTER UPDATE ON users + BEGIN + UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; + END; +CREATE TRIGGER update_ai_models_updated_at + AFTER UPDATE ON ai_models + BEGIN + UPDATE ai_models SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; + END; +CREATE TRIGGER update_exchange_secrets_updated_at + AFTER UPDATE ON exchange_secrets + BEGIN + UPDATE exchange_secrets + SET updated_at = CURRENT_TIMESTAMP + WHERE exchange_id = NEW.exchange_id AND user_id = NEW.user_id AND credential_type = NEW.credential_type; + END; +CREATE TRIGGER update_traders_updated_at + AFTER UPDATE ON traders + BEGIN + UPDATE traders SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; + END; +CREATE TRIGGER update_user_signal_sources_updated_at + AFTER UPDATE ON user_signal_sources + BEGIN + UPDATE user_signal_sources SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; + END; +CREATE TRIGGER update_system_config_updated_at + AFTER UPDATE ON system_config + BEGIN + UPDATE system_config SET updated_at = CURRENT_TIMESTAMP WHERE key = NEW.key; + END; +CREATE TRIGGER update_exchanges_updated_at + AFTER UPDATE ON exchanges + BEGIN + UPDATE exchanges SET updated_at = CURRENT_TIMESTAMP + WHERE id = NEW.id AND user_id = NEW.user_id; + END; +COMMIT;