package config import ( "crypto/rand" "database/sql" "encoding/base32" "encoding/json" "errors" "fmt" "log" "nofx/crypto" "nofx/market" "os" "slices" "strings" "time" _ "modernc.org/sqlite" ) // DatabaseInterface 定义了数据库实现需要提供的方法集合 type DatabaseInterface interface { SetCryptoService(cs *crypto.CryptoService) CreateUser(user *User) 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, lighterWalletAddr, lighterPrivateKey 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 UpdateTraderInitialBalance(userID, id string, newBalance float64) 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 } // Database 配置数据库 type Database struct { db *sql.DB cryptoService *crypto.CryptoService } // NewDatabase 创建配置数据库 func NewDatabase(dbPath string) (*Database, error) { db, err := sql.Open("sqlite", dbPath) if err != nil { return nil, fmt.Errorf("打开数据库失败: %w", err) } db.SetMaxOpenConns(1) db.SetMaxIdleConns(1) if _, err := db.Exec(`PRAGMA foreign_keys = ON`); err != nil { return nil, fmt.Errorf("启用外键失败: %w", err) } if err := tuneSQLiteConnection(db); err != nil { return nil, err } // 🔒 启用 WAL 模式,提高并发性能和崩溃恢复能力 // WAL (Write-Ahead Logging) 模式的优势: // 1. 更好的并发性能:读操作不会被写操作阻塞 // 2. 崩溃安全:即使在断电或强制终止时也能保证数据完整性 // 3. 更快的写入:不需要每次都写入主数据库文件 if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil { db.Close() return nil, fmt.Errorf("启用WAL模式失败: %w", err) } // 🔒 设置 synchronous=FULL 确保数据持久性 // FULL (2) 模式: 确保数据在关键时刻完全写入磁盘 // 配合 WAL 模式,在保证数据安全的同时获得良好性能 if _, err := db.Exec("PRAGMA synchronous=FULL"); err != nil { db.Close() return nil, fmt.Errorf("设置synchronous失败: %w", err) } database := &Database{db: db} if err := database.createTables(); err != nil { return nil, fmt.Errorf("创建表失败: %w", err) } if err := database.ensureBacktestRunColumns(); err != nil { return nil, fmt.Errorf("初始化回测表结构失败: %w", err) } // 确保存在默认用户(用于外键约束和默认配置种子) if _, err := db.Exec(` INSERT OR IGNORE INTO users (id, email, password_hash, otp_secret, otp_verified) VALUES ('default', 'default@local', '__default__', '', 1) `); err != nil { return nil, fmt.Errorf("创建默认用户失败: %w", err) } if err := database.initDefaultData(); err != nil { return nil, fmt.Errorf("初始化默认数据失败: %w", err) } log.Printf("✅ 数据库已启用 WAL 模式和 FULL 同步,数据持久性得到保证") return database, nil } // createTables 创建数据库表 func (d *Database) createTables() error { queries := []string{ // 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 0, api_key TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`, // 交易所配置表 `CREATE TABLE IF NOT EXISTS exchanges ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL DEFAULT 'default', name TEXT NOT NULL, type TEXT NOT NULL, -- 'cex' or 'dex' enabled BOOLEAN DEFAULT 0, api_key TEXT DEFAULT '', secret_key TEXT DEFAULT '', testnet BOOLEAN DEFAULT 0, -- Hyperliquid 特定字段 hyperliquid_wallet_addr TEXT DEFAULT '', -- Aster 特定字段 aster_user TEXT DEFAULT '', aster_signer TEXT DEFAULT '', aster_private_key TEXT DEFAULT '', -- LIGHTER 特定字段 lighter_wallet_addr TEXT DEFAULT '', lighter_private_key TEXT DEFAULT '', lighter_api_key_private_key TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`, // 用户信号源配置表 `CREATE TABLE IF NOT EXISTS 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 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 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, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`, // 用户表 `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 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP )`, // 系统配置表 `CREATE TABLE IF NOT EXISTS system_config ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP )`, // 回测运行主表 `CREATE TABLE IF NOT EXISTS backtest_runs ( run_id TEXT PRIMARY KEY, user_id TEXT NOT NULL DEFAULT 'default', config_json TEXT NOT NULL DEFAULT '', state TEXT NOT NULL DEFAULT 'created', label TEXT DEFAULT '', symbol_count INTEGER DEFAULT 0, decision_tf TEXT DEFAULT '', processed_bars INTEGER DEFAULT 0, progress_pct REAL DEFAULT 0, equity_last REAL DEFAULT 0, max_drawdown_pct REAL DEFAULT 0, liquidated BOOLEAN DEFAULT 0, liquidation_note TEXT DEFAULT '', prompt_template TEXT DEFAULT '', custom_prompt TEXT DEFAULT '', override_prompt BOOLEAN DEFAULT 0, ai_provider TEXT DEFAULT '', ai_model TEXT DEFAULT '', last_error TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP )`, // 回测检查点 `CREATE TABLE IF NOT EXISTS backtest_checkpoints ( run_id TEXT PRIMARY KEY, payload BLOB NOT NULL, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (run_id) REFERENCES backtest_runs(run_id) ON DELETE CASCADE )`, // 回测权益曲线 `CREATE TABLE IF NOT EXISTS backtest_equity ( id INTEGER PRIMARY KEY AUTOINCREMENT, run_id TEXT NOT NULL, ts INTEGER NOT NULL, equity REAL NOT NULL, available REAL NOT NULL, pnl REAL NOT NULL, pnl_pct REAL NOT NULL, dd_pct REAL NOT NULL, cycle INTEGER NOT NULL, FOREIGN KEY (run_id) REFERENCES backtest_runs(run_id) ON DELETE CASCADE )`, // 回测交易记录 `CREATE TABLE IF NOT EXISTS backtest_trades ( id INTEGER PRIMARY KEY AUTOINCREMENT, run_id TEXT NOT NULL, ts INTEGER NOT NULL, symbol TEXT NOT NULL, action TEXT NOT NULL, side TEXT DEFAULT '', qty REAL DEFAULT 0, price REAL DEFAULT 0, fee REAL DEFAULT 0, slippage REAL DEFAULT 0, order_value REAL DEFAULT 0, realized_pnl REAL DEFAULT 0, leverage INTEGER DEFAULT 0, cycle INTEGER DEFAULT 0, position_after REAL DEFAULT 0, liquidation BOOLEAN DEFAULT 0, note TEXT DEFAULT '', FOREIGN KEY (run_id) REFERENCES backtest_runs(run_id) ON DELETE CASCADE )`, // 回测指标 `CREATE TABLE IF NOT EXISTS backtest_metrics ( run_id TEXT PRIMARY KEY, payload BLOB NOT NULL, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (run_id) REFERENCES backtest_runs(run_id) ON DELETE CASCADE )`, // 回测决策日志 `CREATE TABLE IF NOT EXISTS backtest_decisions ( id INTEGER PRIMARY KEY AUTOINCREMENT, run_id TEXT NOT NULL, cycle INTEGER NOT NULL, payload BLOB NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (run_id) REFERENCES backtest_runs(run_id) ON DELETE CASCADE )`, // 索引 `CREATE INDEX IF NOT EXISTS idx_backtest_runs_state ON backtest_runs(state, updated_at)`, `CREATE INDEX IF NOT EXISTS idx_backtest_equity_run_ts ON backtest_equity(run_id, ts)`, `CREATE INDEX IF NOT EXISTS idx_backtest_trades_run_ts ON backtest_trades(run_id, ts)`, `CREATE INDEX IF NOT EXISTS idx_backtest_decisions_run_cycle ON backtest_decisions(run_id, cycle)`, // 内测码表 `CREATE TABLE IF NOT EXISTS 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 )`, // 触发器:自动更新 updated_at `CREATE TRIGGER IF NOT EXISTS update_users_updated_at AFTER UPDATE ON users BEGIN UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END`, `CREATE TRIGGER IF NOT EXISTS 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 IF NOT EXISTS update_exchanges_updated_at AFTER UPDATE ON exchanges BEGIN UPDATE exchanges SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END`, `CREATE TRIGGER IF NOT EXISTS update_traders_updated_at AFTER UPDATE ON traders BEGIN UPDATE traders SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END`, `CREATE TRIGGER IF NOT EXISTS 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 IF NOT EXISTS update_system_config_updated_at AFTER UPDATE ON system_config BEGIN UPDATE system_config SET updated_at = CURRENT_TIMESTAMP WHERE key = NEW.key; END`, } for _, query := range queries { if _, err := d.db.Exec(query); err != nil { return fmt.Errorf("执行SQL失败 [%s]: %w", query, err) } } // 为现有数据库添加新字段(向后兼容) alterQueries := []string{ `ALTER TABLE exchanges ADD COLUMN hyperliquid_wallet_addr TEXT DEFAULT ''`, `ALTER TABLE exchanges ADD COLUMN aster_user TEXT DEFAULT ''`, `ALTER TABLE exchanges ADD COLUMN aster_signer TEXT DEFAULT ''`, `ALTER TABLE exchanges ADD COLUMN aster_private_key TEXT DEFAULT ''`, `ALTER TABLE exchanges ADD COLUMN lighter_wallet_addr TEXT DEFAULT ''`, `ALTER TABLE exchanges ADD COLUMN lighter_private_key TEXT DEFAULT ''`, `ALTER TABLE exchanges ADD COLUMN lighter_api_key_private_key TEXT DEFAULT ''`, `ALTER TABLE traders ADD COLUMN custom_prompt TEXT DEFAULT ''`, `ALTER TABLE traders ADD COLUMN override_base_prompt BOOLEAN DEFAULT 0`, `ALTER TABLE traders ADD COLUMN is_cross_margin BOOLEAN DEFAULT 1`, // 默认为全仓模式 `ALTER TABLE traders ADD COLUMN use_default_coins BOOLEAN DEFAULT 1`, // 默认使用默认币种 `ALTER TABLE traders ADD COLUMN custom_coins TEXT DEFAULT ''`, // 自定义币种列表(JSON格式) `ALTER TABLE traders ADD COLUMN btc_eth_leverage INTEGER DEFAULT 5`, // BTC/ETH杠杆倍数 `ALTER TABLE traders ADD COLUMN altcoin_leverage INTEGER DEFAULT 5`, // 山寨币杠杆倍数 `ALTER TABLE traders ADD COLUMN trading_symbols TEXT DEFAULT ''`, // 交易币种,逗号分隔 `ALTER TABLE traders ADD COLUMN use_coin_pool BOOLEAN DEFAULT 0`, // 是否使用COIN POOL信号源 `ALTER TABLE traders ADD COLUMN use_oi_top BOOLEAN DEFAULT 0`, // 是否使用OI TOP信号源 `ALTER TABLE traders ADD COLUMN system_prompt_template TEXT DEFAULT 'default'`, // 系统提示词模板名称 `ALTER TABLE ai_models ADD COLUMN custom_api_url TEXT DEFAULT ''`, // 自定义API地址 `ALTER TABLE ai_models ADD COLUMN custom_model_name TEXT DEFAULT ''`, // 自定义模型名称 } for _, query := range alterQueries { // 忽略已存在字段的错误 d.db.Exec(query) } // 检查是否需要迁移exchanges表的主键结构 err := d.migrateExchangesTable() if err != nil { log.Printf("⚠️ 迁移exchanges表失败: %v", err) } // 修复traders表的外键约束问题 err = d.migrateTradersTable() if err != nil { log.Printf("⚠️ 迁移traders表失败: %v", err) } return nil } func (d *Database) ensureBacktestRunColumns() error { addColumn := func(table, column, definition string) error { exists, err := columnExists(d.db, table, column) if err != nil { return err } if exists { return nil } _, err = d.db.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", table, column, definition)) return err } if err := addColumn("backtest_runs", "label", "TEXT DEFAULT ''"); err != nil { return err } if err := addColumn("backtest_runs", "last_error", "TEXT DEFAULT ''"); err != nil { return err } if err := addColumn("backtest_trades", "leverage", "INTEGER DEFAULT 0"); err != nil { return err } return nil } func columnExists(db *sql.DB, table, column string) (bool, error) { rows, err := db.Query(fmt.Sprintf("PRAGMA table_info(%s)", table)) if err != nil { return false, err } defer rows.Close() for rows.Next() { var ( cid int name string ctype string notnull int dfltValue any primaryKey int ) if err := rows.Scan(&cid, &name, &ctype, ¬null, &dfltValue, &primaryKey); err != nil { return false, err } if name == column { return true, nil } } return false, rows.Err() } func tuneSQLiteConnection(db *sql.DB) error { if db == nil { return fmt.Errorf("db is nil") } statements := []string{ `PRAGMA busy_timeout = 5000`, `PRAGMA journal_mode = WAL`, `PRAGMA synchronous = NORMAL`, } for _, stmt := range statements { if _, err := db.Exec(stmt); err != nil { return fmt.Errorf("执行 %s 失败: %w", stmt, err) } } return nil } // initDefaultData 初始化默认数据 func (d *Database) initDefaultData() error { // 初始化AI模型(使用default用户) aiModels := []struct { id, name, provider string }{ {"deepseek", "DeepSeek", "deepseek"}, {"qwen", "Qwen", "qwen"}, } for _, model := range aiModels { _, err := d.db.Exec(` INSERT OR IGNORE INTO ai_models (id, user_id, name, provider, enabled) VALUES (?, 'default', ?, ?, 0) `, model.id, model.name, model.provider) if err != nil { return fmt.Errorf("初始化AI模型失败: %w", err) } } // 初始化交易所(使用default用户) exchanges := []struct { id, name, typ string }{ {"binance", "Binance Futures", "binance"}, {"bybit", "Bybit Futures", "bybit"}, {"hyperliquid", "Hyperliquid", "hyperliquid"}, {"aster", "Aster DEX", "aster"}, {"lighter", "LIGHTER DEX", "lighter"}, } for _, exchange := range exchanges { _, err := d.db.Exec(` INSERT OR IGNORE INTO exchanges (id, user_id, name, type, enabled) VALUES (?, 'default', ?, ?, 0) `, exchange.id, exchange.name, exchange.typ) if err != nil { return fmt.Errorf("初始化交易所失败: %w", err) } } // 初始化系统配置 - 创建所有字段,设置默认值,后续由config.json同步更新 systemConfigs := map[string]string{ "beta_mode": "false", // 默认关闭内测模式 "api_server_port": "8080", // 默认API端口 "use_default_coins": "true", // 默认使用内置币种列表 "default_coins": `["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]`, // 默认币种列表(JSON格式) "max_daily_loss": "10.0", // 最大日损失百分比 "max_drawdown": "20.0", // 最大回撤百分比 "stop_trading_minutes": "60", // 停止交易时间(分钟) "btc_eth_leverage": "5", // BTC/ETH杠杆倍数 "altcoin_leverage": "5", // 山寨币杠杆倍数 "jwt_secret": "", // JWT密钥,默认为空,由config.json或系统生成 "registration_enabled": "true", // 默认允许注册 } for key, value := range systemConfigs { _, err := d.db.Exec(` INSERT OR IGNORE INTO system_config (key, value) VALUES (?, ?) `, key, value) if err != nil { return fmt.Errorf("初始化系统配置失败: %w", err) } } return nil } // migrateExchangesTable 迁移exchanges表支持多用户 func (d *Database) migrateExchangesTable() error { // 检查是否已经迁移过 var count int err := d.db.QueryRow(` SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='exchanges_new' `).Scan(&count) if err != nil { return err } // 如果已经迁移过,直接返回 if count > 0 { return nil } log.Printf("🔄 开始迁移exchanges表...") // 创建新的exchanges表,使用复合主键 _, err = d.db.Exec(` CREATE TABLE exchanges_new ( 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 '', lighter_wallet_addr TEXT DEFAULT '', lighter_private_key TEXT DEFAULT '', lighter_api_key_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 ) `) if err != nil { return fmt.Errorf("创建新exchanges表失败: %w", err) } // 复制数据到新表 _, err = d.db.Exec(` INSERT INTO exchanges_new SELECT * FROM exchanges `) if err != nil { return fmt.Errorf("复制数据失败: %w", err) } // 删除旧表 _, err = d.db.Exec(`DROP TABLE exchanges`) if err != nil { return fmt.Errorf("删除旧表失败: %w", err) } // 重命名新表 _, err = d.db.Exec(`ALTER TABLE exchanges_new RENAME TO exchanges`) if err != nil { return fmt.Errorf("重命名表失败: %w", err) } // 重新创建触发器 _, err = d.db.Exec(` CREATE TRIGGER IF NOT EXISTS 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 `) if err != nil { return fmt.Errorf("创建触发器失败: %w", err) } log.Printf("✅ exchanges表迁移完成") 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"` Email string `json:"email"` PasswordHash string `json:"-"` // 不返回到前端 OTPSecret string `json:"-"` // 不返回到前端 OTPVerified bool `json:"otp_verified"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // AIModelConfig AI模型配置 type AIModelConfig struct { ID string `json:"id"` UserID string `json:"user_id"` Name string `json:"name"` Provider string `json:"provider"` Enabled bool `json:"enabled"` APIKey string `json:"apiKey"` CustomAPIURL string `json:"customApiUrl"` CustomModelName string `json:"customModelName"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // ExchangeConfig 交易所配置 type ExchangeConfig struct { ID string `json:"id"` UserID string `json:"user_id"` Name string `json:"name"` Type string `json:"type"` Enabled bool `json:"enabled"` APIKey string `json:"apiKey"` // For Binance: API Key; For Hyperliquid: Agent Private Key (should have ~0 balance) SecretKey string `json:"secretKey"` // For Binance: Secret Key; Not used for Hyperliquid Testnet bool `json:"testnet"` // Hyperliquid Agent Wallet configuration (following official best practices) // Reference: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/nonces-and-api-wallets HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"` // Main Wallet Address (holds funds, never expose private key) // Aster 特定字段 AsterUser string `json:"asterUser"` AsterSigner string `json:"asterSigner"` AsterPrivateKey string `json:"asterPrivateKey"` // LIGHTER 特定字段 LighterWalletAddr string `json:"lighterWalletAddr"` // Ethereum 钱包地址 (L1) LighterPrivateKey string `json:"lighterPrivateKey"` // L1私钥(用于识别账户) LighterAPIKeyPrivateKey string `json:"lighterAPIKeyPrivateKey"` // API Key私钥(40字节,用于签名交易) CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // TraderRecord 交易员配置(数据库实体) type TraderRecord struct { ID string `json:"id"` UserID string `json:"user_id"` Name string `json:"name"` AIModelID string `json:"ai_model_id"` ExchangeID string `json:"exchange_id"` InitialBalance float64 `json:"initial_balance"` ScanIntervalMinutes int `json:"scan_interval_minutes"` IsRunning bool `json:"is_running"` BTCETHLeverage int `json:"btc_eth_leverage"` // BTC/ETH杠杆倍数 AltcoinLeverage int `json:"altcoin_leverage"` // 山寨币杠杆倍数 TradingSymbols string `json:"trading_symbols"` // 交易币种,逗号分隔 UseCoinPool bool `json:"use_coin_pool"` // 是否使用COIN POOL信号源 UseOITop bool `json:"use_oi_top"` // 是否使用OI TOP信号源 CustomPrompt string `json:"custom_prompt"` // 自定义交易策略prompt OverrideBasePrompt bool `json:"override_base_prompt"` // 是否覆盖基础prompt SystemPromptTemplate string `json:"system_prompt_template"` // 系统提示词模板名称 IsCrossMargin bool `json:"is_cross_margin"` // 是否为全仓模式(true=全仓,false=逐仓) CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // UserSignalSource 用户信号源配置 type UserSignalSource struct { ID int `json:"id"` UserID string `json:"user_id"` CoinPoolURL string `json:"coin_pool_url"` OITopURL string `json:"oi_top_url"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // GenerateOTPSecret 生成OTP密钥 func GenerateOTPSecret() (string, error) { secret := make([]byte, 20) _, err := rand.Read(secret) if err != nil { return "", err } return base32.StdEncoding.EncodeToString(secret), nil } // CreateUser 创建用户 func (d *Database) CreateUser(user *User) error { _, err := d.db.Exec(` INSERT INTO users (id, email, password_hash, otp_secret, otp_verified) VALUES (?, ?, ?, ?, ?) `, user.ID, user.Email, user.PasswordHash, user.OTPSecret, user.OTPVerified) return err } // EnsureAdminUser 确保admin用户存在(用于管理员模式) func (d *Database) 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 *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, &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, &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 } // GetAllUsers 获取所有用户ID列表 func (d *Database) 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 *Database) UpdateUserOTPVerified(userID string, verified bool) error { _, err := d.db.Exec(`UPDATE users SET otp_verified = ? WHERE id = ?`, verified, userID) return err } // UpdateUserPassword 更新用户密码 func (d *Database) UpdateUserPassword(userID, passwordHash string) error { _, err := d.db.Exec(` UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `, passwordHash, userID) return err } // GetAIModels 获取用户的AI模型配置 func (d *Database) 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 = ? 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 var createdAt, updatedAt string err := rows.Scan( &model.ID, &model.UserID, &model.Name, &model.Provider, &model.Enabled, &model.APIKey, &model.CustomAPIURL, &model.CustomModelName, &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) } return models, nil } // GetAIModel 根据模型ID和用户ID获取单个AI模型配置,若用户下不存在则回退到default用户。 func (d *Database) GetAIModel(userID, modelID string) (*AIModelConfig, error) { if modelID == "" { return nil, fmt.Errorf("模型ID不能为空") } candidates := []string{} if userID != "" { candidates = append(candidates, userID) } if userID != "default" { candidates = append(candidates, "default") } if len(candidates) == 0 { candidates = append(candidates, "default") } 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 FROM ai_models WHERE user_id = ? AND id = ? LIMIT 1 `, uid, modelID).Scan( &model.ID, &model.UserID, &model.Name, &model.Provider, &model.Enabled, &model.APIKey, &model.CustomAPIURL, &model.CustomModelName, &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 } if !errors.Is(err, sql.ErrNoRows) { return nil, err } } return nil, sql.ErrNoRows } // GetDefaultAIModel 获取指定用户(或默认用户)的首个启用的AI模型。 func (d *Database) GetDefaultAIModel(userID string) (*AIModelConfig, error) { if userID == "" { userID = "default" } model, err := d.firstEnabledAIModel(userID) if err == nil { return model, nil } if !errors.Is(err, sql.ErrNoRows) { return nil, err } if userID != "default" { return d.firstEnabledAIModel("default") } return nil, fmt.Errorf("请先在系统中配置可用的AI模型") } 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 FROM ai_models WHERE user_id = ? AND enabled = 1 ORDER BY datetime(updated_at) DESC, id ASC LIMIT 1 `, userID).Scan( &model.ID, &model.UserID, &model.Name, &model.Provider, &model.Enabled, &model.APIKey, &model.CustomAPIURL, &model.CustomModelName, &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 } // UpdateAIModel 更新AI模型配置,如果不存在则创建用户特定配置 func (d *Database) 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 = ? AND id = ? LIMIT 1 `, userID, id).Scan(&existingID) if err == nil { // 找到了现有配置(精确匹配 ID),更新它 encryptedAPIKey := d.encryptSensitiveData(apiKey) _, err = d.db.Exec(` UPDATE ai_models SET enabled = ?, api_key = ?, custom_api_url = ?, custom_model_name = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ? `, enabled, encryptedAPIKey, customAPIURL, customModelName, existingID, userID) return err } // ID 不存在,尝试兼容旧逻辑:将 id 作为 provider 查找 provider := id err = d.db.QueryRow(` SELECT id FROM ai_models WHERE user_id = ? AND provider = ? LIMIT 1 `, userID, provider).Scan(&existingID) if err == nil { // 找到了现有配置(通过 provider 匹配,兼容旧版),更新它 log.Printf("⚠️ 使用旧版 provider 匹配更新模型: %s -> %s", provider, existingID) encryptedAPIKey := d.encryptSensitiveData(apiKey) _, err = d.db.Exec(` UPDATE ai_models SET enabled = ?, api_key = ?, custom_api_url = ?, custom_model_name = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ? `, enabled, encryptedAPIKey, 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 = ? 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) encryptedAPIKey := d.encryptSensitiveData(apiKey) _, 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 (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now')) `, newModelID, userID, name, provider, enabled, encryptedAPIKey, customAPIURL, customModelName) return err } // GetExchanges 获取用户的交易所配置 func (d *Database) 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, 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) if err != nil { return nil, err } defer rows.Close() // 初始化为空切片而不是nil,确保JSON序列化为[]而不是null 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.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) } return exchanges, nil } // UpdateExchange 更新交易所配置,如果不存在则创建用户特定配置 // 🔒 安全特性:空值不会覆盖现有的敏感字段(api_key, secret_key, aster_private_key, lighter_private_key) func (d *Database) UpdateExchange(userID, id string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, lighterWalletAddr, lighterPrivateKey string) error { log.Printf("🔧 UpdateExchange: userID=%s, id=%s, enabled=%v", userID, id, enabled) // 构建动态 UPDATE SET 子句 // 基础字段:总是更新 setClauses := []string{ "enabled = ?", "testnet = ?", "hyperliquid_wallet_addr = ?", "aster_user = ?", "aster_signer = ?", "lighter_wallet_addr = ?", "updated_at = datetime('now')", } args := []interface{}{enabled, testnet, hyperliquidWalletAddr, asterUser, asterSigner, lighterWalletAddr} // 🔒 敏感字段:只在非空时更新(保护现有数据) if apiKey != "" { encryptedAPIKey := d.encryptSensitiveData(apiKey) setClauses = append(setClauses, "api_key = ?") args = append(args, encryptedAPIKey) } if secretKey != "" { encryptedSecretKey := d.encryptSensitiveData(secretKey) setClauses = append(setClauses, "secret_key = ?") args = append(args, encryptedSecretKey) } if asterPrivateKey != "" { encryptedAsterPrivateKey := d.encryptSensitiveData(asterPrivateKey) setClauses = append(setClauses, "aster_private_key = ?") args = append(args, encryptedAsterPrivateKey) } if lighterPrivateKey != "" { encryptedLighterPrivateKey := d.encryptSensitiveData(lighterPrivateKey) setClauses = append(setClauses, "lighter_private_key = ?") args = append(args, encryptedLighterPrivateKey) } // WHERE 条件 args = append(args, id, userID) // 构建完整的 UPDATE 语句 query := fmt.Sprintf(` UPDATE exchanges SET %s WHERE id = ? AND user_id = ? `, strings.Join(setClauses, ", ")) // 执行更新 result, err := d.db.Exec(query, args...) 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 == "bybit" { name = "Bybit Futures" typ = "cex" } else if id == "hyperliquid" { name = "Hyperliquid" typ = "dex" } else if id == "aster" { name = "Aster DEX" typ = "dex" } else if id == "lighter" { name = "LIGHTER DEX" typ = "dex" } else { name = id + " Exchange" typ = "cex" } log.Printf("🆕 UpdateExchange: 创建新记录 ID=%s, name=%s, type=%s", id, name, typ) // 加密敏感字段 encryptedAPIKey := d.encryptSensitiveData(apiKey) encryptedSecretKey := d.encryptSensitiveData(secretKey) encryptedAsterPrivateKey := d.encryptSensitiveData(asterPrivateKey) encryptedLighterPrivateKey := d.encryptSensitiveData(lighterPrivateKey) // 创建用户特定的配置,使用原始的交易所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, lighter_wallet_addr, lighter_private_key, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now')) `, id, userID, name, typ, enabled, encryptedAPIKey, encryptedSecretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, encryptedAsterPrivateKey, lighterWalletAddr, encryptedLighterPrivateKey) if err != nil { log.Printf("❌ UpdateExchange: 创建记录失败: %v", err) } else { log.Printf("✅ UpdateExchange: 创建记录成功") } return err } log.Printf("✅ UpdateExchange: 更新现有记录成功") return nil } // CreateAIModel 创建AI模型配置 func (d *Database) CreateAIModel(userID, id, name, provider string, enabled bool, apiKey, customAPIURL string) error { _, err := d.db.Exec(` INSERT OR IGNORE INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url) VALUES (?, ?, ?, ?, ?, ?, ?) `, id, userID, name, provider, enabled, apiKey, customAPIURL) return err } // CreateExchange 创建交易所配置 func (d *Database) CreateExchange(userID, id, name, typ string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error { // 加密敏感字段 encryptedAPIKey := d.encryptSensitiveData(apiKey) encryptedSecretKey := d.encryptSensitiveData(secretKey) encryptedAsterPrivateKey := d.encryptSensitiveData(asterPrivateKey) _, err := d.db.Exec(` INSERT OR IGNORE INTO exchanges (id, user_id, name, type, enabled, api_key, secret_key, testnet, hyperliquid_wallet_addr, aster_user, aster_signer, aster_private_key, lighter_wallet_addr, lighter_private_key) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', '') `, id, userID, name, typ, enabled, encryptedAPIKey, encryptedSecretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, encryptedAsterPrivateKey) return err } // CreateTrader 创建交易员 func (d *Database) 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, 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 *Database) 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, 0) as use_coin_pool, COALESCE(use_oi_top, 0) as use_oi_top, COALESCE(custom_prompt, '') as custom_prompt, COALESCE(override_base_prompt, 0) as override_base_prompt, COALESCE(system_prompt_template, 'default') as system_prompt_template, COALESCE(is_cross_margin, 1) as is_cross_margin, created_at, updated_at FROM traders WHERE user_id = ? ORDER BY created_at DESC `, userID) if err != nil { return nil, err } defer rows.Close() 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, &trader.BTCETHLeverage, &trader.AltcoinLeverage, &trader.TradingSymbols, &trader.UseCoinPool, &trader.UseOITop, &trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate, &trader.IsCrossMargin, &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) } return traders, nil } // UpdateTraderStatus 更新交易员状态 func (d *Database) UpdateTraderStatus(userID, id string, isRunning bool) error { _, err := d.db.Exec(`UPDATE traders SET is_running = ? WHERE id = ? AND user_id = ?`, isRunning, id, userID) return err } // UpdateTrader 更新交易员配置 func (d *Database) UpdateTrader(trader *TraderRecord) error { _, err := d.db.Exec(` UPDATE traders SET name = ?, ai_model_id = ?, exchange_id = ?, scan_interval_minutes = ?, btc_eth_leverage = ?, altcoin_leverage = ?, trading_symbols = ?, custom_prompt = ?, override_base_prompt = ?, system_prompt_template = ?, is_cross_margin = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ? `, trader.Name, trader.AIModelID, trader.ExchangeID, 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 *Database) UpdateTraderCustomPrompt(userID, id string, customPrompt string, overrideBase bool) error { _, err := d.db.Exec(`UPDATE traders SET custom_prompt = ?, override_base_prompt = ? WHERE id = ? AND user_id = ?`, customPrompt, overrideBase, id, userID) return err } // UpdateTraderInitialBalance 更新交易员初始余额(仅支持手动更新) // ⚠️ 注意:系统不会自动调用此方法,仅供用户在充值/提现后手动同步使用 func (d *Database) UpdateTraderInitialBalance(userID, id string, newBalance float64) error { _, err := d.db.Exec(`UPDATE traders SET initial_balance = ? WHERE id = ? AND user_id = ?`, newBalance, id, userID) return err } // DeleteTrader 删除交易员 func (d *Database) DeleteTrader(userID, id string) error { _, err := d.db.Exec(`DELETE FROM traders WHERE id = ? AND user_id = ?`, id, userID) return err } // GetTraderConfig 获取交易员完整配置(包含AI模型和交易所信息) func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIModelConfig, *ExchangeConfig, error) { 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 t.id, t.user_id, t.name, t.ai_model_id, t.exchange_id, t.initial_balance, t.scan_interval_minutes, t.is_running, COALESCE(t.btc_eth_leverage, 5) as btc_eth_leverage, COALESCE(t.altcoin_leverage, 5) as altcoin_leverage, COALESCE(t.trading_symbols, '') as trading_symbols, COALESCE(t.use_coin_pool, 0) as use_coin_pool, COALESCE(t.use_oi_top, 0) as use_oi_top, COALESCE(t.custom_prompt, '') as custom_prompt, COALESCE(t.override_base_prompt, 0) as override_base_prompt, COALESCE(t.system_prompt_template, 'default') as system_prompt_template, COALESCE(t.is_cross_margin, 1) as is_cross_margin, t.created_at, t.updated_at, a.id, a.user_id, a.name, a.provider, a.enabled, a.api_key, COALESCE(a.custom_api_url, '') as custom_api_url, COALESCE(a.custom_model_name, '') as custom_model_name, 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, 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 JOIN exchanges e ON t.exchange_id = e.id AND t.user_id = e.user_id WHERE t.id = ? AND t.user_id = ? `, traderID, userID).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, &traderCreatedAt, &traderUpdatedAt, &aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey, &aiModel.CustomAPIURL, &aiModel.CustomModelName, &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.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 } // GetSystemConfig 获取系统配置 func (d *Database) GetSystemConfig(key string) (string, error) { var value string err := d.db.QueryRow(`SELECT value FROM system_config WHERE key = ?`, key).Scan(&value) return value, err } // SetSystemConfig 设置系统配置 func (d *Database) SetSystemConfig(key, value string) error { _, err := d.db.Exec(` INSERT OR REPLACE INTO system_config (key, value) VALUES (?, ?) `, key, value) return err } // CreateUserSignalSource 创建用户信号源配置 func (d *Database) CreateUserSignalSource(userID, coinPoolURL, oiTopURL string) error { _, err := d.db.Exec(` INSERT OR REPLACE INTO user_signal_sources (user_id, coin_pool_url, oi_top_url, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP) `, userID, coinPoolURL, oiTopURL) return err } // 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, &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 } // UpdateUserSignalSource 更新用户信号源配置 func (d *Database) UpdateUserSignalSource(userID, coinPoolURL, oiTopURL string) error { _, err := d.db.Exec(` UPDATE user_signal_sources SET coin_pool_url = ?, oi_top_url = ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ? `, coinPoolURL, oiTopURL, userID) return err } // GetCustomCoins 获取所有交易员自定义币种 / Get all trader-customized currencies func (d *Database) GetCustomCoins() []string { var symbol string var symbols []string _ = d.db.QueryRow(` SELECT GROUP_CONCAT(custom_coins , ',') as symbol FROM main.traders where custom_coins != '' `).Scan(&symbol) // 检测用户是否未配置币种 - 兼容性 if 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 } // Close 关闭数据库连接 // Conn 返回底层 *sql.DB,供需要执行自定义查询的模块使用。 func (d *Database) Conn() *sql.DB { return d.db } func (d *Database) Close() error { return d.db.Close() } // LoadBetaCodesFromFile 从文件加载内测码到数据库 func (d *Database) 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 OR IGNORE INTO beta_codes (code) VALUES (?)`) 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 *Database) ValidateBetaCode(code string) (bool, error) { var used bool err := d.db.QueryRow(`SELECT used FROM beta_codes WHERE code = ?`, code).Scan(&used) if err != nil { if err == sql.ErrNoRows { return false, nil // 内测码不存在 } return false, err } return !used, nil // 内测码存在且未使用 } // UseBetaCode 使用内测码(标记为已使用) func (d *Database) UseBetaCode(code, userEmail string) error { result, err := d.db.Exec(` UPDATE beta_codes SET used = 1, used_by = ?, used_at = CURRENT_TIMESTAMP WHERE code = ? AND used = 0 `, 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 *Database) 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 = 1`).Scan(&used) if err != nil { return 0, 0, err } return total, used, nil } // SetCryptoService 设置加密服务 func (d *Database) SetCryptoService(cs *crypto.CryptoService) { d.cryptoService = cs } // encryptSensitiveData 加密敏感数据用于存储 func (d *Database) encryptSensitiveData(plaintext string) string { if d.cryptoService == nil || plaintext == "" { return plaintext } encrypted, err := d.cryptoService.EncryptForStorage(plaintext) if err != nil { log.Printf("⚠️ 加密失败: %v", err) return plaintext // 返回明文作为降级处理 } return encrypted } // decryptSensitiveData 解密敏感数据 func (d *Database) decryptSensitiveData(encrypted string) string { if d.cryptoService == nil || encrypted == "" { return encrypted } // 如果不是加密格式,直接返回 if !d.cryptoService.IsEncryptedStorageValue(encrypted) { return encrypted } decrypted, err := d.cryptoService.DecryptFromStorage(encrypted) if err != nil { log.Printf("⚠️ 解密失败: %v", err) return encrypted // 返回加密文本作为降级处理 } return decrypted }