diff --git a/api/server.go b/api/server.go index b068d5aa..a10a39f6 100644 --- a/api/server.go +++ b/api/server.go @@ -180,7 +180,6 @@ func (s *Server) handleGetSystemConfig(c *gin.Context) { betaMode := betaModeStr == "true" c.JSON(http.StatusOK, gin.H{ - "admin_mode": auth.IsAdminMode(), "beta_mode": betaMode, "default_coins": defaultCoins, "btc_eth_leverage": btcEthLeverage, @@ -1459,14 +1458,6 @@ func (s *Server) handlePerformance(c *gin.Context) { // authMiddleware JWT认证中间件 func (s *Server) authMiddleware() gin.HandlerFunc { return func(c *gin.Context) { - // 如果是管理员模式,直接使用admin用户 - if auth.IsAdminMode() { - c.Set("user_id", "admin") - c.Set("email", "admin@localhost") - c.Next() - return - } - authHeader := c.GetHeader("Authorization") if authHeader == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少Authorization头"}) diff --git a/auth/auth.go b/auth/auth.go index 89c58e5c..fb1ff822 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -14,9 +14,6 @@ import ( // JWTSecret JWT密钥,将从配置中动态设置 var JWTSecret []byte -// AdminMode 管理员模式标志 -var AdminMode bool = false - // OTPIssuer OTP发行者名称 const OTPIssuer = "nofxAI" @@ -25,16 +22,6 @@ func SetJWTSecret(secret string) { JWTSecret = []byte(secret) } -// SetAdminMode 设置管理员模式 -func SetAdminMode(enabled bool) { - AdminMode = enabled -} - -// IsAdminMode 检查是否为管理员模式 -func IsAdminMode() bool { - return AdminMode -} - // Claims JWT声明 type Claims struct { UserID string `json:"user_id"` diff --git a/config.json.example b/config.json.example index 820f39a7..6a169ff4 100644 --- a/config.json.example +++ b/config.json.example @@ -1,5 +1,4 @@ { - "admin_mode": true, "beta_mode": false, "leverage": { "btc_eth_leverage": 5, @@ -24,4 +23,4 @@ "log": { "level": "info" } -} \ No newline at end of file +} diff --git a/config/database.go b/config/database.go index f5b6cc8c..a1f8da45 100644 --- a/config/database.go +++ b/config/database.go @@ -1,31 +1,13 @@ package config import ( - "crypto/rand" - "database/sql" - "encoding/base32" - "encoding/json" "fmt" - "log" - "nofx/market" - "os" - "slices" - "strings" "time" - - _ "github.com/lib/pq" - _ "github.com/mattn/go-sqlite3" ) -// Database 配置数据库 -type Database struct { - db *sql.DB -} - -// DatabaseInterface 数据库接口 +// DatabaseInterface 定义了数据库实现需要提供的方法集合 type DatabaseInterface interface { CreateUser(user *User) error - EnsureAdminUser() error GetUserByEmail(email string) (*User, error) GetUserByID(userID string) (*User, error) GetAllUsers() ([]string, error) @@ -57,362 +39,6 @@ type DatabaseInterface interface { Close() error } -// NewDatabase 创建配置数据库 -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) - } - - database := &Database{db: db} - if err := database.createTables(); err != nil { - return nil, fmt.Errorf("创建表失败: %w", err) - } - - if err := database.initDefaultData(); err != nil { - return nil, fmt.Errorf("初始化默认数据失败: %w", err) - } - - 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 '', - 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, - FOREIGN KEY (ai_model_id) REFERENCES ai_models(id), - FOREIGN KEY (exchange_id) REFERENCES exchanges(id) - )`, - - // 用户表 - `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 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 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) - } - - 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"}, - {"hyperliquid", "Hyperliquid", "hyperliquid"}, - {"aster", "Aster DEX", "aster"}, - } - - 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{ - "admin_mode": "true", // 默认开启管理员模式,便于首次使用 - "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或系统生成 - } - - 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 '', - 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 -} - // User 用户配置 type User struct { ID string `json:"id"` @@ -440,22 +66,20 @@ type AIModelConfig struct { // 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"` - SecretKey string `json:"secretKey"` - Testnet bool `json:"testnet"` - // Hyperliquid 特定字段 - HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"` - // Aster 特定字段 - AsterUser string `json:"asterUser"` - AsterSigner string `json:"asterSigner"` - AsterPrivateKey string `json:"asterPrivateKey"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + 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"` + SecretKey string `json:"secretKey"` + Testnet bool `json:"testnet"` + HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"` + AsterUser string `json:"asterUser"` + AsterSigner string `json:"asterSigner"` + AsterPrivateKey string `json:"asterPrivateKey"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // TraderRecord 交易员配置(数据库实体) @@ -468,15 +92,15 @@ type TraderRecord struct { 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=逐仓) + BTCETHLeverage int `json:"btc_eth_leverage"` + AltcoinLeverage int `json:"altcoin_leverage"` + TradingSymbols string `json:"trading_symbols"` + UseCoinPool bool `json:"use_coin_pool"` + UseOITop bool `json:"use_oi_top"` + CustomPrompt string `json:"custom_prompt"` + OverrideBasePrompt bool `json:"override_base_prompt"` + SystemPromptTemplate string `json:"system_prompt_template"` + IsCrossMargin bool `json:"is_cross_margin"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } @@ -491,694 +115,11 @@ type UserSignalSource struct { UpdatedAt time.Time `json:"updated_at"` } -// GenerateOTPSecret 生成OTP密钥 -func GenerateOTPSecret() (string, error) { - secret := make([]byte, 20) - _, err := rand.Read(secret) +// NewDatabase 创建数据库连接(仅支持 PostgreSQL) +func NewDatabase() (DatabaseInterface, error) { + pgDB, err := NewPostgreSQLDatabase() if err != nil { - return "", err + return nil, fmt.Errorf("创建PostgreSQL数据库失败: %w", 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 - err := d.db.QueryRow(` - SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at - FROM users WHERE email = ? - `, email).Scan( - &user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret, - &user.OTPVerified, &user.CreatedAt, &user.UpdatedAt, - ) - if err != nil { - return nil, err - } - return &user, nil -} - -// GetUserByID 通过ID获取用户 -func (d *Database) 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 = ? - `, 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 *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 -} - -// 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 - 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 *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),更新它 - _, 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, apiKey, 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) - _, 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, 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 = ? 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 (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now')) - `, newModelID, userID, name, provider, enabled, apiKey, 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, - 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 - 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 -} - -// GetExchangesForAPI 获取交易所配置(专用于API返回,排除敏感字段) -func (d *Database) GetExchangesForAPI(userID string) ([]*ExchangeConfig, error) { - rows, err := d.db.Query(` - SELECT id, user_id, name, type, enabled, - CASE - WHEN type = 'hyperliquid' THEN '' - ELSE COALESCE(api_key, '') - END as api_key, - '' as secret_key, - testnet, - COALESCE(hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr, - COALESCE(aster_user, '') as aster_user, - COALESCE(aster_signer, '') as aster_signer, - '' as aster_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 - 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 *Database) 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 = ?, api_key = ?, secret_key = ?, testnet = ?, - hyperliquid_wallet_addr = ?, aster_user = ?, aster_signer = ?, aster_private_key = ?, updated_at = datetime('now') - WHERE id = ? AND user_id = ? - `, 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now')) - `, 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 *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 { - _, 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) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, id, userID, name, typ, enabled, apiKey, secretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey) - 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 - 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 *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 = ?, initial_balance = ?, - 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.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 *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 - - 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 = ? 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, - &trader.CreatedAt, &trader.UpdatedAt, - &aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey, - &aiModel.CustomAPIURL, &aiModel.CustomModelName, - &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 *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 - err := d.db.QueryRow(` - SELECT id, user_id, coin_pool_url, oi_top_url, created_at, updated_at - FROM user_signal_sources WHERE user_id = ? - `, userID).Scan( - &source.ID, &source.UserID, &source.CoinPoolURL, &source.OITopURL, - &source.CreatedAt, &source.UpdatedAt, - ) - if err != nil { - return nil, err - } - 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 关闭数据库连接 -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 + return pgDB, nil } diff --git a/config/database_pg.go b/config/database_pg.go index ad11ca07..ef41bf07 100644 --- a/config/database_pg.go +++ b/config/database_pg.go @@ -52,6 +52,11 @@ func NewPostgreSQLDatabase() (*PostgreSQLDatabase, error) { database := &PostgreSQLDatabase{db: db} log.Printf("✅ PostgreSQL数据库连接成功") + // 初始化默认数据 + if err := database.initDefaultData(); err != nil { + return nil, fmt.Errorf("初始化默认数据失败: %w", err) + } + return database, nil } @@ -72,32 +77,6 @@ func (d *PostgreSQLDatabase) CreateUser(user *User) error { 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 @@ -701,7 +680,67 @@ func (d *PostgreSQLDatabase) GetBetaCodeStats() (total, used int, err error) { return total, used, nil } +// initDefaultData 初始化默认数据(AI模型和交易所) +func (d *PostgreSQLDatabase) initDefaultData() error { + // 确保traders表存在custom_coins列,防止旧环境缺少字段 + if _, err := d.db.Exec(`ALTER TABLE traders ADD COLUMN IF NOT EXISTS custom_coins TEXT DEFAULT ''`); err != nil { + return fmt.Errorf("添加custom_coins列失败: %w", err) + } + + // 首先创建default用户(如果不存在) + _, err := d.db.Exec(` + INSERT INTO users (id, email, password_hash, otp_secret, otp_verified) + VALUES ('default', 'default@localhost', '', '', true) + ON CONFLICT (id) DO NOTHING + `) + if err != nil { + return fmt.Errorf("创建default用户失败: %w", err) + } + + // 初始化AI模型(使用default用户) + aiModels := []struct { + id, name, provider string + }{ + {"deepseek", "DeepSeek", "deepseek"}, + {"qwen", "Qwen", "qwen"}, + } + + for _, model := range aiModels { + _, 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, 'default', $2, $3, false, '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + ON CONFLICT (id) DO NOTHING + `, 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"}, + {"hyperliquid", "Hyperliquid", "hyperliquid"}, + {"aster", "Aster DEX", "aster"}, + } + + for _, exchange := range exchanges { + _, 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, 'default', $2, $3, false, '', '', false, '', '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + ON CONFLICT (id, user_id) DO NOTHING + `, exchange.id, exchange.name, exchange.typ) + if err != nil { + return fmt.Errorf("初始化交易所失败: %w", err) + } + } + + return 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 index dbd9a335..4def4446 100644 --- a/db/init.sql +++ b/db/init.sql @@ -80,6 +80,7 @@ CREATE TABLE IF NOT EXISTS traders ( override_base_prompt BOOLEAN DEFAULT FALSE, system_prompt_template TEXT DEFAULT 'default', is_cross_margin BOOLEAN DEFAULT TRUE, + custom_coins TEXT DEFAULT '', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, @@ -133,6 +134,11 @@ CREATE TRIGGER update_system_config_updated_at BEFORE UPDATE ON system_config -- 插入默认数据 +-- 创建default用户(如果不存在) +INSERT INTO users (id, email, password_hash, otp_secret, otp_verified) VALUES +('default', 'default@localhost', '', '', TRUE) +ON CONFLICT (id) DO NOTHING; + -- 初始化AI模型(使用default用户) INSERT INTO ai_models (id, user_id, name, provider, enabled) VALUES ('deepseek', 'default', 'DeepSeek', 'deepseek', FALSE), @@ -148,7 +154,6 @@ 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'), @@ -166,4 +171,4 @@ 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 +CREATE INDEX IF NOT EXISTS idx_beta_codes_used ON beta_codes(used); diff --git a/docker-compose.yml b/docker-compose.yml index 31d5a474..7dcf36da 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -105,4 +105,4 @@ networks: volumes: postgres_data: - redis_data: \ No newline at end of file + redis_data: diff --git a/docs/architecture/README.md b/docs/architecture/README.md index fb233a31..e5147770 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -93,7 +93,7 @@ nofx/ | `github.com/gin-gonic/gin` | HTTP API framework | v1.9+ | | `github.com/adshao/go-binance/v2` | Binance API client | v2.4+ | | `github.com/markcheno/go-talib` | Technical indicators (TA-Lib) | Latest | -| `github.com/mattn/go-sqlite3` | SQLite database driver | v1.14+ | +| `github.com/lib/pq` | PostgreSQL database driver | v1.10+ | | `github.com/golang-jwt/jwt/v5` | JWT authentication | v5.0+ | | `github.com/pquerna/otp` | 2FA/TOTP support | v1.4+ | | `golang.org/x/crypto` | Password hashing (bcrypt) | Latest | diff --git a/docs/architecture/README.zh-CN.md b/docs/architecture/README.zh-CN.md index 4acc0f90..0ce428fc 100644 --- a/docs/architecture/README.zh-CN.md +++ b/docs/architecture/README.zh-CN.md @@ -93,7 +93,7 @@ nofx/ | `github.com/gin-gonic/gin` | HTTP API 框架 | v1.9+ | | `github.com/adshao/go-binance/v2` | Binance API 客户端 | v2.4+ | | `github.com/markcheno/go-talib` | 技术指标(TA-Lib) | 最新 | -| `github.com/mattn/go-sqlite3` | SQLite 数据库驱动 | v1.14+ | +| `github.com/lib/pq` | PostgreSQL 数据库驱动 | v1.10+ | | `github.com/golang-jwt/jwt/v5` | JWT 认证 | v5.0+ | | `github.com/pquerna/otp` | 2FA/TOTP 支持 | v1.4+ | | `golang.org/x/crypto` | 密码哈希(bcrypt) | 最新 | @@ -282,7 +282,6 @@ GET /api/decisions/latest # 最近决策 - 基于 JWT token 的认证 - 使用 TOTP 的 2FA(Google Authenticator) - Bcrypt 密码哈希 -- 管理员模式(简化的单用户模式) --- diff --git a/docs/guides/faq.en.md b/docs/guides/faq.en.md index abe3dd5c..644de323 100644 --- a/docs/guides/faq.en.md +++ b/docs/guides/faq.en.md @@ -152,18 +152,17 @@ Yes, to some extent. NOFX provides historical performance feedback in each decis ## Data & Privacy ### Where is my data stored? -All data is stored **locally** on your machine in SQLite databases: -- `config.db` - Trader configurations -- `trading.db` - Trade history +All data is stored **locally** in PostgreSQL (Docker volume `postgres_data`) plus: - `decision_logs/` - AI decision records ### Is my API key secure? API keys are stored in local databases. Never share your databases or `.env` files. We recommend using API keys with IP whitelist restrictions. ### Can I export my trading history? -Yes! Trading data is in SQLite format. You can query it directly: +Yes! Use `pg_dump` or `psql` to export data: ```bash -sqlite3 trading.db "SELECT * FROM trades;" +docker compose exec postgres \ + psql -U nofx -d nofx -c "SELECT * FROM trades;" ``` --- diff --git a/docs/guides/faq.zh-CN.md b/docs/guides/faq.zh-CN.md index 2c74ca89..b33a4282 100644 --- a/docs/guides/faq.zh-CN.md +++ b/docs/guides/faq.zh-CN.md @@ -152,18 +152,17 @@ docker compose up -d ## 数据与隐私 ### 我的数据存储在哪里? -所有数据都**本地存储**在您的机器上,使用 SQLite 数据库: -- `config.db` - 交易员配置 -- `trading.db` - 交易历史 +所有数据都**本地存储**在 PostgreSQL(Docker 卷 `postgres_data`)中,另有: - `decision_logs/` - AI 决策记录 ### API 密钥安全吗? API 密钥存储在本地数据库中。永远不要分享您的数据库或 `.env` 文件。我们建议使用带 IP 白名单限制的 API 密钥。 ### 可以导出交易历史吗? -可以!交易数据是 SQLite 格式。您可以直接查询: +可以!使用 `pg_dump` 或 `psql` 导出数据: ```bash -sqlite3 trading.db "SELECT * FROM trades;" +docker compose exec postgres \ + psql -U nofx -d nofx -c "SELECT * FROM trades;" ``` --- diff --git a/go.mod b/go.mod index 9b0f63e9..99f0147b 100644 --- a/go.mod +++ b/go.mod @@ -11,12 +11,10 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/lib/pq v1.10.9 - github.com/mattn/go-sqlite3 v1.14.32 github.com/pquerna/otp v1.4.0 github.com/sirupsen/logrus v1.9.3 github.com/sonirico/go-hyperliquid v0.17.0 golang.org/x/crypto v0.42.0 - modernc.org/sqlite v1.40.0 ) require ( @@ -31,7 +29,6 @@ require ( github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect github.com/elastic/go-sysinfo v1.15.4 // indirect github.com/elastic/go-windows v1.0.2 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect @@ -55,13 +52,11 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/procfs v0.17.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.54.0 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rs/zerolog v1.34.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sonirico/vago v0.9.0 // indirect @@ -77,7 +72,6 @@ require ( go.elastic.co/fastjson v1.5.1 // indirect go.uber.org/mock v0.5.0 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/mod v0.27.0 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.17.0 // indirect @@ -86,7 +80,4 @@ require ( golang.org/x/tools v0.36.0 // indirect google.golang.org/protobuf v1.36.9 // indirect howett.net/plist v1.0.1 // indirect - modernc.org/libc v1.66.10 // indirect - modernc.org/mathutil v1.7.1 // indirect - modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index 2494cec0..46f4473f 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,6 @@ github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elastic/go-sysinfo v1.15.4 h1:A3zQcunCxik14MgXu39cXFXcIw2sFXZ0zL886eyiv1Q= github.com/elastic/go-sysinfo v1.15.4/go.mod h1:ZBVXmqS368dOn/jvijV/zHLfakWTYHBZPk3G244lHrU= github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI= @@ -84,8 +82,6 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= @@ -126,8 +122,6 @@ 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.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= @@ -136,8 +130,6 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -154,8 +146,6 @@ github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -215,8 +205,6 @@ golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= @@ -246,29 +234,3 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= -modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= -modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= -modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= -modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= -modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= -modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= -modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= -modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= -modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= -modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= -modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= -modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= -modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= -modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= -modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.40.0 h1:bNWEDlYhNPAUdUdBzjAvn8icAs/2gaKlj4vM+tQ6KdQ= -modernc.org/sqlite v1.40.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= -modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= -modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= -modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/import_default_patch.sh b/import_default_patch.sh deleted file mode 100755 index 4a00d3d4..00000000 --- a/import_default_patch.sh +++ /dev/null @@ -1,150 +0,0 @@ -#!/bin/bash - -# 从SQLite导入default用户和系统默认数据到PostgreSQL -echo "🔧 从SQLite导入default用户和系统默认数据" -echo "========================================" - -# 检查SQLite数据库文件 -SQLITE_DB="config.db" -if [ ! -f "$SQLITE_DB" ]; then - echo "❌ 错误:找不到 SQLite 数据库文件 $SQLITE_DB" - exit 1 -fi - -# 检测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 "❌ 错误:找不到 docker-compose 或 docker compose 命令" - exit 1 -fi - -echo "📋 使用命令: $DOCKER_COMPOSE_CMD" - -# 分析SQLite中的default用户数据 -echo "📊 分析SQLite中的default用户数据..." -AI_MODEL_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM ai_models WHERE user_id = 'default';" 2>/dev/null || echo "0") -EXCHANGE_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM exchanges WHERE user_id = 'default';" 2>/dev/null || echo "0") - -echo " 🤖 AI模型: $AI_MODEL_COUNT 个" -echo " 🏢 交易所: $EXCHANGE_COUNT 个" - -if [ "$AI_MODEL_COUNT" -eq 0 ] && [ "$EXCHANGE_COUNT" -eq 0 ]; then - echo "⚠️ SQLite中没有default用户的数据,将跳过导入" - exit 0 -fi - -# 生成导入脚本 -IMPORT_SQL="import_default_data.sql" - -cat > $IMPORT_SQL << EOL --- 从SQLite导入default用户和系统默认数据 --- 生成时间: $(date) - --- 设置时区 -SET timezone = 'Asia/Shanghai'; - --- 1. 创建default用户(如果不存在) -INSERT INTO users (id, email, password_hash, otp_secret, otp_verified, created_at, updated_at) -VALUES ('default', 'default@localhost', '', '', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) -ON CONFLICT (id) DO NOTHING; - -EOL - -# 导出AI模型数据 -if [ "$AI_MODEL_COUNT" -gt 0 ]; then - echo "🤖 导出AI模型数据..." - echo "-- AI模型数据 ($AI_MODEL_COUNT 条记录)" >> $IMPORT_SQL - echo "INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at) VALUES" >> $IMPORT_SQL - - sqlite3 $SQLITE_DB "SELECT '(' || quote(id) || ', ' || quote(user_id) || ', ' || quote(name) || ', ' || quote(provider) || ', ' || - CASE WHEN enabled = 1 THEN 'true' ELSE 'false' END || ', ' || quote(COALESCE(api_key, '')) || ', ' || quote(COALESCE(custom_api_url, '')) || ', ' || - quote(COALESCE(custom_model_name, '')) || ', ' || quote(created_at) || ', ' || quote(updated_at) || '),' - FROM ai_models WHERE user_id = 'default';" | sed '$ s/,$//' >> $IMPORT_SQL - - echo "ON CONFLICT (id) DO UPDATE SET user_id = EXCLUDED.user_id, name = EXCLUDED.name, provider = EXCLUDED.provider, enabled = EXCLUDED.enabled, api_key = EXCLUDED.api_key, custom_api_url = EXCLUDED.custom_api_url, custom_model_name = EXCLUDED.custom_model_name, updated_at = EXCLUDED.updated_at;" >> $IMPORT_SQL - echo "" >> $IMPORT_SQL -fi - -# 导出交易所数据 -if [ "$EXCHANGE_COUNT" -gt 0 ]; then - echo "🏢 导出交易所数据..." - echo "-- 交易所数据 ($EXCHANGE_COUNT 条记录)" >> $IMPORT_SQL - echo "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" >> $IMPORT_SQL - - sqlite3 $SQLITE_DB "SELECT '(' || quote(id) || ', ' || quote(user_id) || ', ' || quote(name) || ', ' || quote(type) || ', ' || - CASE WHEN enabled = 1 THEN 'true' ELSE 'false' END || ', ' || quote(COALESCE(api_key, '')) || ', ' || quote(COALESCE(secret_key, '')) || ', ' || - CASE WHEN testnet = 1 THEN 'true' ELSE 'false' END || ', ' || quote(COALESCE(hyperliquid_wallet_addr, '')) || ', ' || - quote(COALESCE(aster_user, '')) || ', ' || quote(COALESCE(aster_signer, '')) || ', ' || quote(COALESCE(aster_private_key, '')) || ', ' || - quote(created_at) || ', ' || quote(updated_at) || '),' - FROM exchanges WHERE user_id = 'default';" | sed '$ s/,$//' >> $IMPORT_SQL - - echo "ON CONFLICT (id, user_id) DO UPDATE SET name = EXCLUDED.name, type = EXCLUDED.type, enabled = EXCLUDED.enabled, api_key = EXCLUDED.api_key, secret_key = EXCLUDED.secret_key, testnet = EXCLUDED.testnet, hyperliquid_wallet_addr = EXCLUDED.hyperliquid_wallet_addr, aster_user = EXCLUDED.aster_user, aster_signer = EXCLUDED.aster_signer, aster_private_key = EXCLUDED.aster_private_key, updated_at = EXCLUDED.updated_at;" >> $IMPORT_SQL - echo "" >> $IMPORT_SQL -fi - -# 添加验证查询 -cat >> $IMPORT_SQL << 'EOL' --- 验证导入结果 -SELECT '=== 导入完成验证 ===' as status; -SELECT 'default用户' as item, COUNT(*) as count FROM users WHERE id = 'default' -UNION ALL SELECT 'AI模型', COUNT(*) FROM ai_models WHERE user_id = 'default' -UNION ALL SELECT '交易所', COUNT(*) FROM exchanges WHERE user_id = 'default'; -EOL - -echo "✅ 生成导入脚本: $IMPORT_SQL" - -# 确认执行 -echo -echo "⚠️ 准备导入default用户和系统默认数据,包括:" -echo " 1. default用户账户" -echo " 2. $AI_MODEL_COUNT 个AI模型" -echo " 3. $EXCHANGE_COUNT 个交易所" -echo -read -p "确认执行导入? (y/N): " confirm - -if [[ $confirm != [yY] ]]; then - echo "ℹ️ 已取消导入" - echo "手动执行命令: $DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx -f /tmp/$IMPORT_SQL" - exit 0 -fi - -# 检查PostgreSQL连接 -echo "🔌 检查数据库连接..." -if ! $DOCKER_COMPOSE_CMD exec postgres pg_isready -U nofx > /dev/null 2>&1; then - echo "❌ PostgreSQL连接失败,请确保服务正在运行" - exit 1 -fi - -# 复制SQL脚本到容器 -echo "📦 复制导入脚本到容器..." -POSTGRES_CONTAINER=$($DOCKER_COMPOSE_CMD ps -q postgres) -if [ -z "$POSTGRES_CONTAINER" ]; then - echo "❌ 找不到PostgreSQL容器" - exit 1 -fi - -docker cp $IMPORT_SQL ${POSTGRES_CONTAINER}:/tmp/$IMPORT_SQL - -# 执行导入 -echo "🔄 执行数据导入..." -if $DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx --pset pager=off -f /tmp/$IMPORT_SQL; then - echo - echo "✅ default用户和系统默认数据导入成功!" - echo - echo "📋 现在可以访问以下接口:" - echo " - GET /api/supported-models ($AI_MODEL_COUNT 个AI模型)" - echo " - GET /api/supported-exchanges ($EXCHANGE_COUNT 个交易所)" - echo - echo "🧹 清理导入文件..." - rm -f $IMPORT_SQL - $DOCKER_COMPOSE_CMD exec postgres rm -f /tmp/$IMPORT_SQL -else - echo "❌ 数据导入失败" - exit 1 -fi - -echo "🎉 导入完成!" diff --git a/main.go b/main.go index 40ad8136..73dbab1b 100644 --- a/main.go +++ b/main.go @@ -25,7 +25,6 @@ type LeverageConfig struct { // ConfigFile 配置文件结构,只包含需要同步到数据库的字段 type ConfigFile struct { - AdminMode bool `json:"admin_mode"` BetaMode bool `json:"beta_mode"` APIServerPort int `json:"api_server_port"` UseDefaultCoins bool `json:"use_default_coins"` @@ -74,7 +73,6 @@ func syncConfigToDatabase(database config.DatabaseInterface, configFile *ConfigF // 同步各配置项到数据库 configs := map[string]string{ - "admin_mode": fmt.Sprintf("%t", configFile.AdminMode), "beta_mode": fmt.Sprintf("%t", configFile.BetaMode), "api_server_port": strconv.Itoa(configFile.APIServerPort), "use_default_coins": fmt.Sprintf("%t", configFile.UseDefaultCoins), @@ -160,20 +158,14 @@ func main() { fmt.Println("╚════════════════════════════════════════════════════════════╝") fmt.Println() - // 初始化数据库配置 - dbPath := "config.db" - if len(os.Args) > 1 { - dbPath = os.Args[1] - } - // 读取配置文件 configFile, err := loadConfigFile() if err != nil { log.Fatalf("❌ 读取config.json失败: %v", err) } - log.Printf("📋 初始化配置数据库: %s", dbPath) - database, err := config.NewDatabase(dbPath) + log.Printf("📋 初始化配置数据库 (PostgreSQL)") + database, err := config.NewDatabase() if err != nil { log.Fatalf("❌ 初始化数据库失败: %v", err) } @@ -194,10 +186,6 @@ func main() { useDefaultCoins := useDefaultCoinsStr == "true" apiPortStr, _ := database.GetSystemConfig("api_server_port") - // 获取管理员模式配置 - adminModeStr, _ := database.GetSystemConfig("admin_mode") - adminMode := adminModeStr != "false" // 默认为true - // 设置JWT密钥 jwtSecret, _ := database.GetSystemConfig("jwt_secret") if jwtSecret == "" { @@ -206,17 +194,6 @@ func main() { } auth.SetJWTSecret(jwtSecret) - // 在管理员模式下,确保admin用户存在 - if adminMode { - err := database.EnsureAdminUser() - if err != nil { - log.Printf("⚠️ 创建admin用户失败: %v", err) - } else { - log.Printf("✓ 管理员模式已启用,无需登录") - } - auth.SetAdminMode(true) - } - log.Printf("✓ 配置数据库初始化成功") fmt.Println() diff --git a/manager/trader_manager.go b/manager/trader_manager.go index 483421cb..e039b294 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -286,7 +286,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel // AddTrader 从数据库配置添加trader (移除旧版兼容性) // AddTraderFromDB 从数据库配置添加trader -func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string, database *config.Database, userID string) error { +func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string, 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 deleted file mode 100644 index 812594b3..00000000 --- a/migrate_actual_data.sql +++ /dev/null @@ -1,115 +0,0 @@ --- 实际数据迁移脚本 - 从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 deleted file mode 100644 index 0f946cc1..00000000 --- a/migrate_data.sql +++ /dev/null @@ -1,49 +0,0 @@ --- 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 deleted file mode 100755 index b2a00406..00000000 --- a/migrate_to_postgres.sh +++ /dev/null @@ -1,330 +0,0 @@ -#!/bin/bash - -# 生产环境 SQLite -> PostgreSQL 数据迁移脚本 -# 真实数据迁移工具 - 支持完整数据导出和迁移 - -set -e - -# 颜色定义 -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -NC='\033[0m' - -echo "╔══════════════════════════════════════════════════════════════╗" -echo "║ 🚀 生产环境数据迁移工具 SQLite → PostgreSQL ║" -echo "╚══════════════════════════════════════════════════════════════╝" -echo - -# 检查必要文件 -SQLITE_DB="config.db" -if [ ! -f "$SQLITE_DB" ]; then - echo -e "${RED}❌ 错误:找不到 SQLite 数据库文件 $SQLITE_DB${NC}" - exit 1 -fi - -# 检测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}" - exit 1 -fi - -echo -e "${CYAN}📋 使用命令: ${DOCKER_COMPOSE_CMD}${NC}" - -# 分析当前SQLite数据 -echo -e "\n${BLUE}📊 分析当前SQLite数据库...${NC}" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - -# 获取数据统计 -USER_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM users;" 2>/dev/null || echo "0") -AI_MODEL_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM ai_models;" 2>/dev/null || echo "0") -EXCHANGE_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM exchanges;" 2>/dev/null || echo "0") -TRADER_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM traders;" 2>/dev/null || echo "0") -CONFIG_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM system_config;" 2>/dev/null || echo "0") -BETA_CODE_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM beta_codes;" 2>/dev/null || echo "0") - -echo "📈 数据库表统计:" -echo " 👥 用户 (users): $USER_COUNT" -echo " 🤖 AI模型 (ai_models): $AI_MODEL_COUNT" -echo " 🏢 交易所 (exchanges): $EXCHANGE_COUNT" -echo " 🔧 交易员 (traders): $TRADER_COUNT" -echo " ⚙️ 系统配置 (system_config): $CONFIG_COUNT" -echo " 🎟️ 内测码 (beta_codes): $BETA_CODE_COUNT" - -# 检查是否有exchange_secrets表 -if sqlite3 $SQLITE_DB "SELECT name FROM sqlite_master WHERE type='table' AND name='exchange_secrets';" | grep -q exchange_secrets; then - SECRET_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM exchange_secrets;" 2>/dev/null || echo "0") - echo " 🔐 交易所密钥 (exchange_secrets): $SECRET_COUNT" -fi - -# 检查是否有user_signal_sources表 -if sqlite3 $SQLITE_DB "SELECT name FROM sqlite_master WHERE type='table' AND name='user_signal_sources';" | grep -q user_signal_sources; then - SIGNAL_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM user_signal_sources;" 2>/dev/null || echo "0") - echo " 📡 用户信号源 (user_signal_sources): $SIGNAL_COUNT" -fi - -echo - -# 生成迁移时间戳 -TIMESTAMP=$(date '+%Y-%m-%d_%H-%M-%S') -MIGRATION_FILE="migrate_production_${TIMESTAMP}.sql" - -echo -e "${BLUE}📤 生成数据迁移脚本: $MIGRATION_FILE${NC}" - -# 生成SQL迁移脚本 -cat > $MIGRATION_FILE << EOL --- 生产环境数据迁移脚本 --- 从SQLite自动导出生成 --- 执行时间: $(date) - --- 设置时区 -SET timezone = 'Asia/Shanghai'; - -EOL - -# 导出用户数据 -if [ "$USER_COUNT" -gt 0 ]; then - echo -e "${CYAN}👥 导出用户数据...${NC}" - echo "-- 用户数据 ($USER_COUNT 条记录)" >> $MIGRATION_FILE - echo "INSERT INTO users (id, email, password_hash, otp_secret, otp_verified, created_at, updated_at) VALUES" >> $MIGRATION_FILE - - sqlite3 $SQLITE_DB "SELECT '(' || quote(id) || ', ' || quote(COALESCE(email, '')) || ', ' || quote(COALESCE(password_hash, '')) || ', ' || quote(COALESCE(otp_secret, '')) || ', ' || - CASE WHEN otp_verified = 1 THEN 'true' ELSE 'false' END || ', ' || quote(created_at) || ', ' || quote(updated_at) || '),' - FROM users;" | sed '$ s/,$//' >> $MIGRATION_FILE - - echo "ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email, password_hash = EXCLUDED.password_hash, otp_secret = EXCLUDED.otp_secret, otp_verified = EXCLUDED.otp_verified, updated_at = EXCLUDED.updated_at;" >> $MIGRATION_FILE - echo "" >> $MIGRATION_FILE -fi - -# 导出AI模型数据 -if [ "$AI_MODEL_COUNT" -gt 0 ]; then - echo -e "${CYAN}🤖 导出AI模型数据...${NC}" - echo "-- AI模型数据 ($AI_MODEL_COUNT 条记录)" >> $MIGRATION_FILE - echo "INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at) VALUES" >> $MIGRATION_FILE - - sqlite3 $SQLITE_DB "SELECT '(' || quote(id) || ', ' || quote(user_id) || ', ' || - quote(name) || ', ' || quote(provider) || ', ' || - CASE WHEN enabled = 1 THEN 'true' ELSE 'false' END || ', ' || quote(api_key) || ', ' || quote(COALESCE(custom_api_url, '')) || ', ' || - quote(COALESCE(custom_model_name, '')) || ', ' || quote(created_at) || ', ' || quote(updated_at) || '),' - FROM ai_models WHERE user_id IS NOT NULL AND user_id != '' AND user_id != 'default' - AND user_id IN (SELECT id FROM users);" | sed '$ s/,$//' >> $MIGRATION_FILE - - echo "ON CONFLICT (id) DO UPDATE SET user_id = EXCLUDED.user_id, name = EXCLUDED.name, provider = EXCLUDED.provider, enabled = EXCLUDED.enabled, api_key = EXCLUDED.api_key, custom_api_url = EXCLUDED.custom_api_url, custom_model_name = EXCLUDED.custom_model_name, updated_at = EXCLUDED.updated_at;" >> $MIGRATION_FILE - echo "" >> $MIGRATION_FILE -fi - -# 导出交易所数据 -if [ "$EXCHANGE_COUNT" -gt 0 ]; then - echo -e "${CYAN}🏢 导出交易所数据...${NC}" - echo "-- 交易所数据 ($EXCHANGE_COUNT 条记录)" >> $MIGRATION_FILE - echo "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" >> $MIGRATION_FILE - - sqlite3 $SQLITE_DB "SELECT '(' || quote(id) || ', ' || quote(user_id) || ', ' || - quote(name) || ', ' || quote(type) || ', ' || - CASE WHEN enabled = 1 THEN 'true' ELSE 'false' END || ', ' || quote(COALESCE(api_key, '')) || ', ' || quote(COALESCE(secret_key, '')) || ', ' || - CASE WHEN testnet = 1 THEN 'true' ELSE 'false' END || ', ' || quote(COALESCE(hyperliquid_wallet_addr, '')) || ', ' || - quote(COALESCE(aster_user, '')) || ', ' || quote(COALESCE(aster_signer, '')) || ', ' || quote(COALESCE(aster_private_key, '')) || ', ' || - quote(created_at) || ', ' || quote(updated_at) || '),' - FROM exchanges WHERE user_id IS NOT NULL AND user_id != '' AND user_id != 'default' - AND user_id IN (SELECT id FROM users);" | sed '$ s/,$//' >> $MIGRATION_FILE - - echo "ON CONFLICT (id, user_id) DO UPDATE SET name = EXCLUDED.name, type = EXCLUDED.type, enabled = EXCLUDED.enabled, api_key = EXCLUDED.api_key, secret_key = EXCLUDED.secret_key, testnet = EXCLUDED.testnet, hyperliquid_wallet_addr = EXCLUDED.hyperliquid_wallet_addr, aster_user = EXCLUDED.aster_user, aster_signer = EXCLUDED.aster_signer, aster_private_key = EXCLUDED.aster_private_key, updated_at = EXCLUDED.updated_at;" >> $MIGRATION_FILE - echo "" >> $MIGRATION_FILE -fi - -# 导出交易员数据 -if [ "$TRADER_COUNT" -gt 0 ]; then - echo -e "${CYAN}🔧 导出交易员数据...${NC}" - echo "-- 交易员数据 ($TRADER_COUNT 条记录)" >> $MIGRATION_FILE - echo "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" >> $MIGRATION_FILE - - sqlite3 $SQLITE_DB "SELECT '(' || quote(id) || ', ' || quote(user_id) || ', ' || - quote(name) || ', ' || quote(ai_model_id) || ', ' || - quote(exchange_id) || ', ' || initial_balance || ', ' || scan_interval_minutes || ', ' || - CASE WHEN is_running = 1 THEN 'true' ELSE 'false' END || ', ' || btc_eth_leverage || ', ' || altcoin_leverage || ', ' || - quote(COALESCE(trading_symbols, '')) || ', ' || - CASE WHEN use_coin_pool = 1 THEN 'true' ELSE 'false' END || ', ' || CASE WHEN use_oi_top = 1 THEN 'true' ELSE 'false' END || ', ' || - quote(COALESCE(custom_prompt, '')) || ', ' || CASE WHEN override_base_prompt = 1 THEN 'true' ELSE 'false' END || ', ' || - quote(COALESCE(system_prompt_template, 'default')) || ', ' || CASE WHEN is_cross_margin = 1 THEN 'true' ELSE 'false' END || ', ' || - quote(created_at) || ', ' || quote(updated_at) || '),' - FROM traders WHERE user_id IS NOT NULL AND user_id != '' AND user_id != 'default' - AND user_id IN (SELECT id FROM users);" | sed '$ s/,$//' >> $MIGRATION_FILE - - echo "ON CONFLICT (id) DO UPDATE SET user_id = EXCLUDED.user_id, name = EXCLUDED.name, ai_model_id = EXCLUDED.ai_model_id, exchange_id = EXCLUDED.exchange_id, initial_balance = EXCLUDED.initial_balance, scan_interval_minutes = EXCLUDED.scan_interval_minutes, is_running = EXCLUDED.is_running, btc_eth_leverage = EXCLUDED.btc_eth_leverage, altcoin_leverage = EXCLUDED.altcoin_leverage, trading_symbols = EXCLUDED.trading_symbols, use_coin_pool = EXCLUDED.use_coin_pool, use_oi_top = EXCLUDED.use_oi_top, custom_prompt = EXCLUDED.custom_prompt, override_base_prompt = EXCLUDED.override_base_prompt, system_prompt_template = EXCLUDED.system_prompt_template, is_cross_margin = EXCLUDED.is_cross_margin, updated_at = EXCLUDED.updated_at;" >> $MIGRATION_FILE - echo "" >> $MIGRATION_FILE -fi - -# 导出系统配置 -if [ "$CONFIG_COUNT" -gt 0 ]; then - echo -e "${CYAN}⚙️ 导出系统配置...${NC}" - echo "-- 系统配置数据 ($CONFIG_COUNT 条记录)" >> $MIGRATION_FILE - echo "INSERT INTO system_config (key, value, updated_at) VALUES" >> $MIGRATION_FILE - - sqlite3 $SQLITE_DB "SELECT '(' || quote(key) || ', ' || quote(value) || ', ' || quote(updated_at) || '),' - FROM system_config;" | sed '$ s/,$//' >> $MIGRATION_FILE - - echo "ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = EXCLUDED.updated_at;" >> $MIGRATION_FILE - echo "" >> $MIGRATION_FILE -fi - -# 导出内测码数据 -if [ "$BETA_CODE_COUNT" -gt 0 ]; then - echo -e "${CYAN}🎟️ 导出内测码数据...${NC}" - echo "-- 内测码数据 ($BETA_CODE_COUNT 条记录)" >> $MIGRATION_FILE - echo "INSERT INTO beta_codes (code, used, used_by, used_at, created_at) VALUES" >> $MIGRATION_FILE - - sqlite3 $SQLITE_DB "SELECT '(' || quote(code) || ', ' || CASE WHEN used = 1 THEN 'true' ELSE 'false' END || ', ' || - quote(COALESCE(used_by, '')) || ', ' || CASE WHEN used_at IS NULL THEN 'NULL' ELSE quote(used_at) END || ', ' || - quote(created_at) || '),' - FROM beta_codes;" | sed '$ s/,$//' >> $MIGRATION_FILE - - echo "ON CONFLICT (code) DO UPDATE SET used = EXCLUDED.used, used_by = EXCLUDED.used_by, used_at = EXCLUDED.used_at;" >> $MIGRATION_FILE - echo "" >> $MIGRATION_FILE -fi - -# 导出用户信号源(如果存在) -if sqlite3 $SQLITE_DB "SELECT name FROM sqlite_master WHERE type='table' AND name='user_signal_sources';" | grep -q user_signal_sources; then - SIGNAL_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM user_signal_sources;" 2>/dev/null || echo "0") - if [ "$SIGNAL_COUNT" -gt 0 ]; then - echo -e "${CYAN}📡 导出用户信号源数据...${NC}" - echo "-- 用户信号源数据 ($SIGNAL_COUNT 条记录)" >> $MIGRATION_FILE - echo "INSERT INTO user_signal_sources (user_id, coin_pool_url, oi_top_url, created_at, updated_at) VALUES" >> $MIGRATION_FILE - - sqlite3 $SQLITE_DB "SELECT '(' || quote(user_id) || ', ' || - quote(COALESCE(coin_pool_url, '')) || ', ' || - quote(COALESCE(oi_top_url, '')) || ', ' || quote(created_at) || ', ' || quote(updated_at) || '),' - FROM user_signal_sources WHERE user_id IS NOT NULL AND user_id != '' AND user_id != 'default' - AND user_id IN (SELECT id FROM users);" | sed '$ s/,$//' >> $MIGRATION_FILE - - echo "ON CONFLICT (user_id) DO UPDATE SET coin_pool_url = EXCLUDED.coin_pool_url, oi_top_url = EXCLUDED.oi_top_url, updated_at = EXCLUDED.updated_at;" >> $MIGRATION_FILE - echo "" >> $MIGRATION_FILE - fi -fi - -# 添加迁移验证查询 -cat >> $MIGRATION_FILE << 'EOL' --- 迁移验证查询 -SELECT '=== 数据迁移完成验证 ===' as status; -SELECT 'users' as table_name, COUNT(*) as record_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 -UNION ALL SELECT 'user_signal_sources', COUNT(*) FROM user_signal_sources -ORDER BY table_name; - --- 显示关键配置 -SELECT '=== 关键系统配置 ===' as info; -SELECT key, - CASE WHEN LENGTH(value) > 50 THEN LEFT(value, 50) || '...' ELSE value END as value -FROM system_config -WHERE key IN ('admin_mode', 'beta_mode', 'api_server_port', 'default_coins', 'jwt_secret') -ORDER BY key; -EOL - -echo -e "${GREEN}✅ 迁移脚本生成完成: $MIGRATION_FILE${NC}" - -# 确认是否执行迁移 -echo -echo -e "${YELLOW}⚠️ 准备执行数据迁移,这将:${NC}" -echo " 1. 停止现有服务" -echo " 2. 启动PostgreSQL和Redis" -echo " 3. 执行数据迁移" -echo " 4. 验证迁移结果" -echo -read -p "确认执行迁移? (y/N): " confirm - -if [[ $confirm != [yY] ]]; then - echo -e "${BLUE}ℹ️ 迁移脚本已生成,可稍后手动执行${NC}" - echo "手动执行命令: $DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx -f /tmp/$MIGRATION_FILE" - exit 0 -fi - -# 执行迁移 -echo -e "\n${BLUE}🚀 开始执行数据迁移...${NC}" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - -# 停止现有服务 -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=15 -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 3 - 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 $MIGRATION_FILE ${POSTGRES_CONTAINER}:/tmp/$MIGRATION_FILE - -# 验证文件复制成功 -if ! $DOCKER_COMPOSE_CMD exec postgres test -f /tmp/$MIGRATION_FILE; then - echo -e "${RED}❌ 迁移脚本复制失败${NC}" - exit 1 -fi - -# 执行数据迁移 -echo -e "${BLUE}🔄 执行数据迁移...${NC}" -if $DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx --pset pager=off -f /tmp/$MIGRATION_FILE; then - echo -e "${GREEN}✅ 数据迁移成功!${NC}" -else - echo -e "${RED}❌ 数据迁移失败${NC}" - echo "查看错误日志: $DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx -c \"SELECT version();\"" - exit 1 -fi - -echo -echo -e "${GREEN}🎉 生产环境数据迁移完成!${NC}" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo -e "${BLUE}📋 后续步骤:${NC}" -echo -e "1. 备份原SQLite: ${YELLOW}mv config.db config.db.backup.$(date +%Y%m%d)${NC}" -echo -e "2. 启动完整应用: ${YELLOW}$DOCKER_COMPOSE_CMD up${NC}" -echo -e "3. 验证功能: 访问 ${YELLOW}http://localhost:3000${NC}" -echo -e "4. 删除迁移文件: ${YELLOW}rm $MIGRATION_FILE${NC}" -echo -echo -e "${BLUE}🔧 如需回滚:${NC}" -echo -e "1. 停止服务: ${YELLOW}$DOCKER_COMPOSE_CMD down${NC}" -echo -e "2. 恢复SQLite: ${YELLOW}mv config.db.backup.$(date +%Y%m%d) config.db${NC}" -echo -e "3. 删除环境变量或编辑 .env 文件注释掉 POSTGRES_HOST" -echo -e "4. 重启: ${YELLOW}$DOCKER_COMPOSE_CMD up${NC}" -echo -echo -e "${GREEN}🚀 PostgreSQL生产环境迁移成功!${NC}" diff --git a/scripts/import_default_patch.sh b/scripts/import_default_patch.sh new file mode 100755 index 00000000..ef971edd --- /dev/null +++ b/scripts/import_default_patch.sh @@ -0,0 +1,160 @@ +#!/bin/bash + +set -euo pipefail + +echo "🔧 同步默认用户与基础配置" +echo "===============================" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$ROOT_DIR" + +# 检测 Docker Compose 命令 +if command -v docker-compose &> /dev/null; then + DOCKER_COMPOSE_CMD="docker-compose" +elif docker compose version &> /dev/null; then + DOCKER_COMPOSE_CMD="docker compose" +else + echo "❌ 无法找到 docker-compose 或 docker compose 命令" + exit 1 +fi + +echo "📋 使用命令: $DOCKER_COMPOSE_CMD" + +# 加载 .env 配置 +ENV_FILE=".env" +if [ -f "$ENV_FILE" ]; then + echo "📁 加载 .env ..." + set -a + # shellcheck disable=SC1090 + source "$ENV_FILE" + set +a +else + echo "⚠️ 未找到 .env,使用默认数据库配置" +fi + +POSTGRES_HOST=${POSTGRES_HOST:-postgres} +POSTGRES_PORT=${POSTGRES_PORT:-5432} +POSTGRES_DB=${POSTGRES_DB:-nofx} +POSTGRES_USER=${POSTGRES_USER:-nofx} +POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-} +POSTGRES_SERVICE=${POSTGRES_SERVICE:-postgres} +POSTGRES_CONTAINER_NAME=${POSTGRES_CONTAINER_NAME:-nofx-postgres} + +# 查找 PostgreSQL 容器 +POSTGRES_CONTAINER=$($DOCKER_COMPOSE_CMD ps -q "$POSTGRES_SERVICE" 2>/dev/null || true) +if [ -z "$POSTGRES_CONTAINER" ]; then + POSTGRES_CONTAINER=$(docker ps -q --filter "name=$POSTGRES_CONTAINER_NAME" | head -n 1) +fi + +if [ -z "$POSTGRES_CONTAINER" ]; then + echo "❌ 未找到 PostgreSQL 容器 (${POSTGRES_SERVICE}/${POSTGRES_CONTAINER_NAME})" + echo "💡 请先启动数据库容器: $DOCKER_COMPOSE_CMD up -d postgres" + exit 1 +fi + +PG_ENV_ARGS=() +if [ -n "$POSTGRES_PASSWORD" ]; then + PG_ENV_ARGS=(-e "PGPASSWORD=$POSTGRES_PASSWORD") +fi + +echo "🔌 检查数据库连接..." +if ! docker exec "${PG_ENV_ARGS[@]}" "$POSTGRES_CONTAINER" pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" > /dev/null 2>&1; then + echo "❌ 无法连接到 PostgreSQL,请确认容器和凭据" + exit 1 +fi + +echo +read -p "确认写入默认账号和基础配置? (y/N): " confirm +if [[ $confirm != [yY] ]]; then + echo "ℹ️ 已取消操作" + exit 0 +fi + +echo "🚀 执行初始化 SQL..." +if docker exec -i "${PG_ENV_ARGS[@]}" "$POSTGRES_CONTAINER" \ + psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" <<'SQL' +-- 确保 traders 表存在 custom_coins 字段 +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'traders' AND column_name = 'custom_coins' + ) THEN + ALTER TABLE traders ADD COLUMN custom_coins TEXT DEFAULT ''; + END IF; +END +$$; + +-- 创建 default 用户 +INSERT INTO users (id, email, password_hash, otp_secret, otp_verified, created_at, updated_at) +VALUES ('default', 'default@localhost', '', '', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) +ON CONFLICT (id) DO UPDATE + SET email = EXCLUDED.email, + updated_at = CURRENT_TIMESTAMP; + +-- 默认 AI 模型配置 +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, '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), +('qwen', 'default', 'Qwen', 'qwen', false, '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) +ON CONFLICT (id) DO UPDATE + SET user_id = EXCLUDED.user_id, + name = EXCLUDED.name, + provider = EXCLUDED.provider, + enabled = EXCLUDED.enabled, + api_key = EXCLUDED.api_key, + custom_api_url = EXCLUDED.custom_api_url, + custom_model_name = EXCLUDED.custom_model_name, + updated_at = CURRENT_TIMESTAMP; + +-- 默认交易所配置 +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, '', '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), +('hyperliquid', 'default', 'Hyperliquid', 'hyperliquid', false, '', '', false, '', '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), +('aster', 'default', 'Aster DEX', 'aster', false, '', '', false, '', '', '', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) +ON CONFLICT (id, user_id) DO UPDATE + SET name = EXCLUDED.name, + type = EXCLUDED.type, + enabled = EXCLUDED.enabled, + api_key = EXCLUDED.api_key, + secret_key = EXCLUDED.secret_key, + testnet = EXCLUDED.testnet, + hyperliquid_wallet_addr = EXCLUDED.hyperliquid_wallet_addr, + aster_user = EXCLUDED.aster_user, + aster_signer = EXCLUDED.aster_signer, + aster_private_key = EXCLUDED.aster_private_key, + updated_at = CURRENT_TIMESTAMP; + +-- 默认系统配置(不存在时写入) +INSERT INTO system_config (key, value) VALUES +('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; + +-- 输出校验信息 +SELECT 'default_user' AS item, COUNT(*) AS count FROM users WHERE id = 'default' +UNION ALL +SELECT 'default_ai_models', COUNT(*) FROM ai_models WHERE user_id = 'default' +UNION ALL +SELECT 'default_exchanges', COUNT(*) FROM exchanges WHERE user_id = 'default'; +SQL +then + echo + echo "✅ 默认数据写入完成" +else + echo + echo "❌ 数据写入失败" + exit 1 +fi + +echo "🎉 操作完成" diff --git a/sqlite_backup.sql b/sqlite_backup.sql deleted file mode 100644 index 0abf0ebd..00000000 --- a/sqlite_backup.sql +++ /dev/null @@ -1,207 +0,0 @@ -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; diff --git a/web/src/App.tsx b/web/src/App.tsx index b57081c5..65f5a976 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -12,7 +12,6 @@ import AILearning from './components/AILearning' import { LanguageProvider, useLanguage } from './contexts/LanguageContext' import { AuthProvider, useAuth } from './contexts/AuthContext' import { t, type Language } from './i18n/translations' -import { useSystemConfig } from './hooks/useSystemConfig' import { AlertTriangle } from 'lucide-react' import type { SystemStatus, @@ -42,7 +41,7 @@ function getModelDisplayName(modelId: string): string { function App() { const { language, setLanguage } = useLanguage() const { user, token, logout, isLoading } = useAuth() - const { config: systemConfig, loading: configLoading } = useSystemConfig() + const configLoading = false const [route, setRoute] = useState(window.location.pathname) // 从URL路径读取初始页面状态(支持刷新保持页面) @@ -243,7 +242,6 @@ function App() { onLanguageChange={setLanguage} user={user} onLogout={logout} - isAdminMode={systemConfig?.admin_mode} onPageChange={(page) => { console.log('Competition page onPageChange called with:', page) console.log('Current route:', route, 'Current page:', currentPage) @@ -286,7 +284,7 @@ function App() { } // Show main app for authenticated users on other routes - if (!systemConfig?.admin_mode && (!user || !token)) { + if (!user || !token) { // Default to landing page when not authenticated and no specific route return } @@ -303,7 +301,6 @@ function App() { onLanguageChange={setLanguage} user={user} onLogout={logout} - isAdminMode={systemConfig?.admin_mode} onPageChange={(page) => { console.log('Main app onPageChange called with:', page) diff --git a/web/src/components/landing/AboutSection.tsx b/web/src/components/landing/AboutSection.tsx index 9ec4df0a..97b0c21f 100644 --- a/web/src/components/landing/AboutSection.tsx +++ b/web/src/components/landing/AboutSection.tsx @@ -104,8 +104,8 @@ export default function AboutSection({ language }: AboutSectionProps) { lines={[ '$ git clone https://github.com/tinkle-community/nofx.git', '$ cd nofx', - '$ chmod +x start.sh', - '$ ./start.sh start --build', + '$ chmod +x scripts/start.sh', + '$ ./scripts/start.sh start --build', t('startupMessages1', language), t('startupMessages2', language), t('startupMessages3', language), diff --git a/web/src/components/landing/HeaderBar.tsx b/web/src/components/landing/HeaderBar.tsx index 37f2ae8b..665b0fd3 100644 --- a/web/src/components/landing/HeaderBar.tsx +++ b/web/src/components/landing/HeaderBar.tsx @@ -12,7 +12,6 @@ interface HeaderBarProps { onLanguageChange?: (lang: Language) => void user?: { email: string } | null onLogout?: () => void - isAdminMode?: boolean onPageChange?: (page: string) => void } @@ -24,7 +23,6 @@ export default function HeaderBar({ onLanguageChange, user, onLogout, - isAdminMode = false, onPageChange, }: HeaderBarProps) { const [mobileMenuOpen, setMobileMenuOpen] = useState(false) @@ -363,7 +361,7 @@ export default function HeaderBar({ {user.email} - {!isAdminMode && onLogout && ( + {onLogout && (